Tutorial

How to handle structured and nested product data

Imagine, you have a shop that offers food products. As required in many countries you are required to provide the nutrition facts about your food products. To do so, you need to provide quantities for, e.g., sugar, fat, and protein for each of the products you offer. Look at the yummy tub of ice cream below for which we need to store some nutrition facts.

Image Ice Cream Nutrition

Wouldn’t It would be great to have a structured data object that holds all those nutrient information and that we could reuse in all our products? That’s really not a big deal in the commercetools™ platform, you can easily define ProductTypes with custom attributes the products of that type should have in common and you can nest this ProductType into another one. Confused? Don’t worry, we’ll guide you step by step through this tutorial.

Create simple Product Type

Our objective is to construct a food-product-type that contains nutrition information captured in a ProductType nutrient-information. Moreover, the food-product-type does not only contain one object of the type nutrient-information, but a structure such as a Set of them, e.g., one for fat, one for sugar, and so on as depicted in the figure below:.

Image Food ProductType

We always need to start with the creation of the simple ProductType that should be nested into the advanced ProductType. We must do it this way since we need the typeID of the ProductType to be nested first. Otherwise we wouldn’t be able to setup the reference relationship between the advanced ProductType and the nested ProductType properly.

For our example we need to design the nutrient-information ProductType first. To keep it simple this ProductType contains two attributes only: one that holds a number indicating the quantity of a nutrient (quantityContained) and one attribute for the type of nutrient (nutrientTypeCode). We’ll set both attributes as mandatory to be filled with values by setting isRequired to true for each attribute. For now nested attributes are not searchable in the commercetools™ platform so that we need to set is Searchable to false.

#!/bin/sh
curl -sH "Authorization: Bearer ACCESS_TOKEN" https://api.sphere.io/{project-key}/product-types -d @- << EOF
{
  "name": "nutrient-information",
  "description": "The nutrient-information product type.",
  "attributes": [
    {
      "name": "quantityContained",
      "type": {
        "name": "number"
      },
      "isRequired": true,
      "attributeConstraint": "None",
      "isSearchable": false,
      "label": {
        "en": "quantity contained"
      }
    },
    {
      "name": "nutrientTypeCode",
      "type": {
        "name": "text"
      },
      "isRequired": true,
      "attributeConstraint": "None",
      "isSearchable": false,
      "label": {
        "en": "nutrient type Code"
      }
    }
  ]
}
EOF
{
  "name": "nutrient-information",
  "description": "The nutrient-information product type.",
  "attributes": [
    {
      "name": "quantityContained",
      "type": {
        "name": "number"
      },
      "isRequired": true,
      "attributeConstraint": "None",
      "isSearchable": false,
      "label": {
        "en": "quantity contained"
      }
    },
    {
      "name": "nutrientTypeCode",
      "type": {
        "name": "text"
      },
      "isRequired": true,
      "attributeConstraint": "None",
      "isSearchable": false,
      "label": {
        "en": "nutrient type Code"
      }
    }
  ]
}
 AttributeDefinition quantityContainedAttribute = AttributeDefinitionBuilder
         .of("quantityContained", en("quantity contained"), NumberAttributeType.of())
         .isRequired(true)
         .attributeConstraint(AttributeConstraint.NONE)
         .isSearchable(false)
         .build();
 AttributeDefinition nutrientTypeAttribute = AttributeDefinitionBuilder
         .of("nutrientTypeCode", en("nutrient type Code"), StringAttributeType.of())
         .isRequired(true)
         .attributeConstraint(AttributeConstraint.NONE)
         .isSearchable(false)
         .build();
 final ProductTypeDraft productTypeDraft =
         ProductTypeDraft.of(typeKey,
                 "nutrient-information",
                 "The nutrient-information product type.",
                 asList(quantityContainedAttribute, nutrientTypeAttribute));
 return client.execute(ProductTypeCreateCommand.of(productTypeDraft));
<?php
 $name = 'nutrient-information';
 $description = 'The nutrient-information product type.';
 $productTypeDraft = ProductTypeDraft::ofNameAndDescription($name, $description);
 $productTypeDraft->setAttributes(
     AttributeDefinitionCollection::of()
         ->add(
             AttributeDefinition::of()
                 ->setName('quantityContained')
                 ->setType(
                     AttributeType::of()->setName('number')
                 )
                 ->setIsRequired(true)
                 ->setIsSearchable(false)
                 ->setLabel(
                     LocalizedString::ofLangAndText('en', 'quantity contained')
                 )
         )
         ->add(
             AttributeDefinition::of()
                 ->setName('nutrientTypeCode')
                 ->setType(
                     AttributeType::of()->setName('text')
                 )
                 ->setIsRequired(true)
                 ->setIsSearchable(false)
                 ->setLabel(
                     LocalizedString::ofLangAndText('en', 'nutrient type Code')
                 )
         )
 );
 $request = ProductTypeCreateRequest::ofDraft($productTypeDraft);
 $response = $client->execute(ProductTypeCreateRequest::ofDraft($productTypeDraft));
 $productType = $request->mapResponse($response);

