When we implemented #2937717: Add support for manipulating a complete 'feeds item'/'row'. we did not solve at the time the issue of how plugins can be aware of the available item when being configured via a UI.

Original report

When configuring a Tamper plugin, there is no Item available. So there is currently no way for Tamper to know which sources are available (the Feeds Tamper's 'rewrite' plugin needs this information). Maybe Tamper plugins needs an extra method to set the available source field names. This problem can be dealt with in a follow up, but I mention it as it is closely related.

One pattern I have seen when investigating contexts is the use of FormStateInterface::setTemporaryValue and FormStateInterface::getTemporaryValue

For example, the BlockForm module calls $form_state->setTemporaryValue('gathered_contexts', $this->contextRepository->getAvailableContexts()); and the condition plugin base calls $contexts = $form_state->getTemporaryValue('gathered_contexts') ?: []; to be able to access the available contexts with the plugins sub form.

We could implement a similar pattern, however we need to make sure this is document / obvious for both plugin developers and modules like feeds tamper that need to supply the available values.

Example:
In feeds tamper UI - TamperFormBase

      // Current code
      $subform_state = SubformState::createForSubform($form[self::VAR_PLUGIN_CONFIGURATION], $form, $form_state);
      // Additional pseudo code
      $available_source_properties = $this->feedsFeedType->someMethodToGetSources();
      $subform_state->setTemporaryValue('available_source_properties', $available_source_properties);
      // Existing code
      $form[self::VAR_PLUGIN_CONFIGURATION] = $this->plugin->buildConfigurationForm($form[self::VAR_PLUGIN_CONFIGURATION], $subform_state);

Inside a plugins form

  $form['source'] = array(
    '#type' => 'radios',
    '#default_value' => $this->getSetting('source');
    '#options' => $form_state->getTemporaryValue('available_source_properties'),
    '#title' => t('Source'),
  );

Comments

ericgsmith created an issue. See original summary.

ericgsmith’s picture

I intend to sit on this for a bit to think about the pros, cons and an alternative.

Thoughts, suggestions and criticisms welcome.

ericgsmith’s picture

Issue summary: View changes
jamesdixon’s picture

Forgive my beginner's attempt at understanding here. :)

Right so if some plugin wants to use tamper to modify sources of an item/row, right now the developer of the plugin would need to write their own custom code or hard code the sources into a UI (if they need a UI). This is because we are not processing items during configuration.

If that's the case I agree setting something like this up would be useful.

$this->feedsFeedType->someMethodToGetSources();

Could be used to grab the sources outside of the actual processing step from external plugins.

These external plugins could use set and get temporary value to store the sources for use outside of processing.

Is this what you mean?

ericgsmith’s picture

Yes, so imagine you want to configure the copy plugin. The copy plugin will allow you to specify if you want to copy the tampered value from or to another item.

So in your feed row / item you might have:
$item->id = 'foo'
$item->name = 'bar'
$item->zip = 'zap'

If we want to use the copy plugin to copy the value of the source property zip onto the source property of id, we have 2 options for the form UI when we configure the tamper plugin.

Least desirable - is that the source field is a text field, which forces the user to know what sources are available on the feed.

Most desirable - the plugin / form can be told about the available source properties of the feed, and can offer the user a select list of the sources to configure the plugin.

To accomplish the most desirable outcome, we need a clean and easily understood way in which the UI (feeds tamper) and pass the available source properties (from the feed item) into the plugin so that the plugin can present the available options to the user.

jamesdixon’s picture

Ah okay totally makes sense.

Do you think

$this->feedsFeedType->someMethodToGetSources();

Should return the sources in a format a form '#options' would understand, so they could simply drop the result into a 'radios' or 'select' form? Is selecting sources from these form types the main use case, or do you see any other ways people would need to access the sources from a plugin?

ericgsmith’s picture

Status: Active » Needs review
StatusFileSize
new5.37 KB

Yes, at this stage the only use can I can think of is the form - as in the only other public method we already have a real item.

Attached is a patch showing how this could work. I'm not hugely sold on the fact this is hidden behavior as far as the implementer is concerned.

ericgsmith’s picture

StatusFileSize
new4.04 KB

*edit* oops accidentally uploaded the patch I just tested

megachriz’s picture

I see these possible options to make passing sources more obvious:

  1. Maybe throw an exception if 'available_source_properties' is not set on $form_state? Maybe throw that exception even on TamperBase so that the implementer finds out much quicker that something is missing and not until he/she just happens to test with the "copy" plugin?
  2. When constructing a Tamper plugin, require a list of sources to be passed, maybe as part of $configuration (or $plugin_definition?). Con: tamper plugins that are not extending TamperBase would receive the list of sources as well and may not do anything with it. Another con: the list of sources shouldn't be saved when storing the Tamper plugin instance.
  3. Make Tamper plugins context aware as you proposed in #2937717-2: Add support for manipulating a complete 'feeds item'/'row'., but that made things more complicated.
  4. I suppose adding an extra parameter to buildConfigurationForm() to pass in additional context wouldn't work as you then break the interface?
ericgsmith’s picture

1 - Agree on this, an exception would be useful to explicitly state what is required to an implementer, and allows them to be in control of how to handle that result.

2 - I'm not a fan of this, as the available sources are not configuration or plugin definition - in other contexts they could vary of any given instance. It also introduces a level of complexity that not all plugins need, as only a small subset currently use the tamperable item.

3 - Still my preferred approach long term, but I don't have the time to follow this through myself. Happy to revisit this for an 8.2 branch on the assumption we want to get this 8.1 branch stable / released as soon as possible.

4 - I guess we could implement our own interface instead of PluginFormInterface, but I would prefer not to so that we are keeping inline with any a core examples and practices where possible.

Attached is an updated patch on how we could use approach 1.

megachriz’s picture

An other idea, similar to idea 2 from #9:

  1. Add an extra parameter to the constructor of TamperBase.
  2. Override the default factory (\Drupal\Core\Plugin\Factory\ContainerFactory) with a Tamper factory class.
  3. In TamperFactory, override the createInstance() method. Inside $configuration, expect 'available_source_properties' to be set (else throw an exception).
  4. Pass 'available_source_properties' separately to the Tamper plugin and unset it from $configuration before instantiating the plugin.

I did something alike in a project where the base plugin class did not extend \Drupal\Component\Plugin\PluginBase at all and had a complete different set of constructor parameters.
https://github.com/MegaChriz/afasprofit/blob/3.x/src/Core/Entity/EntityF...

It may not be the best approach, but the advantage is that Tamper plugins then would have access to the available sources list outside of buildConfigurationForm() as well.

ericgsmith’s picture

Its been a long time since I've thought about this as I've been on a few non Drupal projects, however coming back with some fresh eyes to give it another look.

I'm not convinced that we should be adding an extra param to the constructor or to the plugin at all for the available sources. For my mind, the plugin structure has enough information already to work for it's purpose - the only thing that is missing is that it can't be configured through the UI (which is only one avenue to configure a plugin). I don't think we need any additional information stored on / in the plugin in order for them to function correctly.

I can also see plugins being configured once to work on a variety of items, where the available sources could be completely different for each item passed it, but a plugin may still expect to exists as a single instance that works consistently.

I'm keen on getting this sorted without having to change any structure on the plugin - but would be happy to rethink if we can think of a reason why the plugin would need to know the available sources if it has been configured properly.

In my mind, the solution should be within forms, either
- using temp form storage
- using a custom sub form state and offering a trait or base class for modules to use when configuring plugins

The main goal is that its immediately obvious for plugin authors how to get the sources if needed, and also obvious for any modules implementing plugins. I'm going to think about this a little longer before doing any more dev experimentation on it.

megachriz’s picture

A type of plugin that uses an extra parameter in the constructor is "CommerceCheckoutPane".

  • CheckoutPaneBase asks in its constructor and also in its ::create() method for a CheckoutFlowInterface. For ::create() the parameter is defined optional in order to comply to the interface \Drupal\Core\Plugin\ContainerFactoryPluginInterface.
  • The plugin manager for checkout panes also defines an extra parameter for createInstance(), asking for CheckoutFlowInterface. Again optional, in order to be compatible with \Drupal\Component\Plugin\PluginManagerBase and \Drupal\Component\Plugin\Factory\FactoryInterface..
  • \Drupal\commerce_checkout\Plugin\Commerce\CheckoutFlow::getPanes() instantiates plugins of type "CommerceCheckoutPane".

The plugin type "FieldFormatter" uses a slightly different approach.

  • FormatterBase::__contruct() requires a instance of FieldDefinitionInterface.
  • FormatterPluginManager::createInstance() expects a FieldDefinitionInterface as part of the configuration, defined with the key 'field_definition'.
  • The instance is created in FormatterPluginManager::getInstance(), however the options array also expects to receive a FieldDefinitionInterface.
  • EntityViewDisplay::getRenderer() finally passes this FieldDefinitionInterface to the plugin manager.

My point is that I think it is a approach that is proven to work.

So I think we should come up with a SourceDefinitionInterface and a SourceDefinition object that consumers can use to pass to the Tamper plugin.

megachriz’s picture

StatusFileSize
new32.28 KB

This is how a SourceDefinition implementation with Tamper would look like.

jamesdixon’s picture

Assigned: Unassigned » jamesdixon

I plan on reviewing this in the week coming up.

jamesdixon’s picture

Seems like the approach in #14 would work.

Would it be possible to make the source definition optional for plugins which aren't using the source definitions in any way?

Or is the idea we always need the source definition so other modules working with the tamper plugins have an idea of the sources for form building etc?

megachriz’s picture

The idea is that other modules always need to pass a source definition, because they won't know beforehand whether or not a Tamper plugin requires that information.

Attached three patches:

  1. One that builds on top of the patch in #14.
  2. One that is a port of the Rewrite plugin, so you can see how the SourceDefinition could actively be used.
  3. And one that shows what changes in Feeds Tamper would be required.
ericgsmith’s picture

Massive thank you for progressing this! Its looking great from a first look.

Very happy to take your lead here on this for this approach.

jamesdixon’s picture

Assigned: jamesdixon » Unassigned
megachriz’s picture

Last week @andileco asked during the Feeds meetup what work is left for this issue.

Basically, the patch in #17 looks good to go. The reason that I didn't commit the patch yet is:

  • I'm not 100% sure if it's the best approach, but it was the best I currently could come up with. I think it could possibly cause some WTF's by developers trying to integrate Tamper in their modules. Because it adds some extra complexity in order to successfully integrate Tamper. Modules are required to instantiate and pass a SourceDefinition.
  • When I post a non-trivial patch for a module that I (co-)maintain, I usually leave it there for a while so I can look at it later with fresh eyes. I didn't come back to this patch yet because I had set other priorities.

I tried hard to think about a better approach, but my mind was going in circles there. Maybe we should accept the less perfect solution, so we can move forward. And perhaps I'm wrong about fearing that my solution makes integrating Tamper too hard.

andileco’s picture

I added the rewrite patch that's part of #17 to the appropriate issue: https://www.drupal.org/project/tamper/issues/2976180

megachriz’s picture

I've taken a look at my code in #17 and investigated parts I didn't like:

  • The many occurrence of unset($configuration['source_definition']);
  • That Feeds Tamper needed to do the following:
    // Add in source definition for each tamper.
    foreach ($tampers as &$tamper) {
      $tamper['source_definition'] = $this->getSourceDefinition();
    }
  • That it was passing the source definition as an extra parameter to a plugin's create() method:
    return $plugin_class::create(\Drupal::getContainer(), $configuration, $plugin_id, $plugin_definition, $source_definition);
    

    The CountryToCode plugin then had to define that extra parameter as an optional one in order to still comply with the interface "ContainerFactoryPluginInterface":

    public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition, SourceDefinitionInterface $source_definition = NULL) {
    

Field formatter plugins in Drupal Core approach

So I've taken another close look to how Drupal Core has organized code for field formatter plugins. FormatterBase requires an instance of FieldDefinitionInterface, so its situation is in that regard similar to the approach I had taken.

I see that TimestampFormatter for example just expects that the field definition is passed as part of $configuration:

/**
 * {@inheritdoc}
 */
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
  return new static(
    $plugin_id,
    $plugin_definition,
    $configuration['field_definition'],
    $configuration['settings'],
    $configuration['label'],
    $configuration['view_mode'],
    $configuration['third_party_settings'],
    $container->get('date.formatter'),
    $container->get('entity.manager')->getStorage('date_format')
  );
}

