8000 [Form] Support event listeners in buttons by fancyweb · Pull Request #33639 · symfony/symfony · GitHub
[go: up one dir, main page]

Skip to content

[Form] Support event listeners in buttons #33639

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 1 commit into from
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
1 change: 1 addition & 0 deletions UPGRADE-4.4.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ Form
reference date is deprecated.
* Using `int` or `float` as data for the `NumberType` when the `input` option is set to `string` is deprecated.
* Overriding the methods `FormIntegrationTestCase::setUp()`, `TypeTestCase::setUp()` and `TypeTestCase::tearDown()` without the `void` return-type is deprecated.
* The `ButtonBuilder::__construct()` method second argument should be an `EventDispatcherInterface` instance. The existing array `$options` argument has been moved to the third position.

FrameworkBundle
---------------
Expand Down
18 changes: 18 additions & 0 deletions src/Symfony/Component/Form/Button.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@

namespace Symfony\Component\Form;

use Symfony\Component\EventDispatcher\LegacyEventDispatcherProxy;
use Symfony\Component\Form\Event\PostSetDataEvent;
use Symfony\Component\Form\Event\PostSubmitEvent;
use Symfony\Component\Form\Event\PreSubmitEvent;
use Symfony\Component\Form\Exception\AlreadySubmittedException;
use Symfony\Component\Form\Exception\BadMethodCallException;

Expand Down Expand Up @@ -200,6 +204,11 @@ public function getErrors($deep = false, $flatten = true)
*/
public function setData($modelData)
{
$dispatcher = LegacyEventDispatcherProxy::decorate($this->config->getEventDispatcher());
if ($dispatcher->hasListeners(FormEvents::POST_SET_DATA)) {
$dispatcher->dispatch(new PostSetDataEvent($this, $modelData), FormEvents::POST_SET_DATA);
}

// no-op, called during initialization of the form tree
return $this;
}
Expand Down Expand Up @@ -384,8 +393,17 @@ public function submit($submittedData, $clearMissing = true)
throw new AlreadySubmittedException('A form can only be submitted once');
}

$dispatcher = LegacyEventDispatcherProxy::decorate($this->config->getEventDispatcher());
if ($dispatcher->hasListeners(FormEvents::PRE_SUBMIT)) {
$dispatcher->dispatch(new PreSubmitEvent($this, $submittedData), FormEvents::PRE_SUBMIT);
}

$this->submitted = true;

if ($dispatcher->hasListeners(FormEvents::POST_SUBMIT)) {
$dispatcher->dispatch(new PostSubmitEvent($this, $submittedData), FormEvents::POST_SUBMIT);
}

return $this;
}

Expand Down
76 changes: 61 additions & 15 deletions src/Symfony/Component/Form/ButtonBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,10 @@

namespace Symfony\Component\Form;

use Symfony\Component\EventDispatcher\EventDispatcher;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\EventDispatcher\ImmutableEventDispatcher;
use Symfony\Component\Form\Exception\BadMethodCallException;
use Symfony\Component\Form\Exception\InvalidArgumentException;

Expand All @@ -22,6 +25,11 @@
*/
class ButtonBuilder implements \IteratorAggregate, FormBuilderInterface
{
private const UNSUPPORTED_FORM_EVENTS = [
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This also define the supported form events, what about UNSUPPORTED_SUPPORTED_FORM_EVENTS instead?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For me:

FORM_EVENTS= The 5 form events from the FormEvents class.

UNSUPPORTED_FORM_EVENTS = Events from the FormEvents class that are not supported by buttons.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, but self::UNSUPPORTED_FORM_EVENTS[$eventName] = Events from the FormEvents class that are supported by buttons :)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alright, I see what you mean 😄 However, I still think the name applies to the whole const (what it represents) and not to its "values" only.

Copy link
Member
@yceruto yceruto Oct 1, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My suggestion is to find a better name that makes it clearer. I could also name it SUPPORTED_FORM_EVENTS due to its values, so we've the chicken-and-egg situation here :)

Other names could be: FORM_EVENTS_SUPPORT, BUTTON_EVENTS, ... though not sure they're better :)

FormEvents::PRE_SET_DATA => [FormEvents::POST_SET_DATA],
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just don't know if we can prevent userland from using our events?

I chose to not dispatch useless events. However, if userland dispatch them manually, it can still be useful to be able to listen to them. But I guess this would be a weird practice 😕 Personally, I just prefer to forbid them.

FormEvents::SUBMIT => [FormEvents::PRE_SUBMIT, FormEvents::POST_SUBMIT],
];

protected $locked = false;

/**
Expand Down Expand Up @@ -49,16 +57,36 @@ class ButtonBuilder implements \IteratorAggregate, FormBuilderInterface
*/
private $options;

private $dispatcher;

