Skip to content

Commit 108958c

Browse files
authored
Add arrays rename key (#45)
1 parent 96627fe commit 108958c

File tree

10 files changed

+205
-3
lines changed

10 files changed

+205
-3
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ A library with everyday use classes and methods:
2121
- `mapAssoc` - Map an array to a new array using a callback, preserving keys.
2222
- `reindex` - Reindex an array with new keys based on the result of the callback.
2323
- `groupBy` - Group items by the given return value of the callback.
24+
- `renameKey` - Rename the `string` key of an array element.
2425
- `remove` - Remove an element from an array based on the callback. Supports `EquatableInterface`.
2526
- `removeKey` - Remove an element from an array based on the key.
2627
- `removeKeys` - Remove multiple elements from an array based on the keys.

extension.neon

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
11
services:
2+
-
3+
class: DR\Utils\PHPStan\Extension\ArraysRenameKeyReturnExtension
4+
tags:
5+
- phpstan.broker.dynamicStaticMethodReturnTypeExtension
26
-
37
class: DR\Utils\PHPStan\Extension\ArraysRemoveTypesReturnExtension
48
tags:

phpmd.baseline.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
<phpmd-baseline>
33
<violation rule="PHPMD\Rule\Design\WeightedMethodCount" file="src/Arrays.php"/>
44
<violation rule="PHPMD\Rule\Design\TooManyPublicMethods" file="src/Arrays.php"/>
5+
<violation rule="PHPMD\Rule\Design\TooManyMethods" file="src/Arrays.php"/>
56
<violation rule="PHPMD\Rule\Design\WeightedMethodCount" file="src/Assert.php"/>
67
<violation rule="PHPMD\Rule\Design\TooManyMethods" file="src/Assert.php"/>
78
<violation rule="PHPMD\Rule\Design\TooManyPublicMethods" file="src/Assert.php"/>

src/Arrays.php

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,41 @@ public static function remove(array $items, mixed $item): array
246246
return $items;
247247
}
248248

