Skip to content

Commit

Permalink
Product class
Browse files Browse the repository at this point in the history
Centralizes all basic product-related querying and overriding.
(Not including attributes or pricing.)

Closes #6367

The easiest override/notifier hook is now `NOTIFY_GET_PRODUCT_OBJECT_DETAILS`
  • Loading branch information
drbyte committed May 15, 2024
1 parent 8dbd105 commit 72db492
Show file tree
Hide file tree
Showing 27 changed files with 687 additions and 357 deletions.
303 changes: 303 additions & 0 deletions includes/classes/Product.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,303 @@
<?php
declare(strict_types=1);

/**
* @copyright Copyright 2003-2024 Zen Cart Development Team
* @license http://www.zen-cart.com/license/2_0.txt GNU Public License V2.0
* @version $Id: DrByte New in 2.0.0 $
*
* @var language $lng
* @var queryFactory $db
*/

use Zencart\Traits\NotifierManager;

class Product
{
use NotifierManager;

protected array $data;
protected array $languages;

/** @deprecated use ->get('property') or ->getData() */
public array $fields;

/** @deprecated use !exists() */
public bool $EOF = true;

public function __construct(protected ?int $product_id = null)
{
$this->initLanguages();

if ($this->product_id !== null) {
$this->data = $this->loadProductDetails($this->product_id);

// set some backward compatibility properties
$this->fields = $this->data;
$this->EOF = false;
}
}

public function forLanguage(?int $language_id): self
{
$this->data = $this->getDataForLanguage($language_id);
$this->fields = $this->data;

return $this;
}

public function withDefaultLanguage(): self
{
$this->data = $this->getDataForLanguage();
$this->fields = $this->data;

return $this;
}

public function getData(): ?array
{
return $this->data;
}

public function get(string $name)
{
return $this->data[$name] ?? $this->data['lang'][$this->languages[(int)$_SESSION['languages_id']]] ?? null;
}

public function getDataForLanguage(?int $language_id = null): ?array
{
if (empty($language_id)) {
$language_id = (int)$_SESSION['languages_id'];
}
$data = $this->data;

// strip all languages except specified one, and merge into parent array instead of sub-array
foreach($data['lang'][$this->languages[$language_id]] as $key => $value) {
$data[$key] = $value;
}
unset($data['lang']);

return $data;
}

public function getId(): ?int
{
return $this->product_id;
}

public function exists(): bool
{
return !empty($this->product_id);
}
public function isValid(): bool
{
return !empty($this->data);
}

public function isLinked(): bool
{
return ($this->data['linked_categories_count'] ?? 0) > 0;
}

public function isVirtual(): bool
{
return ($this->data['products_virtual'] ?? 0) === '1';
}

public function isAlwaysFreeShipping(): bool
{
return ($this->data['product_is_always_free_shipping'] ?? '') === '1';
}

public function isGiftVoucher(): bool
{
return str_starts_with($this->data['products_model'] ?? '', 'GIFT');
}

public function allowsAddToCart(): bool
{
if (empty($this->data)) {
return false;
}

$allow_add_to_cart = ($this->data['allow_add_to_cart'] ?? 'N') !== 'N';

if ($allow_add_to_cart && $this->isGiftVoucher()) {
// if GV feature disabled, can't allow GV's to be added to cart
if (!defined('MODULE_ORDER_TOTAL_GV_STATUS') || MODULE_ORDER_TOTAL_GV_STATUS !== 'true') {
$allow_add_to_cart = false;
}
}

$this->notify('NOTIFY_GET_PRODUCT_ALLOW_ADD_TO_CART', $this->product_id, $allow_add_to_cart, $this->data);

// test for boolean and for 'Y', since observer might try to return 'Y'
return in_array($allow_add_to_cart, [true, 'Y'], true);
}

public function getProductQuantity(): int|float
{
$quantity = $this->data['products_quantity'] ?? '0';
$this->notify('NOTIFY_GET_PRODUCT_QUANTITY', $this->product_id, $quantity);
return zen_str_to_numeric((string)$quantity);
}

public function getTypeHandler(): string
{
return ($this->data['type_handler'] ?? 'product');
}

public function getInfoPage(): string
{
return $this->getTypeHandler() . '_info';
}

public function hasPriceQuantityDiscounts(): bool
{
if (empty($this->data)) {
return false;
}

global $db;
$sql = "SELECT products_id FROM " . TABLE_PRODUCTS_DISCOUNT_QUANTITY . " WHERE products_id=" . (int)$this->product_id;
$results = $db->Execute($sql, 1);
return !$results->EOF;
}

public function hasPriceSpecials()
{
if (empty($this->data)) {
return false;
}

global $db;
$sql = "SELECT products_id FROM " . TABLE_SPECIALS . " WHERE products_id=" . (int)$this->product_id;
$results = $db->Execute($sql, 1);
return !$results->EOF;
}

public function priceIsByAttribute(): bool
{
return ($this->data['products_priced_by_attribute'] ?? '0') === '1';
}

public function priceIsFree(): bool
{
return ($this->data['product_is_free'] ?? '0') === '1';
}

public function priceIsCall(): bool
{
return ($this->data['product_is_call'] ?? '0') === '1';
}

public function __get(string $name)
{
return $this->get($name);
}

protected function loadProductDetails(int $product_id, ?int $language_id = null): array
{
global $db;

$sql = "SELECT p.*, pt.allow_add_to_cart, pt.type_handler, m.manufacturers_name, m.manufacturers_image
FROM " . TABLE_PRODUCTS . " p
LEFT JOIN " . TABLE_PRODUCT_TYPES . " pt ON (p.products_type = pt.type_id)
LEFT JOIN " . TABLE_MANUFACTURERS . " m USING (manufacturers_id)
WHERE p.products_id = " . (int)$product_id;
$product = $db->Execute($sql, 1, true, 900);

if ($product->EOF) {
return [];
}

$data = $product->fields;
$data['id'] = $data['products_id'];
$data['product_id'] = $data['products_id'];
$data['info_page'] = $data['type_handler'] . '_info';
//$data['parent_category_id'] = $data['master_categories_id'];

/**
* Add $data['lang'][code] = [products_name, products_description, etc] for each language
*/
$sql = "SELECT pd.*
FROM " . TABLE_PRODUCTS_DESCRIPTION . " pd
WHERE pd.products_id = " . (int)$product_id . "
ORDER BY language_id";
$pd = $db->Execute($sql, null, true, 900);
foreach ($pd as $result) {
unset($result['products_id']);
$data['lang'][$this->languages[$result['language_id']]] = $result;
}

// count linked categories
$sql = "SELECT categories_id FROM " . TABLE_PRODUCTS_TO_CATEGORIES . " ptc WHERE products_id=" . (int)$product_id;
$results = $db->Execute($sql, null, true, 900);
$data['linked_categories_count'] = $results->RecordCount();
$data['linked_categories'] = [];
foreach($results as $result) {
$data['linked_categories'][] = $result['categories_id'];
}

// get cPath
$categories = [];
zen_get_parent_categories($categories, $data['master_categories_id']);
$categories = array_reverse($categories);
$categories[] = $data['master_categories_id'];
$data['cPath'] = implode('_', $categories);


//Allow an observer to modify details
$this->notify('NOTIFY_GET_PRODUCT_OBJECT_DETAILS', $product_id, $data);

return $data;
}

protected function initLanguages(): void
{
global $lng;

if ($lng === null) {
$lng = new language();
}

$this->languages = $lng->get_language_list(); // [1 => 'en', 2 => 'fr']
}
}


