Problem/Motivation

As per the title, constraints must handle named options in their constructor instead of allowing an array of options to be passed to the base class.

Steps to reproduce

Proposed resolution

Two different approaches, which start the same:

  • All addConstraint() methods are annotated to accept only either NULL, empty array, or an associative array for $options
  • ConstraintManager::create() triggers a deprecation if the options passed are not NULL, empty array, or an associative array. The functionality there that adds the 'value' key to a non-array and non-NULL value is deprecated
  • Follow ups need to be created to change the signatures for the addConstraint() methods in the relevant major branch
  • Need to decide which major (Drupal 12 or 13) support for passing options as an array will be dropped

Option 1: Add ConstraintBase base class to handle named parameters and BC for options

MR 13741

  • Add new abstract ConstraintBase class that can handle either named parameters or the options array being passed
  • Have most constraint classes change from extending the Symfony Constraint class to the new ConstraintBase class
  • Create follow ups to add constructors with property arguments in the relevant major branch
  • Create follow up on what to do with ConstraintBase class once BC support no longer needed
Pros
  • Having a constraint base class in place now can make it easier to extend BC support to D13, if Symfony drops it before then
  • Smaller MR diff now makes it easier to get in earlier, which may be important if BC support will be dropped for D12
  • Can add BC tests by creating test constraint subclasses from the base class
Cons
  • Intent of having named parameters to have typed parameters, which the use of the spread operator in the base class avoids
  • Base class might not have any purpose after BC support is dropped
  • Still have to add constructors to all constraint classes eventually anyway

Option 2: Add constructors to individual constraint classes with named parameters and BC for options

MR 13811

  • Add constructors to relevant constraint classes now. See #18 for an example
  • Create follow up for relevant major branch to drop BC support in all those constructors
Pros
  • Introduces typing to constraint classes as much as possible now
  • Fewer follow up tasks in the future
Cons
  • MR diff is pretty large
  • BC test coverage will likely be limited to a few of the most commonly used constraints

Remaining tasks

  • Decide on which approach to go with
    Going with Option 2 per #28.
  • Decide which Drupal core version to drop BC
  • Finish writing tests
  • Update CR
  • Create follow up issues

User interface changes

Introduced terminology

API changes

Data model changes

Release notes snippet

Issue fork drupal-3555534

Command icon Show commands

Start within a Git clone of the project using the version control instructions.

Or, if you do not have SSH keys set up on git.drupalcode.org:

Comments

longwave created an issue. See original summary.

godotislate’s picture

longwave’s picture

Not sure what to do here. We immediately run into ContextDefinitionInterface::addConstraint() also accepting an array of options:

  /**
   * Adds a validation constraint.
   *
   * @param string $constraint_name
   *   The name of the constraint to add, i.e. its plugin id.
   * @param array|null $options
   *   The constraint options as required by the constraint plugin, or NULL.
   *
   * @return $this
   */
  public function addConstraint($constraint_name, $options = NULL);

and there is also some magic in ConstraintManager::create(), I assume this default $options['value'] key is also deprecated now:

  public function create($name, $options) {
    if (!is_array($options)) {
      // Plugins need an array as configuration, so make sure we have one.
      // The constraint classes support passing the options as part of the
      // 'value' key also.
      $options = isset($options) ? ['value' => $options] : [];
    }
    return $this->createInstance($name, $options);
  }
longwave’s picture

I think we have to deprecate that magic value setter whatever we do, so let's try solving that first.

godotislate’s picture

There's handling for that value setter in the other MR I did on the original issue, similar to what you have so far. One thing I found was the need to be able to know in constraint factory createInstance() that the value setter thing happened in config manager create(). I'll try porting over some of that work piece by piece here and see how much it helps with the deprecations.

godotislate’s picture

Instead of what I said in #6, I tried bringing over the bulk of MR 12291 to see what deprecations it might fix and what tests are breaking.

One major change from there is change the new ConstraintBase class not to pass options to the Symfony Contraint class parent, like this:

