The code currently says this:

          // A field qualifies if it is single value, required and uses a widget
          // with a definite set of options. For the sake of simplicity, this is
          // currently restricted to fields defined by the options module.

However, there's no logistic bar to fields that don't use options module. We have the product entities being referenced, we have Entity Metadata API, so it's easy to collect values as this quick hack demonstrates:

              $existing_values = array();
              foreach ($products as $product_id => $product) {
                $product_wrapper = entity_metadata_wrapper('commerce_product', $product);
                $value = $product_wrapper->{$name}->raw();
                $existing_values[$value] = $value;
              }
              $allowed_values = $existing_values;

-- the above works perfectly on the add to cart form.

Beyond that, this change would probably entail some refactoring of commerce_cart_add_to_cart_form(), as there is currently a two-stage process:

1. Gather the full options list for each qualifying field
2. Collect values.

Step 2 has additional logic which I don't yet fully get:

            // Only add options to the present array that appear on products that
            // match the default value of the previously added attribute widgets.

but it does seem to me that if step 1 now possibly gathers values from product directly, then steps 1 and 2 could do to be combined or refactored in some way.

Comments

joachim’s picture

BTW the use case for this is product sizes, which for clothing are subject to huge variations depending on manufacturers, and where in my current project it's felt that the best thing is to use the size as the manufacturer specifies it exactly, in a text field.

rszrama’s picture

That additional logic comes from our support for dependent attributes. Think of this product matrix:

       S   M   L
Red    *   *   *
Green  *   *
Blue   *