/**
* @throws InvalidArgumentException if the name is empty
*/
public function __construct(?string $name, array $options = [])
public function __construct(?string $name, /*EventDispatcherInterface*/ $dispatcher = null, array $options = [])
{
if (\func_num_args() > 1) {
if (!$dispatcher instanceof EventDispatcherInterface) {
@trigger_error(sprintf('The "%s" method second argument must be an instance of "%s" since Symfony 4.4. The existing array "$options" argument has been moved to the third position.', __METHOD__, EventDispatcherInterface::class), E_USER_DEPRECATED);

if (!\is_array($dispatcher)) {
throw new \TypeError(sprintf('The second argument passed to the "%s" method must be an instance of "%s", "%s" given.', __METHOD__, EventDispatcherInterface::class, \is_object($dispatcher) ? \get_class($dispatcher) : \gettype($dispatcher)));
}

$options = $dispatcher;
$dispatcher = new EventDispatcher();
}
} else {
@trigger_error(sprintf('The "%s" method requires an instance of "%s" as its second argument since Symfony 4.4.', __METHOD__, EventDispatcherInterface::class), E_USER_DEPRECATED);

$dispatcher = new EventDispatcher();
}

if ('' === $name || null === $name) {
throw new InvalidArgumentException('Buttons cannot have empty names.');
}

$this->name = $name;
$this->dispatcher = $dispatcher;
$this->options = $options;

if (preg_match('/^([^a-z0-9_].*)?(.*[^a-zA-Z0-9_\-:].*)?$/D', $name, $matches)) {
Expand Down Expand Up @@ -159,31 +187,45 @@ public function getForm()
}

/**
* Unsupported method.
*
* This method should not be invoked.
*
* @param string $eventName
* @param callable $listener
* @param int $priority
* {@inheritdoc}
*
* @throws BadMethodCallException
*/
public function addEventListener($eventName, $listener, $priority = 0)
{
throw new BadMethodCallException('Buttons do not support event listeners.');
if ($this->locked) {
throw new BadMethodCallException('ButtonBuilder methods cannot be accessed anymore once the builder is turned into a FormConfigInterface instance.');
}

if (isset(self::UNSUPPORTED_FORM_EVENTS[$eventName])) {
throw new BadMethodCallException(sprintf('Buttons do not support the "%s" form event. Use "%s" instead.', $eventName, implode('" or "', self::UNSUPPORTED_FORM_EVENTS[$eventName])));
}

$this->dispatcher->addListener($eventName, $listener, $priority);

return $this;
}

/**
* Unsupported method.
*
* This method should not be invoked.
* {@inheritdoc}
*
* @throws BadMethodCallException
*/
public function addEventSubscriber(EventSubscriberInterface $subscriber)
{
throw new BadMethodCallException('Buttons do not support event subscribers.');
if ($this->locked) {
throw new BadMethodCallException('ButtonBuilder methods cannot be accessed anymore once the builder is turned into a FormConfigInterface instance.');
}

foreach ($subscriber::getSubscribedEvents() as $eventName => $_) {
if (isset(self::UNSUPPORTED_FORM_EVENTS[$eventName])) {
throw new BadMethodCallException(sprintf('Buttons do not support the "%s" form event. Use "%s" instead.', $eventName, implode('" or "', self::UNSUPPORTED_FORM_EVENTS[$eventName])));
}
}

$this->dispatcher->addSubscriber($subscriber);

return $this;
}

/**
Expand Down Expand Up @@ -513,11 +555,15 @@ public function getFormConfig()
}

/**
* Unsupported method.
* {@inheritdoc}
*/
public function getEventDispatcher()
{
return null;
if ($this->locked && !$this->dispatcher instanceof ImmutableEventDispatcher) {
$this->dispatcher = new ImmutableEventDispatcher($this->dispatcher);
}

return $this->dispatcher;
}

/**
Expand Down
2 changes: 2 additions & 0 deletions src/Symfony/Component/Form/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ CHANGELOG
* marked all dispatched event classes as `@final`
* Added the `validate` option to `SubmitType` to toggle the browser built-in form validation.
* Added the `alpha3` option to `LanguageType` and `CountryType` to use alpha3 instead of alpha2 codes
* The `ButtonBuilder::__construct()` method second argument should be an `EventDispatcherInterface` instance. The existing array `$options` argument has been moved to the third position.
* Added support for event listeners on buttons.

4.3.0
-----
Expand Down
4 changes: 2 additions & 2 deletions src/Symfony/Component/Form/ResolvedFormType.php
Original file line number Diff line number Diff line change
Expand Up @@ -206,11 +206,11 @@ public function getOptionsResolver()
protected function newBuilder($name, $dataClass, FormFactoryInterface $factory, array $options)
{
if ($this->innerType instanceof ButtonTypeInterface) {
return new ButtonBuilder($name, $options);
return new ButtonBuilder($name, new EventDispatcher(), $options);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is not point in this support, we never check server side if a button is clicked, only one submit button per request.

}

if ($this->innerType instanceof SubmitButtonTypeInterface) {
return new SubmitButtonBuilder($name, $options);
return new SubmitButtonBuilder($name, new EventDispatcher(), $options);
}

return new FormBuilder($name, $dataClass, new EventDispatcher(), $factory, $options);
Expand Down
111 changes: 108 additions & 3 deletions src/Symfony/Component/Form/Tests/ButtonBuilderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,13 @@
namespace Symfony\Component\Form\Tests;

use PHPUnit\Framework\TestCase;
use Symfony\Component\EventDispatcher\EventDispatcher;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\EventDispatcher\ImmutableEventDispatcher;
use Symfony\Component\Form\ButtonBuilder;
use Symfony\Component\Form\Exception\BadMethodCallException;
use Symfony\Component\Form\Exception\InvalidArgumentException;
use Symfony\Component\Form\FormEvents;

/**
* @author Alexander Cheprasov <cheprasov.84@ya.ru>
Expand All @@ -36,15 +41,15 @@ public function getValidNames()
*/
public function testValidNames($name)
{
$this->assertInstanceOf('\Symfony\Component\Form\ButtonBuilder', new ButtonBuilder($name));
$this->assertInstanceOf('\Symfony\Component\Form\ButtonBuilder', new ButtonBuilder($name, new EventDispatcher()));
}

/**
* @group legacy
*/
public function testNameContainingIllegalCharacters()
{
$this->assertInstanceOf('\Symfony\Component\Form\ButtonBuilder', new ButtonBuilder('button[]'));
$this->assertInstanceOf('\Symfony\Component\Form\ButtonBuilder', new ButtonBuilder('button[]', new EventDispatcher()));
}

/**
Expand All @@ -71,6 +76,106 @@ public function testInvalidNames($name)
{
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('Buttons cannot have empty names.');
new ButtonBuilder($name);
new ButtonBuilder($name, new EventDispatcher());
}

/**
* @dataProvider supportedEventsProvider
*/
public function testSupportedEvents(string $event)
{
$buttonBuilder = new ButtonBuilder('foo', new EventDispatcher());

$buttonBuilder->addEventListener($event, ['class', 'method']);

$subscriber = new class() implements EventSubscriberInterface {
public static $event = 'foo';

/**
* {@inheritdoc}
*/
public static function getSubscribedEvents(): array
{
return [
self::$event => 'method',
];
}
};
$subscriber::$event = $event;
$buttonBuilder->addEventSubscriber($subscriber);

$this->assertSame([
['class', 'method'],
[$subscriber, 'method'],
], $buttonBuilder->getEventDispatcher()->getListeners($event));
}

public function supportedEventsProvider()
{
return [
[FormEvents::POST_SET_DATA],
[FormEvents::PRE_SUBMIT],
[FormEvents::POST_SUBMIT],
];
}

/**
* @dataProvider unsupportedEventsProvider
*/
public function testUnsupportedEventsWithAListener(string $expectedMessage, string $event)
{
$this->expectException(BadMethodCallException::class);
$this->expectExceptionMessage($expectedMessage);

(new ButtonBuilder('foo', new EventDispatcher()))->addEventListener($event, []);
}

/**
* @dataProvider unsupportedEventsProvider
*/
public function testUnsupportedEventsWithASubscriber(string $expectedMessage, string $event)
{
$this->expectException(BadMethodCallException::class);
$this->expectExceptionMessage($expectedMessage);

$subscriber = new class() implements EventSubscriberInterface {
public static $event = 'foo';

/**
* {@inheritdoc}
*/
public static function getSubscribedEvents(): array
{
return [
self::$event => 'method',
];
}
};
$subscriber::$event = $event;

(new ButtonBuilder('foo', new EventDispatcher()))->addEventSubscriber($subscriber);
}

public function unsupportedEventsProvider()
{
return [
['Buttons do not support the "form.pre_set_data" form event. Use "form.post_set_data" instead.', FormEvents::PRE_SET_DATA],
['Buttons do not support the "form.submit" form event. Use "form.pre_submit" or "form.post_submit" instead.', FormEvents::SUBMIT],
];
}

public function testGetEventDispatcher()
{
$eventDispatcher = new EventDispatcher();
$buttonBuilder = new ButtonBuilder('foo', $eventDispatcher);

$this->assertSame($eventDispatcher, $buttonBuilder->getEventDispatcher());

$eventDispatcher->addListener($event = 'foo.bar', $callable = function () {});

$immutableEventDispatcher = $buttonBuilder->getFormConfig()->getEventDispatcher();

$this->assertInstanceOf(ImmutableEventDispatcher::class, $immutableEventDispatcher);
$this->assertSame([$callable], $immutableEventDispatcher->getListeners($event));
}
}
Loading
0