Completed
Push — master ( f6e6fb...0703ed )
by Chad
18s queued 14s
created

Strings   A

Complexity

Total Complexity 41

Size/Duplication

Total Lines 300
Duplicated Lines 0 %

Importance

Changes 5
Bugs 0 Features 0
Metric Value
eloc 94
c 5
b 0
f 0
dl 0
loc 300
rs 9.1199
wmc 41

17 Methods

Rating   Name   Duplication   Size   Complexity  
A translate() 0 7 2
A concat() 0 4 1
A compress() 0 9 3
A redact() 0 19 5
A stripTags() 0 13 3
A filter() 0 18 2
A explode() 0 11 2
A valueIsNullAndValid() 0 7 4
A replaceWordsWithReplacementString() 0 10 2
A generateReplacementsMap() 0 13 2
A validateMinimumLength() 0 4 2
A validateStringLength() 0 7 3
A validateIfObjectIsAString() 0 4 2
A enforceValueCanBeCastAsString() 0 13 2
A stripEmoji() 0 21 1
A validateMaximumLength() 0 4 2
A getMatchingWords() 0 12 3

How to fix   Complexity   

Complex Class

Complex classes like Strings often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Strings, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace TraderInteractive\Filter;
4
5
use TraderInteractive\Exceptions\FilterException;
6
use TypeError;
7
8
/**
9
 * A collection of filters for strings.
10
 */
