You are here

class BulkVariationsCreator in Commerce Bulk 8

Default implementation of the BulkVariationsCreatorInterface.

Hierarchy

Expanded class hierarchy of BulkVariationsCreator

1 string reference to 'BulkVariationsCreator'
commerce_bulk.services.yml in ./commerce_bulk.services.yml
commerce_bulk.services.yml
1 service uses BulkVariationsCreator
commerce_bulk.variations_creator in ./commerce_bulk.services.yml
Drupal\commerce_bulk\BulkVariationsCreator

File

src/BulkVariationsCreator.php, line 17

Namespace

Drupal\commerce_bulk
View source
class BulkVariationsCreator implements BulkVariationsCreatorInterface {

  /**
   * The entity type manager.
   *
   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
   */
  protected $entityTypeManager;

  /**
   * Constructs a new BulkVariationsCreator object.
   *
   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
   *   The entity type manager.
   */
  public function __construct(EntityTypeManagerInterface $entity_type_manager) {
    $this->entityTypeManager = $entity_type_manager;
  }

  /**
   * {@inheritdoc}
   */
  public static function getSkuwidget(ProductVariation $variation) {
    $form_display = \Drupal::service('entity_display.repository')
      ->getFormDisplay($variation
      ->getEntityTypeId(), $variation
      ->bundle(), 'default');
    return $form_display
      ->getRenderer('sku');
  }

  /**
   * {@inheritdoc}
   */
  public static function getSkuSettings(ProductVariation $variation) {

    /** @var Drupal\commerce_product\Plugin\Field\FieldWidget\ProductVariationSkuWidget $widget */

    /** @var Drupal\Core\Field\Plugin\Field\FieldWidget\StringTextfieldWidget $widget */
    $widget = static::getSkuwidget($variation);

    // If no one widget is enabled, then we need to asign uniqid() SKUs at the
    // background to avoid having variations without SKU at all.
    $default_sku_settings = [
      'uniqid_enabled' => TRUE,
      'more_entropy' => FALSE,
      'prefix' => 'default_sku-',
      'suffix' => '',
      'maximum' => 500,
    ];
    return $widget ? $widget
      ->getSettings() : $default_sku_settings;
  }

  /**
   * {@inheritdoc}
   */
  public static function getAutoSku(ProductVariation $variation) {
    extract(static::getSkuSettings($variation));

    // Do return empty string in case of StringTextfieldWidget.
    return isset($uniqid_enabled) ? $uniqid_enabled ? \uniqid($prefix, $more_entropy) . $suffix : "{$prefix}{$suffix}" : '';
  }

  /**
   * {@inheritdoc}
   */
  public static function afterBuildPreRenderArrayAlter(array $element) {
    $i = 0;
    while (isset($element['alter_data_' . $i]) && ($data = $element['alter_data_' . $i])) {
      $parents = [];
      if (isset($data['#parents'])) {
        $parents = $data['#parents'];
        unset($data['#parents']);
      }
      unset($element['alter_data_' . $i]);
      $key_exists = NULL;
      $old_data = NestedArray::getValue($element, $parents, $key_exists);
      if (is_array($old_data)) {
        if (isset($element['#value'])) {
          $old_data['#value'] = $old_data['#default_value'] = $data['#value'];
          $data = $old_data;
        }
        $data = array_replace($old_data, $data);
      }
      elseif ($key_exists && !in_array($old_data, $data)) {
        $data[] = $old_data;
      }
      NestedArray::setValue($element, $parents, $data);
      $i++;
    }
    return $element;
  }

  /**
   * {@inheritdoc}
   */
  public function getProductVariation(Product $product) {
    $variations = $product
      ->getVariations();
    $variation = end($variations);
    $timestamp = time();
    if (!$variation instanceof ProductVariation) {
      $product_type = $this->entityTypeManager
        ->getStorage('commerce_product_type')
        ->load($product
        ->bundle());
      $variation = $this->entityTypeManager
        ->getStorage('commerce_product_variation')
        ->create([
        'type' => $product_type
          ->getVariationTypeId(),
        'product_id' => $product
          ->id(),
        'created' => $timestamp,
        'changed' => $timestamp,
      ]);
    }
    return $variation;
  }

