commercetools/spec-sdks

This package is abandoned and no longer maintained. The author suggests using the commercetools/commercetools-sdk package instead.

The official PHP SDK for the commercetools Composable Commerce APIs

10.10.0 2024-12-09 16:03 UTC

README

Index of Topics

Introduction

This repository contains the PHP SDK generated from the Composable Commerce API reference.

Client and Request Builder for making API requests against Commercetools.

Package and Installation

composer require commercetools/commercetools-sdk

Technical Overview

The SDK consists of the following projects:

  • lib/commercetools-base/src: Contains Client which communicate with Composable Commerce to execute requests, it contains also the classes related to the client like tokens, middlewares and handlers, and mappers and exceptions.
  • lib/commercetools-api/src: Contains all generated models and request builders to communicate with Composable Commerce HTTP API.
  • lib/commercetools-import/src: Contains all generated models and request builders to communicate with the Import API.
  • lib/commercetools-history/src: Contains all generated models and request builders to communicate with the Change History API.

In addition, the SDK has the following directories:

  • examples/ : One Dockerized Symfony app per APM (New Relic, Datadog, Dynatrace, OpenTelemetry) to demo PHP SDK usage.
  • test/integration : Integration Tests for the SDK. A good way for anyone using the PHP SDK to understand it further.
  • test/unit : Unit Tests for
  • lib/commercetools-api-tests : generated unit test for each class for the api folder
  • lib/commercetools-history-tests : generated unit test for each class for the history folder
  • lib/commercetools-import-tests : generated unit test for each class for the import folder

The PHP SDK utilizes various standard interfaces and components to ensure consistency and interoperability:

Placeholder values

Example code in this guide uses placeholders that should be replaced with the following values.

If you do not have an API Client, follow our Get your API Client guide.

Getting Started

Client Creation

The example below shows how to create a client with customized URIs passed in the creation of the Client itself. You will find the same classes in the Import API folder.

namespace Commercetools;

use Commercetools\Api\Client\ClientCredentialsConfig;
use Commercetools\Api\Client\Config;
use Commercetools\Client\ClientCredentials;
use Commercetools\Client\ClientFactory;

require_once __DIR__ . '/vendor/autoload.php';

/** @var string $clientId */
/** @var string $clientSecret */
/** @var string $scope
 *   Provide the scope when you want to request a specific ones for the client. 
 *   Can be omitted to use all scopes of the oauth client.
 *   Format: `<the scope name>:<the project key>`.
 *   Example: `manage_products:project1`. $authConfig 
 */
$authConfig = new ClientCredentialsConfig(
    new ClientCredentials('{clientID}', '{clientSecret}', '{scope}'),
    [],
    'https://auth.{region}.commercetools.com/oauth/token'
);
$client = ClientFactory::of()->createGuzzleClient(
    new Config([], 'https://api.{region}.commercetools.com'),
    $authConfig
);

Customize Endpoint for different regions

By default, the library uses api.europe-west1.gcp.commercetools.com endpoint. If you use a different region, you can configure the client to use a custom endpoint. Here is an example for the us-central1 region:

$authConfig = new ClientCredentialsConfig(
    new ClientCredentials('{clientId}', '{clientSecret}'), 
    [], 
    'https://auth.us-central1.gcp.commercetools.com/oauth/token'
);

$config = new Config([], 'https://api.us-central1.gcp.commercetools.com');
$client = ClientFactory::of()->createGuzzleClient(
    $config,
    $authConfig,
);

Note that the auth endpoint should contain the /oauth/token suffix, but the API endpoint - don't.

Performing Requests

Detailed information of all available methods for the product API can be found here

Information for the Import API can be found here.

Examples to retrieve project information

use Commercetools\Api\Client\ApiRequestBuilder;
use GuzzleHttp\ClientInterface;

/** @var ClientInterface $client */
$builder =  new ApiRequestBuilder($client);
$request = $builder->withProjectKey('{projectKey}')->get();

To avoid specifying the project key for every request built it's possible to use the ones in the Commercetools\Client namespace instead

use Commercetools\Client\ApiRequestBuilder;
use Commercetools\Client\ImportRequestBuilder;
use GuzzleHttp\ClientInterface;

/** @var ClientInterface $client */
$builder =  new ApiRequestBuilder('{projectKey}', $client);
$request = $builder->categories()->get();

$importBuilder =  new ImportRequestBuilder('{projectKey}', $client);
$request = $importBuilder->importSinks()->get();

These are some examples about how to execute a request:

use Commercetools\Client\ApiRequestBuilder;
use GuzzleHttp\ClientInterface;

