Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions src/Symfony/Component/HttpKernel/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
CHANGELOG
=========

5.2.0
-----

* added @QueryParam annotation to read query string from request

5.1.0
-----

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Controller\ArgumentResolver\DefaultValueResolver;
use Symfony\Component\HttpKernel\Controller\ArgumentResolver\QueryParamValueResolver;
use Symfony\Component\HttpKernel\Controller\ArgumentResolver\RequestAttributeValueResolver;
use Symfony\Component\HttpKernel\Controller\ArgumentResolver\RequestValueResolver;
use Symfony\Component\HttpKernel\Controller\ArgumentResolver\SessionValueResolver;
Expand Down Expand Up @@ -91,6 +92,7 @@ public static function getDefaultArgumentValueResolvers(): iterable
new SessionValueResolver(),
new DefaultValueResolver(),
new VariadicValueResolver(),
new QueryParamValueResolver(),
];
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<?php

namespace Symfony\Component\HttpKernel\Controller\ArgumentResolver;

use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Controller\ArgumentValueResolverInterface;
use Symfony\Component\HttpKernel\Controller\Configuration\ConfigurationInterface;
use Symfony\Component\HttpKernel\Controller\Configuration\QueryParam;
use Symfony\Component\HttpKernel\Controller\ConfigurationList;
use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata;