As the color or size are updated (whichever appears first on the form; let's say color), the dependent attribute's options list is going to be updated to only show options available by referenced products that match that first attribute. So what we do is we load the full options list and then filter it by the options referenced on matching products (matching as defined by products that are referenced on the form and whose attribute field values match the selected options of all previous attribute widgets represented on the Add to Cart form).

That said, we could get the initial options list by reading the product data directly, thought it may be a performance concern on sites that use 10,000 products per form. They're out there. : P

We get two other things out of this constraint, though:

  1. Sanitation. The options module lets us prepare options for inclusion in a select list or radio buttons, properly filtering out all tags or just vulnerable tags.
  2. Display. We also know that any field using the options hook will return allowed values that are fine to go in either a select list or radio buttons. If any field type were allowed to be used, we couldn't necessarily know - there are surely field types that wouldn't be appropriate for select lists.

So, I'm not opposed to adding this feature if it can satisfy these concerns - performance, sanitation, and display in the selected widget. The problem is I don't know how you solve the display problem without either mapping field types to appropriate attribute widget types or just letting people choose an inappropriate widget and failing.

joachim’s picture

I think we can expand this and still keep the current way of retrieving options from hook_options_list() for the field types we support. Hence we keep the performance for those fields, and for plain text fields there are scalability limitations that we need to document.

The way to approach this I think is to consider that it's about widget types. The current test is:

function_exists($field['module'] . '_options_list');

which is another way of saying that the module that defines the field type allows an options widget to be used with its field.

(Aside: I'm not sure this test is totally robust, BTW: what if $field[module] defines more than one field type, and only one of them supports options widgets? But that's by the by.)

So the first step is to change commerce_cart_field_attribute_eligible() to instead say that the 3 widget types defined by options module are eligible, and that therefore the field types that those widget types support are eligible too.

Combined with that, we refactor the bit that calls hook_options_list() so that something else could happen there instead.

We can then start adding further types to the mix, provided that we also add a way to get a list of values and know whether or not to sanitize them.

For this issue, I just want to add text_textfield and number widget types, as we know how to sanitize them and they work in select dropdowns.

(And for taking this further, there's follow-up issues. Think how cool it would be if you could have radio buttons with images for selecting product colours!)

joachim’s picture

Digging into this some more I'm suddenly wondering why we need the hook_options_list() stuff at all.

We have this section of the code:

        // Loop through all the field instances on that product type.
        foreach (field_info_instances('commerce_product', $type) as $name => $instance) {
          // ... SNIP

          // If the instance is of a field type that is eligible to function as
          // a product attribute field and if its attribute field settings
          // specify that this functionality is enabled...
          if (commerce_cart_field_attribute_eligible($field) && $commerce_cart_settings['attribute_field']) {

which gathers up the options for each field using hook_options_list(). Which is fair enough: now for each attribute field we have a list of its possible options.

But then after that block we have this:

      if (!empty($qualifying_fields)) {
        // ... SNIP
        foreach ($qualifying_fields as $field_name => $data) {
          // Build an options array of widget options used by referenced products.

Here in this foreach we loop over each field and for each product in the form, we use the entity metadata wrapper to build ourselves a list of the **possible** options for each field in the form.

So for example, suppose we have a field that was letters of the alphabet, and the products in the current form only have values A, B, C between them, we get:

* the total list of possible values: A, B, C, D ... Z
* the list of values that the products in hand vary over, as collected from the entity wrappers: A, B, C.

The only place we use the first list once we've made it (AFAICT) is here:

              '#options' => array_intersect_key($data['options'], drupal_map_assoc($used_options[$field_name])),

But why do we need to do that? FieldAPI already guarantees that the field values must be in the allowed options list. Why are we bothering to check?

So for my comment in the OP that we refactor this to gather values from the product entities -- we already do that! It's just we have this (AFAICT) pointless step of collecting all possible values for a sanity check that's redundant.

EDIT: after writing all that I get it. We're building up the options using hook_options_list() stuff because that takes care of sanitizing the properly. Then we're intersecting to cut the list down to what we actually have.

joachim’s picture

Lightbulb number 2: hook_options_list() also takes care of cases where the field definition defines options in the 'value|Fancy label' style. We obviously want the fancy labels, not the raw values.

I think this can still all work, and allow fields that don't define a list, if we turn the execution of this on its head:

1. gather all the values present on our products in hand
2. do some preparation work on the values, based on the widget type:
-- options lists field do what they used to, just later on
-- textfields sanitize the values

joachim’s picture

Status: Active » Needs review
StatusFileSize
new15.61 KB

Here's a patch!

I've used format-patch to create this as sequence of the commits on my local branch, so it's more easily digestible and each smaller change is explained.

Here's the overview:

- use hook_field_widget_info_alter() to add some properties to the widget types we can work with. This is a callback we'll use later
- change commerce_cart_field_attribute_eligible() to make use of the presence of this in the widget type to determine eligibility
- removed the gathering of all possible options from the main body of commerce_cart_add_to_cart_form(). Now we do the collecting of values from the current products first
- once we have the lists of values on our products, hand them over to our widget-specific callback. In the case of options widgets, the code is just the same as what has been removed from the main function body; just that it's done a bit later on. In the case of textfield widgets, we sanitize the data appropriately for the attribute widget.

This patch only adds support for textfields. Number fields should work too and be a simple follow-on.

To go over your concerns:

- Performance: nothing much has changed. There's a bit of array legwork to figure out eligible fields, and we hop out via a callback. No new queries or anything like that.
- Sanitization: nothing has changed for options lists. Textfields get appropriately sanitized.
- Display: no problems. Textfields are fine to go in either selects or radios. We're not doing image fields yet -- though if a later patch adds support for those, I imagine we could add something in hook_field_widget_info_alter() to state that this field type can only work with radios.

iKb’s picture

I think a new patch for the current code is nedded. If you can please recreate the patch using the latest -dev as base.

joachim’s picture

Unfortunately, I'm no longer working on the project that I wrote this patch for, so it's not very likely I'm going to find the time work reroll this patch, sorry.

Let's retest it to see how badly it needs work at least though :)

joachim’s picture

Status: Needs review » Needs work
camdarley’s picture

I reviewed and rewrited the patch... this one should pass...

camdarley’s picture

Status: Needs work » Needs review

Status: Needs review » Needs work
camdarley’s picture

Little bug with $eligible_widget_types variable fixed:

camdarley’s picture

Status: Needs work » Needs review

Status: Needs review » Needs work
camdarley’s picture

Status: Needs work » Needs review

Status: Needs review » Needs work
camdarley’s picture

Status: Needs work » Needs review
StatusFileSize
new11.4 KB

Status: Needs review » Needs work
camdarley’s picture

Status: Needs work » Needs review
StatusFileSize
new11.44 KB

Hope this is the one!