Skip to content

[FORM] Prevent forms from extending itself as a parent #24387

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 13 commits into from

Conversation

pierredup
Copy link
Contributor

@pierredup pierredup commented Oct 1, 2017

Q A
Branch? 3.4
Bug fix? no
New feature? yes
BC breaks? no
Deprecations? no
Tests pass? yes
Fixed tickets N/A
License MIT
Doc PR N/A

If a form type defines itself as a parent, it results in an endless recursive loop in the form registry, so throw an exception instead.

@ogizanagi
Copy link
Contributor

Thanks for the PR, but I wouldn't qualify this as a bug. As an enhancement, so a new feature, this must target 4.1, where you'll use the form type FQCN rather than the name btw :)

@ogizanagi ogizanagi added this to the 4.1 milestone Oct 1, 2017
@ogizanagi ogizanagi added Feature and removed Bug labels Oct 1, 2017
@pierredup
Copy link
Contributor Author

pierredup commented Oct 1, 2017

I had this issue on 3.4 with FOSUserBundle when I copy-pasted some code from the docs (https://symfony.com/doc/master/bundles/FOSUserBundle/overriding_forms.html), but my class name was the same as one of the core bundle class names (I changed the class strings to RegistrationFormType::class without adding a use alias, so it resolved to the same class name as my form).
As a result, the page just hanged until it timed out, and there was no information in the logs (with xdebug, you would at least see the max-nesting level error in the logs, but I had xdebug disabled at that point). It took a while to debug and figure out that what was happening. So IMHO I don't think this is a new feature. Instead of ending in an endless recursive loop with no idea what is happening and no information given to the user that they did something wrong, we can exit early with a message that gives some more context about why the page suddenly bombs out.

@ogizanagi
Copy link
Contributor

ogizanagi commented Oct 1, 2017

Sure, I understand that. But that's not an issue within the form component itself, so it cannot be part of a patch release. This PR is a DX improvement. We're used to qualify those as features, and it lands on new minor versions.

@Simperfit
Copy link
Contributor

I agree with @ogizanagi's statement, this is not a bug fix but a DX improvement. It allows users to better use the component. Maybe it needs a doc entry with a warning when you do that ?

@nicolas-grekas
Copy link
Member

nicolas-grekas commented Oct 1, 2017

While this changes a behavior, I think the feature freeze period is fine for this kind of changes, where no new feature is really introduced (no new public anything).
Saving debugging time is of primary importance to me.

@nicolas-grekas
Copy link
Member

Preventing an infinite loop also could legitimately qualify as a bug fix to me, so it's not obvious it must go on 4.1.

@ogizanagi
Copy link
Contributor

So talking a bit with @nicolas-grekas, and as it does not only enhance DX but prevents an infinite loop, I agree this must be qualified as a bug fix.

@ogizanagi ogizanagi modified the milestones: 4.1, 2.7 Oct 1, 2017
@ogizanagi ogizanagi added Bug and removed Feature labels Oct 1, 2017

public function getParent()
{
return new FormWithSameParentTypeAsObject();
Copy link
Member

Choose a reason for hiding this comment

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

Please, use return new self(); to make fabbot happy again ;)

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 did consider changing this initially when I saw the failure, but I'm just concerned it would make the test less intuitive

Copy link
Contributor

Choose a reason for hiding this comment

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

Why would it look less intuitive to you? Would be even more explicit to me :)

Could you do the change?

Copy link
Contributor

@ogizanagi ogizanagi left a comment

Choose a reason for hiding this comment

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

except Fabbot must be happy

@stof
Copy link
Member

stof commented Oct 2, 2017

What about more complex infinite loops ? A -> B -> A instead of just A -> A ?

static $checkedTypes = array();

$searchKey = array_search($type->getName(), $checkedTypes);
$checkedTypes[] = $type->getName();
Copy link
Member

Choose a reason for hiding this comment

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

would putting the name as key work instead?

Copy link
Contributor Author

@pierredup pierredup Oct 8, 2017

Choose a reason for hiding this comment

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

If the name is the key, then you won't get the proper dependency path when throwing an error,
E.G if you have the dependencies foo -> bar -> foo then you can only have the values foo > bar in the array that is displayed in the message, where we want to show foo > bar > foo (So we want to be able to have duplicate values in the array)

Copy link
Member

@nicolas-grekas nicolas-grekas Oct 8, 2017

Choose a reason for hiding this comment

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

Is it possible to have duplicate values and not have a circular ref? If not, then shouldn't this be just the terminal condition? (ie the last > foo comes from the current call, not from the checkedTypes stack?)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

You're right, any duplicate value would immediately be the cause of the circular ref. I have updated this

