Failed Conditions
Pull Request — master (#19)
by Chad
01:45
created

Strings   B

Complexity

Total Complexity 43

Size/Duplication

Total Lines 304
Duplicated Lines 0 %

Importance

Changes 5
Bugs 0 Features 0
Metric Value
eloc 90
c 5
b 0
f 0
dl 0
loc 304
rs 8.96
wmc 43

16 Methods

Rating   Name   Duplication   Size   Complexity  
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 compress() 0 9 3
A translate() 0 7 2
A redact() 0 19 5
A stripTags() 0 13 3
A concat() 0 4 1
A filter() 0 18 2
A validateMaximumLength() 0 4 2
A getMatchingWords() 0 12 3
A explode() 0 32 5

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 InvalidArgumentException;
6
use TraderInteractive\Exceptions\FilterException;
7
use TypeError;
8
9
/**
10
 * A collection of filters for strings.
11
 */
12
final class Strings
13
{
14
    /**
15
     * @var string
16
     */
17
    const EXPLODE_PAD_LEFT = 'left';
18
19
    /**
20
     * @var string
21
     */
22
    const EXPLODE_PAD_RIGHT = 'right';
23
24
    /**
25
     * Filter a string.
26
     *
27
     * Verify that the passed in value  is a string.  By default, nulls are not allowed, and the length is restricted
28
     * between 1 and PHP_INT_MAX.  These parameters can be overwritten for custom behavior.
29
     *
30
     * The return value is the string, as expected by the \TraderInteractive\Filterer class.
31
     *
32
     * @param mixed $value The value to filter.
33
     * @param bool $allowNull True to allow nulls through, and false (default) if nulls should not be allowed.
34
     * @param int $minLength Minimum length to allow for $value.
35
     * @param int $maxLength Maximum length to allow for $value.
36
     * @return string|null The passed in $value.
37
     *
38
     * @throws FilterException if the value did not pass validation.
39
     * @throws \InvalidArgumentException if one of the parameters was not correctly typed.
40
     */
41
    public static function filter(
42
        $value = null,
43
        bool $allowNull = false,
44
        int $minLength = 1,
45
        int $maxLength = PHP_INT_MAX
46
    ) {
47
        self::validateMinimumLength($minLength);
48
        self::validateMaximumLength($maxLength);
49
50
        if (self::valueIsNullAndValid($allowNull, $value)) {
51
            return null;
52
        }
53
54
        $value = self::enforceValueCanBeCastAsString($value);
55
56
        self::validateStringLength($value, $minLength, $maxLength);
57
58
        return $value;
59
    }
60
61
    /**
62
     * Explodes a string into an array using the given delimiter.
63
     *
64
     * For example, given the string 'foo,bar,baz', this would return the array ['foo', 'bar', 'baz'].
65
     *
66
     * @param string     $value     The string to explode.
67
     * @param string     $delimiter The non-empty delimiter to explode on.
68
     * @param int|null   $padLength The number of elements to be returned in the result.
69
     * @param mixed      $padValue  The value to use when padding the resulting array.
70
     * @param string     $padType   Argument to specify if the resulting array should be padded on the left or right.
71
     *
72
     * @return array The exploded values.
73
     *
74
     * @throws \InvalidArgumentException if the delimiter does not pass validation.
75
     */
76
    public static function explode(
77
        $value,
78
        string $delimiter = ',',
79
        int $padLength = null,
80
        $padValue = null,
81
        string $padType = self::EXPLODE_PAD_RIGHT
82
    ) : array {
83
        self::validateIfObjectIsAString($value);
84
85
        if (empty($delimiter)) {
86
            throw new \InvalidArgumentException(
87
                "Delimiter '" . var_export($delimiter, true) . "' is not a non-empty string"
88
            );
89
        }
90
91
        $values = explode($delimiter, $value);
92
        $padLength = $padLength ?? count($values);
93
        while (count($values) < $padLength) {
94
            if ($padType === self::EXPLODE_PAD_RIGHT) {
95
                array_push($values, $padValue);
96
                continue;
97
            }
98
99
            if ($padType === self::EXPLODE_PAD_LEFT) {
100
                array_unshift($values, $padValue);
101
                continue;
102
            }
103
104
            throw new InvalidArgumentException('Invalid $padType value provided');
105
        }
106
107
        return $values;
108
    }
109
110
    /**
111
     * This filter takes the given string and translates it using the given value map.
112
     *
113
     * @param string $value    The string value to translate
114
     * @param array  $valueMap Array of key value pairs where a key will match the given $value.
115
     *
116
     * @return string
117
     */
118
    public static function translate(string $value, array $valueMap) : string
119
    {
120
        if (!array_key_exists($value, $valueMap)) {
121
            throw new FilterException("The value '{$value}' was not found in the translation map array.");
122
        }
123
124
        return $valueMap[$value];
125
    }
126
127
    /**
128
     * This filter prepends $prefix and appends $suffix to the string value.
129
     *
130
     * @param mixed  $value  The string value to which $prefix and $suffix will be added.
131
     * @param string $prefix The value to prepend to the string.
132
     * @param string $suffix The value to append to the string.
133
     *
134
     * @return string
135
     *
136
     * @throws FilterException Thrown if $value cannot be casted to a string.
137
     */
138
    public static function concat($value, string $prefix = '', string $suffix = '') : string
139
    {
140
        self::enforceValueCanBeCastAsString($value);
141
        return "{$prefix}{$value}{$suffix}";
142
    }
143
144
    /**
145
     * This filter trims and removes superfluous whitespace characters from the given string.
146
     *
147
     * @param string|null $value                     The string to compress.
148
     * @param bool        $replaceVerticalWhitespace Flag to replace vertical whitespace such as newlines with
149
     *                                               single space.
150
     *
151
     * @return string|null
152
     */
153
    public static function compress(string $value = null, bool $replaceVerticalWhitespace = false)
154
    {
155
        if ($value === null) {
156
            return null;
157
        }
158
159
        $pattern = $replaceVerticalWhitespace ? '\s+' : '\h+';
160
161
        return trim(preg_replace("/{$pattern}/", ' ', $value));
162
    }
163
164
    /**
165
     * This filter replaces the given words with a replacement character.
166
     *
167
     * @param mixed          $value       The raw input to run the filter against.
168
     * @param array|callable $words       The words to filter out.
169
     * @param string         $replacement The character to replace the words with.
170
     *
171
     * @return string|null
172
     *
173
     * @throws FilterException Thrown when a bad value is encountered.
174
     */
175
    public static function redact(
176
        $value,
177
        $words,
178
        string $replacement = ''
179
    ) {
180
        if ($value === null || $value === '') {
181
            return $value;
182
        }
183
184
        $stringValue = self::filter($value);
185
        if (is_callable($words)) {
186
            $words = $words();
187
        }
188
189
        if (is_array($words) === false) {
190
            throw new FilterException("Words was not an array or a callable that returns an array");
191
        }
192
193
        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

193
        return self::replaceWordsWithReplacementString(/** @scrutinizer ignore-type */ $stringValue, $words, $replacement);
Loading history...
194
    }
195
196
    /**
197
     * Strip HTML and PHP tags from a string and, optionally, replace the tags with a string.
198
     * Unlike the strip_tags function, this method will return null if a null value is given.
199
     * The native php function will return an empty string.
200
     *
201
     * @param string|null $value       The input string.
202
     * @param string      $replacement The string to replace the tags with. Defaults to an empty string.
203
     *
204
     * @return string|null
205
     */
206
    public static function stripTags(string $value = null, string $replacement = '')
207
    {
208
        if ($value === null) {
209
            return null;
210
        }
211
212
        if ($replacement === '') {
213
            return strip_tags($value);
214
        }
215
216
        $findTagEntities = '/<[^>]+?>/';
217
        $valueWithReplacements = preg_replace($findTagEntities, $replacement, $value);
218
        return strip_tags($valueWithReplacements); // use built-in as a safeguard to ensure tags are stripped
219
    }
220
221
    private static function validateMinimumLength(int $minLength)
222
    {
223
        if ($minLength < 0) {
224
            throw new \InvalidArgumentException('$minLength was not a positive integer value');
225
        }
226
    }
227
228
    private static function validateMaximumLength(int $maxLength)
229
    {
230
        if ($maxLength < 0) {
231
            throw new \InvalidArgumentException('$maxLength was not a positive integer value');
232
        }
233
    }
234
235
    private static function validateStringLength(string $value = null, int $minLength, int $maxLength)
236
    {
237
        $valueLength = strlen($value);
238
        if ($valueLength < $minLength || $valueLength > $maxLength) {
239
            $format = "Value '%s' with length '%d' is less than '%d' or greater than '%d'";
240
            throw new FilterException(
241
                sprintf($format, $value, $valueLength, $minLength, $maxLength)
242
            );
243
        }
244
    }
245
246
    private static function valueIsNullAndValid(bool $allowNull, $value = null) : bool
247
    {
248
        if ($allowNull === false && $value === null) {
249
            throw new FilterException('Value failed filtering, $allowNull is set to false');
250
        }
251
252
        return $allowNull === true && $value === null;
253
    }
254
255
    private static function validateIfObjectIsAString($value)
256
    {
257
        if (!is_string($value)) {
258
            throw new FilterException("Value '" . var_export($value, true) . "' is not a string");
259
        }
260
    }
261
262
    private static function enforceValueCanBeCastAsString($value)
263
    {
264
        try {
265
            $value = (
266
                function (string $str) : string {
267
                    return $str;
268
                }
269
            )($value);
270
        } catch (TypeError $te) {
271
            throw new FilterException(sprintf("Value '%s' is not a string", var_export($value, true)));
272
        }
273
274
        return $value;
275
    }
276
277
    private static function replaceWordsWithReplacementString(string $value, array $words, string $replacement) : string
278
    {
279
        $matchingWords = self::getMatchingWords($words, $value);
280
        if (count($matchingWords) === 0) {
281
            return $value;
282
        }
283
284
        $replacements = self::generateReplacementsMap($matchingWords, $replacement);
285
286
        return str_ireplace($matchingWords, $replacements, $value);
287
    }
288
289
    private static function getMatchingWords(array $words, string $value) : array
290
    {
291
        $matchingWords = [];
292
        foreach ($words as $word) {
293
            $escapedWord = preg_quote($word, '/');
294
            $caseInsensitiveWordPattern = "/\b{$escapedWord}\b/i";
295
            if (preg_match($caseInsensitiveWordPattern, $value)) {
296
                $matchingWords[] = $word;
297
            }
298
        }
299
300
        return $matchingWords;
301
    }
302
303
    private static function generateReplacementsMap(array $words, string $replacement) : array
304
    {
305
        $replacement = mb_substr($replacement, 0, 1);
306
307
        return array_map(
308
            function ($word) use ($replacement) {
309
                if ($replacement === '') {
310
                    return '';
311
                }
312
313
                return str_repeat($replacement, strlen($word));
314
            },
315
            $words
316
        );
317
    }
318
}
319