Create advanced Product Type

After we created the product type to be nested we can now create our advanced food-product-type by referencing the previously created nutrient-information product type in a nested attribute we call nutrients. Furthermore we add the attribute taste that gives a textual description about how excellent our food product tastes.

#!/bin/sh
curl -sH "Authorization: Bearer ACCESS_TOKEN" https://api.sphere.io/{project-key}/product-types -d @- << EOF
{
  "name": "food-product-type",
  "description": "The food product type.",
  "attributes": [
    {
      "name": "taste",
      "type": {
          "name": "text"
      },
      "isRequired": true,
      "attributeConstraint": "None",
      "isSearchable": false,
      "label": {
          "en": "taste"
      }
    },
    {
      "name": "nutrients",
      "type": {
          "name": "set",
          "elementType": {
              "name": "nested",
             "typeReference": {
                  "id": "<nutrient-information-product-type-id>",
                  "typeId": "product-type"
              }
          }
      },
      "isRequired": false,
      "attributeConstraint": "None",
      "isSearchable": false,
      "label": {
          "en": "food nutrients"
      }
    }
  ]
}
EOF
{
  "name": "food-product-type",
  "description": "The food product type.",
  "attributes": [
    {
      "name": "taste",
      "type": {
        "name": "text"
      },
      "isRequired": true,
      "attributeConstraint": "None",
      "isSearchable": false,
      "label": {
        "en": "taste"
      }
    },
    {
      "name": "nutrients",
      "type": {
        "name": "set",
        "elementType": {
          "name": "nested",
          "typeReference": {
            "id": "<nutrient-information-product-type-id>",
            "typeId": "product-type"
          }
        }
      },
      "isRequired": false,
      "attributeConstraint": "None",
      "isSearchable": false,
      "label": {
        "en": "food nutrients"
      }
    }
  ]
}
 AttributeDefinition tasteAttribute = AttributeDefinitionBuilder
         .of("taste", en("taste"), StringAttributeType.of())
         .isRequired(true)
         .attributeConstraint(AttributeConstraint.NONE)
         .isSearchable(false)
         .build();
 AttributeDefinition nutrientsAttribute = AttributeDefinitionBuilder
         .of("nutrients", en("food nutrients"), SetAttributeType.of(NestedAttributeType.of(nestedProductType)))
         //nestedProductType is a Referenceable<ProductType>
         .isRequired(false)
         .attributeConstraint(AttributeConstraint.NONE)
         .isSearchable(false)
         .build();
 final ProductTypeDraft productTypeDraft = ProductTypeDraft
         .of(typeKey, "food-product-type", "The food product type.", asList(tasteAttribute, nutrientsAttribute));
 return client.execute(ProductTypeCreateCommand.of(productTypeDraft));
<?php
 $name = 'food-product-type';
 $description = 'The food product type.';
 $productTypeDraft = ProductTypeDraft::ofNameAndDescription($name, $description);
 $productTypeDraft->setAttributes(
     AttributeDefinitionCollection::of()
         ->add(
             AttributeDefinition::of()
                 ->setName('taste')
                 ->setType(
                     AttributeType::of()->setName('text')
                 )
                 ->setIsRequired(true)
                 ->setIsSearchable(false)
                 ->setLabel(
                     LocalizedString::ofLangAndText('en', 'taste')
                 )
         )
         ->add(
             AttributeDefinition::of()
                 ->setName('nutrients')
                 ->setType(
                     SetType::of()
                         ->setElementType(
                             NestedType::of()->setTypeReference(
                                 ProductTypeReference::ofId('<nutrient-information-product-type-id>')
                             )
                         )
                 )
                 ->setIsRequired(false)
                 ->setIsSearchable(false)
                 ->setLabel(
                     LocalizedString::ofLangAndText('en', 'food nutrients')
                 )
         )
 );

 $request = ProductTypeCreateRequest::ofDraft($productTypeDraft);
 $response = $client->execute(ProductTypeCreateRequest::ofDraft($productTypeDraft));
 $productType = $request->mapResponse($response);

Create Product

We now have everything we need for creating an example-food-product of our food-product-type. The taste of our example-food-product is excellent of course, but we are also nesting following nutrient-information into our example-food-product: 1.4 units of FAT and 1.15 units of SUGAR. For our example we have reused the nutrient-information product type two times, but you can add more if you need since we defined the nutrients attribute of our food-product-type as set of nutrient-information.