/** @var ClientInterface $client */
$builder =  new ApiRequestBuilder('{projectKey}', $client);
$request = $builder->with()->get();

// executing the request and mapping the response directly to a domain model
$project = $request->execute();

// send the request to get the response object 
$response = $request->send();
// map the response to a domain model
$project = $request->mapFromResponse($response);

// send the request asynchronously 
$promise = $request->sendAsync();
// map the response to a domain model
$project = $request->mapFromResponse($promise->wait());

// send the request using a client instance
$response = $client->send($request);
$project = $request->mapFromResponse($response);

Configuration

Applying PSRs

The PHP SDK utilizes various standard interfaces and components to ensure consistency and interoperability:

$authHandler = HandlerStack::create();
$authHandler->push(
    MiddlewareFactory::createLoggerMiddleware(new Logger('auth', [new StreamHandler('./logs/requests.log')]))
);
$authConfig = new ClientCredentialsConfig(new ClientCredentials($clientId, $clientSecret), [
    'handler' => $authHandler,
]);
$logger = new Logger('client', [new StreamHandler('./logs/requests.log')]);
$client = ClientFactory::of()->createGuzzleClientForHandler(
    new Config(['maxRetries' => 3]),
    OAuthHandlerFactory::ofAuthConfig($authConfig),
    $logger
);
use Symfony\Component\Cache\Adapter\FilesystemAdapter;
$filesystemCache = new FilesystemAdapter();

$config = new Config(['timeout' => 30]);
$client = ClientFactory->createGuzzleClientForHandler(
    $config,
    OAuthHandlerFactory::ofAuthConfig($authConfig, $cache)
);
//set up the client something like the examples before

// create a guzzle request
/** @var CategoryBuilder $category */
$request = $client->with()->categories()->withId($category->getId())->get()->withExpand('parent');
$result = $request->execute();

$request = new \GuzzleHttp\Psr7\Request('GET', '{projectKey}/categories/{ID}');
$response = $client->send($request);
use Symfony\Component\Cache\Simple\FilesystemCache;
use Symfony\Component\Cache\Psr16Cache;

$filesystemCache = new FilesystemAdapter();
$cache = new Psr16Cache($filesystemCache);

$config = new Config(['timeout' => 30]);
$client = ClientFactory->createGuzzleClientForHandler(
    $config,
    OAuthHandlerFactory::ofAuthConfig($authConfig, $cache)
);

Error Handling

The error handle is already provided with the ExceptionFactory class. The methods contained in this class encapsulate the logic for converting Guzzle exceptions into your custom exceptions based on the HTTP response status codes. Which can be called directly in the request or can be called by a dedicated middleware for the error handling. Direct invocation in a request handling or directly handled in a middleware:

    if ($e->getCode() >= 500) {
        throw ExceptionFactory::createServerException($e, $apiRequest, $response, $result);
    } else {
        throw ExceptionFactory::createClientException($e, $apiRequest, $response, $result);
    }

Authentication

The factory class ProviderFactory is for managing authentication and token handling.

Token Storage Creation

To generate a TokenStorageProvider that manages tokens using different flows: Refresh Flow and Anonymous Flow, you can use ProviderFactory::createTokenStorageProvider($anonTokenUrl, $refreshTokenUrl, $clientCredentials, $client, $tokenStorage, $anonymousIdProvider);.

Password Flow

The ProviderFactory::createPasswordFlowProvider($passwordTokenUrl, $clientCredentials, $client, $tokenStorage); method, creates a PasswordFlowTokenProvider for authenticating users with username and password, acquiring tokens securely.

Anonymous Flow

The createAnonymousFlowProvider($anonTokenUrl, $clientCredentials, $client, $refreshFlowTokenProvider, $anonymousIdProvider); method builds an AnonymousFlowTokenProvider to manage tokens for anonymous users, integrating with the API's anonymous token endpoint.

Refresh Flow

The createRefreshFlowProvider($refreshTokenUrl, $clientCredentials, $client, $tokenStorage) method sets up a RefreshFlowTokenProvider to handle token refresh operations seamlessly, ensuring continuous access to API resources.

Middlewares

We introduced middleware to add functionalities to the requests and the responses in the PHP SDK.

You can add middleware when creating the PHP SDK client. Multiple middlewares can be added using the array of middlewares.

The scope of the MiddlewareFactory which is a Factory pattern is to handle all the available middleware and have the chance to have them customized.

The methods that are contained in this class, are meant to create an array of middlewares.

DefaultMiddleware

The method createDefaultMiddlewares creates an array with default values of OAuth Handler, Authentication, Logger, Retry and Correlation ID.

