Skip to content

Commit 3fafc1e

Browse files
authored
Merge pull request #2401 from mbabker/decouple-view-from-sensio
Conditionally stop extending from the SensioFrameworkExtraBundle's Template annotation class
2 parents e01be81 + 0f267a3 commit 3fafc1e

12 files changed

+276
-46
lines changed

Controller/Annotations/View.php

Lines changed: 62 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,65 @@
1313

1414
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template;
1515

16+
if (class_exists(Template::class)) {
17+
/**
18+
* Compat class for applications where SensioFrameworkExtraBundle is installed, to be removed when compatibility with the bundle is no longer provided.
19+
*
20+
* @internal
21+
*/
22+
abstract class CompatView extends Template
23+
{
24+
}
25+
} else {
26+
/**
27+
* Compat class for applications where SensioFrameworkExtraBundle is not installed.
28+
*
29+
* @internal
30+
*/
31+
abstract class CompatView
32+
{
33+
/**
34+
* The controller (+action) this annotation is set to.
35+
*
36+
* @var array
37+
*
38+
* @note This property is declared within this compat class to not conflict with the {@see Template::$owner}
39+
* property when SensioFrameworkExtraBundle is present.
40+
*/
41+
protected $owner = [];
42+
43+
/**
44+
* @note This method is declared within this compat class to not conflict with the {@see Template::setOwner}
45+
* method when SensioFrameworkExtraBundle is present.
46+
*/
47+
public function setOwner(array $owner)
48+
{
49+
$this->owner = $owner;
50+
}
51+
52+
/**
53+
* The controller (+action) this annotation is attached to.
54+
*
55+
* @return array
56+
*
57+
* @note This method is declared within this compat class to not conflict with the {@see Template::getOwner}
58+
* method when SensioFrameworkExtraBundle is present.
59+
*/
60+
public function getOwner()
61+
{
62+
return $this->owner;
63+
}
64+
}
65+
}
66+
1667
/**
1768
* View annotation class.
1869
*
1970
* @Annotation
2071
* @Target({"METHOD","CLASS"})
2172
*/
2273
#[\Attribute(\Attribute::TARGET_CLASS | \Attribute::TARGET_METHOD)]
23-
class View extends Template
74+
class View extends CompatView
2475
{
2576
/**
2677
* @var int|null
@@ -49,12 +100,21 @@ public function __construct(
49100
array $serializerGroups = [],
50101
bool $serializerEnableMaxDepthChecks = false
51102
) {
52-
parent::__construct($data, $vars, $isStreamable, $owner);
103+
if ($this instanceof Template) {
104+
trigger_deprecation('friendsofsymfony/rest-bundle', '3.7', 'Extending from "%s" in "%s" is deprecated, the $vars and $isStreamable constructor arguments will not be supported when "sensio/framework-extra-bundle" is not installed and will be removed completely in 4.0.', Template::class, static::class);
105+
106+
parent::__construct($data, $vars, $isStreamable, $owner);
107+
} elseif ([] !== $vars) {
108+
trigger_deprecation('friendsofsymfony/rest-bundle', '3.7', 'Extending from "%s" in "%s" is deprecated and "sensio/framework-extra-bundle" is not installed, the $vars and $isStreamable constructor arguments will be ignored and removed completely in 4.0.', Template::class, static::class);
109+
}
53110

54111
$values = is_array($data) ? $data : [];
55112
$this->statusCode = $values['statusCode'] ?? $statusCode;
56113
$this->serializerGroups = $values['serializerGroups'] ?? $serializerGroups;
57114
$this->serializerEnableMaxDepthChecks = $values['serializerEnableMaxDepthChecks'] ?? $serializerEnableMaxDepthChecks;
115+
116+
// Use the setter to initialize the owner; when extending the Template class, the property will be private
117+
$this->setOwner($values['owner'] ?? $owner);
58118
}
59119

60120
/**

EventListener/ViewResponseListener.php

Lines changed: 123 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,31 +11,112 @@
1111

1212
namespace FOS\RestBundle\EventListener;
1313

14+
use Doctrine\Common\Annotations\Reader;
15+
use Doctrine\Persistence\Proxy;
1416
use FOS\RestBundle\Controller\Annotations\View as ViewAnnotation;
1517
use FOS\RestBundle\FOSRestBundle;
1618
use FOS\RestBundle\View\View;
1719
use FOS\RestBundle\View\ViewHandlerInterface;
1820
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
1921
use Symfony\Component\HttpFoundation\Response;
22+
use Symfony\Component\HttpKernel\Event\ControllerEvent;
2023
use Symfony\Component\HttpKernel\Event\ViewEvent;
2124
use Symfony\Component\HttpKernel\KernelEvents;
2225

2326
/**
24-
* The ViewResponseListener class handles the View core event as well as the "@extra:Template" annotation.
27+
* The ViewResponseListener class handles the kernel.view event and creates a {@see Response} for a {@see View} provided by the controller result.
2528
*
2629
* @author Lukas Kahwe Smith <smith@pooteeweet.org>
2730
*
2831
* @internal
2932
*/
3033
class ViewResponseListener implements EventSubscriberInterface
3134
{
35+
/**
36+
* @var ViewHandlerInterface
37+
*/
3238
private $viewHandler;
39+
3340
private $forceView;
3441

35-
public function __construct(ViewHandlerInterface $viewHandler, bool $forceView)
42+
/**
43+
* @var Reader|null
44+
*/
45+
private $annotationReader;
46+
47+
public function __construct(ViewHandlerInterface $viewHandler, bool $forceView, ?Reader $annotationReader = null)
3648
{
3749
$this->viewHandler = $viewHandler;
3850
$this->forceView = $forceView;
51+
$this->annotationReader = $annotationReader;
52+
}
53+
54+
/**
55+
* Extracts configuration for a {@see ViewAnnotation} from the controller if present.
56+
*/
57+
public function onKernelController(ControllerEvent $event)
58+
{
59+
$request = $event->getRequest();
60+
61+
if (!$request->attributes->get(FOSRestBundle::ZONE_ATTRIBUTE, true)) {
62+
return;
63+
}
64+
65+
$controller = $event->getController();
66+
67+
if (!\is_array($controller) && method_exists($controller, '__invoke')) {
68+
$controller = [$controller, '__invoke'];
69+
}
70+
71+
if (!\is_array($controller)) {
72+
return;
73+
}
74+
75+
$className = $this->getRealClass(\get_class($controller[0]));
76+
$object = new \ReflectionClass($className);
77+
$method = $object->getMethod($controller[1]);
78+
79+
/** @var ViewAnnotation|null $classConfiguration */
80+
$classConfiguration = null;
81+
82+
/** @var ViewAnnotation|null $methodConfiguration */
83+
$methodConfiguration = null;
84+
85+
if (null !== $this->annotationReader) {
86+
$classConfiguration = $this->getViewConfiguration($this->annotationReader->getClassAnnotations($object));
87+
$methodConfiguration = $this->getViewConfiguration($this->annotationReader->getMethodAnnotations($method));
88+
}
89+
90+
if (80000 <= \PHP_VERSION_ID) {
91+
if (null === $classConfiguration) {
92+
$classAttributes = array_map(
93+
function (\ReflectionAttribute $attribute) {
94+
return $attribute->newInstance();
95+
},
96+
$object->getAttributes(ViewAnnotation::class, \ReflectionAttribute::IS_INSTANCEOF)
97+
);
98+
99+
$classConfiguration = $this->getViewConfiguration($classAttributes);
100+
}
101+
102+
if (null === $methodConfiguration) {
103+
$methodAttributes = array_map(
104+
function (\ReflectionAttribute $attribute) {
105+
return $attribute->newInstance();
106+
},
107+
$method->getAttributes(ViewAnnotation::class, \ReflectionAttribute::IS_INSTANCEOF)
108+
);
109+
110+
$methodConfiguration = $this->getViewConfiguration($methodAttributes);
111+
}
112+
}
113+
114+
// An annotation/attribute on the method takes precedence over the class level
115+
if (null !== $methodConfiguration) {
116+
$request->attributes->set(FOSRestBundle::VIEW_ATTRIBUTE, $methodConfiguration);
117+
} elseif (null !== $classConfiguration) {
118+
$request->attributes->set(FOSRestBundle::VIEW_ATTRIBUTE, $classConfiguration);
119+
}
39120
}
40121

41122
public function onKernelView(ViewEvent $event): void
@@ -46,7 +127,8 @@ public function onKernelView(ViewEvent $event): void
46127
return;
47128
}
48129

49-
$configuration = $request->attributes->get('_template');
130+
/** @var ViewAnnotation|null $configuration */
131+
$configuration = $request->attributes->get(FOSRestBundle::VIEW_ATTRIBUTE);
50132

51133
$view = $event->getControllerResult();
52134
if (!$view instanceof View) {
@@ -81,16 +163,49 @@ public function onKernelView(ViewEvent $event): void
81163
$view->setFormat($request->getRequestFormat());
82164
}
83165

84-
$response = $this->viewHandler->handle($view, $request);
85-
86-
$event->setResponse($response);
166+
$event->setResponse($this->viewHandler->handle($view, $request));
87167
}
88168