/* This class essentially deprecates the following functions (note Notifier hook differences):
zen_get_product_details (er, well, it's now a helper to access this class)
zen_get_products_category_id
zen_products_id_valid
zen_get_products_name
zen_get_products_model
zen_get_products_status
zen_get_product_is_linked
zen_get_products_stock (*)
zen_get_products_manufacturers_name
zen_get_products_manufacturers_image
zen_get_products_manufacturers_id
zen_get_products_url
zen_get_products_description
zen_get_info_page
zen_get_products_type
zen_get_products_image (er, well, must call zen_image yourself)
zen_get_products_virtual
zen_get_products_allow_add_to_cart
zen_get_product_is_always_free_shipping
zen_products_lookup
zen_get_parent_category_id
zen_has_product_discounts
zen_has_product_specials
zen_get_product_path
zen_get_products_price_is_free
zen_get_products_price_is_call
zen_get_products_price_is_priced_by_attributes
zen_get_products_quantity_order_min
zen_get_products_quantity_order_units
zen_get_products_quantity_order_max
zen_get_products_qty_box_status
zen_get_products_quantity_mixed
*/
14 changes: 7 additions & 7 deletions includes/classes/shopping_cart.php
Original file line number Diff line number Diff line change
Expand Up @@ -677,7 +677,7 @@ public function calculate()
$qty = $data['qty'];
$prid = zen_get_prid($uprid);

