Str   A
last analyzed

Complexity

Total Complexity 40

Size/Duplication

Total Lines 268
Duplicated Lines 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
wmc 40
eloc 76
c 1
b 0
f 0
dl 0
loc 268
rs 9.2

11 Methods

Rating   Name   Duplication   Size   Complexity  
A after() 0 16 5
A before() 0 16 5
A decode() 0 3 1
A some() 0 14 4
A slug() 0 9 2
A starts() 0 18 6
A ends() 0 18 6
A uid() 0 31 4
A html() 0 3 1
A in() 0 18 5
A strtime() 0 4 1

How to fix   Complexity   

Complex Class

Complex classes like Str 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 Str, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
/**
4
 * @license LGPLv3, https://opensource.org/licenses/LGPL-3.0
5
 * @copyright Aimeos (aimeos.org), 2020-2025
6
 * @package Base
7
 */
8
9
10
namespace Aimeos\Base;
11
12
13
/**
14
 * String utility class
15
 *
16
 * @package Base
17
 */
18
class Str
19
{
20
	use \Aimeos\Macro\Macroable;
21
22
23
	private static $node;
24
	private static $seq = 0;
25
26
27
	/**
28
	 * Returns the sub-string after the given needle.
29
	 *
30
	 * @param mixed $str Stringable value
31
	 * @param mixed $needles String or strings to search for
32
	 * @return string|null Sub-string after the needle or NULL if needle is not part of the string
33
	 */
34
	public static function after( $str, $needles ) : ?string
35
	{
36
		$str = (string) $str;
37
38
		foreach( (array) $needles as $needle )
39
		{
40
			$needle = (string) $needle;
41
42
			if( ( $len = strlen( $needle ) ) > 0 && ( $pos = strpos( $str, $needle ) ) !== false
43
				&& ( $result = substr( $str, $pos + $len ) ) !== false
44
			) {
45
				return $result;
46
			}
47
		}
48
49
		return null;
50
	}
51
52
53
	/**
54
	 * Returns the sub-string before the given needle.
55
	 *
56
	 * @param mixed $str Stringable value
57
	 * @param mixed $needles String or strings to search for
58
	 * @return string|null Sub-string before the needle or NULL if needle is not part of the string
59
	 */
60
	public static function before( $str, $needles ) : ?string
61
	{
62
		$str = (string) $str;
63
64
		foreach( (array) $needles as $needle )
65
		{
66
			$needle = (string) $needle;
67
68
			if( $needle !== '' && ( $pos = strpos( $str, $needle ) ) !== false
69
				&& ( $result = substr( $str, 0, $pos ) ) !== false
70
			) {
71
				return $result;
72
			}
73
		}
74
75
		return null;
76
	}
77
78
79
	/**
80
	 * Replaces special HTML characters by their entities.
81
	 *
82
	 * @param mixed $str Stringable value
83
	 * @param int $flags Which characters to encode
84
	 * @return string String which isn't interpreted as HTML and save to include in HTML documents
85
	 */
86
	public static function decode( $str, int $flags = ENT_SUBSTITUTE | ENT_QUOTES | ENT_HTML5 ) : string
87
	{
88
		return str_replace( '&#96;', '`', htmlspecialchars_decode( (string) $str, $flags | ENT_QUOTES ) );
89
	}
90
91
92
	/**
93
	 * Tests if the strings ends with the needle.
94
	 *
95
	 * @param mixed $str Stringable value
96
	 * @param array|string|null $needles String/character or list thereof to compare with
97
	 * @return bool TRUE if string ends with needle, FALSE if not
98
	 */
99
	public static function ends( $str, $needles ) : bool
100
	{
101
		$str = (string) $str;
102
103
		if( $str === '' || $needles == null ) {
0 ignored issues
show
Bug introduced by
It seems like you are loosely comparing $needles of type array|null|string against null; this is ambiguous if the string can be empty. Consider using a strict comparison === instead.
Loading history...
104
			return false;
105
		}
106
107
		foreach( (array) $needles as $needle )
108
		{
109
			$needle = (string) $needle;
110
111
			if( $needle !== '' && substr_compare( $str, $needle, -strlen( $needle ) ) === 0 ) {
112
				return true;
113
			}
114
		}
115
116
		return false;
117
	}
118
119
120
	/**
121
	 * Replaces special HTML characters by their entities.
122
	 *
123
	 * @param mixed $str Stringable value
124
	 * @param int $flags Which characters to encode
125
	 * @return string String which isn't interpreted as HTML and save to include in HTML documents
126
	 */
127
	public static function html( $str, int $flags = ENT_SUBSTITUTE | ENT_QUOTES | ENT_HTML5 ) : string
128
	{
129
		return htmlspecialchars( (string) $str, $flags, 'UTF-8' );
130
	}
131
132
133
	/**
134
	 * Tests if the strings contains all of the needles.
135
	 *
136
	 * @param mixed $str Stringable value
137
	 * @param string|array|null $needles String/character or list thereof to search for
138
	 * @return bool TRUE if string contains all needles, FALSE if not
139
	 */
140
	public static function in( $str, $needles ) : bool
141
	{
142
		$str = (string) $str;
143
144
		if( $needles == null ) {
0 ignored issues
show
Bug introduced by
It seems like you are loosely comparing $needles of type array|null|string against null; this is ambiguous if the string can be empty. Consider using a strict comparison === instead.
Loading history...
145
			return false;
146
		}
147
148
		foreach( (array) $needles as $needle )
149
		{
150
			$needle = (string) $needle;
151
152
			if( $needle === '' || strpos( $str, $needle ) === false ) {
153
				return false;
154
			}
155
		}
156
157
		return true;
158
	}
159
160
161
	/**
162
	 * Transforms the string into a suitable URL segment.
163
	 *
164
	 * @param mixed $str Stringable value
165
	 * @param string $lang Two letter ISO language code
166
	 * @param string $sep Separator between the words
167
	 * @return string String suitable for an URL segment
168
	 */
169
	public static function slug( $str, string $lang = 'en', string $sep = '-' ) : string
170
	{
171
		if( $fcn = static::macro( 'slug' ) ) {
172
			return $fcn( $str, $lang, $sep );
173
		}
174
175
		$str = \voku\helper\ASCII::to_ascii( (string) $str, $lang );
176
		$str = strtolower( preg_replace( '/[^A-Za-z0-9\_\-\~\.]+/', $sep, $str ) );
177
		return trim( preg_replace( '/(' . preg_quote( $sep, '/' ) . '){2,}/', $sep, $str ), $sep );
178
	}
179
180
181
	/**
182
	 * Tests if the strings contains at least one of the needles.
183
	 *
184
	 * @param mixed $str Stringable value
185
	 * @param array $needles Strings or characters to search for
186
	 * @return bool TRUE if string contains at least one needle, FALSE if it contains none
187
	 */
188
	public static function some( $str, array $needles ) : bool
189
	{
190
		$str = (string) $str;
191
192
		foreach( $needles as $needle )
193
		{
194
			$needle = (string) $needle;
195
196
			if( $needle !== '' && strpos( $str, $needle ) !== false ) {
197
				return true;
198
			}
199
		}
200
201
		return false;
202
	}
203
204
205
	/**
206
	 * Tests if the strings starts with the needle.
207
	 *
208
	 * @param mixed $str Stringable value
209
	 * @param array|string|null $needles String/character or list thereof to compare with
210
	 * @return bool TRUE if string starts with needle, FALSE if not
211
	 */
212
	public static function starts( $str, $needles ) : bool
213
	{
214
		$str = (string) $str;
215
216
		if( $str === '' || $needles == null ) {
0 ignored issues
show
Bug introduced by
It seems like you are loosely comparing $needles of type array|null|string against null; this is ambiguous if the string can be empty. Consider using a strict comparison === instead.
Loading history...
217
			return false;
218
		}
219
220
		foreach( (array) $needles as $needle )
221
		{
222
			$needle = (string) $needle;
223
224
			if( $needle !== '' && strncmp( $str, $needle, strlen( $needle ) ) === 0 ) {
225
				return true;
226
			}
227
		}
228
229
		return false;
230
	}
231
232
233
	/**
234
	 * Returns a date formatted according to the given format string.
235
	 *
236
	 * @param string $format DateTime format string but with format characters preceded by "%"
237
	 * @return string String with date/time according to the format string
238
	 */
239
	public static function strtime( string $format ) : string
240
	{
241
		$format = str_replace( ['%M', '%S'], ['%i', '%s'], $format );
242
		return trim( date_create()->format( str_replace( '\\%\\', '', '\\' . join( '\\', str_split( $format ) ) ) ) );
0 ignored issues
show
Bug introduced by
It seems like str_split($format) can also be of type true; however, parameter $array of join() does only seem to accept array|null, 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

242
		return trim( date_create()->format( str_replace( '\\%\\', '', '\\' . join( '\\', /** @scrutinizer ignore-type */ str_split( $format ) ) ) ) );
Loading history...
243
	}
244
245
246
	/**
247
	 * Generates a unique ID string suitable as global identifier
248
	 *
249
	 * The ID is similar to an UUID and is as unique as an UUID but it's human
250
	 * readable string is only 20 bytes long. Additionally, the unique ID is
251
	 * optimized for being used as primary key in databases.
252
	 *
253
	 * @return string Global unique ID of 20 bytes length
254
	 */
255
	public static function uid() : string
256
	{
257
		if( self::$node === null )
258
		{
259
			try {
260
				self::$node = random_bytes( 6 );
261
			} catch( \Throwable $t ) {
262
				if( function_exists( 'openssl_random_pseudo_bytes' ) ) {
263
					self::$node = openssl_random_pseudo_bytes( 6 );
264
				} else {
265
					self::$node = pack( 'n*', rand( 0, 0xffff ), rand( 0, 0xffff ), rand( 0, 0xffff ) );
266
				}
267
			}
268
		}
269
270
		$time = microtime( true );
271
		$sec = (int) $time;
272
		$usec = (int) ( fmod( $time, 1 ) * 1000000 );
0 ignored issues
show
Bug introduced by
It seems like $time can also be of type string; however, parameter $num1 of fmod() does only seem to accept double, 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

272
		$usec = (int) ( fmod( /** @scrutinizer ignore-type */ $time, 1 ) * 1000000 );
Loading history...
273
274
		self::$seq = self::$seq + 1 & 0xfff; // 20 bits for sequence (1 to 4,095), wraps around
275
276
		$hsec = ( $sec & 0xff00000000 ) >> 32; // 5th byte from seconds till 1970-01-01T00:00:00 (on 64 bit platforms)
277
		$lsec = $sec & 0xffffffff; // Lowest 32 bits from seconds till 1970-01-01T00:00:00
278
		$husec = ( $usec & 0xffff0 ) >> 4; // Highest 16 bits from micro seconds (total 20 bits)
279
		$mix = ( $usec & 0xf ) << 4 | ( self::$seq & 0xf00 ) >> 8; // Lowest 4 bits (usec) and highest 4 bits (seq)
280
		$lseq = self::$seq & 0xff; // Lowest 16 bits from sequence
281
282
		// 5 bytes seconds, 2 byte usec, 1 byte usec+seq, 1 byte seq, 6 bytes node
283
		$uid = base64_encode( pack( 'CNnCC', $hsec, $lsec, $husec, $mix, $lseq ) . self::$node );
284
285
		return str_replace( ['+', '/'], ['-', '_'], $uid ); // URL safety
286
	}
287
}
288