final class QueryParamValueResolver implements ArgumentValueResolverInterface
{
public function supports(Request $request, ArgumentMetadata $argument)
{
if (!$request->attributes->has('_configurations')) {
return false;
}

/** @var QueryParam $configuration */
$configuration = $this->getConfigurationForArgument($request, $argument);

return null !== $configuration
&& !$argument->isVariadic()
&& ($request->query->has($configuration->getName()) || $argument->hasDefaultValue() || $argument->isNullable());
}

public function resolve(Request $request, ArgumentMetadata $argument)
{
$configuration = $this->getConfigurationForArgument($request, $argument);
$defaultValue = $argument->hasDefaultValue() ? $argument->getDefaultValue() : null;
yield $request->query->get($configuration->getName(), $defaultValue);
}

private function getConfigurationForArgument(Request $request, ArgumentMetadata $argument): ?ConfigurationInterface
{
/** @var ConfigurationList $configurations */
$configurations = $request->attributes->get('_configurations');

$configuration = $configurations->filter(function (ConfigurationInterface $configuration) use ($argument): bool {
return $configuration instanceof QueryParam && $configuration->getArgumentName() === $argument->getName();
});

return $configuration->first();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?php

namespace Symfony\Component\HttpKernel\Controller\Configuration;

abstract class ConfigurationAnnotation implements ConfigurationInterface
{
public function __construct(array $values)
{
foreach ($values as $k => $v) {
if (!method_exists($this, $name = 'set'.$k)) {
throw new \RuntimeException(sprintf('Unknown key "%s" for annotation "@%s".', $k, static::class));
}

$this->$name($v);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?php

namespace Symfony\Component\HttpKernel\Controller\Configuration;

interface ConfigurationInterface
{
public function getUniqueName(): string;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
<?php

namespace Symfony\Component\HttpKernel\Controller\Configuration;

/**
* @Annotation
*/
class QueryParam extends ConfigurationAnnotation
{
private $argumentName;
private $name;

public function __construct(array $values)
{
parent::__construct($values);

if (null === $this->name) {
$this->name = $this->argumentName;
}
}

public function getName()
{
return $this->name;
}

public function setName(string $name): void
{
$this->name = $name;
}

public function setValue(string $value): void
{
$this->setArgumentName($value);
}

public function setArgumentName(string $argumentName): void
{
$this->argumentName = $argumentName;
}

public function getArgumentName()
{
return $this->argumentName;
}

public function getUniqueName(): string
{
return static::class.'.'.$this->argumentName;
}
}
48 changes: 48 additions & 0 deletions src/Symfony/Component/HttpKernel/Controller/ConfigurationList.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
<?php

namespace Symfony\Component\HttpKernel\Controller;

use Symfony\Component\HttpKernel\Controller\Configuration\ConfigurationInterface;

class ConfigurationList implements \Countable, \IteratorAggregate
{
private $configurations = [];

public function __construct(array $configurations = [])
{
foreach ($configurations as $configuration) {
$this->add($configuration);
}
}

public function add(ConfigurationInterface $configuration): self
{
if (isset($this->configurations[$configuration->getUniqueName()])) {
throw new \LogicException(sprintf('Multiples "%s" configurations are not allowed', $configuration->getUniqueName()));
}

$this->configurations[$configuration->getUniqueName()] = $configuration;

return $this;
}

public function filter(callable $filter): self
{
return new static(array_filter($this->configurations, $filter));
}

public function first(): ?ConfigurationInterface
{
return empty($this->configurations) ? null : reset($this->configurations);
}

public function count(): int
{
return \count($this->configurations);
}

public function getIterator()
{
return new \ArrayIterator($this->configurations);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
<?php

namespace Symfony\Component\HttpKernel\EventListener;

use Doctrine\Common\Annotations\Reader;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\Controller\Configuration\ConfigurationInterface;
use Symfony\Component\HttpKernel\Controller\ConfigurationList;
use Symfony\Component\HttpKernel\Event\ControllerEvent;
use Symfony\Component\HttpKernel\KernelEvents;

/**
* The ControllerListener class parses annotation blocks located in
* controller classes and creates a list of configurations to be used
* on others event listeners.
*
* @author Tales Santos <tales.augusto.santos@gmail.com>
*/
class ControllerListener implements EventSubscriberInterface
{
/**
* @var Reader
*/
private $reader;

public function __construct(Reader $reader)
{
$this->reader = $reader;
}

public function onController(ControllerEvent $event): void
{
$controller = $event->getController();

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

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

$reflectionObject = new \ReflectionObject($controller[0]);
$reflectionMethod = $reflectionObject->getMethod($controller[1]);

$configurations = new ConfigurationList();

$this->appendConfigurations($configurations, $this->reader->getClassAnnotations($reflectionObject));
$this->appendConfigurations($configurations, $this->reader->getMethodAnnotations($reflectionMethod));

$event->getRequest()->attributes->set('_configurations', $configurations);
}

private function appendConfigurations(ConfigurationList $configurations, array $annotations): void
{
foreach ($annotations as $annotation) {
if ($annotation instanceof ConfigurationInterface) {
$configurations->add($annotation);
}
}
}

public static function getSubscribedEvents()
{
return [
KernelEvents::CONTROLLER => 'onController',
];
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,10 @@
use Symfony\Component\HttpKernel\Controller\ArgumentResolver;
use Symfony\Component\HttpKernel\Controller\ArgumentResolver\RequestAttributeValueResolver;
use Symfony\Component\HttpKernel\Controller\ArgumentValueResolverInterface;
use Symfony\Component\HttpKernel\Controller\Configuration\QueryParam;
use Symfony\Component\HttpKernel\Controller\ConfigurationList;
use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadataFactory;
use Symfony\Component\HttpKernel\Tests\Fixtures\Controller\AnnotatedController;
use Symfony\Component\HttpKernel\Tests\Fixtures\Controller\ExtendingRequest;
use Symfony\Component\HttpKernel\Tests\Fixtures\Controller\ExtendingSession;
use Symfony\Component\HttpKernel\Tests\Fixtures\Controller\NullableController;
Expand Down Expand Up @@ -280,6 +283,42 @@ public function testGetSessionMissMatchOnNull()
self::$resolver->getArguments($request, $controller);
}

public function testGetQueryParam()
{
$request = Request::create('/?foo=foo&bar=bar');
$request->attributes->set('_configurations', new ConfigurationList([
new QueryParam(['value' => 'foo']),
new QueryParam(['value' => 'bar']),
]));
$controller = [new AnnotatedController(), 'queryParam'];

$this->assertEquals(['foo', 'bar'], self::$resolver->getArguments($request, $controller));
}

public function testGetQueryParamWithDefaultValues()
{
$request = Request::create('/');
$request->attributes->set('_configurations', new ConfigurationList([
new QueryParam(['value' => 'foo']),
new QueryParam(['value' => 'bar']),
]));
$controller = [new AnnotatedController(), 'queryParamWithDefaultValues'];

$this->assertEquals(['foo', 'bar'], self::$resolver->getArguments($request, $controller));
}

public function testGetQueryParamWithNullableValues()
{
$request = Request::create('/?foo=foo');
$request->attributes->set('_configurations', new ConfigurationList([
new QueryParam(['value' => 'foo']),
new QueryParam(['value' => 'bar']),
]));
$controller = [new AnnotatedController(), 'queryParamWithNullableValues'];

$this->assertEquals(['foo', null], self::$resolver->getArguments($request, $controller));
}

public function __invoke($foo, $bar = null)
{
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
<?php

namespace Symfony\Component\HttpKernel\Tests\EventListener;

use Doctrine\Common\Annotations\AnnotationReader;
use PHPUnit\Framework\TestCase;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Event\ControllerEvent;
use Symfony\Component\HttpKernel\EventListener\ControllerListener;
use Symfony\Component\HttpKernel\HttpKernelInterface;
use Symfony\Component\HttpKernel\Tests\Fixtures\Controller\AnnotatedController;
use Symfony\Component\HttpKernel\Tests\Fixtures\KernelForTest;

class ControllerListenerTest extends TestCase
{
private $listener;
private $reader;

protected function setUp(): void
{
$this->reader = new AnnotationReader();
$this->listener = new ControllerListener($this->reader);
}

public function testOnController()
{
$event = $this->createControllerEvent([new AnnotatedController(), 'queryParam']);
$request = $event->getRequest();

$this->listener->onController($event);

$this->assertTrue($request->attributes->has('_configurations'));
$this->assertCount(2, $request->attributes->get('_configurations'));
}

public function testOnControllerWithDuplicatedQueryParam()
{
$this->expectException(\LogicException::class);
$event = $this->createControllerEvent([new AnnotatedController(), 'duplicatedQueryParamConfiguration']);
$this->listener->onController($event);
}

private function createControllerEvent(callable $controller): ControllerEvent
{
$kernel = new KernelForTest('test', true);
$event = new ControllerEvent($kernel, $controller, new Request(), HttpKernelInterface::MASTER_REQUEST);

return $event;
}
}
Loading