Skip to content

Commit fe2c734

Browse files
committed
Add stringify to demysitify exercise
1 parent a626ff5 commit fe2c734

File tree

10 files changed

+340
-1
lines changed

10 files changed

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