friendship ended with social-app. php is my new best friend
1<?php
2/**
3 * Class String
4 *
5 * @created 29.10.2024
6 * @author smiley <smiley@chillerlan.net>
7 * @copyright 2024 smiley
8 * @license MIT
9 */
10declare(strict_types=1);
11
12namespace chillerlan\Utilities;
13
14use RuntimeException;
15use function array_filter;
16use function array_map;
17use function array_values;
18use function is_string;
19use function json_decode;
20use function json_encode;
21use function mb_strtolower;
22use function sodium_bin2base64;
23use function str_contains;
24use function str_replace;
25use function str_starts_with;
26use const JSON_PRETTY_PRINT;
27use const JSON_THROW_ON_ERROR;
28use const JSON_UNESCAPED_SLASHES;
29use const JSON_UNESCAPED_UNICODE;
30use const SODIUM_BASE64_VARIANT_ORIGINAL;
31
32/**
33 * string handling helpers
34 */
35final class Str{
36
37 public const JSON_ENCODE_FLAGS_DEFAULT = (JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
38
39 /**
40 * Filters an array and removes all elements that are not strings. Array keys are *not* retained.
41 *
42 * @see array_filter()
43 * @see array_values()
44 * @see is_string()
45 *
46 * @param array<string|int, mixed> $mixed
47 * @return string[]
48 */
49 public static function filter(array $mixed):array{
50 return array_filter(array_values($mixed), is_string(...));
51 }
52
53 /**
54 * Converts the strings in an array to uppercase
55 *
56 * @see mb_strtoupper()
57 * @see self::filter()
58 *
59 * @param string[] $strings
60 * @return string[]
61 *
62 * @codeCoverageIgnore
63 */
64 public static function toUpper(array $strings):array{
65 return array_map(mb_strtoupper(...), self::filter($strings));
66 }
67
68 /**
69 * Converts the strings in an array to lowercase
70 *
71 * @see mb_strtolower()
72 * @see self::filter()
73 *
74 * @param string[] $strings
75 * @return string[]
76 *
77 * @codeCoverageIgnore
78 */
79 public static function toLower(array $strings):array{
80 return array_map(mb_strtolower(...), self::filter($strings));
81 }
82
83 /**
84 * Checks whether the given string starts with *any* of the given array of needles.
85 *
86 * @see \str_starts_with()
87 *
88 * @param string[] $needles
89 */
90 public static function startsWith(string $haystack, array $needles, bool $ignoreCase = false):bool{
91 $needles = self::filter($needles);
92
93 if($needles === []){
94 return true;
95 }
96
97 if($ignoreCase){
98 $haystack = mb_strtolower($haystack);
99 $needles = array_map(mb_strtolower(...), $needles);
100 }
101
102 foreach($needles as $needle){
103 if($needle !== '' && str_starts_with($haystack, $needle)){
104 return true;
105 }
106 }
107
108 return false;
109 }
110
111 /**
112 * Checks whether the given string (haystack) contains *all* of the given array of needles.
113 * The given array is filtered for string values.
114 *
115 * @see \str_contains()
116 *
117 * @param string[] $needles
118 */
119 public static function containsAll(string $haystack, array $needles, bool $ignoreCase = false):bool{
120 $needles = self::filter($needles);
121
122 if($needles === []){
123 return true;
124 }
125
126 if($ignoreCase){
127 $haystack = mb_strtolower($haystack);
128 $needles = array_map(mb_strtolower(...), $needles);
129 }
130
131 foreach($needles as $needle){
132 if($needle !== '' && !str_contains($haystack, $needle)){
133 return false;
134 }
135 }
136
137 return true;
138 }
139
140 /**
141 * Checks whether the given string (haystack) contains *any* of the given array of needles.
142 * The given array is filtered for string values.
143 *
144 * @param string[] $needles
145 */
146 public static function containsAny(string $haystack, array $needles, bool $ignoreCase = false):bool{
147 $needles = self::filter($needles);
148
149 if($needles === []){
150 return true;
151 }
152
153 if($ignoreCase){
154 $haystack = mb_strtolower($haystack);
155 $needles = array_map(mb_strtolower(...), $needles);
156 }
157
158 return str_replace($needles, '', $haystack) !== $haystack;
159 }
160
161 /**
162 * Decodes a JSON string
163 *
164 * @throws \JsonException
165 * @codeCoverageIgnore
166 */
167 public static function jsonDecode(string $json, bool $associative = false, int $flags = 0):mixed{
168 $flags |= JSON_THROW_ON_ERROR;
169
170 return json_decode(json: $json, associative: $associative, flags: $flags);
171 }
172
173 /**
174 * Encodes a value into a JSON representation
175 *
176 * @throws \JsonException
177 * @codeCoverageIgnore
178 */
179 public static function jsonEncode(mixed $data, int $flags = self::JSON_ENCODE_FLAGS_DEFAULT):string{
180 $flags |= JSON_THROW_ON_ERROR;
181
182 $encoded = json_encode($data, $flags);
183
184 // the chance to run into this is near zero but hey, at least phpstan is happy
185 if($encoded === false){
186 throw new RuntimeException('json_encode() error'); // @codeCoverageIgnore
187 }
188
189 return $encoded;
190 }
191
192 /**
193 * Encodes a binary string to base64 (timing-safe)
194 *
195 * @see sodium_bin2base64()
196 *
197 * @throws \SodiumException
198 * @codeCoverageIgnore
199 */
200 public static function base64encode(string $string, int $variant = SODIUM_BASE64_VARIANT_ORIGINAL):string{
201 return sodium_bin2base64($string, $variant);
202 }
203
204 /**
205 * Decodes a base64 string into binary (timing-safe)
206 *
207 * @see sodium_base642bin()
208 *
209 * @throws \SodiumException
210 * @codeCoverageIgnore
211 */
212 public static function base64decode(
213 string $base64,
214 int $variant = SODIUM_BASE64_VARIANT_ORIGINAL,
215 string $ignore = '',
216 ):string{
217 return sodium_base642bin($base64, $variant, $ignore);
218 }
219
220}