Skip to content

Commit 2217274

Browse files
authored
Merge pull request #9484 from magento-lynx/2.4.8-graphql-api-enhancements
2 parents 3f12d15 + adc6bd5 commit 2217274

File tree

37 files changed

+1696
-231
lines changed

37 files changed

+1696
-231
lines changed

Diff for: app/code/Magento/CatalogGraphQl/Model/PriceRangeDataProvider.php

+60-17
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,17 @@
11
<?php
22
/**
3-
* Copyright © Magento, Inc. All rights reserved.
4-
* See COPYING.txt for license details.
3+
* Copyright 2025 Adobe
4+
* All Rights Reserved.
55
*/
66
declare(strict_types=1);
77

88
namespace Magento\CatalogGraphQl\Model;
99

10-
use Exception;
1110
use Magento\Catalog\Api\Data\ProductInterface;
1211
use Magento\Catalog\Model\Product;
1312
use Magento\CatalogGraphQl\Model\Resolver\Product\Price\Discount;
1413
use Magento\CatalogGraphQl\Model\Resolver\Product\Price\ProviderPool as PriceProviderPool;
1514
use Magento\Framework\Exception\LocalizedException;
16-
use Magento\Framework\GraphQl\Query\Resolver\Value;
1715
use Magento\Framework\GraphQl\Schema\Type\ResolveInfo;
1816
use Magento\Framework\Pricing\PriceCurrencyInterface;
1917
use Magento\Framework\Pricing\SaleableInterface;
@@ -27,6 +25,8 @@ class PriceRangeDataProvider
2725
{
2826
private const STORE_FILTER_CACHE_KEY = '_cache_instance_store_filter';
2927

28+
private const TYPE_DOWNLOADABLE = 'downloadable';
29+
3030
/**
3131
* @param PriceProviderPool $priceProviderPool
3232
* @param Discount $discount
@@ -45,18 +45,48 @@ public function __construct(
4545
* @param ContextInterface $context
4646
* @param ResolveInfo $info
4747
* @param array $value
48-
* @throws Exception
49-
* @return mixed|Value
48+
* @return array
49+
* @throws LocalizedException
5050
*/
5151
public function prepare(ContextInterface $context, ResolveInfo $info, array $value): array
52+
{
53+
$store = $context->getExtensionAttributes()->getStore();
54+
$product = $this->getProduct($value, $context, $store);
55+
56+
$requestedFields = $info->getFieldSelection(10);
57+
$returnArray = [];
58+
59+
$returnArray['minimum_price'] = ($requestedFields['minimum_price'] ?? 0) ? ($this->canShowPrice($product) ?
60+
$this->getMinimumProductPrice($product, $store) : $this->formatEmptyResult()) : $this->formatEmptyResult();
61+
$returnArray['maximum_price'] = ($requestedFields['maximum_price'] ?? 0) ? ($this->canShowPrice($product) ?
62+
$this->getMaximumProductPrice($product, $store) : $this->formatEmptyResult()) : $this->formatEmptyResult();
63+
64+
if ($product->getTypeId() === self::TYPE_DOWNLOADABLE &&
65+
$product->getData('links_purchased_separately')) {
66+
$downloadableLinkPrice = (float)$this->getDownloadableLinkPrice($product);
67+
if ($downloadableLinkPrice > 0) {
68+
$returnArray['maximum_price']['regular_price']['value'] += $downloadableLinkPrice;
69+
$returnArray['maximum_price']['final_price']['value'] += $downloadableLinkPrice;
70+
}
71+
}
72+
73+
return $returnArray;
74+
}
75+
76+
/**
77+
* Validate and return product
78+
*
79+
* @param array $value
80+
* @param ContextInterface $context
81+
* @param StoreInterface $store
82+
* @return Product
83+
* @throws LocalizedException
84+
*/
85+
private function getProduct(array $value, ContextInterface $context, StoreInterface $store): Product
5286
{
5387
if (!isset($value['model'])) {
5488
throw new LocalizedException(__('"model" value should be specified'));
5589
}
56-
/** @var StoreInterface $store */
57-
$store = $context->getExtensionAttributes()->getStore();
58-
59-
/** @var Product $product */
6090
$product = $value['model'];
6191
$product->unsetData('minimal_price');
6292
// add store filter for the product
@@ -69,15 +99,28 @@ public function prepare(ContextInterface $context, ResolveInfo $info, array $val
6999
}
70100
}
71101

72-
$requestedFields = $info->getFieldSelection(10);
73-
$returnArray = [];
102+
return $product;
103+
}
74104

