feat: display product options

This commit is contained in:
Nikita Kiselev
2025-07-15 19:52:04 +03:00
parent 08d2453df9
commit f47bb46751
17 changed files with 678 additions and 53 deletions

View File

@@ -1,5 +1,7 @@
<?php
use App\Adapters\OcImageTool;
use App\Adapters\OcModelCatalogProductAdapter;
use App\ApplicationFactory;
use Cart\Currency;
use Cart\Tax;
@@ -20,9 +22,8 @@ class Controllerextensiontgshophandle extends Controller
{
$app = ApplicationFactory::create([
'oc_config_tax' => $this->config->get('config_tax'),
'oc_currency' => $this->session->data['currency'],
'oc_default_currency' => $this->config->get('config_currency'),
'timezone' => $this->config->get('config_timezone', 'UTC'),
'lang' => $this->config->get('config_admin_language'),
'language_id' => (int)$this->config->get('config_language_id'),
'shop_base_url' => HTTPS_SERVER,
'dir_image' => DIR_IMAGE,
@@ -42,18 +43,21 @@ class Controllerextensiontgshophandle extends Controller
return $this->url;
});
$app->bind(Currency::class, function () { return $this->currency; });
$app->bind(Tax::class, function () { return $this->tax; });
$app->bind(Currency::class, function () {
return $this->currency;
});
$app->bind(Tax::class, function () {
return $this->tax;
});
$this->load->model('tool/image');
$app->bind('image_resize', function () {
return function ($path, $width, $height) {
if (is_file(DIR_IMAGE . $path)) {
return $this->model_tool_image->resize($path, $width, $height);
}
$app->bind(OcModelCatalogProductAdapter::class, function () {
$this->load->model('catalog/product');
return new OcModelCatalogProductAdapter($this->model_catalog_product);
});
return $this->model_tool_image->resize('no_image.png', $width, $height);
};
$app->bind(OcImageTool::class, function () {
$this->load->model('tool/image');
return new OcImageTool($this->model_tool_image);
});
$this->load->model('checkout/order');
@@ -62,6 +66,8 @@ class Controllerextensiontgshophandle extends Controller
return $this->model_checkout_order;
});
$this->session->data['language'] = $this->config->get('config_language');
$app->bootAndHandleRequest();
}
}

View File

