Skip to content

Commit af42d10

Browse files
authored
Merge pull request #25 from gyro-project/AfterResponseTaskYield
yield AfterResponseTask will delay code execution to kernel.terminate
2 parents 88a048e + e736e87 commit af42d10

File tree

7 files changed

+136
-2
lines changed

7 files changed

+136
-2
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@ vendor
33
composer.lock
44
.phpunit.result.cache
55
.phpcs-cache
6+
.idea

README.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,24 @@ class DefaultController
221221
}
222222
```
223223

224+
## Execute code after the response was sent
225+
226+
For a simple way to delay work from the controller to Symfony's `kernel.terminate` event
227+
the Gyro's yield applier abstraction handles a `AfterResponseTask` that accepts a closure
228+
to be executed after `Response::send` is called via event subscriber.
229+
230+
```php
231+
public function registerAction($request): RedirectRoute
232+
{
233+
$user = $this->createUser($request);
234+
$this->entityManager->persist($user);
235+
236+
yield new AfterResponseTask(fn () => $this->sendEmail($user));
237+
238+
return new RedirectRoute('home');
239+
}
240+
```
241+
224242
## Inject TokenContext into actions
225243

226244
In Symfony access to security related information is available through the
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
<?php
2+
3+
namespace Gyro\Bundle\MVCBundle\Controller\ResultConverter;
4+
5+
use Gyro\MVC\AfterResponseTask;
6+
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
7+
use Symfony\Component\HttpFoundation\Request;
8+
use Symfony\Component\HttpFoundation\Response;
9+
use Symfony\Component\HttpKernel\Event\TerminateEvent;
10+
11+
class AfterResponseYieldApplier implements ControllerYieldApplier, EventSubscriberInterface
12+
{
13+
/** @var array<callable> */
14+
private array $tasks = [];
15+
16+
/**
17+
* @param mixed $yield
18+
*/
19+
public function supports($yield): bool
20+
{
21+
return $yield instanceof AfterResponseTask;
22+
}
23+
24+
/**
25+
* @param mixed $yield
26+
*/
27+
public function apply($yield, Request $request, Response $response): void
28+
{
29+
assert(is_callable($yield));
30+
31+
$this->tasks[] = $yield;
32+
}
33+
34+
public function onKernelTerminate(TerminateEvent $event): void
35+
{
36+
foreach ($this->tasks as $task) {
37+
$task();
38+
}
39+
}
40+
41+
/**
42+
* @psalm-return array<string, string|array{0: string, 1: int}|list<array{0: string, 1?: int}>>
43+
*/
44+
public static function getSubscribedEvents()
45+
{
46+
return ['kernel.terminate' => 'onKernelTerminate'];
47+
}
48+
}

src/Gyro/Bundle/MVCBundle/Controller/ResultConverter/ArrayToTemplateResponseConverter.php

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ class ArrayToTemplateResponseConverter implements ControllerResultConverter
2121
private $guesser;
2222
private $engine;
2323

24-
public function __construct(Environment $twig, TemplateGuesser $guesser, string $engine)
24+
public function __construct(?Environment $twig, TemplateGuesser $guesser, string $engine)
2525
{
2626
$this->twig = $twig;
2727
$this->guesser = $guesser;
@@ -58,6 +58,10 @@ public function convert($result, Request $request): Response
5858

5959
private function makeResponseFor(string $controller, TemplateView $templateView, string $requestFormat): Response
6060
{
61+
if ($this->twig === null) {
62+
throw new \RuntimeException('Cannot convert to template response without Twig');
63+
}
64+
6165
$viewName = $this->guesser->guessControllerTemplateName(
6266
$controller,
6367
$templateView->getActionTemplateName(),

src/Gyro/Bundle/MVCBundle/Resources/config/services.xml

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
<call method="addConverter">
1616
<argument type="service">
1717
<service class="Gyro\Bundle\MVCBundle\Controller\ResultConverter\ArrayToTemplateResponseConverter">
18-
<argument type="service" id="twig" />
18+
<argument type="service" id="twig" on-invalid="null" />
1919
<argument type="service" id="gyro_mvc.template_guesser" />
2020
<argument>twig</argument>
2121
</service>
@@ -47,6 +47,14 @@
4747
<service class="Gyro\Bundle\MVCBundle\Controller\ResultConverter\FlashYieldApplier" />
4848
</argument>
4949
</call>
50+
51+
<call method="addYieldApplier">
52+
<argument type="service" id="Gyro\Bundle\MVCBundle\Controller\ResultConverter\AfterResponseYieldApplier" />
53+
</call>
54+
</service>
55+
56+
<service id="Gyro\Bundle\MVCBundle\Controller\ResultConverter\AfterResponseYieldApplier">
57+
<tag name="kernel.event_subscriber" />
5058
</service>
5159

5260
<service id="gyro_mvc.template_guesser" class="Gyro\Bundle\MVCBundle\View\SymfonyConventionsTemplateGuesser">

src/Gyro/MVC/AfterResponseTask.php

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<?php
2+
3+
namespace Gyro\MVC;
4+
5+
class AfterResponseTask
6+
{
7+
private $callable;
8+
9+
public function __construct(callable $callable)
10+
{
11+
$this->callable = $callable;
12+
}
13+
14+
public function __invoke(): void
15+
{
16+
$callable = $this->callable;
17+
$callable();
18+
}
19+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
<?php
2+
3+
namespace Controller\ResultConverter;
4+
5+
use Gyro\Bundle\MVCBundle\Controller\ResultConverter\AfterResponseYieldApplier;
6+
use Gyro\MVC\AfterResponseTask;
7+
use PHPUnit\Framework\TestCase;
8+
use Symfony\Component\HttpFoundation\Request;
9+
use Symfony\Component\HttpFoundation\Response;
10+
use Symfony\Component\HttpKernel\Event\TerminateEvent;
11+
use Symfony\Component\HttpKernel\HttpKernelInterface;
12+
13+
class AfterResponseTaskYieldApplierTest extends TestCase
14+
{
15+
private $taskExecuted = false;
16+
17+
public function testEndToEnd(): void
18+
{
19+
$request = new Request();
20+
$response = new Response();
21+
$kernel = \Phake::mock(HttpKernelInterface::class);
22+
23+
$yieldApplier = new AfterResponseYieldApplier();
24+
$task = new AfterResponseTask(fn () => $this->taskExecuted = true);
25+
26+
$this->assertTrue($yieldApplier->supports($task));
27+
28+
$yieldApplier->apply($task, $request, $response);
29+
30+
$this->assertFalse($this->taskExecuted);
31+
32+
$yieldApplier->onKernelTerminate(new TerminateEvent($kernel, $request, $response));
33+
34+
$this->assertTrue($this->taskExecuted);
35+
}
36+
}

0 commit comments

Comments
 (0)