75-
$returnArray['minimum_price'] = ($requestedFields['minimum_price'] ?? 0) ? ($this->canShowPrice($product) ?
76-
$this->getMinimumProductPrice($product, $store) : $this->formatEmptyResult()) : $this->formatEmptyResult();
77-
$returnArray['maximum_price'] = ($requestedFields['maximum_price'] ?? 0) ? ($this->canShowPrice($product) ?
78-
$this->getMaximumProductPrice($product, $store) : $this->formatEmptyResult()) : $this->formatEmptyResult();
105+
/**
106+
* Get the downloadable link price
107+
*
108+
* @param Product $product
109+
* @return float
110+
*/
111+
private function getDownloadableLinkPrice(Product $product): float
112+
{
113+
$downloadableLinks = $product->getTypeInstance()->getLinks($product);
114+
if (empty($downloadableLinks)) {
115+
return 0.0;
116+
}
79117

80-
return $returnArray;
118+
$price = 0.0;
119+
foreach ($downloadableLinks as $link) {
120+
$price += (float)$link->getPrice();
121+
}
122+
123+
return $price;
81124
}
82125

83126
/**
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
<?php
2+
/**
3+
* Copyright 2025 Adobe
4+
* All Rights Reserved.
5+
*/
6+
declare(strict_types=1);
7+
8+
namespace Magento\CatalogGraphQl\Model\Resolver\Product;
9+
10+
use Magento\Framework\Escaper;
11+
use Magento\Framework\Exception\LocalizedException;
12+
use Magento\Framework\GraphQl\Config\Element\Field;
13+
use Magento\Framework\GraphQl\Query\ResolverInterface;
14+
use Magento\Framework\GraphQl\Schema\Type\ResolveInfo;
15+
16+
class ProductName implements ResolverInterface
17+
{
18+
/**
19+
* ProductName constructor
20+
*
21+
* @param Escaper $escaper
22+
*/
23+
public function __construct(
24+
private readonly Escaper $escaper
25+
) {
26+
}
27+
28+
/**
29+
* @inheritdoc
30+
*
31+
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
32+
*/
33+
public function resolve(
34+
Field $field,
35+
$context,
36+
ResolveInfo $info,
37+
array $value = null,
38+
array $args = null
39+
): string {
40+
if (!isset($value['model'])) {
41+
throw new LocalizedException(__('"model" value should be specified'));
42+
}
43+
44+
// Product name allowed with special characters
45+
return str_replace(
46+
"&apos;",
47+
"'",
48+
str_replace("&amp;", "&", $this->escaper->escapeUrl($value['model']->getName()))
49+
);
50+
}
51+
}

Diff for: app/code/Magento/CatalogGraphQl/etc/schema.graphqls

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
# Copyright © Magento, Inc. All rights reserved.
2-
# See COPYING.txt for license details.
1+
# Copyright 2025 Adobe
2+
# All Rights Reserved.
33