$authConfig = new ClientCredentialsConfig(new ClientCredentials($clientId, $clientSecret), [
            'handler' => $authHandler,
        ]);
$oauthHandler = OAuthHandlerFactory::ofAuthConfig($authConfig),
$logger = new Logger('client', [new StreamHandler('./logs/requests.log')]);
$maxRetries = 3;
$correlationIdProvider = new DefaultCorrelationIdProvider();

$middlewares = MiddlewareFactory::createDefaultMiddlewares(
    $oauthHandler,
    $logger,
    $maxRetries,
    $correlationIdProvider
);

CorrelationIdMiddleware

The method createCorrelationIdMiddleware creates a middleware that adds a correlation ID to the headers of HTTP requests.

$correlationIdProvider = new DefaultCorrelationIdProvider();

$correlationIdMiddleware = MiddlewareFactory::createCorrelationIdMiddleware(
    $correlationIdProvider
);

RetryNAMiddleware

The method createRetryNAMiddleware is designed to create middleware that retries HTTP requests under certain conditions are met. This middleware is particularly useful in scenarios where transient errors, such as temporary server unavailability, may occur.

$maxRetries = 3;

$retryMiddleware = MiddlewareFactory::createRetryNAMiddleware($maxRetries);

OAuthHandlerMiddleware

The method createMiddlewareForOAuthHandler creates a middleware for handling OAuth2 authentication ensuring to include the necessary OAuth credentials.

$tokenProvider = new YourTokenProvider();
$oauthHandler = OAuthHandlerFactory::ofProvider($tokenProvider),

$oauthMiddleware = MiddlewareFactory::createMiddlewareForOAuthHandler($oauthHandler);

LoggerMiddleware

The method createLoggerMiddleware creates a middleware for logging HTTP requests and responses.

$logger = new Logger('auth');
$logger->pushHandler(new StreamHandler('./logs/requests.log', Logger::DEBUG));

$loggerMiddleware = MiddlewareFactory::createLoggerMiddleware($logger);

ReauthenticateMiddleware

The method createReauthenticateMiddleware creates a middleware that automatically reauthenticates HTTP requests when an invalid token error (HTTP 401) is encountered. It uses an OAuth2Handler to refresh the token and retry the request up to a specified number of times.

$authConfig = new ClientCredentialsConfig(new ClientCredentials($clientId, $clientSecret), [
            'handler' => $authHandler,
        ]);
$oauthHandler = OAuthHandlerFactory::ofAuthConfig($authConfig),
//maxRetries have the default value 1 as a second parameter of the function
$reauthMiddleware = MiddlewareFactory::createReauthenticateMiddleware($oauthHandler);

Querying

For the examples that we are mentioning below we are setting the $builder like here:

use Commercetools\Client\ApiRequestBuilder;
use GuzzleHttp\ClientInterface;

/** @var ClientInterface $client */
$builder =  new ApiRequestBuilder('{projectKey}', $client);

Since the most of the variables to pass in the with() method are scalars, this means that we can pass arrays in the related parameter of the method like in the examples below.

Predicates

The system allows the use of predicates when querying the API. Predicates are added as query parameter string to the request itself. The following example shows the usage of input variables:

$builder
    ->customers()
    ->get()
    ->withWhere('lastName=:lastName')
    ->withPredicateVar("lastName", $customerSignIn->getCustomer()->getLastName());

It's also possible to use array values in predicates in case of a varying number of parameters.

$builder
    ->productProjections()
    ->get()
    ->withWhere('masterVariant(sku in :skus)')
    ->withPredicateVar("skus", ["foo", "bar"]);

Get By Id/Key

$builder
    ->productProjections()
    ->withId('test_id')
    ->get();
$builder
    ->productProjections()
    ->withKey('test_key')
    ->get();

Sorting

See Sort for details.

Sorting using one parameter:

$builder
    ->products()
    ->get()
    ->withSort("masterData.current.name.en asc");

Sorting using multiple parameters:

$builder
    ->products()
    ->get()
    ->withSort(["masterData.current.name.en asc", "id asc"]);

Pagination

Limiting the number of the returned documents or page size:

$builder
    ->products()
    ->get()
    ->withLimit(4)
    ->withOffset(4);

Products and ProductTypes

ProductType Creation

A ProductType is like a schema that defines how the product attributes are structured.

ProductType contains a list of AttributeDefinition which corresponds to the name and type of each attribute, along with some additional information. Each name/type pair must be unique across a Project, so if you create an attribute "foo" of type String, you cannot create another ProductType where "foo" has another type (e.g. LocalizedString). If you do it anyway you get an error message like:

