Completed
Push — master ( 1efa22...cfa6a3 )
by Sam
22s
created

Deprecation   B

Complexity

Total Complexity 37

Size/Duplication

Total Lines 240
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 4

Importance

Changes 0
Metric Value
dl 0
loc 240
rs 8.6
c 0
b 0
f 0
wmc 37
lcom 1
cbo 4

8 Methods

Rating   Name   Duplication   Size   Complexity  
A notification_version() 0 8 2
A get_calling_module_from_trace() 0 16 4
A get_called_method_from_trace() 0 14 3
A get_enabled() 0 11 4
A set_enabled() 0 4 1
F notice() 0 70 21
A dump_settings() 0 9 1
A restore_settings() 0 7 1
1
<?php
2
3
namespace SilverStripe\Dev;
4
5
use SilverStripe\Control\Director;
6
use SilverStripe\Core\Manifest\ClassLoader;
7
use SilverStripe\Core\Manifest\Module;
8
use SilverStripe\Core\Manifest\ModuleLoader;
9
10
/**
11
 * Handles raising an notice when accessing a deprecated method
12
 *
13
 * A pattern used in SilverStripe when deprecating a method is to add something like
14
 *   user_error('This method is deprecated', E_USER_NOTICE);
15
 * to the method
16
 *
17
 * However sometimes we want to mark that a method will be deprecated in some future version and shouldn't be used in
18
 * new code, but not forbid in the current version - for instance when that method is still heavily used in framework
19
 * or cms.
20
 *
21
 * This class abstracts the above pattern and adds a way to do that.
22
 *
23
 * Each call to notice passes a version that the notice will be valid from. Additionally this class has a notion of the
24
 * version it should use when deciding whether to raise the notice. If that version is equal to or greater than the
25
 * notices version (and SilverStripe is in dev mode) a deprecation message will be raised.
26
 *
27
 * Normally the checking version will be the release version of SilverStripe, but a developer can choose to set it to a
28
 * future version, to see how their code will behave in future versions.
29
 *
30
 * Modules can also set the version for calls they make - either setting it to a future version in order to ensure
31
 * forwards compatibility or setting it backwards if a module has not yet removed references to deprecated methods.
32
 *
33
 * When set per-module, only direct calls to deprecated methods from those modules are considered - if the module
34
 * calls a non-module method which then calls a deprecated method, that call will use the global check version, not
35
 * the module specific check version.
36
 */
