Нет описания

class-wc-shipping-zone.php 13KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463
  1. <?php
  2. /**
  3. * Represents a single shipping zone
  4. *
  5. * @since 2.6.0
  6. * @version 3.0.0
  7. * @package WooCommerce\Classes
  8. */
  9. defined( 'ABSPATH' ) || exit;
  10. require_once __DIR__ . '/legacy/class-wc-legacy-shipping-zone.php';
  11. /**
  12. * WC_Shipping_Zone class.
  13. */
  14. class WC_Shipping_Zone extends WC_Legacy_Shipping_Zone {
  15. /**
  16. * Zone ID
  17. *
  18. * @var int|null
  19. */
  20. protected $id = null;
  21. /**
  22. * This is the name of this object type.
  23. *
  24. * @var string
  25. */
  26. protected $object_type = 'shipping_zone';
  27. /**
  28. * Zone Data.
  29. *
  30. * @var array
  31. */
  32. protected $data = array(
  33. 'zone_name' => '',
  34. 'zone_order' => 0,
  35. 'zone_locations' => array(),
  36. );
  37. /**
  38. * Constructor for zones.
  39. *
  40. * @param int|object $zone Zone ID to load from the DB or zone object.
  41. */
  42. public function __construct( $zone = null ) {
  43. if ( is_numeric( $zone ) && ! empty( $zone ) ) {
  44. $this->set_id( $zone );
  45. } elseif ( is_object( $zone ) ) {
  46. $this->set_id( $zone->zone_id );
  47. } elseif ( 0 === $zone || '0' === $zone ) {
  48. $this->set_id( 0 );
  49. } else {
  50. $this->set_object_read( true );
  51. }
  52. $this->data_store = WC_Data_Store::load( 'shipping-zone' );
  53. if ( false === $this->get_object_read() ) {
  54. $this->data_store->read( $this );
  55. }
  56. }
  57. /**
  58. * --------------------------------------------------------------------------
  59. * Getters
  60. * --------------------------------------------------------------------------
  61. */
  62. /**
  63. * Get zone name.
  64. *
  65. * @param string $context View or edit context.
  66. * @return string
  67. */
  68. public function get_zone_name( $context = 'view' ) {
  69. return $this->get_prop( 'zone_name', $context );
  70. }
  71. /**
  72. * Get zone order.
  73. *
  74. * @param string $context View or edit context.
  75. * @return int
  76. */
  77. public function get_zone_order( $context = 'view' ) {
  78. return $this->get_prop( 'zone_order', $context );
  79. }
  80. /**
  81. * Get zone locations.
  82. *
  83. * @param string $context View or edit context.
  84. * @return array of zone objects
  85. */
  86. public function get_zone_locations( $context = 'view' ) {
  87. return $this->get_prop( 'zone_locations', $context );
  88. }
  89. /**
  90. * Return a text string representing what this zone is for.
  91. *
  92. * @param int $max Max locations to return.
  93. * @param string $context View or edit context.
  94. * @return string
  95. */
  96. public function get_formatted_location( $max = 10, $context = 'view' ) {
  97. $location_parts = array();
  98. $all_continents = WC()->countries->get_continents();
  99. $all_countries = WC()->countries->get_countries();
  100. $all_states = WC()->countries->get_states();
  101. $locations = $this->get_zone_locations( $context );
  102. $continents = array_filter( $locations, array( $this, 'location_is_continent' ) );
  103. $countries = array_filter( $locations, array( $this, 'location_is_country' ) );
  104. $states = array_filter( $locations, array( $this, 'location_is_state' ) );
  105. $postcodes = array_filter( $locations, array( $this, 'location_is_postcode' ) );
  106. foreach ( $continents as $location ) {
  107. $location_parts[] = $all_continents[ $location->code ]['name'];
  108. }
  109. foreach ( $countries as $location ) {
  110. $location_parts[] = $all_countries[ $location->code ];
  111. }
  112. foreach ( $states as $location ) {
  113. $location_codes = explode( ':', $location->code );
  114. $location_parts[] = $all_states[ $location_codes[0] ][ $location_codes[1] ];
  115. }
  116. foreach ( $postcodes as $location ) {
  117. $location_parts[] = $location->code;
  118. }
  119. // Fix display of encoded characters.
  120. $location_parts = array_map( 'html_entity_decode', $location_parts );
  121. if ( count( $location_parts ) > $max ) {
  122. $remaining = count( $location_parts ) - $max;
  123. // @codingStandardsIgnoreStart
  124. return sprintf( _n( '%s and %d other region', '%s and %d other regions', $remaining, 'woocommerce' ), implode( ', ', array_splice( $location_parts, 0, $max ) ), $remaining );
  125. // @codingStandardsIgnoreEnd
  126. } elseif ( ! empty( $location_parts ) ) {
  127. return implode( ', ', $location_parts );
  128. } else {
  129. return __( 'Everywhere', 'woocommerce' );
  130. }
  131. }
  132. /**
  133. * Get shipping methods linked to this zone.
  134. *
  135. * @param bool $enabled_only Only return enabled methods.
  136. * @param string $context Getting shipping methods for what context. Valid values, admin, json.
  137. * @return array of objects
  138. */
  139. public function get_shipping_methods( $enabled_only = false, $context = 'admin' ) {
  140. if ( null === $this->get_id() ) {
  141. return array();
  142. }
  143. $raw_methods = $this->data_store->get_methods( $this->get_id(), $enabled_only );
  144. $wc_shipping = WC_Shipping::instance();
  145. $allowed_classes = $wc_shipping->get_shipping_method_class_names();
  146. $methods = array();
  147. foreach ( $raw_methods as $raw_method ) {
  148. if ( in_array( $raw_method->method_id, array_keys( $allowed_classes ), true ) ) {
  149. $class_name = $allowed_classes[ $raw_method->method_id ];
  150. $instance_id = $raw_method->instance_id;
  151. // The returned array may contain instances of shipping methods, as well
  152. // as classes. If the "class" is an instance, just use it. If not,
  153. // create an instance.
  154. if ( is_object( $class_name ) ) {
  155. $class_name_of_instance = get_class( $class_name );
  156. $methods[ $instance_id ] = new $class_name_of_instance( $instance_id );
  157. } else {
  158. // If the class is not an object, it should be a string. It's better
  159. // to double check, to be sure (a class must be a string, anything)
  160. // else would be useless.
  161. if ( is_string( $class_name ) && class_exists( $class_name ) ) {
  162. $methods[ $instance_id ] = new $class_name( $instance_id );
  163. }
  164. }
  165. // Let's make sure that we have an instance before setting its attributes.
  166. if ( is_object( $methods[ $instance_id ] ) ) {
  167. $methods[ $instance_id ]->method_order = absint( $raw_method->method_order );
  168. $methods[ $instance_id ]->enabled = $raw_method->is_enabled ? 'yes' : 'no';
  169. $methods[ $instance_id ]->has_settings = $methods[ $instance_id ]->has_settings();
  170. $methods[ $instance_id ]->settings_html = $methods[ $instance_id ]->supports( 'instance-settings-modal' ) ? $methods[ $instance_id ]->get_admin_options_html() : false;
  171. $methods[ $instance_id ]->method_description = wp_kses_post( wpautop( $methods[ $instance_id ]->method_description ) );
  172. }
  173. if ( 'json' === $context ) {
  174. // We don't want the entire object in this context, just the public props.
  175. $methods[ $instance_id ] = (object) get_object_vars( $methods[ $instance_id ] );
  176. unset( $methods[ $instance_id ]->instance_form_fields, $methods[ $instance_id ]->form_fields );
  177. }
  178. }
  179. }
  180. uasort( $methods, 'wc_shipping_zone_method_order_uasort_comparison' );
  181. return apply_filters( 'woocommerce_shipping_zone_shipping_methods', $methods, $raw_methods, $allowed_classes, $this );
  182. }
  183. /**
  184. * --------------------------------------------------------------------------
  185. * Setters
  186. * --------------------------------------------------------------------------
  187. */
  188. /**
  189. * Set zone name.
  190. *
  191. * @param string $set Value to set.
  192. */
  193. public function set_zone_name( $set ) {
  194. $this->set_prop( 'zone_name', wc_clean( $set ) );
  195. }
  196. /**
  197. * Set zone order. Value to set.
  198. *
  199. * @param int $set Value to set.
  200. */
  201. public function set_zone_order( $set ) {
  202. $this->set_prop( 'zone_order', absint( $set ) );
  203. }
  204. /**
  205. * Set zone locations.
  206. *
  207. * @since 3.0.0
  208. * @param array $locations Value to set.
  209. */
  210. public function set_zone_locations( $locations ) {
  211. if ( 0 !== $this->get_id() ) {
  212. $this->set_prop( 'zone_locations', $locations );
  213. }
  214. }
  215. /**
  216. * --------------------------------------------------------------------------
  217. * Other
  218. * --------------------------------------------------------------------------
  219. */
  220. /**
  221. * Save zone data to the database.
  222. *
  223. * @return int
  224. */
  225. public function save() {
  226. if ( ! $this->get_zone_name() ) {
  227. $this->set_zone_name( $this->generate_zone_name() );
  228. }
  229. if ( ! $this->data_store ) {
  230. return $this->get_id();
  231. }
  232. /**
  233. * Trigger action before saving to the DB. Allows you to adjust object props before save.
  234. *
  235. * @param WC_Data $this The object being saved.
  236. * @param WC_Data_Store_WP $data_store THe data store persisting the data.
  237. */
  238. do_action( 'woocommerce_before_' . $this->object_type . '_object_save', $this, $this->data_store );
  239. if ( null !== $this->get_id() ) {
  240. $this->data_store->update( $this );
  241. } else {
  242. $this->data_store->create( $this );
  243. }
  244. /**
  245. * Trigger action after saving to the DB.
  246. *
  247. * @param WC_Data $this The object being saved.
  248. * @param WC_Data_Store_WP $data_store THe data store persisting the data.
  249. */
  250. do_action( 'woocommerce_after_' . $this->object_type . '_object_save', $this, $this->data_store );
  251. return $this->get_id();
  252. }
  253. /**
  254. * Generate a zone name based on location.
  255. *
  256. * @return string
  257. */
  258. protected function generate_zone_name() {
  259. $zone_name = $this->get_formatted_location();
  260. if ( empty( $zone_name ) ) {
  261. $zone_name = __( 'Zone', 'woocommerce' );
  262. }
  263. return $zone_name;
  264. }
  265. /**
  266. * Location type detection.
  267. *
  268. * @param object $location Location to check.
  269. * @return boolean
  270. */
  271. private function location_is_continent( $location ) {
  272. return 'continent' === $location->type;
  273. }
  274. /**
  275. * Location type detection.
  276. *
  277. * @param object $location Location to check.
  278. * @return boolean
  279. */
  280. private function location_is_country( $location ) {
  281. return 'country' === $location->type;
  282. }
  283. /**
  284. * Location type detection.
  285. *
  286. * @param object $location Location to check.
  287. * @return boolean
  288. */
  289. private function location_is_state( $location ) {
  290. return 'state' === $location->type;
  291. }
  292. /**
  293. * Location type detection.
  294. *
  295. * @param object $location Location to check.
  296. * @return boolean
  297. */
  298. private function location_is_postcode( $location ) {
  299. return 'postcode' === $location->type;
  300. }
  301. /**
  302. * Is passed location type valid?
  303. *
  304. * @param string $type Type to check.
  305. * @return boolean
  306. */
  307. public function is_valid_location_type( $type ) {
  308. return in_array( $type, apply_filters( 'woocommerce_valid_location_types', array( 'postcode', 'state', 'country', 'continent' ) ), true );
  309. }
  310. /**
  311. * Add location (state or postcode) to a zone.
  312. *
  313. * @param string $code Location code.
  314. * @param string $type state or postcode.
  315. */
  316. public function add_location( $code, $type ) {
  317. if ( 0 !== $this->get_id() && $this->is_valid_location_type( $type ) ) {
  318. if ( 'postcode' === $type ) {
  319. $code = trim( strtoupper( str_replace( chr( 226 ) . chr( 128 ) . chr( 166 ), '...', $code ) ) ); // No normalization - postcodes are matched against both normal and formatted versions to support wildcards.
  320. }
  321. $location = array(
  322. 'code' => wc_clean( $code ),
  323. 'type' => wc_clean( $type ),
  324. );
  325. $zone_locations = $this->get_prop( 'zone_locations', 'edit' );
  326. $zone_locations[] = (object) $location;
  327. $this->set_prop( 'zone_locations', $zone_locations );
  328. }
  329. }
  330. /**
  331. * Clear all locations for this zone.
  332. *
  333. * @param array|string $types of location to clear.
  334. */
  335. public function clear_locations( $types = array( 'postcode', 'state', 'country', 'continent' ) ) {
  336. if ( ! is_array( $types ) ) {
  337. $types = array( $types );
  338. }
  339. $zone_locations = $this->get_prop( 'zone_locations', 'edit' );
  340. foreach ( $zone_locations as $key => $values ) {
  341. if ( in_array( $values->type, $types, true ) ) {
  342. unset( $zone_locations[ $key ] );
  343. }
  344. }
  345. $zone_locations = array_values( $zone_locations ); // reindex.
  346. $this->set_prop( 'zone_locations', $zone_locations );
  347. }
  348. /**
  349. * Set locations.
  350. *
  351. * @param array $locations Array of locations.
  352. */
  353. public function set_locations( $locations = array() ) {
  354. $this->clear_locations();
  355. foreach ( $locations as $location ) {
  356. $this->add_location( $location['code'], $location['type'] );
  357. }
  358. }
  359. /**
  360. * Add a shipping method to this zone.
  361. *
  362. * @param string $type shipping method type.
  363. * @return int new instance_id, 0 on failure
  364. */
  365. public function add_shipping_method( $type ) {
  366. if ( null === $this->get_id() ) {
  367. $this->save();
  368. }
  369. $instance_id = 0;
  370. $wc_shipping = WC_Shipping::instance();
  371. $allowed_classes = $wc_shipping->get_shipping_method_class_names();
  372. $count = $this->data_store->get_method_count( $this->get_id() );
  373. if ( in_array( $type, array_keys( $allowed_classes ), true ) ) {
  374. $instance_id = $this->data_store->add_method( $this->get_id(), $type, $count + 1 );
  375. }
  376. if ( $instance_id ) {
  377. do_action( 'woocommerce_shipping_zone_method_added', $instance_id, $type, $this->get_id() );
  378. }
  379. WC_Cache_Helper::get_transient_version( 'shipping', true );
  380. return $instance_id;
  381. }
  382. /**
  383. * Delete a shipping method from a zone.
  384. *
  385. * @param int $instance_id Shipping method instance ID.
  386. * @return True on success, false on failure
  387. */
  388. public function delete_shipping_method( $instance_id ) {
  389. if ( null === $this->get_id() ) {
  390. return false;
  391. }
  392. // Get method details.
  393. $method = $this->data_store->get_method( $instance_id );
  394. if ( $method ) {
  395. $this->data_store->delete_method( $instance_id );
  396. do_action( 'woocommerce_shipping_zone_method_deleted', $instance_id, $method->method_id, $this->get_id() );
  397. }
  398. WC_Cache_Helper::get_transient_version( 'shipping', true );
  399. return true;
  400. }
  401. }