1 | <?php |
||
2 | |||
3 | namespace SilverStripe\Dev; |
||
4 | |||
5 | use const DEBUG_BACKTRACE_IGNORE_ARGS; |
||
6 | use function debug_print_backtrace; |
||
7 | use const E_USER_DEPRECATED; |
||
8 | use SilverStripe\Control\Director; |
||
9 | use SilverStripe\Core\Environment; |
||
10 | use SilverStripe\Core\Manifest\Module; |
||
11 | use SilverStripe\Core\Manifest\ModuleLoader; |
||
12 | |||
13 | /** |
||
14 | * Handles raising an notice when accessing a deprecated method |
||
15 | * |
||
16 | * A pattern used in SilverStripe when deprecating a method is to add something like |
||
17 | * user_error('This method is deprecated', E_USER_NOTICE); |
||
18 | * to the method |
||
19 | * |
||
20 | * However sometimes we want to mark that a method will be deprecated in some future version and shouldn't be used in |
||
21 | * new code, but not forbid in the current version - for instance when that method is still heavily used in framework |
||
22 | * or cms. |
||
23 | * |
||
24 | * This class abstracts the above pattern and adds a way to do that. |
||
25 | * |
||
26 | * Each call to notice passes a version that the notice will be valid from. Additionally this class has a notion of the |
||
27 | * version it should use when deciding whether to raise the notice. If that version is equal to or greater than the |
||
28 | * notices version (and SilverStripe is in dev mode) a deprecation message will be raised. |
||
29 | * |
||
30 | * Normally the checking version will be the release version of SilverStripe, but a developer can choose to set it to a |
||
31 | * future version, to see how their code will behave in future versions. |
||
32 | * |
||
33 | * Modules can also set the version for calls they make - either setting it to a future version in order to ensure |
||
34 | * forwards compatibility or setting it backwards if a module has not yet removed references to deprecated methods. |
||
35 | * |
||
36 | * When set per-module, only direct calls to deprecated methods from those modules are considered - if the module |
||
37 | * calls a non-module method which then calls a deprecated method, that call will use the global check version, not |
||
38 | * the module specific check version. |
||
39 | */ |
||
40 | class Deprecation |
||
41 | { |
||
42 | |||
43 | const SCOPE_METHOD = 1; |
||
44 | const SCOPE_CLASS = 2; |
||
45 | const SCOPE_GLOBAL = 4; |
||
46 | |||
47 | /** |
||
48 | * |
||
49 | * @var string |
||
50 | */ |
||
51 | protected static $version; |
||
52 | |||
53 | /** |
||
54 | * Override whether deprecation is enabled. If null, then fallback to |
||
55 | * SS_DEPRECATION_ENABLED, and then true if not defined. |
||
56 | * |
||
57 | * Deprecation is only available on dev. |
||
58 | * |
||
59 | * Must be configured outside of the config API, as deprecation API |
||
60 | * must be available before this to avoid infinite loops. |
||
61 | * |
||
62 | * @var boolean|null |
||
63 | */ |
||
64 | protected static $enabled = null; |
||
65 | |||
66 | /** |
||
67 | * |
||
68 | * @var array |
||
69 | */ |
||
70 | protected static $module_version_overrides = array(); |
||
71 | |||
72 | /** |
||
73 | * @var int - the notice level to raise on a deprecation notice. Defaults to E_USER_DEPRECATED if that exists, |
||
74 | * E_USER_NOTICE if not |
||
75 | */ |
||
76 | public static $notice_level = null; |
||
77 | |||
78 | /** |
||
79 | * Set the version that is used to check against the version passed to notice. If the ::notice version is |
||
80 | * greater than or equal to this version, a message will be raised |
||
81 | * |
||
82 | * @static |
||
83 | * @param $ver string - |
||
84 | * A php standard version string, see http://php.net/manual/en/function.version-compare.php for details. |
||
85 | * @param null $forModule string - |
||
0 ignored issues
–
show
Documentation
Bug
introduced
by
![]() |
|||
86 | * The name of a module. The passed version will be used as the check value for |
||
87 | * calls directly from this module rather than the global value |
||
88 | * @return void |
||
89 | */ |
||
90 | public static function notification_version($ver, $forModule = null) |
||
91 | { |
||
92 | if ($forModule) { |
||
0 ignored issues
–
show
|
|||
93 | self::$module_version_overrides[$forModule] = $ver; |
||
94 | } else { |
||
95 | self::$version = $ver; |
||
96 | } |
||
97 | } |
||
98 | |||
99 | /** |
||
100 | * Given a backtrace, get the module name from the caller two removed (the caller of the method that called |
||
101 | * #notice) |
||
102 | * |
||
103 | * @param array $backtrace A backtrace as returned from debug_backtrace |
||
104 | * @return Module The module being called |
||
105 | */ |
||
106 | protected static function get_calling_module_from_trace($backtrace) |
||
107 | { |
||
108 | if (!isset($backtrace[1]['file'])) { |
||
109 | return null; |
||
110 | } |
||
111 | |||
112 | $callingfile = realpath($backtrace[1]['file']); |
||
113 | |||
114 | return ModuleLoader::inst()->getManifest()->getModuleByPath($callingfile); |
||
115 | } |
||
116 | |||
117 | /** |
||
118 | * Given a backtrace, get the method name from the immediate parent caller (the caller of #notice) |
||
119 | * |
||
120 | * @static |
||
121 | * @param $backtrace array - a backtrace as returned from debug_backtrace |
||
122 | * @param $level - 1 (default) will return immediate caller, 2 will return caller's caller, etc. |
||
0 ignored issues
–
show
|
|||
123 | * @return string - the name of the method |
||
124 | */ |
||
125 | protected static function get_called_method_from_trace($backtrace, $level = 1) |
||
126 | { |
||
127 | $level = (int)$level; |
||
128 | if (!$level) { |
||
129 | $level = 1; |
||
130 | } |
||
131 | $called = $backtrace ? $backtrace[$level] : []; |
||
132 | |||
133 | if (isset($called['class'])) { |
||
134 | return $called['class'] . $called['type'] . $called['function']; |
||
135 | } |
||
136 | return $called['function'] ?? ''; |
||
137 | } |
||
138 | |||
139 | /** |
||
140 | * Determine if deprecation notices should be displayed |
||
141 | * |
||
142 | * @return bool |
||
143 | */ |
||
144 | public static function get_enabled() |
||
145 | { |
||
146 | // Deprecation is only available on dev |
||
147 | if (!Director::isDev()) { |
||
148 | return false; |
||
149 | } |
||
150 | if (isset(self::$enabled)) { |
||
151 | return self::$enabled; |
||
152 | } |
||
153 | return Environment::getEnv('SS_DEPRECATION_ENABLED') ?: true; |
||
154 | } |
||
155 | |||
156 | /** |
||
157 | * Toggle on or off deprecation notices. Will be ignored in live. |
||
158 | * |
||
159 | * @param bool $enabled |
||
160 | */ |
||
161 | public static function set_enabled($enabled) |
||
162 | { |
||
163 | self::$enabled = $enabled; |
||
164 | } |
||
165 | |||
166 | /** |
||
167 | * Raise a notice indicating the method is deprecated if the version passed as the second argument is greater |
||
168 | * than or equal to the check version set via ::notification_version |
||
169 | * |
||
170 | * @param string $atVersion The version at which this notice should start being raised |
||
171 | * @param string $string The notice to raise |
||
172 | * @param int $scope Notice relates to the method or class context its called in. |
||
173 | */ |
||
174 | public static function notice($atVersion, $string = '', $scope = Deprecation::SCOPE_METHOD) |
||
175 | { |
||
176 | if (!static::get_enabled()) { |
||
177 | return; |
||
178 | } |
||
179 | |||
180 | $checkVersion = self::$version; |
||
181 | // Getting a backtrace is slow, so we only do it if we need it |
||
182 | $backtrace = null; |
||
183 | |||
184 | // If you pass #.#, assume #.#.0 |
||
185 | if (preg_match('/^[0-9]+\.[0-9]+$/', $atVersion)) { |
||
186 | $atVersion .= '.0'; |
||
187 | } |
||
188 | if (preg_match('/^[0-9]+\.[0-9]+$/', $checkVersion)) { |
||
189 | $checkVersion .= '.0'; |
||
190 | } |
||
191 | |||
192 | if (self::$module_version_overrides) { |
||
0 ignored issues
–
show
The expression
self::module_version_overrides of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.
This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent. Consider making the comparison explicit by using ![]() |
|||
193 | $module = self::get_calling_module_from_trace($backtrace = debug_backtrace(0)); |
||
194 | if ($module) { |
||
0 ignored issues
–
show
|
|||
195 | if (($name = $module->getComposerName()) |
||
196 | && isset(self::$module_version_overrides[$name]) |
||
197 | ) { |
||
198 | $checkVersion = self::$module_version_overrides[$name]; |
||
199 | } elseif (($name = $module->getShortName()) |
||
200 | && isset(self::$module_version_overrides[$name]) |
||
201 | ) { |
||
202 | $checkVersion = self::$module_version_overrides[$name]; |
||
203 | } |
||
204 | } |
||
205 | } |
||
206 | |||
207 | // Check the version against the notice version |
||
208 | if ($checkVersion && version_compare($checkVersion, $atVersion, '>=')) { |
||
209 | // Get the calling scope |
||
210 | if ($scope == Deprecation::SCOPE_METHOD) { |
||
211 | if (!$backtrace) { |
||
212 | $backtrace = debug_backtrace(0); |
||
213 | } |
||
214 | $caller = self::get_called_method_from_trace($backtrace); |
||
215 | } elseif ($scope == Deprecation::SCOPE_CLASS) { |
||
216 | if (!$backtrace) { |
||
217 | $backtrace = debug_backtrace(0); |
||
218 | } |
||
219 | $caller = isset($backtrace[1]['class']) ? $backtrace[1]['class'] : '(unknown)'; |
||
220 | } else { |
||
221 | $caller = false; |
||
222 | } |
||
223 | |||
224 | // Get the level to raise the notice as |
||
225 | $level = self::$notice_level; |
||
226 | if (!$level) { |
||
227 | $level = E_USER_DEPRECATED; |
||
228 | } |
||
229 | |||
230 | // Then raise the notice |
||
231 | if (substr($string, -1) != '.') { |
||
232 | $string .= "."; |
||
233 | } |
||
234 | |||
235 | $string .= " Called from " . self::get_called_method_from_trace($backtrace, 2) . '.'; |
||
236 | |||
237 | if ($caller) { |
||
238 | user_error($caller . ' is deprecated.' . ($string ? ' ' . $string : ''), $level); |
||
239 | } else { |
||
240 | user_error($string, $level); |
||
241 | } |
||
242 | } |
||
243 | } |
||
244 | |||
245 | /** |
||
246 | * Method for when testing. Dump all the current version settings to a variable for later passing to restore |
||
247 | * |
||
248 | * @return array Opaque array that should only be used to pass to {@see Deprecation::restore_settings()} |
||
249 | */ |
||
250 | public static function dump_settings() |
||
251 | { |
||
252 | return array( |
||
253 | 'level' => self::$notice_level, |
||
254 | 'version' => self::$version, |
||
255 | 'moduleVersions' => self::$module_version_overrides, |
||
256 | 'enabled' => self::$enabled, |
||
257 | ); |
||
258 | } |
||
259 | |||
260 | /** |
||
261 | * Method for when testing. Restore all the current version settings from a variable |
||
262 | * |
||
263 | * @param $settings array An array as returned by {@see Deprecation::dump_settings()} |
||
264 | */ |
||
265 | public static function restore_settings($settings) |
||
266 | { |
||
267 | self::$notice_level = $settings['level']; |
||
268 | self::$version = $settings['version']; |
||
269 | self::$module_version_overrides = $settings['moduleVersions']; |
||
270 | self::$enabled = $settings['enabled']; |
||
271 | } |
||
272 | } |
||
273 |