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