Skip to content

Commit c30574c

Browse files
authored
Merge pull request #28 from php-school/stringify-to-demystify
Add stringify to demysitify exercise
2 parents a626ff5 + 01dd62a commit c30574c

File tree

9 files changed

+328
-1
lines changed

9 files changed

+328
-1
lines changed

app/bootstrap.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
use PhpSchool\PHP8Appreciate\Exercise\TheReturnOfStatic;
3131
use PhpSchool\PHP8Appreciate\Exercise\ThrowAnExpression;
3232
use PhpSchool\PHP8Appreciate\Exercise\UniteTheTypes;
33+
use PhpSchool\PHP8Appreciate\Exercise\StringifyToDemystify;
3334
use PhpSchool\PhpWorkshop\Application;
3435

3536
$app = new Application('PHP8 Appreciate', __DIR__ . '/config.php');
@@ -45,6 +46,7 @@
4546
$app->addExercise(AllMixedUp::class);
4647
$app->addExercise(TheReturnOfStatic::class);
4748
$app->addExercise(ThrowAnExpression::class);
49+
$app->addExercise(StringifyToDemystify::class);
4850

4951
$art = <<<ART
5052
_ __ _

app/config.php

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
use PhpSchool\PHP8Appreciate\Exercise\TheReturnOfStatic;
1212
use PhpSchool\PHP8Appreciate\Exercise\ThrowAnExpression;
1313
use PhpSchool\PHP8Appreciate\Exercise\UniteTheTypes;
14+
use PhpSchool\PHP8Appreciate\Exercise\StringifyToDemystify;
1415
use Psr\Container\ContainerInterface;
1516