Image Example Food Product

For doing this with the commercetools™ platform API you Create a Product with following AttributeDefinition:

#!/bin/sh
curl -sH "Authorization: Bearer ACCESS_TOKEN" https://api.sphere.io/{project-key}/products -d @- << EOF
{
  "productType": {
      "typeId":"product-type",
      "id":"<product-type-id>"
  },
  "name": {
    "en": "Product with kilo price"
  },
  "slug": {
    "en": "product_slug_kilo_price"
  },
  "masterVariant": {
    "sku": "SKU-kiloPrice",
    "prices": [
      {
        "value": {
          "currencyCode": "EUR",
          "centAmount": 100
        },
        "custom": {
            "typeId": "<customized-price-type-id>",
            "fields": {
                "kiloPrice": {
                    "currencyCode": "EUR",
                    "centAmount": 1000
                }
            }
        }
      }
    ]
  }
}
EOF
{
  //...
  "attributes": [
    {
      "name": "taste",
      "value": "excellent!"
    },
    {
      "name": "nutrients",
      "value": [
        [
          {
            "name": "quantityContained",
            "value": 1.4
          },
          {
            "name": "nutrientTypeCode",
            "value": "FAT"
          }
        ],
        [
          {
            "name": "quantityContained",
            "value": 1.15
          },
          {
            "name": "nutrientTypeCode",
            "value": "SUGAR"
          }
        ]
      ]
    }
  ]
  //...
}
 final AttributeDraft tasteAttributeDraft = AttributeDraft.of("taste", "excellent!");
 final AttributeDraft nutrientsAttributeDraft = AttributeDraft.of("nutrients",
         asSet(
                 asSet(
                         AttributeDraft.of("quantityContained", 1.4),
                         AttributeDraft.of("nutrientTypeCode", "FAT")
                 ),
                 asSet(
                         AttributeDraft.of("quantityContained", 1.15),
                         AttributeDraft.of("nutrientTypeCode", "SUGAR")
                 )
         ));
 final List<AttributeDraft> attributesList = asList(tasteAttributeDraft, nutrientsAttributeDraft);
 final ProductVariantDraft productVariantDraft = ProductVariantDraftBuilder.of()
         .sku(sku)
         .attributes(attributesList)
         .build();
 final ProductDraftBuilder productDraftBuilder = ProductDraftBuilder.of(productType, en("example-food-product"),
         en(slug), productVariantDraft);
 return client.execute(ProductCreateCommand.of(productDraftBuilder.publish(true).build()));
<?php
 $name = LocalizedString::ofLangAndText('en', 'example-food-product');
 $slug = LocalizedString::ofLangAndText('en', $slug);
 $productDraft = ProductDraft::ofTypeNameAndSlug(
     $productType->getReference(),
     $name,
     $slug
 );
 $productDraft->setMasterVariant(
     ProductVariantDraft::of()
         ->setSku($sku)
         ->setAttributes(
             AttributeCollection::of()
                 ->add(
                     Attribute::of()
                         ->setName('taste')->setValue('excellent!')
                 )
                 ->add(
                     Attribute::of()
                         ->setName('nutrients')
                         ->setValue(
                             Set::ofType(AttributeCollection::class)
                                 ->add(
                                     AttributeCollection::of()
                                         ->add(Attribute::of()->setName('quantityContained')->setValue(1.4))
                                         ->add(Attribute::of()->setName('nutrientTypeCode')->setValue('FAT'))
                                 )
                                 ->add(
                                     AttributeCollection::of()
                                         ->add(Attribute::of()->setName('quantityContained')->setValue(1.15))
                                         ->add(Attribute::of()->setName('nutrientTypeCode')->setValue('SUGAR'))
                                 )
                         )
                 )
         )
 );

 $request = ProductCreateRequest::ofDraft($productDraft);
 $response = $client->execute(ProductCreateRequest::ofDraft($productDraft));
 $product = $request->mapResponse($response);

Summary

What have we just done? Basically, we have defined a ProductType that we reused in another ProductType. This allows us the composition of advanced product types based on existing ones. The commercetools™ platform supports that by using its nested AttributeType as illustrated in the figure below:

Image Nested ProductType

The following JSON snippet shows how an existing ProductType is nested into an AttributeDefinition of another ProductType in the commercetools™ platform:

{
  "name": "nestedProductType",
  "type": {
    "name": "nested",
    "typeReference": {
      "id": "<nested-product-type-id>",
      "typeId": "product-type"
    }
  }
}
comments powered by Disqus