|array */ public function prepare(\WC_Product $product, mixed $rows): array { if (!is_array($rows) || !array_is_list($rows) || $rows === []) { throw new RuntimeException('attributes must be a non-empty list of attribute edits.'); } if (count($rows) > 100) { throw new RuntimeException('attributes accepts at most 100 edits.'); } $variation = $product instanceof \WC_Product_Variation; $current = $product->get_attributes('edit'); $parent = $variation ? wc_get_product($product->get_parent_id('edit')) : null; if ($variation && !$parent instanceof \WC_Product_Variable) { throw new RuntimeException('The variation needs an existing variable parent.'); } $parentAttributes = $parent instanceof \WC_Product_Variable ? $parent->get_attributes('edit') : []; $seen = []; foreach ($rows as $row) { if (!is_array($row) || count($row) !== 2 || !array_key_exists('key', $row) || !array_key_exists('values', $row)) { throw new RuntimeException('Each attributes edit needs exactly key and values.'); } $key = $row['key']; $values = $row['values']; if (!is_string($key) || trim($key) !== $key || $key === '' || !is_array($values) || !array_is_list($values)) { throw new RuntimeException('Each attributes edit needs an exact key and a values list.'); } $global = str_starts_with($key, 'pa_'); $storedKey = $global ? $key : sanitize_title($key); if ($storedKey === '' || isset($seen[$storedKey])) { throw new RuntimeException('Invalid or duplicate attribute key: ' . $key . '.'); } $seen[$storedKey] = true; if ($global && (!taxonomy_exists($key) || wc_attribute_taxonomy_id_by_name($key) <= 0)) { throw new RuntimeException('Global attribute ' . $key . ' does not exist.'); } if ($variation) { $definition = $this->findAttribute($parentAttributes, $key); if (!$definition || !$definition->get_variation()) { throw new RuntimeException('Attribute ' . $key . ' is not a variation axis on the parent.'); } if (count($values) > 1) { throw new RuntimeException('A variation attribute accepts one value or an empty list for Any.'); } $current[$storedKey] = $this->variationValue($key, $values, $definition, $global); continue; } $existing = $this->findAttribute($current, $key); if (!$global && $existing === null && sanitize_text_field($key) !== $key) { throw new RuntimeException('Product-specific attribute names must be plain text.'); } if ($existing !== null && $existing->is_taxonomy() !== $global) { throw new RuntimeException('Attribute ' . $key . ' has a different kind on this product.'); } $attribute = $existing !== null ? clone $existing : new \WC_Product_Attribute(); $attribute->set_id($global ? (int) wc_attribute_taxonomy_id_by_name($key) : 0); if ($existing === null) { $attribute->set_name($key); } $attribute->set_options($global ? $this->termIds($key, $values) : $this->localValues($values)); if ($existing === null) { $attribute->set_position(count($current)); $attribute->set_visible(true); $attribute->set_variation(false); $current[] = $attribute; } else { foreach ($current as $index => $candidate) { if ($candidate === $existing) { $current[$index] = $attribute; break; } } } } if ($variation) { $this->assertUniqueVariation($product, $parent, $current); return $current; } if ($product instanceof \WC_Product_Variable) { $this->assertParentOptionsRetained($product, $current); } return array_values($current); } /** @return array}> */ public function read(\WC_Product $product): array { $rows = []; if ($product instanceof \WC_Product_Variation) { foreach ($product->get_attributes('edit') as $key => $value) { $key = (string) $key; $value = (string) $value; if (str_starts_with($key, 'pa_') && $value !== '') { $term = get_term_by('slug', $value, $key); $values = $term instanceof \WP_Term ? [(int) $term->term_id] : [$value]; } else { $values = $value === '' ? [] : [$value]; } $rows[] = ['key' => $key, 'values' => $values]; } } else { foreach ($product->get_attributes('edit') as $attribute) { if (!$attribute instanceof \WC_Product_Attribute) { continue; } $rows[] = [ 'key' => (string) $attribute->get_name(), 'values' => $attribute->is_taxonomy() ? array_values(array_map('intval', $attribute->get_options())) : array_values(array_map('strval', $attribute->get_options())), ]; } } usort($rows, static fn (array $a, array $b): int => strcmp($a['key'], $b['key'])); return $rows; } /** @param array $attributes */ private function findAttribute(array $attributes, string $key): ?\WC_Product_Attribute { foreach ($attributes as $attribute) { if (!$attribute instanceof \WC_Product_Attribute) { continue; } $matches = $attribute->get_name() === $key || (!$attribute->is_taxonomy() && sanitize_title($attribute->get_name()) === sanitize_title($key)); if ($matches) { return $attribute; } } return null; } /** @param array $values * @return array */ private function termIds(string $taxonomy, array $values): array { $ids = []; foreach ($values as $id) { if (!is_int($id) || $id <= 0 || !(get_term($id, $taxonomy) instanceof \WP_Term)) { throw new RuntimeException('Values for ' . $taxonomy . ' must be existing term IDs in that taxonomy.'); } if (isset($ids[$id])) { throw new RuntimeException('Duplicate term ID for ' . $taxonomy . '.'); } $ids[$id] = $id; } return array_values($ids); } /** @param array $values * @return array */ private function localValues(array $values): array { $result = []; foreach ($values as $value) { if (!is_string($value) || trim($value) === '' || sanitize_text_field($value) !== $value) { throw new RuntimeException('Product-specific attribute values must be non-empty plain strings.'); } if (in_array($value, $result, true)) { throw new RuntimeException('Duplicate product-specific attribute value.'); } $result[] = $value; } return $result; } /** @param array $values */ private function variationValue(string $key, array $values, \WC_Product_Attribute $definition, bool $global): string { if ($values === []) { return ''; } if ($global) { $ids = $this->termIds($key, $values); if (!in_array($ids[0], array_map('intval', $definition->get_options()), true)) { throw new RuntimeException('Variation value must be enabled on the parent attribute ' . $key . '.'); } $term = get_term($ids[0], $key); return (string) $term->slug; } $value = $this->localValues($values)[0]; if (!in_array($value, array_map('strval', $definition->get_options()), true)) { throw new RuntimeException('Variation value must be enabled on the parent attribute ' . $key . '.'); } return $value; } /** @param array $values */ private function assertUniqueVariation(\WC_Product_Variation $product, \WC_Product_Variable $parent, array $values): void { ksort($values); foreach ($parent->get_children() as $siblingId) { if ((int) $siblingId === (int) $product->get_id()) { continue; } $sibling = wc_get_product((int) $siblingId); if (!$sibling instanceof \WC_Product_Variation) { continue; } $other = $sibling->get_attributes('edit'); ksort($other); if ($other === $values) { throw new RuntimeException('This variation attribute combination already belongs to another variation.'); } } } /** @param array $attributes */ private function assertParentOptionsRetained(\WC_Product_Variable $parent, array $attributes): void { foreach ($parent->get_children() as $childId) { $child = wc_get_product((int) $childId); if (!$child instanceof \WC_Product_Variation) { continue; } foreach ($child->get_attributes('edit') as $key => $value) { if ($value === '') { continue; } $definition = $this->findAttribute($attributes, (string) $key); if (!$definition || !$definition->get_variation()) { continue; } if ($definition->is_taxonomy()) { $term = get_term_by('slug', (string) $value, (string) $key); if (!$term instanceof \WP_Term || !in_array((int) $term->term_id, array_map('intval', $definition->get_options()), true)) { throw new RuntimeException('Cannot remove an attribute option used by a variation.'); } } elseif (!in_array((string) $value, array_map('strval', $definition->get_options()), true)) { throw new RuntimeException('Cannot remove an attribute option used by a variation.'); } } } } }