1617
use function DI\create;
@@ -52,5 +53,8 @@
5253
},
5354
ThrowAnExpression::class => function (ContainerInterface $c) {
5455
return new ThrowAnExpression($c->get(PhpParser\Parser::class), $c->get(\Faker\Generator::class));
55-
}
56+
},
57+
StringifyToDemystify::class => function (ContainerInterface $c) {
58+
return new StringifyToDemystify($c->get(PhpParser\Parser::class));
59+
},
5660
];
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
`__toString()` a magic class method long-standing in PHP but never truly something you could rely on unless you rolled your own interface. All that has changed with the simple introduction of the `Stringable` interface in PHP 8.
2+
3+
`Stringable` is a simple interface that requires the implementation of `__toString(): string`
4+
5+
----------------------------------------------------------------------
6+
7+
Create a program that reads the JSON body of the request input stream (mimicking an API response). The body will need to be JSON decoded and will be a variation of the example below:
8+
9+
```json
10+
{
11+
"success": false,
12+
"status": "401",
13+
"error": "Unauthorized: Not authorised for this resource"
14+
}
15+
```
16+
17+
You will need to define a new class that implements `Stringable` and it's method requirements. This class should take enough information from the request so that the `__toString` method can return a string like below:
18+
19+
```
20+
Status: 401
21+
Error: Unauthorized: Not authorised for this resource
22+
```
23+
24+
Once constructed pass the object to a `log_failure` function. This function is provided for you and has a signature of `log_failure(\Stringable $error): void`.
25+
26+
Your program may also receive successful payloads, which you should ignore for the purposes of this exercise. These requests will be identifiable by the `success` key in the decoded JSON payload.
27+
28+
### The advantages of the new Stringable interface
29+
30+
* Allows typing of string like objects
31+
* Can be used in conjunction with union types alongside `string`
32+
33+
----------------------------------------------------------------------
34+
## HINTS
35+
36+
To easily read the request body you can use `file_get_contents('php://input')`
37+
38+
For more details look at the docs for...
39+
40+
**Stringable** - [https://www.php.net/manual/en/class.stringable.php]()
41+
42+
Note that while the `Stringable` interface isn't required to pass the type hint check, however, it should be used if not only to show intent.
43+
44+
Oh, and don't forget about the basics for classes and interfaces :)
45+
46+
**class** - [https://www.php.net/manual/en/language.oop5.basic.php]()
47+
**interfaces** - [https://www.php.net/manual/en/language.oop5.interfaces.php]()
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<?php
2+
3+
class FailedResponse implements \Stringable {
4+
5+
public function __construct(private string $status, private string $error)
6+
{
7+
}
8+
9+
public function __toString()
10+
{
11+
return "Status: {$this->status} \nError: {$this->error}";
12+
}
13+
}
14+
15+
$request = json_decode(file_get_contents('php://input'), true);
16+
17+
if (!$request['success']) {
18+
log_failure(new FailedResponse($request['status'], $request['error']));
19+
}

src/Exercise/StringifyToDemystify.php

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PhpSchool\PHP8Appreciate\Exercise;
6+
7+
use GuzzleHttp\Psr7\Request;
8+
use PhpParser\Node\Name;
9+
use PhpParser\Node\Stmt;
10+
use PhpParser\Node\Stmt\Class_;
11+
use PhpParser\Node\Stmt\TryCatch;
12+
use PhpParser\NodeFinder;
13+
use PhpParser\Parser;
14+
use PhpSchool\PhpWorkshop\Check\FunctionRequirementsCheck;
15+
use PhpSchool\PhpWorkshop\CodeInsertion;
16+
use PhpSchool\PhpWorkshop\Exercise\AbstractExercise;
17+
use PhpSchool\PhpWorkshop\Exercise\CgiExercise;
18+
use PhpSchool\PhpWorkshop\Exercise\ExerciseInterface;
19+
use PhpSchool\PhpWorkshop\Exercise\ExerciseType;
20+
use PhpSchool\PhpWorkshop\Exercise\SubmissionPatchable;
21+
use PhpSchool\PhpWorkshop\ExerciseCheck\FunctionRequirementsExerciseCheck;
22+
use PhpSchool\PhpWorkshop\ExerciseCheck\SelfCheck;
23+
use PhpSchool\PhpWorkshop\ExerciseDispatcher;
24+
use PhpSchool\PhpWorkshop\Input\Input;
25+
use PhpSchool\PhpWorkshop\Patch;
26+
use PhpSchool\PhpWorkshop\Result\Failure;
27+
use PhpSchool\PhpWorkshop\Result\ResultInterface;
28+
use PhpSchool\PhpWorkshop\Result\Success;
29+
use Psr\Http\Message\RequestInterface;
30+
use Stringable;
31+
32+
class StringifyToDemystify extends AbstractExercise implements
33+
ExerciseInterface,
34+
CgiExercise,
35+
SubmissionPatchable,
36+
FunctionRequirementsExerciseCheck,
37+
SelfCheck
38+
{
39+
public function __construct(private Parser $parser)
40+
{
41+
}
42+
43+
public function getName(): string
44+
{
45+
return 'Stringify to Demystify';
46+
}
47+
48+
public function getDescription(): string
49+
{
50+
return 'PHP 8\'s Stringable Interface';
51+
}
52+
53+
public function getType(): ExerciseType
54+
{
55+
return ExerciseType::CGI();
56+
}
57+
58+
public function configure(ExerciseDispatcher $dispatcher): void
59+
{
60+
$dispatcher->requireCheck(FunctionRequirementsCheck::class);
61+
}
62+
63+
/**
64+
* @return array<RequestInterface>
65+
*/
66+
public function getRequests(): array
67+
{
68+
return array_map(
69+
fn () => new Request(
70+
'POST',
71+
'https://phpschool.io/api',
72+
[],
73+
json_encode($this->getRandRequest(), JSON_THROW_ON_ERROR)
74+
),
75+
array_fill(0, random_int(3, 6), null)
76+
);
77+
}
78+
79+
/**
80+
* @return array{'success': bool, 'status': int, 'error'?: string, 'body'?: string}
81+
* @throws \Exception
82+
*/
83+
public function getRandRequest(): array
84+
{
85+
return [
86+
[
87+
'success' => true,
88+
'status' => 200,
89+
'body' => ['{fake: "data"}', '{stub: "payload"}'][random_int(0, 1)]
90+
],
91+
[
92+
'success' => false,
93+
'status' => 400,
94+
'error' => ['Bad Request: Incorrect mime type', 'Bad Request: Invalid key "id"'][random_int(0, 1)]
95+
],
96+
[
97+
'success' => false,
98+
'status' => 401,
99+
'error' => ['Unauthorized: Not authorised for this resource', 'Bad API token'][random_int(0, 1)]
100+
],
101+
[
102+
'success' => false,
103+
'status' => 403,
104+
'error' => ['Forbidden: Access forbidden', 'Forbidden: ACL not granted for resource'][random_int(0, 1)]
105+
],
106+
[
107+
'success' => false,
108+
'status' => 500,
109+
'error' => ['Server Error: Contact webmaster', 'Server Error: err_code: 3289327'][random_int(0, 1)]
110+
],
111+
][random_int(0, 4)];
112+
}
113+
114+
public function getPatch(): Patch
115+
{
116+
$code = <<<CODE
117+
function log_failure(\Stringable \$error) {
118+
echo \$error;
119+
}
120+
CODE;
121+
122+
return (new Patch())->withInsertion(new CodeInsertion(CodeInsertion::TYPE_BEFORE, $code));
123+
}
124+
125+
public function getRequiredFunctions(): array
126+
{
127+
return ['log_failure'];
128+
}
129+
130+
public function getBannedFunctions(): array
131+
{
132+
return [];
133+
}
134+
135+
public function check(Input $input): ResultInterface
136+
{
137+
/** @var Stmt[] $statements */
138+
$statements = $this->parser->parse((string) file_get_contents($input->getRequiredArgument('program')));
139+
140+
/** @var Class_|null $classStmt */
141+
$classStmt = (new NodeFinder())->findFirstInstanceOf($statements, Class_::class);
142+
143+
if (!$classStmt) {
144+
return new Failure($this->getName(), 'No class statement could be found');
145+
}
146+
147+
$implements = array_filter($classStmt->implements, fn (Name $name) => $name->toString() === Stringable::class);
148+
149+
if (empty($implements)) {
150+
return new Failure($this->getName(), 'Your class should implement the Stringable interface');
151+
}
152+
153+
return new Success($this->getName());
154+
}
155+
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PhpSchool\PHP8AppreciateTest\Exercise;
6+
7+
use PhpSchool\PHP8Appreciate\Exercise\StringifyToDemystify;
8+
use PhpSchool\PhpWorkshop\Application;
9+
use PhpSchool\PhpWorkshop\Result\Failure;
10+
use PhpSchool\PhpWorkshop\Result\FunctionRequirementsFailure;
11+
use PhpSchool\PhpWorkshop\TestUtils\WorkshopExerciseTest;
12+
13+
class StringifyToDemystifyTest extends WorkshopExerciseTest
14+
{
15+
public function getExerciseClass(): string
16+
{
17+
return StringifyToDemystify::class;
18+
}
19+
20+
public function getApplication(): Application
21+
{
22+
return require __DIR__ . '/../../app/bootstrap.php';
23+
}
24+
25+
public function testWithNoClass(): void
26+
{
27+
$this->runExercise('solution-no-class.php');
28+
29+
$this->assertVerifyWasNotSuccessful();
30+
31+
$this->assertResultsHasFailureAndMatches(
32+
FunctionRequirementsFailure::class,
33+
function (FunctionRequirementsFailure $failure) {
34+
self::assertSame(['log_failure'], $failure->getMissingFunctions());
35+
return true;
36+
}
37+
);
38+
}
39+
40+
public function testWithNotImplementingStringable(): void
41+
{
42+
$this->runExercise('solution-no-stringable.php');
43+
44+
$this->assertVerifyWasNotSuccessful();
45+
46+
$this->assertResultsHasFailure(Failure::class, 'Your class should implement the Stringable interface');
47+
}
48+
49+
public function testSolution(): void
50+
{
51+
$this->runExercise('solution.php');
52+
53+
$this->assertVerifyWasSuccessful();
54+
}
55+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<?php
2+
3+
$request = json_decode(file_get_contents('php://input'), true);
4+
5+
if (!$request['success']) {
6+
echo "Status: {$request['status']} \nError: {$request['error']}";
7+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<?php
2+
3+
class FailedResponse {
4+
5+
public function __construct(private string $status, private string $error)
6+
{
7+
}
8+
9+
public function __toString()
10+
{
11+
return "Status: {$this->status} \nError: {$this->error}";
12+
}
13+
}
14+
15+
$request = json_decode(file_get_contents('php://input'), true);
16+
17+
if (!$request['success']) {
18+
log_failure(new FailedResponse($request['status'], $request['error']));
19+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<?php
2+
3+
class FailedResponse implements \Stringable {
4+
5+
public function __construct(private string $status, private string $error)
6+
{
7+
}
8+
9+
public function __toString()
10+
{
11+
return "Status: {$this->status} \nError: {$this->error}";
12+
}
13+
}
14+
15+
$request = json_decode(file_get_contents('php://input'), true);
16+
17+
if (!$request['success']) {
18+
log_failure(new FailedResponse($request['status'], $request['error']));
19+
}

0 commit comments

Comments
 (0)