"The attribute with name 'foo' has a different type on product type 'exampleproducttype'."

In this scenario we provide two ProductTypes book and t-shirt.

The book product type contains the following attributes:

$isbn as String, International Standard Book Number The t-shirt product type contains the following attributes:

$color as AttributeLocalizedEnumValue with the colors green and red and their translations in German and English. $size as AttributePlainEnumValue with S, M and X. $laundrySymbols as set of AttributeLocalizedEnumValue with temperature and tumble drying. $matchingProducts as set of ProductReference, which can point to products that are similar to the current product. $rrp as Money containing the recommended retail price. $availableSince as DateTime which contains the date since when the product is available for the customer in the shop. All available attribute types you can find here: AttributeType in "All Known Implementing Classes".

The code for the creation of the book ProductType:

$isbn = AttributeDefinitionBuilder::of()
    ->withType(AttributeTextTypeBuilder::of()->build())
    ->withName(self::ISBN_ATTR_NAME)
    ->withLabel(LocalizedStringBuilder::of("ISBN")->build())
    ->withIsRequired(false)
    ->build();

$productType = ProductTypeBuilder::of()
    ->withName(self::BOOK_PRODUCT_TYPE_NAME)
    ->withDescription("books")
    ->withAttributes(AttributeDefinitionCollection::of()->add($isbn))
    ->build();
    
$builder =  new ApiRequestBuilder('{projectKey}', $client);
$request = $builder
              ->productTypes()
              ->withId($productType->getId())
              ->get();
$productTypeQueryResponse = $request->execute();

See the Test Code

The code for the creation of the t-shirt ProductType:

$green = AttributeLocalizedEnumValueBuilder::of()
                    ->withKey("green")
                    ->withLabel(LocalizedStringBuilder::fromArray(["en" => "green", "de" => "grün"])->build())
                    ->build();
$red = AttributeLocalizedEnumValueBuilder::of()
            ->withKey("red")
            ->withLabel(LocalizedStringBuilder::fromArray(["en" => "red", "de" => "rot"])->build())
            ->build();
$color = AttributeDefinitionDraftBuilder::of()
            ->withName(self::COLOR_ATTR_NAME)
            ->withLabel(LocalizedStringBuilder::fromArray(["en" => "color"])->build())
            ->withType(AttributeLocalizedEnumTypeBuilder::of()
                        ->withValues(AttributeLocalizedEnumValueCollection::fromArray([$green, $red]))
                        ->build())
            ->withIsRequired(true)
            ->build();
$small = AttributePlainEnumValueBuilder::of()
            ->withKey("S")
            ->withLabel("S")
            ->build();
$medium = AttributePlainEnumValueBuilder::of()
            ->withKey("M")
            ->withLabel("M")
            ->build();
$sizeX = AttributePlainEnumValueBuilder::of()
            ->withKey("X")
            ->withLabel("X")
            ->build();
$size = AttributeDefinitionDraftBuilder::of()
            ->withName(self::SIZE_ATTR_NAME)
            ->withLabel(LocalizedStringBuilder::fromArray(["en" => "Size"])->build())
            ->withType(AttributeEnumTypeBuilder::of()
                            ->withValues(AttributePlainEnumValueCollection::fromArray([$small, $medium, $sizeX]))
                            ->build())
            ->withIsRequired(true)
            ->build();
$cold = AttributeLocalizedEnumValueBuilder::of()
            ->withKey("cold")
            ->withLabel(LocalizedStringBuilder::fromArray(["en" => "Wash at or below 30°C ", "de" => "30°C"])->build())
            ->build();
$hot = AttributeLocalizedEnumValueBuilder::of()
            ->withKey("hot")
            ->withLabel(LocalizedStringBuilder::fromArray(["en" => "Wash at or below 60°C", "de" => "60°C"])->build())
            ->build();
$tumbleDrying = AttributeLocalizedEnumValueBuilder::of()
                    ->withKey("tumbleDrying")
                    ->withLabel(LocalizedStringBuilder::fromArray(["en" => "Tumble Drying", "de" => "Trommeltrocknen"])->build())
                    ->build();
$noTumbleDrying = AttributeLocalizedEnumValueBuilder::of()
                    ->withKey("noTumbleDrying")
                    ->withLabel(LocalizedStringBuilder::fromArray(["en" => "no tumble drying", "de" => "Nicht im Trommeltrockner trocknen"])->build())
                    ->build();
