arch-8.x-1.x-dev/modules/cart/src/Controller/Api/ApiController.php
modules/cart/src/Controller/Api/ApiController.php
<?php namespace Drupal\arch_cart\Controller\Api; use Drupal\arch_cart\Cart\CartHandlerInterface; use Drupal\arch_price\Price\PriceFactoryInterface; use Drupal\arch_price\Price\PriceFormatterInterface; use Drupal\arch_price\Price\PriceInterface; use Drupal\arch_product\Entity\ProductInterface; use Drupal\Core\Controller\ControllerBase; use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\Core\Extension\ModuleHandlerInterface; use Drupal\Core\File\FileUrlGeneratorInterface; use Drupal\Core\Language\LanguageManagerInterface; use Drupal\Core\Render\RendererInterface; use Drupal\Core\Theme\ThemeManagerInterface; use Drupal\file\FileInterface; use Drupal\media\MediaInterface; use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\RequestStack; /** * Cart API controller. * * @package Drupal\arch_cart\Controller\Api */ class ApiController extends ControllerBase { /** * Price factory. * * @var \Drupal\arch_price\Price\PriceFactoryInterface */ protected $priceFactory; /** * Price formatter. * * @var \Drupal\arch_price\Price\PriceFormatterInterface */ protected $priceFormatter; /** * Current request. * * @var \Symfony\Component\HttpFoundation\Request */ protected $request; /** * File URL generator. * * @var \Drupal\Core\File\FileUrlGeneratorInterface */ protected $fileUrlGenerator; /** * Renderer. * * @var \Drupal\Core\Render\RendererInterface */ protected $renderer; /** * Cart. * * @var \Drupal\arch_cart\Cart\CartInterface */ protected $cart; /** * Theme manager. * * @var \Drupal\Core\Theme\ThemeManagerInterface */ protected $themeManager; /** * Current language code. * * @var string */ protected $currentLanguageCode; /** * Block config. * * @var array */ protected $blockConfig; /** * ApiController constructor. * * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager * Entity type manager. * @param \Drupal\arch_price\Price\PriceFactoryInterface $price_factory * Price factory. * @param \Drupal\arch_price\Price\PriceFormatterInterface $price_formatter * Price formatter. * @param \Drupal\arch_cart\Cart\CartHandlerInterface $cart_handler * Cart handler. * @param \Drupal\Core\Language\LanguageManagerInterface $language_manager * Language manager. * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler * Module handler. * @param \Drupal\Core\Theme\ThemeManagerInterface $theme_manager * Theme manager. * @param \Drupal\Core\Render\RendererInterface $renderer * Renderer service. * @param \Symfony\Component\HttpFoundation\RequestStack $request_stack * Request stack. * @param \Drupal\Core\File\FileUrlGeneratorInterface $file_url_generator * File URL generator. */ public function __construct( EntityTypeManagerInterface $entity_type_manager, PriceFactoryInterface $price_factory, PriceFormatterInterface $price_formatter, CartHandlerInterface $cart_handler, LanguageManagerInterface $language_manager, ModuleHandlerInterface $module_handler, ThemeManagerInterface $theme_manager, RendererInterface $renderer, RequestStack $request_stack, FileUrlGeneratorInterface $file_url_generator, ) { $this->entityTypeManager = $entity_type_manager; $this->priceFactory = $price_factory; $this->priceFormatter = $price_formatter; $this->renderer = $renderer; $this->request = $request_stack->getCurrentRequest(); $this->cart = $cart_handler->getCart(); $this->languageManager = $language_manager; $this->currentLanguageCode = $language_manager->getCurrentLanguage()->getId(); $this->moduleHandler = $module_handler; $this->themeManager = $theme_manager; $this->fileUrlGenerator = $file_url_generator; try { /** @var \Drupal\Core\Config\Entity\ConfigEntityStorageInterface $block_storage */ $request_theme = $this->request->get('theme'); if (empty($request_theme)) { $request_theme = $this->themeManager->getActiveTheme()->getName(); } $block_storage = $entity_type_manager->getStorage('block'); $block_config = $block_storage->loadByProperties([ 'theme' => $request_theme, 'plugin' => 'arch_cart_mini_cart', ]); if (!empty($block_config)) { /** @var \Drupal\block\Entity\Block $block */ $block = current($block_config); /** @var \Drupal\arch_cart\Plugin\Block\MiniCartBlock $plugin */ $plugin = $block->getPlugin(); $this->blockConfig = $plugin->getConfiguration(); } } catch (\Exception $e) { // @todo handle exception. } } /** * {@inheritdoc} */ public static function create(ContainerInterface $container) { return new static( $container->get('entity_type.manager'), $container->get('price_factory'), $container->get('price_formatter'), $container->get('arch_cart_handler'), $container->get('language_manager'), $container->get('module_handler'), $container->get('theme.manager'), $container->get('renderer'), $container->get('request_stack'), $container->get('file_url_generator') ); } /** * Get info about cart. * * @return \Symfony\Component\HttpFoundation\JsonResponse * JSON response. */ public function cart() { $status_code = NULL; if ($this->request->isMethod('GET')) { $data = $this->buildCart(TRUE); } else { $status_code = 405; $data = [ 'error' => TRUE, 'message' => 'Method not allowed', ]; } return $this->sendJson($data, $status_code); } /** * Add item to cart. * * @return \Symfony\Component\HttpFoundation\JsonResponse * JSON response. */ public function addItem() { $status_code = NULL; if ($this->request->isMethod('POST')) { $product_id = $this->request->request->get('id'); $quantity = (float) $this->request->request->get('quantity'); $product = $this->loadProduct($product_id); if (!$product) { $status_code = 400; $data = [ 'error' => TRUE, 'message' => 'Failed to add product. Reason: Invalid product id.', ]; } else { $this->cart->addItem([ 'type' => 'product', 'id' => $product_id, 'quantity' => $quantity, ]); $data = $this->buildCart(); $data['do'][] = 'update_cart'; $data['do'][] = 'show_cart'; } } else { $status_code = 405; $data = [ 'error' => TRUE, 'message' => 'Method not allowed', ]; } return $this->sendJson($data, $status_code); } /** * Change product quantity. * * @return \Symfony\Component\HttpFoundation\JsonResponse * JSON response. */ public function quantity() { $status_code = NULL; if ($this->request->isMethod('POST')) { $type = $this->request->request->get('type'); $id = $this->request->request->get('id'); $item_key = $this->request->request->get('key'); $quantity = (float) $this->request->request->get('quantity'); try { if (isset($type) && isset($id)) { try { $this->cart->updateItemQuantityById($type, $id, $quantity); } catch (\Exception $e) { if ($e->getCode() !== 1001) { throw $e; } $this->cart->addItem([ 'type' => $type, 'id' => $id, 'quantity' => $quantity, ]); } } elseif (isset($item_key)) { $this->cart->updateItemQuantity($item_key, $quantity); } else { throw new \Exception('Cannot update missing item'); } $data = $this->buildCart(); $data['do'][] = 'update_cart'; $data['do'][] = 'show_cart'; } catch (\Exception $e) { $data = [ 'error' => TRUE, 'message' => $e->getMessage(), ]; $status_code = 400; } } else { $status_code = 405; $data = [ 'error' => TRUE, 'message' => 'Method not allowed', ]; } return $this->sendJson($data, $status_code); } /** * Remove item from cart. * * @return \Symfony\Component\HttpFoundation\JsonResponse * JSON response. */ public function removeItem() { $status_code = NULL; if ($this->request->isMethod('POST')) { $type = $this->request->request->get('type'); $id = $this->request->request->get('id'); $item_key = $this->request->request->get('key'); try { if (isset($type) && isset($id)) { $this->cart->removeItemById($type, $id); } elseif (isset($item_key)) { $this->cart->removeItem($item_key); } else { throw new \Exception('Cannot remove missing item'); } $data = $this->buildCart(); $data['do'][] = 'update_cart'; $data['do'][] = 'show_cart'; } catch (\Exception $e) { $data = [ 'error' => TRUE, 'message' => $e->getMessage(), ]; $status_code = 400; } } else { $status_code = 405; $data = [ 'error' => TRUE, 'message' => 'Method not allowed', ]; } return $this->sendJson($data, $status_code); } /** * Build cart data. * * @return array * Cart data. */ protected function buildCart($clear_messages = FALSE) { $data = [ 'cart' => [ 'items' => [], 'products' => 0, 'quantity' => 0, 'total' => [ 'raw' => 0, 'formatted' => NULL, ], 'net_total' => [ 'raw' => 0, 'formatted' => NULL, ], 'messages' => [], ], 'error' => FALSE, 'messages' => [], ]; /** @var \Drupal\arch_price\Price\PriceInterface[] $totals */ $totals = []; $products = []; foreach ($this->cart->getProducts() as $key => $line_item) { /** @var \Drupal\arch_product\Entity\ProductInterface $product */ $product = $this->loadProduct($line_item['id']); if (empty($product)) { $this->cart->removeItemById($line_item['type'], $line_item['id']); continue; } $data['cart']['products'] += 1; $data['cart']['quantity'] += (float) $line_item['quantity']; $item = $this->buildCartItem($key, $line_item, $product); /** @var \Drupal\arch_price\Price\PriceInterface $price */ $price = $product->getActivePrice(); $total_price = $this->totalPrice($item['quantity'], $price); $totals[] = $total_price; $products[] = $item; } $total_base_values = [ 'base' => 'net', 'price_type' => 'default', 'currency' => NULL, 'net' => 0, 'gross' => 0, 'vat_category' => 'custom', 'vat_rate' => 0, 'vat_value' => 0, 'date_from' => NULL, 'date_to' => NULL, ]; $this->moduleHandler()->alter('cart_total_base_values', $total_base_values); $total_price_values = [ '_fallback' => TRUE, 'base' => 'net', 'net' => 0, 'gross' => 0, 'currency' => 'XXX', 'vat_category' => 'default', 'vat_rate' => 0, ]; foreach ($totals as $total) { if (!empty($total_price_values) && !empty($total_price_values['_fallback'])) { $total_price_values = $total->getValues(); continue; } $total_price_values['net'] += $total->getNetPrice(); $total_price_values['gross'] += $total->getGrossPrice(); } $total_price_values['base'] = $total_base_values['base']; $total_price_values['vat_category'] = $total_base_values['vat_category']; $total_price_values['vat_rate'] = $total_base_values['vat_rate']; $total_price_values['vat_amount'] = 0; if (isset($total_base_values['vat_amount'])) { $total_price_values['vat_amount'] = $total_base_values['vat_amount']; } // @todo Handle if no totals: !empty($total_price_values['_fallback']). $total_cart_price = $this->cart->getTotalPrice(); $data['cart']['total'] = [ 'raw' => $total_cart_price->getGrossPrice(), 'formatted' => $this->formatPrice($total_cart_price, PriceInterface::FORMAT_GROSS), ]; $data['cart']['net_total'] = [ 'raw' => $total_cart_price->getNetPrice(), 'formatted' => $this->formatPrice($total_cart_price, PriceInterface::FORMAT_NET), ]; $data['cart']['items'] = $products; $data['cart']['messages'] = array_values(array_merge($data['messages'], $this->cart->displayMessages($clear_messages))); $this->moduleHandler()->alter('api_cart_data', $data, $this->cart); return $data; } /** * Send JSON response. * * @param array $data * Response data. * @param int $status_code * Status code. * @param string $status_text * Status text. * * @return \Symfony\Component\HttpFoundation\JsonResponse * JSON response. */ protected function sendJson(array $data, $status_code = NULL, $status_text = NULL) { $response = new JsonResponse(); $response->setData($data); if (isset($status_code)) { $response->setStatusCode($status_code, $status_text); } return $response; } /** * Load product. * * @param int $pid * Product ID. * * @return \Drupal\arch_product\Entity\ProductInterface|null * Product. */ protected function loadProduct($pid) { try { $storage = $this->entityTypeManager()->getStorage('product'); /** @var \Drupal\arch_product\Entity\ProductInterface $product */ $product = $storage->load($pid); if (!$product) { return NULL; } if ($product->hasTranslation($this->currentLanguageCode)) { $product = $product->getTranslation($this->currentLanguageCode); } return $product; } catch (\Exception $e) { // @todo handle this. } return NULL; } /** * Load file. * * @param int $fid * File ID. * * @return \Drupal\file\FileInterface|null * Node. */ protected function loadFile($fid) { try { $storage = $this->entityTypeManager()->getStorage('file'); /** @var \Drupal\file\FileInterface $file */ $file = $storage->load($fid); return $file; } catch (\Exception $e) { // @todo handle this. } return NULL; } /** * Load image style. * * @param string $style * Style name. * * @return \Drupal\image\ImageStyleInterface * Image style. */ protected function loadImageStyle($style) { try { $storage = $this->entityTypeManager()->getStorage('image_style'); /** @var \Drupal\image\ImageStyleInterface $style */ $style = $storage->load($style); return $style; } catch (\Exception $e) { // @todo handle this. } return NULL; } /** * Total price. * * @param float $quantity * Quantity. * @param \Drupal\arch_price\Price\PriceInterface $price * Item price. * * @return \Drupal\arch_price\Price\PriceInterface * Total price. */ protected function totalPrice($quantity, PriceInterface $price) { $values = $price->getValues(); $values['net'] = $values['net'] * $quantity; $values['gross'] = $values['gross'] * $quantity; return $this->priceFactory->getInstance($values); } /** * Formatting price value. * * @param \Drupal\arch_price\Price\PriceInterface $price * Price instance. * @param string $mode * Formatting mode. * * @return string * Rendered price. */ protected function formatPrice(PriceInterface $price, $mode) { return $this->priceFormatter->format($price, $mode, [ 'label' => FALSE, 'vat_info' => FALSE, ]); } /** * Render product image. * * @param \Drupal\arch_product\Entity\ProductInterface $product * Product. * * @return array * Render array. * * @throws \Exception */ protected function showImage(ProductInterface $product) { $image_uri = $this->loadImage($product); $this->moduleHandler->alter('arch_cart_mini_cart_product_image', $image_uri, $product); $this->themeManager->alter('arch_cart_mini_cart_product_image', $image_uri, $product); if (empty($image_uri)) { // @todo find blank image. return NULL; } $image_style_id = $this->getImageStyle($product->bundle()); $this->moduleHandler->alter('arch_cart_mini_cart_product_image_style', $image_style_id, $product); $this->themeManager->alter('arch_cart_mini_cart_product_image_style', $image_style_id, $product); $style = $this->loadImageStyle($image_style_id); $image_formatted = [ '#theme' => 'image_style', '#uri' => $image_uri, '#style_name' => $style->id(), '#attributes' => [ 'data-retina-src' => NULL, ], ]; $raw = $this->fileUrlGenerator->generateAbsoluteString($image_uri); return [ 'raw' => $raw, 'styled_url' => $style->buildUrl($image_uri), 'formatted' => $this->renderer->render($image_formatted), ]; } /** * Build a cart item for JSON response. * * @param int $key * Cart item key. * @param array $line_item * Line item. * @param \Drupal\arch_product\Entity\ProductInterface $product * Product entity. * * @return array * Cart item array. */ protected function buildCartItem($key, array $line_item, ProductInterface $product) { $item = []; $item['_index'] = $key; foreach (['type', 'id', 'quantity'] as $key) { $item['_line_item'][$key] = $line_item[$key] ?? NULL; } $item['product_id'] = $product->id(); $item['quantity'] = (float) $line_item['quantity']; $item['title'] = $product->label(); try { $item['url'] = $product->toUrl()->toString(); } catch (\Exception $e) { $item['url'] = '/product/' . $line_item['id']; } /** @var \Drupal\arch_price\Price\PriceInterface $price */ $price = $product->getActivePrice(); $total_price = $this->totalPrice($item['quantity'], $price); $item['net_price'] = [ 'raw' => $price->getNetPrice(), 'formatted' => $this->formatPrice($price, PriceInterface::FORMAT_NET), ]; $item['net_total'] = [ 'raw' => $total_price->getNetPrice(), 'formatted' => $this->formatPrice($total_price, PriceInterface::FORMAT_NET), ]; $item['price'] = [ 'raw' => $price->getGrossPrice(), 'formatted' => $this->formatPrice($price, PriceInterface::FORMAT_GROSS), ]; $item['total'] = [ 'raw' => $total_price->getGrossPrice(), 'formatted' => $this->formatPrice($total_price, PriceInterface::FORMAT_GROSS), ]; try { $item['image'] = $this->showImage($product); } catch (\Exception $e) { $item['image'] = NULL; } return $item; } /** * Get image URI. * * @param \Drupal\arch_product\Entity\ProductInterface $product * Product to display. * * @return string|null * Image URI or NULL on failure. */ protected function loadImage(ProductInterface $product): ?string { if ( empty($this->blockConfig) || empty($this->blockConfig['bundle_settings'][$product->bundle()]['image_source']) ) { return NULL; } $file = $this->getProductImageFile($product); if ($file instanceof FileInterface) { return $file->getFileUri(); } return NULL; } /** * Get product image. * * @param \Drupal\arch_product\Entity\ProductInterface $product * Product. * * @return \Drupal\file\FileInterface|null * Image file to display. */ protected function getProductImageFile(ProductInterface $product): ?FileInterface { [$field, $sub_field] = explode(':', $this->blockConfig['bundle_settings'][$product->bundle()]['image_source']); if ( $product->hasField($field) && !$product->get($field)->isEmpty() ) { $product_field = $product->get($field); $field_definition = $product_field->getFieldDefinition(); if ($field_definition->getType() == 'image') { return $product_field->referencedEntities()[0] ?? NULL; } if ( $field_definition->getType() == 'entity_reference' && $field_definition->getSetting('target_type') == 'media' ) { /** @var \Drupal\media\MediaInterface|null $media */ $media = $product_field->referencedEntities()[0] ?? NULL; if ( $media instanceof MediaInterface && $media->hasField($sub_field) && !$media->get($sub_field)->isEmpty() ) { return $media->get($sub_field)->referencedEntities()[0] ?? NULL; } } } return NULL; } /** * Get image style for bundle. * * @param string $product_type_id * Product type ID. * * @return string * Image style ID. */ protected function getImageStyle($product_type_id) { if ( !empty($this->blockConfig) && isset($this->blockConfig['bundle_settings'][$product_type_id]['image_style']) && !empty($this->blockConfig['bundle_settings'][$product_type_id]['image_style']) ) { /** @var \Drupal\image\ImageStyleInterface $image_style */ $image_style = $this->loadImageStyle($this->blockConfig['bundle_settings'][$product_type_id]['image_style']); if (!empty($image_style)) { return $image_style->id(); } } return 'thumbnail'; } }