$product = zen_get_product_details($prid);
$product = (new Product($prid))->withDefaultLanguage();
if ($product->EOF) {
$this->removeUprid($uprid);
continue;
Expand Down Expand Up @@ -1231,7 +1231,7 @@ public function get_products(bool $check_for_valid_cart = false)
$products_array = [];
foreach ($this->contents as $uprid => $data) {
$prid = zen_get_prid($uprid);
$products = zen_get_product_details($prid);
$products = (new Product($prid))->withDefaultLanguage();
if ($products->EOF) {
$this->removeUprid($uprid);
continue;
Expand Down Expand Up @@ -1489,7 +1489,7 @@ public function get_content_type($gv_only = false)
} else {
foreach ($this->contents as $uprid => $data) {
$prid = (int)$uprid;
$free_ship_check = zen_get_product_details($prid);
$free_ship_check = (new Product($prid))->withDefaultLanguage();
$free_ship_check = $free_ship_check->fields;

if (str_starts_with($free_ship_check['products_model'], 'GIFT')) {
Expand Down Expand Up @@ -1594,7 +1594,7 @@ public function in_cart_mixed($uprid_to_check)
}

// check if mixed is on
$product = zen_get_product_details((int)$uprid_to_check);
$product = (new Product((int)$uprid_to_check))->withDefaultLanguage();

// if mixed attributes is off return qty for current attribute selection
if ($product->fields['products_quantity_mixed'] === '0') {
Expand Down Expand Up @@ -1630,7 +1630,7 @@ public function in_cart_mixed_discount_quantity($uprid_to_check)
}

// check if mixed is on
$product = zen_get_product_details((int)$uprid_to_check);
$product = (new Product((int)$uprid_to_check))->withDefaultLanguage();

// if mixed attributes is off return qty for current attribute selection
if ($product->fields['products_mixed_discount_quantity'] === '0') {
Expand Down Expand Up @@ -1671,7 +1671,7 @@ public function in_cart_check($check_what, $check_value = '1')
$in_cart_check_qty = 0;
foreach ($this->contents as $uprid => $data) {
// check if field it true
$product_check = zen_get_product_details(zen_get_prid($uprid));
$product_check = (new Product(zen_get_prid($uprid)))->withDefaultLanguage();
if (array_key_exists($check_what, $product_check->fields) && (string)$product_check->fields[$check_what] === (string)$check_value) {
$in_cart_check_qty += $data['qty'];
}
Expand Down Expand Up @@ -2651,7 +2651,7 @@ public function in_cart_product_mixed_changed($product_id, $chk = false)
}

// check if mixed is on
$product = zen_get_product_details((int)$pr_id);
$product = (new Product((int)$pr_id))->withDefaultLanguage();

// if mixed attributes is off identify that this product is the last of its kind (which is also the first of its kind).
if (empty($product->fields['products_quantity_mixed'])) {
Expand Down

0 comments on commit 72db492

Please sign in to comment.