$laundryLabelType = AttributeSetTypeBuilder::of()
                        ->withElementType(AttributeLocalizedEnumTypeBuilder::of()
                                            ->withValues(AttributeLocalizedEnumValueCollection::fromArray([$cold, $hot, $tumbleDrying, $noTumbleDrying]))
                                            ->build())
                        ->build();
$laundrySymbols = AttributeDefinitionDraftBuilder::of()
                    ->withType($laundryLabelType)
                    ->withName(self::LAUNDRY_SYMBOLS_ATTR_NAME)
                    ->withLabel(LocalizedStringBuilder::fromArray(["en" => "washing labels"])->build())
                    ->withIsRequired(false)
                    ->build();

$matchingProducts = AttributeDefinitionDraftBuilder::of()
                        ->withName(self::MATCHING_PRODUCTS_ATTR_NAME)
                        ->withLabel(LocalizedStringBuilder::fromArray(["en" => "matching products"])->build())
                        ->withType(AttributeSetTypeBuilder::of()
                                    ->withElementType(AttributeReferenceTypeBuilder::of()
                                                        ->withReferenceTypeId("product")
                                                        ->build())
                                    ->build())
                        ->withIsRequired(false)
                        ->build();
$rrp = AttributeDefinitionDraftBuilder::of()
            ->withName(self::RRP_ATTR_NAME)
            ->withLabel(LocalizedStringBuilder::fromArray(["en" => "recommended retail price"])->build())
            ->withType(AttributeMoneyTypeBuilder::of()->build())
            ->withIsRequired(false)
            ->build();
$availableSince = AttributeDefinitionDraftBuilder::of()
                    ->withName(self::AVAILABLE_SINCE_ATTR_NAME)
                    ->withLabel(LocalizedStringBuilder::fromArray(["en" => "available since"])->build())
                    ->withType(AttributeDateTimeTypeBuilder::of()->build())
                    ->withIsRequired(false)
                    ->build();
$attributes = AttributeDefinitionDraftCollection::fromArray([$color, $size, $laundrySymbols, $matchingProducts, $rrp, $availableSince]);

$productTypeDraft = ProductTypeDraftBuilder::of()
                      ->withKey(ProductTypeFixture::uniqueProductTypeString())
                      ->withName(self::PRODUCT_TYPE_NAME)
                      ->withDescription("a 'T' shaped cloth")
                      ->withAttributes($attributes)
                      ->build();
$productType = $builder
    ->with()
    ->productTypes()
    ->post($productTypeDraft)
    ->execute();

See the Test Code

ProductTypes have a key (String) which can be used as key to logically identify ProductTypes. The key has an unique constraint.

Product Creation

To create a product you need to reference the product type. Since the ProductType ID of the development system will not be the ID of the production system it is necessary to find the product type by name:

$productType = $builder
            ->with()
            ->productTypes()
            ->get()
            ->withQueryParam('where', 'name="' . $name . '"')
            ->execute();

return $productType->getResults()->current() ?: null;

See the Test Code

The simplest way of adding attributes to a ProductVariant is to use php ProductVariantDraftBuilder::of()->withAttributes($attributes) which enables you to directly put the value of the attribute to the draft. But it cannot check if you put the right objects and types in it.

A book example:

$attributes = AttributeCollection::of()
                ->add(
                    AttributeBuilder::of()
                        ->withName(self::ISBN_ATTR_NAME)
                        ->withValue("978-3-86680-192-9")
                        ->build());
$productVariantDraft = ProductVariantDraftBuilder::of()
                        ->withAttributes($attributes)
                        ->build();
$productTypeResourceIdentifier = ProductTypeResourceIdentifierBuilder::of()
                                    ->withId($productType->getId())
                                    ->build();
$productDraft = ProductDraftBuilder::of()
                ->withProductType($productTypeResourceIdentifier)
                ->withName(LocalizedStringBuilder::of()->put("en", "a book")->build())
                ->withSlug(LocalizedStringBuilder::of()->put("en", ProductTypeFixture::uniqueProductTypeString())->build())
                ->withMasterVariant($productVariantDraft)
                ->build();

$product = $builder->products()
    ->post($productDraft)
    ->execute();

See the Test Code

A T-shirt example:

$referenceableProduct = ProductFixture::referenceableProduct($builder);
$productType = ProductTypeFixture::fetchProductTypeByName($builder, self::PRODUCT_TYPE_NAME);

if (!$productType) {
    $productType = ProductTypeFixture::createProductType($builder, self::PRODUCT_TYPE_NAME);
}

