├── .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 | [](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 |
--------------------------------------------------------------------------------