PluginProbe
WCPOS – Point of Sale (POS) plugin for WooCommerce / trunk
WCPOS – Point of Sale (POS) plugin for WooCommerce vtrunk
1.10.13 1.10.14 1.10.12 1.10.11 1.10.10 1.10.9 1.10.8 untagged-3d9b7ccddc54df87c672 1.10.7 1.10.6 1.10.5 1.10.3 1.10.4 1.10.2 1.10.1 1.10.0 1.9.17 1.9.15 1.9.16 1.9.14 1.9.13 1.9.12 1.9.11 1.9.10 1.9.9 All 158 releases
woocommerce-pos / includes / Logger.php

Logger.php in WCPOS – Point of Sale (POS) plugin for WooCommerce trunk, at includes/Logger.php

228 lines 5.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Logger.
4 *
5 * @package WCPOS\WooCommercePOS
6 */
7
8 namespace WCPOS\WooCommercePOS;
9
10 use function is_string;
11
12 /**
13 * Class Logger
14 *
15 * NOTE: do not put any SQL queries in this class, eg: options table lookup
16 */
17 class Logger {
18 public const WC_LOG_FILENAME = 'woocommerce-pos';
19
20 /**
21 * Logger instance.
22 *
23 * @var \WC_Logger_Interface|null
24 */
25 public static $logger;
26
27 /**
28 * Log level.
29 *
30 * @var string|null
31 */
32 public static $log_level;
33
34 /**
35 * Hash of the last logged message (level + message + context) for dedup.
36 *
37 * @var string|null
38 */
39 private static ?string $last_message_hash = null;
40
41 /**
42 * Log level of the last deduplicated message, so flush writes at the correct level.
43 *
44 * @var string|null
45 */
46 private static ?string $last_message_level = null;
47
48 /**
49 * Number of consecutive duplicate messages suppressed.
50 *
51 * @var int
52 */
53 private static int $repeat_count = 0;
54
55 /**
56 * Whether the shutdown flush hook has been registered.
57 *
58 * @var bool
59 */
60 private static bool $shutdown_registered = false;
61
62 /**
63 * Set the log level.
64 *
65 * @param string $level The log level.
66 */
67 public static function set_log_level( $level ): void {
68 self::$log_level = $level;
69 }
70
71 /**
72 * Utilize WC logger class.
73 *
74 * Suppresses consecutive duplicate messages within the same request.
75 * When a different message arrives, any suppressed count is flushed
76 * as "Previous message repeated N more times".
77 *
78 * @param mixed $message The message to log.
79 * @param mixed $context Optional additional context data to log.
80 */
81 public static function log( $message, $context = null ): void {
82 if ( ! class_exists( 'WC_Logger' ) ) {
83 return;
84 }
85
86 if ( is_null( self::$log_level ) ) {
87 self::$log_level = 'info';
88 }
89
90 if ( ! is_string( $message ) ) {
91 $message = print_r( $message, true ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_print_r
92 }
93
94 $context_string = '';
95 if ( null !== $context ) {
96 $context_string = is_string( $context ) ? $context : print_r( $context, true ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_print_r
97 }
98
99 // Build a hash from level + message + context to detect duplicates.
100 $hash = md5( self::$log_level . '|' . $message . '|' . $context_string );
101
102 if ( null !== self::$last_message_hash && $hash === self::$last_message_hash ) {
103 ++self::$repeat_count;
104
105 return;
106 }
107
108 // Different message — flush any pending repeat count first.
109 self::flush_repeat_count();
110
111 // Now log the new message.
112 self::$last_message_hash = $hash;
113 self::$last_message_level = self::$log_level;
114 self::$repeat_count = 0;
115
116 $full_message = $message;
117 if ( '' !== $context_string ) {
118 $full_message .= ' | Context: ' . $context_string;
119 }
120
121 $written = self::write_log( $full_message );
122
123 // Forward after dedup so "repeated N times" remains one event, and only
124 // when the write itself was allowed: `woocommerce_pos_logging` is how a
125 // site says "do not record this", so it must gate the remote copy too.
126 if ( $written && ( 'error' === self::$log_level || 'critical' === self::$log_level ) ) {
127 try {
128 \WCPOS\WooCommercePOS\Services\Error_Reporter::report_log_error( self::$log_level, $message, $context_string );
129 } catch ( \Throwable $t ) { // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedCatch
130 // Reporting must never break logging.
131 }
132 }
133 }
134
135 /**
136 * Flush any pending repeat count to the log.
137 *
138 * Called when a new (different) message arrives, or at shutdown
139 * to ensure the final repeat count is written.
140 */
141 public static function flush_repeat_count(): void {
142 if ( self::$repeat_count > 0 ) {
143 $saved_level = self::$log_level;
144 self::$log_level = self::$last_message_level ?? 'info';
145
146 self::write_log(
147 sprintf( 'Previous message repeated %d more time(s)', self::$repeat_count )
148 );
149
150 self::$log_level = $saved_level;
151 self::$repeat_count = 0;
152 }
153 }
154
155 /**
156 * Reset deduplication state.
157 *
158 * Used by tests to ensure clean state between test methods.
159 */
160 public static function reset_dedup_state(): void {
161 self::$last_message_hash = null;
162 self::$last_message_level = null;
163 self::$repeat_count = 0;
164 self::$shutdown_registered = false;
165 }
166
167 /**
168 * Log a warning message.
169 *
170 * @param mixed $message The message to log.
171 * @param mixed $context Optional additional context data.
172 */
173 public static function warning( $message, $context = null ): void {
174 $previous_level = self::$log_level;
175 self::$log_level = 'warning';
176 self::log( $message, $context );
177 self::$log_level = $previous_level;
178 }
179
180 /**
181 * Log an error message.
182 *
183 * @param mixed $message The message to log.
184 * @param mixed $context Optional additional context data.
185 */
186 public static function error( $message, $context = null ): void {
187 $previous_level = self::$log_level;
188 self::$log_level = 'error';
189 self::log( $message, $context );
190 self::$log_level = $previous_level;
191 }
192
193 /**
194 * Write a message to the WC logger, applying the logging filter.
195 *
196 * Registers a shutdown hook on first write to flush any remaining
197 * repeat count at end of request.
198 *
199 * @param string $message The formatted message to write.
200 *
201 * @return bool Whether the message was written, so callers can mirror the
202 * `woocommerce_pos_logging` decision instead of re-applying it.
203 */
204 private static function write_log( string $message ): bool {
205 if ( ! apply_filters( 'woocommerce_pos_logging', true, $message ) ) {
206 return false;
207 }
208
209 if ( ! isset( self::$logger ) ) {
210 self::$logger = wc_get_logger();
211 }
212
213 self::$logger->log( self::$log_level, $message, array( 'source' => self::WC_LOG_FILENAME ) );
214
215 if ( ! self::$shutdown_registered ) {
216 self::$shutdown_registered = true;
217 add_action(
218 'shutdown',
219 function () {
220 Logger::flush_repeat_count();
221 }
222 );
223 }
224
225 return true;
226 }
227 }
228