89169
public static function getSubscribedEvents(): array
90170
{
91-
// Must be executed before SensioFrameworkExtraBundle's listener
92171
return [
93-
KernelEvents::VIEW => ['onKernelView', 30],
172+
KernelEvents::CONTROLLER => 'onKernelController',
173+
KernelEvents::VIEW => ['onKernelView', -128],
94174
];
95175
}
176+
177+
/**
178+
* @param object[] $annotations
179+
*/
180+
private function getViewConfiguration(array $annotations): ?ViewAnnotation
181+
{
182+
$viewAnnotation = null;
183+
184+
foreach ($annotations as $annotation) {
185+
if (!$annotation instanceof ViewAnnotation) {
186+
continue;
187+
}
188+
189+
if (null === $viewAnnotation) {
190+
$viewAnnotation = $annotation;
191+
} else {
192+
throw new \LogicException('Multiple "view" annotations are not allowed.');
193+
}
194+
}
195+
196+
return $viewAnnotation;
197+
}
198+
199+
private function getRealClass(string $class): string
200+
{
201+
if (class_exists(Proxy::class)) {
202+
if (false === $pos = strrpos($class, '\\'.Proxy::MARKER.'\\')) {
203+
return $class;
204+
}
205+
206+
return substr($class, $pos + Proxy::MARKER_LENGTH + 2);
207+
}
208+
209+
return $class;
210+
}
96211
}

FOSRestBundle.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
*/
2828
class FOSRestBundle extends Bundle
2929
{
30+
const VIEW_ATTRIBUTE = '_fos_rest_view';
3031
const ZONE_ATTRIBUTE = '_fos_rest_zone';
3132

3233
/**

Resources/config/view_response_listener.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
<tag name="kernel.event_subscriber" />
1111
<argument type="service" id="fos_rest.view_handler" />
1212
<argument /> <!-- force view -->
13+
<argument type="service" id="annotation_reader" on-invalid="null"/>
1314
</service>
1415

1516
</services>

0 commit comments

Comments
 (0)