Skip to content

Commit 6da7523

Browse files
authored
Merge pull request #24 from php-school/static-return-type
Static return type
2 parents bcd0812 + 3f2012f commit 6da7523

File tree

13 files changed

+501
-0
lines changed

13 files changed

+501
-0
lines changed

app/bootstrap.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
use PhpSchool\PHP8Appreciate\Exercise\InfiniteDivisions;
2828
use PhpSchool\PHP8Appreciate\Exercise\PhpPromotion;
2929
use PhpSchool\PHP8Appreciate\Exercise\LordOfTheStrings;
30+
use PhpSchool\PHP8Appreciate\Exercise\TheReturnOfStatic;
3031
use PhpSchool\PHP8Appreciate\Exercise\UniteTheTypes;
3132
use PhpSchool\PhpWorkshop\Application;
3233

@@ -41,6 +42,7 @@
4142
$app->addExercise(InfiniteDivisions::class);
4243
$app->addExercise(ASafeSpaceForNulls::class);
4344
$app->addExercise(AllMixedUp::class);
45+
$app->addExercise(TheReturnOfStatic::class);
4446

4547
$art = <<<ART
4648
_ __ _

app/config.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
use PhpSchool\PHP8Appreciate\Exercise\InfiniteDivisions;
99
use PhpSchool\PHP8Appreciate\Exercise\PhpPromotion;
1010
use PhpSchool\PHP8Appreciate\Exercise\LordOfTheStrings;
11+
use PhpSchool\PHP8Appreciate\Exercise\TheReturnOfStatic;
1112
use PhpSchool\PHP8Appreciate\Exercise\UniteTheTypes;
1213
use Psr\Container\ContainerInterface;
1314