11
final class Strings
12
{
13
    /**
14
     * Filter a string.
15
     *
16
     * Verify that the passed in value  is a string.  By default, nulls are not allowed, and the length is restricted
17
     * between 1 and PHP_INT_MAX.  These parameters can be overwritten for custom behavior.
18
     *
19
     * The return value is the string, as expected by the \TraderInteractive\Filterer class.
20
     *
21
     * @param mixed $value The value to filter.
22
     * @param bool $allowNull True to allow nulls through, and false (default) if nulls should not be allowed.
23
     * @param int $minLength Minimum length to allow for $value.
24
     * @param int $maxLength Maximum length to allow for $value.
25
     * @return string|null The passed in $value.
26
     *
27
     * @throws FilterException if the value did not pass validation.
28
     * @throws \InvalidArgumentException if one of the parameters was not correctly typed.
29
     */
30
    public static function filter(
31
        $value = null,
32
        bool $allowNull = false,
33
        int $minLength = 1,
34
        int $maxLength = PHP_INT_MAX
35
    ) {
36
        self::validateMinimumLength($minLength);
37
        self::validateMaximumLength($maxLength);
38
39
        if (self::valueIsNullAndValid($allowNull, $value)) {
40
            return null;
41
        }
42
43
        $value = self::enforceValueCanBeCastAsString($value);
44
45
        self::validateStringLength($value, $minLength, $maxLength);
46
47
        return $value;
48
    }
49
50
    /**
51
     * Explodes a string into an array using the given delimiter.
52
     *
53
     * For example, given the string 'foo,bar,baz', this would return the array ['foo', 'bar', 'baz'].
54
     *
55
     * @param string $value The string to explode.
56
     * @param string $delimiter The non-empty delimiter to explode on.
57
     * @return array The exploded values.
58
     *
59
     * @throws \InvalidArgumentException if the delimiter does not pass validation.
60
     */
61
    public static function explode($value, string $delimiter = ',')
62
    {
63
        self::validateIfObjectIsAString($value);
64
65
        if (empty($delimiter)) {
66
            throw new \InvalidArgumentException(
67
                "Delimiter '" . var_export($delimiter, true) . "' is not a non-empty string"
68
            );
69
        }
70
71
        return explode($delimiter, $value);
72
    }
73
74
    /**
75
     * This filter takes the given string and translates it using the given value map.
76
     *
77
     * @param string $value    The string value to translate
78
     * @param array  $valueMap Array of key value pairs where a key will match the given $value.
79
     *
80
     * @return string
81
     */
82
    public static function translate(string $value, array $valueMap) : string
83
    {
84
        if (!array_key_exists($value, $valueMap)) {
85
            throw new FilterException("The value '{$value}' was not found in the translation map array.");
86
        }
87
88
        return $valueMap[$value];
89
    }
90
91
    /**
92
     * This filter prepends $prefix and appends $suffix to the string value.
93
     *
94
     * @param mixed  $value  The string value to which $prefix and $suffix will be added.
95
     * @param string $prefix The value to prepend to the string.
96
     * @param string $suffix The value to append to the string.
97
     *
98
     * @return string
99
     *
100
     * @throws FilterException Thrown if $value cannot be casted to a string.
101
     */
102
    public static function concat($value, string $prefix = '', string $suffix = '') : string
103
    {
104
        self::enforceValueCanBeCastAsString($value);
105
        return "{$prefix}{$value}{$suffix}";
106
    }
107
108
    /**
109
     * This filter trims and removes superfluous whitespace characters from the given string.
110
     *
111
     * @param string|null $value                     The string to compress.
112
     * @param bool        $replaceVerticalWhitespace Flag to replace vertical whitespace such as newlines with
113
     *                                               single space.
114
     *
115
     * @return string|null
116
     */
117
    public static function compress(string $value = null, bool $replaceVerticalWhitespace = false)
118
    {
119
        if ($value === null) {
120
            return null;
121
        }
122
123
        $pattern = $replaceVerticalWhitespace ? '\s+' : '\h+';
124
125
        return trim(preg_replace("/{$pattern}/", ' ', $value));
126
    }
127
128
    /**
129
     * This filter replaces the given words with a replacement character.
130
     *
131
     * @param mixed          $value       The raw input to run the filter against.
132
     * @param array|callable $words       The words to filter out.
133
     * @param string         $replacement The character to replace the words with.
134
     *
135
     * @return string|null
136
     *
137
     * @throws FilterException Thrown when a bad value is encountered.
138
     */
139
    public static function redact(
140
        $value,
141
        $words,
142
        string $replacement = ''
143
    ) {
144
        if ($value === null || $value === '') {
145
            return $value;
146
        }
147
148
        $stringValue = self::filter($value);
149
        if (is_callable($words)) {
150
            $words = $words();
151
        }
152
153
        if (is_array($words) === false) {
154
            throw new FilterException("Words was not an array or a callable that returns an array");
155
        }
156
157
        return self::replaceWordsWithReplacementString($stringValue, $words, $replacement);
0 ignored issues
show
Bug introduced by
It seems like $stringValue can also be of type null; however, parameter $value of TraderInteractive\Filter...WithReplacementString() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

157
        return self::replaceWordsWithReplacementString(/** @scrutinizer ignore-type */ $stringValue, $words, $replacement);
Loading history...
158
    }
159
160
    /**
161
     * Strip HTML and PHP tags from a string and, optionally, replace the tags with a string.
162
     * Unlike the strip_tags function, this method will return null if a null value is given.
163
     * The native php function will return an empty string.
164
     *
165
     * @param string|null $value       The input string.
166
     * @param string      $replacement The string to replace the tags with. Defaults to an empty string.
167
     *
168
     * @return string|null
169
     */
170
    public static function stripTags(string $value = null, string $replacement = '')
171
    {
172
        if ($value === null) {
173
            return null;
174
        }
175
176
        if ($replacement === '') {
177
            return strip_tags($value);
178
        }
179
180
        $findTagEntities = '/<[^>]+?>/';
181
        $valueWithReplacements = preg_replace($findTagEntities, $replacement, $value);
182
        return strip_tags($valueWithReplacements); // use built-in as a safeguard to ensure tags are stripped
183
    }
184
185
    /**
186
     * Strip emoji character and various other pictograph characters
187
     *
188
     * @param string $value       The input string.
189
     * @param string $replacement The string to replace the tags with. Defaults to an empty string.
190
     *
191
     * @return string;
192
     */
193
    public static function stripEmoji(string $value, string $replacement = ''): string
194
    {
195
        $alphanumericSupplement = '/[\x{1F100}-\x{1F1FF}]/u';
196
        $pictographRegex = '/[\x{1F300}-\x{1F5FF}]/u';
197
        $emoticonRegex = '/[\x{1F600}-\x{1F64F}]/u';
198
        $transportSymbolRegex = '/[\x{1F680}-\x{1F6FF}]/u';
199
        $supplementalSymbolRegex = '/[\x{1F900}-\x{1F9FF}]/u';
200
        $miscSymbolsRegex = '/[\x{2600}-\x{26FF}]/u';
201
        $dingbatsRegex = '/[\x{2700}-\x{27BF}]/u';
202
203
        $regexPatterns = [
204
            $alphanumericSupplement,
205
            $pictographRegex,
206
            $emoticonRegex,
207
            $transportSymbolRegex,
208
            $supplementalSymbolRegex,
209
            $miscSymbolsRegex,
210
            $dingbatsRegex,
211
        ];
212
213
        return preg_replace($regexPatterns, $replacement, $value);
214
    }
215
216
    private static function validateMinimumLength(int $minLength)
217
    {
218
        if ($minLength < 0) {
219
            throw new \InvalidArgumentException('$minLength was not a positive integer value');
220
        }
221
    }
222
223
    private static function validateMaximumLength(int $maxLength)
224
    {
225
        if ($maxLength < 0) {
226
            throw new \InvalidArgumentException('$maxLength was not a positive integer value');
227
        }
228
    }
229
230
    private static function validateStringLength(string $value = null, int $minLength, int $maxLength)
231
    {
232
        $valueLength = strlen($value);
0 ignored issues
show
Bug introduced by
It seems like $value can also be of type null; however, parameter $string of strlen() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

232
        $valueLength = strlen(/** @scrutinizer ignore-type */ $value);
Loading history...
233
        if ($valueLength < $minLength || $valueLength > $maxLength) {
234
            $format = "Value '%s' with length '%d' is less than '%d' or greater than '%d'";
235
            throw new FilterException(
236
                sprintf($format, $value, $valueLength, $minLength, $maxLength)
237
            );
238
        }
239
    }
240
241
    private static function valueIsNullAndValid(bool $allowNull, $value = null) : bool
242
    {
243
        if ($allowNull === false && $value === null) {
244
            throw new FilterException('Value failed filtering, $allowNull is set to false');
245
        }
246
247
        return $allowNull === true && $value === null;
248
    }
249
250
    private static function validateIfObjectIsAString($value)
251
    {
252
        if (!is_string($value)) {
253
            throw new FilterException("Value '" . var_export($value, true) . "' is not a string");
254
        }
255
    }
256
257
    private static function enforceValueCanBeCastAsString($value)
258
    {
259
        try {
260
            $value = (
261
                function (string $str) : string {
262
                    return $str;
263
                }
264
            )($value);
265
        } catch (TypeError $te) {
266
            throw new FilterException(sprintf("Value '%s' is not a string", var_export($value, true)));
267
        }
268
269
        return $value;
270
    }
271
272
    private static function replaceWordsWithReplacementString(string $value, array $words, string $replacement) : string
273
    {
274
        $matchingWords = self::getMatchingWords($words, $value);
275
        if (count($matchingWords) === 0) {
276
            return $value;
277
        }
278
279
        $replacements = self::generateReplacementsMap($matchingWords, $replacement);
280
281
        return str_ireplace($matchingWords, $replacements, $value);
0 ignored issues
show
Bug Best Practice introduced by
The expression return str_ireplace($mat... $replacements, $value) could return the type array which is incompatible with the type-hinted return string. Consider adding an additional type-check to rule them out.
Loading history...
282
    }
283
284
    private static function getMatchingWords(array $words, string $value) : array
285
    {
286
        $matchingWords = [];
287
        foreach ($words as $word) {
288
            $escapedWord = preg_quote($word, '/');
289
            $caseInsensitiveWordPattern = "/\b{$escapedWord}\b/i";
290
            if (preg_match($caseInsensitiveWordPattern, $value)) {
291
                $matchingWords[] = $word;
292
            }
293
        }
294
295
        return $matchingWords;
296
    }
297
298
    private static function generateReplacementsMap(array $words, string $replacement) : array
299
    {
300
        $replacement = mb_substr($replacement, 0, 1);
301
302
        return array_map(
303
            function ($word) use ($replacement) {
304
                if ($replacement === '') {
305
                    return '';
306
                }
307
308
                return str_repeat($replacement, strlen($word));
309
            },
310
            $words
311
        );
312
    }
313
}
314