public function __construct(...$args) {
    if (!empty($args) && array_is_list($args)) {
      @trigger_error(sprintf('Passing an array of options to configure the "%s" constraint is deprecated in drupal:11.3.0 and will not be supported in drupal:12.0.0. Use named arguments instead. See https://www.drupal.org/node/3522497', get_class($this)), E_USER_DEPRECATED);
      parent::__construct(...$args);
      return;
    }

    $groups = $args['groups'] ?? NULL;
    unset($args['groups']);
    $payload = $args['payload'] ?? NULL;
    unset($args['payload']);

    foreach ($args as $key => $arg) {
      if (property_exists($this, $key)) {
        $this->$key = $arg;
      }
    }
    parent::__construct(NULL, $groups, $payload);
  }

Will investigate the test failures as it seems that might not completely work.

godotislate’s picture

Resolved some of the test issues, but some function test failures still remain.

For the MR removed all the deprecation ignore entries except for "%Since symfony/validator 7.4: Support for passing the choices as the first argument to Symfony\\Component\\Validator\\Constraints\\Choice is deprecated.%", even though this issue is technically about one. They're all closely related, so it might be difficult to address them all completely separately.

After that, still left to do is to double check that changes to certain constraints don't break BC, and adding test coverage.

godotislate’s picture

Wanted to make sure I tracked and resolved any errors related to the changes separate from deprecation warnings, so opened another draft PR 13741 to address actual failures and errors with BC. A bunch of tests are red bc I left out the changes to address the deprecation warnings, which I will add back in later.

godotislate’s picture

Temporarily removed the check on the number of required parameters in a constraint constructor to find which constraint causes errors, and it's ExtensionName.

godotislate changed the visibility of the branch 3555534-deprecate-options to hidden.

godotislate’s picture

Finally got MR 13741 to all green.

Approach is this:

  • All addConstraint() methods are annotated to accept only either NULL, empty array, or an associative array for $options
  • ConstraintManager::create() triggers a deprecation if the options passed are not NULL, empty array, or an associative array. The functionality there that adds the 'value' key to a non-array and non-NULL value is deprecated
  • A new ConstraintBase class is introduced that handles constructor argument juggling, so it can handle either named arguments being passed or, for BC, an $options array being passed. This saves (for now) needing to add a constructor to every Drupal constraint class and doing the argument juggling there
  • A couple specific constructors were also updated for special handling. For example, ComplexDataConstraint has deprecated passing in the options without the 'properties' key

Some future considerations:

  • Follow ups need to be created to change the signatures for the addConstraint() methods, as well as adding constructors with property arguments to every constraint class. One outstanding question is what major this should happen in: it might be a lot of work for contrib/custom modules to be updated by D12, but if D12/Symfony 8 has removed the BC support for the $options array entirely, then D13 might be too far away. Unless for D12, the ConstraintBase class is updated to take over the BC support from the Symfony Constraint class

Other notes:

longwave’s picture

Thanks for working on this, on a first review the changes generally look good.

In two minds whether we want to introduce ConstraintBase here; given Symfony recommends initializing properties in the constructor should we just go down that route? Unsure how many custom constraint plugins there are in contrib/custom code, but I can't imagine it being too many. If we want parity with Symfony to remove $options support and get rid of ConstraintBase eventually, maybe we just don't introduce it in the first place.

godotislate’s picture

My thinking for introducing the new base class is to avoid having to add a constructor that has to do parameter juggling for every core constraint class right now , and then having to remove all that constructor parameter juggling later in the major branch. With the base class, most of the individual constructors only need to be touched once later.

For example, if we did the constructors now, this is is what FileSizeLimitConstrait would look something like:

