Passed
Push — master ( 036a41...3639fd )
by Aimeos
05:48
created

Str::method()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 3
c 0
b 0
f 0
nc 2
nop 2
dl 0
loc 7
rs 10
1
<?php
2
3
/**
4
 * @license LGPLv3, https://opensource.org/licenses/LGPL-3.0
5
 * @copyright Aimeos (aimeos.org), 2020-2021
6
 * @package MW
7
 */
8
9
10
namespace Aimeos\MW;
11
12
13
/**
14
 * String utility class
15
 *
16
 * @package MW
17
 */
18
class Str
19
{
20
	private static $methods = [];
21
	private static $node;
22
	private static $seq = 0;
23
24
25
	/**
26
	 * Calls custom methods
27
	 *
28
	 * @param string $method Name of the method
29
	 * @param array $args Method parameters
30
	 * @return mixed Return value of the called method
31
	 */
32
	public static function __callStatic( string $method, array $args )
33
	{
34
		if( $fcn = static::method( $method ) ) {
35
			return $fcn( ...$args );
36
		}
37
38
		throw new \InvalidArgumentException( sprintf( 'Unknown method "%1$s" in "%2$s"', $method, __CLASS__ ) );
39
	}
40
41
42
	/**
43
	 * Returns the sub-string after the given needle.
44
	 *
45
	 * @param mixed $str Stringable value
46
	 * @param mixed $needle String or character to search for
47
	 * @return string|null Sub-string after the needle or NULL if needle is not part of the string
48
	 */
49
	public static function after( $str, $needle ) : ?string
50
	{
51
		$str = (string) $str;
52
		$needle = (string) $needle;
53
54
		if( ( $len = strlen( $needle ) ) > 0 && ( $pos = strpos( $str, $needle ) ) !== false
55
			&& ( $result = substr( $str, $pos + $len ) ) !== false
56
		) {
57
			return $result;
58
		}
59
60
		return null;
61
	}
62
63
64
	/**
65
	 * Returns the sub-string before the given needle.
66
	 *
67
	 * @param mixed $str Stringable value
68
	 * @param mixed $needle String or character to search for
69
	 * @return string|null Sub-string before the needle or NULL if needle is not part of the string
70
	 */
71
	public static function before( $str, $needle ) : ?string
72
	{
73
		$str = (string) $str;
74
		$needle = (string) $needle;
75
76
		if( $needle !== '' && ( $pos = strpos( $str, $needle ) ) !== false
77
			&& ( $result = substr( $str, 0, $pos ) ) !== false
78
		) {
79
			return $result;
80
		}
81
82
		return null;
83
	}
84
85
86
	/**
87
	 * Tests if the strings ends with the needle.
88
	 *
89
	 * @param mixed $str Stringable value
90
	 * @param array|string|null $needles String/character or list thereof to compare with
91
	 * @return bool TRUE if string ends with needle, FALSE if not
92
	 */
93
	public static function ends( $str, $needles ) : bool
94
	{
95
		$str = (string) $str;
96
97
		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...
98
			return false;
99
		}
100
101
		foreach( (array) $needles as $needle )
102
		{
103
			$needle = (string) $needle;
104
105
			if( $needle !== '' && substr_compare( $str, $needle, -strlen( $needle ) ) === 0 ) {
106
				return true;
107
			}
108
		}
109
110
		return false;
111
	}
112
113
114
	/**
115
	 * Replaces special HTML characters by their entities.
116
	 *
117
	 * @param mixed $str Stringable value
118
	 * @param int $flags Which characters to encode
119
	 * @return string String which isn't interpreted as HTML and save to include in HTML documents
120
	 */
121
	public static function html( $str, int $flags = ENT_COMPAT | ENT_HTML401 ) : string
122
	{
123
		return htmlspecialchars( (string) $str, $flags, 'UTF-8' );
124
	}
125
126
127
	/**
128
	 * Tests if the strings contains all of the needles.
129
	 *
130
	 * @param mixed $str Stringable value
131
	 * @param string|array|null $needles String/character or list thereof to search for
132
	 * @return bool TRUE if string contains all needles, FALSE if not
133
	 */
134
	public static function in( $str, $needles ) : bool
135
	{
136
		$str = (string) $str;
137
138
		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...
139
			return false;
140
		}
141
142
		foreach( (array) $needles as $needle )
143
		{
144
			$needle = (string) $needle;
145
146
			if( $needle === '' || strpos( $str, $needle ) === false ) {
147
				return false;
148
			}
149
		}
150
151
		return true;
152
	}
153
154
155
	/**
156
	 * Registers a custom method
157
	 *
158
	 * @param string $method Name of the method
159
	 * @param \Closure|null $fcn Anonymous function which receives the same parameters as the original method
160
	 * @return \Closure|null Registered anonymous function or NULL if none has been registered
161
	 */
