Skip to content

Commit

Permalink
Merge pull request #2401 from mbabker/decouple-view-from-sensio
Browse files Browse the repository at this point in the history
Conditionally stop extending from the SensioFrameworkExtraBundle's Template annotation class
  • Loading branch information
goetas authored Apr 3, 2024
2 parents e01be81 + 0f267a3 commit 3fafc1e
Show file tree
Hide file tree
Showing 12 changed files with 276 additions and 46 deletions.
64 changes: 62 additions & 2 deletions Controller/Annotations/View.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,65 @@

use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template;

if (class_exists(Template::class)) {
/**
* Compat class for applications where SensioFrameworkExtraBundle is installed, to be removed when compatibility with the bundle is no longer provided.
*
* @internal
*/
abstract class CompatView extends Template
{
}
} else {
/**
* Compat class for applications where SensioFrameworkExtraBundle is not installed.
*
* @internal
*/
abstract class CompatView
{
/**
* The controller (+action) this annotation is set to.
*
* @var array
*
* @note This property is declared within this compat class to not conflict with the {@see Template::$owner}
* property when SensioFrameworkExtraBundle is present.
*/
protected $owner = [];

/**
* @note This method is declared within this compat class to not conflict with the {@see Template::setOwner}
* method when SensioFrameworkExtraBundle is present.
*/
public function setOwner(array $owner)
{
$this->owner = $owner;
}

/**
* The controller (+action) this annotation is attached to.
*
* @return array
*
* @note This method is declared within this compat class to not conflict with the {@see Template::getOwner}
* method when SensioFrameworkExtraBundle is present.
*/
public function getOwner()
{
return $this->owner;
}
}
}

/**
* View annotation class.
*
* @Annotation
* @Target({"METHOD","CLASS"})
*/
#[\Attribute(\Attribute::TARGET_CLASS | \Attribute::TARGET_METHOD)]
class View extends Template
class View extends CompatView
{
/**
* @var int|null
Expand Down Expand Up @@ -49,12 +100,21 @@ public function __construct(
array $serializerGroups = [],
bool $serializerEnableMaxDepthChecks = false
) {
parent::__construct($data, $vars, $isStreamable, $owner);
if ($this instanceof Template) {
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);

parent::__construct($data, $vars, $isStreamable, $owner);
} elseif ([] !== $vars) {
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);
}

$values = is_array($data) ? $data : [];
$this->statusCode = $values['statusCode'] ?? $statusCode;
$this->serializerGroups = $values['serializerGroups'] ?? $serializerGroups;
$this->serializerEnableMaxDepthChecks = $values['serializerEnableMaxDepthChecks'] ?? $serializerEnableMaxDepthChecks;

// Use the setter to initialize the owner; when extending the Template class, the property will be private
$this->setOwner($values['owner'] ?? $owner);
}

/**
Expand Down
131 changes: 123 additions & 8 deletions EventListener/ViewResponseListener.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,31 +11,112 @@

namespace FOS\RestBundle\EventListener;

use Doctrine\Common\Annotations\Reader;
use Doctrine\Persistence\Proxy;
use FOS\RestBundle\Controller\Annotations\View as ViewAnnotation;
use FOS\RestBundle\FOSRestBundle;
use FOS\RestBundle\View\View;
use FOS\RestBundle\View\ViewHandlerInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Event\ControllerEvent;
use Symfony\Component\HttpKernel\Event\ViewEvent;
use Symfony\Component\HttpKernel\KernelEvents;

/**
* The ViewResponseListener class handles the View core event as well as the "@extra:Template" annotation.
* The ViewResponseListener class handles the kernel.view event and creates a {@see Response} for a {@see View} provided by the controller result.
*
* @author Lukas Kahwe Smith <[email protected]>
*
* @internal
*/
class ViewResponseListener implements EventSubscriberInterface
{
/**
* @var ViewHandlerInterface
*/
private $viewHandler;

private $forceView;

public function __construct(ViewHandlerInterface $viewHandler, bool $forceView)
/**
* @var Reader|null
*/
private $annotationReader;

public function __construct(ViewHandlerInterface $viewHandler, bool $forceView, ?Reader $annotationReader = null)
{
$this->viewHandler = $viewHandler;
$this->forceView = $forceView;
$this->annotationReader = $annotationReader;
}

/**
* Extracts configuration for a {@see ViewAnnotation} from the controller if present.
*/
public function onKernelController(ControllerEvent $event)
{
$request = $event->getRequest();

if (!$request->attributes->get(FOSRestBundle::ZONE_ATTRIBUTE, true)) {
return;
}

$controller = $event->getController();

if (!\is_array($controller) && method_exists($controller, '__invoke')) {
$controller = [$controller, '__invoke'];
}

if (!\is_array($controller)) {
return;
}

$className = $this->getRealClass(\get_class($controller[0]));
$object = new \ReflectionClass($className);
$method = $object->getMethod($controller[1]);

/** @var ViewAnnotation|null $classConfiguration */
$classConfiguration = null;

/** @var ViewAnnotation|null $methodConfiguration */
$methodConfiguration = null;

if (null !== $this->annotationReader) {
$classConfiguration = $this->getViewConfiguration($this->annotationReader->getClassAnnotations($object));
$methodConfiguration = $this->getViewConfiguration($this->annotationReader->getMethodAnnotations($method));
}

if (80000 <= \PHP_VERSION_ID) {
if (null === $classConfiguration) {
$classAttributes = array_map(
function (\ReflectionAttribute $attribute) {
return $attribute->newInstance();
},
$object->getAttributes(ViewAnnotation::class, \ReflectionAttribute::IS_INSTANCEOF)
);

$classConfiguration = $this->getViewConfiguration($classAttributes);
}

if (null === $methodConfiguration) {
$methodAttributes = array_map(
function (\ReflectionAttribute $attribute) {
return $attribute->newInstance();
},
$method->getAttributes(ViewAnnotation::class, \ReflectionAttribute::IS_INSTANCEOF)
);

$methodConfiguration = $this->getViewConfiguration($methodAttributes);
}
}

// An annotation/attribute on the method takes precedence over the class level
if (null !== $methodConfiguration) {
$request->attributes->set(FOSRestBundle::VIEW_ATTRIBUTE, $methodConfiguration);
} elseif (null !== $classConfiguration) {
$request->attributes->set(FOSRestBundle::VIEW_ATTRIBUTE, $classConfiguration);
}
}

public function onKernelView(ViewEvent $event): void
Expand All @@ -46,7 +127,8 @@ public function onKernelView(ViewEvent $event): void
return;
}

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

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

$response = $this->viewHandler->handle($view, $request);

$event->setResponse($response);
$event->setResponse($this->viewHandler->handle($view, $request));
}