  /**
   * {@inheritdoc}
   */
  public function createProductVariation(Product $product, array $variation_custom_values = [], array $not_all = [
    'not_all' => TRUE,
  ]) {
    $combination = [];
    $variation = $this
      ->getProductVariation($product);
    if (($all = $this
      ->getAttributesCombinations([
      $variation,
    ], $not_all)) && $all['not_used_combinations']) {
      $combination = reset($all['not_used_combinations']);
      foreach ($combination as $field_name => $id) {
        $variation
          ->get($field_name)
          ->setValue([
          'target_id' => $id == '_none' ? NULL : $id,
        ]);
      }
    }
    $sku = static::getAutoSku($variation);
    $settings = static::getSkuSettings($variation);
    $clone = clone $variation;
    $sku = empty($sku) ? \uniqid() : $sku;
    $settings['combination'] = $combination;
    \Drupal::moduleHandler()
      ->alter("bulk_creator_sku", $sku, $settings, $clone);
    $variation
      ->setSku($sku);
    foreach ($variation_custom_values as $name => $value) {
      $variation
        ->set($name, $value);
    }
    if (!$variation
      ->getPrice() instanceof Price) {
      $currency_storage = $this->entityTypeManager
        ->getStorage('commerce_currency');
      $currencies = array_keys($currency_storage
        ->loadMultiple());
      $currency = empty($currencies) ? 'USD' : $currencies[0];

      // Decimals are omitted intentionally as $currency format is unknown here.
      // The prices still will have valid format after saving.
      $variation
        ->setPrice(new Price('1', $currency));
    }
    $variation
      ->updateOriginalValues();
    return $variation;
  }

  /**
   * {@inheritdoc}
   */
  public function duplicateAllProductVariations(Product $product) {
    if (!($variations = $product
      ->getVariations())) {
      return $variations;
    }
    if (!($product_id = $product
      ->id())) {
      $product
        ->save();
      $product_id = $product
        ->id();
    }
    $settings = static::getSkuSettings(end($variations));
    extract($settings);
    $prefix = isset($prefix) ? $prefix : '';
    $suffix = isset($suffix) ? $suffix : '';
    $more_entropy = isset($more_entropy) ? $more_entropy : FALSE;
    $duplicates = [];
    foreach ($variations as $variation) {
      $duplicate = $variation
        ->createDuplicate();
      $duplicate
        ->setSku(\uniqid($prefix, $more_entropy) . $suffix);
      $duplicate
        ->set('product_id', $product_id);
      $duplicates[] = $duplicate;
    }
    return $duplicates;
  }

  /**
   * {@inheritdoc}
   */
  public function createAllProductVariations(Product $product, array $variation_custom_values = [], array $all = []) {
    $timestamp = time();
    $shuffle_variations = !empty($all['shuffle_variations']);
    $not_all['not_all'] = $max = !empty($all['max_nb_skus']) ? $all['max_nb_skus'] - 2 : TRUE;
    unset($all['shuffle_variations'], $all['max_nb_skus']);
    if (!$all) {
      $variations = $product
        ->getVariations();
      if (empty($variations) || !empty($variation_custom_values)) {
        $variations[] = $this
          ->createProductVariation($product, $variation_custom_values, $not_all);
        $timestamp--;
      }
      if (!($all = $this
        ->getAttributesCombinations($variations, $not_all))) {
        return;
      }
    }
    else {
      $variations = $all['variations'];
      $all = $all['all'];
    }

    // Improve perfomance by getting sku settings just once instead of
    // calling static::getAutoSku() in the loop.
    $settings = static::getSkuSettings($all['last_variation']);
    extract($settings);
    $prefix = isset($prefix) ? $prefix : '';
    $suffix = isset($suffix) ? $suffix : '';
    $more_entropy = isset($more_entropy) ? $more_entropy : FALSE;
    $module_handler = \Drupal::moduleHandler();
    $clone = clone $all['last_variation'];
    $shuffle_variations && shuffle($all['not_used_combinations']);
    $max = is_numeric($max) ? $max : count($all['not_used_combinations']);
    foreach ($all['not_used_combinations'] as $combination) {
      $variation = $all['last_variation']
        ->createDuplicate()
        ->setChangedTime($timestamp)
        ->setCreatedTime($timestamp);
      $sku = \uniqid($prefix, $more_entropy) . $suffix;
      $settings['combination'] = $combination;
      $module_handler
        ->alter("bulk_creator_sku", $sku, $settings, $clone);
      $variation
        ->setSku($sku);
      foreach ($settings['combination'] as $field_name => $id) {
        $variation
          ->get($field_name)
          ->setValue([
          'target_id' => $id == '_none' ? NULL : $id,
        ]);
      }
      $variation
        ->updateOriginalValues();
      $variations[] = $variation;

      // To avoid the same CreatedTime on multiple variations decrease the
      // $timestamp by one second instead of calling time() in the loop.
      $timestamp--;
      if (!$max--) {
        break;
      }
    }
    return $variations;
  }

