Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
19.30% covered (danger)
19.30%
22 / 114
0.00% covered (danger)
0.00%
0 / 5
CRAP
0.00% covered (danger)
0.00%
0 / 1
Functions
19.30% covered (danger)
19.30%
22 / 114
0.00% covered (danger)
0.00%
0 / 5
188.29
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 log_deprecated_functions_only_once_per_day
78.57% covered (warning)
78.57%
22 / 28
0.00% covered (danger)
0.00%
0 / 1
4.16
 log_deprecated_arguments_only_once_per_day
0.00% covered (danger)
0.00%
0 / 28
0.00% covered (danger)
0.00%
0 / 1
20
 log_doing_it_wrong_only_once_per_day
0.00% covered (danger)
0.00%
0 / 27
0.00% covered (danger)
0.00%
0 / 1
20
 log_deprecated_hook_only_once_per_day
0.00% covered (danger)
0.00%
0 / 30
0.00% covered (danger)
0.00%
0 / 1
30
1<?php
2/**
3 * Hook into the behaviour of WordPress functions.php.
4 *
5 * Use transients to log errors only once per day, with more detail than before.
6 * i.e. to prevent logs being flooded with deprecation warnings.
7 *
8 * Doing this here means we can set the log level per plugin without relying on WP_DEBUG being true.
9 *
10 * @since      1.0.0
11 *
12 * @package brianhenryie/bh-wp-logger
13 * @author  Brian Henry <BrianHenryIE@gmail.com>
14 */
15
16namespace BrianHenryIE\WP_Logger\WP_Includes;
17
18use BrianHenryIE\WP_Logger\API_Interface;
19use BrianHenryIE\WP_Logger\Logger_Settings_Interface;
20use Psr\Log\LoggerAwareTrait;
21use Psr\Log\LoggerInterface;
22use Psr\Log\NullLogger;
23
24/**
25 * Intercepts WordPress's logging to prevent duplicate logs and to add detail.
26 *
27 * @see _deprecated_function()
28 * @see _deprecated_argument()
29 * @see _doing_it_wrong()
30 * @see _deprecated_hook()
31 */
32class Functions {
33
34    use LoggerAwareTrait;
35
36    /**
37     * Constructor.
38     * No logic, just assignments.
39     *
40     * @param API_Interface             $api The logger's utility functions. Used to check the backtrace to see is it relevant to this plugin.
41     * @param Logger_Settings_Interface $settings The logger settings. Not used here.
42     * @param ?LoggerInterface          $logger PSR logger.
43     */
44    public function __construct(
45        protected API_Interface $api,
46        protected Logger_Settings_Interface $settings,
47        LoggerInterface $logger
48    ) {
49        $this->setLogger( $logger );
50    }
51
52    /**
53     * Limits logging deprecated functions to once per day.
54     * Logs more detail than usual.
55     *
56     * @hooked deprecated_function_run
57     *
58     * @param string $function_name    The function that was called.
59     * @param string $replacement_function Optional. The function that should have been called. Default empty.
60     * @param string $version     The version of WordPress that deprecated the function.
61     *
62     * @see _deprecated_function()
63     */
64    public function log_deprecated_functions_only_once_per_day( $function_name, $replacement_function, $version ): void {
65
66        if ( ! $this->api->is_backtrace_contains_plugin( implode( '', func_get_args() ) ) ) {
67            return;
68        }
69
70        $plugin_slug = $this->settings->get_plugin_slug();
71
72        $transient_name = "log_deprecated_function_{$function_name}_{$plugin_slug}";
73
74        $recently_logged = get_transient( $transient_name );
75
76        if ( empty( $recently_logged ) ) {
77
78            if ( $replacement_function ) {
79                $log_message =
80                    sprintf(
81                    /* translators: 1: PHP function name, 2: Version number, 3: Alternative function name. */
82                        __( '%1$s is <strong>deprecated</strong> since version %2$s! Use %3$s instead.' ),
83                        $function_name,
84                        $version,
85                        $replacement_function
86                    );
87            } else {
88                $log_message =
89                    sprintf(
90                    /* translators: 1: PHP function name, 2: Version number. */
91                        __( '%1$s is <strong>deprecated</strong> since version %2$s with no alternative available.' ),
92                        $function_name,
93                        $version
94                    );
95            }
96
97            $context = array(
98                'function'             => $function_name,
99                'replacement_function' => $replacement_function,
100                'version'              => $version,
101            );
102
103            $this->logger->warning( $log_message, $context );
104
105            set_transient( $transient_name, $log_message, DAY_IN_SECONDS );
106        }
107
108        // Suppress WordPress's own logging.
109        add_filter( 'deprecated_function_trigger_error', '__return_false' );
110    }
111
112    /**
113     * Limits logging deprecated arguments to once per day.
114     * Logs more detail than usual.
115     *
116     * @hooked deprecated_function_run
117     *
118     * @param string $function_name The function that was called.
119     * @param string $message  A message regarding the change.
120     * @param string $version  The version of WordPress that deprecated the argument used.
121     *
122     * @see _deprecated_argument()
123     */
124    public function log_deprecated_arguments_only_once_per_day( $function_name, $message, $version ): void {
125
126        if ( ! $this->api->is_backtrace_contains_plugin( implode( '', func_get_args() ) ) ) {
127            return;
128        }
129
130        $plugin_slug = $this->settings->get_plugin_slug();
131
132        $transient_name = "log_deprecated_argument_{$function_name}_{$plugin_slug}";
133
134        $recently_logged = get_transient( $transient_name );
135
136        if ( empty( $recently_logged ) ) {
137
138            if ( $message ) {
139                $log_message =
140                    sprintf(
141                    /* translators: 1: PHP function name, 2: Version number, 3: Optional message regarding the change. */
142                        __( '%1$s was called with an argument that is <strong>deprecated</strong> since version %2$s! %3$s' ),
143                        $function_name,
144                        $version,
145                        $message
146                    );
147            } else {
148                $log_message =
149                    sprintf(
150                    /* translators: 1: PHP function name, 2: Version number. */
151                        __( '%1$s was called with an argument that is <strong>deprecated</strong> since version %2$s with no alternative available.' ),
152                        $function_name,
153                        $version
154                    );
155            }
156
157            $context = array(
158                'function' => $function_name,
159                'message'  => $message,
160                'version'  => $version,
161            );
162
163            $this->logger->warning( $log_message, $context );
164
165            set_transient( $transient_name, $log_message, DAY_IN_SECONDS );
166        }
167
168        add_filter( 'deprecated_argument_trigger_error', '__return_false' );
169    }
170
171    /**
172     * `_doing_it_wrong` runs e.g. when a function is called before a required action has run.
173     * This function limits logging `_doing_it_wrong` errors to once per day.
174     * Logs more detail than usual.
175     *
176     * @hooked doing_it_wrong_run
177     * @see _doing_it_wrong()
178     *
179     * @hooked doing_it_wrong_run
180     *
181     * @param string $function_name The function that was called.
182     * @param string $message  A message explaining what has been done incorrectly.
183     * @param string $version  The version of WordPress where the message was added.
184     */
185    public function log_doing_it_wrong_only_once_per_day( $function_name, $message, $version ): void {
186
187        if ( ! $this->api->is_backtrace_contains_plugin( implode( '', func_get_args() ) ) ) {
188            return;
189        }
190
191        $plugin_slug = $this->settings->get_plugin_slug();
192
193        $transient_name = "log_doing_it_wrong_run_{$function_name}_{$plugin_slug}";
194
195        $recently_logged = get_transient( $transient_name );
196
197        if ( empty( $recently_logged ) ) {
198
199            if ( $version ) {
200                /* translators: %s: Version number. */
201                $version = sprintf( __( '(This message was added in version %s.)' ), $version );
202            }
203
204            $message .= ' ' . sprintf(
205                /* translators: %s: Documentation URL. */
206                __( 'Please see <a href="%s">Debugging in WordPress</a> for more information.' ),
207                __( 'https://wordpress.org/support/article/debugging-in-wordpress/' )
208            );
209
210            $log_message =
211                sprintf(
212                /* translators: Developer debugging message. 1: PHP function name, 2: Explanatory message, 3: WordPress version number. */
213                    __( '%1$s was called <strong>incorrectly</strong>. %2$s %3$s' ),
214                    $function_name,
215                    $message,
216                    $version
217                );
218
219            $context = array(
220                'function' => $function_name,
221                'message'  => $message,
222                'version'  => $version,
223            );
224
225            $this->logger->warning( $log_message, $context );
226
227            set_transient( $transient_name, $log_message, DAY_IN_SECONDS );
228        }
229
230        add_filter( 'doing_it_wrong_trigger_error', '__return_false' );
231    }
232
233    /**
234     * Log deprecated hooks called by this plugin only once per day.
235     *
236     * When a deprecated hook is called, WordPress will call this function.
237     * If the backtrace does not contain this logger's plugin, it will return early.
238     * Otherwise:
239     * * Check for a transient indicating we have logged this already
240     * * If absent, create one
241     * * Prevent WordPress from logging the deprecation warning (w/filter)
242     *
243     * @hooked deprecated_hook_run
244     * @see _deprecated_hook()
245     *
246     * @param string $hook        The hook that was called.
247     * @param string $replacement The hook that should be used as a replacement.
248     * @param string $version     The version of WordPress that deprecated the argument used.
249     * @param string $message     A message regarding the change.
250     */
251    public function log_deprecated_hook_only_once_per_day( $hook, $replacement, $version, $message ): void {
252
253        if ( ! $this->api->is_backtrace_contains_plugin( implode( '', func_get_args() ) ) ) {
254            return;
255        }
256
257        $plugin_slug = $this->settings->get_plugin_slug();
258
259        $transient_name = "log_deprecated_hook_run_{$hook}_{$plugin_slug}";
260
261        $recently_logged = get_transient( $transient_name );
262
263        if ( empty( $recently_logged ) ) {
264
265            $message = empty( $message ) ? '' : ' ' . $message;
266
267            if ( $replacement ) {
268                $log_message =
269                    sprintf(
270                    /* translators: 1: WordPress hook name, 2: Version number, 3: Alternative hook name. */
271                        __( '%1$s is <strong>deprecated</strong> since version %2$s! Use %3$s instead.' ),
272                        $hook,
273                        $version,
274                        $replacement
275                    ) . $message;
276
277            } else {
278                $log_message =
279                    sprintf(
280                    /* translators: 1: WordPress hook name, 2: Version number. */
281                        __( '%1$s is <strong>deprecated</strong> since version %2$s with no alternative available.' ),
282                        $hook,
283                        $version
284                    ) . $message;
285            }
286
287            $context = array(
288                'hook'        => $hook,
289                'replacement' => $replacement,
290                'version'     => $version,
291                'message'     => $message,
292            );
293
294            $this->logger->warning( $log_message, $context );
295
296            set_transient( $transient_name, $log_message, DAY_IN_SECONDS );
297        }
298
299        add_filter( 'deprecated_hook_trigger_error', '__return_false' );
300    }
301}