PluginProbe
FluentCommunity – Ultra-Fast High-Performance Social Network, Community, LMS & Online Courses / 2.6.0
FluentCommunity – Ultra-Fast High-Performance Social Network, Community, LMS & Online Courses v2.6.0
2.11.0 2.10.0 2.10.01 2.9.1 2.9.0 2.8.1 2.8.0 2.7.7 2.7.5 2.7.0 2.6.01 2.6.0 2.5.0 2.4.01 trunk 1.0.90 1.0.91 1.0.92 1.0.93 1.0.94 1.0.95 1.0.96 1.0.97 1.0.98 1.0.99 All 78 releases
fluent-community / vendor / wpfluent / framework / src / WPFluent / Support / Str.php

Str.php in FluentCommunity – Ultra-Fast High-Performance Social Network, Community, LMS & Online Courses 2.6.0, at vendor/wpfluent/framework/src/WPFluent/Support/Str.php

1,834 lines 58.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace FluentCommunity\Framework\Support;
4
5 use Exception;
6 use Traversable;
7 use JsonException;
8 use FluentCommunity\Framework\Foundation\App;
9 use FluentCommunity\Framework\Support\Helper;
10 use FluentCommunity\Framework\Support\Stringable;
11 use FluentCommunity\Framework\Support\MacroableTrait;
12
13 class Str
14 {
15 use MacroableTrait;
16
17 /**
18 * The cache of snake-cased words.
19 *
20 * @var array
21 */
22 protected static $snakeCache = [];
23
24 /**
25 * The cache of camel-cased words.
26 *
27 * @var array
28 */
29 protected static $camelCache = [];
30
31 /**
32 * The cache of studly-cased words.
33 *
34 * @var array
35 */
36 protected static $studlyCache = [];
37
38 /**
39 * Get a new stringable object from the given string.
40 *
41 * @param string $string
42 * @return \FluentCommunity\Framework\Support\Stringable
43 */
44 public static function of($string)
45 {
46 return new Stringable($string);
47 }
48
49 /**
50 * Makes an acronum from a string of words
51 *
52 * @param string $string
53 * @param string $delimiter
54 * @return string
55 */
56 public static function acronym($string, $delimiter = '')
57 {
58 if (empty($string)) {
59 return '';
60 }
61
62 $acronym = '';
63 foreach (preg_split('/[^\p{L}]+/u', $string) as $word) {
64 if(!empty($word)){
65 $first_letter = mb_substr($word, 0, 1);
66 $acronym .= $first_letter . $delimiter;
67 }
68 }
69
70 return $acronym;
71 }
72
73 /**
74 * Return the remainder of a string after the first occurrence of a given value.
75 *
76 * @param string $subject
77 * @param string $search
78 * @return string
79 */
80 public static function after($subject, $search)
81 {
82 return $search === '' ? $subject : array_reverse(explode($search, $subject, 2))[0];
83 }
84
85 /**
86 * Return the remainder of a string after the last occurrence of a given value.
87 *
88 * @param string $subject
89 * @param string $search
90 * @return string
91 */
92 public static function afterLast($subject, $search)
93 {
94 if ($search === '') {
95 return $subject;
96 }
97
98 $position = strrpos($subject, (string) $search);
99
100 if ($position === false) {
101 return $subject;
102 }
103
104 return substr($subject, $position + strlen($search));
105 }
106
107 /**
108 * Transliterate a UTF-8 value to ASCII.
109 *
110 * @param string $value
111 * @return string
112 * @see https://github.com/fzaninotto/Faker/blob/master/src/Faker/Provider/Internet.php#L245
113 */
114 public static function ascii($value)
115 {
116 static $arrayFrom, $arrayTo;
117
118 if (empty($arrayFrom)) {
119 $transliterationTable = [
120 'IJ' => 'I', 'Ö' => 'O', 'Œ' => 'O', 'Ü' => 'U', 'ä' => 'a', 'æ' => 'a',
121 'ij' => 'i', 'ö' => 'o', 'œ' => 'o', 'ü' => 'u', 'ß' => 's', 'ſ' => 's',
122 'À' => 'A', 'Á' => 'A', 'Â' => 'A', 'Ã' => 'A', 'Ä' => 'A', '�
123 ' => 'A',
124 'Æ' => 'A', 'Ā' => 'A', 'Ą' => 'A', 'Ă' => 'A', 'Ç' => 'C', 'Ć' => 'C',
125 'Č' => 'C', 'Ĉ' => 'C', 'Ċ' => 'C', 'Ď' => 'D', 'Đ' => 'D', 'È' => 'E',
126 'É' => 'E', 'Ê' => 'E', 'Ë' => 'E', 'Ē' => 'E', 'Ę' => 'E', 'Ě' => 'E',
127 'Ĕ' => 'E', 'Ė' => 'E', 'Ĝ' => 'G', 'Ğ' => 'G', 'Ġ' => 'G', 'Ģ' => 'G',
128 'Ĥ' => 'H', 'Ħ' => 'H', 'Ì' => 'I', 'Í' => 'I', 'Î' => 'I', 'Ï' => 'I',
129 'Ī' => 'I', 'Ĩ' => 'I', 'Ĭ' => 'I', 'Į' => 'I', 'İ' => 'I', 'Ĵ' => 'J',
130 'Ķ' => 'K', 'Ľ' => 'K', 'Ĺ' => 'K', 'Ļ' => 'K', 'Ŀ' => 'K', 'Ł' => 'L',
131 'Ñ' => 'N', 'Ń' => 'N', 'Ň' => 'N', '�
132 ' => 'N', 'Ŋ' => 'N', 'Ò' => 'O',
133 'Ó' => 'O', 'Ô' => 'O', 'Õ' => 'O', 'Ø' => 'O', 'Ō' => 'O', 'Ő' => 'O',
134 'Ŏ' => 'O', 'Ŕ' => 'R', 'Ř' => 'R', 'Ŗ' => 'R', 'Ś' => 'S', 'Ş' => 'S',
135 'Ŝ' => 'S', 'Ș' => 'S', 'Š' => 'S', 'Ť' => 'T', 'Ţ' => 'T', 'Ŧ' => 'T',
136 'Ț' => 'T', 'Ù' => 'U', 'Ú' => 'U', 'Û' => 'U', 'Ū' => 'U', 'Ů' => 'U',
137 'Ű' => 'U', 'Ŭ' => 'U', 'Ũ' => 'U', 'Ų' => 'U', 'Ŵ' => 'W', 'Ŷ' => 'Y',
138 'Ÿ' => 'Y', 'Ý' => 'Y', 'Ź' => 'Z', 'Ż' => 'Z', 'Ž' => 'Z', 'à' => 'a',
139 'á' => 'a', 'â' => 'a', 'ã' => 'a', 'ā' => 'a', '�
140 ' => 'a', 'ă' => 'a',
141 'å' => 'a', 'ç' => 'c', 'ć' => 'c', 'č' => 'c', 'ĉ' => 'c', 'ċ' => 'c',
142 'ď' => 'd', 'đ' => 'd', 'è' => 'e', 'é' => 'e', 'ê' => 'e', 'ë' => 'e',
143 'ē' => 'e', 'ę' => 'e', 'ě' => 'e', 'ĕ' => 'e', 'ė' => 'e', 'ƒ' => 'f',
144 'ĝ' => 'g', 'ğ' => 'g', 'ġ' => 'g', 'ģ' => 'g', 'ĥ' => 'h', 'ħ' => 'h',
145 'ì' => 'i', 'í' => 'i', 'î' => 'i', 'ï' => 'i', 'ī' => 'i', 'ĩ' => 'i',
146 'ĭ' => 'i', 'į' => 'i', 'ı' => 'i', 'ĵ' => 'j', 'ķ' => 'k', 'ĸ' => 'k',
147 'ł' => 'l', 'ľ' => 'l', 'ĺ' => 'l', 'ļ' => 'l', 'ŀ' => 'l', 'ñ' => 'n',
148 'ń' => 'n', 'ň' => 'n', 'ņ' => 'n', 'ʼn' => 'n', 'ŋ' => 'n', 'ò' => 'o',
149 'ó' => 'o', 'ô' => 'o', 'õ' => 'o', 'ø' => 'o', 'ō' => 'o', 'ő' => 'o',
150 'ŏ' => 'o', 'ŕ' => 'r', 'ř' => 'r', 'ŗ' => 'r', 'ś' => 's', 'š' => 's',
151 'ť' => 't', 'ù' => 'u', 'ú' => 'u', 'û' => 'u', 'ū' => 'u', 'ů' => 'u',
152 'ű' => 'u', 'ŭ' => 'u', 'ũ' => 'u', 'ų' => 'u', 'ŵ' => 'w', 'ÿ' => 'y',
153 'ý' => 'y', 'ŷ' => 'y', 'ż' => 'z', 'ź' => 'z', 'ž' => 'z', 'Α' => 'A',
154 'Ά' => 'A', '' => 'A', '' => 'A', '' => 'A', '' => 'A', '' => 'A',
155 '' => 'A', '' => 'A', '' => 'A', '' => 'A', '' => 'A', '' => 'A',
156 '' => 'A', '' => 'A', '' => 'A', '' => 'A', '' => 'A', '' => 'A',
157 '' => 'A', '' => 'A', '' => 'A', 'Β' => 'B', 'Γ' => 'G', 'Δ' => 'D',
158 'Ε' => 'E', 'Έ' => 'E', '' => 'E', '' => 'E', '' => 'E', '' => 'E',
159 '' => 'E', '' => 'E', '' => 'E', 'Ζ' => 'Z', 'Η' => 'I', 'Ή' => 'I',
160 '' => 'I', '' => 'I', '' => 'I', '' => 'I', '' => 'I', '' => 'I',
161 '' => 'I', '' => 'I', '' => 'I', '' => 'I', '' => 'I', '' => 'I',
162 '' => 'I', '' => 'I', '' => 'I', '' => 'I', '' => 'I', '' => 'I',
163 'Θ' => 'T', 'Ι' => 'I', 'Ί' => 'I', 'Ϊ' => 'I', '' => 'I', '' => 'I',
164 '' => 'I', '' => 'I', '' => 'I', '' => 'I', '' => 'I', 'Ἷ' => 'I',
165 '' => 'I', '' => 'I', '' => 'I', 'Κ' => 'K', 'Λ' => 'L', 'Μ' => 'M',
166 'Ν' => 'N', 'Ξ' => 'K', 'Ο' => 'O', 'Ό' => 'O', '' => 'O', '' => 'O',
167 '' => 'O', '' => 'O', '' => 'O', '' => 'O', '' => 'O', 'Π' => 'P',
168 'Ρ' => 'R', '' => 'R', 'Σ' => 'S', 'Τ' => 'T', 'Υ' => 'Y', 'Ύ' => 'Y',
169 'Ϋ' => 'Y', '' => 'Y', '' => 'Y', '' => 'Y', '' => 'Y', '' => 'Y',
170 '' => 'Y', '' => 'Y', 'Φ' => 'F', 'Χ' => 'X', 'Ψ' => 'P', 'Ω' => 'O',
171 'Ώ' => 'O', '' => 'O', '' => 'O', '' => 'O', '' => 'O', '' => 'O',
172 '' => 'O', '' => 'O', '' => 'O', '' => 'O', '' => 'O', '' => 'O',
173 '' => 'O', '' => 'O', '' => 'O', '' => 'O', '' => 'O', '' => 'O',
174 '' => 'O', 'α' => 'a', 'ά' => 'a', '' => 'a', '' => 'a', '' => 'a',
175 'ἃ' => 'a', 'ἄ' => 'a', '�
176 ' => 'a', '' => 'a', '' => 'a', '' => 'a',
177 'ᾁ' => 'a', 'ᾂ' => 'a', 'ᾃ' => 'a', 'ᾄ' => 'a', '�
178 ' => 'a', '' => 'a',
179 '' => 'a', '' => 'a', '' => 'a', '' => 'a', '' => 'a', '' => 'a',
180 '' => 'a', '' => 'a', '' => 'a', 'β' => 'b', 'γ' => 'g', 'δ' => 'd',
181 'ε' => 'e', 'έ' => 'e', '' => 'e', '' => 'e', '' => 'e', '' => 'e',
182 '' => 'e', '' => 'e', '' => 'e', 'ζ' => 'z', 'η' => 'i', 'ή' => 'i',
183 '' => 'i', '' => 'i', '' => 'i', '' => 'i', '' => 'i', '' => 'i',
184 '' => 'i', '' => 'i', '' => 'i', '' => 'i', '' => 'i', '' => 'i',
185 '' => 'i', '' => 'i', '' => 'i', '' => 'i', '' => 'i', '' => 'i',
186 '' => 'i', '' => 'i', '' => 'i', '' => 'i', 'θ' => 't', 'ι' => 'i',
187 'ί' => 'i', 'ϊ' => 'i', 'ΐ' => 'i', '' => 'i', '' => 'i', '' => 'i',
188 '' => 'i', '' => 'i', '' => 'i', '' => 'i', '' => 'i', '' => 'i',
189 '' => 'i', '' => 'i', '' => 'i', '' => 'i', '' => 'i', 'κ' => 'k',
190 'λ' => 'l', 'μ' => 'm', 'ν' => 'n', 'ξ' => 'k', 'ο' => 'o', 'ό' => 'o',
191 'ὀ' => 'o', 'ὁ' => 'o', 'ὂ' => 'o', 'ὃ' => 'o', 'ὄ' => 'o', '�
192 ' => 'o',
193 '' => 'o', 'π' => 'p', 'ρ' => 'r', '' => 'r', '' => 'r', 'σ' => 's',
194 'ς' => 's', 'τ' => 't', '�
195 ' => 'y', 'ύ' => 'y', 'ϋ' => 'y', 'ΰ' => 'y',
196 '' => 'y', '' => 'y', '' => 'y', '' => 'y', '' => 'y', '' => 'y',
197 '' => 'y', '' => 'y', '' => 'y', '' => 'y', '' => 'y', '' => 'y',
198 '' => 'y', '' => 'y', 'φ' => 'f', 'χ' => 'x', 'ψ' => 'p', 'ω' => 'o',
199 'ώ' => 'o', '' => 'o', '' => 'o', '' => 'o', '' => 'o', '' => 'o',
200 '' => 'o', '' => 'o', '' => 'o', '' => 'o', '' => 'o', '' => 'o',
201 '' => 'o', '' => 'o', '' => 'o', '' => 'o', '' => 'o', '' => 'o',
202 '' => 'o', '' => 'o', '' => 'o', '' => 'o', '' => 'o', 'А' => 'A',
203 'Б' => 'B', 'В' => 'V', 'Г' => 'G', 'Д' => 'D', 'Е' => 'E', 'Ё' => 'E',
204 'Ж' => 'Z', 'З' => 'Z', 'И' => 'I', 'Й' => 'I', 'К' => 'K', 'Л' => 'L',
205 'М' => 'M', 'Н' => 'N', 'О' => 'O', 'П' => 'P', 'Р' => 'R', 'С' => 'S',
206 'Т' => 'T', 'У' => 'U', 'Ф' => 'F', 'Х' => 'K', 'Ц' => 'T', 'Ч' => 'C',
207 'Ш' => 'S', 'Щ' => 'S', 'Ы' => 'Y', 'Э' => 'E', 'Ю' => 'Y', 'Я' => 'Y',
208 'а' => 'A', 'б' => 'B', 'в' => 'V', 'г' => 'G', 'д' => 'D', 'е' => 'E',
209 'ё' => 'E', 'ж' => 'Z', 'з' => 'Z', 'и' => 'I', 'й' => 'I', 'к' => 'K',
210 'л' => 'L', 'м' => 'M', 'н' => 'N', 'о' => 'O', 'п' => 'P', 'р' => 'R',
211 'с' => 'S', 'т' => 'T', 'у' => 'U', 'ф' => 'F', '�
212 ' => 'K', 'ц' => 'T',
213 'ч' => 'C', 'ш' => 'S', 'щ' => 'S', 'ы' => 'Y', 'э' => 'E', 'ю' => 'Y',
214 'я' => 'Y', 'ð' => 'd', 'Ð' => 'D', 'þ' => 't', 'Þ' => 'T', '' => 'a',
215 '' => 'b', '' => 'g', '' => 'd', '' => 'e', '' => 'v', '' => 'z',
216 '' => 't', '' => 'i', '' => 'k', '' => 'l', '' => 'm', '' => 'n',
217 '' => 'o', '' => 'p', '' => 'z', '' => 'r', '' => 's', '' => 't',
218 '' => 'u', '' => 'p', '' => 'k', '' => 'g', '' => 'q', '' => 's',
219 '' => 'c', '' => 't', '' => 'd', '' => 't', '' => 'c', '' => 'k',
220 '' => 'j', '' => 'h', 'ţ' => 't', 'ʼ' => "'", '̧' => '', '' => 'h',
221 '' => "'", '' => "'", '' => 'u', '/' => '', 'ế' => 'e', '' => 'a',
222 '' => 'i', '' => 'a', '' => 'e', '' => 'i', '' => 'o', '' => 'e',
223 'ơ' => 'o', '' => 'a', '' => 'a', 'ư' => 'u', '' => 'a', '' => 'a',
224 '' => 'd', '' => 'H', '' => 'D', 'ș' => 's', 'ț' => 't', '' => 'o',
225 '' => 'a', 'ş' => 's', "'" => '', 'ու' => 'u', 'ա' => 'a', 'բ' => 'b',
226 'գ' => 'g', 'դ' => 'd', 'ե' => 'e', 'զ' => 'z', 'է' => 'e', 'ը' => 'y',
227 'թ' => 't', 'ժ' => 'zh', 'ի' => 'i', 'լ' => 'l', 'խ' => 'kh', 'ծ' => 'ts',
228 'կ' => 'k', 'հ' => 'h', 'ձ' => 'dz', 'ղ' => 'gh', 'ճ' => 'ch', 'մ' => 'm',
229 'յ' => 'y', 'ն' => 'n', 'շ' => 'sh', 'ո' => 'o', 'չ' => 'ch', 'պ' => 'p',
230 'ջ' => 'j', 'ռ' => 'r', 'ս' => 's', 'վ' => 'v', 'տ' => 't', 'ր' => 'r',
231 'ց' => 'ts', 'փ' => 'p', 'ք' => 'q', 'և' => 'ev', '�
232 ' => 'o', 'ֆ' => 'f',
233 ];
234 $arrayFrom = array_keys($transliterationTable);
235 $arrayTo = array_values($transliterationTable);
236 }
237
238 return str_replace($arrayFrom, $arrayTo, $value);
239 }
240
241 /**
242 * Transliterate a string to its closest ASCII representation.
243 *
244 * @param string $string
245 * @return string
246 * @see https://github.com/fzaninotto/Faker/blob/master/src/Faker/Provider/Internet.php#L229
247 */
248 public static function transliterate($string)
249 {
250 if (0 === preg_match('/[^A-Za-z0-9_.]/', $string)) {
251 return $string;
252 }
253
254 $transString = static::ascii($string);
255
256 return preg_replace('/[^A-Za-z0-9_.]/u', '', $transString);
257 }
258
259 /**
260 * Get the portion of a string before the first occurrence of a given value.
261 *
262 * @param string $subject
263 * @param string $search
264 * @return string
265 */
266 public static function before($subject, $search)
267 {
268 if ($search === '') {
269 return $subject;
270 }
271
272 $result = strstr($subject, (string) $search, true);
273
274 return $result === false ? $subject : $result;
275 }
276
277 /**
278 * Get the portion of a string before the last occurrence of a given value.
279 *
280 * @param string $subject
281 * @param string $search
282 * @return string
283 */
284 public static function beforeLast($subject, $search)
285 {
286 if ($search === '') {
287 return $subject;
288 }
289
290 $pos = mb_strrpos($subject, $search);
291
292 if ($pos === false) {
293 return $subject;
294 }
295
296 return static::substr($subject, 0, $pos);
297 }
298
299 /**
300 * Get the portion of a string between two given values.
301 *
302 * @param string $subject
303 * @param string $from
304 * @param string $to
305 * @return string
306 */
307 public static function between($subject, $from, $to)
308 {
309 if ($from === '' || $to === '') {
310 return $subject;
311 }
312
313 return static::beforeLast(static::after($subject, $from), $to);
314 }
315
316 /**
317 * Get the smallest possible portion of a string between two given values.
318 *
319 * @param string $subject
320 * @param string $from
321 * @param string $to
322 * @return string
323 */
324 public static function betweenFirst($subject, $from, $to)
325 {
326 if ($from === '' || $to === '') {
327 return $subject;
328 }
329
330 return static::before(static::after($subject, $from), $to);
331 }
332
333 /**
334 * Convert a value to camel case.
335 *
336 * @param string $value
337 * @return string
338 */
339 public static function camel($value)
340 {
341 if (isset(static::$camelCache[$value])) {
342 return static::$camelCache[$value];
343 }
344
345 return static::$camelCache[$value] = lcfirst(static::studly($value));
346 }
347
348 /**
349 * Get the character at the specified index.
350 *
351 * @param string $subject
352 * @param int $index
353 * @return string|false
354 */
355 public static function charAt($subject, $index)
356 {
357 $length = mb_strlen($subject);
358
359 if ($index < 0 ? $index < -$length : $index > $length - 1) {
360 return false;
361 }
362
363 return mb_substr($subject, $index, 1);
364 }
365
366 /**
367 * Remove the given string(s) if it exists at the start of the haystack.
368 *
369 * @param string $subject
370 * @param string|array $needle
371 * @return string
372 */
373 public static function chopStart($subject, $needle)
374 {
375 foreach ((array) $needle as $n) {
376 if (str_starts_with($subject, $n)) {
377 return substr($subject, strlen($n));
378 }
379 }
380
381 return $subject;
382 }
383
384 /**
385 * Remove the given string(s) if it exists at the end of the haystack.
386 *
387 * @param string $subject
388 * @param string|array $needle
389 * @return string
390 */
391 public static function chopEnd($subject, $needle)
392 {
393 foreach ((array) $needle as $n) {
394 if (str_ends_with($subject, $n)) {
395 return substr($subject, 0, -strlen($n));
396 }
397 }
398
399 return $subject;
400 }
401
402 /**
403 * Determine if a given string contains a given substring.
404 *
405 * @param string $haystack
406 * @param string|iterable<string> $needles
407 * @param bool $ignoreCase
408 * @return bool
409 */
410 public static function contains($haystack, $needles, $ignoreCase = false)
411 {
412 if ($ignoreCase) {
413 $haystack = mb_strtolower($haystack);
414 }
415
416 if (! is_iterable($needles)) {
417 $needles = (array) $needles;
418 }
419
420 foreach ($needles as $needle) {
421 if ($ignoreCase) {
422 $needle = mb_strtolower($needle);
423 }
424
425 if ($needle !== '' && str_contains($haystack, $needle)) {
426 return true;
427 }
428 }
429
430 return false;
431 }
432
433 /**
434 * Determine if a given string contains all array values.
435 *
436 * @param string $haystack
437 * @param iterable<string> $needles
438 * @param bool $ignoreCase
439 * @return bool
440 */
441 public static function containsAll($haystack, $needles, $ignoreCase = false)
442 {
443 foreach ($needles as $needle) {
444 if (! static::contains($haystack, $needle, $ignoreCase)) {
445 return false;
446 }
447 }
448
449 return true;
450 }
451
452 /**
453 * Replace consecutive instances of a given character
454 * with a single character in the given string.
455 *
456 * @param string $string
457 * @param string $character
458 * @return string
459 */
460 public static function deduplicate(string $string, string $character = ' ')
461 {
462 return preg_replace(
463 '/'.preg_quote($character, '/').'+/u', $character, $string
464 );
465 }
466
467 /**
468 * Determine if a given string ends with a given substring.
469 *
470 * @param string $haystack
471 * @param array|string $needles
472 * @return bool
473 */
474 public static function endsWith($haystack, $needles)
475 {
476 if (! is_iterable($needles)) {
477 $needles = (array) $needles;
478 }
479
480 if (is_null($haystack)) {
481 return false;
482 }
483
484 foreach ($needles as $needle) {
485 if ((string) $needle !== '' && str_ends_with($haystack, $needle)) {
486 return true;
487 }
488 }
489
490 return false;
491 }
492
493 /**
494 * Cap a string with a single instance of a given value.
495 *
496 * @param string $value
497 * @param string $cap
498 * @return string
499 */
500 public static function finish($value, $cap)
501 {
502 $quoted = preg_quote($cap, '/');
503
504 return preg_replace('/(?:'.$quoted.')+$/u', '', $value).$cap;
505 }
506
507 /**
508 * Determine if a given string matches a given pattern.
509 *
510 * @param string|array $pattern
511 * @param string $value
512 * @return bool
513 */
514 public static function is($pattern, $value)
515 {
516 $patterns = Arr::wrap($pattern);
517
518 $value = (string) $value;
519
520 if (empty($patterns)) {
521 return false;
522 }
523
524 foreach ($patterns as $pattern) {
525 $pattern = (string) $pattern;
526
527 // If the given value is an exact match we can of course return
528 // true right from the beginning. Otherwise, we will translate
529 // asterisks and do an actual pattern match against the
530 // two strings to see if they match.
531 if ($pattern == $value) {
532 return true;
533 }
534
535 $pattern = preg_quote($pattern, '#');
536
537 // Asterisks are translated into zero-or-more regular expression
538 // wildcards to make it convenient to check if the strings
539 // starts with the given pattern such as "library/*",
540 // making any string check convenient.
541 $pattern = str_replace('\*', '.*', $pattern);
542
543 if (preg_match('#^'.$pattern.'\z#u', $value) === 1) {
544 return true;
545 }
546 }
547
548 return false;
549 }
550
551 /**
552 * Converts a non-boolean value to boolean
553 * @param string|int $str
554 * @return bool|null
555 */
556 public static function toBool($str)
557 {
558 if (is_bool($str)) {
559 return $str;
560 }
561
562 $truthy = ['yes', 'on', 'true', '1', 1];
563
564 $falsy = ['no', 'off', 'false', '0', 0, ''];
565
566 if (in_array($str, $truthy, true)) {
567 return true;
568 } elseif (in_array($str, $falsy, true)) {
569 return false;
570 }
571 }
572
573 /**
574 * Determine if a given string is 7 bit ASCII.
575 *
576 * @param string $value
577 * @return bool
578 * @see https://developer.wordpress.org/reference/classes/requests_idnaencoder/is_ascii/
579 */
580 public static function isAscii($value)
581 {
582 return (preg_match('/(?:[^\x00-\x7F])/', $value) !== 1);
583 }
584
585 /**
586 * Checks if the word(s) are in capitalized form.
587 * @param string $str
588 * @param boolean $onlyFirst if true, checks only the first charatcer
589 * @return boolean
590 */
591 public static function isCapitalized($str, $onlyFirst = false)
592 {
593 if ($onlyFirst) {
594 return static::isUpper($str[0]);
595 }
596
597 if ($words = mb_split('\s', $str)) {
598 foreach ($words as $word) {
599 $isCap[] = static::isUpper($word[0]) && static::isLower(mb_substr($word, 1));
600 }
601 return count(array_filter($isCap)) === count($isCap);
602 }
603 }
604
605 /**
606 * Checks if the first character is in capital form.
607 *
608 * @param string $str
609 * @return boolean
610 */
611 public static function isCapital($str)
612 {
613 return static::isCapitalized($str, true);
614 }
615
616 /**
617 * Checks if the chracters are in upper case
618 *
619 * @param string $str
620 * @return boolean
621 */
622 public static function isUpper($str)
623 {
624 return $str === static::upper($str);
625 }
626
627 /**
628 * Checks if the chracters are in lower case
629 *
630 * @param string $str
631 * @return boolean
632 */
633 public static function isLower($str)
634 {
635 return $str === static::lower($str);
636 }
637
638 /**
639 * Checks if two words sounds alike
640 *
641 * @param string $str1
642 * @param string $str2
643 * @return bool
644 */
645 public static function soundsAlike($str1, $str2)
646 {
647 return soundex($str1) == soundex($str2);
648 }
649
650 /**
651 * Checks if two words are similar
652 *
653 * @param string $str1
654 * @param string $str2
655 *
656 * @return bool
657 */
658 public static function isSimilar($str1, $str2, $accuracy = 60)
659 {
660 $percent = static::similarityOf($str1, $str2);
661
662 return $percent > $accuracy;
663 }
664
665 /**
666 * Checks if two words are similar
667 *
668 * @param string $str1
669 * @param string $str2
670 *
671 * @return bool
672 */
673 public static function similarityOf($str1, $str2)
674 {
675 similar_text($str1, $str2, $percent);
676
677 return $percent;
678 }
679
680 /**
681 * Determine if a given value is valid JSON.
682 *
683 * @param mixed $value
684 * @return bool
685 */
686 public static function isJson($value)
687 {
688 if (! is_string($value)) {
689 return false;
690 }
691
692 try {
693 json_decode($value, true, 512, JSON_THROW_ON_ERROR);
694 } catch (JsonException $e) {
695 return false;
696 }
697
698 return true;
699 }
700
701 /**
702 * Determine if a given value is a valid URL.
703 *
704 * @param mixed $value
705 * @param array $protocols
706 * @return bool
707 */
708 public static function isUrl($value, array $protocols = [])
709 {
710 if (! is_string($value)) {
711 return false;
712 }
713
714 $protocolList = empty($protocols)
715 ? 'aaa|aaas|about|acap|acct|acd|acr|adiumxtra|adt|afp|afs|aim|amss|android|appdata|apt|ark|attachment|aw|barion|beshare|bitcoin|bitcoincash|blob|bolo|browserext|calculator|callto|cap|cast|casts|chrome|chrome-extension|cid|coap|coap\+tcp|coap\+ws|coaps|coaps\+tcp|coaps\+ws|com-eventbrite-attendee|content|conti|crid|cvs|dab|data|dav|diaspora|dict|did|dis|dlna-playcontainer|dlna-playsingle|dns|dntp|dpp|drm|drop|dtn|dvb|ed2k|elsi|example|facetime|fax|feed|feedready|file|filesystem|finger|first-run-pen-experience|fish|fm|ftp|fuchsia-pkg|geo|gg|git|gizmoproject|go|gopher|graph|gtalk|h323|ham|hcap|hcp|http|https|hxxp|hxxps|hydrazone|iax|icap|icon|im|imap|info|iotdisco|ipn|ipp|ipps|irc|irc6|ircs|iris|iris\.beep|iris\.lwz|iris\.xpc|iris\.xpcs|isostore|itms|jabber|jar|jms|keyparc|lastfm|ldap|ldaps|leaptofrogans|lorawan|lvlt|magnet|mailserver|mailto|maps|market|message|mid|mms|modem|mongodb|moz|ms-access|ms-browser-extension|ms-calculator|ms-drive-to|ms-enrollment|ms-excel|ms-eyecontrolspeech|ms-gamebarservices|ms-gamingoverlay|ms-getoffice|ms-help|ms-infopath|ms-inputapp|ms-lockscreencomponent-config|ms-media-stream-id|ms-mixedrealitycapture|ms-mobileplans|ms-officeapp|ms-people|ms-project|ms-powerpoint|ms-publisher|ms-restoretabcompanion|ms-screenclip|ms-screensketch|ms-search|ms-search-repair|ms-secondary-screen-controller|ms-secondary-screen-setup|ms-settings|ms-settings-airplanemode|ms-settings-bluetooth|ms-settings-camera|ms-settings-cellular|ms-settings-cloudstorage|ms-settings-connectabledevices|ms-settings-displays-topology|ms-settings-emailandaccounts|ms-settings-language|ms-settings-location|ms-settings-lock|ms-settings-nfctransactions|ms-settings-notifications|ms-settings-power|ms-settings-privacy|ms-settings-proximity|ms-settings-screenrotation|ms-settings-wifi|ms-settings-workplace|ms-spd|ms-sttoverlay|ms-transit-to|ms-useractivityset|ms-virtualtouchpad|ms-visio|ms-walk-to|ms-whiteboard|ms-whiteboard-cmd|ms-word|msnim|msrp|msrps|mss|mtqp|mumble|mupdate|mvn|news|nfs|ni|nih|nntp|notes|ocf|oid|onenote|onenote-cmd|opaquelocktoken|openpgp4fpr|pack|palm|paparazzi|payto|pkcs11|platform|pop|pres|prospero|proxy|pwid|psyc|pttp|qb|query|redis|rediss|reload|res|resource|rmi|rsync|rtmfp|rtmp|rtsp|rtsps|rtspu|s3|secondlife|service|session|sftp|sgn|shttp|sieve|simpleledger|sip|sips|skype|smb|sms|smtp|snews|snmp|soap\.beep|soap\.beeps|soldat|spiffe|spotify|ssh|steam|stun|stuns|submit|svn|tag|teamspeak|tel|teliaeid|telnet|tftp|tg|things|thismessage|tip|tn3270|tool|ts3server|turn|turns|tv|udp|unreal|urn|ut2004|v-event|vemmi|ventrilo|videotex|vnc|view-source|wais|webcal|wpid|ws|wss|wtai|wyciwyg|xcon|xcon-userid|xfire|xmlrpc\.beep|xmlrpc\.beeps|xmpp|xri|ymsgr|z39\.50|z39\.50r|z39\.50s'
716 : implode('|', $protocols);
717
718 /*
719 * This pattern is derived from Symfony\Component\Validator\Constraints\UrlValidator (5.0.7).
720 *
721 * (c) Fabien Potencier <fabien@symfony.com> http://symfony.com
722 */
723 $pattern = '~^
724 (FLUENT_PROTOCOLS):// # protocol
725 (((?:[\_\.\pL\pN-]|%[0-9A-Fa-f]{2})+:)?((?:[\_\.\pL\pN-]|%[0-9A-Fa-f]{2})+)@)? # basic auth
726 (
727 ([\pL\pN\pS\-\_\.])+(\.?([\pL\pN]|xn\-\-[\pL\pN-]+)+\.?) # a domain name
728 | # or
729 \d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3} # an IP address
730 | # or
731 \[
732 (?:(?:(?:(?:(?:(?:(?:[0-9a-f]{1,4})):){6})(?:(?:(?:(?:(?:[0-9a-f]{1,4})):(?:(?:[0-9a-f]{1,4})))|(?:(?:(?:(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9]))\.){3}(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9])))))))|(?:(?:::(?:(?:(?:[0-9a-f]{1,4})):){5})(?:(?:(?:(?:(?:[0-9a-f]{1,4})):(?:(?:[0-9a-f]{1,4})))|(?:(?:(?:(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9]))\.){3}(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9])))))))|(?:(?:(?:(?:(?:[0-9a-f]{1,4})))?::(?:(?:(?:[0-9a-f]{1,4})):){4})(?:(?:(?:(?:(?:[0-9a-f]{1,4})):(?:(?:[0-9a-f]{1,4})))|(?:(?:(?:(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9]))\.){3}(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9])))))))|(?:(?:(?:(?:(?:(?:[0-9a-f]{1,4})):){0,1}(?:(?:[0-9a-f]{1,4})))?::(?:(?:(?:[0-9a-f]{1,4})):){3})(?:(?:(?:(?:(?:[0-9a-f]{1,4})):(?:(?:[0-9a-f]{1,4})))|(?:(?:(?:(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9]))\.){3}(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9])))))))|(?:(?:(?:(?:(?:(?:[0-9a-f]{1,4})):){0,2}(?:(?:[0-9a-f]{1,4})))?::(?:(?:(?:[0-9a-f]{1,4})):){2})(?:(?:(?:(?:(?:[0-9a-f]{1,4})):(?:(?:[0-9a-f]{1,4})))|(?:(?:(?:(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9]))\.){3}(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9])))))))|(?:(?:(?:(?:(?:(?:[0-9a-f]{1,4})):){0,3}(?:(?:[0-9a-f]{1,4})))?::(?:(?:[0-9a-f]{1,4})):)(?:(?:(?:(?:(?:[0-9a-f]{1,4})):(?:(?:[0-9a-f]{1,4})))|(?:(?:(?:(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9]))\.){3}(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9])))))))|(?:(?:(?:(?:(?:(?:[0-9a-f]{1,4})):){0,4}(?:(?:[0-9a-f]{1,4})))?::)(?:(?:(?:(?:(?:[0-9a-f]{1,4})):(?:(?:[0-9a-f]{1,4})))|(?:(?:(?:(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9]))\.){3}(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9])))))))|(?:(?:(?:(?:(?:(?:[0-9a-f]{1,4})):){0,5}(?:(?:[0-9a-f]{1,4})))?::)(?:(?:[0-9a-f]{1,4})))|(?:(?:(?:(?:(?:(?:[0-9a-f]{1,4})):){0,6}(?:(?:[0-9a-f]{1,4})))?::))))
733 \] # an IPv6 address
734 )
735 (:[0-9]+)? # a port (optional)
736 (?:/ (?:[\pL\pN\-._\~!$&\'()*+,;=:@]|%[0-9A-Fa-f]{2})* )* # a path
737 (?:\? (?:[\pL\pN\-._\~!$&\'\[\]()*+,;=:@/?]|%[0-9A-Fa-f]{2})* )? # a query (optional)
738 (?:\# (?:[\pL\pN\-._\~!$&\'()*+,;=:@/?]|%[0-9A-Fa-f]{2})* )? # a fragment (optional)
739 $~ixu';
740
741 return preg_match(str_replace('FLUENT_PROTOCOLS', $protocolList, $pattern), $value) > 0;
742 }
743
744 /**
745 * Determine if a given string is a valid UUID.
746 *
747 * @param string $value
748 * @return bool
749 */
750 public static function isUuid($value)
751 {
752 return wp_is_uuid($value);
753 }
754
755 /**
756 * Convert a string to kebab case.
757 *
758 * @param string $value
759 * @return string
760 */
761 public static function kebab($value)
762 {
763 return static::snake($value, '-');
764 }
765
766 /**
767 * Return the length of the given string.
768 *
769 * @param string $value
770 * @param string|null $encoding
771 * @return int
772 */
773 public static function length($value, $encoding = null)
774 {
775 if ($encoding) {
776 return mb_strlen($value, $encoding);
777 }
778
779 return mb_strlen($value);
780 }
781
782 /**
783 * Limit the number of characters in a string.
784 *
785 * @param string $value
786 * @param int $limit
787 * @param string $end
788 * @param bool $preserveWords
789 * @return string
790 */
791 public static function limit(
792 $value, $limit = 100, $end = '...', $preserveWords = false
793 )
794 {
795 if (mb_strwidth($value, 'UTF-8') <= $limit) {
796 return $value;
797 }
798
799 if (! $preserveWords) {
800 return rtrim(mb_strimwidth($value, 0, $limit, '', 'UTF-8')).$end;
801 }
802
803 $value = trim(preg_replace('/[\n\r]+/', ' ', strip_tags($value)));
804
805 $trimmed = rtrim(mb_strimwidth($value, 0, $limit, '', 'UTF-8'));
806
807 if (mb_substr($value, $limit, 1, 'UTF-8') === ' ') {
808 return $trimmed.$end;
809 }
810
811 return preg_replace("/(.*)\s.*/", '$1', $trimmed).$end;
812 }
813
814 /**
815 * Convert the given string to lower-case.
816 *
817 * @param string $value
818 * @return string
819 */
820 public static function lower($value)
821 {
822 return mb_strtolower($value, 'UTF-8');
823 }
824
825 /**
826 * Limit the number of words in a string.
827 *
828 * @param string $value
829 * @param int $words
830 * @param string $end
831 * @return string
832 */
833 public static function words($value, $words = 100, $end = '...')
834 {
835 preg_match('/^\s*+(?:\S++\s*+){1,'.$words.'}/u', $value, $matches);
836
837 if (! isset($matches[0]) || static::length($value) === static::length($matches[0])) {
838 return $value;
839 }
840
841 return rtrim($matches[0]).$end;
842 }
843
844 /**
845 * Masks a portion of a string with a repeated character.
846 *
847 * @param string $string
848 * @param string $character
849 * @param int $index
850 * @param int|null $length
851 * @param string $encoding
852 * @return string
853 */
854 public static function mask(
855 $string, $character, $index, $length = null, $encoding = 'UTF-8'
856 )
857 {
858 if ($character === '') {
859 return $string;
860 }
861
862 if (is_null($length) && PHP_MAJOR_VERSION < 8) {
863 $length = mb_strlen($string, $encoding);
864 }
865
866 $segment = mb_substr($string, $index, $length, $encoding);
867
868 if ($segment === '') {
869 return $string;
870 }
871
872 $strlen = mb_strlen($string, $encoding);
873 $startIndex = $index;
874
875 if ($index < 0) {
876 $startIndex = $index < -$strlen ? 0 : $strlen + $index;
877 }
878
879 $start = mb_substr($string, 0, $startIndex, $encoding);
880 $segmentLen = mb_strlen($segment, $encoding);
881 $end = mb_substr($string, $startIndex + $segmentLen);
882
883 return $start.str_repeat(mb_substr($character, 0, 1, $encoding), $segmentLen).$end;
884 }
885
886 /**
887 * Get the string matching the given pattern.
888 *
889 * @param string $pattern
890 * @param string $subject
891 * @return string
892 */
893 public static function match($pattern, $subject)
894 {
895 preg_match($pattern, $subject, $matches);
896
897 if (! $matches) {
898 return '';
899 }
900
901 return $matches[1] ?? $matches[0];
902 }
903
904 /**
905 * Determine if a given string matches a given pattern.
906 *
907 * @param string|iterable<string> $pattern
908 * @param string $value
909 * @return bool
910 */
911 public static function isMatch($pattern, $value)
912 {
913 $value = (string) $value;
914
915 if (! is_iterable($pattern)) {
916 $pattern = [$pattern];
917 }
918
919 foreach ($pattern as $pattern) {
920 $pattern = (string) $pattern;
921
922 if (preg_match($pattern, $value) === 1) {
923 return true;
924 }
925 }
926
927 return false;
928 }
929
930 /**
931 * Get the string matching the given pattern.
932 *
933 * @param string $pattern
934 * @param string $subject
935 * @return \FluentCommunity\Framework\Support\Collection
936 */
937 public static function matchAll($pattern, $subject)
938 {
939 preg_match_all($pattern, $subject, $matches);
940
941 if (empty($matches[0])) {
942 return Helper::collect();
943 }
944
945 return Helper::collect($matches[1] ?? $matches[0]);
946 }
947
948 /**
949 * Pad both sides of a string with another.
950 *
951 * @param string $value
952 * @param int $length
953 * @param string $pad
954 * @return string
955 */
956 public static function padBoth($value, $length, $pad = ' ')
957 {
958 return str_pad($value, strlen($value) - mb_strlen($value) + $length, $pad, STR_PAD_BOTH);
959 }
960
961 /**
962 * Pad the left side of a string with another.
963 *
964 * @param string $value
965 * @param int $length
966 * @param string $pad
967 * @return string
968 */
969 public static function padLeft($value, $length, $pad = ' ')
970 {
971 return str_pad($value, strlen($value) - mb_strlen($value) + $length, $pad, STR_PAD_LEFT);
972 }
973
974 /**
975 * Pad the right side of a string with another.
976 *
977 * @param string $value
978 * @param int $length
979 * @param string $pad
980 * @return string
981 */
982 public static function padRight($value, $length, $pad = ' ')
983 {
984 return str_pad($value, strlen($value) - mb_strlen($value) + $length, $pad, STR_PAD_RIGHT);
985 }
986
987 /**
988 * Parse a Class[@]method style callback into class and method.
989 *
990 * @param string $callback
991 * @param string|null $default
992 * @return array<int, string|null>
993 */
994 public static function parseCallback($callback, $default = null)
995 {
996 return static::contains($callback, '@') ? explode('@', $callback, 2) : [$callback, $default];
997 }
998
999 /**
1000 * Parse an integer from a string.
1001 *
1002 * @param string $value
1003 * @return int|null
1004 */
1005 public static function parseInt($value)
1006 {
1007 if (preg_match('/[-+]?\d+/', $value, $matches)) {
1008 return (int) $matches[0];
1009 }
1010 }
1011
1012 /**
1013 * Parse a floasting point number from a string.
1014 *
1015 * @param string $value
1016 * @return float|null
1017 */
1018 public static function parseFloat($value)
1019 {
1020 if (preg_match('/[-+]?\d*\.?\d+(e[-+]?\d+)?/i', $value, $matches)) {
1021 return (float) $matches[0];
1022 }
1023 }
1024
1025 /**
1026 * Remove all non-numeric characters from a string.
1027 *
1028 * @param string $value
1029 * @return string
1030 */
1031 public static function parseNumber($value)
1032 {
1033 return preg_replace('/[^0-9]/', '', $value);
1034 }
1035
1036 /**
1037 * Get the plural form of an English word.
1038 *
1039 * @param string $value
1040 * @param int|array|\Countable $count
1041 * @return string
1042 */
1043 public static function plural($value, $count = 2)
1044 {
1045 return Pluralizer::plural($value, $count);
1046 }
1047
1048 /**
1049 * Pluralize the last word of an English, studly caps case string.
1050 *
1051 * @param string $value
1052 * @param int|array|\Countable $count
1053 * @return string
1054 */
1055 public static function pluralStudly($value, $count = 2)
1056 {
1057 $parts = preg_split('/(.)(?=[A-Z])/u', $value, -1, PREG_SPLIT_DELIM_CAPTURE);
1058
1059 $lastWord = array_pop($parts);
1060
1061 return implode('', $parts).self::plural($lastWord, $count);
1062 }
1063
1064 /**
1065 * Find the multi-byte safe position of the first
1066 * occurrence of a given substring in a string.
1067 *
1068 * @param string $haystack
1069 * @param string $needle
1070 * @param int $offset
1071 * @param string|null $encoding
1072 * @return int|false
1073 */
1074 public static function position(
1075 $haystack, $needle, $offset = 0, $encoding = null
1076 )
1077 {
1078 return mb_strpos($haystack, (string) $needle, $offset, $encoding);
1079 }
1080
1081 /**
1082 * Generate a more truly "random" alpha-numeric string.
1083 *
1084 * @param int $length
1085 * @return string
1086 */
1087 public static function random($length = 16)
1088 {
1089 $string = '';
1090
1091 while (($len = strlen($string)) < $length) {
1092 $size = $length - $len;
1093
1094 $bytes = random_bytes($size);
1095
1096 $string .= substr(
1097 str_replace(
1098 ['/', '+', '='], '', base64_encode($bytes)
1099 ), 0, $size
1100 );
1101 }
1102
1103 return $string;
1104 }
1105
1106 /**
1107 * Repeat the given string.
1108 *
1109 * @param string $string
1110 * @param int $times
1111 * @return string
1112 */
1113 public static function repeat(string $string, int $times)
1114 {
1115 return str_repeat($string, $times);
1116 }
1117
1118 /**
1119 * Replace a given value in the string sequentially with an array.
1120 *
1121 * @param string $search
1122 * @param array<int|string, string> $replace
1123 * @param string $subject
1124 * @return string
1125 */
1126 public static function replaceArray($search, array $replace, $subject)
1127 {
1128 $segments = explode($search, $subject);
1129
1130 $result = array_shift($segments);
1131
1132 foreach ($segments as $segment) {
1133 $result .= (array_shift($replace) ?? $search).$segment;
1134 }
1135
1136 return $result;
1137 }
1138
1139 /**
1140 * Replace the given value in the given string.
1141 *
1142 * @param string|iterable<string> $search
1143 * @param string|iterable<string> $replace
1144 * @param string|iterable<string> $subject
1145 * @param bool $caseSensitive
1146 * @return string|string[]
1147 */
1148 public static function replace($search, $replace, $subject, $caseSensitive = true)
1149 {
1150 if ($search instanceof Traversable) {
1151 $search = (new Collection($search))->all();
1152 }
1153
1154 if ($replace instanceof Traversable) {
1155 $replace = (new Collection($replace))->all();
1156 }
1157
1158 if ($subject instanceof Traversable) {
1159 $subject = (new Collection($subject))->all();
1160 }
1161
1162 return $caseSensitive
1163 ? str_replace($search, $replace, $subject)
1164 : str_ireplace($search, $replace, $subject);
1165 }
1166
1167 /**
1168 * Replace the first occurrence of a given value in the string.
1169 *
1170 * @param string $search
1171 * @param string $replace
1172 * @param string $subject
1173 * @return string
1174 */
1175 public static function replaceFirst($search, $replace, $subject)
1176 {
1177 if ($search === '') {
1178 return $subject;
1179 }
1180
1181 $position = strpos($subject, $search);
1182
1183 if ($position !== false) {
1184 return substr_replace($subject, $replace, $position, strlen($search));
1185 }
1186
1187 return $subject;
1188 }
1189
1190 /**
1191 * Replace the first occurrence of the given value if it appears at the start of the string.
1192 *
1193 * @param string $search
1194 * @param string $replace
1195 * @param string $subject
1196 * @return string
1197 */
1198 public static function replaceStart($search, $replace, $subject)
1199 {
1200 $search = (string) $search;
1201
1202 if ($search === '') {
1203 return $subject;
1204 }
1205
1206 if (static::startsWith($subject, $search)) {
1207 return static::replaceFirst($search, $replace, $subject);
1208 }
1209
1210 return $subject;
1211 }
1212
1213 /**
1214 * Replace the last occurrence of a given value in the string.
1215 *
1216 * @param string $search
1217 * @param string $replace
1218 * @param string $subject
1219 * @return string
1220 */
1221 public static function replaceLast($search, $replace, $subject)
1222 {
1223 if ($search === '') {
1224 return $subject;
1225 }
1226
1227 $position = strrpos($subject, $search);
1228
1229 if ($position !== false) {
1230 return substr_replace($subject, $replace, $position, strlen($search));
1231 }
1232
1233 return $subject;
1234 }
1235
1236 /**
1237 * Replace the last occurrence of a given value if it appears at the end of the string.
1238 *
1239 * @param string $search
1240 * @param string $replace
1241 * @param string $subject
1242 * @return string
1243 */
1244 public static function replaceEnd($search, $replace, $subject)
1245 {
1246 $search = (string) $search;
1247
1248 if ($search === '') {
1249 return $subject;
1250 }
1251
1252 if (static::endsWith($subject, $search)) {
1253 return static::replaceLast($search, $replace, $subject);
1254 }
1255
1256 return $subject;
1257 }
1258
1259 /**
1260 * Replace the patterns matching the given regular expression.
1261 *
1262 * @param array|string $pattern
1263 * @param \Closure|string[]|string $replace
1264 * @param array|string $subject
1265 * @param int $limit
1266 * @return string|string[]|null
1267 */
1268 public static function replaceMatches($pattern, $replace, $subject, $limit = -1)
1269 {
1270 if ($replace instanceof \Closure) {
1271 return preg_replace_callback($pattern, $replace, $subject, $limit);
1272 }
1273
1274 return preg_replace($pattern, $replace, $subject, $limit);
1275 }
1276
1277 /**
1278 * Remove any occurrence of the given string in the subject.
1279 *
1280 * @param string|array<string> $search
1281 * @param string $subject
1282 * @param bool $caseSensitive
1283 * @return string
1284 */
1285 public static function remove($search, $subject, $caseSensitive = true)
1286 {
1287 $subject = $caseSensitive
1288 ? str_replace($search, '', $subject)
1289 : str_ireplace($search, '', $subject);
1290
1291 return $subject;
1292 }
1293
1294 /**
1295 * Reverse the given string.
1296 *
1297 * @param string $value
1298 * @return string
1299 */
1300 public static function reverse(string $value)
1301 {
1302 return implode(array_reverse(mb_str_split($value)));
1303 }
1304
1305 /**
1306 * Begin a string with a single instance of a given value.
1307 *
1308 * @param string $value
1309 * @param string $prefix
1310 * @return string
1311 */
1312 public static function start($value, $prefix)
1313 {
1314 $quoted = preg_quote($prefix, '/');
1315
1316 return $prefix.preg_replace('/^(?:'.$quoted.')+/u', '', $value);
1317 }
1318
1319 /**
1320 * Convert the given string to upper-case.
1321 *
1322 * @param string $value
1323 * @return string
1324 */
1325 public static function upper($value)
1326 {
1327 return mb_strtoupper($value, 'UTF-8');
1328 }
1329
1330 /**
1331 * Convert the given string to title case.
1332 *
1333 * @param string $value
1334 * @return string
1335 */
1336 public static function title($value)
1337 {
1338 return mb_convert_case($value, MB_CASE_TITLE, 'UTF-8');
1339 }
1340
1341 /**
1342 * Convert the given string to title case for each word.
1343 *
1344 * @param string $value
1345 * @return string
1346 */
1347 public static function headline($value)
1348 {
1349 $parts = explode(' ', $value);
1350
1351 $parts = count($parts) > 1
1352 ? $parts = array_map([static::class, 'title'], $parts)
1353 : $parts = array_map([static::class, 'title'], static::ucsplit(implode('_', $parts)));
1354
1355 $collapsed = static::replace(['-', '_', ' '], '_', implode('_', $parts));
1356
1357 return implode(' ', array_filter(explode('_', $collapsed)));
1358 }
1359
1360 /**
1361 * Convert the given string to APA-style title case.
1362 *
1363 * See: https://apastyle.apa.org/style-grammar-guidelines/capitalization/title-case
1364 *
1365 * @param string $value
1366 * @return string
1367 */
1368 public static function apa($value)
1369 {
1370 if (trim($value) === '') {
1371 return $value;
1372 }
1373
1374 $minorWords = [
1375 'and', 'as', 'but', 'for', 'if', 'nor', 'or', 'so', 'yet', 'a', 'an',
1376 'the', 'at', 'by', 'for', 'in', 'of', 'off', 'on', 'per', 'to', 'up', 'via',
1377 'et', 'ou', 'un', 'une', 'la', 'le', 'les', 'de', 'du', 'des', 'par', 'à',
1378 ];
1379
1380 $endPunctuation = ['.', '!', '?', ':', '', ','];
1381
1382 $words = preg_split('/\s+/', $value, -1, PREG_SPLIT_NO_EMPTY);
1383
1384 for ($i = 0; $i < count($words); $i++) {
1385 $lowercaseWord = mb_strtolower($words[$i]);
1386
1387 if (str_contains($lowercaseWord, '-')) {
1388 $hyphenatedWords = explode('-', $lowercaseWord);
1389
1390 $hyphenatedWords = array_map(function ($part) use ($minorWords) {
1391 return (in_array($part, $minorWords) && mb_strlen($part) <= 3)
1392 ? $part
1393 : mb_strtoupper(mb_substr($part, 0, 1)).mb_substr($part, 1);
1394 }, $hyphenatedWords);
1395
1396 $words[$i] = implode('-', $hyphenatedWords);
1397 } else {
1398 if (in_array($lowercaseWord, $minorWords) &&
1399 mb_strlen($lowercaseWord) <= 3 &&
1400 ! ($i === 0 || in_array(mb_substr($words[$i - 1], -1), $endPunctuation))) {
1401 $words[$i] = $lowercaseWord;
1402 } else {
1403 $words[$i] = mb_strtoupper(mb_substr($lowercaseWord, 0, 1)).mb_substr($lowercaseWord, 1);
1404 }
1405 }
1406 }
1407
1408 return implode(' ', $words);
1409 }
1410
1411 /**
1412 * Get the singular form of an English word.
1413 *
1414 * @param string $value
1415 * @return string
1416 */
1417 public static function singular($value)
1418 {
1419 return Pluralizer::singular($value);
1420 }
1421
1422 /**
1423 * Generate a URL friendly "slug" from a given string.
1424 *
1425 * @param string $title
1426 * @return string
1427 * @see https://developer.wordpress.org/reference/functions/sanitize_title/
1428 */
1429 public static function slug($title, $fallback_title = '', $context = 'save')
1430 {
1431 return sanitize_title($title, $fallback_title, $context);
1432 }
1433
1434 /**
1435 * Convert a string to snake case.
1436 *
1437 * @param string $value
1438 * @param string $delimiter
1439 * @return string
1440 */
1441 public static function snake($value, $delimiter = '_')
1442 {
1443 $key = $value;
1444
1445 if (isset(static::$snakeCache[$key][$delimiter])) {
1446 return static::$snakeCache[$key][$delimiter];
1447 }
1448
1449 if (! ctype_lower($value)) {
1450 $value = preg_replace('/\s+/u', '', ucwords($value));
1451
1452 $value = static::lower(preg_replace('/(.)(?=[A-Z])/u', '$1'.$delimiter, $value));
1453 }
1454
1455 return static::$snakeCache[$key][$delimiter] = $value;
1456 }
1457
1458 /**
1459 * Remove whitespace (including special Unicode spaces) from a string.
1460 *
1461 * Supports trimming from left, right, or both ends, and allows
1462 * specifying additional characters to trim.
1463 *
1464 * @param string $value The string to trim.
1465 * @param string|null $charlist Optional additional characters to trim.
1466 * @param string $mode One of 'both' (default), 'left', 'right'.
1467 *
1468 * @return string The trimmed string.
1469 */
1470 protected static function _unicodeTrim(string $value, ?string $charlist = null, string $mode = 'both'): string
1471 {
1472 $defaultChars = " \n\r\t\v"; // \0 omitted to avoid null byte errors
1473
1474 $chars = $charlist ?? $defaultChars;
1475 $quoted = preg_quote($chars, '~');
1476
1477 // Unicode invisible spaces we want to include
1478 $unicodeSpaces = '\s\x{FEFF}\x{200B}\x{200E}';
1479
1480 switch ($mode) {
1481 case 'left':
1482 $pattern = '~^[' . $unicodeSpaces . $quoted . ']+~u';
1483 break;
1484 case 'right':
1485 $pattern = '~[' . $unicodeSpaces . $quoted . ']+$~u';
1486 break;
1487 case 'both':
1488 default:
1489 $pattern = '~^[' . $unicodeSpaces . $quoted . ']+|[' . $unicodeSpaces . $quoted . ']+$~u';
1490 break;
1491 }
1492
1493 return preg_replace($pattern, '', $value) ?? $value;
1494 }
1495
1496 /**
1497 * Remove all whitespace (including special Unicode spaces) from both ends of a string.
1498 *
1499 * @param string $value The string to trim.
1500 * @param string|null $charlist Optional additional characters to trim.
1501 *
1502 * @return string The trimmed string.
1503 */
1504 public static function trim(string $value, ?string $charlist = null): string
1505 {
1506 return static::_unicodeTrim($value, $charlist, 'both');
1507 }
1508
1509 /**
1510 * Remove all whitespace (including special Unicode spaces) from the beginning of a string.
1511 *
1512 * @param string $value The string to trim.
1513 * @param string|null $charlist Optional additional characters to trim.
1514 *
1515 * @return string The trimmed string.
1516 */
1517 public static function ltrim(string $value, ?string $charlist = null): string
1518 {
1519 return static::_unicodeTrim($value, $charlist, 'left');
1520 }
1521
1522 /**
1523 * Remove all whitespace (including special Unicode spaces) from the end of a string.
1524 *
1525 * @param string $value The string to trim.
1526 * @param string|null $charlist Optional additional characters to trim.
1527 *
1528 * @return string The trimmed string.
1529 */
1530 public static function rtrim(string $value, ?string $charlist = null): string
1531 {
1532 return static::_unicodeTrim($value, $charlist, 'right');
1533 }
1534
1535 /**
1536 * Remove all "extra" blank space from the given string.
1537 *
1538 * @param string $value
1539 * @return string
1540 */
1541 public static function squish($value)
1542 {
1543 return preg_replace('~(\s|\x{3164}|\x{1160})+~u', ' ', preg_replace('~^[\s\x{FEFF}]+|[\s\x{FEFF}]+$~u', '', $value));
1544 }
1545
1546 /**
1547 * Determine if a given string starts with a given substring.
1548 *
1549 * @param string $haystack
1550 * @param string|iterable<string> $needles
1551 * @return bool
1552 */
1553 public static function startsWith($haystack, $needles)
1554 {
1555 if (!is_iterable($needles)) {
1556 $needles = [$needles];
1557 }
1558
1559 if ($haystack === null) {
1560 return false;
1561 }
1562
1563 foreach ($needles as $needle) {
1564 if ((string) $needle !== '' && strncmp($haystack, $needle, strlen($needle)) === 0) {
1565 return true;
1566 }
1567 }
1568
1569 return false;
1570 }
1571
1572 /**
1573 * Convert a value to studly caps case.
1574 *
1575 * @param string $value
1576 * @return string
1577 */
1578 public static function studly($value)
1579 {
1580 $key = $value;
1581
1582 if (isset(static::$studlyCache[$key])) {
1583 return static::$studlyCache[$key];
1584 }
1585
1586 $words = explode(' ', static::replace(['-', '_'], ' ', $value));
1587
1588 $studlyWords = array_map(function ($word) {
1589 return static::ucfirst($word);
1590 }, $words);
1591
1592 return static::$studlyCache[$key] = implode($studlyWords);
1593 }
1594
1595 /**
1596 * Convert a value to Pascal case.
1597 *
1598 * @param string $value
1599 * @return string
1600 */
1601 public static function pascal($value)
1602 {
1603 return static::studly($value);
1604 }
1605
1606 /**
1607 * Returns the portion of the string specified by the start and length parameters.
1608 *
1609 * @param string $string
1610 * @param int $start
1611 * @param int|null $length
1612 * @return string
1613 */
1614 public static function substr($string, $start, $length = null)
1615 {
1616 return mb_substr($string, $start, $length, 'UTF-8');
1617 }
1618
1619 /**
1620 * Returns the number of substring occurrences.
1621 *
1622 * @param string $haystack
1623 * @param string $needle
1624 * @param int $offset
1625 * @param int|null $length
1626 * @return int
1627 */
1628 public static function substrCount(
1629 $haystack, $needle, $offset = 0, $length = null
1630 ) {
1631 if (! is_null($length)) {
1632 return substr_count($haystack, $needle, $offset, $length);
1633 } else {
1634 return substr_count($haystack, $needle, $offset);
1635 }
1636 }
1637
1638 /**
1639 * Replace text within a portion of a string.
1640 *
1641 * @param string|array $string
1642 * @param string|array $replace
1643 * @param array|int $offset
1644 * @param array|int|null $length
1645 * @return string|array
1646 */
1647 public static function substrReplace(
1648 $string, $replace, $offset = 0, $length = null
1649 ) {
1650 if ($length === null) {
1651 $length = strlen($string);
1652 }
1653
1654 return substr_replace($string, $replace, $offset, $length);
1655 }
1656
1657 /**
1658 * Swap multiple keywords in a string with other keywords.
1659 *
1660 * @param array $map
1661 * @param string $subject
1662 * @return string
1663 */
1664 public static function swap(array $map, $subject)
1665 {
1666 return strtr($subject, $map);
1667 }
1668
1669 /**
1670 * Take the first or last {$limit} characters of a string.
1671 *
1672 * @param string $string
1673 * @param int $limit
1674 * @return string
1675 */
1676 public static function take($string, int $limit): string
1677 {
1678 if ($limit < 0) {
1679 return static::substr($string, $limit);
1680 }
1681
1682 return static::substr($string, 0, $limit);
1683 }
1684
1685 /**
1686 * Convert the given string to Base64 encoding.
1687 *
1688 * @param string $string
1689 * @return string
1690 */
1691 public static function toBase64($string)
1692 {
1693 return base64_encode($string);
1694 }
1695
1696 /**
1697 * Decode the given Base64 encoded string.
1698 *
1699 * @param string $string
1700 * @param bool $strict
1701 * @return string|false
1702 */
1703 public static function fromBase64($string, $strict = false)
1704 {
1705 return base64_decode($string, $strict);
1706 }
1707
1708 /**
1709 * Make a string's first character lowercase.
1710 *
1711 * @param string $string
1712 * @return string
1713 */
1714 public static function lcfirst($string)
1715 {
1716 return static::lower(
1717 static::substr($string, 0, 1)
1718 ).static::substr($string, 1);
1719 }
1720
1721 /**
1722 * Make a string's first character uppercase.
1723 *
1724 * @param string $string
1725 * @return string
1726 */
1727 public static function ucfirst($string)
1728 {
1729 return static::upper(
1730 static::substr($string, 0, 1)
1731 ).static::substr($string, 1);
1732 }
1733
1734 /**
1735 * Split a string into pieces by uppercase characters.
1736 *
1737 * @param string $string
1738 * @return array
1739 */
1740 public static function ucsplit($string)
1741 {
1742 return preg_split('/(?=\p{Lu})/u', $string, -1, PREG_SPLIT_NO_EMPTY);
1743 }
1744
1745 /**
1746 * Count the number of words in a UTF-8 string.
1747 *
1748 * @param string $string
1749 * @return int
1750 */
1751 public static function wordCount(string $string)
1752 {
1753 $words = preg_split('/[^\p{L}\p{N}]+/u', $string, -1, PREG_SPLIT_NO_EMPTY);
1754
1755 return count($words);
1756 }
1757
1758 /**
1759 * Wrap a string to a given number of characters.
1760 *
1761 * @param string $string
1762 * @param int $characters
1763 * @param string $break
1764 * @param bool $cutLongWords
1765 * @return string
1766 */
1767 public static function wordWrap(
1768 $string,
1769 $characters = 75,
1770 $break = "\n",
1771 $cutLongWords = false
1772 )
1773 {
1774 return wordwrap($string, $characters, $break, $cutLongWords);
1775 }
1776
1777 /**
1778 * Wrap the string with the given strings.
1779 *
1780 * @param string $value
1781 * @param string $before
1782 * @param string|null $after
1783 * @return string
1784 */
1785 public static function wrap($value, $before, $after = null)
1786 {
1787 return $before.$value.($after = $after ?: $before);
1788 }
1789
1790 /**
1791 * Generate a UUID (version 4).
1792 *
1793 * @return string
1794 */
1795 public static function uuid()
1796 {
1797 return wp_generate_uuid4();
1798 }
1799
1800 /**
1801 * Increment a counter.
1802 * @return int
1803 */
1804 public static function incrementCounter($prefix = '')
1805 {
1806 return wp_unique_id($prefix);
1807 }
1808
1809 /**
1810 * Generate a deterministic unique id by a prefix.
1811 *
1812 * @param string $slug
1813 * @return string
1814 */
1815 public static function incrementBySlug($slug = '')
1816 {
1817 return wp_unique_prefixed_id(
1818 $slug = $slug ?: App::config()->get('app.slug')
1819 );
1820 }
1821
1822 /**
1823 * Remove all strings from the casing caches.
1824 *
1825 * @return void
1826 */
1827 public static function flushCache()
1828 {
1829 static::$snakeCache = [];
1830 static::$camelCache = [];
1831 static::$studlyCache = [];
1832 }
1833 }
1834