  /**
   * {@inheritdoc}
   */
  public function createAllIefFormVariations(array $form, FormStateInterface $form_state) {

    // Rid of entity type manager here as that prevents to use instance of
    // BulkVariationsCreator as an AJAX callback therefore forcing to use
    // just the class name instead of object and define all functions as static.
    $this->entityTypeManager = NULL;
    $ief_id = $form['variations']['widget']['#ief_id'];
    $ief_entities = $form_state
      ->get([
      'inline_entity_form',
      $ief_id,
      'entities',
    ]) ?: [];
    if (!($all = $this
      ->getAttributesCombinations(array_column($ief_entities, 'entity')))) {
      return;
    }

    // The attributes (ids and options) may be quite heavy, so unset them.
    unset($all['attributes']);
    $timestamp = time();
    $ief_entity = end($ief_entities);
    $settings = static::getSkuSettings($all['last_variation']);
    extract($settings);
    $prefix = isset($prefix) ? $prefix : '';
    $suffix = isset($suffix) ? $suffix : '';
    $more_entropy = isset($more_entropy) ? $more_entropy : FALSE;
    $module_handler = \Drupal::moduleHandler();
    $clone = clone $all['last_variation'];
    foreach ($all['not_used_combinations'] as $combination) {
      $variation = $all['last_variation']
        ->createDuplicate()
        ->setChangedTime($timestamp)
        ->setCreatedTime($timestamp);
      $sku = \uniqid($prefix, $more_entropy) . $suffix;
      unset($settings['combination']);
      $settings['combination'] = $combination;
      $module_handler
        ->alter("bulk_creator_sku", $sku, $settings, $clone);
      $variation
        ->setSku($sku);
      foreach ($combination as $field_name => $id) {
        $variation
          ->get($field_name)
          ->setValue([
          'target_id' => $id == '_none' ? NULL : $id,
        ]);
      }
      $variation
        ->updateOriginalValues();
      $ief_entity['entity'] = $variation;
      $ief_entity['weight'] += 1;
      $ief_entity['needs_save'] = TRUE;
      array_push($ief_entities, $ief_entity);
      $timestamp--;
    }

    // Before continuing unset $all['*combinations'] which might be a huge data.
    unset($all);
    $form_state
      ->set([
      'inline_entity_form',
      $ief_id,
      'entities',
    ], $ief_entities);
    $form_state
      ->setRebuild();
  }

  /**
   * {@inheritdoc}
   */
  public function getIefFormNotUsedAttributesCombination(FormStateInterface $form_state, $ief_id = '') {
    $this->entityTypeManager = NULL;
    $ief_entities = $form_state
      ->get([
      'inline_entity_form',
      $ief_id,
      'entities',
    ]) ?: [];
    return $this
      ->getNotUsedAttributesCombination(array_column($ief_entities, 'entity'));
  }

  /**
   * {@inheritdoc}
   */
  public function getNotUsedAttributesCombination(array $variations) {
    if (!($all = $this
      ->getDuplicationsHtmlList($variations))) {
      return;
    }
    $all['not_used_combination'] = reset($all['not_used_combinations']);

    // Rid of unecessary data which might be quite heavy.
    unset($all['used_combinations'], $all['not_used_combinations']);
    return $all;
  }

  /**
   * {@inheritdoc}
   */
  public function getUsedAttributesCombinations(array $variations) {
    $all = [];
    $all['duplicated'] = $all['used_combinations'] = [];
    $all['last_variation'] = end($variations);
    $all['attributes'] = $this
      ->getAttributeFieldOptionIds(end($variations));
    $nones = array_fill_keys(array_keys($all['attributes']['ids']), '_none');
    foreach ($variations as $index => $variation) {

      // ProductVariation->getAttributeValueIds() does not return empty optional
      // fields. Merge 'field_name' => '_none' as a choice in the combination.
      // @todo Render '_none' option on an Add to Cart form.
      // @see ProductVariationAttributesWidget->formElement()
      // @see CommerceProductRenderedAttribute::processRadios()
      $combination = array_merge($nones, $variation
        ->getAttributeValueIds());
      if (in_array($combination, $all['used_combinations'])) {
        $all['duplicated'][$index] = $combination;
      }
      else {
        $all['used_combinations'][$index] = $combination;
      }
    }
    $all['used'] = count($all['used_combinations']);
    $all['count'] = $all['attributes']['count'];
    return $all;
  }