249+
/**
250+
* Rename the key to the given key. Order of the keys will be preserved if requested.
251+
* If the key does not exist the array will be returned as is.
252+
* @template T of mixed
253+
* @template K of array-key
254+
*
255+
* @param array<K, T> $items
256+
*
257+
* @return array<K, T>
258+
*/
259+
public static function renameKey(array $items, string $fromKey, string $toKey, bool $preserveOrder = false, bool $overwrite = false): array
260+
{
261+
if (array_key_exists($fromKey, $items) === false) {
262+
return $items;
263+
}
264+
265+
if ($overwrite === false && array_key_exists($toKey, $items)) {
266+
throw new InvalidArgumentException('The toKey "' . $toKey . '" already exists in the array');
267+
}
268+
269+
if ($preserveOrder) {
270+
$result = [];
271+
foreach ($items as $key => $value) {
272+
$result[$key === $fromKey ? $toKey : $key] = $value;
273+
}
274+
275+
return $result;
276+
}
277+
278+
$items[$toKey] = $items[$fromKey];
279+
unset($items[$fromKey]);
280+
281+
return $items;
282+
}
283+
249284
/**
250285
* Remove an item from the given array by the given key.
251286
* @template T of array<int|string, mixed>
@@ -463,7 +498,6 @@ public static function toJson(array $items, ?string $failureMessage = null): str
463498

464499
/**
465500
* Converts a JSON string to an array
466-
*
467501
* @return mixed[]
468502
*/
469503
public static function fromJson(string $jsonString, ?string $failureMessage = null): array
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
<?php
2+
declare(strict_types=1);
3+
4+
namespace DR\Utils\PHPStan\Extension;
5+
6+
use DR\Utils\Arrays;
7+
use PhpParser\Node\Expr\StaticCall;
8+
use PhpParser\Node\Scalar\String_;
9+
use PHPStan\Analyser\Scope;
10+
use PHPStan\Reflection\MethodReflection;
11+
use PHPStan\Type\Constant\ConstantArrayType;
12+
use PHPStan\Type\Constant\ConstantStringType;
13+
use PHPStan\Type\DynamicStaticMethodReturnTypeExtension;
14+
use PHPStan\Type\Type;
15+
16+
class ArraysRenameKeyReturnExtension implements DynamicStaticMethodReturnTypeExtension
17+
{
18+
public function getClass(): string
19+
{
20+
return Arrays::class;
21+
}
22+
23+
public function isStaticMethodSupported(MethodReflection $methodReflection): bool
24+
{
25+
return $methodReflection->getName() === 'renameKey';
26+
}
27+
28+
/**
29+
* @inheritDoc
30+
*/
31+
public function getTypeFromStaticMethodCall(MethodReflection $methodReflection, StaticCall $methodCall, Scope $scope): Type
32+
{
33+
[$items, $fromKey, $toKey] = $methodCall->getArgs();
34+
35+
$arrayType = $scope->getType($items->value);
36+
37+
if ($fromKey->value instanceof String_ === false ||
38+
$toKey->value instanceof String_ === false ||
39+
$arrayType instanceof ConstantArrayType === false
40+
) {
41+
return $scope->getType($items->value);
42+
}
43+
44+
$keyTypes = $arrayType->getKeyTypes();
45+
$fromKeyValue = $fromKey->value->value;
46+
$toKeyValue = $toKey->value->value;
47+
48+
$newKeyTypes = [];
49+
foreach ($keyTypes as $keyType) {
50+
if ($keyType instanceof ConstantStringType && $keyType->getValue() === $fromKeyValue) {
51+
$newKeyTypes[] = new ConstantStringType($toKeyValue);
52+
} else {
53+
$newKeyTypes[] = $keyType;
54+
}
55+
}
56+
57+
return new ConstantArrayType($newKeyTypes, $arrayType->getValueTypes());
58+
}
59+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<?php
2+
declare(strict_types=1);
3+
4+
namespace DR\Utils\Tests\Integration\PHPStan;
5+
6+
use DR\Utils\Assert;
7+
use DR\Utils\PHPStan\Extension\ArraysRemoveTypesReturnExtension;
8+
use DR\Utils\PHPStan\Extension\ArraysRenameKeyReturnExtension;
9+
use DR\Utils\PHPStan\Lib\TypeNarrower;
10+
use PHPStan\Testing\TypeInferenceTestCase;
11+
use PHPUnit\Framework\Attributes\CoversClass;
12+
13+
#[CoversClass(ArraysRenameKeyReturnExtension::class)]
14+
class ArraysRenameKeyReturnExtensionTest extends TypeInferenceTestCase
15+
{
16+
public function testFileAsserts(): void
17+
{
18+
$results = self::gatherAssertTypes(__DIR__ . '/data/ArraysRenameKeyReturnAssertions.php');
19+
foreach ($results as $result) {
20+
$assertType = Assert::string(array_shift($result));
21+
$file = Assert::string(array_shift($result));
22+
$this->assertFileAsserts($assertType, $file, ...$result);
23+
}
24+
}
25+
26+
/**
27+
* @return string[]
28+
*/
29+
public static function getAdditionalConfigFiles(): array
30+
{
31+
return [dirname(__DIR__, 3) . '/extension.neon'];
32+
}
33+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace DR\Utils\Tests\Integration\PHPStan\data;
6+
7+
use DR\Utils\Arrays;
8+
9+
use function PHPStan\Testing\assertType;
10+
11+
class ArraysRenameKeyReturnAssertions
12+
{
13+
public function assertions(): void
14+
{
15+
assertType("array{bar: 124}", Arrays::renameKey(['foo' => 124], 'foo', 'bar'));
16+
assertType("array{bar: 124}", Arrays::renameKey(['bar' => 124], 'foo', 'bar'));
17+
assertType("array{bar: 124, 0: 'baz'}", Arrays::renameKey(['foo' => 124, 'baz'], 'foo', 'bar'));
18+
19+
// assert dynamic key-value array
20+
/** @var array<string, int|string> $data */
21+
$data = ['foo' => 'bar', 'test' => 123];
22+
assertType("array<string, int|string>", Arrays::renameKey($data, 'foo', 'bar'));
23+
}
24+
}

tests/Unit/ArraysTest.php

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,26 @@ public function testRemove(): void
147147
static::assertSame([$eqObjA, $eqObjB], Arrays::remove([$eqObjA, $eqObjB], 'foobar'));
148148
}
149149

150+
public function testRenameKey(): void
151+
{
152+
static::assertSame(['foo' => 'bar'], Arrays::renameKey(['foo' => 'bar'], 'bar', 'foo'));
153+
static::assertSame(['bar' => 'bar'], Arrays::renameKey(['foo' => 'bar'], 'foo', 'bar'));
154+
155+
// preserve order yes/no
156+
static::assertSame(['foz' => 'baz', 'bar' => 'bar'], Arrays::renameKey(['foo' => 'bar', 'foz' => 'baz'], 'foo', 'bar'));
157+
static::assertSame(['bar' => 'bar', 'foz' => 'baz'], Arrays::renameKey(['foo' => 'bar', 'foz' => 'baz'], 'foo', 'bar', true));
158+
159+
// overwrite: yes
160+
static::assertSame(['foz' => 'bar'], Arrays::renameKey(['foo' => 'bar', 'foz' => 'baz'], 'foo', 'foz', false, true));
161+
}
162+
163+
public function testRenameKeyDisallowOverwrite(): void
164+
{
165+
$this->expectException(InvalidArgumentException::class);
166+
$this->expectExceptionMessage('The toKey "foz" already exists in the array');
167+
Arrays::renameKey(['foo' => 'bar', 'foz' => 'baz'], 'foo', 'foz');
168+
}
169+
150170
public function testRemoveKey(): void
151171
{
152172
static::assertSame(['foo' => 'bar'], Arrays::removeKey(['foo' => 'bar'], 'bar'));
@@ -298,7 +318,7 @@ public function testWrap(): void
298318
public function testRemoveTypes(): void
299319
{
300320
$input = [false, 0, '0', 'false', true, 1, '1', 'true'];
301-
static::assertEqualsCanonicalizing([0, '0', 'false', 1, '1', 'true'], Arrays::removeTypes($input, ['bool']));
321+
static::assertSame([0, '0', 'false', 1, '1', 'true'], array_values(Arrays::removeTypes($input, ['bool'])));
302322
static::assertSame(['null'], Arrays::removeTypes(['null', null], ['null']));
303323
static::assertSame(['1', 2.00], Arrays::removeTypes(['1', 2.00, 3], ['int']));
304324
static::assertSame([1, '2.00'], Arrays::removeTypes([1, '2.00', 3.00], ['float']));
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace DR\Utils\Tests\Unit\PHPStan\Extension;
6+
7+
use DR\Utils\Arrays;
8+
use DR\Utils\PHPStan\Extension\ArraysRemoveTypesReturnExtension;
9+
use DR\Utils\PHPStan\Extension\ArraysRenameKeyReturnExtension;
10+
use DR\Utils\PHPStan\Lib\TypeNarrower;
11+
use PHPUnit\Framework\Attributes\CoversClass;
12+
use PHPUnit\Framework\TestCase;
13+
14+
#[CoversClass(ArraysRenameKeyReturnExtension::class)]
15+
class ArraysRenameKeyReturnExtensionTest extends TestCase
16+
{
17+
public function testGetClass(): void
18+
{
19+
$extension = new ArraysRenameKeyReturnExtension();
20+
self::assertSame(Arrays::class, $extension->getClass());
21+
}
22+
}

tests/Unit/PHPStan/Lib/TypeNarrowerTest.php

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,11 @@ public function testGetTypesFromStringArray(): void
1818
{
1919
$arg = new Arg($this->createMock(Expr::class));
2020

21-
$narrower = new TypeNarrower($this->createMock(TypeStringResolver::class));
21+
$narrower = $this->getMockBuilder(TypeNarrower::class)
22+
->onlyMethods([])
23+
->disableOriginalConstructor()
24+
->getMock();
25+
2226
static::assertSame([], $narrower->getTypesFromStringArray($arg));
2327
}
2428
}

0 commit comments

Comments
 (0)