@@ -0,0 +1,295 @@
<?php
namespace Openguru\OpenCartFramework\Collection;
use ArrayAccess;
use InvalidArgumentException;
use ArrayIterator;
use IteratorAggregate;
use Countable;
use JsonSerializable;
/**
* A generic collection for managing objects of a specific type.
*
* @template T
* @implements IteratorAggregate<T>
*/
class Collection implements IteratorAggregate, Countable, ArrayAccess, JsonSerializable
{
/** @var array<T> */
protected array $items;
/** @var string|null */
protected ?string $expectedClass;
/**
* Constructor.
*
* @param array $items Initial items for the collection.
* @param string|null $expectedClass The expected class type for the items.
* @throws InvalidArgumentException
*/
public function __construct(array $items = [], ?string $expectedClass = null)
{
$this->expectedClass = $expectedClass;
foreach ($items as $item) {
$this->validateItem($item);
}
$this->items = $items;
}
/**
* Add an item to the collection.
*
* @param mixed $item
* @return self
* @throws InvalidArgumentException
*/
public function add($item): self
{
$this->validateItem($item);
$this->items[] = $item;
return $this;
}
/**
* Get all items.
*
* @return array<T>
*/
public function all(): array
{
return $this->items;
}
/**
* Filter items based on a callback.
*
* @param callable $callback
* @return self
*/
public function filter(callable $callback): self
{
return new self(array_values(array_filter($this->items, $callback)), $this->expectedClass);
}
/**
* Apply a callback to each item and return a new collection.
*
* @param callable $callback
* @return array
*/
public function map(callable $callback): array
{
return array_map($callback, $this->items);
}
/**
* Reduce the collection to a single value.
*
* @param callable $callback
* @param mixed $initial
* @return mixed
*/
public function reduce(callable $callback, $initial)
{
return array_reduce($this->items, $callback, $initial);
}
/**
* Find an item by a specific property and value.
*
* @param string $property
* @param mixed $value
* @return T|null
*/
public function findByProperty(string $property, $value)
{
foreach ($this->items as $item) {
if (is_object($item) && property_exists($item, $property) && $item->$property === $value) {
return $item;
}
}
return null;
}
/**
* Check if an item exists with a specific property and value.
*
* @param string $property
* @param mixed $value
* @return bool
*/
public function hasValue(string $property, $value): bool
{
return $this->findByProperty($property, $value) !== null;
}
/**
* Validate an item type.
*
* @param mixed $item
* @throws InvalidArgumentException
*/
private function validateItem($item): void
{
if ($this->expectedClass !== null && !($item instanceof $this->expectedClass)) {
throw new InvalidArgumentException(
sprintf(
'Item must be an instance of %s, %s given.',
$this->expectedClass,
is_object($item) ? get_class($item) : gettype($item)
)
);
}
}
/**
* Get an iterator for the collection.
*/
public function getIterator(): ArrayIterator
{
return new ArrayIterator($this->items);
}
/**
* Count the number of items in the collection.
*
* @return int
*/
public function count(): int
{
return count($this->items);
}
/**
* Check if an offset exists.
*
* @param mixed $offset
* @return bool
*/
public function offsetExists($offset): bool
{
return isset($this->items[$offset]);
}
/**
* Get an item at a specific offset.
*
* @param mixed $offset
* @return mixed
*/
public function offsetGet($offset)
{
return $this->items[$offset] ?? null;
}
/**
* Set an item at a specific offset.
*
* @param mixed $offset
* @param mixed $value
*/
public function offsetSet($offset, $value): void
{
$this->validateItem($value);
if ($offset === null) {
$this->items[] = $value;
} else {
$this->items[$offset] = $value;
}
}
/**
* Unset an item at a specific offset.
*
* @param mixed $offset
*/
public function offsetUnset($offset): void
{
unset($this->items[$offset]);
}
public function getValueByProperty(string $property, $propertyValue)
{
$item = $this->findByProperty($property, $propertyValue);
return $item ? $item->getValue() : null;
}
/**
* @return T|null
*/
public function first()
{
return $this->items[0] ?? null;
}
public function getFirstItemValue(string $field, $default = null)
{
if ($this->first() === null) {
return $default;
}
return $this->first()->{$field};
}
public function groupBy(callable $callback): array
{
$grouped = [];
foreach ($this->items as $item) {
$key = $callback($item);
$grouped[$key][] = $item;
}
return $grouped;
}
public function keyByField(callable $callback): array
{
$items = [];
foreach ($this->items as $item) {
$key = $callback($item);
$items[$key] = $item;
}
return $items;
}
public function toArray(): array
{
return $this->all();
}
public function pluck(string $field): array
{
$result = [];
foreach ($this->items as $item) {
if (is_object($item)) {
$result[] = $item->$field;
} else {
$result[] = $item[$field];
}
}
return $result;
}
public function jsonSerialize(): array
{
return $this->all();
}
public function append(array $items): Collection
{
return new Collection(array_merge($this->items, $items), $this->expectedClass);
}
}

View File