37
class Deprecation
38
{
39
40
    const SCOPE_METHOD = 1;
41
    const SCOPE_CLASS = 2;
42
    const SCOPE_GLOBAL = 4;
43
44
    /**
45
     *
46
     * @var string
47
     */
48
    protected static $version;
49
50
    /**
51
     * Override whether deprecation is enabled. If null, then fallback to
52
     * SS_DEPRECATION_ENABLED, and then true if not defined.
53
     *
54
     * Deprecation is only available on dev.
55
     *
56
     * Must be configured outside of the config API, as deprecation API
57
     * must be available before this to avoid infinite loops.
58
     *
59
     * @var boolean|null
60
     */
61
    protected static $enabled = null;
62
63
    /**
64
     *
65
     * @var array
66
     */
67
    protected static $module_version_overrides = array();
68
69
    /**
70
     * @var int - the notice level to raise on a deprecation notice. Defaults to E_USER_DEPRECATED if that exists,
71
     * E_USER_NOTICE if not
72
     */
73
    public static $notice_level = null;
74
75
    /**
76
     * Set the version that is used to check against the version passed to notice. If the ::notice version is
77
     * greater than or equal to this version, a message will be raised
78
     *
79
     * @static
80
     * @param $ver string -
81
     *     A php standard version string, see http://php.net/manual/en/function.version-compare.php for details.
82
     * @param null $forModule string -
83
     *    The name of a module. The passed version will be used as the check value for
84
     *    calls directly from this module rather than the global value
85
     * @return void
86
     */
87
    public static function notification_version($ver, $forModule = null)
88
    {
89
        if ($forModule) {
90
            self::$module_version_overrides[$forModule] = $ver;
91
        } else {
92
            self::$version = $ver;
93
        }
94
    }
95
96
    /**
97
     * Given a backtrace, get the module name from the caller two removed (the caller of the method that called
98
     * #notice)
99
     *
100
     * @param array $backtrace A backtrace as returned from debug_backtrace
101
     * @return Module The module being called
102
     */
103
    protected static function get_calling_module_from_trace($backtrace)
104
    {
105
        if (!isset($backtrace[1]['file'])) {
106
            return null;
107
        }
108
109
        $callingfile = realpath($backtrace[1]['file']);
110
111
        $modules = ModuleLoader::instance()->getManifest()->getModules();
112
        foreach ($modules as $module) {
113
            if (strpos($callingfile, realpath($module->getPath())) === 0) {
114
                return $module;
115
            }
116
        }
117
        return null;
118
    }
119
120
    /**
121
     * Given a backtrace, get the method name from the immediate parent caller (the caller of #notice)
122
     *
123
     * @static
124
     * @param $backtrace array - a backtrace as returned from debug_backtrace
125
     * @param $level - 1 (default) will return immediate caller, 2 will return caller's caller, etc.
126
     * @return string - the name of the method
127
     */
128
    protected static function get_called_method_from_trace($backtrace, $level = 1)
129
    {
130
        $level = (int)$level;
131
        if (!$level) {
132
            $level = 1;
133
        }
134
        $called = $backtrace[$level];
135
136
        if (isset($called['class'])) {
137
            return $called['class'] . $called['type'] . $called['function'];
138
        } else {
139
            return $called['function'];
140
        }
141
    }
142
143
    /**
144
     * Determine if deprecation notices should be displayed
145
     *
146
     * @return bool
147
     */
148
    public static function get_enabled()
149
    {
150
        // Deprecation is only available on dev
151
        if (!Director::isDev()) {
152
            return false;
153
        }
154
        if (isset(self::$enabled)) {
155
            return self::$enabled;
156
        }
157
        return getenv('SS_DEPRECATION_ENABLED') ?: true;
158
    }
159
160
    /**
161
     * Toggle on or off deprecation notices. Will be ignored in live.
162
     *
163
     * @param bool $enabled
164
     */
165
    public static function set_enabled($enabled)
166
    {
167
        self::$enabled = $enabled;
168
    }
169
170
    /**
171
     * Raise a notice indicating the method is deprecated if the version passed as the second argument is greater
172
     * than or equal to the check version set via ::notification_version
173
     *
174
     * @param string $atVersion The version at which this notice should start being raised
175
     * @param string $string The notice to raise
176
     * @param int $scope Notice relates to the method or class context its called in.
177
     */
178
    public static function notice($atVersion, $string = '', $scope = Deprecation::SCOPE_METHOD)
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

This check looks for accesses to local static members using the fully qualified name instead of self::.

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

    public function __construct()
    {
        $this->key = Certificate::TRIPLEDES_CBC;
    }
}

While this is perfectly valid, the fully qualified name of Certificate::TRIPLEDES_CBC could just as well be replaced by self::TRIPLEDES_CBC. Referencing local members with self:: assured the access will still work when the class is renamed, makes it perfectly clear that the member is in fact local and will usually be shorter.