So in TamperManager, passing the source definition as part of $configuration sounds like a better approach to me. And it eliminates one occurrence of unset($configuration['source_definition']); as well.

TamperPluginCollection

I already left a @todo in that class to investigate if an instance of it should require a source definition. I tried how that worked out and I think this made the code in Feeds Tamper a bit cleaner:

$this->tamperCollection = new TamperPluginCollection($this->tamperManager, $this->getSourceDefinition(), $tampers);

And guess what, it eliminated another occurrence of unset($configuration['source_definition']); ;).

So that concludes my motivation for the changes I had made in this new patch. It might not solve the (potential) issue that it will get harder to integrate Tamper in other systems, but I think that the implementation is much cleaner. Now I hope it will pass tests. At least all Feeds Tamper tests pass locally.

The last submitted patch, 22: tamper-source-definition-2971881-22.patch, failed testing. View results
- codesniffer_fixes.patch Interdiff of automated coding standards fixes only.

megachriz’s picture

Ah, I forgot to update SprintfTest and I missed updating TamperConfigSchemaTest as well.

New patch. Also fixes some coding standard issues.

Status: Needs review » Needs work

The last submitted patch, 24: tamper-source-definition-2971881-24.patch, failed testing. View results
- codesniffer_fixes.patch Interdiff of automated coding standards fixes only.

megachriz’s picture

Status: Needs work » Needs review
StatusFileSize
new40.25 KB
new542 bytes

Hehe, referenced the wrong thing in TamperConfigSchemaTest.

megachriz’s picture

Hide some files, so the most useful patches show up on the top of this issue.

  • MegaChriz committed d1344fd on 8.x-1.x
    Issue #2971881 by MegaChriz, ericgsmith, jamesdixon: Plugins that depend...
megachriz’s picture

Status: Needs review » Fixed

Committed #26, with some slight changes in the code comments.

Now onto the rewrite plugin in #2976180: Rewrite Plugin! And adapting Feeds Tamper of course.

megachriz’s picture

And opened follow-up for Feeds Tamper: #3061554: Pass new SourceDefinition to Tamper plugins.

Status: Fixed » Closed (fixed)

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