  /**
   * {@inheritdoc}
   */
  public function getDuplicationsHtmlList(array $variations) {
    if (!($all = $this
      ->getAttributesCombinations($variations))) {
      return;
    }
    if (!empty($all['duplicated'])) {
      $all['duplications_list'] = '<ul>';
      foreach ($all['duplicated'] as $fields) {
        $label = [];
        foreach ($fields as $field_name => $id) {
          if (isset($all['attributes']['options'][$field_name][$id])) {
            $label[] = $all['attributes']['options'][$field_name][$id];
          }
        }
        $label = Html::escape(implode(', ', $label));
        $all['duplications_list'] .= '<li>' . $label . '</li>';
      }
      $all['duplications_list'] .= '</ul>';
      $all['duplications_list'] = Markup::create($all['duplications_list']);
    }
    $all['duplicated'] = count($all['duplicated']);
    return $all;
  }

  /**
   * {@inheritdoc}
   */
  public function getAttributesCombinations(array $variations, array $return = [
    'not_all' => TRUE,
  ]) {
    $all = $this
      ->getUsedAttributesCombinations($variations);

    // Restrict by default the number of returned not used combinations if their
    // number exceeds some resonable number (500). To get all possible
    // combinations call this method with an empty array as the second argument.
    if (!empty($return['not_all'])) {
      if ($return['not_all'] === TRUE) {
        $return['not_all'] = static::getSkuSettings($all['last_variation'])['maximum'] - 2;
      }
      if ($all['count'] > $return['not_all']) {
        $all += $return;
        $all['used_combinations']['not_all'] = $return['not_all'];
      }
    }
    $all['not_used_combinations'] = $this
      ->getArrayValueCombinations($all['attributes']['ids'], $all['used_combinations']);
    unset($all['used_combinations']['not_all']);
    $all['not_used'] = count($all['not_used_combinations']);
    return $all;
  }

  /**
   * {@inheritdoc}
   */
  public function getArrayValueCombinations(array $data = [], array $exclude = [], array &$all = [], array $group = [], $value = NULL, $i = 0, $k = NULL, $c = NULL, $f = NULL) {
    $keys = $k ?: array_keys($data);
    $count = $c ?: count($data);
    if ($include = isset($value) === TRUE) {
      $group[$f] = $value;
    }
    if ($i >= $count && $include) {
      foreach ($exclude as $index => $combination) {
        if ($group == $combination) {
          unset($exclude[$index]);
          $include = FALSE;
          break;
        }
      }
      if ($include) {
        $all[] = $group;
      }
    }
    elseif (isset($keys[$i])) {
      if (isset($exclude['not_all']) && !empty($all) && count($all) > $exclude['not_all']) {
        return $all;
      }
      $field_name = $keys[$i];
      foreach ($data[$field_name] as $val) {
        $this
          ->getArrayValueCombinations($data, $exclude, $all, $group, $val, $i + 1, $keys, $count, $field_name);
      }
    }
    return $all;
  }

  /**
   * {@inheritdoc}
   */
  public function getAttributeFieldOptionIds(ProductVariation $variation) {
    $count = 1;
    $field_options = $ids = $options = $attributeopt = [];
    if (($product = $variation
      ->getProduct()) && $product->attributeopt) {
      $attributeopt = $product->attributeopt->value;
    }
    foreach ($this
      ->getAttributeFieldNames($variation) as $field_name => $values) {
      if ($attributeopt) {
        $values = array_filter($values, function ($k) use ($attributeopt, $field_name) {
          return isset($attributeopt[$field_name][$k]);
        }, ARRAY_FILTER_USE_KEY);
      }
      $definition = $variation
        ->get($field_name)
        ->getFieldDefinition();
      $ids[$field_name] = $options[$field_name] = [];
      foreach ($values as $value) {
        if (is_array($value) && ($keys = array_keys($value))) {
          $ids[$field_name] = array_unique(array_merge($ids[$field_name], $keys));
          $options[$field_name] += $value;
        }
        elseif ($keys = array_keys($values)) {
          $ids[$field_name] = array_unique(array_merge($ids[$field_name], $keys));
          $options[$field_name] += $values;
        }

        // Optional fields need '_none' id as a possible choice.
        !$definition
          ->isRequired() && !in_array('_none', $ids[$field_name]) && array_unshift($ids[$field_name], '_none');
        array_walk($ids[$field_name], function (&$id) {
          $id = (string) $id;
        });
      }
      $count *= count($ids[$field_name]);
    }
    $field_options['ids'] = $ids;
    $field_options['options'] = $options;
    $field_options['count'] = $count;
    return $field_options;
  }