$productReference = ProductReferenceBuilder::of()->withId($referenceableProduct->getId())->build();
$datetime = new \DateTime('2015-02-02');
$datetime = $datetime->format(\DateTime::ATOM);
$attributes = AttributeCollection::of()
    ->add(AttributeBuilder::of()->withName(self::COLOR_ATTR_NAME)->withValue("green")->build())
    ->add(AttributeBuilder::of()->withName(self::SIZE_ATTR_NAME)->withValue("S")->build())
    ->add(AttributeBuilder::of()->withName(self::LAUNDRY_SYMBOLS_ATTR_NAME)->withValue(["cold", "tumbleDrying"])->build())
    ->add(AttributeBuilder::of()->withName(self::RRP_ATTR_NAME)->withValue(MoneyBuilder::of()->withCentAmount(300)->withCurrencyCode("EUR")->build())->build())
    ->add(AttributeBuilder::of()->withName(self::AVAILABLE_SINCE_ATTR_NAME)->withValue($datetime)->build())
    ->add(AttributeBuilder::of()->withName(self::MATCHING_PRODUCTS_ATTR_NAME)->withValue([$productReference])->build());
$productVariantDraft = ProductVariantDraftBuilder::of()
    ->withAttributes($attributes)
    ->build();
$productTypeResourceIdentifier = ProductTypeResourceIdentifierBuilder::of()
    ->withId($productType->getId())
    ->build();
$productDraft = ProductDraftBuilder::of()
    ->withProductType($productTypeResourceIdentifier)
    ->withKey(ProductFixture::uniqueProductString())
    ->withName(LocalizedStringBuilder::of()->put('en', 'basic shirt')->build())
    ->withSlug(LocalizedStringBuilder::of()->put('en', ProductFixture::uniqueProductString())->build())
    ->withMasterVariant($productVariantDraft)
    ->build();

$product = $builder->products()
    ->post($productDraft)
    ->execute();

See the Test Code

A wrong value for a field or an invalid type will cause a BadRequestException with an error code of "InvalidField".

$productType = $builder->productTypes()
            ->post($productTypeDraft)
            ->execute();
$productVariantDraft  = ProductVariantDraftBuilder::of()
    ->withAttributes(AttributeCollection::of()
        ->add(AttributeBuilder::of()
                ->withName(self::COLOR_ATTR_NAME)
                ->withValue(1) //1 is of illegal type and of illegal key
                ->build()))
    ->build();
$productTypeResourceIdentifier = ProductTypeResourceIdentifierBuilder::of()
    ->withId($productType->getId())
    ->build();
$productDraft = ProductDraftBuilder::of()
    ->withProductType($productTypeResourceIdentifier)
    ->withName(LocalizedStringBuilder::of()->put("en", "basic shirt")->build())
    ->withSlug(LocalizedStringBuilder::of()->put("en", ProductTypeFixture::uniqueProductTypeString())->build())
    ->withMasterVariant($productVariantDraft)

See the Test Code

As alternative, you could declare your attributes at the same place and use these to read and write attribute values:

$green = AttributeLocalizedEnumValueBuilder::of()
            ->withKey("green")
            ->withLabel(LocalizedStringBuilder::of()->put("en", "green ")->put("de", "grün")->build())
            ->build();
$cold = AttributeLocalizedEnumValueBuilder::of()
    ->withKey("cold")
    ->withLabel(LocalizedStringBuilder::of()->put("en", "Wash at or below 30°C ")->put("de", "30°C")->build())
    ->build();
$tumbleDrying = AttributeLocalizedEnumValueBuilder::of()
    ->withKey("tumbleDrying")
    ->withLabel(LocalizedStringBuilder::of()->put("en", "tumble drying")->put("de", "Trommeltrocknen")->build())
    ->build();
$productReference = ProductReferenceBuilder::of()->withId($referenceableProduct->getId())->build();

$attributes = AttributeCollection::of()
    ->add(AttributeBuilder::of()->withName(self::COLOR_ATTR_NAME)->withValue("green")->build())
    ->add(AttributeBuilder::of()->withName(self::SIZE_ATTR_NAME)->withValue("S")->build())
    ->add(AttributeBuilder::of()->withName(self::LAUNDRY_SYMBOLS_ATTR_NAME)->withValue(["cold", "tumbleDrying"])->build())
    ->add(AttributeBuilder::of()->withName(self::RRP_ATTR_NAME)->withValue(MoneyBuilder::of()->withCentAmount(300)->withCurrencyCode("EUR")->build())->build())
    ->add(AttributeBuilder::of()->withName(self::AVAILABLE_SINCE_ATTR_NAME)->withValue($datetime)->build())
    ->add(AttributeBuilder::of()->withName(self::MATCHING_PRODUCTS_ATTR_NAME)->withValue([$productReference])->build());