class FileSizeLimitConstraint extends SymfonyConstraint {

#[HasNamedArguments]
public function __construct(
  int|array|null $fileLimit = 0,
  public int $userLimit = 0,
  ?array $groups = NULL,
  mixed $payload = NULL,
) {
  if (is_array($fileLimit)) {
    $options = $fileLimit;
    parent::__construct($options);
    return;
  }

  parent::_construct(NULL, $groups, $payload);
  $this->fileLimit = $fileLimit ?? 0;
}  

And we'd have to do something similar to that (some easier, some more complicated, depending on how many properties a constraint has and whether the properties can have array values) for every constraint class.

But I'm also of two minds of whether going with the base class is a good idea, and what the future of the base class would be is a good question. Do we get rid of it? (Or maybe we use it to bring constraints more in line with other Drupal plugin types by implementing PluginInspectionInterface? Not sure what that gets us though other than access to the plugin ID and definition. Just throwing out ideas.)

longwave’s picture

Can we do this simpler pattern everywhere? Symfony seems to do this (in some places at least)

public function __construct(
  $options = NULL,
  ?int $fileLimit = 0,
  ?int $userLimit = 0,
  ?array $groups = NULL,
  mixed $payload = NULL,
) {
  parent::__construct($options, $groups, $payload);
  $this->fileLimit = $fileLimit ?? $this->fileLimit;
  $this->userLimit = $userLimit ?? $this->userLimit;
}
godotislate’s picture

#18 looks like it should work for most cases, except weird ones like ComplexDataConstraint, but that is weird regardless of how we want to do it.

Also the edge(?) case if someone does something like ->addConstraint('FileSizeLimit', ['value' => 100]), but maybe we can just document not to do that in the CR.

If we update every constructor, do we want do any BC testing? Do we just pick a random sample? My plan on testing BC with the base class was to create a few test constraint classes that extend the base class and cover the various conditions in ConstraintFactory::createInstance(). As well as perhaps coverage for the few specific classes that have constructor changes.

Just checking before I go update or add 68 constructors...

longwave’s picture

I don't think we need to provide tests for every single case, maybe we can do a sample of the more commonly used constraints?

Worth making this into yet another branch I think so we can compare approaches as well! I know this is a huge amount of work and hassle so thanks for taking it on!

godotislate’s picture

OK, did individual constructors in MR 13811, and tests are finally green. Still need to do tests for both MRs. I also did property promotion for the *message* properties, because it was possible previously to override the message text via the options array, even if not common. There were examples of this happening in core/core test code, so it should still be possible now.

godotislate changed the visibility of the branch 3555534-validator-options-base to hidden.

godotislate’s picture

Issue summary: View changes
Status: Active » Needs review
Issue tags: -Needs issue summary update

I updated the IS with details about the two approaches in the MRs, as well as pros/cons that I think each have.

I'm moving this to NR now to try to get consensus on which approach to take before writing the relevant tests for either approach.

quietone’s picture

Then let's tag it for framework and rm review.

kim.pepper’s picture

godotislate’s picture

One thought I just had about possible growing use of named parameters generally, and not just for constraint classes:
For classes where there's a reasonable expectation for named parameters to be passed to their constructors, the named parameters kind of implicitly become API, resulting in some tension with the documented BC policy. This is especially exacerbated since passing named parameters that don't exist will cause fatal errors.

I wonder if we can make this a bit easier generally by making these constructors variadic, with a ...$other or similar parameter used widely. The parameter doesn't have to correspond to any class properties, or technically be used at all. It's just there to allow named parameters to be passed with backward for forward compatibility. I grant that it seems a little ugly, but maybe worth considering?

longwave’s picture

The individual constructors and HasNamedArguments approach is obviously the way Symfony wants us to go, and I think it's worth the pain of synchronising with them. There is churn for downstream users either way because they can't do nothing, at a minimum they have to swap to the new ConstraintBase class - we will surely want to get rid of this eventually so we might as well make them go through this pain once instead of twice? Core provides a lot of validators (and is adding more with the ongoing config schema work) but I am not sure how widely added they are in contrib or custom code.

Unfortunately the deprecations need updating to 11.4 now.

godotislate’s picture

Sounds good. Will rebase and update MR 13811 and have ready for review hopefully by early next week.

Tagging for IS update based on #28.

Core provides a lot of validators (and is adding more with the ongoing config schema work) but I am not sure how widely added they are in contrib or custom code.

Not sure about contrib, but custom validators have been a regular part of projects I've worked on, especially those having custom entity types or field types. Or situations where the value of one field is constrained by other fields. Maybe we can search "extends Constraint" or "extends SymfonyConstraint" to get an idea of number of contrib constraints.

Either way, since we missed 11.3, the runway between 11.4 and 12.0 might be short, so getting this in early is ideal.

godotislate changed the visibility of the branch 3555534-deprecate-alt to hidden.

godotislate’s picture

Issue summary: View changes
godotislate’s picture

Issue summary: View changes
godotislate’s picture

Status: Needs review » Needs work
Issue tags: -Needs issue summary update

Rebased and updated the deprecation message version. Also Updated the IS.

Still TBD
Add BC tests for a selected/representatve set of constraint
Update CR to include deprecations for addConstraint() method parameters and any other relevant changes

godotislate’s picture

I was thinking about the best way to write the BC tests, and then took a break for holidays. I will revisit as soon as I can, though I certainly welcome anyone else writing some constraint plugin BC tests.

godotislate’s picture

Issue summary: View changes
Status: Needs work » Needs review
Issue tags: -Needs tests +Needs followup

Finally got back to this. Added BC tests, but I decided only to test Drupal constraint plugins whose getDefaultOption() method returns a non-null value. This is so that I could test passing values other than non-associative arrays as $options to ConstraintManager::create(). This simulates passing non-associative array options to all the addConstraint() methods.

For other constraint plugins, passing non-associative array values to addConstraint() should have already been resulting in error to date, so I think testing the constructor changes are of lesser importance. The possible BC issue is if there are contrib/custom projects that implement their own constraint manager or constraint factory, but I think it's fairly low risk?

Will review and update CR tomorrow.

Also tagging "Needs followup" to deprecate implementations of getDefaultOption() and getRequiredOptions() in Drupal constraint plugins, since those methods are deprecated in the Symfony Constraint class. Deprecating those are probably lower priority because with base methods removed in Symfony 8, they'll effectively will never be called.

godotislate’s picture

Updated the CR to include notes about the method signature changes to all the addConstraint() methods and ConstraintManager::create().

Also stubbed these follow ups:
#3569092: Update method signatures for addConstraint() and ConstraintManager::create() and remove HasNamedArguments use
#3569093: [12.x] Remove getDefaultOption() and getRequiredOptions() methods in Drupal Constraint plugin classe
#3569096: Remove BC layer $options argument from Constraint plugin constructors

Cleared the respective issue tags and updated the IS to reflect current state.

Version: 11.x-dev » main

Drupal core is now using the main branch as the primary development branch. New developments and disruptive changes should now be targeted to the main branch.

Read more in the announcement.

longwave’s picture

Thanks again for keeping up with this. Added some questions to the MR but I think this is close, and we should try and get it in ASAP - this is the biggest blocker to the main branch Drupal 12 upgrading to Symfony 8.

The possible BC issue is if there are contrib/custom projects that implement their own constraint manager or constraint factory

Given the complexity of this subsystem, if you are doing this I think you are on your own and we can't promise BC. I don't immediately see a reason why anyone would even try something like this.

deprecate implementations of getDefaultOption() and getRequiredOptions()

TBH I think we could hopefully just delete these implementations in D12 given Symfony itself is removing them from the interface, again if you were doing something with these directly then you are probably doing something non-standard and it's your problem to solve.

godotislate’s picture

I think we could hopefully just delete these implementations in D12 given Symfony itself is removing them from the interface

Agreed. Changed the follow up to be a removal instead of deprecation: [##3569093].

I think I addressed the latest MR comments. I also had to make some changes for new constraints upstream.

godotislate’s picture

For other constraint plugins, passing non-associative array values to addConstraint() should have already been resulting in error to date, so I think testing the constructor changes are of lesser importance. The possible BC issue is if there are contrib/custom projects that implement their own constraint manager or constraint factory, but I think it's fairly low risk?

Thinking about this some more, I wonder if we can just drop the $options BC argument now from the constructors of all these constraint plugins (the ones that return NULL from getDefaultOption()). The only BC break I can think of is if there are any contrib or custom subclasses that implemented their own constructor and called the parent constructor, but in those cases, we're already changing the constructor signature and breaking that already.
Edit: maybe not? since I imagine not many are passing the $payload or $groups argument.

If there's any reason to keep the $options argument, I'm all for it, since I'm not overly eager to update a whole bunch of constructors again right now, but they'll have to be removed eventually, so it might make sense to just do it if we can.

longwave’s picture

Status: Needs review » Reviewed & tested by the community

Symfony removed the $options argument and the options methods at the same time: https://github.com/symfony/validator/commit/1d7117a57add22f42edef20a8219...

Perhaps we should just do the same thing - get this into Drupal 11 then make those changes early in D12?

No further comments on the MR, I think this is ready now.

larowlan’s picture

Status: Reviewed & tested by the community » Needs work

Great work! Just one issue with ComplexDataConstraint and property collision

godotislate’s picture

Status: Needs work » Needs review

Replied to MR comment. I think this isn't a regression, but we can handle in a follow up.

longwave’s picture

Status: Needs review » Reviewed & tested by the community

Thanks for the explanation, I agree with the analysis; fwiw it would have been fine for you to set this straight back to RTBC.

longwave’s picture

Over in #3570077: Update to Symfony 8 I opened a trial MR with Symfony 8, there are some validator unit test failures:

https://git.drupalcode.org/issue/drupal-3570077/-/pipelines/726284/test_...

Haven't looked at the detail yet.

longwave’s picture

#46 is a false positive; we need to remove the BC layer in ConstraintFactory::createInstance() when we upgrade to Symfony 8, as the #[HasNamedArguments] attribute is removed.

godotislate’s picture

Updated #3569096: Remove BC layer $options argument from Constraint plugin constructors to include removing the #[HasNamedArgument] use as well as the BC layer in ConstraintManager/ConstraintFactory.

larowlan’s picture

Status: Reviewed & tested by the community » Needs work
Issue tags: +Needs followup, +Needs reroll
godotislate’s picture

godotislate’s picture

Status: Needs work » Reviewed & tested by the community

Rebased so back to RTBC.

Note that the only conflict was in .deprecation-ignore.txt.

needs-review-queue-bot’s picture

Status: Reviewed & tested by the community » Needs work
StatusFileSize
new27.63 KB

The Needs Review Queue Bot tested this issue. It fails the Drupal core commit checks. Therefore, this issue status is now "Needs work".

This does not mean that the patch necessarily needs to be re-rolled or the MR rebased. Read the Issue Summary, the issue tags and the latest discussion here to determine what needs to be done.

Consult the Drupal Contributor Guide to find step-by-step guides for working with issues.

godotislate’s picture

Status: Needs work » Reviewed & tested by the community
Issue tags: -Needs followup, -Needs reroll +no-needs-review-bot

Shushing the bot. Back to RTBC.

  • larowlan committed b6627d2b on main
    task: #3555534 Since symfony/validator 7.4: Support for evaluating...
larowlan’s picture

Status: Reviewed & tested by the community » Fixed

Committed to main - thanks folks!

Now that this issue is closed, review the contribution record.

As a contributor, attribute any organization that helped you, or if you volunteered your own time.

Maintainers, credit people who helped resolve this issue.

  • larowlan committed 2f828800 on 11.x
    task: #3555534 Since symfony/validator 7.4: Support for evaluating...
larowlan’s picture

Version: main » 11.x-dev

Backported to 11.x and published CR

godotislate’s picture

Now that this is in, the D12 follow up/Symfony 8 blocker is ready for review: #3569092: Update method signatures for addConstraint() and ConstraintManager::create() and remove HasNamedArguments use

#3569096: Remove BC layer $options argument from Constraint plugin constructors is lower priority since Symfony 8 has kept the $options parameter around. It just does nothing.

wim leers’s picture

Canvas validates many things very thoroughly. In doing so, it seems to trigger a higher number of deprecation notices than most projects: here's an example with ~6000 deprecation notices or a >1 MiB large subject string to check the expected deprecations against … which makes preg_match() fail incorrectly 🤯🧟

See: https://git.drupalcode.org/project/canvas/-/merge_requests/280#note_674164.

Work-around: only check unique deprecation notices: https://git.drupalcode.org/project/canvas/-/merge_requests/280#note_674181

Note that this only happens if your core/phpunit.xml does not have SYMFONY_DEPRECATIONS_HELPER configured to either disabled or not at all (i.e. using the default). An ignore file does NOT help.

Note that simply changing https://www.drupal.org/project/gitlab_templates' default of SYMFONY_DEPRECATIONS_HELPER: disabled and instead setting SYMFONY_DEPRECATIONS_HELPER: "ignoreFile=$CI_PROJECT_DIR/$_WEB_ROOT/core/.deprecation-ignore.txt" causes it to fail the same way on GitLab CI: https://git.drupalcode.org/project/canvas/-/jobs/8388370#L1117

Status: Fixed » Closed (fixed)

Automatically closed - issue fixed for 2 weeks with no activity.

ptmkenny’s picture

Just to note, I think that the fix here has a couple issues for 11.4.x, which I have reported here: #3599189: Some deprecation messages are missing E_USER_DEPRECATED