commercetools-8.x-1.2-alpha1/src/CommercetoolsCustomers.php
src/CommercetoolsCustomers.php
<?php
namespace Drupal\commercetools;
use Commercetools\Exception\InvalidArgumentException;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Cache\CacheTagsInvalidatorInterface;
use Drupal\Core\Cache\UseCacheBackendTrait;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Field\BaseFieldDefinition;
use Drupal\Core\Field\FieldStorageDefinitionInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\Site\Settings;
use Drupal\user\UserInterface;
use GraphQL\Actions\Mutation;
use GraphQL\Actions\Query;
use GraphQL\Entities\Node;
use GraphQL\Entities\Variable;
use Symfony\Component\DependencyInjection\Exception\LogicException;
/**
* The Commercetools customers service.
*/
class CommercetoolsCustomers {
use UseCacheBackendTrait;
const CACHE_CID_PREFIX_CUSTOMER_ID = 'commercetools_customer_id:';
const CACHE_TAG_CUSTOMER_ID = 'commercetools_customer_id:';
const CACHE_TAG_CUSTOMER_LIST = 'commercetools_customer_list';
const SHIPPING_ADDRESS_KEY = 'shippingAddress';
const BILLING_ADDRESS_KEY = 'billingAddress';
const ADDRESSES_MULTIPLE_KEYS = [
self::SHIPPING_ADDRESS_KEY => 'shippingAddresses',
self::BILLING_ADDRESS_KEY => 'billingAddresses',
];
const ADDRESS_FIELDS = [
'id',
'key',
'title',
'country',
'state',
'city',
'postalCode',
'streetName',
'streetNumber',
'phone',
'firstName',
'lastName',
];
/**
* The current customer id.
*
* @var string
*/
protected string|null $currentCustomerId;
/**
* CommercetoolsContentCustomers constructor.
*
* @param \Drupal\Core\Session\AccountInterface $user
* The current user.
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entityTypeManager
* The entity type manager.
* @param \Drupal\commercetools\CommercetoolsApiServiceInterface $ctApi
* The Commercetools API service.
* @param \Drupal\Core\Site\Settings $settings
* The settings instance.
* @param \Drupal\Core\Cache\CacheBackendInterface $cacheBackend
* A cache backend.
* @param \Drupal\Core\Cache\CacheTagsInvalidatorInterface $cacheTagsInvalidator
* The cache tags invalidator.
*/
public function __construct(
protected readonly AccountInterface $user,
protected readonly EntityTypeManagerInterface $entityTypeManager,
protected readonly CommercetoolsApiServiceInterface $ctApi,
protected readonly Settings $settings,
CacheBackendInterface $cacheBackend,
protected CacheTagsInvalidatorInterface $cacheTagsInvalidator,
) {
$this->cacheBackend = $cacheBackend;
}
/**
* Provides a customer ID field storage definitions.
*
* @return \Drupal\Core\Field\FieldStorageDefinitionInterface
* The customer ID field storage definitions.
*/
public static function customerIdFieldDefinitions(): FieldStorageDefinitionInterface {
return BaseFieldDefinition::create('string')
->setLabel('commercetools customer ID')
->setDescription('External commercetools customer ID for internal use.')
->setRevisionable(FALSE)
->setSetting('max_length', 86);
}
/**
* Loads a Commercetools customer by ID.
*
* @param string $customerId
* The Commercetools customer ID.
*
* @return array
* An existing customer data from API.
*
* @throws \Throwable
*/
public function load(string $customerId): array {
$arguments = [
'id' => new Variable('id', 'String!'),
];
$query = new Query('customer', $arguments);
$this->addFields($query);
$variables = [
'id' => $customerId,
];
$response = $this->ctApi->executeGraphQlOperation($query, $variables);
return (array) $response->getData()['customer'];
}
/**
* Creates a new Commercetools customer.
*
* @param array $draft
* The array of values which will be used for creating customer.
*
* @return array
* An added customer data from API.
*
* @throws \Commercetools\Exception\InvalidArgumentException
* @throws \Throwable
*/
public function create(array $draft = []): array {
// Force password to be non-mandatory.
$draft['authenticationMode'] = 'ExternalAuth';
// Set current user's email if not provided.
if (empty($draft['email'])) {
$email = $this->getUserEmail();
if ($email) {
$draft['email'] = $email;
}
else {
throw new InvalidArgumentException('An email is required to create a customer');
}
}
$arguments = [
'draft' => new Variable('draft', 'CustomerSignUpDraft!'),
];
$mutation = new Mutation('customerSignUp', $arguments);
$customer = $mutation->customer([]);
$this->addFields($customer);
$variables = [
'draft' => $draft,
];
$response = $this->ctApi->executeGraphQlOperation($mutation, $variables);
return (array) $response->getData()['customerSignUp'];
}
/**
* Deletes a Commercetools customer.
*
* @param string $customerId
* The Commercetools customer ID.
* @param int $version
* The last seen version of the Commercetools customer.
*
* @return array
* A customer data being deleted.
*
* @throws \Throwable
*/
public function delete(string $customerId, int $version): array {
$arguments = [
'id' => new Variable('id', 'String'),
'version' => new Variable('version', 'Long!'),
];
$mutation = new Mutation('deleteCustomer', $arguments);
$this->addFields($mutation);
$variables = [
'id' => $customerId,
'version' => $version,
];
$response = $this->ctApi->executeGraphQlOperation($mutation, $variables);
return (array) $response->getData()['deleteCustomer'];
}
/**
* Retrieves a Commercetools customer for the given or current user.
*
* Conditionally store Customer ID into user on update.
*
* @param \Drupal\user\UserInterface|null $user
* The user to retrieve a customer by.
* Fallbacks to current user if not provided.
* @param bool $seek
* Indicates whether to seek for existing customer by email.
* Updates the user entity with customer ID if found.
* @param bool $create
* Indicates whether to create a new customer.
* Updates the user entity with created customer ID.
* @param bool $store
* Indicates whether to store Customer ID into user.
*
* @return array|null
* A customer data retrieved.
*
* @throws \Throwable
*/
public function getCustomerByUser(?UserInterface $user = NULL, bool $seek = TRUE, bool $create = TRUE, bool $store = TRUE): ?array {
// First, try to load Customer by stored ID.
$id = $this->getUserCustomerId($user);
if ($id) {
$data = $this->load($id);
if (!empty($data['version'])) {
return $data;
}
}
$email = $this->getUserEmail($user);
// Second, conditionally seek for Customer by email.
if ($email && $seek) {
try {
$data = $this->query(['email' => $email]);
}
catch (\Exception) {
// If the query fails, we return NULL to indicate no customer found.
// @todo Rework this to cache and not repeat the checks.
return NULL;
}
$customer = reset($data);
if (!empty($customer['version'])) {
if ($store) {
$this->setUserCustomerId($customer['id'], $user);
}
return $customer;
}
}
// Last, conditionally create a Customer.
if ($email && $create) {
$data = $this->create(['email' => $email]);
if (!empty($data['version'])) {
if ($store) {
$this->setUserCustomerId($data['id'], $user);
}
return $data;
}
}
return NULL;
}
/**
* Removes a Commercetools customer for the given or current user.
*
* @param \Drupal\user\UserInterface|null $user
* The user to retrieve by.
* Fallbacks to current user if not provided.
*
* @return array|null
* Customer data being deleted.
*
* @throws \Throwable
*/
public function deleteCustomerByUser(?UserInterface $user = NULL): ?array {
/** @var \Symfony\Component\EventDispatcher\EventDispatcherInterface $event_dispatcher */
$id = $this->getUserCustomerId($user);
$version = NULL;
if ($id) {
$customer = $this->load($id);
$version = $customer['version'] ?? NULL;
}
return $id && $version ? $this->delete($id, $version) : NULL;
}
/**
* Sets the user's customer ID.
*
* @param string $customerId
* The Commercetools customer ID.
* @param \Drupal\user\UserInterface|null $user
* The user entity to update. Fallbacks to current user if not provided.
*
* @throws \Drupal\Core\Entity\EntityStorageException
* @throws \Drupal\Core\Entity\EntityMalformedException
*/
public function setUserCustomerId(string $customerId, ?UserInterface $user = NULL): void {
if (empty($user)) {
$user = $this->getCurrentUser();
}
if ($user->isAuthenticated()) {
$this->cacheSet(self::CACHE_CID_PREFIX_CUSTOMER_ID . $user->id(), $customerId, tags: [
self::CACHE_TAG_CUSTOMER_ID . $customerId,
self::CACHE_TAG_CUSTOMER_LIST,
]);
}
}
/**
* Returns the user's customer ID if set.
*
* @param \Drupal\user\UserInterface|null $user
* The user to retrieve by. Fallbacks to current user if not provided.
*
* @return ?string
* A user customer ID if available.
*
* @throws \Drupal\Core\Entity\EntityMalformedException
*/
private function getUserCustomerId(?UserInterface $user = NULL): ?string {
if (empty($user)) {
$user = $this->getCurrentUser();
}
if ($user->isAuthenticated()) {
$cache = $this->cacheGet(self::CACHE_CID_PREFIX_CUSTOMER_ID . $user->id());
return empty($cache) ? NULL : $cache->data;
}
return NULL;
}
/**
* Returns the current user's customer ID if available.
*
* @param bool $reset
* If the current customer id should be reset.
*
* @return ?string
* A customer ID if available.
*
* @throws \Drupal\Core\Entity\EntityMalformedException
* @throws \Throwable
*/
public function getCurrentUserCustomerId(bool $reset = FALSE): ?string {
if (isset($this->currentCustomerId) && !$reset) {
return $this->currentCustomerId;
}
// First, quickly check if customer ID is set for a user account.
$this->currentCustomerId = $this->getUserCustomerId();
// If no, try to determine a customer ID via API call.
if (!$this->currentCustomerId && ($customer = $this->getCustomerByUser())) {
$this->currentCustomerId = $customer['id'];
}
return $this->currentCustomerId;
}
/**
* Removes all customer IDs previously assigned to Users.
*
* @param array $uids
* The User IDs.
*/
public function unsetCustomerIdForUsers(array $uids = []): void {
$tags = empty($uids) ? [self::CACHE_TAG_CUSTOMER_LIST] : array_map(function ($uid) {
return self::CACHE_TAG_CUSTOMER_ID . $uid;
}, array_unique($uids));
$this->cacheTagsInvalidator->invalidateTags($tags);
}
/**
* Returns the user's addresses.
*/
public function getAddressDataByCustomer(?UserInterface $user = NULL): ?array {
$customerId = $this->getUserCustomerId($user);
$arguments = [
'id' => new Variable('id', 'String!'),
];
$query = new Query('customer', $arguments);
$this->addFields($query);
$variables = [
'id' => $customerId,
];
$query->use(
'defaultBillingAddressId',
'defaultShippingAddressId',
);
$query->shippingAddresses([])->use(...static::ADDRESS_FIELDS);
$query->billingAddresses([])->use(...static::ADDRESS_FIELDS);
$response = $this->ctApi->executeGraphQlOperation($query, $variables);
return (array) $response->getData()['customer'];
}
/**
* Update customer.
*/
public function updateCustomer(array $actions, ?UserInterface $user = NULL): array {
$customer = $this->getCustomerByUser($user);
$arguments = [
'id' => new Variable('id', 'String'),
'version' => new Variable('version', 'Long!'),
'actions' => new Variable('actions', '[CustomerUpdateAction!]!'),
];
$updateCustomer = new Mutation('updateCustomer', $arguments);
$variables = [
'id' => $customer['id'],
'version' => $customer['version'],
'actions' => $actions,
];
$this->addFields($updateCustomer);
$updateCustomer->addresses([])->use(...static::ADDRESS_FIELDS);
$response = $this->ctApi->executeGraphQlOperation($updateCustomer, $variables);
return $response->getData();
}
/**
* Update address customer.
*/
public function addAddress(array $addressData, ?string $type = NULL, ?bool $setDefault = NULL, ?UserInterface $user = NULL) {
$actions = [
[
'addAddress' => ['address' => $addressData],
],
];
$responseData = $this->updateCustomer($actions, $user);
$addressesIds = array_column($responseData['updateCustomer']['addresses'], 'id');
$addressId = !empty($addressesIds) ? end($addressesIds) : NULL;
if ($type) {
if ($type !== static::SHIPPING_ADDRESS_KEY && $type !== static::BILLING_ADDRESS_KEY) {
throw new LogicException('Invalid address type.');
}
$action = $type === static::SHIPPING_ADDRESS_KEY
? 'addShippingAddressId'
: 'addBillingAddressId';
$actions = [
[
$action => ['addressId' => $addressId],
],
];
if ($setDefault) {
$action = $type === static::SHIPPING_ADDRESS_KEY
? 'setDefaultShippingAddress'
: 'setDefaultBillingAddress';
$actions[][$action] = [
'addressId' => $addressId,
];
}
$this->updateCustomer($actions, $user);
}
return end($responseData['updateCustomer']['addresses']);
}
/**
* Adds customer fields to request node.
*
* @param \GraphQL\Entities\Node $query
* The node to which the fields will be added.
*/
protected function addFields(Node $query): void {
$query->use('id', 'version', 'email');
}
/**
* Query a Commercetools customers.
*
* @param array $where
* The where clause array.
*
* @return array
* A customers dataset queried from API.
*
* @throws \Throwable
*/
private function query(array $where): array {
$arguments = [
'limit' => 1,
'where' => new Variable('where', 'String'),
];
$query = new Query('customers', $arguments);
$result = $query->results([]);
$this->addFields($result);
$variables = [
'where' => CommercetoolsService::whereToString($where),
];
$response = $this->ctApi->executeGraphQlOperation($query, $variables);
return (array) $response->getData()['customers']['results'];
}
/**
* Loads current user entity.
*
* @return \Drupal\user\UserInterface
* The current user entity.
*/
private function getCurrentUser(): UserInterface {
/** @var \Drupal\user\UserInterface $user */
$user = $this->entityTypeManager->getStorage('user')
->load($this->user->id());
return $user;
}
/**
* Returns email of given or currently logged-in user if any.
*
* @param \Drupal\user\UserInterface|null $user
* The user entity.
*
* @return string|null
* An email address if any.
*/
private function getUserEmail(?UserInterface $user = NULL): ?string {
if ($user) {
return $user->getEmail();
}
elseif ($this->user->isAuthenticated()) {
return $this->user->getEmail();
}
return NULL;
}
}