@@ -51,7 +51,7 @@ if (!function_exists('config')) {
}
}
if (! function_exists('dd')) {
if (!function_exists('dd')) {
function dd(): void
{
Utils::dd(func_get_args());

View File

@@ -0,0 +1,30 @@
<?php
namespace App\Adapters;
use Proxy;
use RuntimeException;
class OcImageTool
{
private Proxy $image;
public function __construct(Proxy $image)
{
$this->image = $image;
}
public function resize(string $path, int $width, int $height, ?string $default = null): ?string
{
if ($path && is_file(DIR_IMAGE . $path)) {
$image = $path;
return $this->image->resize($image, $width, $height);
}
if ($default && is_file(DIR_IMAGE . $default)) {
return $this->image->resize($default, $width, $height);
}
return null;
}
}

View File

@@ -0,0 +1,22 @@
<?php
namespace App\Adapters;
use ModelCatalogProduct;
use Proxy;
class OcModelCatalogProductAdapter
{
/** @var Proxy|ModelCatalogProduct */
private Proxy $model;
public function __construct(Proxy $model)
{
$this->model = $model;
}
public function getProductOptions(int $productId): array
{
return $this->model->getProductOptions($productId);
}
}

View File

@@ -2,8 +2,7 @@
namespace App\Handlers;
use Closure;
use Openguru\OpenCartFramework\Application;
use App\Adapters\OcImageTool;
use Openguru\OpenCartFramework\Http\JsonResponse;
use Openguru\OpenCartFramework\Http\Request;
use Openguru\OpenCartFramework\QueryBuilder\Builder;
@@ -12,10 +11,12 @@ use Openguru\OpenCartFramework\QueryBuilder\JoinClause;
class CategoriesHandler
{
private Builder $queryBuilder;
private OcImageTool $imageTool;
public function __construct(Builder $queryBuilder)
public function __construct(Builder $queryBuilder, OcImageTool $ocImageTool)
{
$this->queryBuilder = $queryBuilder;
$this->ocImageTool = $ocImageTool;
}
public function index(Request $request): JsonResponse
@@ -47,16 +48,9 @@ class CategoriesHandler
->limit($perPage)
->get();
/** @var Closure $resize */
$resize = Application::getInstance()->get('image_resize');
return new JsonResponse([
'data' => array_map(static function ($category) use ($resize, $imageWidth, $imageHeight) {
if ($category['image']) {
$image = $resize($category['image'], $imageWidth, $imageHeight);
} else {
$image = $resize('no_image.png', $imageWidth, $imageHeight);
}
'data' => array_map(function ($category) use ($imageWidth, $imageHeight) {
$image = $this->ocImageTool->resize($category['image'], $imageWidth, $imageHeight, 'no_image.png');
return [
'id' => (int)$category['id'],

View File

@@ -2,13 +2,14 @@
namespace App\Handlers;
use App\Adapters\OcImageTool;
use App\Adapters\OcModelCatalogProductAdapter;
use Cart\Currency;
use Cart\Tax;
use Closure;
use Openguru\OpenCartFramework\Application;
use Openguru\OpenCartFramework\Config\Settings;
use Openguru\OpenCartFramework\Http\JsonResponse;
use Openguru\OpenCartFramework\Http\Request;
use Openguru\OpenCartFramework\Http\Response;
use Openguru\OpenCartFramework\QueryBuilder\Builder;
use Openguru\OpenCartFramework\QueryBuilder\JoinClause;
use Openguru\OpenCartFramework\Support\Arr;
@@ -19,13 +20,23 @@ class ProductsHandler
private Currency $currency;
private Tax $tax;
private Settings $settings;
private OcModelCatalogProductAdapter $ocModelCatalogProduct;
private OcImageTool $ocImageTool;
public function __construct(Builder $queryBuilder, Currency $currency, Tax $tax, Settings $settings)
{
public function __construct(
Builder $queryBuilder,
Currency $currency,
Tax $tax,
Settings $settings,
OcModelCatalogProductAdapter $ocModelCatalogProduct,
OcImageTool $ocImageTool
) {
$this->queryBuilder = $queryBuilder;
$this->currency = $currency;
$this->tax = $tax;
$this->settings = $settings;
$this->ocModelCatalogProduct = $ocModelCatalogProduct;
$this->ocImageTool = $ocImageTool;
}
public function handle(Request $request): JsonResponse
@@ -93,26 +104,24 @@ class ProductsHandler
->get();
}
/** @var Closure $resize */
$resize = Application::getInstance()->get('image_resize');
$productsImagesMap = [];
foreach ($productsImages as $item) {
$productsImagesMap[$item['product_id']][] = [
'url' => $resize($item['image'], $imageWidth, $imageHeight),
'url' => $this->ocImageTool->resize($item['image'], $imageWidth, $imageHeight, 'placeholder.png'),
'alt' => 'Product Image',
];
}
return new JsonResponse([
'data' => array_map(function ($product) use ($resize, $productsImagesMap, $imageWidth, $imageHeight) {
'data' => array_map(function ($product) use ($productsImagesMap, $imageWidth, $imageHeight) {
$allImages = [];
if ($product['product_image']) {
$image = $resize($product['product_image'], $imageWidth, $imageHeight);
} else {
$image = $resize('placeholder.png', $imageWidth, $imageHeight);
}
$image = $this->ocImageTool->resize(
$product['product_image'],
$imageWidth,
$imageHeight,
'placeholder.png'
);
$allImages[] = [
'url' => $image,
@@ -125,7 +134,7 @@ class ProductsHandler
$product['tax_class_id'],
$this->settings->get('oc_config_tax'),
),
$this->settings->get('oc_currency'),
$this->settings->get('oc_default_currency'),
);
if (!empty($productsImagesMap[$product['product_id']])) {
@@ -173,7 +182,7 @@ class ProductsHandler
->where('product_description.language_id', '=', $languageId);
}
)
->join(
->leftJoin(
db_table('manufacturer') . ' AS manufacturer',
function (JoinClause $join) use ($languageId) {
$join->on('products.manufacturer_id', '=', 'manufacturer.manufacturer_id');
@@ -183,6 +192,10 @@ class ProductsHandler
->limit(1)
->firstOrNull();
if (!$product) {
return new JsonResponse([], Response::HTTP_NOT_FOUND);
}
$productsImages = $this->queryBuilder->newQuery()
->select([
'products_images.product_id' => 'product_id',
@@ -193,17 +206,20 @@ class ProductsHandler
->where('products_images.product_id', '=', $productId)
->get();
/** @var Closure $resize */
$resize = Application::getInstance()->get('image_resize');
$images = [];
$images[] = [
'url' => $resize($product['product_image'], $imageWidth, $imageHeight),
'url' => $this->ocImageTool->resize(
$product['product_image'],
$imageWidth,
$imageHeight,
'placeholder.png'
),
'alt' => $product['product_name'],
];
foreach ($productsImages as $item) {
$images[] = [
'url' => $resize($item['image'], $imageWidth, $imageHeight),
'url' => $this->ocImageTool->resize($item['image'], $imageWidth, $imageHeight, 'placeholder.png'),
'alt' => $product['product_name'],
];
}
@@ -214,7 +230,7 @@ class ProductsHandler
$product['tax_class_id'],
$this->settings->get('oc_config_tax'),
),
$this->settings->get('oc_currency'),
$this->settings->get('oc_default_currency'),
);
$data = [
@@ -224,10 +240,64 @@ class ProductsHandler
'manufacturer' => $product['product_manufacturer'],
'price' => $price,
'images' => $images,
'options' => $this->loadProductOptions($product),
];
return new JsonResponse([
'data' => $data,
]);
}
private function loadProductOptions($product): array
{
$result = [];
$productId = $product['product_id'];
$taxClassId = $product['tax_class_id'];
$options = $this->ocModelCatalogProduct->getProductOptions($productId);
$ocConfigTax = $this->settings->get('oc_config_tax');
$ocDefaultCurrency = $this->settings->get('oc_default_currency');
foreach ($options as $option) {
$product_option_value_data = [];
foreach ($option['product_option_value'] as $option_value) {
if (!$option_value['subtract'] || ($option_value['quantity'] > 0)) {
if ((float)$option_value['price']) {
$priceWithTax = $this->tax->calculate(
$option_value['price'],
$taxClassId,
$ocConfigTax ? 'Р' : false,
);
$price = $this->currency->format($priceWithTax, $ocDefaultCurrency);
} else {
$price = false;
}
$product_option_value_data[] = [
'product_option_value_id' => (int)$option_value['product_option_value_id'],
'option_value_id' => (int)$option_value['option_value_id'],
'name' => $option_value['name'],
'image' => $this->ocImageTool->resize($option_value['image'], 50, 50),
'price' => $price,
'price_prefix' => $option_value['price_prefix'],
'selected' => false,
];
}
}
$result[] = [
'product_option_id' => (int)$option['product_option_id'],
'values' => $product_option_value_data,
'option_id' => (int)$option['option_id'],
'name' => $option['name'],
'type' => $option['type'],
'value' => $option['value'],
'required' => filter_var($option['required'], FILTER_VALIDATE_BOOLEAN),
];
}
return $result;
}
}