$productVariantDraft = ProductVariantDraftBuilder::of()
    ->withAttributes($attributes)
    ->build();
$productTypeResourceIdentifier = ProductTypeResourceIdentifierBuilder::of()
    ->withId($productType->getId())
    ->build();
$productDraft = ProductDraftBuilder::of()
    ->withProductType($productTypeResourceIdentifier)
    ->withKey(ProductFixture::uniqueProductString())
    ->withName(LocalizedStringBuilder::of()->put('en', 'basic shirt')->build())
    ->withSlug(LocalizedStringBuilder::of()->put('en', ProductFixture::uniqueProductString())->build())
    ->withMasterVariant($productVariantDraft)
    ->build();
$product = $builder->products()
    ->post($productDraft)
    ->execute();

$masterVariant = $product->getMasterData()->getStaged()->getMasterVariant();
foreach ($masterVariant->getAttributes() as $attribute) {
    if ($attribute->getName() === self::COLOR_ATTR_NAME) {
        assertEquals($attribute->getValue()->key, "green");
    }
    if ($attribute->getName() === self::SIZE_ATTR_NAME) {
        assertEquals($attribute->getValue()->key, "S");
    }

See the Test Code

Reading Attributes

The simplest way to get the value of the attribute is to use getValue() methods of Attribute, like php $attribute->getValue():

$product = $this->createProduct();
$masterVariant = $product->getMasterData()->getStaged()->getMasterVariant();
foreach ($masterVariant->getAttributes() as $attribute) {
    if ($attribute->getName() === self::SIZE_ATTR_NAME) {
        assertEquals($attribute->getValue()->key, "S");
    }
}

See the Test Code

You might also use the php getValueAs() method as a conversion for the attribute, like you have a EnumValue but extract it as boolean because these methods cast the values passed:

$product = $builder->products()->post($productDraft)->execute();
$masterVariant = $product->getMasterData()->getStaged()->getMasterVariant();

$result = null;
foreach ($masterVariant->getAttributes() as $attribute) {
    if ($attribute->getName() === self::SIZE_ATTR_NAME) {
        /** @var AttributeAccessor $attrAccessor */
        $attrAccessor = $attribute->with(AttributeAccessor::of());

        $result = $attrAccessor->getValueAsBool();
    }
}

$this->assertIsBool($result);

See the Test Code

Update attribute values of a product

Here below some examples about setting attribute values is like a product creation:

Example for books:

$product = $this->createBookProduct();
$masterVariantId = 1;
$productUpdate = ProductUpdateBuilder::of()
    ->withVersion($product->getVersion())
    ->withActions(
        ProductUpdateActionCollection::fromArray([
            ProductSetAttributeActionBuilder::of()
                ->withVariantId($masterVariantId)
                ->withName(self::ISBN_ATTR_NAME)
                ->withValue("978-3-86680-192-8")
                ->build()
        ])
        )->build();

$productUpdated = $builder
    ->products()
    ->withId($product->getId())
    ->post($productUpdate)
    ->execute();
$masterVariant = $productUpdated->getMasterData()->getStaged()->getMasterVariant();
$attribute = ProductTypeFixture::findAttributes($masterVariant->getAttributes(), self::ISBN_ATTR_NAME);

assertEquals($attribute->getValue(), "978-3-86680-192-8");

See the Test Code

Example for T-shirts:

$masterVariantId = 1;
$productUpdatedAction = ProductUpdateBuilder::of()
    ->withVersion($product->getVersion())
    ->withActions(
        ProductUpdateActionCollection::fromArray([
            ProductSetAttributeActionBuilder::of()
                ->withVariantId($masterVariantId)
                ->withName(self::COLOR_ATTR_NAME)
                ->withValue("red")
                ->build(),
            ProductSetAttributeActionBuilder::of()
                ->withVariantId($masterVariantId)
                ->withName(self::SIZE_ATTR_NAME)
                ->withValue("M")
                ->build(),
            ProductSetAttributeActionBuilder::of()
                ->withVariantId($masterVariantId)
                ->withName(self::LAUNDRY_SYMBOLS_ATTR_NAME)
                ->withValue(["cold"])
                ->build(),
            ProductSetAttributeActionBuilder::of()
                ->withVariantId($masterVariantId)
                ->withName(self::RRP_ATTR_NAME)
                ->withValue(MoneyBuilder::of()->withCurrencyCode("EUR")->withCentAmount(2000)->build())
                ->build(),
        ])
    )->build();
$productUpdated = $builder
    ->with()
    ->products()
    ->withId($product->getId())
    ->post($productUpdatedAction)
    ->execute();

$attributesUpdatedProduct = $productUpdated->getMasterData()->getStaged()->getMasterVariant()->getAttributes();

self::assertEquals(ProductTypeFixture::findAttribute($attributesUpdatedProduct, self::SIZE_ATTR_NAME)->getValue()->key, "M");
self::assertEquals(ProductTypeFixture::findAttribute($attributesUpdatedProduct, self::COLOR_ATTR_NAME)->getValue()->key, "red");
self::assertEquals(ProductTypeFixture::findAttribute($attributesUpdatedProduct, self::LAUNDRY_SYMBOLS_ATTR_NAME)->getValue()[0]->key, "cold");
self::assertEquals(ProductTypeFixture::findAttribute($attributesUpdatedProduct, self::RRP_ATTR_NAME)->getValue()->centAmount, 2000);

See the Test Code

Create attributes for importing orders

Importing attribute values for orders works different from updating products. In orders you provide the full value for enum-like types instead of just the key as done for all other types. This makes it possible to create a new enum value on the fly. The other attributes behave as expected.

Example:

$product = $this->createProduct($builder);
$attributes = AttributeCollection::of()
    ->add(AttributeBuilder::of()->withName(self::COLOR_ATTR_NAME)->withValue("yellow")->build())
    ->add(AttributeBuilder::of()->withName(self::RRP_ATTR_NAME)->withValue(MoneyBuilder::of()->withCurrencyCode("EUR")->withCentAmount(30)->build())->build());

$productVariantImportDraft = ProductVariantImportDraftBuilder::of()
                                ->withId(1)
                                ->withAttributes($attributes)
                                ->build();
$lineItemImportDraft = LineItemImportDraftBuilder::of()
    ->withProductId($product->getId())
    ->withVariant($productVariantImportDraft)
    ->withQuantity(1)
    ->withPrice(ProductFixture::priceDraft())
    ->withName(LocalizedStringBuilder::of()->put("en", "product name")->build())
    ->build();
$orderImportDraft = OrderImportDraftBuilder::of()
    ->withLineItems(LineItemImportDraftCollection::of()->add($lineItemImportDraft))
    ->withTotalPrice(MoneyBuilder::of()->withCentAmount(20)->withCurrencyCode("EUR")->build())
    ->withOrderState(OrderState::COMPLETE)
    ->build();
$order = $builder->orders()
    ->importOrder()
    ->post($orderImportDraft)
    ->execute();
    
$productVariant = $order->getLineItems()->current()->getVariant();
$colorAttribute = ProductTypeFixture::findAttribute($productVariant->getAttributes(), self::COLOR_ATTR_NAME);
assertEquals("yellow", $colorAttribute->getValue());
$rrpAttribute = ProductTypeFixture::findAttribute($productVariant->getAttributes(), self::RRP_ATTR_NAME);
assertEquals(30, $rrpAttribute->getValue()->centAmount);

See the Test Code

Serialization

In the PHP SDK some classes implement the JsonSerializable interface, and they have a customized jsonSerialize() method to convert the instance of the class to a JSON string easily. This mean that when the method json_encode() will be called, the object will be correctly converted and formatted to a JSON string.

See the example below:

$messagePayload = new MessageDeliveryPayloadModel(
    "{projectKey}",
    null, // Replace with an actual Reference object if needed
    null, // Replace with an actual UserProvidedIdentifiers object if needed
    "uniqueId456", // ID
    1, // The version
    new DateTimeImmutable("2024-08-06T12:34:56+00:00"), // CreatedAt
    new DateTimeImmutable("2024-08-06T12:34:56+00:00"), // LastModifiedAt
    42, // SequenceNumber
    1, // Resource version
    null, // Replace with an actual PayloadNotIncluded object if needed
    "Message" // notification type
);

$messagePayloadJSON = json_encode($messagePayload);

Migration Guidelines from SDK v1

To migrate from the 1.x to the 2.x, there is a guideline below:

Observability

To monitor and observe the SDK, see the official documentation Observability, there is a Demo application which shows how to monitor the PHP SDK with New Relic, Datadog, Dynatrace and Open Telemetry.

To monitor and observe the SDK, refer to the official documentation on Observability.

The Demo application demonstrates how to monitor the PHP SDK with the following APMs:

  • New Relic
  • Datadog
  • Dynatrace
  • OpenTelemetry (configured to work with New Relic for distributed tracing) Each APM integration is implemented through configuration and can be easily enabled using the provided instructions in the demo app for each platform.

Documentation

License

MIT