PageRenderTime 58ms CodeModel.GetById 21ms RepoModel.GetById 0ms app.codeStats 0ms

/wp-content/plugins/woocommerce-subscriptions-master/includes/admin/reports/class-wcs-report-cache-manager.php

https://bitbucket.org/tristangemus/tribe-demo
PHP | 357 lines | 175 code | 62 blank | 120 comment | 32 complexity | b3c6faf088811033acbe7d54dd632acb MD5 | raw file
Possible License(s): GPL-2.0
  1. <?php
  2. /**
  3. * Subscriptions Report Cache Manager
  4. *
  5. * Update report data caches on appropriate events, like renewal order payment.
  6. *
  7. * @class WCS_Cache_Manager
  8. * @since 2.1
  9. * @package WooCommerce Subscriptions/Classes
  10. * @category Class
  11. */
  12. if ( ! defined( 'ABSPATH' ) ) {
  13. exit;
  14. } // Exit if accessed directly
  15. class WCS_Report_Cache_Manager {
  16. /**
  17. * Array of event => report classes to determine which reports need to be updated on certain events.
  18. *
  19. * The index for each report's class is specified as its used later to determine when to schedule the report and we want
  20. * it to be consistently at the same time, regardless of the hook which triggered the cache update. The indexes are based
  21. * on the order of the reports in the menu on the WooCommerce > Reports > Subscriptions screen, which is why the indexes
  22. * are not sequential (because not all reports need caching).
  23. *
  24. */
  25. private $update_events_and_classes = array(
  26. 'woocommerce_subscriptions_reports_schedule_cache_updates' => array( // a custom hook that can be called to schedule a full cache update, used by WC_Subscriptions_Upgrader
  27. 0 => 'WC_Report_Subscription_Events_By_Date',
  28. 1 => 'WC_Report_Upcoming_Recurring_Revenue',
  29. 3 => 'WC_Report_Subscription_By_Product',
  30. 4 => 'WC_Report_Subscription_By_Customer',
  31. ),
  32. 'woocommerce_subscription_payment_complete' => array( // this hook takes care of renewal, switch and initial payments
  33. 0 => 'WC_Report_Subscription_Events_By_Date',
  34. 4 => 'WC_Report_Subscription_By_Customer',
  35. ),
  36. 'woocommerce_subscriptions_switch_completed' => array(
  37. 0 => 'WC_Report_Subscription_Events_By_Date',
  38. ),
  39. 'woocommerce_subscription_status_changed' => array(
  40. 0 => 'WC_Report_Subscription_Events_By_Date', // we really only need cancelled, expired and active status here, but we'll use a more generic hook for convenience
  41. 4 => 'WC_Report_Subscription_By_Customer',
  42. ),
  43. 'woocommerce_subscription_status_active' => array(
  44. 1 => 'WC_Report_Upcoming_Recurring_Revenue',
  45. ),
  46. 'woocommerce_new_order_item' => array(
  47. 3 => 'WC_Report_Subscription_By_Product',
  48. ),
  49. 'woocommerce_update_order_item' => array(
  50. 3 => 'WC_Report_Subscription_By_Product',
  51. ),
  52. );
  53. /**
  54. * Record of all the report calsses to need to have the cache updated during this request. Prevents duplicate updates in the same request for different events.
  55. */
  56. private $reports_to_update = array();
  57. /**
  58. * The hook name to use for our WP-Cron entry for updating report cache.
  59. */
  60. private $cron_hook = 'wcs_report_update_cache';
  61. /**
  62. * The hook name to use for our WP-Cron entry for updating report cache.
  63. */
  64. protected $use_large_site_cache;
  65. /**
  66. * Attach callbacks to manage cache updates
  67. *
  68. * @since 2.1
  69. */
  70. public function __construct() {
  71. // Use the old hooks
  72. if ( WC_Subscriptions::is_woocommerce_pre( '3.0' ) ) {
  73. $hooks = array(
  74. 'woocommerce_order_add_product' => 'woocommerce_new_order_item',
  75. 'woocommerce_order_edit_product' => 'woocommerce_update_order_item',
  76. );
  77. foreach ( $hooks as $old_hook => $new_hook ) {
  78. $this->update_events_and_classes[ $old_hook ] = $this->update_events_and_classes[ $new_hook ];
  79. unset( $this->update_events_and_classes[ $new_hook ] ); // New hooks aren't called, so no need to attach to them
  80. }
  81. }
  82. add_action( $this->cron_hook, array( $this, 'update_cache' ), 10, 1 );
  83. foreach ( $this->update_events_and_classes as $event_hook => $report_classes ) {
  84. add_action( $event_hook, array( $this, 'set_reports_to_update' ), 10 );
  85. }
  86. add_action( 'shutdown', array( $this, 'schedule_cache_updates' ), 10 );
  87. // Notify store owners that report data can be out-of-date
  88. add_action( 'admin_notices', array( $this, 'admin_notices' ), 0 );
  89. // Add system status information.
  90. add_filter( 'wcs_system_status', array( $this, 'add_system_status_info' ) );
  91. }
  92. /**
  93. * Check if the given hook has reports associated with it, and if so, add them to our $this->reports_to_update
  94. * property so we know to schedule an event to update their cache at the end of the request.
  95. *
  96. * This function is attached as a callback on the events in the $update_events_and_classes property.
  97. *
  98. * @since 2.1
  99. * @return null
  100. */
  101. public function set_reports_to_update() {
  102. if ( isset( $this->update_events_and_classes[ current_filter() ] ) ) {
  103. $this->reports_to_update = array_unique( array_merge( $this->reports_to_update, $this->update_events_and_classes[ current_filter() ] ) );
  104. }
  105. }
  106. /**
  107. * At the end of the request, schedule cache updates for any events that occured during this request.
  108. *
  109. * For large sites, cache updates are run only once per day to avoid overloading the DB where the queries are very resource intensive
  110. * (as reported during beta testing in https://github.com/Prospress/woocommerce-subscriptions/issues/1732). We do this at 4am in the
  111. * site's timezone, which helps avoid running the queries during busy periods and also runs them after all the renewals for synchronised
  112. * subscriptions should have finished for the day (which begins at 3am and rarely takes more than 1 hours of processing to get through
  113. * an entire queue).
  114. *
  115. * This function is attached as a callback on 'shutdown' and will schedule cache updates for any reports found to need updates by
  116. * @see $this->set_reports_to_update().
  117. *
  118. * @since 2.1
  119. */
  120. public function schedule_cache_updates() {
  121. if ( ! empty( $this->reports_to_update ) ) {
  122. // On large sites, we want to run the cache update once at 4am in the site's timezone
  123. if ( $this->use_large_site_cache() ) {
  124. $four_am_site_time = new DateTime( '4 am', wcs_get_sites_timezone() );
  125. // Convert to a UTC timestamp for scheduling
  126. $cache_update_timestamp = $four_am_site_time->format( 'U' );
  127. // PHP doesn't support a "next 4am" time format equivalent, so we need to manually handle getting 4am from earlier today (which will always happen when this is run after 4am and before midnight in the site's timezone)
  128. if ( $cache_update_timestamp <= gmdate( 'U' ) ) {
  129. $cache_update_timestamp += DAY_IN_SECONDS;
  130. }
  131. // Schedule one update event for each class to avoid updating cache more than once for the same class for different events
  132. foreach ( $this->reports_to_update as $index => $report_class ) {
  133. $cron_args = array( 'report_class' => $report_class );
  134. if ( false === wp_next_scheduled( $this->cron_hook, $cron_args ) ) {
  135. // Use the index to space out caching of each report to make them 15 minutes apart so that on large sites, where we assume they'll get a request at least once every few minutes, we don't try to update the caches of all reports in the same request
  136. wp_schedule_single_event( $cache_update_timestamp + 15 * MINUTE_IN_SECONDS * ( $index + 1 ), $this->cron_hook, $cron_args );
  137. }
  138. }
  139. } else { // Otherwise, run it 10 minutes after the last cache invalidating event
  140. // Schedule one update event for each class to avoid updating cache more than once for the same class for different events
  141. foreach ( $this->reports_to_update as $index => $report_class ) {
  142. $cron_args = array( 'report_class' => $report_class );
  143. if ( false !== ( $next_scheduled = wp_next_scheduled( $this->cron_hook, $cron_args ) ) ) {
  144. wp_unschedule_event( $next_scheduled, $this->cron_hook, $cron_args );
  145. }
  146. // Use the index to space out caching of each report to make them 5 minutes apart so that on large sites, where we assume they'll get a request at least once every few minutes, we don't try to update the caches of all reports in the same request
  147. wp_schedule_single_event( gmdate( 'U' ) + MINUTE_IN_SECONDS * ( $index + 1 ) * 5, $this->cron_hook, $cron_args );
  148. }
  149. }
  150. }
  151. }
  152. /**
  153. * Update the cache data for a given report, as specified with $report_class, by call it's get_data() method.
  154. *
  155. * @since 2.1
  156. * @return null
  157. */
  158. public function update_cache( $report_class ) {
  159. /**
  160. * Filter whether Report Cache Updates are enabled.
  161. *
  162. * @param bool $enabled Whether report updates are enabled.
  163. * @param string $report_class The report class to use.
  164. */
  165. if ( ! apply_filters( 'wcs_report_cache_updates_enabled', 'yes' === get_option( 'woocommerce_subscriptions_cache_updates_enabled', 'yes' ), $report_class ) ) {
  166. return;
  167. }
  168. // Validate the report class
  169. $valid_report_class = false;
  170. foreach ( $this->update_events_and_classes as $event_hook => $report_classes ) {
  171. if ( in_array( $report_class, $report_classes ) ) {
  172. $valid_report_class = true;
  173. break;
  174. }
  175. }
  176. if ( false === $valid_report_class ) {
  177. return;
  178. }
  179. // Hook our error catcher.
  180. add_action( 'shutdown', array( $this, 'catch_unexpected_shutdown' ) );
  181. // Load report class dependencies
  182. require_once( ABSPATH . 'wp-admin/includes/class-wp-list-table.php' );
  183. require_once( WC()->plugin_path() . '/includes/admin/reports/class-wc-admin-report.php' );
  184. $report_name = strtolower( str_replace( '_', '-', str_replace( 'WC_Report_', '', $report_class ) ) );
  185. $report_path = WCS_Admin_Reports::initialize_reports_path( '', $report_name, $report_class );
  186. require_once( $report_path );
  187. $reflector = new ReflectionMethod( $report_class, 'get_data' );
  188. // Some report classes extend WP_List_Table which has a constructor using methods not available on WP-Cron (and unable to be loaded with a __doing_it_wrong() notice), so they have a static get_data() method and do not need to be instantiated
  189. if ( $reflector->isStatic() ) {
  190. call_user_func( array( $report_class, 'get_data' ), array( 'no_cache' => true ) );
  191. } else {
  192. $report = new $report_class();
  193. // Classes with a non-static get_data() method can be displayed for different time series, so we need to update the cache for each of those ranges
  194. foreach ( array( 'year', 'last_month', 'month', '7day' ) as $range ) {
  195. $report->calculate_current_range( $range );
  196. $report->get_data( array( 'no_cache' => true ) );
  197. }
  198. }
  199. // Remove our error catcher.
  200. remove_action( 'shutdown', array( $this, 'catch_unexpected_shutdown' ) );
  201. }
  202. /**
  203. * Boolean flag to check whether to use a the large site cache method or not, which is determined based on the number of
  204. * subscriptions and orders on the site (using arbitrary counts).
  205. *
  206. * @since 2.1
  207. * @return bool
  208. */
  209. protected function use_large_site_cache() {
  210. if ( null === $this->use_large_site_cache ) {
  211. if ( false == get_option( 'wcs_report_use_large_site_cache' ) ) {
  212. $subscription_counts = (array) wp_count_posts( 'shop_subscription' );
  213. $order_counts = (array) wp_count_posts( 'shop_order' );
  214. if ( array_sum( $subscription_counts ) > 3000 || array_sum( $order_counts ) > 25000 ) {
  215. update_option( 'wcs_report_use_large_site_cache', 'true', false );
  216. $this->use_large_site_cache = true;
  217. } else {
  218. $this->use_large_site_cache = false;
  219. }
  220. } else {
  221. $this->use_large_site_cache = true;
  222. }
  223. }
  224. return apply_filters( 'wcs_report_use_large_site_cache', $this->use_large_site_cache );
  225. }
  226. /**
  227. * Make it clear to store owners that data for some reports can be out-of-date.
  228. *
  229. * @since 2.1
  230. */
  231. public function admin_notices() {
  232. $screen = get_current_screen();
  233. $wc_screen_id = sanitize_title( __( 'WooCommerce', 'woocommerce-subscriptions' ) );
  234. if ( in_array( $screen->id, apply_filters( 'woocommerce_reports_screen_ids', array( $wc_screen_id . '_page_wc-reports', 'dashboard' ) ) ) && isset( $_GET['tab'] ) && 'subscriptions' == $_GET['tab'] && ( ! isset( $_GET['report'] ) || in_array( $_GET['report'], array( 'subscription_events_by_date', 'upcoming_recurring_revenue', 'subscription_by_product', 'subscription_by_customer' ) ) ) && $this->use_large_site_cache() ) {
  235. wcs_add_admin_notice( __( 'Please note: data for this report is cached. The data displayed may be out of date by up to 24 hours. The cache is updated each morning at 4am in your site\'s timezone.', 'woocommerce-subscriptions' ) );
  236. }
  237. }
  238. /**
  239. * Handle error instances that lead to an unexpected shutdown.
  240. *
  241. * This attempts to detect if there was an error, and proactively prevent errors
  242. * from piling up.
  243. *
  244. * @author Jeremy Pry
  245. */
  246. public function catch_unexpected_shutdown() {
  247. $error = error_get_last();
  248. if ( null === $error || ! isset( $error['type'] ) ) {
  249. return;
  250. }
  251. // Check for the error types that matter to us.
  252. if ( $error['type'] & ( E_ERROR | E_PARSE | E_COMPILE_ERROR | E_USER_ERROR | E_RECOVERABLE_ERROR ) ) {
  253. $failures = get_option( 'woocommerce_subscriptions_cache_updates_failures', 0 );
  254. $failures++;
  255. update_option( 'woocommerce_subscriptions_cache_updates_failures', $failures, false );
  256. /**
  257. * Filter the allowed number of detected failures before we turn off cache updates.
  258. *
  259. * @param int $threshold The failure count threshold.
  260. */
  261. if ( $failures > apply_filters( 'woocommerce_subscriptions_cache_updates_failures_threshold', 2 ) ) {
  262. update_option( 'woocommerce_subscriptions_cache_updates_enabled', 'no', false );
  263. }
  264. }
  265. }
  266. /**
  267. * Add system status information to include failure count and cache update status.
  268. *
  269. * @author Jeremy Pry
  270. *
  271. * @param array $data Existing status data.
  272. *
  273. * @return array Filtered status data.
  274. */
  275. public function add_system_status_info( $data ) {
  276. $cache_enabled = ( 'yes' === get_option( 'woocommerce_subscriptions_cache_updates_enabled', 'yes' ) );
  277. $failures = get_option( 'woocommerce_subscriptions_cache_updates_failures', 0 );
  278. $new_data = array(
  279. 'wcs_report_cache_enabled' => array(
  280. 'name' => _x( 'Report Cache Enabled', 'Whether the Report Cache has been enabled', 'woocommerce-subscriptions' ),
  281. 'note' => $cache_enabled ? __( 'Yes', 'woocommerce-subscriptions' ) : __( 'No', 'woocommerce-subscriptions' ),
  282. 'success' => $cache_enabled,
  283. ),
  284. 'wcs_cache_update_failures' => array(
  285. 'name' => __( 'Cache Update Failures', 'woocommerce-subscriptions' ),
  286. /* translators: %d refers to the number of times we have detected cache update failures */
  287. 'note' => sprintf( _n( '%d failures', '%d failure', $failures, 'woocommerce-subscriptions' ), $failures ),
  288. 'success' => 0 === $failures,
  289. ),
  290. );
  291. $data = array_merge( $data, $new_data );
  292. return $data;
  293. }
  294. }
  295. return new WCS_Report_Cache_Manager();