@@ -44,5 +45,8 @@
4445
},
4546
AllMixedUp::class => function (ContainerInterface $c) {
4647
return new AllMixedUp($c->get(PhpParser\Parser::class), $c->get(\Faker\Generator::class));
48+
},
49+
TheReturnOfStatic::class => function (ContainerInterface $c) {
50+
return new TheReturnOfStatic($c->get(PhpParser\Parser::class));
4751
}
4852
];
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
<?php
2+
3+
class File
4+
{
5+
private ?string $permissions = null;
6+
public function withPermissions(string $permissions): static
7+
{
8+
$clone = new self();
9+
$clone->permissions = $permissions;
10+
return $clone;
11+
}
12+
}
13+
14+
class Image extends File
15+
{
16+
private ?string $ext = null;
17+
private ?string $crop = null;
18+
19+
public function withExt(string $ext): static
20+
{
21+
$clone = clone $this;
22+
$clone->ext = $ext;
23+
return $clone;
24+
}
25+
26+
public function withCrop(string $crop): static
27+
{
28+
$clone = clone $this;
29+
$clone->crop = $crop;
30+
return $clone;
31+
}
32+
}
33+
$image = (new Image())
34+
->withPermissions('w+')
35+
->withExt('jpeg')
36+
->withCrop('16x9');
37+
38+
var_dump($image);
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
You have been given a piece of code (look for `the-return-of-static.php` in your working directory) which is using static return types.
2+
3+
You will find two classes. `File`, a base class, and `Image` a class extending and adding behavior to `File`. We instantiate `Image`, set some properties using a fluent interface and then dump the object using `var_dump`.
4+
5+
If you run the code using `{appname} run the-return-of-static.php` you will see it is broken.
6+
7+
Locate and fix the issue!
8+
9+
### The advantages of the static return type
10+
11+
* Enforces that an instance of the class the method is called from, is returned.
12+
* Most useful for fluent interfaces and static constructors to ensure an instance of a parent class is not returned.
13+
14+
----------------------------------------------------------------------
15+
## HINTS
16+
17+
(Brief) Documentation on the static return type feature can be found by pointing your browser here:
18+
[https://www.php.net/manual/en/language.types.declarations.php#language.types.declarations.static]()
19+
20+
The static return type enforces methods to return an instance of the class that the method was called from, rather than the one it was defined in.
21+
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
<?php
2+
3+
class File
4+
{
5+
private ?string $permissions = null;
6+
public function withPermissions(string $permissions): static
7+
{
8+
$clone = clone $this;
9+
$clone->permissions = $permissions;
10+
return $clone;
11+
}
12+
}
13+
14+
class Image extends File
15+
{
16+
private ?string $ext = null;
17+
private ?string $crop = null;
18+
19+
public function withExt(string $ext): static
20+
{
21+
$clone = clone $this;
22+
$clone->ext = $ext;
23+
return $clone;
24+
}
25+
26+
public function withCrop(string $crop): static
27+
{
28+
$clone = clone $this;
29+
$clone->crop = $crop;
30+
return $clone;
31+
}
32+
}
33+
$image = (new Image())
34+
->withPermissions('w+')
35+
->withExt('jpeg')
36+
->withCrop('16x9');
37+
38+
var_dump($image);

src/Exercise/TheReturnOfStatic.php

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
<?php
2+
3+
namespace PhpSchool\PHP8Appreciate\Exercise;
4+
5+
use PhpParser\Node;
6+
use PhpParser\Node\Expr\Assign;
7+
use PhpParser\Node\Expr\Clone_;
8+
use PhpParser\Node\Expr\Variable;
9+
use PhpParser\Node\Stmt;
10+
use PhpParser\Node\Stmt\ClassMethod;
11+
use PhpParser\Node\Stmt\Expression;
12+
use PhpParser\Node\Stmt\Function_;
13+
use PhpParser\NodeFinder;
14+
use PhpParser\Parser;
15+
use PhpSchool\PhpWorkshop\Exercise\AbstractExercise;
16+
use PhpSchool\PhpWorkshop\Exercise\CliExercise;
17+
use PhpSchool\PhpWorkshop\Exercise\ExerciseInterface;
18+
use PhpSchool\PhpWorkshop\Exercise\ExerciseType;
19+
use PhpSchool\PhpWorkshop\Exercise\ProvidesInitialCode;
20+
use PhpSchool\PhpWorkshop\ExerciseCheck\SelfCheck;
21+
use PhpSchool\PhpWorkshop\Input\Input;
22+
use PhpSchool\PhpWorkshop\Result\Failure;
23+
use PhpSchool\PhpWorkshop\Result\ResultInterface;
24+
use PhpSchool\PhpWorkshop\Result\Success;
25+
use PhpSchool\PhpWorkshop\Solution\SingleFileSolution;
26+
use PhpSchool\PhpWorkshop\Solution\SolutionInterface;
27+
28+
class TheReturnOfStatic extends AbstractExercise implements
29+
ExerciseInterface,
30+
ProvidesInitialCode,
31+
CliExercise,
32+
SelfCheck
33+
{
34+
public function __construct(private Parser $parser)
35+
{
36+
}
37+
38+
public function getName(): string
39+
{
40+
return 'The Return of Static';
41+
}
42+
43+
public function getDescription(): string
44+
{
45+
return 'PHP 8\'s static return types';
46+
}
47+
48+
public function getInitialCode(): SolutionInterface
49+
{
50+
return SingleFileSolution::fromFile(
51+
__DIR__ . '/../../exercises/the-return-of-static/initial/the-return-of-static.php'
52+
);
53+
}
54+
55+
public function getType(): ExerciseType
56+
{
57+
return ExerciseType::CLI();
58+
}
59+
60+
public function getArgs(): array
61+
{
62+
return [];
63+
}
64+
65+
public function check(Input $input): ResultInterface
66+
{
67+
/** @var array<Stmt> $statements */
68+
$statements = $this->parser->parse((string) file_get_contents($input->getRequiredArgument('program')));
69+
70+
$finder = new NodeFinder();
71+
72+
/** @var Stmt\Class_|null $class */
73+
$class = $finder->findFirst($statements, function (Node $node) {
74+
return $node instanceof Stmt\Class_ && $node->name && $node->name->toString() === 'File';
75+
});
76+
77+
/** @var ClassMethod|null $method */
78+
$method = $finder->findFirst($class ? [$class] : [], function (Node $node) {
79+
return $node instanceof ClassMethod && $node->name->toString() === 'withPermissions';
80+
});
81+
82+
if (!$class || !$method) {
83+
return new Failure($this->getName(), 'The method withPermissions cannot be found');
84+
}
85+
86+
87+
if (!($assign = $this->findAssignOnFirstLine($method))) {
88+
return new Failure($this->getName(), 'The first statement in withPermissions is not an assign');
89+
}
90+
91+
if ($this->isCloneOfThis($assign)) {
92+
return new Success($this->getName());
93+
}
94+
95+
return new Failure($this->getName(), 'The first statement is not a clone of `$this`');
96+
}
97+
98+
private function findAssignOnFirstLine(ClassMethod $method): ?Assign
99+
{
100+
if (!isset($method->stmts[0])) {
101+
return null;
102+
}
103+
104+
if (!$method->stmts[0] instanceof Expression) {
105+
return null;
106+
}
107+
108+
if (!$method->stmts[0]->expr instanceof Assign) {
109+
return null;
110+
}
111+
112+
return $method->stmts[0]->expr;
113+
}
114+
115+
private function isCloneOfThis(Assign $assign): bool
116+
{
117+
if (!$assign->expr instanceof Clone_) {
118+
return false;
119+
}
120+
121+
if (!$assign->expr->expr instanceof Variable) {
122+
return false;
123+
}
124+
125+
return $assign->expr->expr->name === 'this';
126+
}
127+
}
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
<?php
2+
3+
namespace PhpSchool\PHP8AppreciateTest\Exercise;
4+
5+
use PhpSchool\PHP8Appreciate\Exercise\TheReturnOfStatic;
6+
use PhpSchool\PhpWorkshop\Application;
7+
use PhpSchool\PhpWorkshop\Result\Failure;
8+
use PhpSchool\PhpWorkshop\TestUtils\WorkshopExerciseTest;
9+
10+
class TheReturnOfStaticTest extends WorkshopExerciseTest
11+
{
12+
public function getExerciseClass(): string
13+
{
14+
return TheReturnOfStatic::class;
15+
}
16+
17+
public function getApplication(): Application
18+
{
19+
return require __DIR__ . '/../../app/bootstrap.php';
20+
}
21+
22+
public function testFailureWhenNoClassNameFile(): void
23+
{
24+
$this->runExercise('no-file-class.php');
25+
26+
$this->assertVerifyWasNotSuccessful();
27+
28+
$this->assertResultsHasFailure(Failure::class, 'The method withPermissions cannot be found');
29+
}
30+
31+
public function testFailureWhenNoWithPermissionsMethod(): void
32+
{
33+
$this->runExercise('no-with-permissions-method.php');
34+
35+
$this->assertVerifyWasNotSuccessful();
36+
37+
$this->assertResultsHasFailure(Failure::class, 'The method withPermissions cannot be found');
38+
}
39+
40+
public function testFailureWhenFirstStatementIsNotAnAssign(): void
41+
{
42+
$this->runExercise('no-assign-statement.php');
43+
44+
$this->assertVerifyWasNotSuccessful();
45+
46+
$this->assertResultsHasFailure(Failure::class, 'The first statement in withPermissions is not an assign');
47+
}
48+
49+
public function testFailureWhenAssignIsNotClone(): void
50+
{
51+
$this->runExercise('no-clone.php');
52+
53+
$this->assertVerifyWasNotSuccessful();
54+
55+
$this->assertResultsHasFailure(Failure::class, 'The first statement is not a clone of `$this`');
56+
}
57+
58+
public function testFailureWhenAssignIsNotCloneOfThis(): void
59+
{
60+
$this->runExercise('no-clone-this.php');
61+
62+
$this->assertVerifyWasNotSuccessful();
63+
64+
$this->assertResultsHasFailure(Failure::class, 'The first statement is not a clone of `$this`');
65+
}
66+
67+
public function testSuccessfulSolution(): void
68+
{
69+
$this->runExercise('solution-correct.php');
70+
71+
$this->assertVerifyWasSuccessful();
72+
}
73+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
<?php
2+
3+
class File
4+
{
5+
private ?string $permissions = null;
6+
7+
public function withPermissions(string $permissions): static
8+
{
9+
new static;
10+
$clone = clone $this;
11+
$clone->permissions = $permissions;
12+
return $clone;
13+
}
14+
}
15+
16+
class Image extends File
17+
{
18+
private ?string $ext = null;
19+
private ?string $crop = null;
20+
21+
public function withExt(string $ext): static
22+
{
23+
$clone = clone $this;
24+
$clone->ext = $ext;
25+
return $clone;
26+
}
27+
28+
public function withCrop(string $crop): static
29+
{
30+
$clone = clone $this;
31+
$clone->crop = $crop;
32+
return $clone;
33+
}
34+
}
35+
36+
$image = (new Image())->withPermissions('w+')->withExt('jpeg')->withCrop('16x9');
37+
var_dump($image);

0 commit comments

Comments
 (0)