162
	public static function method( string $method, \Closure $fcn = null ) : ?\Closure
163
	{
164
		if( $fcn ) {
165
			self::$methods[$method] = $fcn;
166
		}
167
168
		return self::$methods[$method] ?? null;
169
	}
170
171
172
	/**
173
	 * Transforms the string into a suitable URL segment.
174
	 *
175
	 * @param mixed $str Stringable value
176
	 * @param string $lang Two letter ISO language code
177
	 * @param string $sep Separator between the words
178
	 * @return string String suitable for an URL segment
179
	 */
180
	public static function slug( $str, string $lang = 'en', string $sep = '-' ) : string
181
	{
182
		if( $fcn = static::method( 'slug' ) ) {
183
			return $fcn( $str, $lang, $sep );
184
		}
185
186
		$str = \voku\helper\ASCII::to_ascii( (string) $str, $lang );
187
		return trim( preg_replace( '/[^A-Za-z0-9]+/', $sep, $str ), $sep );
188
	}
189
190
191
	/**
192
	 * Tests if the strings contains at least one of the needles.
193
	 *
194
	 * @param mixed $str Stringable value
195
	 * @param array $needles Strings or characters to search for
196
	 * @return bool TRUE if string contains at least one needle, FALSE if it contains none
197
	 */
198
	public static function some( $str, array $needles ) : bool
199
	{
200
		$str = (string) $str;
201
202
		foreach( $needles as $needle )
203
		{
204
			$needle = (string) $needle;
205
206
			if( $needle !== '' && strpos( $str, $needle ) !== false ) {
207
				return true;
208
			}
209
		}
210
211
		return false;
212
	}
213
214
215
	/**
216
	 * Tests if the strings starts with the needle.
217
	 *
218
	 * @param mixed $str Stringable value
219
	 * @param array|string|null $needles String/character or list thereof to compare with
220
	 * @return bool TRUE if string starts with needle, FALSE if not
221
	 */
222
	public static function starts( $str, $needles ) : bool
223
	{
224
		$str = (string) $str;
225
226
		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...
227
			return false;
228
		}
229
230
		foreach( (array) $needles as $needle )
231
		{
232
			$needle = (string) $needle;
233
234
			if( $needle !== '' && strncmp( $str, $needle, strlen( $needle ) ) === 0 ) {
235
				return true;
236
			}
237
		}
238
239
		return false;
240
	}
241
242
243
	/**
244
	 * Generates a unique ID string suitable as global identifier
245
	 *
246
	 * The ID is similar to an UUID and is as unique as an UUID but it's human
247
	 * readable string is only 20 bytes long. Additionally, the unique ID is
248
	 * optimized for being used as primary key in databases.
249
	 *
250
	 * @return string Global unique ID of 20 bytes length
251
	 */
252
	public static function uid() : string
253
	{
254
		if( self::$node === null )
255
		{
256
			try {
257
				self::$node = random_bytes( 6 );
258
			} catch( \Throwable $t ) {
259
				if( function_exists( 'openssl_random_pseudo_bytes' ) ) {
260
					self::$node = openssl_random_pseudo_bytes( 6 );
261
				} else {
262
					self::$node = pack( 'n*', rand( 0, 0xffff ), rand( 0, 0xffff ), rand( 0, 0xffff ) );
263
				}
264
			}
265
		}
266
267
		$t = gettimeofday();
268
		$sec = $t['sec'];
269
		$usec = $t['usec'];
270
271
		self::$seq = self::$seq + 1 & 0xfff; // 20 bits for sequence (1 to 4,095), wraps around
272
273
		$hsec = ( $sec & 0xff00000000 ) >> 32; // 5th byte from seconds till 1970-01-01T00:00:00 (on 64 bit platforms)
274
		$lsec = $sec & 0xffffffff; // Lowest 32 bits from seconds till 1970-01-01T00:00:00
275
		$husec = ( $usec & 0xffff0 ) >> 4; // Highest 16 bits from micro seconds (total 20 bits)
276
		$mix = ( $usec & 0xf ) << 4 | ( self::$seq & 0xf00 ) >> 8; // Lowest 4 bits (usec) and highest 4 bits (seq)
277
		$lseq = self::$seq & 0xff; // Lowest 16 bits from sequence
278
279
		// 5 bytes seconds, 2 byte usec, 1 byte usec+seq, 1 byte seq, 6 bytes node
280
		$uid = base64_encode( pack( 'CNnCC', $hsec, $lsec, $husec, $mix, $lseq ) . self::$node );
281
282
		return str_replace( ['+', '/'], ['-', '_'], $uid ); // URL safety
283
	}
284
}
285