├── tests ├── gbk.txt ├── bootstrap.php ├── StrTest.php ├── ValidatorTest.php └── AryTest.php ├── .travis.yml ├── src ├── Exceptions │ ├── AryKeyTypeException.php │ ├── StrEncodingException.php │ ├── ValidatorNotFoundException.php │ └── AryOutOfRangeException.php ├── Validator.php ├── Str.php └── Ary.php ├── composer.json ├── phpunit.xml ├── LICENSE └── README.md /tests/gbk.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zanemmm/utils/HEAD/tests/gbk.txt -------------------------------------------------------------------------------- /tests/bootstrap.php: -------------------------------------------------------------------------------- 1 | message = "The type of key must be string or integer, at {$this->file}, line: {$this->line}"; 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Exceptions/StrEncodingException.php: -------------------------------------------------------------------------------- 1 | message = "The string must be in UTF-8 format encoding, at {$this->file}, line: {$this->line}"; 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Exceptions/ValidatorNotFoundException.php: -------------------------------------------------------------------------------- 1 | message = "Custom validator not found, at {$this->file}, line: {$this->line}"; 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Exceptions/AryOutOfRangeException.php: -------------------------------------------------------------------------------- 1 | message = "Index of Ary is out of range, at {$this->file}, line: {$this->line}"; 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "zane/utils", 3 | "type": "library", 4 | "keywords": [ 5 | "array", 6 | "string", 7 | "validator", 8 | "utils" 9 | ], 10 | "description": "A collection of useful classes like Ary, Str and Validator", 11 | "require": { 12 | "php": "7.*", 13 | "ext-json": "*", 14 | "ext-mbstring": "*", 15 | "ext-ctype": "*" 16 | }, 17 | "require-dev": { 18 | "phpunit/phpunit": "^6.5" 19 | }, 20 | "license": "MIT", 21 | "authors": [ 22 | { 23 | "name": "zane", 24 | "email": "pi@0php.net", 25 | "homepage": "https://www.0php.net" 26 | } 27 | ], 28 | "autoload": { 29 | "psr-4": { 30 | "Zane\\Utils\\": "src/" 31 | } 32 | }, 33 | "minimum-stability": "dev", 34 | "prefer-stable": true 35 | } 36 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 11 | 12 | 13 | tests 14 | 15 | 16 | 17 | 18 | src 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Zane 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 关于本项目 2 | 3 | [![GitHub license](https://img.shields.io/github/license/zanemmm/utils.svg)](https://github.com/zanemmm/utils/blob/master/LICENSE) [![Build Status](https://www.travis-ci.org/zanemmm/utils.svg?branch=master)](https://www.travis-ci.org/zanemmm/utils) [![StyleCI](https://github.styleci.io/repos/142729124/shield?branch=master)](https://github.styleci.io/repos/142729124) 4 | 5 | 开发这个项目的目的是为了将日常开发中经常能用到的方法封装在一起,节省开发时间。同时在开发本项目的过程中熟悉PHP的自带函数的应用,提高自己的水平。 6 | 7 | 另外在开发过程中还写了一篇关于 [PHP 数组函数的使用技巧](https://www.0php.net/posts/%E5%B7%A7%E7%94%A8-PHP-%E6%95%B0%E7%BB%84%E5%87%BD%E6%95%B0.html) 的博文。 8 | 9 | # 项目特性 10 | 11 | 我觉得最大的特点就是: :sparkles:**链式调用**:sparkles: 。嗯:wink:,其他有待各位发掘。 12 | 13 | 举个栗子: 14 | 15 | ``` php 16 | countValues()->maxKey(); // red 29 | ``` 30 | 31 | 是不是方便很多:bangbang:其实项目里很多方法都只是PHP自带函数的简单封装,但是通过链式调用在可读性和可维护性上真的是完爆使用自带函数:laughing:。 32 | 33 | # 安装 34 | 35 | ``` 36 | composer require zane/utils dev-master 37 | ``` 38 | 39 | # 项目进度 40 | 41 | - [x] Ary 数组类 2018.7.29 42 | - [x] Str 字符串类 2018.8.1 43 | - [x] Validator 验证器类 2018.8.3 44 | 45 | # 贡献代码 46 | 47 | 代码风格采用 PSR2 标准 48 | 49 | 测试覆盖率 >= 90% 50 | 51 | 欢迎各位小伙伴提 issue 和 pr,立个 :triangular_flag_on_post: 一定要把这个项目坚持下去! 52 | 53 | # 感谢 54 | 55 | 部分函数的实现参考以下项目 56 | 57 | [Laravel/Framework](https://github.com/laravel/framework) 58 | 59 | [JBZoo/Utils](https://github.com/JBZoo/Utils) 60 | 61 | # 开源协议 62 | 63 | MIT -------------------------------------------------------------------------------- /tests/StrTest.php: -------------------------------------------------------------------------------- 1 | assertEquals('PHP 是世界上最好的语言', $str); 20 | 21 | $str = Str::new($gbk, 'GBK'); 22 | $this->assertEquals('PHP 是世界上最好的语言', $str->str()); 23 | 24 | // throw StrEncodingException 25 | Str::new($gbk); 26 | } 27 | 28 | /** 29 | * @throws \Zane\Utils\Exceptions\StrEncodingException 30 | */ 31 | public function testStr() 32 | { 33 | $str = Str::new('PHP 是世界上最好的语言'); 34 | $this->assertEquals('PHP 是世界上最好的语言', $str->str()); 35 | } 36 | 37 | /** 38 | * @expectedException \Zane\Utils\Exceptions\StrEncodingException 39 | * 40 | * @throws \Zane\Utils\Exceptions\StrEncodingException 41 | */ 42 | public function testSet() 43 | { 44 | $str = Str::new('PHP 是世界上最好的语言'); 45 | $str->set('你好世界'); 46 | $this->assertEquals('你好世界', $str->str()); 47 | 48 | $str2 = Str::new('世界你好'); 49 | $this->assertEquals('世界你好', $str->set($str2)->str()); 50 | 51 | $gbk = file_get_contents(__DIR__.DIRECTORY_SEPARATOR.'gbk.txt'); 52 | $str2->set($gbk); 53 | } 54 | 55 | /** 56 | * @throws \Zane\Utils\Exceptions\StrEncodingException 57 | */ 58 | public function testToUpperCase() 59 | { 60 | $str = Str::new('hello world'); 61 | $this->assertEquals('HELLO WORLD', $str->toUpperCase()->str()); 62 | } 63 | 64 | /** 65 | * @throws \Zane\Utils\Exceptions\StrEncodingException 66 | */ 67 | public function testToLowerCase() 68 | { 69 | $str = Str::new('HELLO WORLD'); 70 | $this->assertEquals('hello world', $str->toLowerCase()->str()); 71 | } 72 | 73 | /** 74 | * @throws \Zane\Utils\Exceptions\StrEncodingException 75 | */ 76 | public function testToTitleCase() 77 | { 78 | $str = Str::new('hello world'); 79 | $this->assertEquals('Hello World', $str->toTitleCase()->str()); 80 | } 81 | 82 | /** 83 | * @throws \Zane\Utils\Exceptions\StrEncodingException 84 | */ 85 | public function testExplode() 86 | { 87 | $str = Str::new('第一|第二|第三'); 88 | $ary = $str->explode('|'); 89 | $this->assertEquals(Ary::new(['第一', '第二', '第三']), $ary); 90 | 91 | $ary = $str->explode('|', 2); 92 | $this->assertEquals(Ary::new(['第一', '第二|第三']), $ary); 93 | 94 | $ary = $str->explode(Str::new('第二')); 95 | $this->assertEquals(Ary::new(['第一|', '|第三']), $ary); 96 | } 97 | 98 | /** 99 | * @throws \Zane\Utils\Exceptions\StrEncodingException 100 | */ 101 | public function testSplit() 102 | { 103 | $str = Str::new('你好66世界99好你'); 104 | $ary = $str->split('\d{2,}'); 105 | $this->assertEquals(['你好', '世界', '好你'], $ary->val()); 106 | 107 | $ary = $str->split('\d{2,}', 2); 108 | $this->assertEquals(['你好', '世界99好你'], $ary->val()); 109 | } 110 | 111 | /** 112 | * @throws \Zane\Utils\Exceptions\StrEncodingException 113 | */ 114 | public function testSubstring() 115 | { 116 | $str = Str::new('PHP 是世界上最好的语言'); 117 | $this->assertEquals('世界上最好的语言', $str->substring(5)); 118 | } 119 | 120 | /** 121 | * @throws \Zane\Utils\Exceptions\StrEncodingException 122 | */ 123 | public function testSubstringCount() 124 | { 125 | $str = Str::new('四十是四十,十四是十四'); 126 | $this->assertEquals(2, $str->substringCount('十四')); 127 | 128 | $needle = Str::new('四十'); 129 | $this->assertEquals(2, $str->substringCount($needle)); 130 | 131 | $this->assertEquals(0, $str->substringCount('不存在')); 132 | } 133 | 134 | /** 135 | * @throws \Zane\Utils\Exceptions\StrEncodingException 136 | */ 137 | public function testTruncate() 138 | { 139 | $str = Str::new('四十是十四,十四是四十'); 140 | $this->assertEquals('四十是十四', $str->truncate(5)); 141 | $this->assertEquals('四十是十四......', $str->truncate(5, '......')); 142 | $this->assertEquals($str, $str->truncate(11)); 143 | $this->assertEquals($str, $str->truncate(12, '......')); 144 | } 145 | 146 | /** 147 | * @throws \Zane\Utils\Exceptions\StrEncodingException 148 | */ 149 | public function testPosition() 150 | { 151 | $str = Str::new('最好的语言PHP言语的好最PHP'); 152 | $this->assertEquals(5, $str->position('PHP', 0, true, false)); 153 | $this->assertEquals(13, $str->position('PHP', 0, true, true)); 154 | $this->assertEquals(5, $str->position('php', 0, false, false)); 155 | $this->assertEquals(13, $str->position('php', 0, false, true)); 156 | 157 | $this->assertFalse($str->position('php', 0, true, false)); 158 | $this->assertEquals(5, $str->position('PHP', 5, true, false)); 159 | } 160 | 161 | /** 162 | * @throws StrEncodingException 163 | */ 164 | public function testSearch() 165 | { 166 | $str = Str::new('最好的语言PHP言语的好最PHP'); 167 | $this->assertEquals('最好的语言', $str->search('PHP', true, true, false)); 168 | $this->assertEquals('最好的语言', $str->search('php', true, false, false)); 169 | $this->assertEquals('最好的语言PHP言语的好最', $str->search('PHP', true, true, true)); 170 | $this->assertEquals('最好的语言PHP言语的好最', $str->search('php', true, false, true)); 171 | 172 | $this->assertEquals('PHP言语的好最PHP', $str->search('PHP', false, true, false)); 173 | $this->assertEquals('PHP言语的好最PHP', $str->search('php', false, false, false)); 174 | $this->assertEquals('PHP', $str->search('PHP', false, true, true)); 175 | $this->assertEquals('PHP', $str->search('php', false, false, true)); 176 | 177 | $this->assertFalse($str->search('php', true, true, false)); 178 | } 179 | 180 | /** 181 | * @throws StrEncodingException 182 | */ 183 | public function testBefore() 184 | { 185 | $str = Str::new('pi@0php.net'); 186 | $this->assertEquals('pi', $str->before('@', false)); 187 | $this->assertEquals('pi@', $str->before('@', true)); 188 | $this->assertEquals(Str::new('pi'), $str->before('@', false)); 189 | $this->assertEquals(Str::new('pi@'), $str->before('@', true)); 190 | 191 | $this->assertFalse($str->before('null', false)); 192 | $this->assertFalse($str->before('null', true)); 193 | } 194 | 195 | /** 196 | * @throws StrEncodingException 197 | */ 198 | public function testAfter() 199 | { 200 | $str = Str::new('pi@0php.net'); 201 | 202 | $this->assertEquals('0php.net', $str->after('@', false)); 203 | $this->assertEquals('@0php.net', $str->after('@', true)); 204 | $this->assertEquals(Str::new('0php.net'), $str->after('@', false)); 205 | $this->assertEquals(Str::new('@0php.net'), $str->after('@', true)); 206 | 207 | $this->assertFalse($str->after('null', false)); 208 | $this->assertFalse($str->after('null', true)); 209 | } 210 | 211 | /** 212 | * @throws StrEncodingException 213 | */ 214 | public function testRepeat() 215 | { 216 | $str = Str::new('再来一瓶'); 217 | 218 | $this->assertEquals('再来一瓶再来一瓶', $str->repeat(2)); 219 | $this->assertEquals('再来一瓶|再来一瓶', $str->repeat(2, '|')); 220 | $this->assertEquals('再来一瓶', $str->repeat(1, '|')); 221 | $this->assertEquals('', $str->repeat(0)); 222 | $this->assertEquals('', $str->repeat(0, '|')); 223 | } 224 | 225 | /** 226 | * @throws StrEncodingException 227 | */ 228 | public function testReplace() 229 | { 230 | $str = Str::new('java是世界上最好的语言'); 231 | 232 | $this->assertEquals('php是世界上最好的语言', $str->replace('java', 'php')); 233 | $this->assertEquals('php是世界上最好的语言', $str->replace('JAVA', 'php', false)); 234 | } 235 | 236 | /** 237 | * @throws StrEncodingException 238 | */ 239 | public function testToBase64() 240 | { 241 | $str = Str::new('你好世界'); 242 | $base64 = base64_encode($str->str()); 243 | 244 | $this->assertEquals($base64, $str->toBase64()->str()); 245 | } 246 | 247 | /** 248 | * @throws StrEncodingException 249 | */ 250 | public function testToMd5() 251 | { 252 | $str = Str::new('你好世界'); 253 | $md5 = md5($str->str()); 254 | 255 | $this->assertEquals($md5, $str->toMd5()); 256 | } 257 | 258 | /** 259 | * @throws StrEncodingException 260 | */ 261 | public function testSha1() 262 | { 263 | $str = Str::new('你好世界'); 264 | $md5 = sha1($str->str()); 265 | 266 | $this->assertEquals($md5, $str->toSha1()); 267 | } 268 | 269 | /** 270 | * @throws StrEncodingException 271 | */ 272 | public function testPasswordHash() 273 | { 274 | $str = Str::new('你好世界'); 275 | 276 | $this->assertTrue(password_verify('你好世界', $str->passwordHash())); 277 | } 278 | 279 | /** 280 | * @throws StrEncodingException 281 | */ 282 | public function testTrim() 283 | { 284 | $str = Str::new(' 你好世界 '); 285 | $this->assertEquals('你好世界', $str->trim()); 286 | 287 | $str = Str::new('#你好世界#'); 288 | $this->assertEquals('你好世界', $str->trim('#')); 289 | } 290 | 291 | /** 292 | * @throws StrEncodingException 293 | */ 294 | public function testLtrim() 295 | { 296 | $str = Str::new(' 你好世界 '); 297 | $this->assertEquals('你好世界 ', $str->ltrim()); 298 | 299 | $str = Str::new('#你好世界#'); 300 | $this->assertEquals('你好世界#', $str->ltrim('#')); 301 | } 302 | 303 | /** 304 | * @throws StrEncodingException 305 | */ 306 | public function testRtrim() 307 | { 308 | $str = Str::new(' 你好世界 '); 309 | $this->assertEquals(' 你好世界', $str->rtrim()); 310 | 311 | $str = Str::new('#你好世界#'); 312 | $this->assertEquals('#你好世界', $str->rtrim('#')); 313 | } 314 | 315 | /** 316 | * @throws StrEncodingException 317 | */ 318 | public function testComp() 319 | { 320 | $str1 = Str::new('star utils'); 321 | $str2 = Str::new('star php'); 322 | 323 | $this->assertTrue($str1->comp($str2) > 0); 324 | $this->assertTrue($str1->comp($str2, true, 5) === 0); 325 | 326 | $str3 = Str::new('STAR php'); 327 | $this->assertTrue($str1->comp($str3, false, 5) === 0); 328 | $this->assertTrue($str2->comp($str3, false) === 0); 329 | } 330 | 331 | /** 332 | * @throws StrEncodingException 333 | */ 334 | public function testNatComp() 335 | { 336 | $str1 = Str::new('img2'); 337 | $str2 = Str::new('img12'); 338 | $this->assertTrue($str1->natComp($str2) < 0); 339 | 340 | $str3 = Str::new('IMG13'); 341 | $this->assertTrue($str3->natComp($str2, false) > 0); 342 | } 343 | 344 | /** 345 | * @throws StrEncodingException 346 | */ 347 | public function testEquals() 348 | { 349 | $str = Str::new('hello world'); 350 | $this->assertTrue($str->equals('hello world')); 351 | $this->assertFalse($str->equals('HELLO WORLD')); 352 | } 353 | 354 | /** 355 | * @throws StrEncodingException 356 | */ 357 | public function testReverse() 358 | { 359 | $str = Str::new('十四是十四'); 360 | $this->assertEquals('四十是四十', $str->reverse()); 361 | } 362 | 363 | /** 364 | * @throws StrEncodingException 365 | */ 366 | public function testToArray() 367 | { 368 | $str = Str::new('十四是十四'); 369 | $this->assertEquals(['十', '四', '是', '十', '四'], $str->toArray()); 370 | } 371 | 372 | /** 373 | * @throws StrEncodingException 374 | */ 375 | public function testToAry() 376 | { 377 | $str = Str::new('十四是十四'); 378 | $ary = Ary::new(['十', '四', '是', '十', '四']); 379 | $this->assertEquals($ary, $str->toAry()); 380 | } 381 | 382 | /** 383 | * @throws StrEncodingException 384 | */ 385 | public function testLen() 386 | { 387 | $str = Str::new('十四是十四'); 388 | $this->assertEquals(5, $str->len()); 389 | } 390 | 391 | /** 392 | * @throws StrEncodingException 393 | */ 394 | public function testCount() 395 | { 396 | $str = Str::new('十四是十四'); 397 | $this->assertEquals(5, $str->count()); 398 | $this->assertEquals(5, count($str)); 399 | } 400 | 401 | /** 402 | * @throws StrEncodingException 403 | */ 404 | public function testSetDefault() 405 | { 406 | $str = Str::new('十四是十四'); 407 | Str::setDefault(['toMd5RawOutput' => true]); 408 | $md5 = md5($str->str(), true); 409 | $this->assertEquals($md5, $str->toMd5()); 410 | } 411 | } 412 | -------------------------------------------------------------------------------- /src/Validator.php: -------------------------------------------------------------------------------- 1 | ['max_range' => $max]]; 133 | $int = filter_var($input, FILTER_VALIDATE_INT, $options); 134 | 135 | return $int !== false; 136 | } 137 | 138 | /** 139 | * 验证字符串为 int 类型并且大于等于 $min,注意:整型的范围 140 | * 数字左右的空白符,如:空格,Tab 不会影响验证 141 | * 142 | * @param string $input 143 | * @param int $min 144 | * 145 | * @return bool 146 | */ 147 | public static function intMin(string $input, int $min): bool 148 | { 149 | $options = ['options' => ['min_range' => $min]]; 150 | $int = filter_var($input, FILTER_VALIDATE_INT, $options); 151 | 152 | return $int !== false; 153 | } 154 | 155 | /** 156 | * 验证字符串为 int 类型,大于等于 $min,小于等于 $max,注意:整型的范围 157 | * 数字左右的空白符,如:空格,Tab 不会影响验证 158 | * 159 | * @param string $input 160 | * @param int $min 161 | * @param int $max 162 | * 163 | * @return bool 164 | */ 165 | public static function intBetween(string $input, int $min, int $max): bool 166 | { 167 | $options = [ 168 | 'options' => [ 169 | 'min_range' => $min, 170 | 'max_range' => $max, 171 | ], 172 | ]; 173 | $int = filter_var($input, FILTER_VALIDATE_INT, $options); 174 | 175 | return $int !== false; 176 | } 177 | 178 | /** 179 | * 验证字符串为 float 类型,注意:浮点型的范围 180 | * float 方法兼容 int 方法 181 | * 数字左右的空白符,如:空格,Tab 不会影响验证 182 | * 183 | * @param string $input 184 | * 185 | * @return bool 186 | */ 187 | public static function float(string $input): bool 188 | { 189 | $float = filter_var($input, FILTER_VALIDATE_FLOAT); 190 | 191 | return $float !== false; 192 | } 193 | 194 | /** 195 | * 验证字符串为 float 类型并且小于等于 $max,注意:浮点型的范围 196 | * 数字左右的空白符,如:空格,Tab 不会影响验证 197 | * 198 | * @param string $input 199 | * @param float $max 200 | * 201 | * @return bool 202 | */ 203 | public static function floatMax(string $input, float $max): bool 204 | { 205 | $float = filter_var($input, FILTER_VALIDATE_FLOAT); 206 | 207 | return $float !== false && $float <= $max; 208 | } 209 | 210 | /** 211 | * 验证字符串为 float 类型并且大于等于 $min,注意:浮点型的范围 212 | * 数字左右的空白符,如:空格,Tab 不会影响验证 213 | * 214 | * @param string $input 215 | * @param float $min 216 | * 217 | * @return bool 218 | */ 219 | public static function floatMin(string $input, float $min): bool 220 | { 221 | $float = filter_var($input, FILTER_VALIDATE_FLOAT); 222 | 223 | return $float !== false && $float >= $min; 224 | } 225 | 226 | /** 227 | * 验证字符串为 float 类型,大于等于 $min,小于等于 $max,注意:浮点型的范围 228 | * 数字左右的空白符,如:空格,Tab 不会影响验证 229 | * 230 | * @param string $input 231 | * @param float $min 232 | * @param float $max 233 | * 234 | * @return bool 235 | */ 236 | public static function floatBetween(string $input, float $min, float $max): bool 237 | { 238 | $float = filter_var($input, FILTER_VALIDATE_FLOAT); 239 | 240 | return $float !== false && $float >= $min && $float <= $max; 241 | } 242 | 243 | /** 244 | * 验证字符串为 json 格式,注意: json 格式嵌套深度不能超过 512. 245 | * 246 | * @see http://php.net/manual/zh/function.json-decode.php 247 | * 248 | * @param string $input 249 | * 250 | * @return bool 251 | */ 252 | public static function json(string $input): bool 253 | { 254 | json_decode($input, true, 512); 255 | 256 | return json_last_error() === JSON_ERROR_NONE; 257 | } 258 | 259 | /** 260 | * 验证字符串为合法 IP,IPv4 和 IPv6 都可以. 261 | * 262 | * @param string $input 263 | * 264 | * @return bool 265 | */ 266 | public static function ip(string $input): bool 267 | { 268 | $ip = filter_var($input, FILTER_VALIDATE_IP); 269 | 270 | return $ip !== false; 271 | } 272 | 273 | /** 274 | * 验证字符串为合法 IPv4 地址 275 | * 276 | * @param string $input 277 | * 278 | * @return bool 279 | */ 280 | public static function ipv4(string $input): bool 281 | { 282 | $ip = filter_var($input, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4); 283 | 284 | return $ip !== false; 285 | } 286 | 287 | /** 288 | * 验证字符串为合法 IPv6 地址 289 | * 290 | * @param string $input 291 | * 292 | * @return bool 293 | */ 294 | public static function ipv6(string $input): bool 295 | { 296 | $ip = filter_var($input, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6); 297 | 298 | return $ip !== false; 299 | } 300 | 301 | /** 302 | * 验证字符串为域名(域名最后不能加 . 号),不支持中文域名. 303 | * 304 | * @param string $input 305 | * 306 | * @return bool 307 | */ 308 | public static function domain(string $input) 309 | { 310 | if (preg_match( 311 | '/^([a-zA-Z0-9][-a-zA-Z0-9]{0,61}[a-zA-Z0-9]|[a-zA-Z0-9])' 312 | ."(.([a-zA-Z0-9][-a-zA-Z0-9]{0,61}[a-zA-Z0-9]|[a-zA-Z0-9]))*\.[a-zA-Z]{2,62}$/", 313 | $input 314 | )) { 315 | return true; 316 | } 317 | 318 | return false; 319 | } 320 | 321 | /** 322 | * 验证域名合法且能通过 DNS 解析 323 | * 注:该方法会进行 DNS 解析,需要消耗网络资源. 324 | * 325 | * @param string $input 326 | * 327 | * @return bool 328 | */ 329 | public static function activeDomain(string $input): bool 330 | { 331 | if (static::domain($input)) { 332 | return checkdnsrr($input, 'ANY'); 333 | } 334 | 335 | return false; 336 | } 337 | 338 | /** 339 | * 验证字符串为合法 URL,默认允许任何协议. 340 | * 341 | * @param string $input 342 | * @param string|string[] $schemes 协议名称如: http、https,可传递允许协议名称数组 343 | * 344 | * @return bool 345 | */ 346 | public static function url(string $input, $schemes = null): bool 347 | { 348 | $url = filter_var($input, FILTER_VALIDATE_URL); 349 | if ($url === false) { 350 | return false; 351 | } 352 | 353 | if (!is_null($schemes)) { 354 | if (!is_array($schemes)) { 355 | $schemes = [$schemes]; 356 | } 357 | foreach ($schemes as $scheme) { 358 | $scheme .= '://'; 359 | $pos = strpos($input, $scheme); 360 | // 找到对应协议 361 | if ($pos === 0) { 362 | return true; 363 | } 364 | } 365 | // 没有找到对应协议 366 | return false; 367 | } 368 | 369 | return true; 370 | } 371 | 372 | /** 373 | * 验证字符串为邮箱,不支持支持中文域名和中文用户名. 374 | * 375 | * @param string $input 376 | * 377 | * @return bool 378 | */ 379 | public static function email(string $input): bool 380 | { 381 | $email = filter_var($input, FILTER_VALIDATE_EMAIL); 382 | 383 | return $email !== false; 384 | } 385 | 386 | /** 387 | * 验证字符串为国内手机号. 388 | * 389 | * @param string $input 390 | * 391 | * @return bool 392 | */ 393 | public static function phone(string $input): bool 394 | { 395 | if (preg_match("/^1[34578]{1}\d{9}$/", $input)) { 396 | return true; 397 | } 398 | 399 | return false; 400 | } 401 | 402 | /** 403 | * 验证国内身份证号码 404 | * 405 | * @param string $input 406 | * 407 | * @return bool 408 | */ 409 | public static function IDCard(string $input): bool 410 | { 411 | if (strlen($input) !== 18 || !static::num(substr($input, 0, 17))) { 412 | return false; 413 | } 414 | 415 | // 加权因子 416 | $factor = [7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2]; 417 | // 校验码对应值 418 | $code = ['1', '0', 'X', '9', '8', '7', '6', '5', '4', '3', '2']; 419 | // 校验和 420 | $checksum = 0; 421 | for ($i = 0; $i < 17; $i++) { 422 | $checksum += substr($input, $i, 1) * $factor[$i]; 423 | } 424 | // 最后身份证最后一位数字 425 | $last = $code[$checksum % 11]; 426 | 427 | return $input[17] == $last; 428 | } 429 | 430 | /** 431 | * 验证国内身份证号码且年龄小于等于 $maxAge. 432 | * 433 | * @param string $input 434 | * @param int $maxAge 435 | * 436 | * @return bool 437 | */ 438 | public static function IDCardMaxAge(string $input, int $maxAge): bool 439 | { 440 | if (static::IDCard($input)) { 441 | $year = date('Y') - substr($input, 6, 4); 442 | $monthDay = date('md') - substr($input, 10, 4); 443 | 444 | return $year < $maxAge || ($year == $maxAge && $monthDay <= 0); 445 | } 446 | 447 | return false; 448 | } 449 | 450 | /** 451 | * 验证国内身份证号码且年龄大于等于 $minAge. 452 | * 453 | * @param string $input 454 | * @param int $minAge 455 | * 456 | * @return bool 457 | */ 458 | public static function IDCardMinAge(string $input, int $minAge): bool 459 | { 460 | if (static::IDCard($input)) { 461 | $year = date('Y') - substr($input, 6, 4); 462 | $monthDay = date('md') - substr($input, 10, 4); 463 | 464 | return $year > $minAge || ($year == $minAge && $monthDay >= 0); 465 | } 466 | 467 | return false; 468 | } 469 | 470 | /** 471 | * 验证国内身份证号码且年龄在 $minAge 和 $maxAge 之间. 472 | * 473 | * @param string $input 474 | * @param int $minAge 475 | * @param int $maxAge 476 | * 477 | * @return bool 478 | */ 479 | public static function IDCardBetween(string $input, int $minAge, int $maxAge) 480 | { 481 | if (static::IDCard($input)) { 482 | $year = date('Y') - substr($input, 6, 4); 483 | $monthDay = date('md') - substr($input, 10, 4); 484 | $minAgeOk = $year > $minAge || ($year == $minAge && $monthDay >= 0); 485 | $maxAgeOk = $year < $maxAge || ($year == $maxAge && $monthDay <= 0); 486 | 487 | return $minAgeOk && $maxAgeOk; 488 | } 489 | 490 | return false; 491 | } 492 | 493 | /** 494 | * 设置一个自定义的验证器. 495 | * 496 | * @param $key 497 | * @param callable $fn 498 | */ 499 | public static function set($key, callable $fn) 500 | { 501 | static::$validators[$key] = $fn; 502 | } 503 | 504 | /** 505 | * 获取指定验证器. 506 | * 507 | * @param string $key 508 | * 509 | * @throws ValidatorNotFoundException 510 | * 511 | * @return callable 512 | */ 513 | public static function get(string $key): callable 514 | { 515 | // 若 $key 与当前类中方法相同,则构造一个该方法的闭包并返回 516 | if (method_exists(__CLASS__, $key)) { 517 | return function (...$arguments) use ($key) { 518 | return static::{$key}(...$arguments); 519 | }; 520 | } 521 | 522 | if (!isset(static::$validators[$key])) { 523 | throw new ValidatorNotFoundException(); 524 | } 525 | 526 | return static::$validators[$key]; 527 | } 528 | 529 | /** 530 | * 调用自定义验证器,不存在时会抛出异常. 531 | * 532 | * @param string $key 533 | * @param array $arguments 534 | * 535 | * @throws ValidatorNotFoundException 536 | * 537 | * @return mixed 538 | */ 539 | public static function __callStatic(string $key, array $arguments) 540 | { 541 | if (!isset(static::$validators[$key])) { 542 | throw new ValidatorNotFoundException(); 543 | } 544 | 545 | return (static::$validators[$key])(...$arguments); 546 | } 547 | } 548 | -------------------------------------------------------------------------------- /tests/ValidatorTest.php: -------------------------------------------------------------------------------- 1 | assertTrue(Validator::accepted('yes')); 13 | $this->assertTrue(Validator::accepted('Yes')); 14 | $this->assertTrue(Validator::accepted('yEs')); 15 | $this->assertTrue(Validator::accepted('true')); 16 | $this->assertTrue(Validator::accepted('on')); 17 | $this->assertTrue(Validator::accepted('1')); 18 | $this->assertTrue(Validator::accepted(' yes ')); 19 | $this->assertFalse(Validator::accepted(' y e s ')); 20 | $this->assertFalse(Validator::accepted('off')); 21 | $this->assertFalse(Validator::accepted('0')); 22 | $this->assertFalse(Validator::accepted('other')); 23 | } 24 | 25 | public function testBoolean() 26 | { 27 | $this->assertTrue(Validator::boolean('yes')); 28 | $this->assertTrue(Validator::boolean('Yes')); 29 | $this->assertTrue(Validator::boolean('yEs')); 30 | $this->assertTrue(Validator::boolean('true')); 31 | $this->assertTrue(Validator::boolean('on')); 32 | $this->assertTrue(Validator::boolean('1')); 33 | $this->assertTrue(Validator::boolean(' yes ')); 34 | $this->assertTrue(Validator::boolean('off')); 35 | $this->assertTrue(Validator::boolean('no')); 36 | $this->assertTrue(Validator::boolean('false')); 37 | $this->assertTrue(Validator::boolean('0')); 38 | $this->assertFalse(Validator::boolean(' y e s ')); 39 | $this->assertFalse(Validator::boolean('other')); 40 | } 41 | 42 | public function testAlpha() 43 | { 44 | $this->assertTrue(Validator::alpha('test')); 45 | $this->assertTrue(Validator::alpha('TEST')); 46 | $this->assertFalse(Validator::alpha(' test ')); 47 | $this->assertFalse(Validator::alpha('test1')); 48 | $this->assertFalse(Validator::alpha('测试test')); 49 | $this->assertFalse(Validator::alpha('\test\\')); 50 | } 51 | 52 | public function testAlphaNum() 53 | { 54 | $this->assertTrue(Validator::alphaNum('test')); 55 | $this->assertTrue(Validator::alphaNum('TEST')); 56 | $this->assertTrue(Validator::alphaNum('test1')); 57 | $this->assertTrue(Validator::alphaNum('1test1')); 58 | $this->assertFalse(Validator::alphaNum(' test ')); 59 | $this->assertFalse(Validator::alphaNum('测试test')); 60 | $this->assertFalse(Validator::alphaNum('\test\\')); 61 | } 62 | 63 | public function testNum() 64 | { 65 | $this->assertTrue(Validator::num('123')); 66 | $this->assertTrue(Validator::num('01239716526350713274912465110000')); 67 | $this->assertFalse(Validator::num('9527.0')); 68 | $this->assertFalse(Validator::num('-12')); 69 | $this->assertFalse(Validator::num(' 12 ')); 70 | $this->assertFalse(Validator::num('1ABC')); 71 | 72 | $this->assertTrue(Validator::num('123', 3)); 73 | $this->assertTrue(Validator::num('0123', 4)); 74 | $this->assertFalse(Validator::num('1234', 3)); 75 | } 76 | 77 | public function testNumeric() 78 | { 79 | $this->assertTrue(Validator::numeric('123')); 80 | $this->assertTrue(Validator::numeric('-123')); 81 | $this->assertTrue(Validator::numeric('00000')); 82 | $this->assertTrue(Validator::numeric('999999999999999999999999999999999999')); 83 | $this->assertTrue(Validator::numeric('09527.0')); 84 | $this->assertFalse(Validator::numeric('1.0.2.3.4')); 85 | $this->assertFalse(Validator::numeric('1ABC')); 86 | $this->assertFalse(Validator::numeric(' 123 ')); 87 | } 88 | 89 | public function testInt() 90 | { 91 | $this->assertTrue(Validator::int('123')); 92 | $this->assertTrue(Validator::int('-123')); 93 | $this->assertTrue(Validator::int(' 123 ')); 94 | $this->assertFalse(Validator::int('0123')); 95 | $this->assertFalse(Validator::int('123-')); 96 | $this->assertFalse(Validator::int('123.1')); 97 | // 整数溢出 98 | $this->assertFalse(Validator::int('9223372036854775808')); 99 | $this->assertFalse(Validator::int('other')); 100 | } 101 | 102 | /** 103 | * @depends testInt 104 | */ 105 | public function testIntMax() 106 | { 107 | $this->assertTrue(Validator::intMax('123', 124)); 108 | $this->assertTrue(Validator::intMax('123', 123)); 109 | $this->assertTrue(Validator::intMax('-123', 123)); 110 | $this->assertFalse(Validator::intMax('123', -123)); 111 | $this->assertFalse(Validator::intMax('123', 122)); 112 | } 113 | 114 | /** 115 | * @depends testInt 116 | */ 117 | public function testIntMin() 118 | { 119 | $this->assertTrue(Validator::intMin('124', 123)); 120 | $this->assertTrue(Validator::intMin('123', 123)); 121 | $this->assertFalse(Validator::intMin('122', 123)); 122 | $this->assertFalse(Validator::intMin('-122', 123)); 123 | } 124 | 125 | /** 126 | * @depends testIntMax 127 | * @depends testIntMin 128 | */ 129 | public function testIntBetween() 130 | { 131 | $this->assertTrue(Validator::intBetween('123', 123, 123)); 132 | $this->assertTrue(Validator::intBetween('123', 122, 124)); 133 | $this->assertFalse(Validator::intBetween('123', 124, 122)); 134 | $this->assertFalse(Validator::intBetween('256', 124, 255)); 135 | $this->assertFalse(Validator::intBetween('123', 124, 255)); 136 | } 137 | 138 | public function testFloat() 139 | { 140 | $this->assertTrue(Validator::float('123')); 141 | $this->assertTrue(Validator::float('-123')); 142 | $this->assertTrue(Validator::float(' 123 ')); 143 | $this->assertTrue(Validator::float('123.0')); 144 | $this->assertTrue(Validator::float('0123')); 145 | $this->assertFalse(Validator::float('123-')); 146 | $this->assertFalse(Validator::float('other')); 147 | } 148 | 149 | /** 150 | * @depends testFloat 151 | */ 152 | public function testFloatMax() 153 | { 154 | $this->assertTrue(Validator::floatMax('123.5', 123.5)); 155 | $this->assertTrue(Validator::floatMax('123', 123)); 156 | $this->assertFalse(Validator::floatMax('124', 123.9)); 157 | } 158 | 159 | /** 160 | * @depends testFloat 161 | */ 162 | public function testFloatMin() 163 | { 164 | $this->assertTrue(Validator::floatMin('123.5', 123.5)); 165 | $this->assertTrue(Validator::floatMin('123', 123)); 166 | $this->assertFalse(Validator::floatMin('122', 123.9)); 167 | } 168 | 169 | /** 170 | * @depends testFloatMax 171 | * @depends testFloatMin 172 | */ 173 | public function testFloatBetween() 174 | { 175 | $this->assertTrue(Validator::floatBetween('123', 123.0, 123.0)); 176 | $this->assertTrue(Validator::floatBetween('123.0', 122.9, 123.1)); 177 | $this->assertFalse(Validator::floatBetween('123', 123, 122)); 178 | $this->assertFalse(Validator::floatBetween('255.1', 124, 255)); 179 | $this->assertFalse(Validator::intBetween('123.9', 124, 255)); 180 | } 181 | 182 | public function testJson() 183 | { 184 | $json = <<<'EOT' 185 | { 186 | "name": "zane", 187 | "email": "pi@0php.net" 188 | } 189 | EOT; 190 | $this->assertTrue(Validator::json($json)); 191 | $this->assertFalse(Validator::json('other')); 192 | } 193 | 194 | public function testIp() 195 | { 196 | $this->assertTrue(Validator::ip('127.0.0.1')); 197 | $this->assertTrue(Validator::ip('192.168.1.1')); 198 | $this->assertTrue(Validator::ip('1030::C9B4:FF12:48AA:1A2B')); 199 | $this->assertTrue(Validator::ip('2001:DB8:2de::e13')); 200 | $this->assertFalse(Validator::ip('192.168.1.256')); 201 | $this->assertFalse(Validator::ip('G000:0:0:0:0:0:0:1')); 202 | } 203 | 204 | public function testIpv4() 205 | { 206 | $this->assertTrue(Validator::ipv4('127.0.0.1')); 207 | $this->assertTrue(Validator::ipv4('192.168.1.1')); 208 | $this->assertFalse(Validator::ipv4('1030::C9B4:FF12:48AA:1A2B')); 209 | $this->assertFalse(Validator::ipv4('2001:DB8:2de::e13')); 210 | } 211 | 212 | public function testIpv6() 213 | { 214 | $this->assertFalse(Validator::ipv6('127.0.0.1')); 215 | $this->assertFalse(Validator::ipv6('192.168.1.1')); 216 | $this->assertTrue(Validator::ipv6('1030::C9B4:FF12:48AA:1A2B')); 217 | $this->assertTrue(Validator::ipv6('2001:DB8:2de::e13')); 218 | } 219 | 220 | public function testDomain() 221 | { 222 | $this->assertTrue(Validator::domain('www.bai-du.com')); 223 | $this->assertTrue(Validator::domain('google.com')); 224 | $this->assertTrue(Validator::domain('google.com.cn')); 225 | $this->assertTrue(Validator::domain('test.google.com.cn')); 226 | $this->assertTrue(Validator::domain('test.test.google.com.cn')); 227 | $this->assertFalse(Validator::domain('我爱你.中国')); 228 | $this->assertFalse(Validator::domain('.google.com.cn')); 229 | $this->assertFalse(Validator::domain('google.com.cn.')); 230 | $this->assertFalse(Validator::domain('-google.com')); 231 | $this->assertFalse(Validator::domain('google-.com')); 232 | $this->assertFalse(Validator::domain('www.-google.com')); 233 | $this->assertFalse(Validator::domain('google.com.cn1')); 234 | $this->assertFalse(Validator::domain('google.com.cn.1')); 235 | $this->assertFalse(Validator::domain('127.0.0.1')); 236 | } 237 | 238 | /** 239 | * @depends testDomain 240 | */ 241 | public function testActiveDomain() 242 | { 243 | $this->assertTrue(Validator::activeDomain('google.com')); 244 | $this->assertFalse(Validator::activeDomain('IGuessNoOneRegisterThisDomain.com')); 245 | $this->assertFalse(Validator::activeDomain('127.0.0.1')); 246 | } 247 | 248 | public function testUrl() 249 | { 250 | $this->assertTrue(Validator::url('http://www.google.com')); 251 | $this->assertTrue(Validator::url('ftp://www.google.com')); 252 | $this->assertTrue(Validator::url('http://www.google.com/')); 253 | $this->assertTrue(Validator::url('http://www.google.com/index.php')); 254 | $this->assertTrue(Validator::url('http://www.google.com/index.php?hello=world')); 255 | $this->assertTrue(Validator::url('http://www.google.com/index.php?hello=world#x')); 256 | $this->assertFalse(Validator::url('www.google.com')); 257 | $this->assertFalse(Validator::url('www.google.com/index.php')); 258 | 259 | $this->assertTrue(Validator::url('http://www.google.com', 'http')); 260 | $this->assertTrue(Validator::url('https://www.google.com', ['http', 'https'])); 261 | $this->assertTrue(Validator::url('http://www.google.com/index.php?hello=world#x', ['http', 'https'])); 262 | $this->assertTrue(Validator::url('ftp://www.google.com', 'ftp')); 263 | $this->assertFalse(Validator::url('https://www.google.com', 'http')); 264 | } 265 | 266 | public function testEmail() 267 | { 268 | $this->assertTrue(Validator::email('pi@0php.net')); 269 | $this->assertFalse(Validator::email('php.net')); 270 | } 271 | 272 | public function testPhone() 273 | { 274 | $this->assertTrue(Validator::phone('13012345678')); 275 | $this->assertFalse(Validator::phone('1301234567')); 276 | $this->assertFalse(Validator::phone('130123456789')); 277 | $this->assertFalse(Validator::phone('16012345678')); 278 | $this->assertFalse(Validator::phone('03012345678')); 279 | $this->assertFalse(Validator::phone('19012345678')); 280 | } 281 | 282 | public function testIDCard() 283 | { 284 | $this->assertTrue(Validator::IDCard('110101200001011953')); 285 | $this->assertTrue(Validator::IDCard('11010120000101103X')); 286 | $this->assertFalse(Validator::IDCard('110101200001011952')); 287 | $this->assertFalse(Validator::IDCard('11010120000101195')); 288 | $this->assertFalse(Validator::IDCard('X11010120000101195')); 289 | } 290 | 291 | /** 292 | * @depends testIDCard 293 | */ 294 | public function testIDCardMaxAge() 295 | { 296 | $this->assertTrue(Validator::IDCardMaxAge('110101200001011953', 999)); 297 | $this->assertFalse(Validator::IDCardMaxAge('110101200001011954', 999)); 298 | $this->assertFalse(Validator::IDCardMaxAge('11010120000101195', 999)); 299 | $this->assertFalse(Validator::IDCardMaxAge('110101200001011953', 17)); 300 | } 301 | 302 | /** 303 | * @depends testIDCard 304 | */ 305 | public function testIDCardMinAge() 306 | { 307 | $this->assertTrue(Validator::IDCardMinAge('110101200001011953', 17)); 308 | $this->assertFalse(Validator::IDCardMinAge('110101200001011954', 17)); 309 | $this->assertFalse(Validator::IDCardMinAge('11010120000101195', 17)); 310 | $this->assertFalse(Validator::IDCardMinAge('110101200001011953', 999)); 311 | } 312 | 313 | public function testIDCardBetween() 314 | { 315 | $this->assertTrue(Validator::IDCardBetween('110101200001011953', 17, 999)); 316 | $this->assertFalse(Validator::IDCardBetween('110101200001011954', 17, 999)); 317 | $this->assertFalse(Validator::IDCardBetween('11010120000101195', 17, 999)); 318 | $this->assertFalse(Validator::IDCardBetween('11010120000101195', 9, 10)); 319 | $this->assertFalse(Validator::IDCardBetween('11010120000101195', 99, 999)); 320 | $this->assertFalse(Validator::IDCardBetween('11010120000101195', 999, 17)); 321 | } 322 | 323 | /** 324 | * @expectedException \Zane\Utils\Exceptions\ValidatorNotFoundException 325 | */ 326 | public function testSet() 327 | { 328 | $isA = function ($val) { 329 | if ($val === 'A') { 330 | return true; 331 | } 332 | 333 | return false; 334 | }; 335 | 336 | Validator::set('isA', $isA); 337 | $this->assertTrue(Validator::isA('A')); 338 | $this->assertFalse(Validator::isA('B')); 339 | // throw ValidatorNotFoundException 340 | Validator::isB('B'); 341 | } 342 | 343 | /** 344 | * @expectedException \Zane\Utils\Exceptions\ValidatorNotFoundException 345 | */ 346 | public function testGet() 347 | { 348 | $isA = Validator::get('isA'); 349 | $this->assertTrue($isA('A')); 350 | $this->assertFalse($isA('B')); 351 | 352 | $int = Validator::get('int'); 353 | $this->assertTrue($int('123')); 354 | $this->assertFalse($int('ABC')); 355 | // throw ValidatorNotFoundException 356 | Validator::get('isB'); 357 | } 358 | } 359 | -------------------------------------------------------------------------------- /src/Str.php: -------------------------------------------------------------------------------- 1 | true, 21 | 'positionReverse' => false, 22 | 'searchBefore' => false, 23 | 'searchCaseSensitive' => true, 24 | 'searchReverse' => false, 25 | 'beforeContain' => false, 26 | 'afterContain' => false, 27 | 'replaceCaseSensitive' => true, 28 | 'toMd5RawOutput' => false, 29 | 'toSha1RawOutput' => false, 30 | 'passwordHashAlgorithm' => PASSWORD_DEFAULT, 31 | 'passwordHashCost' => 10, 32 | 'compCaseSensitive' => true, 33 | 'natCompCaseSensitive' => true, 34 | ]; 35 | 36 | private $str; 37 | 38 | /** 39 | * Str constructor. 40 | * 41 | * @param string $str 42 | * @param string|null $fromEncoding 43 | * 44 | * @throws StrEncodingException 45 | */ 46 | public function __construct(string $str, string $fromEncoding = null) 47 | { 48 | if (!is_null($fromEncoding)) { 49 | $str = static::convert($str, $fromEncoding); 50 | } elseif (!mb_check_encoding($str, 'UTF-8')) { 51 | throw new StrEncodingException(); 52 | } 53 | 54 | $this->str = $str; 55 | } 56 | 57 | /** 58 | * 返回实例中的字符串. 59 | * 60 | * @return string 61 | */ 62 | public function str(): string 63 | { 64 | return $this->str; 65 | } 66 | 67 | /** 68 | * 设置实例中的字符串. 69 | * 70 | * @param string|Str $str 设置的字符串必须以 UTF-8 编码 71 | * 72 | * @throws StrEncodingException 73 | * 74 | * @return $this 原实例 75 | */ 76 | public function set($str) 77 | { 78 | $str = static::getStr($str); 79 | if (!mb_check_encoding($str, 'UTF-8')) { 80 | throw new StrEncodingException(); 81 | } 82 | $this->str = $str; 83 | 84 | return $this; 85 | } 86 | 87 | /** 88 | * 字符串转大写. 89 | * 90 | * @see http://php.net/manual/zh/function.mb-convert-case.php 91 | * 92 | * @throws StrEncodingException 93 | * 94 | * @return Str 新实例 95 | */ 96 | public function toUpperCase(): self 97 | { 98 | $str = mb_convert_case($this->str, MB_CASE_UPPER, 'UTF-8'); 99 | 100 | return static::new($str); 101 | } 102 | 103 | /** 104 | * 字符串转小写. 105 | * 106 | * @see http://php.net/manual/zh/function.mb-convert-case.php 107 | * 108 | * @throws StrEncodingException 109 | * 110 | * @return Str 新实例 111 | */ 112 | public function toLowerCase(): self 113 | { 114 | $str = mb_convert_case($this->str, MB_CASE_LOWER, 'UTF-8'); 115 | 116 | return static::new($str); 117 | } 118 | 119 | /** 120 | * 字符串单词首字母转大写. 121 | * 122 | * @see http://php.net/manual/zh/function.mb-convert-case.php 123 | * 124 | * @throws StrEncodingException 125 | * 126 | * @return Str 新实例 127 | */ 128 | public function toTitleCase(): self 129 | { 130 | $str = mb_convert_case($this->str, MB_CASE_TITLE, 'UTF-8'); 131 | 132 | return static::new($str); 133 | } 134 | 135 | /** 136 | * 使用一个字符串分割另一个字符串. 137 | * 138 | * @see http://php.net/manual/zh/function.explode.php 139 | * 140 | * @param string|Str $delimiter 边界上的分隔字符 141 | * @param int|null $limit 最多分割为 limit 个元素 142 | * 143 | * @return Ary 144 | */ 145 | public function explode($delimiter, int $limit = null): Ary 146 | { 147 | $delimiter = static::getStr($delimiter); 148 | 149 | if (func_num_args() === 1) { 150 | $array = explode($delimiter, $this->str); 151 | } else { 152 | $array = explode($delimiter, $this->str, $limit); 153 | } 154 | 155 | return Ary::new($array); 156 | } 157 | 158 | /** 159 | * 使用正则表达式分割多字节字符串. 160 | * 161 | * @see http://php.net/manual/zh/function.mb-split.php 162 | * 163 | * @param string|Str $pattern 正则表达式 164 | * @param int|null $limit 最多分割为 limit 个元素 165 | * 166 | * @return Ary 167 | */ 168 | public function split($pattern, int $limit = null): Ary 169 | { 170 | $pattern = static::getStr($pattern); 171 | 172 | if (func_num_args() === 1) { 173 | $array = mb_split($pattern, $this->str); 174 | } else { 175 | $array = mb_split($pattern, $this->str, $limit); 176 | } 177 | 178 | return Ary::new($array); 179 | } 180 | 181 | /** 182 | * 获取部分字符串. 183 | * 184 | * @see http://php.net/manual/zh/function.mb-substr.php 185 | * 186 | * @param int $start 开始字符数 187 | * @param int|null $len 子串长度 188 | * 189 | * @throws StrEncodingException 190 | * 191 | * @return Str 新实例 192 | */ 193 | public function substring(int $start, int $len = null): self 194 | { 195 | $str = mb_substr($this->str, $start, $len, 'UTF-8'); 196 | 197 | return static::new($str); 198 | } 199 | 200 | /** 201 | * 子字符串出现次数. 202 | * 203 | * @see http://php.net/manual/zh/function.mb-substr-count.php 204 | * 205 | * @param string|Str $needle 子字符串 206 | * 207 | * @return int 208 | */ 209 | public function substringCount($needle): int 210 | { 211 | $needle = static::getStr($needle); 212 | 213 | return mb_substr_count($this->str, $needle, 'UTF-8'); 214 | } 215 | 216 | /** 217 | * 以指定长度截断字符串. 218 | * 219 | * @param int $len 指定长度 220 | * @param string|Str|null $marker 截断字符串后连接的字符串,比如可以为:“……” 221 | * 222 | * @throws StrEncodingException 223 | * 224 | * @return Str 新实例 225 | */ 226 | public function truncate(int $len, $marker = null): self 227 | { 228 | if ($len >= mb_strlen($this->str)) { 229 | return static::new($this->str); 230 | } 231 | 232 | $str = mb_substr($this->str, 0, $len, 'UTF-8').static::getStr($marker); 233 | 234 | return static::new($str); 235 | } 236 | 237 | /** 238 | * 查找字符串在另一个字符串中出现的位置. 239 | * 240 | * @see http://php.net/manual/zh/function.mb-strpos.php 241 | * @see http://php.net/manual/zh/function.mb-stripos.php 242 | * @see http://php.net/manual/zh/function.mb-strrpos.php 243 | * @see http://php.net/manual/zh/function.mb-strripos.php 244 | * 245 | * @param string|Str $needle 要查找的字符串 246 | * @param int $offset 开始查找偏移量 247 | * @param bool|null $caseSensitive 大小写敏感 248 | * @param bool|null $reverse true 为首次出现的位置,false为最后一次出现的位置 249 | * 250 | * @return false|int 251 | */ 252 | public function position($needle, int $offset = 0, bool $caseSensitive = null, bool $reverse = null) 253 | { 254 | $needle = static::getStr($needle); 255 | 256 | $position = false; 257 | $status = static::default($caseSensitive, 'positionCaseSensitive') << 1 258 | | static::default($reverse, 'positionReverse'); 259 | switch ($status) { 260 | // $caseSensitive = false, $reverse = false 261 | case 0b00: 262 | $position = mb_stripos($this->str, $needle, $offset, 'UTF-8'); 263 | break; 264 | // $caseSensitive = false, $reverse = true 265 | case 0b01: 266 | $position = mb_strripos($this->str, $needle, $offset, 'UTF-8'); 267 | break; 268 | // $caseSensitive = true, $reverse = false 269 | case 0b10: 270 | $position = mb_strpos($this->str, $needle, $offset, 'UTF-8'); 271 | break; 272 | // $caseSensitive = true, $reverse = true 273 | case 0b11: 274 | $position = mb_strrpos($this->str, $needle, $offset, 'UTF-8'); 275 | break; 276 | } 277 | 278 | return $position; 279 | } 280 | 281 | /** 282 | * 查找并返回子串. 283 | * 284 | * @see http://php.net/manual/zh/function.mb-strstr.php 285 | * @see http://php.net/manual/zh/function.mb-stristr.php 286 | * @see http://php.net/manual/zh/function.mb-strrchr.php 287 | * @see http://php.net/manual/zh/function.mb-strrichr.php 288 | * 289 | * @param string|Str $needle 要查找的字符串 290 | * @param bool|null $before 返回 $needle 之前的字符串 291 | * @param bool|null $caseSensitive 大小写敏感 292 | * @param bool|null $reverse true 为首次出现的位置,false为最后一次出现的位置 293 | * 294 | * @throws StrEncodingException 295 | * 296 | * @return Str|false 新实例或 false 297 | */ 298 | public function search($needle, bool $before = null, bool $caseSensitive = null, bool $reverse = null) 299 | { 300 | $needle = static::getStr($needle); 301 | $before = static::default($before, 'searchBefore'); 302 | 303 | $str = false; 304 | $status = static::default($caseSensitive, 'searchCaseSensitive') << 1 305 | | static::default($reverse, 'searchReverse'); 306 | switch ($status) { 307 | // $caseSensitive = false, $reverse = false 308 | case 0b00: 309 | $str = mb_stristr($this->str, $needle, $before, 'UTF-8'); 310 | break; 311 | // $caseSensitive = false, $reverse = true 312 | case 0b01: 313 | $str = mb_strrichr($this->str, $needle, $before, 'UTF-8'); 314 | break; 315 | // $caseSensitive = true, $reverse = false 316 | case 0b10: 317 | $str = mb_strstr($this->str, $needle, $before, 'UTF-8'); 318 | break; 319 | // $caseSensitive = true, $reverse = true 320 | case 0b11: 321 | $str = mb_strrchr($this->str, $needle, $before, 'UTF-8'); 322 | break; 323 | } 324 | 325 | if ($str === false) { 326 | return false; 327 | } 328 | 329 | return static::new($str); 330 | } 331 | 332 | /** 333 | * 返回子字符串之前的字符串. 334 | * 335 | * @param string|Str $needle 指定的字符串 336 | * @param bool|null $contain 包含指定的字符串 337 | * 338 | * @throws StrEncodingException 339 | * 340 | * @return Str|false 新实例或 false 341 | */ 342 | public function before($needle, bool $contain = null) 343 | { 344 | $needle = static::getStr($needle); 345 | 346 | if (static::default($contain, 'beforeContain')) { 347 | $pos = mb_strpos($this->str, $needle, 0, 'UTF-8'); 348 | if ($pos === false) { 349 | return false; 350 | } 351 | $str = mb_substr($this->str, 0, $pos + mb_strlen($needle), 'UTF-8'); 352 | 353 | return static::new($str); 354 | } 355 | 356 | $str = mb_strstr($this->str, $needle, true, 'UTF-8'); 357 | if ($str === false) { 358 | return false; 359 | } 360 | 361 | return static::new($str); 362 | } 363 | 364 | /** 365 | * 返回子字符串之后的字符串. 366 | * 367 | * @param string|Str $needle 指定的字符串 368 | * @param bool|null $contain 包含指定的字符串 369 | * 370 | * @throws StrEncodingException 371 | * 372 | * @return Str|false 新实例或 false 373 | */ 374 | public function after($needle, bool $contain = null) 375 | { 376 | $needle = static::getStr($needle); 377 | 378 | $str = mb_strstr($this->str, $needle, false, 'UTF-8'); 379 | if ($str === false) { 380 | return false; 381 | } 382 | 383 | if (!static::default($contain, 'afterContain')) { 384 | $str = mb_substr($str, mb_strlen($needle), null, 'UTF-8'); 385 | } 386 | 387 | return static::new($str); 388 | } 389 | 390 | /** 391 | * 重复一个字符串. 392 | * 393 | * @see http://php.net/manual/zh/function.str-repeat.php 394 | * 395 | * @param int $num 重复次数 396 | * @param string|Str $separator 分隔符 397 | * 398 | * @throws StrEncodingException 399 | * 400 | * @return Str 新实例 401 | */ 402 | public function repeat(int $num, $separator = null): self 403 | { 404 | if (is_null($separator)) { 405 | $str = str_repeat($this->str, $num); 406 | } else { 407 | $separator = static::getStr($separator); 408 | $str = implode($separator, array_fill(0, $num, $this->str)); 409 | } 410 | 411 | return static::new($str); 412 | } 413 | 414 | /** 415 | * 子字符串替换. 416 | * 417 | * @see http://php.net/manual/zh/function.str-replace.php 418 | * @see http://php.net/manual/zh/function.str-ireplace.php 419 | * 420 | * @param string|Str $search 421 | * @param string|Str $replace 422 | * @param bool|null $caseSensitive 423 | * 424 | * @throws StrEncodingException 425 | * 426 | * @return Str 新实例 427 | */ 428 | public function replace($search, $replace, bool $caseSensitive = null): self 429 | { 430 | $search = static::getStr($search); 431 | $replace = static::getStr($replace); 432 | 433 | if (static::default($caseSensitive, 'replaceCaseSensitive')) { 434 | $str = str_replace($search, $replace, $this->str); 435 | } else { 436 | $str = str_ireplace($search, $replace, $this->str); 437 | } 438 | 439 | return static::new($str); 440 | } 441 | 442 | /** 443 | * 转为 base64 编码 444 | * 445 | * @see http://php.net/manual/zh/function.base64-encode.php 446 | * 447 | * @throws StrEncodingException 448 | * 449 | * @return Str 新实例 450 | */ 451 | public function toBase64(): self 452 | { 453 | $str = base64_encode($this->str); 454 | 455 | return static::new($str); 456 | } 457 | 458 | /** 459 | * 返回字符串的 MD5 散列值 460 | * 461 | * @see http://php.net/manual/zh/function.md5.php 462 | * 463 | * @param bool|null $rawOutput 464 | * 465 | * @return string 466 | */ 467 | public function toMd5(bool $rawOutput = null): string 468 | { 469 | $str = md5($this->str, static::default($rawOutput, 'toMd5RawOutput')); 470 | 471 | return $str; 472 | } 473 | 474 | /** 475 | * 返回字符串的 sha1 散列值 476 | * 477 | * @see http://php.net/manual/zh/function.sha1.php 478 | * 479 | * @param bool|null $rawOutput 480 | * 481 | * @return string 482 | */ 483 | public function toSha1(bool $rawOutput = null): string 484 | { 485 | $str = sha1($this->str, static::default($rawOutput, 'toSha1RawOutput')); 486 | 487 | return $str; 488 | } 489 | 490 | /** 491 | * 创建密码的散列. 492 | * 493 | * @see http://php.net/manual/zh/function.password-hash.php 494 | * 495 | * @param int|null $algorithm 496 | * @param int|null $cost 497 | * 498 | * @throws StrEncodingException 499 | * 500 | * @return Str 501 | */ 502 | public function passwordHash(int $algorithm = null, int $cost = null) 503 | { 504 | $algorithm = static::default($algorithm, 'passwordHashAlgorithm'); 505 | $cost = static::default($cost, 'passwordHashCost'); 506 | 507 | $hash = password_hash($this->str, $algorithm, ['cost' => $cost]); 508 | 509 | return static::new($hash); 510 | } 511 | 512 | /** 513 | * 去除字符串首尾处的空白字符(或者其他字符). 514 | * 515 | * @see http://php.net/manual/zh/function.trim.php 516 | * 517 | * @param string|Str|null $characterMask 518 | * 519 | * @throws StrEncodingException 520 | * 521 | * @return Str 新实例 522 | */ 523 | public function trim($characterMask = null): self 524 | { 525 | if (is_null($characterMask)) { 526 | return static::new(trim($this->str)); 527 | } 528 | 529 | $characterMask = static::getStr($characterMask); 530 | 531 | return static::new(trim($this->str, $characterMask)); 532 | } 533 | 534 | /** 535 | * 删除字符串开头的空白字符(或其他字符). 536 | * 537 | * @see http://php.net/manual/zh/function.ltrim.php 538 | * 539 | * @param string|Str|null $characterMask 540 | * 541 | * @throws StrEncodingException 542 | * 543 | * @return Str 新实例 544 | */ 545 | public function ltrim($characterMask = null): self 546 | { 547 | if (is_null($characterMask)) { 548 | return static::new(ltrim($this->str)); 549 | } 550 | 551 | $characterMask = static::getStr($characterMask); 552 | 553 | return static::new(ltrim($this->str, $characterMask)); 554 | } 555 | 556 | /** 557 | * 删除字符串开头的空白字符(或其他字符). 558 | * 559 | * @see http://php.net/manual/zh/function.rtrim.php 560 | * 561 | * @param string|Str|null $characterMask 562 | * 563 | * @throws StrEncodingException 564 | * 565 | * @return Str 新实例 566 | */ 567 | public function rtrim($characterMask = null): self 568 | { 569 | if (is_null($characterMask)) { 570 | return static::new(rtrim($this->str)); 571 | } 572 | 573 | $characterMask = static::getStr($characterMask); 574 | 575 | return static::new(rtrim($this->str, $characterMask)); 576 | } 577 | 578 | /** 579 | * 比较字符串. 580 | * 581 | * @see http://php.net/manual/zh/function.strcmp.php 582 | * @see http://php.net/manual/zh/function.strcasecmp.php 583 | * @see http://php.net/manual/zh/function.strncmp.php 584 | * @see http://php.net/manual/zh/function.strncasecmp.php 585 | * 586 | * @param string|Str $compStr 用于比较的字符串 587 | * @param bool|null $caseSensitive 大小写敏感 588 | * @param int|null $num 比较字节数,空则为全部,注:比较的是字节数而不是字符数 589 | * 590 | * @return int|false 参数有误则返回 false 591 | */ 592 | public function comp($compStr, bool $caseSensitive = null, int $num = null): int 593 | { 594 | $compStr = static::getStr($compStr); 595 | $status = static::default($caseSensitive, 'compCaseSensitive') << 1 | !is_null($num); 596 | $diff = false; 597 | switch ($status) { 598 | // $caseSensitive = false, $num = null 599 | case 0b00: 600 | $diff = strcasecmp($this->str, $compStr); 601 | break; 602 | // $caseSensitive = false, $num != null 603 | case 0b01: 604 | $diff = strncasecmp($this->str, $compStr, $num); 605 | break; 606 | // $caseSensitive = true, $num = null 607 | case 0b10: 608 | $diff = strcmp($this->str, $compStr); 609 | break; 610 | // $caseSensitive = true, $num != null 611 | case 0b11: 612 | $diff = strncmp($this->str, $compStr, $num); 613 | break; 614 | } 615 | 616 | return $diff; 617 | } 618 | 619 | /** 620 | * 使用自然排序算法比较字符串. 621 | * 622 | * @see http://php.net/manual/zh/function.strnatcmp.php 623 | * @see http://php.net/manual/zh/function.strnatcasecmp.php 624 | * 625 | * @param string|Str $compStr 用于比较的字符串 626 | * @param bool|null $caseSensitive 大小写敏感 627 | * 628 | * @return int 629 | */ 630 | public function natComp($compStr, bool $caseSensitive = null): int 631 | { 632 | if (static::default($caseSensitive, 'natCompCaseSensitive')) { 633 | return strnatcmp($this->str, $compStr); 634 | } 635 | 636 | return strnatcasecmp($this->str, $compStr); 637 | } 638 | 639 | /** 640 | * 判断字符串是否相等. 641 | * 642 | * @param string|Str $str 643 | * 644 | * @return bool 645 | */ 646 | public function equals($str) 647 | { 648 | $str = static::getStr($str); 649 | 650 | return $this->str === $str; 651 | } 652 | 653 | /** 654 | * 反转字符串. 655 | * 656 | * @throws StrEncodingException 657 | * 658 | * @return Str 新实例 659 | */ 660 | public function reverse(): self 661 | { 662 | $reverseStr = ''; 663 | for ($i = mb_strlen($this->str); $i >= 0; $i--) { 664 | $reverseStr .= mb_substr($this->str, $i, 1); 665 | } 666 | 667 | return static::new($reverseStr); 668 | } 669 | 670 | /** 671 | * 字符串转为字符数组. 672 | * 673 | * @return array 674 | */ 675 | public function toArray(): array 676 | { 677 | $str = $this->str; 678 | $len = mb_strlen($str); 679 | $array = []; 680 | 681 | while ($len) { 682 | $array[] = mb_substr($str, 0, 1, 'UTF-8'); 683 | $str = mb_substr($str, 1, $len, 'UTF-8'); 684 | $len = mb_strlen($str); 685 | } 686 | 687 | return $array; 688 | } 689 | 690 | /** 691 | * 字符串转为 Ary 数组. 692 | * 693 | * @return Ary 694 | */ 695 | public function toAry(): Ary 696 | { 697 | return Ary::new($this->toArray()); 698 | } 699 | 700 | /** 701 | * 返回字符串长度. 702 | * 703 | * @return int 704 | */ 705 | public function len(): int 706 | { 707 | return mb_strlen($this->str); 708 | } 709 | 710 | /** 711 | * 返回字符串长度. 712 | * 713 | * @return int 714 | */ 715 | public function count(): int 716 | { 717 | return mb_strlen($this->str); 718 | } 719 | 720 | /** 721 | * @return string 722 | */ 723 | public function __toString(): string 724 | { 725 | return $this->str; 726 | } 727 | 728 | /** 729 | * 返回一个新实例. 730 | * 731 | * @param string $str 实例包含的字符串 732 | * @param string|null $fromEncoding 字符串的原编码 733 | * 734 | * @throws StrEncodingException 735 | * 736 | * @return Str 新实例 737 | */ 738 | public static function new(string $str, string $fromEncoding = null): self 739 | { 740 | return new static($str, $fromEncoding); 741 | } 742 | 743 | /** 744 | * 将字符串转为 UTF-8 编码 745 | * 746 | * @param string $str 原字符串 747 | * @param string $fromEncoding 原编码格式 748 | * 749 | * @return string UTF-8 编码的字符串 750 | */ 751 | public static function convert(string $str, string $fromEncoding): string 752 | { 753 | return mb_convert_encoding($str, 'UTF-8', $fromEncoding); 754 | } 755 | 756 | /** 757 | * @param $str 758 | * 759 | * @return string 760 | */ 761 | protected static function getStr($str): string 762 | { 763 | if (is_string($str)) { 764 | return $str; 765 | } elseif ($str instanceof static) { 766 | return $str->str(); 767 | } 768 | 769 | return (string) $str; 770 | } 771 | 772 | /** 773 | * 设置类方法的默认值 774 | * 775 | * @param array $default 776 | * 777 | * @return bool 778 | */ 779 | public static function setDefault(array $default): bool 780 | { 781 | foreach ($default as $key => $val) { 782 | static::$default[$key] = $val; 783 | } 784 | 785 | return true; 786 | } 787 | 788 | /** 789 | * 若 $val 不为 null 则返回 $val 790 | * 若 $val 为 null 则直接返回 $default 数组中以 $key 为键名的值 791 | * 792 | * @param mixed $val 793 | * @param string|null $key 794 | * 795 | * @return mixed|null 796 | */ 797 | protected static function default($val, string $key = null) 798 | { 799 | if (is_null($val)) { 800 | return static::$default[$key] ?? null; 801 | } 802 | 803 | return $val; 804 | } 805 | } 806 | -------------------------------------------------------------------------------- /tests/AryTest.php: -------------------------------------------------------------------------------- 1 | assertEmpty($empty->val()); 15 | 16 | $array = [1, 2, 3]; 17 | $ary = Ary::new($array); 18 | 19 | $this->assertEquals($array, $ary->val()); 20 | } 21 | 22 | public function testVal() 23 | { 24 | $arrayA = [1, 2, 3, '4']; 25 | $ary = Ary::new($arrayA); 26 | 27 | $this->assertEquals($arrayA, $ary->val()); 28 | 29 | $arrayB = [1, 2, 3, 4]; 30 | $ary->val($arrayB); 31 | 32 | $this->assertEquals($arrayB, $ary->val()); 33 | } 34 | 35 | public function testAccessible() 36 | { 37 | $this->assertTrue(Ary::accessible([])); 38 | $this->assertTrue(Ary::accessible(Ary::new([]))); 39 | $this->assertFalse(Ary::accessible(null)); 40 | } 41 | 42 | /** 43 | * @depends testVal 44 | */ 45 | public function testGet() 46 | { 47 | $array = ['products.desk' => ['price' => 100]]; 48 | $this->assertEquals(['price' => 100], Ary::new($array)->get('products.desk')); 49 | 50 | $array = ['products' =>['desk' => ['price' => 100]]]; 51 | $this->assertEquals(['price' => 100], Ary::new($array)->get('products.desk')); 52 | $this->assertEquals(100, Ary::new($array)->get('products.desk.price')); 53 | 54 | $array = ['foo' => null, 'bar' => ['baz' => null]]; 55 | $this->assertNull(Ary::new($array)->get('foo', 'default')); 56 | $this->assertNull(Ary::new($array)->get('bar.baz', 'default')); 57 | 58 | $ary = Ary::new(['products' => Ary::new(['desk' => Ary::new(['price' => 100])])]); 59 | $this->assertEquals(['price' => 100], $ary->get('products.desk')->val()); 60 | 61 | $ary = Ary::new(['foo', 'bar']); 62 | $this->assertEquals($ary->val(), $ary->get()); 63 | $this->assertEquals('default', $ary->get('?', 'default')); 64 | } 65 | 66 | /** 67 | * @depends testGet 68 | */ 69 | public function testSet() 70 | { 71 | $ary = Ary::new([]); 72 | $ary->set('hello', 'world'); 73 | $this->assertEquals(['hello' => 'world'], $ary->val()); 74 | 75 | $ary->set('hello', ['world' => 'hi']); 76 | $this->assertEquals(['hello' => ['world' => 'hi']], $ary->val()); 77 | 78 | $ary->set('hello.world', 'utils'); 79 | $this->assertEquals(['hello' => ['world' => 'utils']], $ary->val()); 80 | 81 | $ary->val([])->set('hi.utils', 'world'); 82 | $this->assertEquals(['hi' => ['utils' => 'world']], $ary->val()); 83 | 84 | $ary = Ary::new(['hello' => Ary::new(['world' => 'hi'])]); 85 | $ary->set('hello.world', 'utils'); 86 | $this->assertEquals('utils', $ary->get('hello.world')); 87 | $this->assertEquals(['hello' => Ary::new(['world' => 'utils'])], $ary->val()); 88 | } 89 | 90 | public function testHas() 91 | { 92 | $array = ['products.desk' => ['price' => 100]]; 93 | $this->assertTrue(Ary::new($array)->has('products.desk')); 94 | $this->assertFalse(Ary::new($array)->has('products.empty')); 95 | $this->assertFalse(Ary::new($array)->has('products')); 96 | 97 | $array = ['products' =>['desk' => ['price' => 100]]]; 98 | $this->assertTrue(Ary::new($array)->has('products')); 99 | $this->assertTrue(Ary::new($array)->has('products.desk')); 100 | $this->assertTrue(Ary::new($array)->has('products.desk.price')); 101 | $this->assertFalse(Ary::new($array)->has('products.price')); 102 | 103 | $array = ['foo' => null, 'bar' => ['baz' => null]]; 104 | $this->assertTrue(Ary::new($array)->has('foo')); 105 | $this->assertTrue(Ary::new($array)->has('bar.baz')); 106 | 107 | $ary = Ary::new(['products' => Ary::new(['desk' => Ary::new(['price' => 100])])]); 108 | $this->assertTrue($ary->has('products')); 109 | $this->assertTrue($ary->has('products.desk')); 110 | $this->assertTrue($ary->has('products.desk.price')); 111 | 112 | $ary = Ary::new(['foo', 'bar']); 113 | $this->assertFalse($ary->has('hello')); 114 | $this->assertFalse($ary->has('hello.world')); 115 | } 116 | 117 | public function testOnly() 118 | { 119 | $array = ['name' => 'Desk', 'price' => 100, 'orders' => 10]; 120 | $ary = Ary::new($array); 121 | 122 | $this->assertEquals(['name' => 'Desk', 'price' => 100], $ary->only('name', 'price')->val()); 123 | } 124 | 125 | /** 126 | * @depends testVal 127 | */ 128 | public function testToArray() 129 | { 130 | $a = [ 131 | ['start' => 1, 2, 3], 132 | [4, 5, 6], 133 | [7, 8, 'end' => 9], 134 | ]; 135 | $b = [ 136 | Ary::new(['start' => 1, 2, 3]), 137 | Ary::new([4, 5, 6]), 138 | Ary::new([7, 8, 'end' => 9]), 139 | ]; 140 | $ary = Ary::new($b); 141 | 142 | $this->assertEquals($a, $ary->toArray(false)); 143 | $this->assertEquals($a, $ary->val($a)->toArray(false)); 144 | 145 | // 测试递归 146 | $c = ['start' => 100, 99, 98, $a]; 147 | $d = ['start' => 100, 99, 98, $ary]; 148 | $ary = Ary::new($d); 149 | 150 | $this->assertEquals($c, $ary->toArray(true)); 151 | 152 | $e = ['hello' => ['world' => Ary::new(['utils'])]]; 153 | $f = ['hello' => ['world' => ['utils']]]; 154 | $ary = Ary::new($e); 155 | 156 | $this->assertEquals($f, $ary->toArray(true)); 157 | } 158 | 159 | public function testValues() 160 | { 161 | $array = ['x' => 'a', 'y' => 'b', 'z' => 'c', 'd', 'e']; 162 | $ary = Ary::new($array); 163 | 164 | $this->assertEquals(array_values($array), $ary->values()->val()); 165 | } 166 | 167 | public function testKeys() 168 | { 169 | $array = ['a' => 0, 'b' => 0, 'c' => '1', 'd' => 1, 0, 1, 2, 'e' => null, 'f' => []]; 170 | $ary = Ary::new($array); 171 | 172 | $this->assertEquals(array_keys($array), $ary->keys()->val()); 173 | // 筛选数组值 174 | $this->assertEquals(array_keys($array, 1, true), $ary->keys(1, true)->val()); 175 | $this->assertEquals(array_keys($array, null, false), $ary->keys(null, false)->val()); 176 | $this->assertEquals(array_keys($array, null, true), $ary->keys(null, true)->val()); 177 | } 178 | 179 | public function testKeyToUpperCase() 180 | { 181 | $ary = Ary::new(['a' => 0, 'b' => 1]); 182 | $this->assertEquals(['A' => 0, 'B' => 1], $ary->keyToUpperCase()->val()); 183 | } 184 | 185 | public function testKeyToLowerCase() 186 | { 187 | $ary = Ary::new(['A' => 0, 'B' => 1]); 188 | $this->assertEquals(['a' => 0, 'b' => 1], $ary->keyToLowerCase()->val()); 189 | } 190 | 191 | /** 192 | * @depends testValues 193 | * @depends testKeys 194 | */ 195 | public function testDivide() 196 | { 197 | $array = ['x' => 'a', 'y' => 'b', 'z' => 'c', 'd', 'e']; 198 | $ary = Ary::new($array); 199 | list($key, $val) = $ary->divide(); 200 | 201 | $this->assertEquals(['x', 'y', 'z', 0, 1], $key->val()); 202 | $this->assertEquals(['a', 'b', 'c', 'd', 'e'], $val->val()); 203 | } 204 | 205 | public function testFirst() 206 | { 207 | $array = [1, 2, 3]; 208 | $ary = Ary::new($array); 209 | 210 | $this->assertEquals($array[0], $ary->first()); 211 | 212 | $array = ['a' => 1, 2, 3]; 213 | $ary = Ary::new($array); 214 | 215 | $this->assertEquals($array['a'], $ary->first()); 216 | } 217 | 218 | public function testLast() 219 | { 220 | $array = [1, 2, 3]; 221 | $ary = Ary::new($array); 222 | 223 | $this->assertEquals($array[2], $ary->last()); 224 | 225 | $array = [1, 2, 'b' => 3]; 226 | $ary = Ary::new($array); 227 | 228 | $this->assertEquals($array['b'], $ary->last()); 229 | } 230 | 231 | public function testFirstKey() 232 | { 233 | $array = [1, 2, 3]; 234 | $ary = Ary::new($array); 235 | 236 | $this->assertEquals(0, $ary->firstKey()); 237 | 238 | $array = ['a' => 1, 2, 3]; 239 | $ary = Ary::new($array); 240 | 241 | $this->assertEquals('a', $ary->firstKey()); 242 | } 243 | 244 | public function testLastKey() 245 | { 246 | $array = [1, 2, 3]; 247 | $ary = Ary::new($array); 248 | 249 | $this->assertEquals(2, $ary->lastKey()); 250 | 251 | $array = [1, 2, 'b' => 3]; 252 | $ary = Ary::new($array); 253 | 254 | $this->assertEquals('b', $ary->lastKey()); 255 | } 256 | 257 | public function testLimit() 258 | { 259 | $array = ['a' => 1, 'b' => 3, 99 => 4, 5, 6]; 260 | $ary = Ary::new($array); 261 | 262 | $this->assertEquals(['a' => 1, 'b' => 3, 4], $ary->limit(3, false)->val()); 263 | $this->assertEquals($array, $ary->limit(100, true)->val()); 264 | $this->assertEquals(['a' => 1, 'b' => 3, 4, 5, 6], $ary->limit(100, false)->val()); 265 | $this->assertEmpty($ary->limit(0)->val()); 266 | $this->assertEmpty($ary->limit(-1)->val()); 267 | } 268 | 269 | public function testTail() 270 | { 271 | $array = [99 => 1, 2, 3, 'zane' => 'utils', 'ary' => 'array']; 272 | $ary = Ary::new($array); 273 | 274 | $this->assertEquals([3, 'zane' => 'utils', 'ary' => 'array'], $ary->tail(3, false)->val()); 275 | $this->assertEquals($array, $ary->tail(100, true)->val()); 276 | $this->assertEquals([1, 2, 3, 'zane' => 'utils', 'ary' => 'array'], $ary->tail(100, false)->val()); 277 | $this->assertEmpty($ary->tail(0)->val()); 278 | $this->assertEmpty($ary->tail(-1)->val()); 279 | } 280 | 281 | public function testSlice() 282 | { 283 | $array = ['a' => 1, 'b' => 3, 99 => 4, 5, 6]; 284 | $ary = Ary::new($array); 285 | 286 | $this->assertEquals(array_slice($array, 0, 3), $ary->slice(0, 3)->val()); 287 | $this->assertEquals(array_slice($array, -1, 3), $ary->slice(-1, 3)->val()); 288 | $this->assertEquals(array_slice($array, -1, 100), $ary->slice(-1, 100)->val()); 289 | $this->assertEquals(array_slice($array, -1, 0), $ary->slice(-1, 0)->val()); 290 | } 291 | 292 | public function testChunk() 293 | { 294 | $array = [1, 2, 3, 4, 5, 6]; 295 | $ary = Ary::new($array); 296 | 297 | $ary = $ary->chunk(1, false); 298 | $chunks = array_chunk($array, 1, false); 299 | foreach ($chunks as $key => $chunk) { 300 | $this->assertEquals($chunk, $ary[$key]->val()); 301 | } 302 | } 303 | 304 | public function testColumn() 305 | { 306 | $array = [ 307 | ['id' => 1, 'name' => 'a', 'val' => 'x'], 308 | ['id' => 2, 'name' => 'a', 'val' => 'y'], 309 | ['id' => 3, 'name' => 'a', 'val' => 'z'], 310 | ]; 311 | $ary = Ary::new($array); 312 | 313 | $col = array_column($array, 'name', 'id'); 314 | $val = $ary->column('name', 'id')->val(); 315 | $this->assertEquals($col, $val); 316 | 317 | $AryArray = [ 318 | Ary::new(['id' => 1, 'name' => 'a', 'val' => 'x']), 319 | Ary::new(['id' => 2, 'name' => 'b', 'val' => 'y']), 320 | Ary::new(['id' => 3, 'name' => 'c', 'val' => 'z']), 321 | ]; 322 | $ary = Ary::new($AryArray); 323 | 324 | $col = array_column($AryArray, 'val'); 325 | $val = $ary->column('val')->val(); 326 | $this->assertEquals($col, $val); 327 | 328 | $col = array_column($AryArray, 'val', 'name'); 329 | $val = $ary->column('val', 'name')->val(); 330 | $this->assertEquals($col, $val); 331 | 332 | $col = array_column($AryArray, 'name', 'val'); 333 | $val = $ary->column('name', 'val')->val(); 334 | $this->assertEquals($col, $val); 335 | } 336 | 337 | /** 338 | * @expectedException \Zane\Utils\Exceptions\AryKeyTypeException 339 | */ 340 | public function testSelect() 341 | { 342 | $array = [ 343 | ['id' => 1, 'name' => 'a', 'val' => 'x'], 344 | ['id' => 2, 'name' => 'b', 'val' => 'y'], 345 | ['id' => 3, 'name' => 'c', 'val' => 'z'], 346 | ]; 347 | $AryArray = [ 348 | Ary::new(['id' => 1, 'name' => 'a', 'val' => 'x']), 349 | Ary::new(['id' => 2, 'name' => 'b', 'val' => 'y']), 350 | Ary::new(['id' => 3, 'name' => 'c', 'val' => 'z']), 351 | ]; 352 | $data = [ 353 | 1 => ['name' => 'a'], 354 | 2 => ['name' => 'b'], 355 | 3 => ['name' => 'c'], 356 | ]; 357 | $val = Ary::new($array)->select(['name'], 'id')->val(); 358 | $this->assertEquals($data, $val); 359 | $val = Ary::new($AryArray)->select(['name'], 'id')->val(); 360 | $this->assertEquals($data, $val); 361 | 362 | $data = [ 363 | 1 => ['name' => 'a', 'val' => 'x'], 364 | 2 => ['name' => 'b', 'val' => 'y'], 365 | 3 => ['name' => 'c', 'val' => 'z'], 366 | ]; 367 | $val = Ary::new($array)->select(['name', 'val'], 'id')->val(); 368 | $this->assertEquals($data, $val); 369 | $val = Ary::new($AryArray)->select(['name', 'val'], 'id')->val(); 370 | $this->assertEquals($data, $val); 371 | 372 | $data = [ 373 | 0 => ['name' => 'a', 'val' => 'x'], 374 | 1 => ['name' => 'b', 'val' => 'y'], 375 | 2 => ['name' => 'c', 'val' => 'z'], 376 | ]; 377 | $val = Ary::new($array)->select(['name', 'val'])->val(); 378 | $this->assertEquals($data, $val); 379 | $val = Ary::new($AryArray)->select(['name', 'val'])->val(); 380 | $this->assertEquals($data, $val); 381 | $val = Ary::new($AryArray)->select(['name', 'val'], 'other')->val(); 382 | $this->assertEquals($data, $val); 383 | $this->assertEquals(Ary::new($AryArray), Ary::new($AryArray)->select()); 384 | // throw AryKeyTypeException 385 | $this->assertEquals($data, Ary::new($AryArray)->select(['name', 'val'], [])); 386 | } 387 | 388 | /** 389 | * @expectedException \Zane\Utils\Exceptions\AryKeyTypeException 390 | */ 391 | public function testWhere() 392 | { 393 | $array = [ 394 | ['id' => 1, 'name' => 'a', 'val' => 'x'], 395 | ['id' => 2, 'name' => 'b', 'val' => 'y'], 396 | ['id' => 3, 'name' => 'c', 'val' => 'z'], 397 | ]; 398 | $AryArray = [ 399 | Ary::new(['id' => 1, 'name' => 'a', 'val' => 'x']), 400 | Ary::new(['id' => 2, 'name' => 'b', 'val' => 'y']), 401 | Ary::new(['id' => 3, 'name' => 'c', 'val' => 'z']), 402 | ]; 403 | $data = [ 404 | 2 => ['id' => 3, 'name' => 'c', 'val' => 'z'], 405 | ]; 406 | $val = Ary::new($array)->where('id', '>', 2)->val(); 407 | $this->assertEquals($data, $val); 408 | 409 | $data = [ 410 | 1 => ['id' => 2, 'name' => 'b', 'val' => 'y'], 411 | 2 => ['id' => 3, 'name' => 'c', 'val' => 'z'], 412 | ]; 413 | $val = Ary::new($array)->where('id', '>=', 2)->val(); 414 | $this->assertEquals($data, $val); 415 | 416 | $data = [ 417 | ['id' => 1, 'name' => 'a', 'val' => 'x'], 418 | ]; 419 | $val = Ary::new($array)->where('id', '<', 2)->val(); 420 | $this->assertEquals($data, $val); 421 | 422 | $data = [ 423 | ['id' => 1, 'name' => 'a', 'val' => 'x'], 424 | ['id' => 2, 'name' => 'b', 'val' => 'y'], 425 | ]; 426 | $val = Ary::new($array)->where('id', '<=', 2)->val(); 427 | $this->assertEquals($data, $val); 428 | 429 | $data = [ 430 | 1 => ['id' => 2, 'name' => 'b', 'val' => 'y'], 431 | ]; 432 | $val = Ary::new($array)->where('id', '==', '2')->val(); 433 | $this->assertEquals($data, $val); 434 | 435 | $data = [ 436 | 1 => ['id' => 2, 'name' => 'b', 'val' => 'y'], 437 | ]; 438 | $val = Ary::new($array)->where('id', '===', 2)->val(); 439 | $this->assertEquals($data, $val); 440 | $val = Ary::new($array)->where('id', '===', '2')->val(); 441 | $this->assertEmpty($val); 442 | 443 | $data = [ 444 | Ary::new(['id' => 1, 'name' => 'a', 'val' => 'x']), 445 | ]; 446 | $val = Ary::new($AryArray)->where('id', '===', 1)->val(); 447 | $this->assertEquals($data, $val); 448 | 449 | $val = Ary::new($AryArray)->where('id', '?', 1)->val(); 450 | $this->assertEmpty($val); 451 | 452 | $val = Ary::new([1, 2, 3])->where('id', '==', 1)->val(); 453 | $this->assertEmpty($val); 454 | 455 | // throw AryKeyTypeException 456 | Ary::new($array)->where(null, '===', 2)->val(); 457 | } 458 | 459 | public function testCountValues() 460 | { 461 | $array = [1, 'hello', 1, 'world', 'hello']; 462 | $ary = Ary::new($array); 463 | 464 | $this->assertEquals(array_count_values($array), $ary->countValues()->val()); 465 | } 466 | 467 | public function testFlip() 468 | { 469 | $array = ['oranges', 'apples', 'pears']; 470 | $ary = Ary::new($array); 471 | 472 | $this->assertEquals(array_flip($array), $ary->flip()->val()); 473 | } 474 | 475 | public function testExist() 476 | { 477 | $array = ['1.10', 12.4, 1.13]; 478 | $ary = Ary::new($array); 479 | 480 | $this->assertEquals(in_array('12.4', $array), $ary->exist('12.4', false)); 481 | $this->assertEquals(in_array('12.4', $array, true), $ary->exist('12.4', true)); 482 | } 483 | 484 | /** 485 | * @expectedException \Zane\Utils\Exceptions\AryKeyTypeException 486 | */ 487 | public function testExistKey() 488 | { 489 | $array = ['first' => null, 'second' => 2, 3]; 490 | $ary = Ary::new($array); 491 | 492 | $this->assertEquals(array_key_exists('first', $array), $ary->existKey('first')); 493 | // 与 isset 的区别 494 | $this->assertEquals(isset($array['first']), !$ary->existKey('first')); 495 | // 数字字符串的键名默认转为数字索引 496 | $this->assertEquals(array_key_exists('0', $array), $ary->existKey('0')); 497 | // throw AryKeyTypeException 498 | $ary->existKey(null); 499 | } 500 | 501 | /** 502 | * @expectedException \Zane\Utils\Exceptions\AryKeyTypeException 503 | */ 504 | public function testIsSet() 505 | { 506 | $array = ['first' => null, 'second' => 2, 3]; 507 | $ary = Ary::new($array); 508 | 509 | // 数组成员的值为 null 会返回 false 510 | $this->assertEquals(false, $ary->isSet('first')); 511 | $this->assertEquals(isset($array['second']), $ary->isSet('second')); 512 | // 数字字符串的键名默认转为数字索引 513 | $this->assertEquals(isset($array['0']), $ary->isSet('0')); 514 | // throw AryKeyTypeException 515 | $ary->isSet(null); 516 | } 517 | 518 | public function testIsAssoc() 519 | { 520 | $this->assertTrue(Ary::new(['a' => 'a', 0 => 'b'])->isAssoc()); 521 | $this->assertTrue(Ary::new([1 => 'a', 0 => 'b'])->isAssoc()); 522 | $this->assertTrue(Ary::new([1 => 'a', 2 => 'b'])->isAssoc()); 523 | $this->assertFalse(Ary::new([0 => 'a', 1 => 'b'])->isAssoc()); 524 | $this->assertFalse(Ary::new(['a', 'b'])->isAssoc()); 525 | } 526 | 527 | public function testSort() 528 | { 529 | $array = ['l' => 'lemon', 'o' => 'orange', 'b' => 'banana', 'a' => 'apple']; 530 | $ary = Ary::new($array); 531 | 532 | asort($array); 533 | $ary->sort(true, true); 534 | $this->assertEquals($array, $ary->val()); 535 | 536 | arsort($array); 537 | $ary->sort(false, true); 538 | $this->assertEquals($array, $ary->val()); 539 | 540 | sort($array); 541 | $ary->sort(true, false); 542 | $this->assertEquals($array, $ary->val()); 543 | 544 | rsort($array); 545 | $ary->sort(false, false); 546 | $this->assertEquals($array, $ary->val()); 547 | } 548 | 549 | public function testUserSort() 550 | { 551 | $fn = function ($x, $y) { 552 | return strlen($x) <=> strlen($y); 553 | }; 554 | $array = ['longLong', 'long', 'float', 'int']; 555 | $ary = Ary::new($array); 556 | 557 | uasort($array, $fn); 558 | $ary->userSort($fn, true); 559 | $this->assertEquals($array, $ary->val()); 560 | 561 | usort($array, $fn); 562 | $ary->userSort($fn, false); 563 | $this->assertEquals($array, $ary->val()); 564 | } 565 | 566 | public function testNatSort() 567 | { 568 | $array = ['IMG0.png', 'img12.png', 'img10.png', 'img2.png', 'img1.png', 'IMG3.png']; 569 | $ary = Ary::new($array); 570 | 571 | natsort($array); 572 | $ary->natSort(true); 573 | $this->assertEquals($array, $ary->val()); 574 | 575 | natcasesort($array); 576 | $ary->natSort(false); 577 | $this->assertEquals($array, $ary->val()); 578 | } 579 | 580 | public function testKeySort() 581 | { 582 | $array = ['l' => 'lemon', 'o' => 'orange', 'b' => 'banana', 'a' => 'apple']; 583 | $ary = Ary::new($array); 584 | 585 | ksort($array); 586 | $ary->keySort(true); 587 | $this->assertEquals($array, $ary->val()); 588 | 589 | krsort($array); 590 | $ary->keySort(false); 591 | $this->assertEquals($array, $ary->val()); 592 | } 593 | 594 | public function testUserKeySort() 595 | { 596 | $fn = function ($x, $y) { 597 | return strlen($x) <=> strlen($y); 598 | }; 599 | $array = ['longLong' => 0, 'long' => 1, 'float' => 2, 'int' => 3]; 600 | $ary = Ary::new($array); 601 | 602 | uksort($array, $fn); 603 | $ary->userKeySort($fn); 604 | $this->assertEquals($array, $ary->val()); 605 | } 606 | 607 | public function testMax() 608 | { 609 | $max = Ary::new([1, 7, 9, 23, 2, 0])->max(); 610 | $this->assertEquals(23, $max); 611 | } 612 | 613 | public function testMin() 614 | { 615 | $min = Ary::new([1, 7, 9, 23, 2, 0])->min(); 616 | $this->assertEquals(0, $min); 617 | } 618 | 619 | public function testMaxKey() 620 | { 621 | $maxKey = Ary::new([2, 2, 1, 2, 1, 1, 2])->maxKey(); 622 | $this->assertEquals(0, $maxKey); 623 | 624 | $maxKey = Ary::new(['hello' => 1, 'world' => 2, 'hi' => 1, 'utils' => 2])->maxKey(); 625 | $this->assertEquals('world', $maxKey); 626 | } 627 | 628 | public function testMinKey() 629 | { 630 | $minKey = Ary::new([2, 2, 1, 2, 1, 1, 2])->minKey(); 631 | $this->assertEquals(2, $minKey); 632 | 633 | $minKey = Ary::new(['hello' => 1, 'world' => 2, 'hi' => 1, 'utils' => 2])->minKey(); 634 | $this->assertEquals('hello', $minKey); 635 | } 636 | 637 | public function testShuffle() 638 | { 639 | $ary = Ary::new(range(1, 20)); 640 | 641 | $this->assertNotEquals(range(1, 20), $ary->shuffle()->val()); 642 | } 643 | 644 | public function testUnique() 645 | { 646 | $array = ['a' => 'green', 'red', 'b' => 'green', 'blue', 'red']; 647 | $ary = Ary::new($array); 648 | 649 | $this->assertEquals(array_unique($array), $ary->unique()->val()); 650 | } 651 | 652 | public function testReverse() 653 | { 654 | $array = ['a' => 0, 'b' => 0, 'c' => '1', 'd' => 1, 0, 1, 2, 'e' => null, 'f' => []]; 655 | $ary = Ary::new($array); 656 | 657 | $this->assertEquals(array_reverse($array), $ary->reverse()->toArray()); 658 | } 659 | 660 | public function testExcept() 661 | { 662 | $array = [1, 'hello' => 'world', 'hi' => 'utils', 2, 99 => 100]; 663 | $ary = Ary::new($array); 664 | 665 | $this->assertEquals([1, 2, 99 => 100], $ary->except('hello', 'hi')->val()); 666 | $this->assertEquals($array, $ary->except('none')->val()); 667 | $this->assertEmpty($ary->val([[1, 2, 99 => 100]])->except(0, 1, 99)->val()); 668 | } 669 | 670 | public function testPush() 671 | { 672 | $array = []; 673 | $ary = Ary::new($array); 674 | 675 | $ary->push(1, 2, 3, '4'); 676 | array_push($array, 1, 2, 3, '4'); 677 | 678 | $this->assertEquals($array, $ary->val()); 679 | 680 | return [$array, $ary]; 681 | } 682 | 683 | /** 684 | * @param array $pushResult 685 | * @depends testPush 686 | */ 687 | public function testPop($pushResult) 688 | { 689 | list($array, $ary) = $pushResult; 690 | 691 | $a = array_pop($array); 692 | $b = $ary->pop(); 693 | 694 | $this->assertEquals($a, $b); 695 | $this->assertEquals($array, $ary->val()); 696 | } 697 | 698 | public function testUnShift() 699 | { 700 | $array = ['orange', 'banana']; 701 | $ary = Ary::new($array); 702 | 703 | array_unshift($array, 'apple', 'raspberry'); 704 | $ary->unShift('apple', 'raspberry'); 705 | $this->assertEquals($array, $ary->val()); 706 | 707 | return [$array, $ary]; 708 | } 709 | 710 | /** 711 | * @param array $unShiftResult 712 | * @depends testUnShift 713 | */ 714 | public function testShift($unShiftResult) 715 | { 716 | list($array, $ary) = $unShiftResult; 717 | 718 | $a = array_shift($array); 719 | $b = $ary->shift(); 720 | $this->assertEquals($a, $b); 721 | $this->assertEquals($array, $ary->val()); 722 | array_shift($array); 723 | $this->assertEquals($array, $ary->shift(false)->val()); 724 | } 725 | 726 | public function testAppend() 727 | { 728 | $a = [1, 2, 3, 4]; 729 | $b = ['a', 'b', 'c', 'd']; 730 | $ab = array_merge($a, $b); 731 | 732 | $aryA = Ary::new($a); 733 | $aryB = Ary::new($b); 734 | 735 | // append Ary 736 | $aryAB = $aryA->append($aryB); 737 | $this->assertEquals($ab, $aryAB->val()); 738 | 739 | // preserve values 740 | $a = [1, 2, 3]; 741 | $b = [4, 5, 6, 7, 99 => 100]; 742 | $aryA->val($a); 743 | $aryB->val($b); 744 | 745 | $this->assertEquals([1, 2, 3, 7, 99 => 100], $aryA->append($aryB, true)->val()); 746 | } 747 | 748 | public function testSearch() 749 | { 750 | $array = ['blue', 'red', 'green', 'red', '1', 1]; 751 | $ary = Ary::new($array); 752 | 753 | $a = array_search('red', $array, true); 754 | $b = $ary->search('red', true); 755 | $this->assertEquals($a, $b); 756 | 757 | // strict 为 false 时 '1' 与 1 相等 758 | $a = array_search(1, $array, false); 759 | $b = $ary->search(1, false); 760 | $this->assertEquals($a, $b); 761 | } 762 | 763 | /** 764 | * @depends testKeys 765 | * @depends testSearch 766 | * @depends testSlice 767 | * @depends testLimit 768 | */ 769 | public function testBefore() 770 | { 771 | $ary = Ary::new(['pi', '@', '0php.net', '233', 233, 233, 'hello' => 'world']); 772 | 773 | $this->assertEquals(['pi'], $ary->before('@', false, false)->val()); 774 | $this->assertEquals(['pi', '@'], $ary->before('@', true, false)->val()); 775 | // 确认采用了严格比较 776 | $this->assertEquals($ary->limit(3), $ary->before('233', false, false)); 777 | // 确认只包含第一个指定值之前的元素 778 | $this->assertEquals($ary->limit(4), $ary->before(233, false)); 779 | // 确认键名保护 780 | $this->assertEquals($ary, $ary->before('world', true, true)); 781 | // 确认字符串键名始终不变 782 | $a = ['pi', '@', '0php.net', '233', 233, 233, 'world']; 783 | $this->assertNotEquals($a, $ary->before('world', true, false)->val()); 784 | // 确认值不存在返回空 785 | $this->assertEmpty($ary->before('hello')->val()); 786 | } 787 | 788 | /** 789 | * @depends testKeys 790 | * @depends testSearch 791 | * @depends testSlice 792 | * @depends testTail 793 | */ 794 | public function testAfter() 795 | { 796 | $ary = Ary::new(['hello' => 233, 233, '233', '0php.net', '@', 'pi']); 797 | 798 | $this->assertEquals(['pi'], $ary->after('@', false, false)->val()); 799 | $this->assertEquals(['@', 'pi'], $ary->after('@', true, false)->val()); 800 | // 确认采用了严格比较 801 | $this->assertEquals($ary->tail(3, false), $ary->after('233', false, false)); 802 | // 确认只包含第一个指定值之前的元素 803 | $this->assertEquals($ary->tail(5, false), $ary->after(233, false, false)); 804 | // 确认键名保护 805 | $this->assertEquals($ary->tail(5, true), $ary->after(233, false, true)); 806 | // 确认字符串键名始终不变 807 | $a = [233, 233, '233', '0php.net', '@', 'pi']; 808 | $this->assertNotEquals($a, $ary->after(233, true, false)); 809 | // 确认值不存在返回空 810 | $this->assertEmpty($ary->after('zane')->val()); 811 | } 812 | 813 | /** 814 | * @depends testKeys 815 | * @depends testSearch 816 | * @depends testSlice 817 | * @depends testLimit 818 | */ 819 | public function testBeforeKey() 820 | { 821 | $array = ['hello' => 'world', 'hi' => 'utils', 'I' => 'am', 'very' => 'nice', 233]; 822 | $ary = Ary::new($array); 823 | 824 | $this->assertEquals($ary->limit(1), $ary->beforeKey('hi', false)); 825 | $this->assertEquals($ary->limit(2), $ary->beforeKey('hi', true)); 826 | $this->assertEquals($ary->limit(4), $ary->beforeKey(0, false)); 827 | $this->assertEquals($ary, $ary->beforeKey(0, true)); 828 | $this->assertEmpty($ary->beforeKey(1)->val()); 829 | } 830 | 831 | /** 832 | * @depends testKeys 833 | * @depends testSearch 834 | * @depends testSlice 835 | * @depends testTail 836 | */ 837 | public function testAfterKey() 838 | { 839 | $array = ['hello' => 'world', 'hi' => 'utils', 'I' => 'am', 'very' => 'nice', 233]; 840 | $ary = Ary::new($array); 841 | 842 | $this->assertEquals($ary->tail(3), $ary->afterKey('hi', false)); 843 | $this->assertEquals($ary->tail(4), $ary->afterKey('hi', true)); 844 | $this->assertEquals($ary->limit(0), $ary->afterKey(0, false)); 845 | $this->assertEquals($ary, $ary->afterKey('hello', true)); 846 | $this->assertEmpty($ary->afterKey(1)->val()); 847 | } 848 | 849 | public function testReplace() 850 | { 851 | $base = ['orange', 'banana', 'apple', 'raspberry']; 852 | $replacements = [0 => 'pineapple', 4 => 'cherry']; 853 | $replacements2 = [0 => 'grape']; 854 | $aryBase = Ary::new($base); 855 | $aryReplacements = Ary::new($replacements); 856 | $aryReplacements2 = Ary::new($replacements2); 857 | 858 | $basket = array_replace($base, $replacements, $replacements2); 859 | $aryBasket = $aryBase->replace($aryReplacements, $aryReplacements2); 860 | $this->assertEquals($basket, $aryBasket->val()); 861 | } 862 | 863 | public function testIntersect() 864 | { 865 | $ary1 = Ary::new(['a' => 'green', 'red', 'blue']); 866 | $ary2 = Ary::new(['b' => 'green', 'yellow', 'red']); 867 | $this->assertEquals(['a' => 'green', 'red'], $ary1->intersect($ary2, false)->val()); 868 | 869 | $ary1 = Ary::new(['a' => 'green', 'b' => 'brown', 'c' => 'blue', 'red']); 870 | $ary2 = Ary::new(['a' => 'green', 'b' => 'yellow', 'blue', 'red']); 871 | $this->assertEquals(['a' => 'green'], $ary1->intersect($ary2, true)->val()); 872 | } 873 | 874 | public function testDiff() 875 | { 876 | $ary1 = Ary::new(['a' => 'green', 'red', 'blue', 'red']); 877 | $ary2 = Ary::new(['b' => 'green', 'yellow', 'red']); 878 | $this->assertEquals([1 => 'blue'], $ary1->diff($ary2, false)->val()); 879 | 880 | $ary1 = Ary::new(['a' => 'green', 'b' => 'brown', 'c' => 'blue', 'red']); 881 | $ary2 = Ary::new(['a' => 'green', 'yellow', 'red']); 882 | $this->assertEquals(['b' => 'brown', 'c' => 'blue', 'red'], $ary1->diff($ary2, true)->val()); 883 | } 884 | 885 | public function testIntersectKey() 886 | { 887 | $ary1 = Ary::new(['blue' => 1, 'red' => 2, 'green' => 3, 'purple' => 4]); 888 | $ary2 = Ary::new(['green' => 5, 'blue' => 6, 'yellow' => 7, 'cyan' => 8]); 889 | 890 | $this->assertEquals(['blue' => 1, 'green' => 3], $ary1->intersectKey($ary2)->val()); 891 | } 892 | 893 | public function testDiffKey() 894 | { 895 | $ary1 = Ary::new(['blue' => 1, 'red' => 2, 'green' => 3, 'purple' => 4]); 896 | $ary2 = Ary::new(['green' => 5, 'blue' => 6, 'yellow' => 7, 'cyan' => 8]); 897 | 898 | $this->assertEquals(['red' => 2, 'purple' => 4], $ary1->diffKey($ary2)->val()); 899 | } 900 | 901 | public function testClean() 902 | { 903 | $a = [0 => 'foo', 1 => false, 2 => -1, 3 => null, 4 => '', 5 => []]; 904 | $ary = Ary::new($a); 905 | 906 | $this->assertEquals(['foo', 2 => -1], $ary->clean()->val()); 907 | } 908 | 909 | public function testJoin() 910 | { 911 | $a = ['hello', 'world', '!']; 912 | $aryA = Ary::new($a); 913 | 914 | $this->assertEquals(implode(' ', $a), $aryA->join(' ')); 915 | 916 | $b = ['I', 'am', 'utils', $aryA]; 917 | $aryB = Ary::new($b); 918 | $this->assertEquals('I am utils hello world !', $aryB->join(' ')); 919 | } 920 | 921 | /** 922 | * @depends testToArray 923 | */ 924 | public function testEach() 925 | { 926 | $str1 = ''; 927 | $str2 = ''; 928 | $fn = function ($val, $key, $isAry) use (&$str1, &$str2) { 929 | if ($isAry) { 930 | $str2 .= $key.'=>'.$val.PHP_EOL; 931 | } else { 932 | $str1 .= $key.'=>'.$val.PHP_EOL; 933 | } 934 | }; 935 | $array = ['hello' => 'world', 'pi' => '0php.net', 1, 2, 3]; 936 | $ary = Ary::new($array); 937 | array_walk($array, $fn, false); 938 | $ary->each($fn, true, false); 939 | $this->assertEquals($str1, $str2); 940 | 941 | // 测试递归 942 | $array2 = ['my' => 'second', 'array' => $array]; 943 | $ary2 = Ary::new($array2); 944 | array_walk_recursive($array2, $fn, false); 945 | $ary2->each($fn, true, true); 946 | $this->assertEquals($str1, $str2); 947 | } 948 | 949 | public function testMap() 950 | { 951 | $fn = function ($n) { 952 | return $n * $n; 953 | }; 954 | $array = [1, 2, 3, 4, 5]; 955 | $ary = Ary::new($array); 956 | 957 | $this->assertEquals(array_map($fn, $array), $ary->map($fn)->val()); 958 | } 959 | 960 | public function testFilter() 961 | { 962 | $fn = function ($n, $k) { 963 | return $n % 2 || $k == 5; 964 | }; 965 | $array = [1, 2, 3, 4, 5, 6]; 966 | $ary = Ary::new($array); 967 | 968 | $this->assertEquals( 969 | array_filter($array, $fn, ARRAY_FILTER_USE_BOTH), 970 | $ary->filter($fn, ARRAY_FILTER_USE_BOTH)->val() 971 | ); 972 | } 973 | 974 | public function testReduce() 975 | { 976 | $fn = function ($carry, $item) { 977 | $carry += $item; 978 | 979 | return $carry; 980 | }; 981 | $array = [1, 2, 3, 4, 5, 6]; 982 | $ary = Ary::new($array); 983 | 984 | $this->assertEquals(array_reduce($array, $fn), $ary->reduce($fn)); 985 | $this->assertEquals(array_reduce($array, $fn, 1), $ary->reduce($fn, 1)); 986 | } 987 | 988 | public function testFlat() 989 | { 990 | $array = ['a', 'b', 'c' => ['d', 'e' => ['f', 'g' => Ary::new(['h', 'i' => Ary::new(['j'])])]], 'x' => 'y']; 991 | $ary = Ary::new($array); 992 | 993 | $this->assertEquals(['a', 'b', 'd', 'f', 'h', 'j', 'y'], $ary->flat(false)->val()); 994 | $this->assertEquals(['a', 'b', 'd', 'f', 'h', 'j', 'x' => 'y'], $ary->flat(true)->val()); 995 | } 996 | 997 | public function testPad() 998 | { 999 | $array = [1, 2, 3]; 1000 | $ary = Ary::new($array); 1001 | 1002 | $this->assertEquals(array_pad($array, 5, 1), $ary->pad(5, 1)->val()); 1003 | $this->assertEquals(array_pad($array, 1, 1), $ary->pad(1, 1)->val()); 1004 | } 1005 | 1006 | public function testFill() 1007 | { 1008 | $array = ['hello' => 'world', 'hi' => 'world']; 1009 | $ary = Ary::new($array); 1010 | 1011 | $ary = $ary->fill(1); 1012 | $this->assertEquals(1, $ary->val()['hello']); 1013 | $this->assertEquals(1, $ary->val()['hi']); 1014 | } 1015 | 1016 | public function testEmpty() 1017 | { 1018 | $ary = Ary::new([]); 1019 | $this->assertEquals(true, $ary->empty()); 1020 | 1021 | $ary->val([1]); 1022 | $this->assertEquals(false, $ary->empty()); 1023 | } 1024 | 1025 | public function product() 1026 | { 1027 | $array = [1, 2, 3, 4]; 1028 | $ary = Ary::new($array); 1029 | 1030 | $this->assertEquals(array_product($array), $ary->product()); 1031 | } 1032 | 1033 | /** 1034 | * @depends testPush 1035 | * @depends testPop 1036 | */ 1037 | public function testAllTrue() 1038 | { 1039 | $ary = Ary::new([true, true, true, true]); 1040 | 1041 | $this->assertEquals(true, $ary->allTrue()); 1042 | $this->assertEquals(true, $ary->push(true)->allTrue()); 1043 | $this->assertEquals(false, $ary->pop(false)->push(false)->allTrue()); 1044 | $this->assertEquals(true, $ary->val([])->allTrue()); 1045 | } 1046 | 1047 | public function testSum() 1048 | { 1049 | $array = [1, 2, 3, 4, 5, 6]; 1050 | $ary = Ary::new($array); 1051 | 1052 | $this->assertEquals(array_sum($array), $ary->sum()); 1053 | } 1054 | 1055 | /** 1056 | * @expectedException \Zane\Utils\Exceptions\AryOutOfRangeException 1057 | */ 1058 | public function testRand() 1059 | { 1060 | $array = ['hello' => 1, 'world' => 2, 'x' => 3, 4, 5, 6]; 1061 | $ary = Ary::new($array); 1062 | 1063 | $this->assertEquals(true, in_array($ary->rand(1)->first(), $array)); 1064 | $this->assertEquals(true, array_key_exists($ary->rand(1)->firstKey(), $array)); 1065 | $this->assertEquals(true, in_array($ary->rand(3)->first(), $array)); 1066 | $this->assertEquals(true, array_key_exists($ary->rand(3)->firstKey(), $array)); 1067 | // throw AryOutOfRangeException 1068 | $this->assertEmpty(Ary::new([])->rand(1)); 1069 | } 1070 | 1071 | /** 1072 | * @expectedException \Zane\Utils\Exceptions\AryOutOfRangeException 1073 | */ 1074 | public function testRandVal() 1075 | { 1076 | $array = ['hello' => 1, 'world' => 2, 'x' => 3, 4, 5, 6]; 1077 | $ary = Ary::new($array); 1078 | 1079 | $this->assertTrue(in_array($ary->randVal(), $array)); 1080 | // throw AryOutOfRangeException 1081 | $this->assertEmpty(Ary::new([])->randVal()); 1082 | } 1083 | 1084 | /** 1085 | * @expectedException \Zane\Utils\Exceptions\AryOutOfRangeException 1086 | */ 1087 | public function testRandKey() 1088 | { 1089 | $array = ['hello' => 1, 'world' => 2, 'x' => 3, 4, 5, 6]; 1090 | $ary = Ary::new($array); 1091 | 1092 | $this->assertEquals(true, array_key_exists($ary->randKey(), $array)); 1093 | // throw AryOutOfRangeException 1094 | $this->assertEmpty(Ary::new([])->randKey()); 1095 | } 1096 | 1097 | public function testToJson() 1098 | { 1099 | $ary = Ary::new([ 1100 | 'name' => 'zane', 1101 | 'email' => 'pi@0php.net', 1102 | ]); 1103 | $json = <<<'EOT' 1104 | { 1105 | "name": "zane", 1106 | "email": "pi@0php.net" 1107 | } 1108 | EOT; 1109 | 1110 | $this->assertEquals($json, $ary->toJson()); 1111 | 1112 | // 测试递归 1113 | $ary2 = Ary::new([ 1114 | 'id' => 1, 1115 | 'profile' => $ary, 1116 | ]); 1117 | $json2 = <<<'EOT' 1118 | { 1119 | "id": 1, 1120 | "profile": { 1121 | "name": "zane", 1122 | "email": "pi@0php.net" 1123 | } 1124 | } 1125 | EOT; 1126 | 1127 | $this->assertEquals($json2, $ary2->toJson()); 1128 | 1129 | return [$ary, $json, $ary2, $json2]; 1130 | } 1131 | 1132 | /** 1133 | * @param array $array 1134 | * @depends testToJson 1135 | */ 1136 | public function testFromJson($array) 1137 | { 1138 | list($ary, $json, $ary2, $json2) = $array; 1139 | $this->assertEquals($ary->val(), Ary::fromJson($json)->val()); 1140 | $this->assertEquals($ary2->toArray(), Ary::fromJson($json2)->val()); 1141 | } 1142 | 1143 | public function testCombine() 1144 | { 1145 | $array = ['hello' => 'world', 'hi' => 'ary']; 1146 | $key = Ary::new(['hello', 'hi']); 1147 | $val = Ary::new(['world', 'ary']); 1148 | 1149 | self::assertEquals($array, Ary::combine($key, $val)->val()); 1150 | } 1151 | 1152 | public function testNewFill() 1153 | { 1154 | $a = [1, 1, 1]; 1155 | $b = Ary::newFill(0, 3, 1)->val(); 1156 | $this->assertEquals($a, $b); 1157 | 1158 | $c = [99 => 1, 1, 1]; 1159 | $d = Ary::newFill(99, 3, 1)->val(); 1160 | $this->assertEquals($c, $d); 1161 | } 1162 | 1163 | public function testIteratorAggregate() 1164 | { 1165 | $a = ['a' => 0, 'b' => 1, 'c' => 2, 3, 4, 5]; 1166 | 1167 | $ary = Ary::new($a); 1168 | 1169 | foreach ($ary as $key => $val) { 1170 | $this->assertEquals($a[$key], $val); 1171 | } 1172 | } 1173 | 1174 | public function testArrayAccess() 1175 | { 1176 | $array = ['hello' => 'world', 'my' => 'name', 'is' => '?', 1]; 1177 | $ary = Ary::new($array); 1178 | 1179 | $this->assertEquals('world', $ary['hello']); 1180 | $this->assertEquals(1, $ary[0]); 1181 | $this->assertEquals(null, $ary['?']); 1182 | $this->assertEquals(false, isset($ary['?'])); 1183 | unset($ary['my']); 1184 | $this->assertEquals(false, isset($ary['my'])); 1185 | $ary['hello'] = 'other'; 1186 | $this->assertEquals('other', $ary['hello']); 1187 | $ary[] = 'dot'; 1188 | $this->assertEquals('dot', $ary->last()); 1189 | } 1190 | 1191 | public function testCountable() 1192 | { 1193 | $ary = Ary::new(range(1, 10)); 1194 | $this->assertEquals(10, count($ary)); 1195 | 1196 | $ary->val([]); 1197 | $this->assertEquals(0, count($ary)); 1198 | } 1199 | 1200 | public function testObjectAccess() 1201 | { 1202 | $ary = Ary::new(['val' => 1, 'test' => 2]); 1203 | $this->assertEquals(1, $ary->val); 1204 | $this->assertEquals(2, $ary->test); 1205 | $this->assertEquals(true, isset($ary->test)); 1206 | $this->assertEquals(false, isset($ary->other)); 1207 | unset($ary->test); 1208 | $this->assertEquals(false, isset($ary->test)); 1209 | $ary->val = 2; 1210 | $this->assertEquals(2, $ary->val); 1211 | } 1212 | } 1213 | -------------------------------------------------------------------------------- /src/Ary.php: -------------------------------------------------------------------------------- 1 | true, 26 | 'limitPreserveKeys' => false, 27 | 'tailPreserveKeys' => false, 28 | 'slicePreserveKeys' => false, 29 | 'chunkPreserveKeys' => false, 30 | 'existStrict' => true, 31 | 'sortAsc' => true, 32 | 'sortPreserveKeys' => false, 33 | 'sortFlag' => SORT_REGULAR, 34 | 'userSortPreserveKeys' => false, 35 | 'natSortCaseSensitive' => true, 36 | 'keySortAsc' => true, 37 | 'keySortFlag' => SORT_REGULAR, 38 | 'uniqueFlag' => SORT_STRING, 39 | 'popGetElement' => true, 40 | 'shiftGetElement' => true, 41 | 'appendPreserveValues' => false, 42 | 'searchStrict' => true, 43 | 'beforePreserveKeys' => true, 44 | 'afterPreserveKeys' => true, 45 | 'beforeContain' => false, 46 | 'afterContain' => false, 47 | 'beforeKeyContain' => false, 48 | 'afterKeyContain' => false, 49 | 'intersectCompKey' => false, 50 | 'diffCompKey' => false, 51 | 'joinGlue' => '', 52 | 'eachRecursive' => false, 53 | 'filterFlag' => 0, 54 | 'flatPreserveKeys' => false, 55 | 'toArrayRecursive' => false, 56 | 'toJsonOptions' => JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT, 57 | 'toJsonDepth' => 512, 58 | 'fromJsonDepth' => 512, 59 | 'fromJsonOptions' => 0, 60 | ]; 61 | 62 | private $val; 63 | 64 | public function __construct(array $array = []) 65 | { 66 | $this->val = $array; 67 | } 68 | 69 | /** 70 | * 获取或设置该实例的数组. 71 | * 72 | * @param array|null $array 为空时获取实例的数组,非空时设置实例的数组 73 | * 74 | * @return Ary|array 原实例或数组 75 | */ 76 | public function val(array $array = null) 77 | { 78 | if (is_null($array)) { 79 | return $this->val; 80 | } 81 | 82 | $this->val = $array; 83 | 84 | return $this; 85 | } 86 | 87 | /** 88 | * 使用「点」式语法从深度嵌套数组中取回指定的值 89 | * 90 | * @see https://laravel.com/docs/5.6/helpers#method-array-get 91 | * 92 | * @param string $dotKey 符合「点」式语法的键名 93 | * @param mixed|null $default 默认值 94 | * 95 | * @return mixed 96 | */ 97 | public function get(string $dotKey = null, $default = null) 98 | { 99 | if (is_null($dotKey)) { 100 | return $this->val; 101 | } 102 | 103 | if (array_key_exists($dotKey, $this->val)) { 104 | return $this->val[$dotKey]; 105 | } 106 | 107 | $keys = explode('.', $dotKey); 108 | $array = $this->val; 109 | 110 | foreach ($keys as $key) { 111 | if (is_array($array) && array_key_exists($key, $array)) { 112 | $array = $array[$key]; 113 | } elseif ($array instanceof static && array_key_exists($key, $array->val())) { 114 | $array = $array[$key]; 115 | } else { 116 | return $default; 117 | } 118 | } 119 | 120 | return $array; 121 | } 122 | 123 | /** 124 | * 使用「点」式语法从深度嵌套数组中设置指定的值 125 | * 126 | * @see https://laravel.com/docs/5.6/helpers#method-array-set 127 | * 128 | * @param string $dotKey 符合「点」式语法的键名 129 | * @param mixed $val 要设置的值 130 | * 131 | * @return Ary 原实例 132 | */ 133 | public function set(string $dotKey, $val): self 134 | { 135 | $keys = explode('.', $dotKey); 136 | $array = &$this->val; 137 | 138 | while (count($keys) > 1) { 139 | $key = array_shift($keys); 140 | 141 | if (!isset($array[$key]) || !static::accessible($array[$key])) { 142 | $array[$key] = []; 143 | } 144 | 145 | $array = &$array[$key]; 146 | } 147 | 148 | $array[array_shift($keys)] = $val; 149 | 150 | return $this; 151 | } 152 | 153 | /** 154 | * 使用「点」式语法判断是否存在指定键名的值,多个键名要全部存在才返回 true. 155 | * 156 | * @see https://laravel.com/docs/5.6/helpers#method-array-has 157 | * 158 | * @param string[] $dotKeys 符合「点」式语法的键名 159 | * 160 | * @return bool 161 | */ 162 | public function has(string ...$dotKeys): bool 163 | { 164 | foreach ($dotKeys as $dotKey) { 165 | if (array_key_exists($dotKey, $this->val)) { 166 | continue; 167 | } 168 | 169 | $keys = explode('.', $dotKey); 170 | $array = $this->val; 171 | 172 | foreach ($keys as $key) { 173 | if (is_array($array) && array_key_exists($key, $array)) { 174 | $array = $array[$key]; 175 | } elseif ($array instanceof static && array_key_exists($key, $array->val())) { 176 | $array = $array[$key]; 177 | } else { 178 | return false; 179 | } 180 | } 181 | } 182 | 183 | return true; 184 | } 185 | 186 | /** 187 | * 返回只包含指定键名的新实例. 188 | * 189 | * @param array ...$keys 190 | * 191 | * @return Ary 新实例 192 | */ 193 | public function only(...$keys) 194 | { 195 | $val = array_intersect_key($this->val, array_flip($keys)); 196 | 197 | return static::new($val); 198 | } 199 | 200 | /** 201 | * 获取实例数组中的全部值 202 | * 203 | * @see http://php.net/manual/zh/function.array-values.php 204 | * 205 | * @return Ary 新实例 206 | */ 207 | public function values(): self 208 | { 209 | return static::new(array_values($this->val)); 210 | } 211 | 212 | /** 213 | * 获取实例数组中的键名. 214 | * 215 | * @see http://php.net/manual/zh/function.array-keys.php 216 | * 217 | * @param mixed|null $searchValue 空则返回全部键,非空则返回对应 $searchValue 值的键 218 | * @param bool|null $strict 为真时数组中的值与 $searchValue 采用严格比较 219 | * 220 | * @return Ary 新实例 221 | */ 222 | public function keys($searchValue = null, bool $strict = null): self 223 | { 224 | // 实参个数为零时直接调用 array_keys 函数 225 | // 若此时调用 array_keys 函数时带入 $searchValue 和 $strict 参数会导致 array_keys 返回 数组值为 null 的键名, 与预期结果不符 226 | $args = func_num_args(); 227 | if ($args === 0) { 228 | return static::new(array_keys($this->val)); 229 | } 230 | 231 | return static::new( 232 | array_keys( 233 | $this->val, 234 | $searchValue, 235 | static::default($strict, 'keysStrict') 236 | ) 237 | ); 238 | } 239 | 240 | /** 241 | * 返回一个全部键名大写的新实例. 242 | * 243 | * @return Ary 新实例 244 | */ 245 | public function keyToUpperCase(): self 246 | { 247 | $val = array_change_key_case($this->val, CASE_UPPER); 248 | 249 | return static::new($val); 250 | } 251 | 252 | /** 253 | * 返回一个全部键名小写的新实例. 254 | * 255 | * @return Ary 新实例 256 | */ 257 | public function keyToLowerCase() 258 | { 259 | $val = array_change_key_case($this->val, CASE_LOWER); 260 | 261 | return static::new($val); 262 | } 263 | 264 | /** 265 | * 返回两个实例,一个包含原本实例的键,另一个包含原本实例的值 266 | * 267 | * @return Ary[] 268 | */ 269 | public function divide(): array 270 | { 271 | return [$this->keys(), $this->values()]; 272 | } 273 | 274 | /** 275 | * 获取实例数组中第一个元素的值 276 | * 277 | * @return mixed 278 | */ 279 | public function first() 280 | { 281 | return reset($this->val); 282 | } 283 | 284 | /** 285 | * 获取实例数组中最后一个元素的值 286 | * 287 | * @return mixed 288 | */ 289 | public function last() 290 | { 291 | return end($this->val); 292 | } 293 | 294 | /** 295 | * 获取实例数组中第一个元素的键. 296 | * 297 | * @return int|null|string 298 | */ 299 | public function firstKey() 300 | { 301 | $this->first(); 302 | 303 | return key($this->val); 304 | } 305 | 306 | /** 307 | * 获取实例数组中最后一个元素的键. 308 | * 309 | * @return int|null|string 310 | */ 311 | public function lastKey() 312 | { 313 | $this->last(); 314 | 315 | return key($this->val); 316 | } 317 | 318 | /** 319 | * 获取实例数组中前 $len 个元素组成的新 Ary 实例. 320 | * 321 | * @see \Zane\Utils\Ary::slice() 322 | * 323 | * @param int $len 获取元素的个数,小于等于 0 则返回空数组的实例,大于等于实例数组的长度则返回原数组(索引可能会改变具体看 $preserveKeys 参数)的新实例 324 | * @param bool|null $preserveKeys 为 true 则数字索引保持不变,false 则会重置数组的数字索引,字符串键名始终保持不变 325 | * 326 | * @return Ary 新实例 327 | */ 328 | public function limit(int $len, bool $preserveKeys = null): self 329 | { 330 | if ($len <= 0) { 331 | return static::new([]); 332 | } 333 | 334 | $val = array_slice($this->val, 0, $len, static::default($preserveKeys, 'limitPreserveKeys')); 335 | 336 | return static::new($val); 337 | } 338 | 339 | /** 340 | * 获取实例数组中后 $len 个元素组成的新 Ary 实例. 341 | * 342 | * @see \Zane\Utils\Ary::slice() 343 | * 344 | * @param int $len 获取元素的个数,小于等于 0 则返回空数组的实例,大于等于实例数组的长度则返回原数组(索引可能会改变具体看 $preserveKeys 参数)的新实例 345 | * @param bool|null $preserveKeys 为 true 则数字索引保持不变,false 则会重置数组的数字索引,字符串键名始终保持不变 346 | * 347 | * @return Ary 新实例 348 | */ 349 | public function tail(int $len, bool $preserveKeys = null): self 350 | { 351 | if ($len <= 0) { 352 | return static::new([]); 353 | } 354 | 355 | $val = array_slice($this->val, -$len, null, static::default($preserveKeys, 'tailPreserveKeys')); 356 | 357 | return static::new($val); 358 | } 359 | 360 | /** 361 | * 从实例数组中取出一段并返回新的实例. 362 | * 363 | * @see http://php.net/manual/zh/function.array-slice.php 364 | * 365 | * @param int $offset 起始偏移量 366 | * @param int $len 长度 367 | * @param bool|null $preserveKeys 为 true 则数字索引保持不变,false 则会重置数组的数字索引,字符串键名始终保持不变 368 | * 369 | * @return Ary 新实例 370 | */ 371 | public function slice(int $offset, int $len = null, bool $preserveKeys = null): self 372 | { 373 | $val = array_slice($this->val, $offset, $len, static::default($preserveKeys, 'slicePreserveKeys')); 374 | 375 | return static::new($val); 376 | } 377 | 378 | /** 379 | * 将一个实例数组分割为多个并返回多个新实例. 380 | * 381 | * @see http://php.net/manual/zh/function.array-chunk.php 382 | * 383 | * @param int $size 每个新实例数组的大小,最后一个实例数组可能小于 $size 384 | * @param bool|null $preserveKeys 为 true 则数字索引保持不变,false 则会重置数组的数字索引 385 | * 386 | * @return Ary 新实例 387 | */ 388 | public function chunk(int $size, bool $preserveKeys = null): self 389 | { 390 | $chunks = array_chunk($this->val, $size, static::default($preserveKeys, 'chunkPreserveKeys')); 391 | 392 | $val = []; 393 | foreach ($chunks as $chunk) { 394 | $val[] = static::new($chunk); 395 | } 396 | 397 | return static::new($val); 398 | } 399 | 400 | /** 401 | * 返回由实例数组中指定的一列所组成的新实例. 402 | * 403 | * @see http://php.net/manual/zh/function.array-column.php 404 | * 405 | * @param mixed $columnKey 需要返回值的列,它可以是索引数组的列索引,或者是关联数组的列的键,也可以是属性名,为 NULL 时返回整个数组 406 | * @param string|int|null $indexKey 作为返回数组的索引或键的列 407 | * 408 | * @return Ary 新实例 409 | */ 410 | public function column($columnKey, $indexKey = null): self 411 | { 412 | // 当键名为 val 会与数组属性 val 冲突,因为在 PHP 中同一个类的对象即使不是同一个实例也可以互相访问对方的私有与受保护成员 413 | // 所以并不会触发 __get() 方法,来获取数组中的值,而是直接将整个 val 属性返回 414 | // 为了解决这个问题采用匿名类的方式将 array_column 函数从 Ary 类调用改变到在匿名类中调用 415 | if ($columnKey === 'val' || $indexKey === 'val') { 416 | $object = new class() { 417 | public function do($array, $columnKey, $indexKey = null) 418 | { 419 | return array_column($array, $columnKey, $indexKey); 420 | } 421 | }; 422 | 423 | return static::new($object->do($this->val, $columnKey, $indexKey)); 424 | } 425 | 426 | return static::new(array_column($this->val, $columnKey, $indexKey)); 427 | } 428 | 429 | /** 430 | * column 的加强版,能获取多列,但不会获取对象的属性. 431 | * 432 | * @param array|null $columnKeys 包含指定键的数组 433 | * @param mixed|null $indexKey 作为返回数组的索引或键的列 434 | * 435 | * @throws AryKeyTypeException 436 | * 437 | * @return Ary 新实例 438 | */ 439 | public function select(array $columnKeys = null, $indexKey = null): self 440 | { 441 | if (!is_null($indexKey) && !static::isValidKey($indexKey)) { 442 | throw new AryKeyTypeException(); 443 | } 444 | 445 | if (empty($columnKeys)) { 446 | return static::new($this->val); 447 | } 448 | 449 | $array = []; 450 | foreach ($this->val as $rowKey => $rowVal) { 451 | if (is_array($rowVal) || $rowVal instanceof static) { 452 | $row = []; 453 | foreach ($rowVal as $colKey => $colVal) { 454 | if (in_array($colKey, $columnKeys)) { 455 | $row[$colKey] = $colVal; 456 | } 457 | } 458 | if (is_null($indexKey) || !isset($rowVal[$indexKey])) { 459 | $array[] = $row; 460 | } else { 461 | $array[$rowVal[$indexKey]] = $row; 462 | } 463 | } 464 | } 465 | 466 | return static::new($array); 467 | } 468 | 469 | /** 470 | * 返回与指定列条件相符的所有行组成的实例. 471 | * 472 | * @param string|int $columnKey 指定列键名 473 | * @param string $operator 比较符,包括: >、>=、==、===、<、<= 474 | * @param mixed $expected 比较值 475 | * 476 | * @throws AryKeyTypeException 477 | * 478 | * @return Ary 新的实例 479 | */ 480 | public function where($columnKey, string $operator, $expected): self 481 | { 482 | if (!static::isValidKey($columnKey)) { 483 | throw new AryKeyTypeException(); 484 | } 485 | 486 | $fn = function ($row) use ($columnKey, $operator, $expected) { 487 | if (!is_array($row) && !($row instanceof static)) { 488 | return false; 489 | } 490 | $val = $row[$columnKey]; 491 | switch ($operator) { 492 | case '>': 493 | return $val > $expected; 494 | case '>=': 495 | return $val >= $expected; 496 | case '==': 497 | return $val == $expected; 498 | case '===': 499 | return $val === $expected; 500 | case '<': 501 | return $val < $expected; 502 | case '<=': 503 | return $val <= $expected; 504 | default: 505 | return false; 506 | } 507 | }; 508 | 509 | $val = array_filter($this->val, $fn); 510 | 511 | return static::new($val); 512 | } 513 | 514 | /** 515 | * 统计实例数组中值的出现次数, 516 | * 返回一个键为原数组的值,值为原数组值出现的次数的新实例. 517 | * 518 | * @see http://php.net/manual/zh/function.array-count-values.php 519 | * 520 | * @return Ary 新实例 521 | */ 522 | public function countValues(): self 523 | { 524 | return static::new(array_count_values($this->val)); 525 | } 526 | 527 | /** 528 | * 交换实例数组中的键和值并返回新的实例. 529 | * 530 | * @see http://php.net/manual/zh/function.array-flip.php 531 | * 532 | * @return Ary 新实例 533 | */ 534 | public function flip(): self 535 | { 536 | return static::new(array_flip($this->val)); 537 | } 538 | 539 | /** 540 | * 检查实例数组中是否存在某个值 541 | * 542 | * @see http://php.net/manual/zh/function.in-array.php 543 | * 544 | * @param mixed $needle 要检查的值 545 | * @param bool|null $strict 是否严格比较 546 | * 547 | * @return bool 548 | */ 549 | public function exist($needle, bool $strict = null): bool 550 | { 551 | return in_array($needle, $this->val, static::default($strict, 'existStrict')); 552 | } 553 | 554 | /** 555 | * 检查实例数组里是否有指定的键名或索引. 556 | * 557 | * @see http://php.net/manual/zh/function.array-key-exists.php 558 | * 559 | * @param int|string $key 要检查的键 560 | * 561 | * @throws AryKeyTypeException 562 | * 563 | * @return bool 564 | */ 565 | public function existKey($key): bool 566 | { 567 | if (!static::isValidKey($key)) { 568 | throw new AryKeyTypeException(); 569 | } 570 | // isset 性能比 array_key_exists 高,但 isset 在数组成员的值为 null 时会返回 false 571 | // 所以利用 || 运算符的短路特性,仅当 isset 为 false 时使用 array_key_exists 判断是否存在该键 572 | return isset($this->val[$key]) || array_key_exists($key, $this->val); 573 | } 574 | 575 | /** 576 | * 检查实例数组存在指定键名的值,且值不为 null. 577 | * 578 | * @see http://php.net/manual/zh/function.isset.php 579 | * 580 | * @param int|string $key 581 | * 582 | * @throws AryKeyTypeException 583 | * 584 | * @return bool 585 | */ 586 | public function isSet($key): bool 587 | { 588 | if (!static::isValidKey($key)) { 589 | throw new AryKeyTypeException(); 590 | } 591 | 592 | return isset($this->val[$key]); 593 | } 594 | 595 | /** 596 | * 判断实例数组是否为关联数组. 597 | * 598 | * @return bool 599 | */ 600 | public function isAssoc() 601 | { 602 | $keys = array_keys($this->val); 603 | 604 | return array_keys($keys) !== $keys; 605 | } 606 | 607 | /** 608 | * 对实例数组的值进行排序. 609 | * 610 | * @see http://php.net/manual/zh/function.sort.php 611 | * @see http://php.net/manual/zh/function.rsort.php 612 | * @see http://php.net/manual/zh/function.asort.php 613 | * @see http://php.net/manual/zh/function.arsort.php 614 | * 615 | * @param bool|null $asc true为升序,false为降序 616 | * @param bool|null $preserveKeys true 则保持索引关联,false 则以数字索引重置 617 | * @param int|null $flag 排序类型标记,参见 PHP 手册 sort 函数 618 | * 619 | * @return Ary 原实例 620 | */ 621 | public function sort(bool $asc = null, bool $preserveKeys = null, int $flag = null): self 622 | { 623 | // 通过将 $asc 左移一位并或上 $preserveKeys 得出范围在 0b00 到 0b11 的 $status 624 | // 通过 $status 的值选择不同的排序函数 625 | $status = static::default($asc, 'sortAsc') << 1 | static::default($preserveKeys, 'sortPreserveKeys'); 626 | $flag = static::default($flag, 'sortFlag'); 627 | 628 | switch ($status) { 629 | // $asc = false, $preserveKeys = false 630 | case 0b00: 631 | rsort($this->val, $flag); 632 | break; 633 | // $asc = false, $preserveKeys = true 634 | case 0b01: 635 | arsort($this->val, $flag); 636 | break; 637 | // $asc = true, $preserveKeys = false 638 | case 0b10: 639 | sort($this->val, $flag); 640 | break; 641 | // $asc = true, $preserveKeys = true 642 | case 0b11: 643 | asort($this->val, $flag); 644 | break; 645 | } 646 | 647 | return $this; 648 | } 649 | 650 | /** 651 | * 使用用户自定义的比较函数对数组中的值进行排序. 652 | * 653 | * @see http://php.net/manual/zh/function.usort.php 654 | * @see http://php.net/manual/zh/function.uasort.php 655 | * 656 | * @param callable $fn 比较函数 657 | * @param bool|null $preserveKeys true 则保持索引关联,false 则以数字索引重置 658 | * 659 | * @return Ary 原实例 660 | */ 661 | public function userSort(callable $fn, bool $preserveKeys = null): self 662 | { 663 | if (static::default($preserveKeys, 'userSortPreserveKeys')) { 664 | uasort($this->val, $fn); 665 | } else { 666 | usort($this->val, $fn); 667 | } 668 | 669 | return $this; 670 | } 671 | 672 | /** 673 | * 用“自然排序”算法对实例数组的值进行排序. 674 | * 675 | * @see http://php.net/manual/zh/function.natsort.php 676 | * @see http://php.net/manual/zh/function.natcasesort.php 677 | * 678 | * @param bool|null $caseSensitive 大小写敏感 679 | * 680 | * @return Ary 原实例 681 | */ 682 | public function natSort(bool $caseSensitive = null): self 683 | { 684 | if (static::default($caseSensitive, 'natSortCaseSensitive')) { 685 | natsort($this->val); 686 | } else { 687 | natcasesort($this->val); 688 | } 689 | 690 | return $this; 691 | } 692 | 693 | /** 694 | * 对实例数组按照键名排序. 695 | * 696 | * @see http://php.net/manual/zh/function.ksort.php 697 | * 698 | * @param bool|null $asc true 为升序,false 为降序 699 | * @param int|null $flag 排序类型标记,参见 PHP 手册 sort 函数 700 | * 701 | * @return Ary 原实例 702 | */ 703 | public function keySort(bool $asc = null, int $flag = null): self 704 | { 705 | if (static::default($asc, 'keySortAsc')) { 706 | ksort($this->val, static::default($flag, 'keySortFlag')); 707 | } else { 708 | krsort($this->val, static::default($flag, 'keySortFlag')); 709 | } 710 | 711 | return $this; 712 | } 713 | 714 | /** 715 | * 使用用户自定义的比较函数对数组中的键名进行排序. 716 | * 717 | * @see http://php.net/manual/zh/function.uksort.php 718 | * 719 | * @param callable $fn 比较函数 720 | * 721 | * @return Ary 原实例 722 | */ 723 | public function userKeySort(callable $fn): self 724 | { 725 | uksort($this->val, $fn); 726 | 727 | return $this; 728 | } 729 | 730 | /** 731 | * 返回实例数组中的最大值 732 | * 733 | * @return mixed 734 | */ 735 | public function max() 736 | { 737 | return static::new($this->val)->sort(false, false, SORT_REGULAR)->first(); 738 | } 739 | 740 | /** 741 | * 返回实例数组中的最小值 742 | * 743 | * @return mixed 744 | */ 745 | public function min() 746 | { 747 | return static::new($this->val)->sort(true, false, SORT_REGULAR)->first(); 748 | } 749 | 750 | /** 751 | * 返回实例数组中的最大值的键名. 752 | * 753 | * @return mixed 754 | */ 755 | public function maxKey() 756 | { 757 | return static::new($this->val)->sort(false, true, SORT_REGULAR)->firstKey(); 758 | } 759 | 760 | /** 761 | * 返回实例数组中的最小值的键名. 762 | * 763 | * @return mixed 764 | */ 765 | public function minKey() 766 | { 767 | return static::new($this->val)->sort(true, true, SORT_REGULAR)->firstKey(); 768 | } 769 | 770 | /** 771 | * 打乱实例数组顺序. 772 | * 773 | * @see http://php.net/manual/zh/function.shuffle.php 774 | * 775 | * @return Ary 原实例 776 | */ 777 | public function shuffle(): self 778 | { 779 | shuffle($this->val); 780 | 781 | return $this; 782 | } 783 | 784 | /** 785 | * 移除实例数组中重复的值 786 | * 787 | * @see http://php.net/manual/zh/function.array-unique.php 788 | * 789 | * @param int|null $flag 排序类型标记 790 | * 791 | * @return Ary 新实例 792 | */ 793 | public function unique(int $flag = null): self 794 | { 795 | $val = array_unique($this->val, static::default($flag, 'uniqueFlag')); 796 | 797 | return static::new($val); 798 | } 799 | 800 | /** 801 | * 返回单元顺序相反的实例数组. 802 | * 803 | * @see http://php.net/manual/zh/function.array-reverse.php 804 | * 805 | * @return Ary 新实例 806 | */ 807 | public function reverse(): self 808 | { 809 | $val = array_reverse($this->val); 810 | 811 | return static::new($val); 812 | } 813 | 814 | /** 815 | * 移除实例数组中指定键名的值 816 | * 817 | * @param array ...$keys 指定键名 818 | * 819 | * @return Ary 新实例 820 | */ 821 | public function except(...$keys): self 822 | { 823 | $val = array_diff_key($this->val, array_flip($keys)); 824 | 825 | return static::new($val); 826 | } 827 | 828 | /** 829 | * 向实例数组的末尾插入元素(入栈). 830 | * 831 | * @see http://php.net/manual/zh/function.array-push.php 832 | * 833 | * @param array ...$elements 插入的元素,可为任意多个 834 | * 835 | * @return Ary 原实例 836 | */ 837 | public function push(...$elements): self 838 | { 839 | array_push($this->val, ...$elements); 840 | 841 | return $this; 842 | } 843 | 844 | /** 845 | * 从实例数组中的末尾弹出一个元素(出栈). 846 | * 847 | * @see http://php.net/manual/zh/function.array-pop.php 848 | * 849 | * @param bool|null $getElement true 则返回元素值,false 则返回原实例 850 | * 851 | * @return Ary|mixed 原实例或元素值 852 | */ 853 | public function pop(bool $getElement = null) 854 | { 855 | $element = array_pop($this->val); 856 | if (static::default($getElement, 'popGetElement')) { 857 | return $element; 858 | } 859 | 860 | return $this; 861 | } 862 | 863 | /** 864 | * 向实例数组的开头插入元素. 865 | * 866 | * @see http://php.net/manual/zh/function.array-unshift.php 867 | * 868 | * @param array ...$elements 插入的元素可为任意多个 869 | * 870 | * @return Ary 原实例 871 | */ 872 | public function unShift(...$elements): self 873 | { 874 | array_unshift($this->val, ...$elements); 875 | 876 | return $this; 877 | } 878 | 879 | /** 880 | * 从实例数组中的开头弹出一个元素. 881 | * 882 | * @see http://php.net/manual/zh/function.array-shift.php 883 | * 884 | * @param bool|null $getElement true 则返回元素值,false 则返回原实例 885 | * 886 | * @return Ary|mixed 原实例或元素值 887 | */ 888 | public function shift(bool $getElement = null) 889 | { 890 | $element = array_shift($this->val); 891 | if (static::default($getElement, 'shiftGetElement')) { 892 | return $element; 893 | } 894 | 895 | return $this; 896 | } 897 | 898 | /** 899 | * 向实例数组尾部追加一个实例数组. 900 | * 901 | * @see http://php.net/manual/zh/function.array-merge.php 902 | * 903 | * @param Ary $array 要追加的数组 904 | * @param bool|null $preserveValues true 则相同键名(包括数字索引)不会覆盖原实例数组,false 相同键名会覆盖原实例数组,数字索引会重新索引 905 | * 906 | * @return Ary 新实例 907 | */ 908 | public function append(self $array, bool $preserveValues = null): self 909 | { 910 | $preserveValues = static::default($preserveValues, 'appendPreserveValues'); 911 | if ($preserveValues) { 912 | $val = $this->val + $array->val(); 913 | } else { 914 | $val = array_merge($this->val, $array->val()); 915 | } 916 | 917 | return static::new($val); 918 | } 919 | 920 | /** 921 | * 在数组中搜索给定的值,如果成功则返回首个相应的键名. 922 | * 923 | * @see http://php.net/manual/zh/function.array-search.php 924 | * 925 | * @param mixed $needle 要搜索的值 926 | * @param bool|null $strict 是否采用严格比较 927 | * 928 | * @return false|int|string 搜索结果的键名或 false 929 | */ 930 | public function search($needle, bool $strict = null) 931 | { 932 | return array_search($needle, $this->val, static::default($strict, 'searchStrict')); 933 | } 934 | 935 | /** 936 | * 获取第一个指定值之前的元素所组成的实例数组. 937 | * 938 | * @param mixed $needle 指定值 939 | * @param bool|null $contain 结果是否包含指定值 940 | * @param bool|null $preserveKeys 为 true 则数字索引保持不变,false 则会重置数组的数字索引,字符串键名始终保持不变 941 | * 942 | * @return Ary 新实例 943 | */ 944 | public function before($needle, bool $contain = null, bool $preserveKeys = null): self 945 | { 946 | $key = $this->search($needle, true); 947 | // 如果数组中无此值,直接返回空实例数组 948 | if ($key === false) { 949 | return static::new([]); 950 | } 951 | // keys 将原数组的键名作为值,重新索引为新的数组 952 | // 通过 search 可以获取原数组的键名所对应的数字索引,此时的数字索引即为 slice 所需的长度 953 | $len = (int) $this->keys()->search($key, true); 954 | if (static::default($contain, 'beforeContain')) { 955 | $len++; 956 | } 957 | 958 | return $this->slice(0, $len, static::default($preserveKeys, 'beforePreserveKeys')); 959 | } 960 | 961 | /** 962 | * 获取第一个指定值之后的元素所组成的实例数组. 963 | * 964 | * @param mixed $needle 指定值 965 | * @param bool|null $contain 结果是否包含指定值 966 | * @param bool|null $preserveKeys 为 true 则数字索引保持不变,false 则会重置数组的数字索引,字符串键名始终保持不变 967 | * 968 | * @return Ary 新实例 969 | */ 970 | public function after($needle, bool $contain = null, bool $preserveKeys = null): self 971 | { 972 | $key = $this->search($needle, true); 973 | // 如果数组中无此值,直接返回空实例数组 974 | if ($key === false) { 975 | return static::new([]); 976 | } 977 | // 原理与 before 相同 978 | $offset = (int) $this->keys()->search($key, true); 979 | if (!static::default($contain, 'afterContain')) { 980 | $offset++; 981 | } 982 | 983 | return $this->slice($offset, null, static::default($preserveKeys, 'afterPreserveKeys')); 984 | } 985 | 986 | /** 987 | * 获取指定键名之前的元素所组成的实例数组. 988 | * 989 | * @param string|int $key 指定的键名 990 | * @param bool|null $contain 是否包含该键名的元素 991 | * 992 | * @return Ary 新实例 993 | */ 994 | public function beforeKey($key, bool $contain = null): self 995 | { 996 | if (!array_key_exists($key, $this->val)) { 997 | return static::new([]); 998 | } 999 | 1000 | $len = (int) $this->keys()->search($key, true); 1001 | if (static::default($contain, 'beforeKeyContain')) { 1002 | $len++; 1003 | } 1004 | 1005 | return $this->slice(0, $len, true); 1006 | } 1007 | 1008 | /** 1009 | * 获取指定键名之后的元素所组成的实例数组. 1010 | * 1011 | * @param string|int $key 指定的键名 1012 | * @param bool|null $contain 是否包含该键名的元素 1013 | * 1014 | * @return Ary 新实例 1015 | */ 1016 | public function afterKey($key, bool $contain = null) 1017 | { 1018 | if (!array_key_exists($key, $this->val)) { 1019 | return static::new([]); 1020 | } 1021 | 1022 | $offset = (int) $this->keys()->search($key, true); 1023 | if (!static::default($contain, 'afterKeyContain')) { 1024 | $offset++; 1025 | } 1026 | 1027 | return $this->slice($offset, null, true); 1028 | } 1029 | 1030 | /** 1031 | * 使用传递的实例数组替换原实例数组中的元素. 1032 | * 1033 | * @see http://php.net/manual/zh/function.array-replace.php 1034 | * 1035 | * @param Ary ...$arrays 替换的实例数组 1036 | * 1037 | * @return Ary 新实例 1038 | */ 1039 | public function replace(self ...$arrays): self 1040 | { 1041 | $ary = static::new($arrays); 1042 | 1043 | return static::new( 1044 | array_replace($this->val, ...$ary->toArray(false)) 1045 | ); 1046 | } 1047 | 1048 | /** 1049 | * 返回两实例数组的值的交集. 1050 | * 1051 | * @see http://php.net/manual/zh/function.array-intersect.php 1052 | * @see http://php.net/manual/zh/function.array-intersect-assoc.php 1053 | * 1054 | * @param Ary $ary 用于比较的实例 1055 | * @param bool|null $compKey 是否比较键名 1056 | * 1057 | * @return Ary 新实例 1058 | */ 1059 | public function intersect(self $ary, bool $compKey = null): self 1060 | { 1061 | if (static::default($compKey, 'intersectCompKey')) { 1062 | $val = array_intersect_assoc($this->val, $ary->val()); 1063 | } else { 1064 | $val = array_intersect($this->val, $ary->val()); 1065 | } 1066 | 1067 | return static::new($val); 1068 | } 1069 | 1070 | /** 1071 | * 返回两实例数组的值的差集. 1072 | * 1073 | * @see http://php.net/manual/zh/function.array-diff.php 1074 | * @see http://php.net/manual/zh/function.array-diff-assoc.php 1075 | * 1076 | * @param Ary $ary 用于比较的实例 1077 | * @param bool|null $compKey 是否比较键名 1078 | * 1079 | * @return Ary 新实例 1080 | */ 1081 | public function diff(self $ary, bool $compKey = null): self 1082 | { 1083 | if (static::default($compKey, 'diffCompKey')) { 1084 | $val = array_diff_assoc($this->val, $ary->val()); 1085 | } else { 1086 | $val = array_diff($this->val, $ary->val()); 1087 | } 1088 | 1089 | return static::new($val); 1090 | } 1091 | 1092 | /** 1093 | * 使用键名比较计算实例数组的交集. 1094 | * 1095 | * @see http://php.net/manual/zh/function.array-intersect-key.php 1096 | * 1097 | * @param Ary $ary 用于比较的实例 1098 | * 1099 | * @return Ary 新实例 1100 | */ 1101 | public function intersectKey(self $ary): self 1102 | { 1103 | $val = array_intersect_key($this->val, $ary->val()); 1104 | 1105 | return static::new($val); 1106 | } 1107 | 1108 | /** 1109 | * 使用键名比较计算实例数组的差集. 1110 | * 1111 | * @see http://php.net/manual/zh/function.array-diff-key.php 1112 | * 1113 | * @param Ary $ary 用于比较的实例 1114 | * 1115 | * @return Ary 新实例 1116 | */ 1117 | public function diffKey(self $ary): self 1118 | { 1119 | $val = array_diff_key($this->val, $ary->val()); 1120 | 1121 | return static::new($val); 1122 | } 1123 | 1124 | /** 1125 | * 清除实例数组中所有等值为 false 的元素(包括: null false 0 '' []) 1126 | * 警告:空的 Ary 实例并不会清除. 1127 | * 1128 | * @see http://php.net/manual/zh/function.array-filter.php 1129 | * 1130 | * @return Ary 原实例 1131 | */ 1132 | public function clean(): self 1133 | { 1134 | $this->val = array_filter($this->val); 1135 | 1136 | return $this; 1137 | } 1138 | 1139 | /** 1140 | * 将实例数组连接为字符串. 1141 | * 1142 | * @see http://php.net/manual/zh/function.implode.php 1143 | * 1144 | * @param string $glue 元素间连接的字符串,默认为 '' 1145 | * 1146 | * @return string 1147 | */ 1148 | public function join(string $glue = ''): string 1149 | { 1150 | // 当 val 数组中存在 Ary 实例时,其魔术方法 __toString 会同样调用 join 方法,并以 $default 中 joinGlue 的值为 $glue 1151 | // 所以调用 join 方法时需要将 $default 中 joinGlue 的值设置为当前实参 $glue, 并在调用结束后恢复原值 1152 | $oldGlue = static::default('joinGlue'); 1153 | static::setDefault(['joinGlue' => $glue]); 1154 | $str = implode($glue, $this->val); 1155 | static::setDefault(['joinGlue' => $oldGlue]); 1156 | 1157 | return $str; 1158 | } 1159 | 1160 | /** 1161 | * 使用用户自定义函数对实例数组中的每个元素做回调处理. 1162 | * 1163 | * @see http://php.net/manual/zh/function.array-walk.php 1164 | * 1165 | * @param callable $fn 使用的回调函数 1166 | * @param mixed $userData 用户数据,回调函数的第三个参数 1167 | * @param bool $recursive 是否递归实例数组 1168 | * 1169 | * @return Ary 原实例 1170 | */ 1171 | public function each(callable $fn, $userData = null, bool $recursive = null): self 1172 | { 1173 | if (static::default($recursive, 'eachRecursive')) { 1174 | $array = $this->toArray(true); 1175 | array_walk_recursive($array, $fn, $userData); 1176 | } else { 1177 | array_walk($this->val, $fn, $userData); 1178 | } 1179 | 1180 | return $this; 1181 | } 1182 | 1183 | /** 1184 | * 为实例数组的每个元素应用回调函数. 1185 | * 1186 | * @see http://php.net/manual/zh/function.array-map.php 1187 | * 1188 | * @param callable $fn 使用的回调函数 1189 | * 1190 | * @return Ary 新实例 1191 | */ 1192 | public function map(callable $fn): self 1193 | { 1194 | $val = array_map($fn, $this->val); 1195 | 1196 | return static::new($val); 1197 | } 1198 | 1199 | /** 1200 | * 用回调函数过滤实例数组中的元素. 1201 | * 1202 | * @see http://php.net/manual/zh/function.array-filter.php 1203 | * 1204 | * @param callable $fn 使用的回调函数 1205 | * @param int|null $flag 决定callback接收的参数形式 1206 | * 1207 | * @return Ary 新实例 1208 | */ 1209 | public function filter(callable $fn, int $flag = null): self 1210 | { 1211 | $val = array_filter($this->val, $fn, static::default($flag, 'filterFlag')); 1212 | 1213 | return static::new($val); 1214 | } 1215 | 1216 | /** 1217 | * 用回调函数迭代地将实例数组简化为单一的值 1218 | * 1219 | * @see http://php.net/manual/zh/function.array-reduce.php 1220 | * 1221 | * @param callable $fn 使用的回调函数 1222 | * @param mixed|null $initial 回调函数初始值 1223 | * 1224 | * @return mixed 1225 | */ 1226 | public function reduce(callable $fn, $initial = null) 1227 | { 1228 | return array_reduce($this->val, $fn, $initial); 1229 | } 1230 | 1231 | /** 1232 | * 将一个多维数组扁平化为一个一维数组. 1233 | * 1234 | * @param bool|null $preserveKeys true 保持值不为数组或 Ary 对象的字符串键名,重置数字索引,false 则删除所有键名,重新以数字索引 1235 | * 1236 | * @return Ary 新实例 1237 | */ 1238 | public function flat(bool $preserveKeys = null) 1239 | { 1240 | $array = []; 1241 | $fn = function ($val, $key, $preserveKeys) use (&$array) { 1242 | if ($preserveKeys && is_string($key)) { 1243 | $array[$key] = $val; 1244 | } else { 1245 | $array[] = $val; 1246 | } 1247 | }; 1248 | 1249 | $this->each($fn, static::default($preserveKeys, 'flatPreserveKeys'), true); 1250 | 1251 | return static::new($array); 1252 | } 1253 | 1254 | /** 1255 | * 以指定长度将一个值填充进实例数组. 1256 | * 1257 | * @see http://php.net/manual/zh/function.array-pad.php 1258 | * 1259 | * @param int $size 新实例数组的长度 1260 | * @param mixed $element 填充的值 1261 | * 1262 | * @return Ary 新实例 1263 | */ 1264 | public function pad(int $size, $element): self 1265 | { 1266 | $val = array_pad($this->val, $size, $element); 1267 | 1268 | return static::new($val); 1269 | } 1270 | 1271 | /** 1272 | * 返回用 $val 填充当前所有键的新实例. 1273 | * 1274 | * @param mixed $val 填充值 1275 | * 1276 | * @return Ary 新实例 1277 | */ 1278 | public function fill($val): self 1279 | { 1280 | return static::new(array_fill_keys($this->keys()->val(), $val)); 1281 | } 1282 | 1283 | /** 1284 | * 判断实例数组是否为空. 1285 | * 1286 | * @see http://php.net/manual/zh/function.empty.php 1287 | * 1288 | * @return bool 1289 | */ 1290 | public function empty(): bool 1291 | { 1292 | return empty($this->val); 1293 | } 1294 | 1295 | /** 1296 | * 计算实例数组中所有值的乘积. 1297 | * 1298 | * @see http://php.net/manual/zh/function.array-product.php 1299 | * 1300 | * @return float 1301 | */ 1302 | public function product(): float 1303 | { 1304 | return array_product($this->val); 1305 | } 1306 | 1307 | /** 1308 | * 确认实例数组中的值全为 true,请确保实例数组中只包含:布尔类型的数据,否则将产生未预料的结果. 1309 | * 1310 | * @return bool 1311 | */ 1312 | public function allTrue(): bool 1313 | { 1314 | return (bool) $this->product(); 1315 | } 1316 | 1317 | /** 1318 | * 对实例数组中所有值求和. 1319 | * 1320 | * @see http://php.net/manual/zh/function.array-sum.php 1321 | * 1322 | * @return float 1323 | */ 1324 | public function sum(): float 1325 | { 1326 | return array_sum($this->val); 1327 | } 1328 | 1329 | /** 1330 | * 从实例数组中随机取出一个或多个元素组成新的实例,保持索引关联. 1331 | * 1332 | * @see http://php.net/manual/zh/function.array-rand.php 1333 | * 1334 | * @param int $num 取出数量 1335 | * 1336 | * @throws AryOutOfRangeException 1337 | * 1338 | * @return Ary 新实例 1339 | */ 1340 | public function rand(int $num): self 1341 | { 1342 | if ($num > count($this->val)) { 1343 | throw new AryOutOfRangeException(); 1344 | } 1345 | 1346 | if ($num === 1) { 1347 | $key = array_rand($this->val, $num); 1348 | $val = [$key => $this->val[$key]]; 1349 | } else { 1350 | $keys = array_rand($this->val, $num); 1351 | $val = array_intersect_key($this->val, array_flip($keys)); 1352 | } 1353 | 1354 | return static::new($val); 1355 | } 1356 | 1357 | /** 1358 | * 从实例数组中随机取出一个元素的值 1359 | * 1360 | * @throws AryOutOfRangeException 1361 | * 1362 | * @return mixed 1363 | */ 1364 | public function randVal() 1365 | { 1366 | if (empty($this->val)) { 1367 | throw new AryOutOfRangeException(); 1368 | } 1369 | 1370 | return $this->val[array_rand($this->val, 1)]; 1371 | } 1372 | 1373 | /** 1374 | * 从实例数组中随机取出一个元素的键名. 1375 | * 1376 | * @throws AryOutOfRangeException 1377 | * 1378 | * @return mixed 1379 | */ 1380 | public function randKey() 1381 | { 1382 | if (empty($this->val)) { 1383 | throw new AryOutOfRangeException(); 1384 | } 1385 | 1386 | return array_rand($this->val, 1); 1387 | } 1388 | 1389 | /** 1390 | * 将实例数组转成普通数组. 1391 | * 1392 | * @param bool|null $recursive true 则递归调用,false 则只会将当前实例数组中的 Ary 元素转为数组后便不再递归 1393 | * 1394 | * @return array 1395 | */ 1396 | public function toArray(bool $recursive = null): array 1397 | { 1398 | $array = []; 1399 | if (static::default($recursive, 'toArrayRecursive')) { 1400 | $array = static::valToArray($this->val); 1401 | } else { 1402 | foreach ($this->val as $k => $v) { 1403 | if ($v instanceof static) { 1404 | $array[$k] = $v->val(); 1405 | } else { 1406 | $array[$k] = $v; 1407 | } 1408 | } 1409 | } 1410 | 1411 | return $array; 1412 | } 1413 | 1414 | protected static function valToArray(array $val) 1415 | { 1416 | $array = $val; 1417 | foreach ($array as $k => $v) { 1418 | if (is_array($v)) { 1419 | $array[$k] = static::valToArray($v); 1420 | } elseif ($v instanceof static) { 1421 | $array[$k] = static::valToArray($v->val()); 1422 | } else { 1423 | $array[$k] = $v; 1424 | } 1425 | } 1426 | 1427 | return $array; 1428 | } 1429 | 1430 | /** 1431 | * 将实例数组转为 json 字符串. 1432 | * 1433 | * @param int|null $options 格式选项 1434 | * @param int|null $depth 最大嵌套深度 1435 | * 1436 | * @return string 1437 | */ 1438 | public function toJson(int $options = null, int $depth = null): string 1439 | { 1440 | return json_encode( 1441 | $this->val, 1442 | static::default($options, 'toJsonOptions'), 1443 | static::default($depth, 'toJsonDepth') 1444 | ); 1445 | } 1446 | 1447 | public function getIterator(): ArrayIterator 1448 | { 1449 | return new ArrayIterator($this->val); 1450 | } 1451 | 1452 | public function offsetExists($offset) 1453 | { 1454 | return isset($this->val[$offset]); 1455 | } 1456 | 1457 | public function offsetGet($offset) 1458 | { 1459 | return isset($this->val[$offset]) ? $this->val[$offset] : null; 1460 | } 1461 | 1462 | public function offsetSet($offset, $value) 1463 | { 1464 | if (is_null($offset)) { 1465 | $this->val[] = $value; 1466 | } else { 1467 | $this->val[$offset] = $value; 1468 | } 1469 | } 1470 | 1471 | public function offsetUnset($offset) 1472 | { 1473 | unset($this->val[$offset]); 1474 | } 1475 | 1476 | public function count(): int 1477 | { 1478 | return count($this->val); 1479 | } 1480 | 1481 | public function jsonSerialize(): array 1482 | { 1483 | return $this->val; 1484 | } 1485 | 1486 | public function __toString(): string 1487 | { 1488 | return $this->join((string) static::default('joinGlue')); 1489 | } 1490 | 1491 | public function __isset($key) : bool 1492 | { 1493 | return isset($this->val[$key]); 1494 | } 1495 | 1496 | public function __get($key) 1497 | { 1498 | return $this->val[$key]; 1499 | } 1500 | 1501 | public function __set($key, $val) 1502 | { 1503 | $this->val[$key] = $val; 1504 | } 1505 | 1506 | public function __unset($key) 1507 | { 1508 | unset($this->val[$key]); 1509 | } 1510 | 1511 | /** 1512 | * 返回一个新实例. 1513 | * 1514 | * @param array $array 实例包含的数组 1515 | * 1516 | * @return Ary 新实例 1517 | */ 1518 | public static function new(array $array = []): self 1519 | { 1520 | return new static($array); 1521 | } 1522 | 1523 | /** 1524 | * 设置类方法的默认值 1525 | * 1526 | * @param array $default 1527 | * 1528 | * @return bool 1529 | */ 1530 | public static function setDefault(array $default): bool 1531 | { 1532 | foreach ($default as $key => $val) { 1533 | static::$default[$key] = $val; 1534 | } 1535 | 1536 | return true; 1537 | } 1538 | 1539 | /** 1540 | * 从 json 字符串创建一个新实例. 1541 | * 1542 | * @param string $json json 1543 | * @param int|null $depth 最大嵌套深度 1544 | * @param int|null $options 格式选项 1545 | * 1546 | * @return Ary 新实例 1547 | */ 1548 | public static function fromJson(string $json, int $depth = null, int $options = null): self 1549 | { 1550 | return static::new( 1551 | json_decode( 1552 | $json, 1553 | true, 1554 | static::default($depth, 'fromJsonDepth'), 1555 | static::default($options, 'fromJsonOptions') 1556 | ) 1557 | ); 1558 | } 1559 | 1560 | /** 1561 | * 创建一个新的实例数组,用第一个实例数组的值作为其键名,第二个实例数组的值作为其值 1562 | * 1563 | * @see http://php.net/manual/zh/function.array-combine.php 1564 | * 1565 | * @param Ary $key 作为键的实例数组 1566 | * @param Ary $val 作为值的实例数组 1567 | * 1568 | * @return Ary 新实例 1569 | */ 1570 | public static function combine(self $key, self $val): self 1571 | { 1572 | return static::new( 1573 | array_combine($key->val(), $val->val()) 1574 | ); 1575 | } 1576 | 1577 | /** 1578 | * 创建指定长度的实例数组并填充一个值 1579 | * 1580 | * @param int $startIndex 实例数组第一个索引 1581 | * @param int $num 实例数组长度 1582 | * @param mixed $val 填充值 1583 | * 1584 | * @return Ary 新实例 1585 | */ 1586 | public static function newFill(int $startIndex, int $num, $val): self 1587 | { 1588 | return static::new(array_fill($startIndex, $num, $val)); 1589 | } 1590 | 1591 | /** 1592 | * 判断 $val 是否能以数组的方式访问. 1593 | * 1594 | * @param mixed $val 1595 | * 1596 | * @return bool 1597 | */ 1598 | public static function accessible($val): bool 1599 | { 1600 | return is_array($val) || $val instanceof ArrayAccess; 1601 | } 1602 | 1603 | /** 1604 | * 判断 $key 是否能作键名. 1605 | * 1606 | * @param mixed $key 1607 | * 1608 | * @return bool 1609 | */ 1610 | public static function isValidKey($key): bool 1611 | { 1612 | return is_string($key) || is_int($key); 1613 | } 1614 | 1615 | /** 1616 | * 若传入两个参数,第一个参数为 null 时,则返回 $default 数组中以 $second 为键名的值 1617 | * 若第一个参数不为 null 则返回第一个参数的值 1618 | * 若只传入一个参数则直接返回 $default 数组中以 $first 为键名的值 1619 | * 1620 | * @param mixed $first 1621 | * @param string|null $second 1622 | * 1623 | * @return mixed 1624 | */ 1625 | protected static function default($first, string $second = null) 1626 | { 1627 | if (func_num_args() < 2) { 1628 | return static::$default[$first] ?? null; 1629 | } 1630 | 1631 | if (is_null($first)) { 1632 | return static::$default[$second] ?? null; 1633 | } 1634 | 1635 | return $first; 1636 | } 1637 | } 1638 | --------------------------------------------------------------------------------