Skip to content

Commit

Permalink
Merge pull request #9484 from magento-lynx/2.4.8-graphql-api-enhancem…
Browse files Browse the repository at this point in the history
…ents
  • Loading branch information
svera authored Jan 28, 2025
2 parents 3f12d15 + adc6bd5 commit 2217274
Show file tree
Hide file tree
Showing 37 changed files with 1,696 additions and 231 deletions.
77 changes: 60 additions & 17 deletions app/code/Magento/CatalogGraphQl/Model/PriceRangeDataProvider.php
Original file line number Diff line number Diff line change
@@ -1,19 +1,17 @@
<?php
/**
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
* Copyright 2025 Adobe
* All Rights Reserved.
*/
declare(strict_types=1);

namespace Magento\CatalogGraphQl\Model;

use Exception;
use Magento\Catalog\Api\Data\ProductInterface;
use Magento\Catalog\Model\Product;
use Magento\CatalogGraphQl\Model\Resolver\Product\Price\Discount;
use Magento\CatalogGraphQl\Model\Resolver\Product\Price\ProviderPool as PriceProviderPool;
use Magento\Framework\Exception\LocalizedException;
use Magento\Framework\GraphQl\Query\Resolver\Value;
use Magento\Framework\GraphQl\Schema\Type\ResolveInfo;
use Magento\Framework\Pricing\PriceCurrencyInterface;
use Magento\Framework\Pricing\SaleableInterface;
Expand All @@ -27,6 +25,8 @@ class PriceRangeDataProvider
{
private const STORE_FILTER_CACHE_KEY = '_cache_instance_store_filter';

private const TYPE_DOWNLOADABLE = 'downloadable';

/**
* @param PriceProviderPool $priceProviderPool
* @param Discount $discount
Expand All @@ -45,18 +45,48 @@ public function __construct(
* @param ContextInterface $context
* @param ResolveInfo $info
* @param array $value
* @throws Exception
* @return mixed|Value
* @return array
* @throws LocalizedException
*/
public function prepare(ContextInterface $context, ResolveInfo $info, array $value): array
{
$store = $context->getExtensionAttributes()->getStore();
$product = $this->getProduct($value, $context, $store);

$requestedFields = $info->getFieldSelection(10);
$returnArray = [];

$returnArray['minimum_price'] = ($requestedFields['minimum_price'] ?? 0) ? ($this->canShowPrice($product) ?
$this->getMinimumProductPrice($product, $store) : $this->formatEmptyResult()) : $this->formatEmptyResult();
$returnArray['maximum_price'] = ($requestedFields['maximum_price'] ?? 0) ? ($this->canShowPrice($product) ?
$this->getMaximumProductPrice($product, $store) : $this->formatEmptyResult()) : $this->formatEmptyResult();

if ($product->getTypeId() === self::TYPE_DOWNLOADABLE &&
$product->getData('links_purchased_separately')) {
$downloadableLinkPrice = (float)$this->getDownloadableLinkPrice($product);
if ($downloadableLinkPrice > 0) {
$returnArray['maximum_price']['regular_price']['value'] += $downloadableLinkPrice;
$returnArray['maximum_price']['final_price']['value'] += $downloadableLinkPrice;
}
}

return $returnArray;
}

/**
* Validate and return product
*
* @param array $value
* @param ContextInterface $context
* @param StoreInterface $store
* @return Product
* @throws LocalizedException
*/
private function getProduct(array $value, ContextInterface $context, StoreInterface $store): Product
{
if (!isset($value['model'])) {
throw new LocalizedException(__('"model" value should be specified'));
}
/** @var StoreInterface $store */
$store = $context->getExtensionAttributes()->getStore();

/** @var Product $product */
$product = $value['model'];
$product->unsetData('minimal_price');
// add store filter for the product
Expand All @@ -69,15 +99,28 @@ public function prepare(ContextInterface $context, ResolveInfo $info, array $val
}
}

$requestedFields = $info->getFieldSelection(10);
$returnArray = [];
return $product;
}