@@ -97,13 +98,33 @@ public function getType($name)
*/
private function resolveAndAddType(FormTypeInterface $type)
{
static $checkedTypes = array();
Copy link
Member

@nicolas-grekas nicolas-grekas Oct 8, 2017

Choose a reason for hiding this comment

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

does this need to be static, or can it be a regular private property?
also, what about replacing the $checkedTypes = array(); reset by a (method-wide) finally?
That'd be more robust I think

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Initially when I had it as a private property, it didn't work and still ended in an infinite loop, but I must have been doing something wrong since it's working fine now :)
Also this still needs to support PHP 5.3, so we can't use finally

$parentType = $type->getParent();

if ($parentType instanceof FormTypeInterface) {
if ($parentType->getName() === $type->getName()) {
$this->checkedTypes = array();
Copy link
Member

Choose a reason for hiding this comment

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

I think we should still use a try/catch emulating finally, so that the code can recover from any unexpected exception

Copy link
Member

@nicolas-grekas nicolas-grekas left a comment

Choose a reason for hiding this comment

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

(with one minor comment)

@nicolas-grekas
Copy link
Member

(please mind fabbot also)

@fabpot
Copy link
Member

fabpot commented Oct 10, 2017

Sorry, but this does not qualify as a bug fix. It's a nice DX improvement for sure, but it does not fix anything (as having an infinite loop is a dev error, nothing wrong with the code). I would merge this in 3.4 instead as a new feature (which by the way is we added some similar circular refs checks in the past).

@pierredup pierredup changed the base branch from 2.7 to 3.4 October 10, 2017 08:27
@pierredup
Copy link
Contributor Author

Rebased against 3.4

@nicolas-grekas nicolas-grekas modified the milestones: 2.7, 3.4 Oct 10, 2017
unset($this->checkedTypes[$fqcn]);
}
} finally {
$this->checkedTypes = array();
Copy link
Member

Choose a reason for hiding this comment

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

I think this is buggy: there should be only one try/finally, it should start right after $this->checkedTypes[$fqcn] = true;, and should only unset($this->checkedTypes[$fqcn]); (doing $this->checkedTypes = array() could be possible, but only on exceptions - but is not required if we unset properly)

$this->checkedTypes[$fqcn] = true;

try {
if ($parentType === $fqcn) {
Copy link
Member

@nicolas-grekas nicolas-grekas Oct 10, 2017

Choose a reason for hiding this comment

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

one last thing sorry: this "if" can be moved before L117, isn't it?

Copy link
Contributor Author

@pierredup pierredup Oct 10, 2017

Choose a reason for hiding this comment

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

I'm wondering if this is even necessary anymore? Without it, you would still get the circular ref error. Or is it better to be explicit in checking the same type as the parent?

Copy link
Member

Choose a reason for hiding this comment

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

let's remove it then you're right

@nicolas-grekas
Copy link
Member

Thank you @pierredup.

nicolas-grekas added a commit that referenced this pull request Oct 10, 2017
… (pierredup)

This PR was squashed before being merged into the 3.4 branch (closes #24387).

Discussion
----------

[FORM] Prevent forms from extending itself as a parent

| Q             | A
| ------------- | ---
| Branch?       | 3.4
| Bug fix?      | no
| New feature?  | yes
| BC breaks?    | no
| Deprecations? | no
| Tests pass?   | yes
| Fixed tickets | N/A
| License       | MIT
| Doc PR        | N/A

If a form type defines itself as a parent, it results in an endless recursive loop in the form registry, so throw an exception instead.

Commits
-------

2ee6fd5 [FORM] Prevent forms from extending itself as a parent
$typeExtensions = array_merge(
if (isset($this->checkedTypes[$fqcn])) {
$types = implode(' > ', array_merge(array_keys($this->checkedTypes), array($fqcn)));
throw new LogicException(sprintf('Circular reference detected for form "%s" (%s).', $fqcn, $types));
Copy link
Member

Choose a reason for hiding this comment

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

it is detected for a form type, not for a form.

stof added a commit that referenced this pull request Oct 10, 2017
…ies check (pierredup)

This PR was squashed before being merged into the 3.4 branch (closes #24504).

Discussion
----------

[Form] Fix error message in circular reference dependencies check

| Q             | A
| ------------- | ---
| Branch?       | 3.4
| Bug fix?      | yes
| New feature?  | no
| BC breaks?    | no
| Deprecations? | no
| Tests pass?   | no
| Fixed tickets |
| License       | MIT
| Doc PR        |

Fix comments from PR #24387

Commits
-------

48eb43d [Form] Fix error message in circular reference dependencies check
This was referenced Oct 18, 2017
@pierredup pierredup deleted the form-parents branch August 28, 2018 11:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

8 participants