class-wc-shipping-zone.php 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445
  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 '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. // Trigger action before saving to the DB. Allows you to adjust object props before save.
  231. do_action( 'woocommerce_before_' . $this->object_type . '_object_save', $this, $this->data_store );
  232. if ( null === $this->get_id() ) {
  233. $this->data_store->create( $this );
  234. } else {
  235. $this->data_store->update( $this );
  236. }
  237. return $this->get_id();
  238. }
  239. }
  240. /**
  241. * Generate a zone name based on location.
  242. *
  243. * @return string
  244. */
  245. protected function generate_zone_name() {
  246. $zone_name = $this->get_formatted_location();
  247. if ( empty( $zone_name ) ) {
  248. $zone_name = __( 'Zone', 'woocommerce' );
  249. }
  250. return $zone_name;
  251. }
  252. /**
  253. * Location type detection.
  254. *
  255. * @param object $location Location to check.
  256. * @return boolean
  257. */
  258. private function location_is_continent( $location ) {
  259. return 'continent' === $location->type;
  260. }
  261. /**
  262. * Location type detection.
  263. *
  264. * @param object $location Location to check.
  265. * @return boolean
  266. */
  267. private function location_is_country( $location ) {
  268. return 'country' === $location->type;
  269. }
  270. /**
  271. * Location type detection.
  272. *
  273. * @param object $location Location to check.
  274. * @return boolean
  275. */
  276. private function location_is_state( $location ) {
  277. return 'state' === $location->type;
  278. }
  279. /**
  280. * Location type detection.
  281. *
  282. * @param object $location Location to check.
  283. * @return boolean
  284. */
  285. private function location_is_postcode( $location ) {
  286. return 'postcode' === $location->type;
  287. }
  288. /**
  289. * Is passed location type valid?
  290. *
  291. * @param string $type Type to check.
  292. * @return boolean
  293. */
  294. public function is_valid_location_type( $type ) {
  295. return in_array( $type, array( 'postcode', 'state', 'country', 'continent' ), true );
  296. }
  297. /**
  298. * Add location (state or postcode) to a zone.
  299. *
  300. * @param string $code Location code.
  301. * @param string $type state or postcode.
  302. */
  303. public function add_location( $code, $type ) {
  304. if ( 0 !== $this->get_id() && $this->is_valid_location_type( $type ) ) {
  305. if ( 'postcode' === $type ) {
  306. $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.
  307. }
  308. $location = array(
  309. 'code' => wc_clean( $code ),
  310. 'type' => wc_clean( $type ),
  311. );
  312. $zone_locations = $this->get_prop( 'zone_locations', 'edit' );
  313. $zone_locations[] = (object) $location;
  314. $this->set_prop( 'zone_locations', $zone_locations );
  315. }
  316. }
  317. /**
  318. * Clear all locations for this zone.
  319. *
  320. * @param array|string $types of location to clear.
  321. */
  322. public function clear_locations( $types = array( 'postcode', 'state', 'country', 'continent' ) ) {
  323. if ( ! is_array( $types ) ) {
  324. $types = array( $types );
  325. }
  326. $zone_locations = $this->get_prop( 'zone_locations', 'edit' );
  327. foreach ( $zone_locations as $key => $values ) {
  328. if ( in_array( $values->type, $types, true ) ) {
  329. unset( $zone_locations[ $key ] );
  330. }
  331. }
  332. $zone_locations = array_values( $zone_locations ); // reindex.
  333. $this->set_prop( 'zone_locations', $zone_locations );
  334. }
  335. /**
  336. * Set locations.
  337. *
  338. * @param array $locations Array of locations.
  339. */
  340. public function set_locations( $locations = array() ) {
  341. $this->clear_locations();
  342. foreach ( $locations as $location ) {
  343. $this->add_location( $location['code'], $location['type'] );
  344. }
  345. }
  346. /**
  347. * Add a shipping method to this zone.
  348. *
  349. * @param string $type shipping method type.
  350. * @return int new instance_id, 0 on failure
  351. */
  352. public function add_shipping_method( $type ) {
  353. if ( null === $this->get_id() ) {
  354. $this->save();
  355. }
  356. $instance_id = 0;
  357. $wc_shipping = WC_Shipping::instance();
  358. $allowed_classes = $wc_shipping->get_shipping_method_class_names();
  359. $count = $this->data_store->get_method_count( $this->get_id() );
  360. if ( in_array( $type, array_keys( $allowed_classes ), true ) ) {
  361. $instance_id = $this->data_store->add_method( $this->get_id(), $type, $count + 1 );
  362. }
  363. if ( $instance_id ) {
  364. do_action( 'woocommerce_shipping_zone_method_added', $instance_id, $type, $this->get_id() );
  365. }
  366. WC_Cache_Helper::get_transient_version( 'shipping', true );
  367. return $instance_id;
  368. }
  369. /**
  370. * Delete a shipping method from a zone.
  371. *
  372. * @param int $instance_id Shipping method instance ID.
  373. * @return True on success, false on failure
  374. */
  375. public function delete_shipping_method( $instance_id ) {
  376. if ( null === $this->get_id() ) {
  377. return false;
  378. }
  379. // Get method details.
  380. $method = $this->data_store->get_method( $instance_id );
  381. if ( $method ) {
  382. $this->data_store->delete_method( $instance_id );
  383. do_action( 'woocommerce_shipping_zone_method_deleted', $instance_id, $method->method_id, $this->get_id() );
  384. }
  385. WC_Cache_Helper::get_transient_version( 'shipping', true );
  386. return true;
  387. }
  388. }