  /**
   * {@inheritdoc}
   */
  public function getAttributeFieldNames(ProductVariation $variation) {
    $attribute_field_manager = \Drupal::service('commerce_product.attribute_field_manager');
    $field_map = $attribute_field_manager
      ->getFieldMap($variation
      ->bundle());
    $attribute_ids = array_column($field_map, 'attribute_id');
    $field_names = array_column($field_map, 'field_name');
    $storage = \Drupal::entityTypeManager()
      ->getStorage('commerce_product_attribute_value');
    $names = $fields = [];
    foreach ($field_names as $index => $name) {
      $fields[$name][] = $attribute_ids[$index];
    }
    foreach ($fields as $field_name => $ids) {
      $values = [];
      foreach ($ids as $attribute_id) {
        foreach ($storage
          ->loadMultipleByAttribute($attribute_id) as $id => $value) {
          $values[$id] = $value
            ->getName();
        }
        $names[$field_name] = $values;
      }
    }
    return $names;
  }

}

Members

Namesort descending Modifiers Type Description Overrides
BulkVariationsCreator::$entityTypeManager protected property The entity type manager.
BulkVariationsCreator::afterBuildPreRenderArrayAlter public static function A callback which might be set to #pre_render or #after_build form element. Overrides BulkVariationsCreatorInterface::afterBuildPreRenderArrayAlter
BulkVariationsCreator::createAllIefFormVariations public function An AJAX callback to create all possible variations. Overrides BulkVariationsCreatorInterface::createAllIefFormVariations
BulkVariationsCreator::createAllProductVariations public function Creates all possible variations for commerce_product. Overrides BulkVariationsCreatorInterface::createAllProductVariations
BulkVariationsCreator::createProductVariation public function Creates a variation for commerce_product. Overrides BulkVariationsCreatorInterface::createProductVariation
BulkVariationsCreator::duplicateAllProductVariations public function
BulkVariationsCreator::getArrayValueCombinations public function Gets combinations of an Array values. Overrides BulkVariationsCreatorInterface::getArrayValueCombinations
BulkVariationsCreator::getAttributeFieldNames public function Gets the names of the entity's attribute fields. Overrides BulkVariationsCreatorInterface::getAttributeFieldNames
BulkVariationsCreator::getAttributeFieldOptionIds public function Gets the IDs of the variation's attribute fields. Overrides BulkVariationsCreatorInterface::getAttributeFieldOptionIds
BulkVariationsCreator::getAttributesCombinations public function Gets all ids combinations of the commerce_product's attribute fields. Overrides BulkVariationsCreatorInterface::getAttributesCombinations
BulkVariationsCreator::getAutoSku public static function Default value callback for the 'sku' base field definition. Overrides BulkVariationsCreatorInterface::getAutoSku
BulkVariationsCreator::getDuplicationsHtmlList public function Gets duplicated variations HTML list. Overrides BulkVariationsCreatorInterface::getDuplicationsHtmlList
BulkVariationsCreator::getIefFormNotUsedAttributesCombination public function Gets first not used combination on a product IEF form. Overrides BulkVariationsCreatorInterface::getIefFormNotUsedAttributesCombination
BulkVariationsCreator::getNotUsedAttributesCombination public function Gets first not used combination on a product. Overrides BulkVariationsCreatorInterface::getNotUsedAttributesCombination
BulkVariationsCreator::getProductVariation public function Gets a variation for commerce_product. Overrides BulkVariationsCreatorInterface::getProductVariation
BulkVariationsCreator::getSkuSettings public static function Helper method to get variation sku field form display settings. Overrides BulkVariationsCreatorInterface::getSkuSettings
BulkVariationsCreator::getSkuwidget public static function Helper method to get variation sku field form display settings. Overrides BulkVariationsCreatorInterface::getSkuwidget
BulkVariationsCreator::getUsedAttributesCombinations public function Gets used combinations on a product. Overrides BulkVariationsCreatorInterface::getUsedAttributesCombinations
BulkVariationsCreator::__construct public function Constructs a new BulkVariationsCreator object.