44
type Query {
55
products (
@@ -93,7 +93,7 @@ interface ProductLinksInterface @typeResolver(class: "Magento\\CatalogGraphQl\\M
9393
interface ProductInterface @typeResolver(class: "Magento\\CatalogGraphQl\\Model\\ProductInterfaceTypeResolverComposite") @doc(description: "Contains fields that are common to all types of products.") {
9494
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")
9595
uid: ID! @doc(description: "The unique ID for a `ProductInterface` object.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\EntityIdToUid")
96-
name: String @doc(description: "The product name. Customers use this name to identify the product.")
96+
name: String @doc(description: "The product name. Customers use this name to identify the product.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\ProductName")
9797
sku: String @doc(description: "A number or code assigned to a product to identify the product, options, price, and manufacturer.")
9898
description: ComplexTextValue @doc(description: "Detailed information about the product. The value can include simple HTML tags.") @resolver(class: "\\Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\ProductComplexTextAttribute")
9999
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")

Diff for: app/code/Magento/CustomerGraphQl/Model/Resolver/UpdateCustomerEmail.php

+4-5
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<?php
22
/**
3-
* Copyright © Magento, Inc. All rights reserved.
4-
* See COPYING.txt for license details.
3+
* Copyright 2025 Adobe
4+
* All Rights Reserved.
55
*/
66
declare(strict_types=1);
77

@@ -77,6 +77,7 @@ public function resolve(
7777
}
7878

7979
$customer = $this->getCustomer->execute($context);
80+
$customer->setData('ignore_validation_flag', true);
8081
$this->updateCustomerAccount->execute(
8182
$customer,
8283
[
@@ -86,8 +87,6 @@ public function resolve(
8687
$context->getExtensionAttributes()->getStore()
8788
);
8889

89-
$data = $this->extractCustomerData->execute($customer);
90-
91-
return ['customer' => $data];
90+
return ['customer' => $this->extractCustomerData->execute($customer)];
9291
}
9392
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
<?php
2+
/**
3+
* Copyright 2025 Adobe
4+
* All Rights Reserved.
5+
*/
6+
declare(strict_types=1);
7+
8+
namespace Magento\Downloadable\Test\Fixture;
9+
10+
use Magento\Catalog\Api\ProductRepositoryInterface;
11+
use Magento\Catalog\Test\Fixture\Product;
12+
use Magento\Downloadable\Model\Link;
13+
use Magento\Downloadable\Model\Product\Type;
14+
use Magento\Framework\App\Filesystem\DirectoryList;
15+
use Magento\Framework\DataObject;
16+
use Magento\Framework\Exception\FileSystemException;
17+
use Magento\Framework\Exception\LocalizedException;
18+
use Magento\Framework\Filesystem\Io\File;
19+
use Magento\TestFramework\Fixture\Api\DataMerger;
20+
use Magento\TestFramework\Fixture\Api\ServiceFactory;
21+
use Magento\TestFramework\Fixture\Data\ProcessorInterface;
22+
23+
class DownloadableProduct extends Product
24+
{
25+
private const DEFAULT_DATA = [
26+
'type_id' => Type::TYPE_DOWNLOADABLE,
27+
'name' => 'DownloadableProduct%uniqid%',
28+
'sku' => 'downloadable-product%uniqid%',
29+
'price' => 0.00,
30+
'links_purchased_separately' => 1,
31+
'links_title' => 'Downloadable Links%uniqid%',
32+
'links_exist' => 0,
33+
'extension_attributes' => [
34+
'website_ids' => [1],
35+
'stock_item' => [
36+
'use_config_manage_stock' => true,
37+
'qty' => 100,
38+
'is_qty_decimal' => false,
39+
'is_in_stock' => true,
40+
],
41+
'downloadable_product_links' => [],
42+
'downloadable_product_samples' => null
43+
],
44+
];
45+
46+
/**
47+
* DownloadableProduct constructor
48+
*
49+
* @param ServiceFactory $serviceFactory
50+
* @param ProcessorInterface $dataProcessor
51+
* @param DataMerger $dataMerger
52+
* @param ProductRepositoryInterface $productRepository
53+
* @param DirectoryList $directoryList
54+
* @param Link $link
55+
* @param File $file
56+
*/
57+
public function __construct(
58+
private readonly ServiceFactory $serviceFactory,
59+
private readonly ProcessorInterface $dataProcessor,
60+
private readonly DataMerger $dataMerger,
61+
private readonly ProductRepositoryInterface $productRepository,
62+
private readonly DirectoryList $directoryList,
63+
private readonly Link $link,
64+
private readonly File $file
65+
) {
66+
parent::__construct($serviceFactory, $dataProcessor, $dataMerger, $productRepository);
67+
}
68+
69+
/**
70+
* @inheritdoc
71+
*
72+
* @throws FileSystemException
73+
* @throws LocalizedException
74+
*/
75+
public function apply(array $data = []): ?DataObject
76+
{
77+
return parent::apply($this->prepareData($data));
78+
}
79+
80+
/**
81+
* Prepare product data
82+
*
83+
* @param array $data
84+
* @return array
85+
* @throws FileSystemException
86+
* @throws LocalizedException
87+
*/
88+
private function prepareData(array $data): array
89+
{
90+
$data = $this->dataMerger->merge(self::DEFAULT_DATA, $data);
91+
92+
// Remove common properties not needed for downloadable products
93+
unset($data['weight']);
94+
95+
// Prepare downloadable links
96+
$links = $this->prepareLinksData($data);
97+
$data['extension_attributes']['downloadable_product_links'] = $links;
98+
$data['links_exist'] = count($links);
99+
100+
return $this->dataProcessor->process($this, $data);
101+
}
102+
103+
/**
104+
* Prepare links data
105+
*
106+
* @param array $data
107+
* @return array
108+
* @throws FileSystemException
109+
* @throws LocalizedException
110+
*/
111+
private function prepareLinksData(array $data): array
112+
{
113+
$links = [];
114+
foreach ($data['extension_attributes']['downloadable_product_links'] as $link) {
115+
$links[] = [
116+
'id' => null,
117+
'title' => $link['title'] ?? 'Test Link%uniqid%',
118+
'price' => $link['price'] ?? 0,
119+
'link_type' => $link['link_type'] ?? 'file',
120+
'link_url' => null,
121+
'link_file' => $this->generateDownloadableLink($link['link_file'] ?? 'test-' . uniqid() . '.txt'),
122+
'is_shareable' => $link['is_shareable'] ?? 0,
123+
'number_of_downloads' => $link['number_of_downloads'] ?? 5,
124+
'sort_order' => $link['sort_order'] ?? 10,
125+
];
126+
}
127+
128+
return $links;
129+
}
130+
131+
/**
132+
* Generate downloadable link file
133+
*
134+
* @param string $fileName
135+
* @return string
136+
* @throws FileSystemException|LocalizedException
137+
*/
138+
public function generateDownloadableLink(string $fileName): string
139+
{
140+
try {
141+
$subDir = sprintf('%s/%s', $fileName[0], $fileName[1]);
142+
$mediaPath = sprintf(
143+
'%s/%s/%s',
144+
$this->directoryList->getPath(DirectoryList::MEDIA),
145+
$this->link->getBasePath(),
146+
$subDir
147+
);
148+
$this->file->checkAndCreateFolder($mediaPath);
149+
$this->file->write(sprintf('%s/%s', $mediaPath, $fileName), "This is a temporary text file.");
150+
151+
return sprintf('/%s/%s', $subDir, $fileName);
152+
} catch (FileSystemException $e) {
153+
throw new FileSystemException(__($e->getMessage()));
154+
} catch (LocalizedException $e) {
155+
throw new LocalizedException(__($e->getMessage()));
156+
}
157+
}
158+
}

Diff for: app/code/Magento/GraphQl/etc/di.xml

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<?xml version="1.0"?>
22
<!--
33
/**
4-
* Copyright 2017 Adobe
4+
* Copyright 2024 Adobe
55
* All Rights Reserved.
66
*/
77
-->

0 commit comments

Comments
 (0)