$returnArray['minimum_price'] = ($requestedFields['minimum_price'] ?? 0) ? ($this->canShowPrice($product) ?
$this->getMinimumProductPrice($product, $store) : $this->formatEmptyResult()) : $this->formatEmptyResult();
$returnArray['maximum_price'] = ($requestedFields['maximum_price'] ?? 0) ? ($this->canShowPrice($product) ?
$this->getMaximumProductPrice($product, $store) : $this->formatEmptyResult()) : $this->formatEmptyResult();
/**
* Get the downloadable link price
*
* @param Product $product
* @return float
*/
private function getDownloadableLinkPrice(Product $product): float
{
$downloadableLinks = $product->getTypeInstance()->getLinks($product);
if (empty($downloadableLinks)) {
return 0.0;
}

return $returnArray;
$price = 0.0;
foreach ($downloadableLinks as $link) {
$price += (float)$link->getPrice();
}

return $price;
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
<?php
/**
* Copyright 2025 Adobe
* All Rights Reserved.
*/
declare(strict_types=1);

namespace Magento\CatalogGraphQl\Model\Resolver\Product;

use Magento\Framework\Escaper;
use Magento\Framework\Exception\LocalizedException;
use Magento\Framework\GraphQl\Config\Element\Field;
use Magento\Framework\GraphQl\Query\ResolverInterface;
use Magento\Framework\GraphQl\Schema\Type\ResolveInfo;

class ProductName implements ResolverInterface
{
/**
* ProductName constructor
*
* @param Escaper $escaper
*/
public function __construct(
private readonly Escaper $escaper
) {
}

/**
* @inheritdoc
*
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
*/
public function resolve(
Field $field,
$context,
ResolveInfo $info,
array $value = null,
array $args = null
): string {
if (!isset($value['model'])) {
throw new LocalizedException(__('"model" value should be specified'));
}

// Product name allowed with special characters
return str_replace(
"&apos;",
"'",
str_replace("&amp;", "&", $this->escaper->escapeUrl($value['model']->getName()))
);
}
}
6 changes: 3 additions & 3 deletions app/code/Magento/CatalogGraphQl/etc/schema.graphqls
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# Copyright © Magento, Inc. All rights reserved.
# See COPYING.txt for license details.
# Copyright 2025 Adobe
# All Rights Reserved.

type Query {
products (
Expand Down Expand Up @@ -93,7 +93,7 @@ interface ProductLinksInterface @typeResolver(class: "Magento\\CatalogGraphQl\\M
interface ProductInterface @typeResolver(class: "Magento\\CatalogGraphQl\\Model\\ProductInterfaceTypeResolverComposite") @doc(description: "Contains fields that are common to all types of products.") {
id: Int @deprecated(reason: "Use the `uid` field instead.") @doc(description: "The ID number assigned to the product.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\EntityIdToId")
uid: ID! @doc(description: "The unique ID for a `ProductInterface` object.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\EntityIdToUid")
name: String @doc(description: "The product name. Customers use this name to identify the product.")
name: String @doc(description: "The product name. Customers use this name to identify the product.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\ProductName")
sku: String @doc(description: "A number or code assigned to a product to identify the product, options, price, and manufacturer.")
description: ComplexTextValue @doc(description: "Detailed information about the product. The value can include simple HTML tags.") @resolver(class: "\\Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\ProductComplexTextAttribute")
short_description: ComplexTextValue @doc(description: "A short description of the product. Its use depends on the theme.") @resolver(class: "\\Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\ProductComplexTextAttribute")
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<?php
/**
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
* Copyright 2025 Adobe
* All Rights Reserved.
*/
declare(strict_types=1);

Expand Down Expand Up @@ -77,6 +77,7 @@ public function resolve(
}

$customer = $this->getCustomer->execute($context);
$customer->setData('ignore_validation_flag', true);
$this->updateCustomerAccount->execute(
$customer,
[
Expand All @@ -86,8 +87,6 @@ public function resolve(
$context->getExtensionAttributes()->getStore()
);

$data = $this->extractCustomerData->execute($customer);

return ['customer' => $data];
return ['customer' => $this->extractCustomerData->execute($customer)];
}
}
158 changes: 158 additions & 0 deletions app/code/Magento/Downloadable/Test/Fixture/DownloadableProduct.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
<?php
/**
* Copyright 2025 Adobe
* All Rights Reserved.
*/
declare(strict_types=1);

namespace Magento\Downloadable\Test\Fixture;

use Magento\Catalog\Api\ProductRepositoryInterface;
use Magento\Catalog\Test\Fixture\Product;
use Magento\Downloadable\Model\Link;
use Magento\Downloadable\Model\Product\Type;
use Magento\Framework\App\Filesystem\DirectoryList;
use Magento\Framework\DataObject;
use Magento\Framework\Exception\FileSystemException;
use Magento\Framework\Exception\LocalizedException;
use Magento\Framework\Filesystem\Io\File;
use Magento\TestFramework\Fixture\Api\DataMerger;
use Magento\TestFramework\Fixture\Api\ServiceFactory;
use Magento\TestFramework\Fixture\Data\ProcessorInterface;

class DownloadableProduct extends Product
{
private const DEFAULT_DATA = [
'type_id' => Type::TYPE_DOWNLOADABLE,
'name' => 'DownloadableProduct%uniqid%',
'sku' => 'downloadable-product%uniqid%',
'price' => 0.00,
'links_purchased_separately' => 1,
'links_title' => 'Downloadable Links%uniqid%',
'links_exist' => 0,
'extension_attributes' => [
'website_ids' => [1],
'stock_item' => [
'use_config_manage_stock' => true,
'qty' => 100,
'is_qty_decimal' => false,
'is_in_stock' => true,
],
'downloadable_product_links' => [],
'downloadable_product_samples' => null
],
];

/**
* DownloadableProduct constructor
*
* @param ServiceFactory $serviceFactory
* @param ProcessorInterface $dataProcessor
* @param DataMerger $dataMerger
* @param ProductRepositoryInterface $productRepository
* @param DirectoryList $directoryList
* @param Link $link
* @param File $file
*/
public function __construct(
private readonly ServiceFactory $serviceFactory,
private readonly ProcessorInterface $dataProcessor,
private readonly DataMerger $dataMerger,
private readonly ProductRepositoryInterface $productRepository,
private readonly DirectoryList $directoryList,
private readonly Link $link,
private readonly File $file
) {
parent::__construct($serviceFactory, $dataProcessor, $dataMerger, $productRepository);
}

/**
* @inheritdoc
*
* @throws FileSystemException
* @throws LocalizedException
*/
public function apply(array $data = []): ?DataObject
{
return parent::apply($this->prepareData($data));
}

/**
* Prepare product data
*
* @param array $data
* @return array
* @throws FileSystemException
* @throws LocalizedException
*/
private function prepareData(array $data): array
{
$data = $this->dataMerger->merge(self::DEFAULT_DATA, $data);

// Remove common properties not needed for downloadable products
unset($data['weight']);

// Prepare downloadable links
$links = $this->prepareLinksData($data);
$data['extension_attributes']['downloadable_product_links'] = $links;
$data['links_exist'] = count($links);

return $this->dataProcessor->process($this, $data);
}

/**
* Prepare links data
*
* @param array $data
* @return array
* @throws FileSystemException
* @throws LocalizedException
*/
private function prepareLinksData(array $data): array
{
$links = [];
foreach ($data['extension_attributes']['downloadable_product_links'] as $link) {
$links[] = [
'id' => null,
'title' => $link['title'] ?? 'Test Link%uniqid%',
'price' => $link['price'] ?? 0,
'link_type' => $link['link_type'] ?? 'file',
'link_url' => null,
'link_file' => $this->generateDownloadableLink($link['link_file'] ?? 'test-' . uniqid() . '.txt'),
'is_shareable' => $link['is_shareable'] ?? 0,
'number_of_downloads' => $link['number_of_downloads'] ?? 5,
'sort_order' => $link['sort_order'] ?? 10,
];
}

return $links;
}

/**
* Generate downloadable link file
*
* @param string $fileName
* @return string
* @throws FileSystemException|LocalizedException
*/
public function generateDownloadableLink(string $fileName): string
{
try {
$subDir = sprintf('%s/%s', $fileName[0], $fileName[1]);
$mediaPath = sprintf(
'%s/%s/%s',
$this->directoryList->getPath(DirectoryList::MEDIA),
$this->link->getBasePath(),
$subDir
);
$this->file->checkAndCreateFolder($mediaPath);
$this->file->write(sprintf('%s/%s', $mediaPath, $fileName), "This is a temporary text file.");

return sprintf('/%s/%s', $subDir, $fileName);
} catch (FileSystemException $e) {
throw new FileSystemException(__($e->getMessage()));
} catch (LocalizedException $e) {
throw new LocalizedException(__($e->getMessage()));
}
}
}
2 changes: 1 addition & 1 deletion app/code/Magento/GraphQl/etc/di.xml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<?xml version="1.0"?>
<!--
/**
* Copyright 2017 Adobe
* Copyright 2024 Adobe
* All Rights Reserved.
*/
-->
Expand Down
Loading

0 comments on commit 2217274

Please sign in to comment.