public static function getSubscribedEvents(): array
{
// Must be executed before SensioFrameworkExtraBundle's listener
return [
KernelEvents::VIEW => ['onKernelView', 30],
KernelEvents::CONTROLLER => 'onKernelController',
KernelEvents::VIEW => ['onKernelView', -128],
];
}

/**
* @param object[] $annotations
*/
private function getViewConfiguration(array $annotations): ?ViewAnnotation
{
$viewAnnotation = null;

foreach ($annotations as $annotation) {
if (!$annotation instanceof ViewAnnotation) {
continue;
}

if (null === $viewAnnotation) {
$viewAnnotation = $annotation;
} else {
throw new \LogicException('Multiple "view" annotations are not allowed.');
}
}

return $viewAnnotation;
}

private function getRealClass(string $class): string
{
if (class_exists(Proxy::class)) {
if (false === $pos = strrpos($class, '\\'.Proxy::MARKER.'\\')) {
return $class;
}

return substr($class, $pos + Proxy::MARKER_LENGTH + 2);
}

return $class;
}
}
1 change: 1 addition & 0 deletions FOSRestBundle.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
*/
class FOSRestBundle extends Bundle
{
const VIEW_ATTRIBUTE = '_fos_rest_view';
const ZONE_ATTRIBUTE = '_fos_rest_zone';

/**
Expand Down
1 change: 1 addition & 0 deletions Resources/config/view_response_listener.xml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
<tag name="kernel.event_subscriber" />
<argument type="service" id="fos_rest.view_handler" />
<argument /> <!-- force view -->
<argument type="service" id="annotation_reader" on-invalid="null"/>
</service>

</services>
Expand Down
Loading

0 comments on commit 3fafc1e

Please sign in to comment.