Loading history...
179
    {
180
        if (!static::get_enabled()) {
181
            return;
182
        }
183
184
        $checkVersion = self::$version;
185
        // Getting a backtrace is slow, so we only do it if we need it
186
        $backtrace = null;
187
188
        // If you pass #.#, assume #.#.0
189
        if (preg_match('/^[0-9]+\.[0-9]+$/', $atVersion)) {
190
            $atVersion .= '.0';
191
        }
192
        if (preg_match('/^[0-9]+\.[0-9]+$/', $checkVersion)) {
193
            $checkVersion .= '.0';
194
        }
195
196
        if (self::$module_version_overrides) {
0 ignored issues
show
Bug Best Practice introduced by
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 empty(..) or ! empty(...) instead.

Loading history...
197
            $module = self::get_calling_module_from_trace($backtrace = debug_backtrace(0));
198
            if ($module) {
199
                if (($name = $module->getComposerName())
200
                    && isset(self::$module_version_overrides[$name])
201
                ) {
202
                    $checkVersion = self::$module_version_overrides[$name];
203
                } elseif (($name = $module->getShortName())
204
                    && isset(self::$module_version_overrides[$name])
205
                ) {
206
                    $checkVersion = self::$module_version_overrides[$name];
207
                }
208
            }
209
        }
210
211
        // Check the version against the notice version
212
        if ($checkVersion && version_compare($checkVersion, $atVersion, '>=')) {
213
            // Get the calling scope
214
            if ($scope == Deprecation::SCOPE_METHOD) {
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

This check looks for accesses to local static members using the fully qualified name instead of self::.

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

    public function __construct()
    {
        $this->key = Certificate::TRIPLEDES_CBC;
    }
}

While this is perfectly valid, the fully qualified name of Certificate::TRIPLEDES_CBC could just as well be replaced by self::TRIPLEDES_CBC. Referencing local members with self:: assured the access will still work when the class is renamed, makes it perfectly clear that the member is in fact local and will usually be shorter.

Loading history...
215
                if (!$backtrace) {
216
                    $backtrace = debug_backtrace(0);
217
                }
218
                $caller = self::get_called_method_from_trace($backtrace);
219
            } elseif ($scope == Deprecation::SCOPE_CLASS) {
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

This check looks for accesses to local static members using the fully qualified name instead of self::.

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

    public function __construct()
    {
        $this->key = Certificate::TRIPLEDES_CBC;
    }
}

While this is perfectly valid, the fully qualified name of Certificate::TRIPLEDES_CBC could just as well be replaced by self::TRIPLEDES_CBC. Referencing local members with self:: assured the access will still work when the class is renamed, makes it perfectly clear that the member is in fact local and will usually be shorter.

Loading history...
220
                if (!$backtrace) {
221
                    $backtrace = debug_backtrace(0);
222
                }
223
                $caller = isset($backtrace[1]['class']) ? $backtrace[1]['class'] : '(unknown)';
224
            } else {
225
                $caller = false;
226
            }
227
228
            // Get the level to raise the notice as
229
            $level = self::$notice_level;
230
            if (!$level) {
231
                $level = E_USER_DEPRECATED;
232
            }
233
234
            // Then raise the notice
235
            if (substr($string, -1) != '.') {
236
                $string .= ".";
237
            }
238
239
            $string .= " Called from " . self::get_called_method_from_trace($backtrace, 2) . '.';
240
241
            if ($caller) {
242
                user_error($caller.' is deprecated.'.($string ? ' '.$string : ''), $level);
243
            } else {
244
                user_error($string, $level);
245
            }
246
        }
247
    }
248
249
    /**
250
     * Method for when testing. Dump all the current version settings to a variable for later passing to restore
251
     *
252
     * @return array Opaque array that should only be used to pass to {@see Deprecation::restore_settings()}
253
     */
254
    public static function dump_settings()
255
    {
256
        return array(
257
            'level' => self::$notice_level,
258
            'version' => self::$version,
259
            'moduleVersions' => self::$module_version_overrides,
260
            'enabled' => self::$enabled,
261
        );
262
    }
263
264
    /**
265
     * Method for when testing. Restore all the current version settings from a variable
266
     *
267
     * @param $settings array An array as returned by {@see Deprecation::dump_settings()}
268
     */
269
    public static function restore_settings($settings)
270
    {
271
        self::$notice_level = $settings['level'];
272
        self::$version = $settings['version'];
273
        self::$module_version_overrides = $settings['moduleVersions'];
274
        self::$enabled = $settings['enabled'];
275
    }
276
}
277