├── .github └── workflows │ ├── ci.yml │ └── php.yml ├── .gitignore ├── LICENSE ├── README.md ├── composer.json ├── phpunit.xml.dist ├── src ├── Collection.php ├── contract │ ├── Arrayable.php │ └── Jsonable.php ├── helper.php └── helper │ ├── Arr.php │ ├── Macroable.php │ └── Str.php └── tests ├── ArrTest.php ├── CollectionTest.php ├── IsAssocTest.php ├── MergeDeepTest.php ├── StrTest.php └── TestCase.php /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | 10 | jobs: 11 | phpcs: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v2 15 | - name: Setup PHP environment 16 | uses: shivammathur/setup-php@v2 17 | - name: Install dependencies 18 | run: composer install 19 | - name: PHPCSFixer check 20 | run: composer check-style 21 | phpunit: 22 | strategy: 23 | matrix: 24 | php_version: [7.3, 7.4, 8.0] 25 | runs-on: ubuntu-latest 26 | steps: 27 | - uses: actions/checkout@v2 28 | - name: Setup PHP environment 29 | uses: shivammathur/setup-php@v2 30 | with: 31 | php-version: ${{ matrix.php_version }} 32 | coverage: xdebug 33 | - name: Install dependencies 34 | run: composer install 35 | - name: PHPUnit check 36 | run: ./vendor/bin/phpunit --coverage-text 37 | -------------------------------------------------------------------------------- /.github/workflows/php.yml: -------------------------------------------------------------------------------- 1 | name: PHP Composer 2 | 3 | on: 4 | push: 5 | branches: [ 3.0 ] 6 | pull_request: 7 | branches: [ 3.0 ] 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - uses: actions/checkout@v2 16 | 17 | - name: Validate composer.json and composer.lock 18 | run: composer validate --strict 19 | 20 | - name: Cache Composer packages 21 | id: composer-cache 22 | uses: actions/cache@v2 23 | with: 24 | path: vendor 25 | key: ${{ runner.os }}-php-${{ hashFiles('**/composer.lock') }} 26 | restore-keys: | 27 | ${{ runner.os }}-php- 28 | 29 | - name: Install dependencies 30 | run: composer install --prefer-dist --no-progress 31 | 32 | # Add a test script to composer.json, for instance: "test": "vendor/bin/phpunit" 33 | # Docs: https://getcomposer.org/doc/articles/scripts.md 34 | 35 | - name: Run test suite 36 | run: composer test 37 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor/ 2 | /.idea/ 3 | composer.lock 4 | .phpunit.result.cache -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # thinkphp6 常用的一些扩展类库 2 | 3 | 基于PHP7.1+ 4 | 5 | [![PHP Composer](https://github.com/larvatecn/think-helper/actions/workflows/php.yml/badge.svg)](https://github.com/larvatecn/think-helper/actions/workflows/php.yml) 6 | 7 | > 以下类库都在`\\think\\helper`命名空间下 8 | 9 | ## Str 10 | 11 | > 字符串操作 12 | 13 | ``` 14 | // 检查字符串中是否包含某些字符串 15 | Str::contains($haystack, $needles) 16 | 17 | // 检查字符串是否以某些字符串结尾 18 | Str::endsWith($haystack, $needles) 19 | 20 | // 获取指定长度的随机字母数字组合的字符串 21 | Str::random($length = 16) 22 | 23 | // 字符串转小写 24 | Str::lower($value) 25 | 26 | // 字符串转大写 27 | Str::upper($value) 28 | 29 | // 获取字符串的长度 30 | Str::length($value) 31 | 32 | // 截取字符串 33 | Str::substr($string, $start, $length = null) 34 | 35 | ``` -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "topthink/think-helper", 3 | "description": "The ThinkPHP6 Helper Package", 4 | "license": "Apache-2.0", 5 | "authors": [ 6 | { 7 | "name": "yunwuxin", 8 | "email": "448901948@qq.com" 9 | } 10 | ], 11 | "require": { 12 | "php": ">=7.1.0" 13 | }, 14 | "require-dev": { 15 | "phpunit/phpunit": "^9.5" 16 | }, 17 | "autoload": { 18 | "psr-4": { 19 | "think\\": "src" 20 | }, 21 | "files": [ 22 | "src/helper.php" 23 | ] 24 | }, 25 | "autoload-dev": { 26 | "psr-4": { 27 | "Tests\\": "tests" 28 | } 29 | }, 30 | "scripts": { 31 | "test": "./vendor/bin/phpunit --colors" 32 | }, 33 | "scripts-descriptions": { 34 | "test": "Run all tests." 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | ./tests/ 10 | 11 | 12 | 13 | 14 | ./src 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/Collection.php: -------------------------------------------------------------------------------- 1 | 10 | // +---------------------------------------------------------------------- 11 | declare (strict_types = 1); 12 | 13 | namespace think; 14 | 15 | use ArrayAccess; 16 | use ArrayIterator; 17 | use Countable; 18 | use IteratorAggregate; 19 | use JsonSerializable; 20 | use think\contract\Arrayable; 21 | use think\contract\Jsonable; 22 | use think\helper\Arr; 23 | use Traversable; 24 | 25 | /** 26 | * 数据集管理类 27 | * 28 | * @template TKey of array-key 29 | * @template-covariant TValue 30 | * 31 | * @implements ArrayAccess 32 | * @implements IteratorAggregate 33 | */ 34 | class Collection implements ArrayAccess, Countable, IteratorAggregate, JsonSerializable, Arrayable, Jsonable 35 | { 36 | /** 37 | * 数据集数据 38 | * @var array 39 | */ 40 | protected $items = []; 41 | 42 | /** 43 | * 构造函数 44 | * @param iterable|Collection $items 数据 45 | */ 46 | public function __construct($items = []) 47 | { 48 | $this->items = $this->convertToArray($items); 49 | } 50 | 51 | /** 52 | * @param iterable|Collection $items 53 | * @return static 54 | */ 55 | public static function make($items = []) 56 | { 57 | return new static($items); 58 | } 59 | 60 | /** 61 | * 是否为空 62 | * @return bool 63 | */ 64 | public function isEmpty(): bool 65 | { 66 | return empty($this->items); 67 | } 68 | 69 | public function toArray(): array 70 | { 71 | return array_map(function ($value) { 72 | return $value instanceof Arrayable ? $value->toArray() : $value; 73 | }, $this->items); 74 | } 75 | 76 | /** 77 | * @return array 78 | */ 79 | public function all(): array 80 | { 81 | return $this->items; 82 | } 83 | 84 | /** 85 | * 合并数组 86 | * 87 | * @param mixed $items 数据 88 | * @return static 89 | */ 90 | public function merge($items) 91 | { 92 | return new static(array_merge($this->items, $this->convertToArray($items))); 93 | } 94 | 95 | /** 96 | * 按指定键整理数据 97 | * 98 | * @param mixed $items 数据 99 | * @param string|null $indexKey 键名 100 | * @return array 101 | */ 102 | public function dictionary($items = null, ?string &$indexKey = null) 103 | { 104 | if ($items instanceof self) { 105 | $items = $items->all(); 106 | } 107 | 108 | $items = is_null($items) ? $this->items : $items; 109 | 110 | if ($items && empty($indexKey)) { 111 | $indexKey = is_array($items[0]) ? 'id' : $items[0]->getPk(); 112 | } 113 | 114 | if (isset($indexKey) && is_string($indexKey)) { 115 | return array_column($items, null, $indexKey); 116 | } 117 | 118 | return $items; 119 | } 120 | 121 | /** 122 | * 比较数组,返回差集 123 | * 124 | * @param mixed $items 数据 125 | * @param string|null $indexKey 指定比较的键名 126 | * @return static 127 | */ 128 | public function diff($items, ?string $indexKey = null) 129 | { 130 | if ($this->isEmpty() || is_scalar($this->items[0])) { 131 | return new static(array_diff($this->items, $this->convertToArray($items))); 132 | } 133 | 134 | $diff = []; 135 | $dictionary = $this->dictionary($items, $indexKey); 136 | 137 | if (is_string($indexKey)) { 138 | foreach ($this->items as $item) { 139 | if (!isset($dictionary[$item[$indexKey]])) { 140 | $diff[] = $item; 141 | } 142 | } 143 | } 144 | 145 | return new static($diff); 146 | } 147 | 148 | /** 149 | * 比较数组,返回交集 150 | * 151 | * @param mixed $items 数据 152 | * @param string|null $indexKey 指定比较的键名 153 | * @return static 154 | */ 155 | public function intersect($items, ?string $indexKey = null) 156 | { 157 | if ($this->isEmpty() || is_scalar($this->items[0])) { 158 | return new static(array_intersect($this->items, $this->convertToArray($items))); 159 | } 160 | 161 | $intersect = []; 162 | $dictionary = $this->dictionary($items, $indexKey); 163 | 164 | if (is_string($indexKey)) { 165 | foreach ($this->items as $item) { 166 | if (isset($dictionary[$item[$indexKey]])) { 167 | $intersect[] = $item; 168 | } 169 | } 170 | } 171 | 172 | return new static($intersect); 173 | } 174 | 175 | /** 176 | * 交换数组中的键和值 177 | * 178 | * @return static 179 | */ 180 | public function flip() 181 | { 182 | return new static(array_flip($this->items)); 183 | } 184 | 185 | /** 186 | * 返回数组中所有的键名 187 | * 188 | * @return static 189 | */ 190 | public function keys() 191 | { 192 | return new static(array_keys($this->items)); 193 | } 194 | 195 | /** 196 | * 返回数组中所有的值组成的新 Collection 实例 197 | * @return static 198 | */ 199 | public function values() 200 | { 201 | return new static(array_values($this->items)); 202 | } 203 | 204 | /** 205 | * 删除数组的最后一个元素(出栈) 206 | * 207 | * @return TValue 208 | */ 209 | public function pop() 210 | { 211 | return array_pop($this->items); 212 | } 213 | 214 | /** 215 | * 通过使用用户自定义函数,以字符串返回数组 216 | * 217 | * @param callable $callback 调用方法 218 | * @param mixed $initial 219 | * @return mixed 220 | */ 221 | public function reduce(callable $callback, $initial = null) 222 | { 223 | return array_reduce($this->items, $callback, $initial); 224 | } 225 | 226 | /** 227 | * 以相反的顺序返回数组。 228 | * 229 | * @return static 230 | */ 231 | public function reverse() 232 | { 233 | return new static(array_reverse($this->items)); 234 | } 235 | 236 | /** 237 | * 删除数组中首个元素,并返回被删除元素的值 238 | * 239 | * @return TValue 240 | */ 241 | public function shift() 242 | { 243 | return array_shift($this->items); 244 | } 245 | 246 | /** 247 | * 在数组结尾插入一个元素 248 | * 249 | * @param mixed $value 元素 250 | * @param string|null $key KEY 251 | * @return $this 252 | */ 253 | public function push($value, ?string $key = null) 254 | { 255 | if (is_null($key)) { 256 | $this->items[] = $value; 257 | } else { 258 | $this->items[$key] = $value; 259 | } 260 | 261 | return $this; 262 | } 263 | 264 | /** 265 | * 把一个数组分割为新的数组块. 266 | * 267 | * @param int $size 块大小 268 | * @param bool $preserveKeys 269 | * @return static 270 | */ 271 | public function chunk(int $size, bool $preserveKeys = false) 272 | { 273 | $chunks = []; 274 | 275 | foreach (array_chunk($this->items, $size, $preserveKeys) as $chunk) { 276 | $chunks[] = new static($chunk); 277 | } 278 | 279 | return new static($chunks); 280 | } 281 | 282 | /** 283 | * 在数组开头插入一个元素 284 | * 285 | * @param mixed $value 元素 286 | * @param string|null $key KEY 287 | * @return $this 288 | */ 289 | public function unshift($value, ?string $key = null) 290 | { 291 | if (is_null($key)) { 292 | array_unshift($this->items, $value); 293 | } else { 294 | $this->items = [$key => $value] + $this->items; 295 | } 296 | 297 | return $this; 298 | } 299 | 300 | /** 301 | * 给每个元素执行个回调 302 | * 303 | * 304 | * @param callable $callback 回调 305 | * @return $this 306 | */ 307 | public function each(callable $callback) 308 | { 309 | foreach ($this->items as $key => $item) { 310 | $result = $callback($item, $key); 311 | 312 | if (false === $result) { 313 | break; 314 | } elseif (!is_object($item)) { 315 | $this->items[$key] = $result; 316 | } 317 | } 318 | 319 | return $this; 320 | } 321 | 322 | /** 323 | * 用回调函数处理数组中的元素 324 | * 325 | * @param callable|null $callback 回调 326 | * @return static 327 | */ 328 | public function map(callable $callback) 329 | { 330 | return new static(array_map($callback, $this->items)); 331 | } 332 | 333 | /** 334 | * 用回调函数过滤数组中的元素 335 | * 336 | * @param callable|null $callback 回调 337 | * @return static 338 | */ 339 | public function filter(?callable $callback = null) 340 | { 341 | if ($callback) { 342 | return new static(array_filter($this->items, $callback)); 343 | } 344 | 345 | return new static(array_filter($this->items)); 346 | } 347 | 348 | /** 349 | * 根据字段条件过滤数组中的元素 350 | * 351 | * @param string $field 字段名 352 | * @param mixed $operator 操作符 353 | * @param mixed $value 数据 354 | * @return static 355 | */ 356 | public function where(string $field, $operator, $value = null) 357 | { 358 | if (is_null($value)) { 359 | $value = $operator; 360 | $operator = '='; 361 | } 362 | 363 | return $this->filter(function ($data) use ($field, $operator, $value) { 364 | if (strpos($field, '.')) { 365 | [$field, $relation] = explode('.', $field); 366 | 367 | $result = $data[$field][$relation] ?? null; 368 | } else { 369 | $result = $data[$field] ?? null; 370 | } 371 | 372 | switch (strtolower($operator)) { 373 | case '===': 374 | return $result === $value; 375 | case '!==': 376 | return $result !== $value; 377 | case '!=': 378 | case '<>': 379 | return $result != $value; 380 | case '>': 381 | return $result > $value; 382 | case '>=': 383 | return $result >= $value; 384 | case '<': 385 | return $result < $value; 386 | case '<=': 387 | return $result <= $value; 388 | case 'like': 389 | return is_string($result) && false !== strpos($result, $value); 390 | case 'not like': 391 | return is_string($result) && false === strpos($result, $value); 392 | case 'in': 393 | return is_scalar($result) && in_array($result, $value, true); 394 | case 'not in': 395 | return is_scalar($result) && !in_array($result, $value, true); 396 | case 'between': 397 | [$min, $max] = is_string($value) ? explode(',', $value) : $value; 398 | return is_scalar($result) && $result >= $min && $result <= $max; 399 | case 'not between': 400 | [$min, $max] = is_string($value) ? explode(',', $value) : $value; 401 | return is_scalar($result) && $result > $max || $result < $min; 402 | case '==': 403 | case '=': 404 | default: 405 | return $result == $value; 406 | } 407 | }); 408 | } 409 | 410 | /** 411 | * LIKE过滤 412 | * 413 | * @param string $field 字段名 414 | * @param string $value 数据 415 | * @return static 416 | */ 417 | public function whereLike(string $field, string $value) 418 | { 419 | return $this->where($field, 'like', $value); 420 | } 421 | 422 | /** 423 | * NOT LIKE过滤 424 | * 425 | * @param string $field 字段名 426 | * @param string $value 数据 427 | * @return static 428 | */ 429 | public function whereNotLike(string $field, string $value) 430 | { 431 | return $this->where($field, 'not like', $value); 432 | } 433 | 434 | /** 435 | * IN过滤 436 | * 437 | * @param string $field 字段名 438 | * @param array $value 数据 439 | * @return static 440 | */ 441 | public function whereIn(string $field, array $value) 442 | { 443 | return $this->where($field, 'in', $value); 444 | } 445 | 446 | /** 447 | * NOT IN过滤 448 | * 449 | * @param string $field 字段名 450 | * @param array $value 数据 451 | * @return static 452 | */ 453 | public function whereNotIn(string $field, array $value) 454 | { 455 | return $this->where($field, 'not in', $value); 456 | } 457 | 458 | /** 459 | * BETWEEN 过滤 460 | * 461 | * @param string $field 字段名 462 | * @param mixed $value 数据 463 | * @return static 464 | */ 465 | public function whereBetween(string $field, $value) 466 | { 467 | return $this->where($field, 'between', $value); 468 | } 469 | 470 | /** 471 | * NOT BETWEEN 过滤 472 | * 473 | * @param string $field 字段名 474 | * @param mixed $value 数据 475 | * @return static 476 | */ 477 | public function whereNotBetween(string $field, $value) 478 | { 479 | return $this->where($field, 'not between', $value); 480 | } 481 | 482 | /** 483 | * 返回数据中指定的一列 484 | * 485 | * @param string|null $columnKey 键名 486 | * @param string|null $indexKey 作为索引值的列 487 | * @return array 488 | */ 489 | public function column(?string $columnKey, ?string $indexKey = null) 490 | { 491 | return array_column($this->items, $columnKey, $indexKey); 492 | } 493 | 494 | /** 495 | * 对数组排序 496 | * 497 | * @param callable|null $callback 回调 498 | * @return static 499 | */ 500 | public function sort(?callable $callback = null) 501 | { 502 | $items = $this->items; 503 | 504 | $callback = $callback ?: function ($a, $b) { 505 | return $a == $b ? 0 : (($a < $b) ? -1 : 1); 506 | }; 507 | 508 | uasort($items, $callback); 509 | 510 | return new static($items); 511 | } 512 | 513 | /** 514 | * 指定字段排序 515 | * 516 | * @param string $field 排序字段 517 | * @param string $order 排序 518 | * @return $this 519 | */ 520 | public function order(string $field, string $order = 'asc') 521 | { 522 | return $this->sort(function ($a, $b) use ($field, $order) { 523 | $fieldA = $a[$field] ?? null; 524 | $fieldB = $b[$field] ?? null; 525 | 526 | return 'desc' == strtolower($order) ? ($fieldB <=> $fieldA) : ($fieldA <=> $fieldB); 527 | }); 528 | } 529 | 530 | /** 531 | * 将数组打乱 532 | * 533 | * @return static 534 | */ 535 | public function shuffle() 536 | { 537 | $items = $this->items; 538 | 539 | shuffle($items); 540 | 541 | return new static($items); 542 | } 543 | 544 | /** 545 | * 获取第一个单元数据 546 | * 547 | * @param callable|null $callback 548 | * @param null $default 549 | * @return TValue 550 | */ 551 | public function first(?callable $callback = null, $default = null) 552 | { 553 | return Arr::first($this->items, $callback, $default); 554 | } 555 | 556 | /** 557 | * 获取最后一个单元数据 558 | * 559 | * @param callable|null $callback 560 | * @param null $default 561 | * @return TValue 562 | */ 563 | public function last(?callable $callback = null, $default = null) 564 | { 565 | return Arr::last($this->items, $callback, $default); 566 | } 567 | 568 | /** 569 | * 截取数组 570 | * 571 | * @param int $offset 起始位置 572 | * @param int|null $length 截取长度 573 | * @param bool $preserveKeys preserveKeys 574 | * @return static 575 | */ 576 | public function slice(int $offset, ?int $length = null, bool $preserveKeys = false) 577 | { 578 | return new static(array_slice($this->items, $offset, $length, $preserveKeys)); 579 | } 580 | 581 | /** 582 | * @param TKey $key 583 | * @return bool 584 | */ 585 | #[\ReturnTypeWillChange] 586 | public function offsetExists($offset) : bool 587 | { 588 | return array_key_exists($offset, $this->items); 589 | } 590 | 591 | /** 592 | * @param TKey $offset 593 | * @return TValue 594 | */ 595 | #[\ReturnTypeWillChange] 596 | public function offsetGet($offset) 597 | { 598 | return $this->items[$offset]; 599 | } 600 | 601 | /** 602 | * @param TKey|null $offset 603 | * @param TValue $value 604 | * @return void 605 | */ 606 | #[\ReturnTypeWillChange] 607 | public function offsetSet($offset, $value) 608 | { 609 | if (is_null($offset)) { 610 | $this->items[] = $value; 611 | } else { 612 | $this->items[$offset] = $value; 613 | } 614 | } 615 | 616 | /** 617 | * @param TKey $offset 618 | * @return void 619 | */ 620 | #[\ReturnTypeWillChange] 621 | public function offsetUnset($offset) 622 | { 623 | unset($this->items[$offset]); 624 | } 625 | 626 | //Countable 627 | public function count(): int 628 | { 629 | return count($this->items); 630 | } 631 | 632 | /** 633 | * @return ArrayIterator 634 | */ 635 | #[\ReturnTypeWillChange] 636 | public function getIterator(): Traversable 637 | { 638 | return new ArrayIterator($this->items); 639 | } 640 | 641 | //JsonSerializable 642 | #[\ReturnTypeWillChange] 643 | public function jsonSerialize() 644 | { 645 | return $this->toArray(); 646 | } 647 | 648 | /** 649 | * 转换当前数据集为JSON字符串 650 | * 651 | * @param integer $options json参数 652 | * @return string 653 | */ 654 | public function toJson(int $options = JSON_UNESCAPED_UNICODE): string 655 | { 656 | return json_encode($this->toArray(), $options); 657 | } 658 | 659 | public function __toString() 660 | { 661 | return $this->toJson(); 662 | } 663 | 664 | /** 665 | * 转换成数组 666 | * 667 | * @param mixed $items 数据 668 | * @return array 669 | */ 670 | protected function convertToArray($items): array 671 | { 672 | if ($items instanceof self) { 673 | return $items->all(); 674 | } 675 | 676 | return (array) $items; 677 | } 678 | } 679 | -------------------------------------------------------------------------------- /src/contract/Arrayable.php: -------------------------------------------------------------------------------- 1 | 10 | // +---------------------------------------------------------------------- 11 | 12 | use think\Collection; 13 | use think\helper\Arr; 14 | 15 | if (!function_exists('throw_if')) { 16 | /** 17 | * 按条件抛异常 18 | * 19 | * @template TValue 20 | * @template TException of \Throwable 21 | * 22 | * @param TValue $condition 23 | * @param TException|class-string|string $exception 24 | * @param mixed ...$parameters 25 | * @return TValue 26 | * 27 | * @throws TException 28 | */ 29 | function throw_if($condition, $exception, ...$parameters) 30 | { 31 | if ($condition) { 32 | throw (is_string($exception) ? new $exception(...$parameters) : $exception); 33 | } 34 | 35 | return $condition; 36 | } 37 | } 38 | 39 | if (!function_exists('throw_unless')) { 40 | /** 41 | * 按条件抛异常 42 | * 43 | * @template TValue 44 | * @template TException of \Throwable 45 | * 46 | * @param TValue $condition 47 | * @param TException|class-string|string $exception 48 | * @param mixed ...$parameters 49 | * @return TValue 50 | * 51 | * @throws TException 52 | */ 53 | function throw_unless($condition, $exception, ...$parameters) 54 | { 55 | if (!$condition) { 56 | throw (is_string($exception) ? new $exception(...$parameters) : $exception); 57 | } 58 | 59 | return $condition; 60 | } 61 | } 62 | 63 | if (!function_exists('tap')) { 64 | /** 65 | * 对一个值调用给定的闭包,然后返回该值 66 | * 67 | * @template TValue 68 | * 69 | * @param TValue $value 70 | * @param (callable(TValue): mixed)|null $callback 71 | * @return TValue 72 | */ 73 | function tap($value, $callback = null) 74 | { 75 | if (is_null($callback)) { 76 | return $value; 77 | } 78 | 79 | $callback($value); 80 | 81 | return $value; 82 | } 83 | } 84 | 85 | if (!function_exists('value')) { 86 | /** 87 | * Return the default value of the given value. 88 | * 89 | * @template TValue 90 | * 91 | * @param TValue|\Closure(): TValue $value 92 | * @return TValue 93 | */ 94 | function value($value) 95 | { 96 | return $value instanceof Closure ? $value() : $value; 97 | } 98 | } 99 | 100 | if (!function_exists('collect')) { 101 | /** 102 | * Create a collection from the given value. 103 | * 104 | * @param mixed $value 105 | * @return Collection 106 | */ 107 | function collect($value = null) 108 | { 109 | return new Collection($value); 110 | } 111 | } 112 | 113 | if (!function_exists('data_fill')) { 114 | /** 115 | * Fill in data where it's missing. 116 | * 117 | * @param mixed $target 118 | * @param string|array $key 119 | * @param mixed $value 120 | * @return mixed 121 | */ 122 | function data_fill(&$target, $key, $value) 123 | { 124 | return data_set($target, $key, $value, false); 125 | } 126 | } 127 | 128 | if (!function_exists('data_get')) { 129 | /** 130 | * Get an item from an array or object using "dot" notation. 131 | * 132 | * @param mixed $target 133 | * @param string|array|int $key 134 | * @param mixed $default 135 | * @return mixed 136 | */ 137 | function data_get($target, $key, $default = null) 138 | { 139 | if (is_null($key)) { 140 | return $target; 141 | } 142 | 143 | $key = is_array($key) ? $key : explode('.', $key); 144 | 145 | while (!is_null($segment = array_shift($key))) { 146 | if ('*' === $segment) { 147 | if ($target instanceof Collection) { 148 | $target = $target->all(); 149 | } elseif (!is_array($target)) { 150 | return value($default); 151 | } 152 | 153 | $result = []; 154 | 155 | foreach ($target as $item) { 156 | $result[] = data_get($item, $key); 157 | } 158 | 159 | return in_array('*', $key) ? Arr::collapse($result) : $result; 160 | } 161 | 162 | if (Arr::accessible($target) && Arr::exists($target, $segment)) { 163 | $target = $target[$segment]; 164 | } elseif (is_object($target) && isset($target->{$segment})) { 165 | $target = $target->{$segment}; 166 | } else { 167 | return value($default); 168 | } 169 | } 170 | 171 | return $target; 172 | } 173 | } 174 | 175 | if (!function_exists('data_set')) { 176 | /** 177 | * Set an item on an array or object using dot notation. 178 | * 179 | * @param mixed $target 180 | * @param string|array $key 181 | * @param mixed $value 182 | * @param bool $overwrite 183 | * @return mixed 184 | */ 185 | function data_set(&$target, $key, $value, $overwrite = true) 186 | { 187 | $segments = is_array($key) ? $key : explode('.', $key); 188 | 189 | if (($segment = array_shift($segments)) === '*') { 190 | if (!Arr::accessible($target)) { 191 | $target = []; 192 | } 193 | 194 | if ($segments) { 195 | foreach ($target as &$inner) { 196 | data_set($inner, $segments, $value, $overwrite); 197 | } 198 | } elseif ($overwrite) { 199 | foreach ($target as &$inner) { 200 | $inner = $value; 201 | } 202 | } 203 | } elseif (Arr::accessible($target)) { 204 | if ($segments) { 205 | if (!Arr::exists($target, $segment)) { 206 | $target[$segment] = []; 207 | } 208 | 209 | data_set($target[$segment], $segments, $value, $overwrite); 210 | } elseif ($overwrite || !Arr::exists($target, $segment)) { 211 | $target[$segment] = $value; 212 | } 213 | } elseif (is_object($target)) { 214 | if ($segments) { 215 | if (!isset($target->{$segment})) { 216 | $target->{$segment} = []; 217 | } 218 | 219 | data_set($target->{$segment}, $segments, $value, $overwrite); 220 | } elseif ($overwrite || !isset($target->{$segment})) { 221 | $target->{$segment} = $value; 222 | } 223 | } else { 224 | $target = []; 225 | 226 | if ($segments) { 227 | data_set($target[$segment], $segments, $value, $overwrite); 228 | } elseif ($overwrite) { 229 | $target[$segment] = $value; 230 | } 231 | } 232 | 233 | return $target; 234 | } 235 | } 236 | 237 | if (!function_exists('trait_uses_recursive')) { 238 | /** 239 | * 获取一个trait里所有引用到的trait 240 | * 241 | * @param string $trait Trait 242 | * @return array 243 | */ 244 | function trait_uses_recursive(string $trait): array 245 | { 246 | $traits = class_uses($trait); 247 | foreach ($traits as $trait) { 248 | $traits += trait_uses_recursive($trait); 249 | } 250 | 251 | return $traits; 252 | } 253 | } 254 | 255 | if (!function_exists('class_basename')) { 256 | /** 257 | * 获取类名(不包含命名空间) 258 | * 259 | * @param mixed $class 类名 260 | * @return string 261 | */ 262 | function class_basename($class): string 263 | { 264 | $class = is_object($class) ? get_class($class) : $class; 265 | return basename(str_replace('\\', '/', $class)); 266 | } 267 | } 268 | 269 | if (!function_exists('class_uses_recursive')) { 270 | /** 271 | *获取一个类里所有用到的trait,包括父类的 272 | * 273 | * @param mixed $class 类名 274 | * @return array 275 | */ 276 | function class_uses_recursive($class): array 277 | { 278 | if (is_object($class)) { 279 | $class = get_class($class); 280 | } 281 | 282 | $results = []; 283 | $classes = array_merge([$class => $class], class_parents($class)); 284 | foreach ($classes as $class) { 285 | $results += trait_uses_recursive($class); 286 | } 287 | 288 | return array_unique($results); 289 | } 290 | } 291 | 292 | if (!function_exists('array_is_list')) { 293 | /** 294 | * 判断数组是否为list 295 | * 296 | * @param array $array 数据 297 | * @return bool 298 | */ 299 | function array_is_list(array $array): bool 300 | { 301 | return array_values($array) === $array; 302 | } 303 | } 304 | 305 | if (!function_exists('json_validate')) { 306 | /** 307 | * 判断是否为有效json数据 308 | * 309 | * @param string $string 数据 310 | * @return bool 311 | */ 312 | function json_validate(string $string): bool 313 | { 314 | json_decode($string); 315 | return json_last_error() === JSON_ERROR_NONE; 316 | } 317 | } -------------------------------------------------------------------------------- /src/helper/Arr.php: -------------------------------------------------------------------------------- 1 | 10 | // +---------------------------------------------------------------------- 11 | 12 | namespace think\helper; 13 | 14 | use ArrayAccess; 15 | use InvalidArgumentException; 16 | use think\Collection; 17 | 18 | class Arr 19 | { 20 | 21 | /** 22 | * Determine whether the given value is array accessible. 23 | * 24 | * @param mixed $value 25 | * @return bool 26 | */ 27 | public static function accessible($value) 28 | { 29 | return is_array($value) || $value instanceof ArrayAccess; 30 | } 31 | 32 | /** 33 | * Add an element to an array using "dot" notation if it doesn't exist. 34 | * 35 | * @param array $array 36 | * @param string $key 37 | * @param mixed $value 38 | * @return array 39 | */ 40 | public static function add($array, $key, $value) 41 | { 42 | if (is_null(static::get($array, $key))) { 43 | static::set($array, $key, $value); 44 | } 45 | 46 | return $array; 47 | } 48 | 49 | /** 50 | * Collapse an array of arrays into a single array. 51 | * 52 | * @param array $array 53 | * @return array 54 | */ 55 | public static function collapse($array) 56 | { 57 | $results = []; 58 | 59 | foreach ($array as $values) { 60 | if ($values instanceof Collection) { 61 | $values = $values->all(); 62 | } elseif (!is_array($values)) { 63 | continue; 64 | } 65 | 66 | $results = array_merge($results, $values); 67 | } 68 | 69 | return $results; 70 | } 71 | 72 | /** 73 | * Cross join the given arrays, returning all possible permutations. 74 | * 75 | * @param array ...$arrays 76 | * @return array 77 | */ 78 | public static function crossJoin(...$arrays) 79 | { 80 | $results = [[]]; 81 | 82 | foreach ($arrays as $index => $array) { 83 | $append = []; 84 | 85 | foreach ($results as $product) { 86 | foreach ($array as $item) { 87 | $product[$index] = $item; 88 | 89 | $append[] = $product; 90 | } 91 | } 92 | 93 | $results = $append; 94 | } 95 | 96 | return $results; 97 | } 98 | 99 | /** 100 | * Divide an array into two arrays. One with keys and the other with values. 101 | * 102 | * @param array $array 103 | * @return array 104 | */ 105 | public static function divide($array) 106 | { 107 | return [array_keys($array), array_values($array)]; 108 | } 109 | 110 | /** 111 | * Flatten a multi-dimensional associative array with dots. 112 | * 113 | * @param array $array 114 | * @param string $prepend 115 | * @return array 116 | */ 117 | public static function dot($array, $prepend = '') 118 | { 119 | $results = []; 120 | 121 | foreach ($array as $key => $value) { 122 | if (is_array($value) && !empty($value)) { 123 | $results = array_merge($results, static::dot($value, $prepend . $key . '.')); 124 | } else { 125 | $results[$prepend . $key] = $value; 126 | } 127 | } 128 | 129 | return $results; 130 | } 131 | 132 | /** 133 | * Get all of the given array except for a specified array of keys. 134 | * 135 | * @param array $array 136 | * @param array|string $keys 137 | * @return array 138 | */ 139 | public static function except($array, $keys) 140 | { 141 | static::forget($array, $keys); 142 | 143 | return $array; 144 | } 145 | 146 | /** 147 | * Determine if the given key exists in the provided array. 148 | * 149 | * @param \ArrayAccess|array $array 150 | * @param string|int $key 151 | * @return bool 152 | */ 153 | public static function exists($array, $key) 154 | { 155 | if ($array instanceof ArrayAccess) { 156 | return $array->offsetExists($key); 157 | } 158 | 159 | return array_key_exists($key, $array); 160 | } 161 | 162 | /** 163 | * Return the first element in an array passing a given truth test. 164 | * 165 | * @param array $array 166 | * @param callable|null $callback 167 | * @param mixed $default 168 | * @return mixed 169 | */ 170 | public static function first($array, ?callable $callback = null, $default = null) 171 | { 172 | if (is_null($callback)) { 173 | if (empty($array)) { 174 | return value($default); 175 | } 176 | 177 | foreach ($array as $item) { 178 | return $item; 179 | } 180 | } 181 | 182 | foreach ($array as $key => $value) { 183 | if (call_user_func($callback, $value, $key)) { 184 | return $value; 185 | } 186 | } 187 | 188 | return value($default); 189 | } 190 | 191 | /** 192 | * Return the last element in an array passing a given truth test. 193 | * 194 | * @param array $array 195 | * @param callable|null $callback 196 | * @param mixed $default 197 | * @return mixed 198 | */ 199 | public static function last($array, ?callable $callback = null, $default = null) 200 | { 201 | if (is_null($callback)) { 202 | return empty($array) ? value($default) : end($array); 203 | } 204 | 205 | return static::first(array_reverse($array, true), $callback, $default); 206 | } 207 | 208 | /** 209 | * Flatten a multi-dimensional array into a single level. 210 | * 211 | * @param array $array 212 | * @param int $depth 213 | * @return array 214 | */ 215 | public static function flatten($array, $depth = INF) 216 | { 217 | $result = []; 218 | 219 | foreach ($array as $item) { 220 | $item = $item instanceof Collection ? $item->all() : $item; 221 | 222 | if (!is_array($item)) { 223 | $result[] = $item; 224 | } elseif ($depth === 1) { 225 | $result = array_merge($result, array_values($item)); 226 | } else { 227 | $result = array_merge($result, static::flatten($item, $depth - 1)); 228 | } 229 | } 230 | 231 | return $result; 232 | } 233 | 234 | /** 235 | * Remove one or many array items from a given array using "dot" notation. 236 | * 237 | * @param array $array 238 | * @param array|string $keys 239 | * @return void 240 | */ 241 | public static function forget(&$array, $keys) 242 | { 243 | $original = &$array; 244 | 245 | $keys = (array) $keys; 246 | 247 | if (count($keys) === 0) { 248 | return; 249 | } 250 | 251 | foreach ($keys as $key) { 252 | // if the exact key exists in the top-level, remove it 253 | if (static::exists($array, $key)) { 254 | unset($array[$key]); 255 | 256 | continue; 257 | } 258 | 259 | $parts = explode('.', $key); 260 | 261 | // clean up before each pass 262 | $array = &$original; 263 | 264 | while (count($parts) > 1) { 265 | $part = array_shift($parts); 266 | 267 | if (isset($array[$part]) && is_array($array[$part])) { 268 | $array = &$array[$part]; 269 | } else { 270 | continue 2; 271 | } 272 | } 273 | 274 | unset($array[array_shift($parts)]); 275 | } 276 | } 277 | 278 | /** 279 | * Get an item from an array using "dot" notation. 280 | * 281 | * @param \ArrayAccess|array $array 282 | * @param string $key 283 | * @param mixed $default 284 | * @return mixed 285 | */ 286 | public static function get($array, $key, $default = null) 287 | { 288 | if (!static::accessible($array)) { 289 | return value($default); 290 | } 291 | 292 | if (is_null($key)) { 293 | return $array; 294 | } 295 | 296 | if (static::exists($array, $key)) { 297 | return $array[$key]; 298 | } 299 | 300 | if (strpos($key, '.') === false) { 301 | return $array[$key] ?? value($default); 302 | } 303 | 304 | foreach (explode('.', $key) as $segment) { 305 | if (static::accessible($array) && static::exists($array, $segment)) { 306 | $array = $array[$segment]; 307 | } else { 308 | return value($default); 309 | } 310 | } 311 | 312 | return $array; 313 | } 314 | 315 | /** 316 | * Check if an item or items exist in an array using "dot" notation. 317 | * 318 | * @param \ArrayAccess|array $array 319 | * @param string|array $keys 320 | * @return bool 321 | */ 322 | public static function has($array, $keys) 323 | { 324 | $keys = (array) $keys; 325 | 326 | if (!$array || $keys === []) { 327 | return false; 328 | } 329 | 330 | foreach ($keys as $key) { 331 | $subKeyArray = $array; 332 | 333 | if (static::exists($array, $key)) { 334 | continue; 335 | } 336 | 337 | foreach (explode('.', $key) as $segment) { 338 | if (static::accessible($subKeyArray) && static::exists($subKeyArray, $segment)) { 339 | $subKeyArray = $subKeyArray[$segment]; 340 | } else { 341 | return false; 342 | } 343 | } 344 | } 345 | 346 | return true; 347 | } 348 | 349 | /** 350 | * Determines if an array is associative. 351 | * 352 | * An array is "associative" if it doesn't have sequential numerical keys beginning with zero. 353 | * 354 | * @param array $array 355 | * @return bool 356 | */ 357 | public static function isAssoc(array $array) 358 | { 359 | return !array_is_list($array); 360 | } 361 | 362 | /** 363 | * Get a subset of the items from the given array. 364 | * 365 | * @param array $array 366 | * @param array|string $keys 367 | * @return array 368 | */ 369 | public static function only($array, $keys) 370 | { 371 | return array_intersect_key($array, array_flip((array) $keys)); 372 | } 373 | 374 | /** 375 | * Pluck an array of values from an array. 376 | * 377 | * @param array $array 378 | * @param string|array $value 379 | * @param string|array|null $key 380 | * @return array 381 | */ 382 | public static function pluck($array, $value, $key = null) 383 | { 384 | $results = []; 385 | 386 | [$value, $key] = static::explodePluckParameters($value, $key); 387 | 388 | foreach ($array as $item) { 389 | $itemValue = data_get($item, $value); 390 | 391 | // If the key is "null", we will just append the value to the array and keep 392 | // looping. Otherwise we will key the array using the value of the key we 393 | // received from the developer. Then we'll return the final array form. 394 | if (is_null($key)) { 395 | $results[] = $itemValue; 396 | } else { 397 | $itemKey = data_get($item, $key); 398 | 399 | if (is_object($itemKey) && method_exists($itemKey, '__toString')) { 400 | $itemKey = (string) $itemKey; 401 | } 402 | 403 | $results[$itemKey] = $itemValue; 404 | } 405 | } 406 | 407 | return $results; 408 | } 409 | 410 | /** 411 | * Explode the "value" and "key" arguments passed to "pluck". 412 | * 413 | * @param string|array $value 414 | * @param string|array|null $key 415 | * @return array 416 | */ 417 | protected static function explodePluckParameters($value, $key) 418 | { 419 | $value = is_string($value) ? explode('.', $value) : $value; 420 | 421 | $key = is_null($key) || is_array($key) ? $key : explode('.', $key); 422 | 423 | return [$value, $key]; 424 | } 425 | 426 | /** 427 | * Push an item onto the beginning of an array. 428 | * 429 | * @param array $array 430 | * @param mixed $value 431 | * @param mixed $key 432 | * @return array 433 | */ 434 | public static function prepend($array, $value, $key = null) 435 | { 436 | if (is_null($key)) { 437 | array_unshift($array, $value); 438 | } else { 439 | $array = [$key => $value] + $array; 440 | } 441 | 442 | return $array; 443 | } 444 | 445 | /** 446 | * Get a value from the array, and remove it. 447 | * 448 | * @param array $array 449 | * @param string $key 450 | * @param mixed $default 451 | * @return mixed 452 | */ 453 | public static function pull(&$array, $key, $default = null) 454 | { 455 | $value = static::get($array, $key, $default); 456 | 457 | static::forget($array, $key); 458 | 459 | return $value; 460 | } 461 | 462 | /** 463 | * Get one or a specified number of random values from an array. 464 | * 465 | * @param array $array 466 | * @param int|null $number 467 | * @return mixed 468 | * 469 | * @throws \InvalidArgumentException 470 | */ 471 | public static function random($array, $number = null) 472 | { 473 | $requested = is_null($number) ? 1 : $number; 474 | 475 | $count = count($array); 476 | 477 | if ($requested > $count) { 478 | throw new InvalidArgumentException( 479 | "You requested {$requested} items, but there are only {$count} items available." 480 | ); 481 | } 482 | 483 | if (is_null($number)) { 484 | return $array[array_rand($array)]; 485 | } 486 | 487 | if ((int) $number === 0) { 488 | return []; 489 | } 490 | 491 | $keys = array_rand($array, $number); 492 | 493 | $results = []; 494 | 495 | foreach ((array) $keys as $key) { 496 | $results[] = $array[$key]; 497 | } 498 | 499 | return $results; 500 | } 501 | 502 | /** 503 | * Set an array item to a given value using "dot" notation. 504 | * 505 | * If no key is given to the method, the entire array will be replaced. 506 | * 507 | * @param array $array 508 | * @param string $key 509 | * @param mixed $value 510 | * @return array 511 | */ 512 | public static function set(&$array, $key, $value) 513 | { 514 | if (is_null($key)) { 515 | return $array = $value; 516 | } 517 | 518 | $keys = explode('.', $key); 519 | 520 | while (count($keys) > 1) { 521 | $key = array_shift($keys); 522 | 523 | // If the key doesn't exist at this depth, we will just create an empty array 524 | // to hold the next value, allowing us to create the arrays to hold final 525 | // values at the correct depth. Then we'll keep digging into the array. 526 | if (!isset($array[$key]) || !is_array($array[$key])) { 527 | $array[$key] = []; 528 | } 529 | 530 | $array = &$array[$key]; 531 | } 532 | 533 | $array[array_shift($keys)] = $value; 534 | 535 | return $array; 536 | } 537 | 538 | /** 539 | * Shuffle the given array and return the result. 540 | * 541 | * @param array $array 542 | * @param int|null $seed 543 | * @return array 544 | */ 545 | public static function shuffle($array, $seed = null) 546 | { 547 | if (is_null($seed)) { 548 | shuffle($array); 549 | } else { 550 | srand($seed); 551 | 552 | usort($array, function () { 553 | return rand(-1, 1); 554 | }); 555 | } 556 | 557 | return $array; 558 | } 559 | 560 | /** 561 | * Sort the array using the given callback or "dot" notation. 562 | * 563 | * @param array $array 564 | * @param callable|string|null $callback 565 | * @return array 566 | */ 567 | public static function sort($array, $callback = null) 568 | { 569 | return Collection::make($array)->sort($callback)->all(); 570 | } 571 | 572 | /** 573 | * Recursively sort an array by keys and values. 574 | * 575 | * @param array $array 576 | * @return array 577 | */ 578 | public static function sortRecursive($array) 579 | { 580 | foreach ($array as &$value) { 581 | if (is_array($value)) { 582 | $value = static::sortRecursive($value); 583 | } 584 | } 585 | 586 | if (static::isAssoc($array)) { 587 | ksort($array); 588 | } else { 589 | sort($array); 590 | } 591 | 592 | return $array; 593 | } 594 | 595 | /** 596 | * Convert the array into a query string. 597 | * 598 | * @param array $array 599 | * @return string 600 | */ 601 | public static function query($array) 602 | { 603 | return http_build_query($array, null, '&', PHP_QUERY_RFC3986); 604 | } 605 | 606 | /** 607 | * Filter the array using the given callback. 608 | * 609 | * @param array $array 610 | * @param callable $callback 611 | * @return array 612 | */ 613 | public static function where($array, callable $callback) 614 | { 615 | return array_filter($array, $callback, ARRAY_FILTER_USE_BOTH); 616 | } 617 | 618 | /** 619 | * If the given value is not an array and not null, wrap it in one. 620 | * 621 | * @param mixed $value 622 | * @return array 623 | */ 624 | public static function wrap($value) 625 | { 626 | if (is_null($value)) { 627 | return []; 628 | } 629 | 630 | return is_array($value) ? $value : [$value]; 631 | } 632 | 633 | /** 634 | * Recursively merge arrays. 635 | * If the value is an associative array, it will be merged recursively. 636 | * If the value is an indexed array, it will be replaced entirely. 637 | * 638 | * @param array ...$arrays 639 | * @return array 640 | */ 641 | public static function mergeDeep(array ...$arrays): array 642 | { 643 | $result = []; 644 | foreach ($arrays as $array) { 645 | foreach ($array as $key => $value) { 646 | if (isset($result[$key]) && is_array($result[$key]) && is_array($value)) { 647 | // 只有当两个数组都是关联数组时才递归合并 648 | if (self::isAssoc($result[$key]) && self::isAssoc($value)) { 649 | $result[$key] = self::mergeDeep( 650 | $result[$key], 651 | $value 652 | ); 653 | } else { 654 | // 如果任一数组是索引数组,则直接覆盖 655 | $result[$key] = $value; 656 | } 657 | } else { 658 | $result[$key] = $value; 659 | } 660 | } 661 | } 662 | return $result; 663 | } 664 | 665 | public static function flatMap(callable $fn, array $array): array 666 | { 667 | return array_merge(...array_map($fn, $array)); 668 | } 669 | } 670 | -------------------------------------------------------------------------------- /src/helper/Macroable.php: -------------------------------------------------------------------------------- 1 | 10 | // +---------------------------------------------------------------------- 11 | namespace think\helper; 12 | 13 | use Closure; 14 | use think\exception\FuncNotFoundException; 15 | 16 | trait Macroable 17 | { 18 | /** 19 | * 方法注入. 20 | * 21 | * @var Closure[] 22 | */ 23 | protected static $macro = []; 24 | 25 | /** 26 | * 设置方法注入. 27 | * 28 | * @param string $method 29 | * @param Closure $closure 30 | * 31 | * @return void 32 | */ 33 | public static function macro(string $method, Closure $closure) 34 | { 35 | static::$macro[$method] = $closure; 36 | } 37 | 38 | /** 39 | * 检查方法是否已经有注入 40 | * 41 | * @param string $name 42 | * @return bool 43 | */ 44 | public static function hasMacro(string $method) 45 | { 46 | return isset(static::$macro[$method]); 47 | } 48 | 49 | public function __call($method, $args) 50 | { 51 | if (!isset(static::$macro[$method])) { 52 | throw new FuncNotFoundException('method not exists: ' . static::class . '::' . $method . '()', "{static::class}::{$method}"); 53 | } 54 | 55 | return call_user_func_array(static::$macro[$method]->bindTo($this, static::class), $args); 56 | } 57 | 58 | public static function __callStatic($method, $args) 59 | { 60 | if (!isset(static::$macro[$method])) { 61 | throw new FuncNotFoundException('method not exists: ' . static::class . '::' . $method . '()', "{static::class}::{$method}"); 62 | } 63 | 64 | return call_user_func_array(static::$macro[$method]->bindTo(null, static::class), $args); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/helper/Str.php: -------------------------------------------------------------------------------- 1 | 10 | // +---------------------------------------------------------------------- 11 | namespace think\helper; 12 | 13 | class Str 14 | { 15 | 16 | protected static $snakeCache = []; 17 | 18 | protected static $camelCache = []; 19 | 20 | protected static $studlyCache = []; 21 | 22 | /** 23 | * 检查字符串中是否包含某些字符串 24 | * @param string $haystack 25 | * @param string|array $needles 26 | * @return bool 27 | */ 28 | public static function contains(string $haystack, $needles): bool 29 | { 30 | foreach ((array) $needles as $needle) { 31 | if ('' != $needle && mb_strpos($haystack, $needle) !== false) { 32 | return true; 33 | } 34 | } 35 | 36 | return false; 37 | } 38 | 39 | /** 40 | * 检查字符串是否以某些字符串结尾 41 | * 42 | * @param string $haystack 43 | * @param string|array $needles 44 | * @return bool 45 | */ 46 | public static function endsWith(string $haystack, $needles): bool 47 | { 48 | foreach ((array) $needles as $needle) { 49 | if ((string) $needle === static::substr($haystack, -static::length($needle))) { 50 | return true; 51 | } 52 | } 53 | 54 | return false; 55 | } 56 | 57 | /** 58 | * 检查字符串是否以某些字符串开头 59 | * 60 | * @param string $haystack 61 | * @param string|array $needles 62 | * @return bool 63 | */ 64 | public static function startsWith(string $haystack, $needles): bool 65 | { 66 | foreach ((array) $needles as $needle) { 67 | if ('' != $needle && mb_strpos($haystack, $needle) === 0) { 68 | return true; 69 | } 70 | } 71 | 72 | return false; 73 | } 74 | 75 | /** 76 | * 获取指定长度的随机字母数字组合的字符串 77 | * 78 | * @param int $length 79 | * @param int $type 80 | * @param string $addChars 81 | * @return string 82 | */ 83 | public static function random(int $length = 6, ?int $type = null, string $addChars = ''): string 84 | { 85 | $str = ''; 86 | switch ($type) { 87 | case 0: 88 | $chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz' . $addChars; 89 | break; 90 | case 1: 91 | $chars = str_repeat('0123456789', 3); 92 | break; 93 | case 2: 94 | $chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' . $addChars; 95 | break; 96 | case 3: 97 | $chars = 'abcdefghijklmnopqrstuvwxyz' . $addChars; 98 | break; 99 | case 4: 100 | $chars = "们以我到他会作时要动国产的一是工就年阶义发成部民可出能方进在了不和有大这主中人上为来分生对于学下级地个用同行面说种过命度革而多子后自社加小机也经力线本电高量长党得实家定深法表着水理化争现所二起政三好十战无农使性前等反体合斗路图把结第里正新开论之物从当两些还天资事队批点育重其思与间内去因件日利相由压员气业代全组数果期导平各基或月毛然如应形想制心样干都向变关问比展那它最及外没看治提五解系林者米群头意只明四道马认次文通但条较克又公孔领军流入接席位情运器并飞原油放立题质指建区验活众很教决特此常石强极土少已根共直团统式转别造切九你取西持总料连任志观调七么山程百报更见必真保热委手改管处己将修支识病象几先老光专什六型具示复安带每东增则完风回南广劳轮科北打积车计给节做务被整联步类集号列温装即毫知轴研单色坚据速防史拉世设达尔场织历花受求传口断况采精金界品判参层止边清至万确究书" . $addChars; 101 | break; 102 | default: 103 | $chars = 'ABCDEFGHIJKMNPQRSTUVWXYZabcdefghijkmnpqrstuvwxyz23456789' . $addChars; 104 | break; 105 | } 106 | if ($length > 10) { 107 | $chars = $type == 1 ? str_repeat($chars, $length) : str_repeat($chars, 5); 108 | } 109 | if ($type != 4) { 110 | $chars = str_shuffle($chars); 111 | $str = substr($chars, 0, $length); 112 | } else { 113 | for ($i = 0; $i < $length; $i++) { 114 | $str .= mb_substr($chars, floor(mt_rand(0, mb_strlen($chars, 'utf-8') - 1)), 1); 115 | } 116 | } 117 | return $str; 118 | } 119 | 120 | /** 121 | * 字符串转小写 122 | * 123 | * @param string $value 124 | * @return string 125 | */ 126 | public static function lower(string $value): string 127 | { 128 | return mb_strtolower($value, 'UTF-8'); 129 | } 130 | 131 | /** 132 | * 字符串转大写 133 | * 134 | * @param string $value 135 | * @return string 136 | */ 137 | public static function upper(string $value): string 138 | { 139 | return mb_strtoupper($value, 'UTF-8'); 140 | } 141 | 142 | /** 143 | * 获取字符串的长度 144 | * 145 | * @param string $value 146 | * @return int 147 | */ 148 | public static function length(string $value): int 149 | { 150 | return mb_strlen($value); 151 | } 152 | 153 | /** 154 | * 截取字符串 155 | * 156 | * @param string $string 157 | * @param int $start 158 | * @param int|null $length 159 | * @return string 160 | */ 161 | public static function substr(string $string, int $start, ?int $length = null): string 162 | { 163 | return mb_substr($string, $start, $length, 'UTF-8'); 164 | } 165 | 166 | /** 167 | * 驼峰转下划线 168 | * 169 | * @param string $value 170 | * @param string $delimiter 171 | * @return string 172 | */ 173 | public static function snake(string $value, string $delimiter = '_'): string 174 | { 175 | $key = $value; 176 | 177 | if (isset(static::$snakeCache[$key][$delimiter])) { 178 | return static::$snakeCache[$key][$delimiter]; 179 | } 180 | 181 | if (!ctype_lower($value)) { 182 | $value = preg_replace('/\s+/u', '', ucwords($value)); 183 | 184 | $value = static::lower(preg_replace('/(.)(?=[A-Z])/u', '$1' . $delimiter, $value)); 185 | } 186 | 187 | return static::$snakeCache[$key][$delimiter] = $value; 188 | } 189 | 190 | /** 191 | * 下划线转驼峰(首字母小写) 192 | * 193 | * @param string $value 194 | * @return string 195 | */ 196 | public static function camel(string $value): string 197 | { 198 | if (isset(static::$camelCache[$value])) { 199 | return static::$camelCache[$value]; 200 | } 201 | 202 | return static::$camelCache[$value] = lcfirst(static::studly($value)); 203 | } 204 | 205 | /** 206 | * 下划线转驼峰(首字母大写) 207 | * 208 | * @param string $value 209 | * @return string 210 | */ 211 | public static function studly(string $value): string 212 | { 213 | $key = $value; 214 | 215 | if (isset(static::$studlyCache[$key])) { 216 | return static::$studlyCache[$key]; 217 | } 218 | 219 | $value = ucwords(str_replace(['-', '_'], ' ', $value)); 220 | 221 | return static::$studlyCache[$key] = str_replace(' ', '', $value); 222 | } 223 | 224 | /** 225 | * 转为首字母大写的标题格式 226 | * 227 | * @param string $value 228 | * @return string 229 | */ 230 | public static function title(string $value): string 231 | { 232 | return mb_convert_case($value, MB_CASE_TITLE, 'UTF-8'); 233 | } 234 | } 235 | -------------------------------------------------------------------------------- /tests/ArrTest.php: -------------------------------------------------------------------------------- 1 | 'ThinkPHP'], 'price', 100); 14 | $this->assertSame(['name' => 'ThinkPHP', 'price' => 100], $array); 15 | } 16 | 17 | public function testCrossJoin() 18 | { 19 | // Single dimension 20 | $this->assertSame( 21 | [[1, 'a'], [1, 'b'], [1, 'c']], 22 | Arr::crossJoin([1], ['a', 'b', 'c']) 23 | ); 24 | // Square matrix 25 | $this->assertSame( 26 | [[1, 'a'], [1, 'b'], [2, 'a'], [2, 'b']], 27 | Arr::crossJoin([1, 2], ['a', 'b']) 28 | ); 29 | // Rectangular matrix 30 | $this->assertSame( 31 | [[1, 'a'], [1, 'b'], [1, 'c'], [2, 'a'], [2, 'b'], [2, 'c']], 32 | Arr::crossJoin([1, 2], ['a', 'b', 'c']) 33 | ); 34 | // 3D matrix 35 | $this->assertSame( 36 | [ 37 | [1, 'a', 'I'], [1, 'a', 'II'], [1, 'a', 'III'], 38 | [1, 'b', 'I'], [1, 'b', 'II'], [1, 'b', 'III'], 39 | [2, 'a', 'I'], [2, 'a', 'II'], [2, 'a', 'III'], 40 | [2, 'b', 'I'], [2, 'b', 'II'], [2, 'b', 'III'], 41 | ], 42 | Arr::crossJoin([1, 2], ['a', 'b'], ['I', 'II', 'III']) 43 | ); 44 | // With 1 empty dimension 45 | $this->assertSame([], Arr::crossJoin([], ['a', 'b'], ['I', 'II', 'III'])); 46 | $this->assertSame([], Arr::crossJoin([1, 2], [], ['I', 'II', 'III'])); 47 | $this->assertSame([], Arr::crossJoin([1, 2], ['a', 'b'], [])); 48 | // With empty arrays 49 | $this->assertSame([], Arr::crossJoin([], [], [])); 50 | $this->assertSame([], Arr::crossJoin([], [])); 51 | $this->assertSame([], Arr::crossJoin([])); 52 | // Not really a proper usage, still, test for preserving BC 53 | $this->assertSame([[]], Arr::crossJoin()); 54 | } 55 | 56 | public function testDivide() 57 | { 58 | [$keys, $values] = Arr::divide(['name' => 'ThinkPHP']); 59 | $this->assertSame(['name'], $keys); 60 | $this->assertSame(['ThinkPHP'], $values); 61 | } 62 | 63 | public function testDot() 64 | { 65 | $array = Arr::dot(['foo' => ['bar' => 'baz']]); 66 | $this->assertSame(['foo.bar' => 'baz'], $array); 67 | $array = Arr::dot([]); 68 | $this->assertSame([], $array); 69 | $array = Arr::dot(['foo' => []]); 70 | $this->assertSame(['foo' => []], $array); 71 | $array = Arr::dot(['foo' => ['bar' => []]]); 72 | $this->assertSame(['foo.bar' => []], $array); 73 | } 74 | 75 | public function testExcept() 76 | { 77 | $array = ['name' => 'ThinkPHP', 'price' => 100]; 78 | $array = Arr::except($array, ['price']); 79 | $this->assertSame(['name' => 'ThinkPHP'], $array); 80 | } 81 | 82 | public function testExists() 83 | { 84 | $this->assertTrue(Arr::exists([1], 0)); 85 | $this->assertTrue(Arr::exists([null], 0)); 86 | $this->assertTrue(Arr::exists(['a' => 1], 'a')); 87 | $this->assertTrue(Arr::exists(['a' => null], 'a')); 88 | $this->assertFalse(Arr::exists([1], 1)); 89 | $this->assertFalse(Arr::exists([null], 1)); 90 | $this->assertFalse(Arr::exists(['a' => 1], 0)); 91 | } 92 | 93 | public function testFirst() 94 | { 95 | $array = [100, 200, 300]; 96 | $value = Arr::first($array, function ($value) { 97 | return $value >= 150; 98 | }); 99 | $this->assertSame(200, $value); 100 | $this->assertSame(100, Arr::first($array)); 101 | 102 | $this->assertSame('default', Arr::first([], null, 'default')); 103 | 104 | $this->assertSame('default', Arr::first([], function () { 105 | return false; 106 | }, 'default')); 107 | } 108 | 109 | public function testLast() 110 | { 111 | $array = [100, 200, 300]; 112 | $last = Arr::last($array, function ($value) { 113 | return $value < 250; 114 | }); 115 | $this->assertSame(200, $last); 116 | $last = Arr::last($array, function ($value, $key) { 117 | return $key < 2; 118 | }); 119 | $this->assertSame(200, $last); 120 | $this->assertSame(300, Arr::last($array)); 121 | } 122 | 123 | public function testFlatten() 124 | { 125 | // Flat arrays are unaffected 126 | $array = ['#foo', '#bar', '#baz']; 127 | $this->assertSame(['#foo', '#bar', '#baz'], Arr::flatten(['#foo', '#bar', '#baz'])); 128 | // Nested arrays are flattened with existing flat items 129 | $array = [['#foo', '#bar'], '#baz']; 130 | $this->assertSame(['#foo', '#bar', '#baz'], Arr::flatten($array)); 131 | // Flattened array includes "null" items 132 | $array = [['#foo', null], '#baz', null]; 133 | $this->assertSame(['#foo', null, '#baz', null], Arr::flatten($array)); 134 | // Sets of nested arrays are flattened 135 | $array = [['#foo', '#bar'], ['#baz']]; 136 | $this->assertSame(['#foo', '#bar', '#baz'], Arr::flatten($array)); 137 | // Deeply nested arrays are flattened 138 | $array = [['#foo', ['#bar']], ['#baz']]; 139 | $this->assertSame(['#foo', '#bar', '#baz'], Arr::flatten($array)); 140 | // Nested arrays are flattened alongside arrays 141 | $array = [new Collection(['#foo', '#bar']), ['#baz']]; 142 | $this->assertSame(['#foo', '#bar', '#baz'], Arr::flatten($array)); 143 | // Nested arrays containing plain arrays are flattened 144 | $array = [new Collection(['#foo', ['#bar']]), ['#baz']]; 145 | $this->assertSame(['#foo', '#bar', '#baz'], Arr::flatten($array)); 146 | // Nested arrays containing arrays are flattened 147 | $array = [['#foo', new Collection(['#bar'])], ['#baz']]; 148 | $this->assertSame(['#foo', '#bar', '#baz'], Arr::flatten($array)); 149 | // Nested arrays containing arrays containing arrays are flattened 150 | $array = [['#foo', new Collection(['#bar', ['#zap']])], ['#baz']]; 151 | $this->assertSame(['#foo', '#bar', '#zap', '#baz'], Arr::flatten($array)); 152 | } 153 | 154 | public function testFlattenWithDepth() 155 | { 156 | // No depth flattens recursively 157 | $array = [['#foo', ['#bar', ['#baz']]], '#zap']; 158 | $this->assertSame(['#foo', '#bar', '#baz', '#zap'], Arr::flatten($array)); 159 | // Specifying a depth only flattens to that depth 160 | $array = [['#foo', ['#bar', ['#baz']]], '#zap']; 161 | $this->assertSame(['#foo', ['#bar', ['#baz']], '#zap'], Arr::flatten($array, 1)); 162 | $array = [['#foo', ['#bar', ['#baz']]], '#zap']; 163 | $this->assertSame(['#foo', '#bar', ['#baz'], '#zap'], Arr::flatten($array, 2)); 164 | } 165 | 166 | public function testGet() 167 | { 168 | $array = ['products.item' => ['price' => 100]]; 169 | $this->assertSame(['price' => 100], Arr::get($array, 'products.item')); 170 | $array = ['products' => ['item' => ['price' => 100]]]; 171 | $value = Arr::get($array, 'products.item'); 172 | $this->assertSame(['price' => 100], $value); 173 | // Test null array values 174 | $array = ['foo' => null, 'bar' => ['baz' => null]]; 175 | $this->assertNull(Arr::get($array, 'foo', 'default')); 176 | $this->assertNull(Arr::get($array, 'bar.baz', 'default')); 177 | // Test null key returns the whole array 178 | $array = ['foo', 'bar']; 179 | $this->assertSame($array, Arr::get($array, null)); 180 | // Test $array is empty and key is null 181 | $this->assertSame([], Arr::get([], null)); 182 | $this->assertSame([], Arr::get([], null, 'default')); 183 | } 184 | 185 | public function testHas() 186 | { 187 | $array = ['products.item' => ['price' => 100]]; 188 | $this->assertTrue(Arr::has($array, 'products.item')); 189 | $array = ['products' => ['item' => ['price' => 100]]]; 190 | $this->assertTrue(Arr::has($array, 'products.item')); 191 | $this->assertTrue(Arr::has($array, 'products.item.price')); 192 | $this->assertFalse(Arr::has($array, 'products.foo')); 193 | $this->assertFalse(Arr::has($array, 'products.item.foo')); 194 | $array = ['foo' => null, 'bar' => ['baz' => null]]; 195 | $this->assertTrue(Arr::has($array, 'foo')); 196 | $this->assertTrue(Arr::has($array, 'bar.baz')); 197 | $array = ['foo', 'bar']; 198 | $this->assertFalse(Arr::has($array, null)); 199 | $this->assertFalse(Arr::has([], null)); 200 | $array = ['products' => ['item' => ['price' => 100]]]; 201 | $this->assertTrue(Arr::has($array, ['products.item'])); 202 | $this->assertTrue(Arr::has($array, ['products.item', 'products.item.price'])); 203 | $this->assertTrue(Arr::has($array, ['products', 'products'])); 204 | $this->assertFalse(Arr::has($array, ['foo'])); 205 | $this->assertFalse(Arr::has($array, [])); 206 | $this->assertFalse(Arr::has($array, ['products.item', 'products.price'])); 207 | $this->assertFalse(Arr::has([], [null])); 208 | } 209 | 210 | public function testIsAssoc() 211 | { 212 | $this->assertTrue(Arr::isAssoc(['a' => 'a', 0 => 'b'])); 213 | $this->assertTrue(Arr::isAssoc([1 => 'a', 0 => 'b'])); 214 | $this->assertTrue(Arr::isAssoc([1 => 'a', 2 => 'b'])); 215 | $this->assertFalse(Arr::isAssoc([0 => 'a', 1 => 'b'])); 216 | $this->assertFalse(Arr::isAssoc(['a', 'b'])); 217 | } 218 | 219 | public function testOnly() 220 | { 221 | $array = ['name' => 'ThinkPHP', 'price' => 100, 'orders' => 10]; 222 | $array = Arr::only($array, ['name', 'price']); 223 | $this->assertSame(['name' => 'ThinkPHP', 'price' => 100], $array); 224 | } 225 | 226 | public function testPrepend() 227 | { 228 | $array = Arr::prepend(['one', 'two', 'three', 'four'], 'zero'); 229 | $this->assertSame(['zero', 'one', 'two', 'three', 'four'], $array); 230 | $array = Arr::prepend(['one' => 1, 'two' => 2], 0, 'zero'); 231 | $this->assertSame(['zero' => 0, 'one' => 1, 'two' => 2], $array); 232 | } 233 | 234 | public function testPull() 235 | { 236 | $array = ['name' => 'ThinkPHP', 'price' => 100]; 237 | $name = Arr::pull($array, 'name'); 238 | $this->assertSame('ThinkPHP', $name); 239 | $this->assertSame(['price' => 100], $array); 240 | // Only works on first level keys 241 | $array = ['i@example.com' => 'Joe', 'jack@localhost' => 'Jane']; 242 | $name = Arr::pull($array, 'i@example.com'); 243 | $this->assertSame('Joe', $name); 244 | $this->assertSame(['jack@localhost' => 'Jane'], $array); 245 | // Does not work for nested keys 246 | $array = ['emails' => ['i@example.com' => 'Joe', 'jack@localhost' => 'Jane']]; 247 | $name = Arr::pull($array, 'emails.i@example.com'); 248 | $this->assertNull($name); 249 | $this->assertSame(['emails' => ['i@example.com' => 'Joe', 'jack@localhost' => 'Jane']], $array); 250 | } 251 | 252 | public function testRandom() 253 | { 254 | $randomValue = Arr::random(['foo', 'bar', 'baz']); 255 | $this->assertContains($randomValue, ['foo', 'bar', 'baz']); 256 | $randomValues = Arr::random(['foo', 'bar', 'baz'], 1); 257 | $this->assertIsArray($randomValues); 258 | $this->assertCount(1, $randomValues); 259 | $this->assertContains($randomValues[0], ['foo', 'bar', 'baz']); 260 | $randomValues = Arr::random(['foo', 'bar', 'baz'], 2); 261 | $this->assertIsArray($randomValues); 262 | $this->assertCount(2, $randomValues); 263 | $this->assertContains($randomValues[0], ['foo', 'bar', 'baz']); 264 | $this->assertContains($randomValues[1], ['foo', 'bar', 'baz']); 265 | } 266 | 267 | public function testSet() 268 | { 269 | $array = ['products' => ['item' => ['price' => 100]]]; 270 | Arr::set($array, 'products.item.price', 200); 271 | Arr::set($array, 'goods.item.price', 200); 272 | $this->assertSame(['products' => ['item' => ['price' => 200]], 'goods' => ['item' => ['price' => 200]]], $array); 273 | } 274 | 275 | public function testWhere() 276 | { 277 | $array = [100, '200', 300, '400', 500]; 278 | $array = Arr::where($array, function ($value, $key) { 279 | return is_string($value); 280 | }); 281 | $this->assertSame([1 => '200', 3 => '400'], $array); 282 | } 283 | 284 | public function testWhereKey() 285 | { 286 | $array = ['10' => 1, 'foo' => 3, 20 => 2]; 287 | $array = Arr::where($array, function ($value, $key) { 288 | return is_numeric($key); 289 | }); 290 | $this->assertSame(['10' => 1, 20 => 2], $array); 291 | } 292 | 293 | public function testForget() 294 | { 295 | $array = ['products' => ['item' => ['price' => 100]]]; 296 | Arr::forget($array, null); 297 | $this->assertSame(['products' => ['item' => ['price' => 100]]], $array); 298 | $array = ['products' => ['item' => ['price' => 100]]]; 299 | Arr::forget($array, []); 300 | $this->assertSame(['products' => ['item' => ['price' => 100]]], $array); 301 | $array = ['products' => ['item' => ['price' => 100]]]; 302 | Arr::forget($array, 'products.item'); 303 | $this->assertSame(['products' => []], $array); 304 | $array = ['products' => ['item' => ['price' => 100]]]; 305 | Arr::forget($array, 'products.item.price'); 306 | $this->assertSame(['products' => ['item' => []]], $array); 307 | $array = ['products' => ['item' => ['price' => 100]]]; 308 | Arr::forget($array, 'products.final.price'); 309 | $this->assertSame(['products' => ['item' => ['price' => 100]]], $array); 310 | $array = ['shop' => ['cart' => [150 => 0]]]; 311 | Arr::forget($array, 'shop.final.cart'); 312 | $this->assertSame(['shop' => ['cart' => [150 => 0]]], $array); 313 | $array = ['products' => ['item' => ['price' => ['original' => 50, 'taxes' => 60]]]]; 314 | Arr::forget($array, 'products.item.price.taxes'); 315 | $this->assertSame(['products' => ['item' => ['price' => ['original' => 50]]]], $array); 316 | $array = ['products' => ['item' => ['price' => ['original' => 50, 'taxes' => 60]]]]; 317 | Arr::forget($array, 'products.item.final.taxes'); 318 | $this->assertSame(['products' => ['item' => ['price' => ['original' => 50, 'taxes' => 60]]]], $array); 319 | $array = ['products' => ['item' => ['price' => 50], null => 'something']]; 320 | Arr::forget($array, ['products.amount.all', 'products.item.price']); 321 | $this->assertSame(['products' => ['item' => [], null => 'something']], $array); 322 | // Only works on first level keys 323 | $array = ['i@example.com' => 'Joe', 'i@thinkphp.com' => 'Jane']; 324 | Arr::forget($array, 'i@example.com'); 325 | $this->assertSame(['i@thinkphp.com' => 'Jane'], $array); 326 | // Does not work for nested keys 327 | $array = ['emails' => ['i@example.com' => ['name' => 'Joe'], 'jack@localhost' => ['name' => 'Jane']]]; 328 | Arr::forget($array, ['emails.i@example.com', 'emails.jack@localhost']); 329 | $this->assertSame(['emails' => ['i@example.com' => ['name' => 'Joe']]], $array); 330 | } 331 | 332 | public function testWrap() 333 | { 334 | $string = 'a'; 335 | $array = ['a']; 336 | $object = new stdClass(); 337 | $object->value = 'a'; 338 | $this->assertSame(['a'], Arr::wrap($string)); 339 | $this->assertSame($array, Arr::wrap($array)); 340 | $this->assertSame([$object], Arr::wrap($object)); 341 | } 342 | 343 | public function testMergeDeep() 344 | { 345 | $this->assertSame( 346 | [ 347 | 'a' => [ 348 | 'c' => [2], 349 | 'e' => 5, 350 | 'f' => 4, 351 | ], 352 | 'x' => 3, 353 | ], 354 | Arr::mergeDeep( 355 | [ 356 | 'a' => [ 357 | 'c' => [1], 358 | 'e' => 5, 359 | ], 360 | 'x' => 4, 361 | ], 362 | [ 363 | 'a' => [ 364 | 'c' => [2], 365 | 'f' => 4, 366 | ], 367 | 'x' => 3, 368 | ] 369 | ) 370 | ); 371 | } 372 | } 373 | -------------------------------------------------------------------------------- /tests/CollectionTest.php: -------------------------------------------------------------------------------- 1 | 'Hello']); 12 | $this->assertSame(['name' => 'Hello', 'id' => 1], $c->merge(['id' => 1])->all()); 13 | } 14 | 15 | public function testFirst() 16 | { 17 | $c = new Collection(['name' => 'Hello', 'age' => 25]); 18 | 19 | $this->assertSame('Hello', $c->first()); 20 | } 21 | 22 | public function testLast() 23 | { 24 | $c = new Collection(['name' => 'Hello', 'age' => 25]); 25 | 26 | $this->assertSame(25, $c->last()); 27 | } 28 | 29 | public function testToArray() 30 | { 31 | $c = new Collection(['name' => 'Hello', 'age' => 25]); 32 | 33 | $this->assertSame(['name' => 'Hello', 'age' => 25], $c->toArray()); 34 | } 35 | 36 | public function testToJson() 37 | { 38 | $c = new Collection(['name' => 'Hello', 'age' => 25]); 39 | 40 | $this->assertSame(json_encode(['name' => 'Hello', 'age' => 25]), $c->toJson()); 41 | $this->assertSame(json_encode(['name' => 'Hello', 'age' => 25]), (string) $c); 42 | $this->assertSame(json_encode(['name' => 'Hello', 'age' => 25]), json_encode($c)); 43 | } 44 | 45 | public function testSerialize() 46 | { 47 | $c = new Collection(['name' => 'Hello', 'age' => 25]); 48 | 49 | $sc = serialize($c); 50 | $c = unserialize($sc); 51 | 52 | $this->assertSame(['name' => 'Hello', 'age' => 25], $c->all()); 53 | } 54 | 55 | public function testGetIterator() 56 | { 57 | $c = new Collection(['name' => 'Hello', 'age' => 25]); 58 | 59 | $this->assertInstanceOf(\ArrayIterator::class, $c->getIterator()); 60 | 61 | $this->assertSame(['name' => 'Hello', 'age' => 25], $c->getIterator()->getArrayCopy()); 62 | } 63 | 64 | public function testCount() 65 | { 66 | $c = new Collection(['name' => 'Hello', 'age' => 25]); 67 | 68 | $this->assertCount(2, $c); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /tests/IsAssocTest.php: -------------------------------------------------------------------------------- 1 | assertFalse(Arr::isAssoc([])); 13 | } 14 | 15 | public function testSequentialArray() 16 | { 17 | // 顺序索引数组不是关联数组 18 | $this->assertFalse(Arr::isAssoc([1, 2, 3])); 19 | $this->assertFalse(Arr::isAssoc(['a', 'b', 'c'])); 20 | $this->assertFalse(Arr::isAssoc([null, false, true])); 21 | } 22 | 23 | public function testNonSequentialArray() 24 | { 25 | // 非顺序索引数组是关联数组 26 | $this->assertTrue(Arr::isAssoc([1 => 'a', 0 => 'b'])); // 键顺序不是0,1 27 | $this->assertTrue(Arr::isAssoc([1 => 'a', 2 => 'b'])); // 不是从0开始 28 | $this->assertTrue(Arr::isAssoc([0 => 'a', 2 => 'b'])); // 不连续 29 | } 30 | 31 | public function testStringKeys() 32 | { 33 | // 字符串键的数组是关联数组 34 | $this->assertTrue(Arr::isAssoc(['a' => 1, 'b' => 2])); 35 | // 注意:PHP会将字符串数字键'0'、'1'自动转换为整数键0、1 36 | // 所以这个实际上是顺序索引数组,不是关联数组 37 | $this->assertFalse(Arr::isAssoc(['0' => 'a', '1' => 'b'])); 38 | $this->assertTrue(Arr::isAssoc(['a' => 'a', 0 => 'b'])); // 混合键 39 | } 40 | 41 | public function testMixedKeys() 42 | { 43 | // 混合键类型的数组是关联数组 44 | $this->assertTrue(Arr::isAssoc([0 => 'a', 'b' => 'b'])); 45 | $this->assertTrue(Arr::isAssoc(['a' => 1, 2 => 'b'])); 46 | } 47 | 48 | } 49 | -------------------------------------------------------------------------------- /tests/MergeDeepTest.php: -------------------------------------------------------------------------------- 1 | ['b' => 2], 'c' => 3]; 13 | $array2 = ['a' => ['b' => 4, 'd' => 5], 'e' => 6]; 14 | 15 | $result = Arr::mergeDeep($array1, $array2); 16 | 17 | $expected = [ 18 | 'a' => ['b' => 4, 'd' => 5], 19 | 'c' => 3, 20 | 'e' => 6 21 | ]; 22 | 23 | $this->assertEquals($expected, $result); 24 | } 25 | 26 | public function testMergeDeepWithIndexedArrays() 27 | { 28 | // 测试索引数组的覆盖 29 | $array1 = ['a' => [1, 2, 3], 'b' => 2]; 30 | $array2 = ['a' => [4, 5], 'c' => 3]; 31 | 32 | $result = Arr::mergeDeep($array1, $array2); 33 | 34 | $expected = [ 35 | 'a' => [4, 5], // 索引数组应该被完全覆盖 36 | 'b' => 2, 37 | 'c' => 3 38 | ]; 39 | 40 | $this->assertEquals($expected, $result); 41 | } 42 | 43 | public function testMergeDeepWithMixedArrays() 44 | { 45 | // 测试混合数组类型 46 | $array1 = [ 47 | 'a' => ['b' => 2, 'c' => 3], 48 | 'd' => [1, 2, 3], 49 | 'e' => 4 50 | ]; 51 | 52 | $array2 = [ 53 | 'a' => ['b' => 5, 'f' => 6], 54 | 'd' => [7, 8], 55 | 'g' => 9 56 | ]; 57 | 58 | $result = Arr::mergeDeep($array1, $array2); 59 | 60 | $expected = [ 61 | 'a' => ['b' => 5, 'c' => 3, 'f' => 6], // 关联数组递归合并 62 | 'd' => [7, 8], // 索引数组被覆盖 63 | 'e' => 4, 64 | 'g' => 9 65 | ]; 66 | 67 | $this->assertEquals($expected, $result); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /tests/StrTest.php: -------------------------------------------------------------------------------- 1 | assertSame('fooBar', Str::camel('FooBar')); 11 | $this->assertSame('fooBar', Str::camel('FooBar')); 12 | $this->assertSame('fooBar', Str::camel('foo_bar')); 13 | $this->assertSame('fooBar', Str::camel('_foo_bar')); 14 | $this->assertSame('fooBar', Str::camel('_foo_bar_')); 15 | } 16 | 17 | public function testStudly() 18 | { 19 | $this->assertSame('FooBar', Str::studly('fooBar')); 20 | $this->assertSame('FooBar', Str::studly('_foo_bar')); 21 | $this->assertSame('FooBar', Str::studly('_foo_bar_')); 22 | $this->assertSame('FooBar', Str::studly('_foo_bar_')); 23 | } 24 | 25 | public function testSnake() 26 | { 27 | $this->assertSame('think_p_h_p_framework', Str::snake('ThinkPHPFramework')); 28 | $this->assertSame('think_php_framework', Str::snake('ThinkPhpFramework')); 29 | $this->assertSame('think php framework', Str::snake('ThinkPhpFramework', ' ')); 30 | $this->assertSame('think_php_framework', Str::snake('Think Php Framework')); 31 | $this->assertSame('think_php_framework', Str::snake('Think Php Framework ')); 32 | // ensure cache keys don't overlap 33 | $this->assertSame('think__php__framework', Str::snake('ThinkPhpFramework', '__')); 34 | $this->assertSame('think_php_framework_', Str::snake('ThinkPhpFramework_', '_')); 35 | $this->assertSame('think_php_framework', Str::snake('think php Framework')); 36 | $this->assertSame('think_php_frame_work', Str::snake('think php FrameWork')); 37 | // prevent breaking changes 38 | $this->assertSame('foo-bar', Str::snake('foo-bar')); 39 | $this->assertSame('foo-_bar', Str::snake('Foo-Bar')); 40 | $this->assertSame('foo__bar', Str::snake('Foo_Bar')); 41 | $this->assertSame('żółtałódka', Str::snake('ŻółtaŁódka')); 42 | } 43 | 44 | public function testTitle() 45 | { 46 | $this->assertSame('Welcome Back', Str::title('welcome back')); 47 | } 48 | 49 | public function testRandom() 50 | { 51 | $this->assertIsString(Str::random(10)); 52 | } 53 | 54 | public function testUpper() 55 | { 56 | $this->assertSame('USERNAME', Str::upper('username')); 57 | $this->assertSame('USERNAME', Str::upper('userNaMe')); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /tests/TestCase.php: -------------------------------------------------------------------------------- 1 | 10 | */ 11 | class TestCase extends BaseTestCase 12 | { 13 | } 14 | --------------------------------------------------------------------------------