class-wc-order-item.php 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409
  1. <?php
  2. /**
  3. * Order Item
  4. *
  5. * A class which represents an item within an order and handles CRUD.
  6. * Uses ArrayAccess to be BW compatible with WC_Orders::get_items().
  7. *
  8. * @package WooCommerce/Classes
  9. * @version 3.0.0
  10. * @since 3.0.0
  11. */
  12. defined( 'ABSPATH' ) || exit;
  13. /**
  14. * Order item class.
  15. */
  16. class WC_Order_Item extends WC_Data implements ArrayAccess {
  17. /**
  18. * Order Data array. This is the core order data exposed in APIs since 3.0.0.
  19. *
  20. * @since 3.0.0
  21. * @var array
  22. */
  23. protected $data = array(
  24. 'order_id' => 0,
  25. 'name' => '',
  26. );
  27. /**
  28. * Stores meta in cache for future reads.
  29. * A group must be set to to enable caching.
  30. *
  31. * @var string
  32. */
  33. protected $cache_group = 'order-items';
  34. /**
  35. * Meta type. This should match up with
  36. * the types available at https://codex.wordpress.org/Function_Reference/add_metadata.
  37. * WP defines 'post', 'user', 'comment', and 'term'.
  38. *
  39. * @var string
  40. */
  41. protected $meta_type = 'order_item';
  42. /**
  43. * This is the name of this object type.
  44. *
  45. * @var string
  46. */
  47. protected $object_type = 'order_item';
  48. /**
  49. * Constructor.
  50. *
  51. * @param int|object|array $item ID to load from the DB, or WC_Order_Item object.
  52. */
  53. public function __construct( $item = 0 ) {
  54. parent::__construct( $item );
  55. if ( $item instanceof WC_Order_Item ) {
  56. $this->set_id( $item->get_id() );
  57. } elseif ( is_numeric( $item ) && $item > 0 ) {
  58. $this->set_id( $item );
  59. } else {
  60. $this->set_object_read( true );
  61. }
  62. $type = 'line_item' === $this->get_type() ? 'product' : $this->get_type();
  63. $this->data_store = WC_Data_Store::load( 'order-item-' . $type );
  64. if ( $this->get_id() > 0 ) {
  65. $this->data_store->read( $this );
  66. }
  67. }
  68. /**
  69. * Merge changes with data and clear.
  70. * Overrides WC_Data::apply_changes.
  71. * array_replace_recursive does not work well for order items because it merges taxes instead
  72. * of replacing them.
  73. *
  74. * @since 3.2.0
  75. */
  76. public function apply_changes() {
  77. if ( function_exists( 'array_replace' ) ) {
  78. $this->data = array_replace( $this->data, $this->changes ); // phpcs:ignore PHPCompatibility.PHP.NewFunctions.array_replaceFound
  79. } else { // PHP 5.2 compatibility.
  80. foreach ( $this->changes as $key => $change ) {
  81. $this->data[ $key ] = $change;
  82. }
  83. }
  84. $this->changes = array();
  85. }
  86. /*
  87. |--------------------------------------------------------------------------
  88. | Getters
  89. |--------------------------------------------------------------------------
  90. */
  91. /**
  92. * Get order ID this meta belongs to.
  93. *
  94. * @param string $context What the value is for. Valid values are 'view' and 'edit'.
  95. * @return int
  96. */
  97. public function get_order_id( $context = 'view' ) {
  98. return $this->get_prop( 'order_id', $context );
  99. }
  100. /**
  101. * Get order item name.
  102. *
  103. * @param string $context What the value is for. Valid values are 'view' and 'edit'.
  104. * @return string
  105. */
  106. public function get_name( $context = 'view' ) {
  107. return $this->get_prop( 'name', $context );
  108. }
  109. /**
  110. * Get order item type. Overridden by child classes.
  111. *
  112. * @return string
  113. */
  114. public function get_type() {
  115. return '';
  116. }
  117. /**
  118. * Get quantity.
  119. *
  120. * @return int
  121. */
  122. public function get_quantity() {
  123. return 1;
  124. }
  125. /**
  126. * Get tax status.
  127. *
  128. * @return string
  129. */
  130. public function get_tax_status() {
  131. return 'taxable';
  132. }
  133. /**
  134. * Get tax class.
  135. *
  136. * @return string
  137. */
  138. public function get_tax_class() {
  139. return '';
  140. }
  141. /**
  142. * Get parent order object.
  143. *
  144. * @return WC_Order
  145. */
  146. public function get_order() {
  147. return wc_get_order( $this->get_order_id() );
  148. }
  149. /*
  150. |--------------------------------------------------------------------------
  151. | Setters
  152. |--------------------------------------------------------------------------
  153. */
  154. /**
  155. * Set order ID.
  156. *
  157. * @param int $value Order ID.
  158. */
  159. public function set_order_id( $value ) {
  160. $this->set_prop( 'order_id', absint( $value ) );
  161. }
  162. /**
  163. * Set order item name.
  164. *
  165. * @param string $value Item name.
  166. */
  167. public function set_name( $value ) {
  168. $this->set_prop( 'name', wc_clean( $value ) );
  169. }
  170. /*
  171. |--------------------------------------------------------------------------
  172. | Other Methods
  173. |--------------------------------------------------------------------------
  174. */
  175. /**
  176. * Type checking.
  177. *
  178. * @param string|array $type Type.
  179. * @return boolean
  180. */
  181. public function is_type( $type ) {
  182. return is_array( $type ) ? in_array( $this->get_type(), $type, true ) : $type === $this->get_type();
  183. }
  184. /**
  185. * Calculate item taxes.
  186. *
  187. * @since 3.2.0
  188. * @param array $calculate_tax_for Location data to get taxes for. Required.
  189. * @return bool True if taxes were calculated.
  190. */
  191. public function calculate_taxes( $calculate_tax_for = array() ) {
  192. if ( ! isset( $calculate_tax_for['country'], $calculate_tax_for['state'], $calculate_tax_for['postcode'], $calculate_tax_for['city'] ) ) {
  193. return false;
  194. }
  195. if ( '0' !== $this->get_tax_class() && 'taxable' === $this->get_tax_status() && wc_tax_enabled() ) {
  196. $calculate_tax_for['tax_class'] = $this->get_tax_class();
  197. $tax_rates = WC_Tax::find_rates( $calculate_tax_for );
  198. $taxes = WC_Tax::calc_tax( $this->get_total(), $tax_rates, false );
  199. if ( method_exists( $this, 'get_subtotal' ) ) {
  200. $subtotal_taxes = WC_Tax::calc_tax( $this->get_subtotal(), $tax_rates, false );
  201. $this->set_taxes(
  202. array(
  203. 'total' => $taxes,
  204. 'subtotal' => $subtotal_taxes,
  205. )
  206. );
  207. } else {
  208. $this->set_taxes( array( 'total' => $taxes ) );
  209. }
  210. } else {
  211. $this->set_taxes( false );
  212. }
  213. do_action( 'woocommerce_order_item_after_calculate_taxes', $this, $calculate_tax_for );
  214. return true;
  215. }
  216. /*
  217. |--------------------------------------------------------------------------
  218. | Meta Data Handling
  219. |--------------------------------------------------------------------------
  220. */
  221. /**
  222. * Expands things like term slugs before return.
  223. *
  224. * @param string $hideprefix Meta data prefix, (default: _).
  225. * @param bool $include_all Include all meta data, this stop skip items with values already in the product name.
  226. * @return array
  227. */
  228. public function get_formatted_meta_data( $hideprefix = '_', $include_all = false ) {
  229. $formatted_meta = array();
  230. $meta_data = $this->get_meta_data();
  231. $hideprefix_length = ! empty( $hideprefix ) ? strlen( $hideprefix ) : 0;
  232. $product = is_callable( array( $this, 'get_product' ) ) ? $this->get_product() : false;
  233. $order_item_name = $this->get_name();
  234. foreach ( $meta_data as $meta ) {
  235. if ( empty( $meta->id ) || '' === $meta->value || ! is_scalar( $meta->value ) || ( $hideprefix_length && substr( $meta->key, 0, $hideprefix_length ) === $hideprefix ) ) {
  236. continue;
  237. }
  238. $meta->key = rawurldecode( (string) $meta->key );
  239. $meta->value = rawurldecode( (string) $meta->value );
  240. $attribute_key = str_replace( 'attribute_', '', $meta->key );
  241. $display_key = wc_attribute_label( $attribute_key, $product );
  242. $display_value = wp_kses_post( $meta->value );
  243. if ( taxonomy_exists( $attribute_key ) ) {
  244. $term = get_term_by( 'slug', $meta->value, $attribute_key );
  245. if ( ! is_wp_error( $term ) && is_object( $term ) && $term->name ) {
  246. $display_value = $term->name;
  247. }
  248. }
  249. // Skip items with values already in the product details area of the product name.
  250. if ( ! $include_all && $product && $product->is_type( 'variation' ) && wc_is_attribute_in_product_name( $display_value, $order_item_name ) ) {
  251. continue;
  252. }
  253. $formatted_meta[ $meta->id ] = (object) array(
  254. 'key' => $meta->key,
  255. 'value' => $meta->value,
  256. 'display_key' => apply_filters( 'woocommerce_order_item_display_meta_key', $display_key, $meta, $this ),
  257. 'display_value' => wpautop( make_clickable( apply_filters( 'woocommerce_order_item_display_meta_value', $display_value, $meta, $this ) ) ),
  258. );
  259. }
  260. return apply_filters( 'woocommerce_order_item_get_formatted_meta_data', $formatted_meta, $this );
  261. }
  262. /*
  263. |--------------------------------------------------------------------------
  264. | Array Access Methods
  265. |--------------------------------------------------------------------------
  266. |
  267. | For backwards compatibility with legacy arrays.
  268. |
  269. */
  270. /**
  271. * OffsetSet for ArrayAccess.
  272. *
  273. * @param string $offset Offset.
  274. * @param mixed $value Value.
  275. */
  276. public function offsetSet( $offset, $value ) {
  277. if ( 'item_meta_array' === $offset ) {
  278. foreach ( $value as $meta_id => $meta ) {
  279. $this->update_meta_data( $meta->key, $meta->value, $meta_id );
  280. }
  281. return;
  282. }
  283. if ( array_key_exists( $offset, $this->data ) ) {
  284. $setter = "set_$offset";
  285. if ( is_callable( array( $this, $setter ) ) ) {
  286. $this->$setter( $value );
  287. }
  288. return;
  289. }
  290. $this->update_meta_data( $offset, $value );
  291. }
  292. /**
  293. * OffsetUnset for ArrayAccess.
  294. *
  295. * @param string $offset Offset.
  296. */
  297. public function offsetUnset( $offset ) {
  298. $this->maybe_read_meta_data();
  299. if ( 'item_meta_array' === $offset || 'item_meta' === $offset ) {
  300. $this->meta_data = array();
  301. return;
  302. }
  303. if ( array_key_exists( $offset, $this->data ) ) {
  304. unset( $this->data[ $offset ] );
  305. }
  306. if ( array_key_exists( $offset, $this->changes ) ) {
  307. unset( $this->changes[ $offset ] );
  308. }
  309. $this->delete_meta_data( $offset );
  310. }
  311. /**
  312. * OffsetExists for ArrayAccess.
  313. *
  314. * @param string $offset Offset.
  315. * @return bool
  316. */
  317. public function offsetExists( $offset ) {
  318. $this->maybe_read_meta_data();
  319. if ( 'item_meta_array' === $offset || 'item_meta' === $offset || array_key_exists( $offset, $this->data ) ) {
  320. return true;
  321. }
  322. return array_key_exists( $offset, wp_list_pluck( $this->meta_data, 'value', 'key' ) ) || array_key_exists( '_' . $offset, wp_list_pluck( $this->meta_data, 'value', 'key' ) );
  323. }
  324. /**
  325. * OffsetGet for ArrayAccess.
  326. *
  327. * @param string $offset Offset.
  328. * @return mixed
  329. */
  330. public function offsetGet( $offset ) {
  331. $this->maybe_read_meta_data();
  332. if ( 'item_meta_array' === $offset ) {
  333. $return = array();
  334. foreach ( $this->meta_data as $meta ) {
  335. $return[ $meta->id ] = $meta;
  336. }
  337. return $return;
  338. }
  339. $meta_values = wp_list_pluck( $this->meta_data, 'value', 'key' );
  340. if ( 'item_meta' === $offset ) {
  341. return $meta_values;
  342. } elseif ( 'type' === $offset ) {
  343. return $this->get_type();
  344. } elseif ( array_key_exists( $offset, $this->data ) ) {
  345. $getter = "get_$offset";
  346. if ( is_callable( array( $this, $getter ) ) ) {
  347. return $this->$getter();
  348. }
  349. } elseif ( array_key_exists( '_' . $offset, $meta_values ) ) {
  350. // Item meta was expanded in previous versions, with prefixes removed. This maintains support.
  351. return $meta_values[ '_' . $offset ];
  352. } elseif ( array_key_exists( $offset, $meta_values ) ) {
  353. return $meta_values[ $offset ];
  354. }
  355. return null;
  356. }
  357. }