Skip to content

Commit 03fd6c5

Browse files
Cache reflection and case conversion on URL hot path
Memoize hot lookups and tighten array helpers used during transformation URL building: - StringUtils: cache snakeCaseToCamelCase / camelCaseToSnakeCase results keyed by input (and separator when non-default). Inputs are bounded by SDK property/config names, so the caches saturate immediately. - ClassUtils: cache the unfiltered ReflectionClass::getConstants() result per class name. Exclusions are applied after the cache lookup so the cache is not polluted by per-call filters. - ArrayUtils: replace array_values()-based isAssoc with array_is_list(); add fast paths in safeFilterFunc for null and string; refactor safeImplode into a single foreach that only touches float values. These changes complement the cloudinary_php Configuration clone fast-path and together drop URL build cost from ~361 µs/op to ~28 µs/op on the profiling workload, with no measurable memory growth (caches saturate at ~7 KB total and never grow with workload).
1 parent 2f75344 commit 03fd6c5

7 files changed

Lines changed: 141 additions & 22 deletions

File tree

src/Utils/ArrayUtils.php

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -146,13 +146,13 @@ public static function implodeFiltered(
146146
*/
147147
public static function safeImplode(array|string $glue, array|null $pieces): string
148148
{
149-
if (! is_null($pieces)) {
150-
array_walk(
151-
$pieces,
152-
static function (&$value) {
149+
if ($pieces !== null) {
150+
foreach ($pieces as &$value) {
151+
if (is_float($value)) {
153152
$value = TransformationUtils::floatToString($value);
154153
}
155-
);
154+
}
155+
unset($value);
156156
}
157157

158158
return implode($glue, $pieces);
@@ -190,6 +190,14 @@ public static function escapedImplode(string|array $glue, array|null $pieces): s
190190
*/
191191
protected static function safeFilterFunc(mixed $value): bool|int
192192
{
193+
if ($value === null) {
194+
return 0;
195+
}
196+
197+
if (is_string($value)) {
198+
return $value !== '' ? 1 : 0;
199+
}
200+
193201
if (is_array($value)) {
194202
foreach ($value as $val) {
195203
if (self::safeFilterFunc($val)) {
@@ -200,11 +208,7 @@ protected static function safeFilterFunc(mixed $value): bool|int
200208
return false;
201209
}
202210

203-
if (is_null($value)) {
204-
return 0;
205-
}
206-
207-
return strlen($value);
211+
return strlen((string) $value);
208212
}
209213

210214
/**
@@ -439,7 +443,7 @@ public static function isAssoc(mixed $array): bool
439443
return false;
440444
}
441445

442-
return $array !== array_values($array);
446+
return ! array_is_list($array);
443447
}
444448

445449
/**

src/Utils/ClassUtils.php

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,11 @@
2020
*/
2121
class ClassUtils
2222
{
23+
/**
24+
* @var array<class-string, array>
25+
*/
26+
private static array $constantsCache = [];
27+
2328
/**
2429
* Gets class name from the instance object.
2530
*
@@ -58,16 +63,24 @@ public static function getBaseName(string $className): string
5863
*/
5964
public static function getConstants(object|string $instance, array $exclusions = []): array
6065
{
61-
$constants = [];
66+
$className = is_object($instance) ? $instance::class : $instance;
67+
68+
if (! isset(self::$constantsCache[$className])) {
69+
$constants = [];
70+
try {
71+
$reflectionClass = new ReflectionClass($className);
72+
$constants = array_values($reflectionClass->getConstants());
73+
} catch (ReflectionException) {
74+
//TODO: log it?
75+
}
76+
self::$constantsCache[$className] = $constants;
77+
}
6278

63-
try {
64-
$reflectionClass = new ReflectionClass($instance);
65-
$constants = array_values($reflectionClass->getConstants());
66-
} catch (ReflectionException) {
67-
//TODO: log it?
79+
if ($exclusions === []) {
80+
return self::$constantsCache[$className];
6881
}
6982

70-
return array_diff($constants, $exclusions);
83+
return array_diff(self::$constantsCache[$className], $exclusions);
7184
}
7285

7386
/**

src/Utils/StringUtils.php

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,16 @@ class StringUtils
1919
{
2020
public const MAX_STRING_LENGTH = 255;
2121

22+
/**
23+
* @var array<string, string>
24+
*/
25+
private static array $camelToSnakeCache = [];
26+
27+
/**
28+
* @var array<string, string>
29+
*/
30+
private static array $snakeToCamelCache = [];
31+
2232
/**
2333
* Converts camelCase to snake_case.
2434
*
@@ -28,13 +38,18 @@ class StringUtils
2838
*/
2939
public static function camelCaseToSnakeCase(string $input, string $separator = '_'): string
3040
{
41+
$cacheKey = $separator === '_' ? $input : $input . "\0" . $separator;
42+
if (isset(self::$camelToSnakeCache[$cacheKey])) {
43+
return self::$camelToSnakeCache[$cacheKey];
44+
}
45+
3146
$val = preg_replace('/(?<!^)[A-Z]/', $separator . '$0', $input);
3247

33-
if (is_null($val)) {
34-
return $input;
48+
if ($val === null) {
49+
return self::$camelToSnakeCache[$cacheKey] = $input;
3550
}
3651

37-
return strtolower($val);
52+
return self::$camelToSnakeCache[$cacheKey] = strtolower($val);
3853
}
3954

4055
/**
@@ -46,7 +61,14 @@ public static function camelCaseToSnakeCase(string $input, string $separator = '
4661
*/
4762
public static function snakeCaseToCamelCase(string $input, string $separator = '_'): string
4863
{
49-
return lcfirst(str_replace($separator, '', ucwords($input, $separator)));
64+
$cacheKey = $separator === '_' ? $input : $input . "\0" . $separator;
65+
if (isset(self::$snakeToCamelCache[$cacheKey])) {
66+
return self::$snakeToCamelCache[$cacheKey];
67+
}
68+
69+
return self::$snakeToCamelCache[$cacheKey] = lcfirst(
70+
str_replace($separator, '', ucwords($input, $separator))
71+
);
5072
}
5173

5274
/**

tests/Unit/TestHelpers/TestClassA.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@
1515
*/
1616
class TestClassA
1717
{
18+
public const FOO = 'foo';
19+
public const BAR = 'bar';
20+
public const BAZ = 'baz';
21+
1822
protected $args;
1923

2024
/**

tests/Unit/Utils/ArrayUtilsTest.php

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,4 +73,25 @@ public function testSafeImplode()
7373
ArrayUtils::implodeFiltered(',', ['s', 1, 1.0, 1.1, '1.0', null])
7474
);
7575
}
76+
77+
public function testSafeImplodeWithoutFloats()
78+
{
79+
self::assertSame('a,b,c', ArrayUtils::safeImplode(',', ['a', 'b', 'c']));
80+
self::assertSame('1,2,3', ArrayUtils::safeImplode(',', [1, 2, 3]));
81+
self::assertSame('', ArrayUtils::safeImplode(',', []));
82+
}
83+
84+
public function testIsAssoc()
85+
{
86+
self::assertFalse(ArrayUtils::isAssoc([]));
87+
self::assertFalse(ArrayUtils::isAssoc(['a', 'b', 'c']));
88+
self::assertFalse(ArrayUtils::isAssoc([0 => 'a', 1 => 'b', 2 => 'c']));
89+
90+
self::assertTrue(ArrayUtils::isAssoc(['a' => 1]));
91+
self::assertTrue(ArrayUtils::isAssoc([1 => 'a', 2 => 'b']));
92+
self::assertTrue(ArrayUtils::isAssoc([0 => 'a', 2 => 'c']));
93+
94+
self::assertFalse(ArrayUtils::isAssoc('not_an_array'));
95+
self::assertFalse(ArrayUtils::isAssoc(null));
96+
}
7697
}

tests/Unit/Utils/ClassUtilsTest.php

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,5 +56,41 @@ public function testVerifyInstance()
5656
);
5757
}
5858

59+
public function testGetConstantsReturnsAllConstants()
60+
{
61+
$constants = ClassUtils::getConstants(TestClassA::class);
62+
63+
sort($constants);
64+
65+
self::assertSame(['bar', 'baz', 'foo'], $constants);
66+
}
67+
68+
public function testGetConstantsAppliesExclusions()
69+
{
70+
$constants = ClassUtils::getConstants(TestClassA::class, ['foo']);
71+
72+
sort($constants);
73+
74+
self::assertSame(['bar', 'baz'], $constants);
75+
}
76+
77+
public function testGetConstantsAcceptsInstanceAndClassNameInterchangeably()
78+
{
79+
self::assertSame(
80+
ClassUtils::getConstants(TestClassA::class),
81+
ClassUtils::getConstants(new TestClassA())
82+
);
83+
}
84+
85+
public function testGetConstantsCacheDoesNotLeakExclusions()
86+
{
87+
ClassUtils::getConstants(TestClassA::class, ['foo']);
88+
89+
$all = ClassUtils::getConstants(TestClassA::class);
90+
sort($all);
91+
92+
self::assertSame(['bar', 'baz', 'foo'], $all);
93+
}
94+
5995
// TODO: add tests for all ClassUtils functions
6096
}

tests/Unit/Utils/StringUtilsTest.php

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,25 @@ public function testCamelCaseToSnakeCase()
9999
);
100100
}
101101

102+
public function testCaseConversionsAreStableAcrossRepeatedCalls()
103+
{
104+
for ($i = 0; $i < 3; $i++) {
105+
self::assertSame('test_string', StringUtils::camelCaseToSnakeCase('testString'));
106+
self::assertSame('test@string', StringUtils::camelCaseToSnakeCase('testString', '@'));
107+
self::assertSame('testString', StringUtils::snakeCaseToCamelCase('test_string'));
108+
self::assertSame('testString', StringUtils::snakeCaseToCamelCase('test@string', '@'));
109+
}
110+
}
111+
112+
public function testCaseConversionsKeepSeparatorIsolation()
113+
{
114+
self::assertSame('test_string', StringUtils::camelCaseToSnakeCase('testString'));
115+
self::assertSame('test@string', StringUtils::camelCaseToSnakeCase('testString', '@'));
116+
117+
self::assertSame('testString', StringUtils::snakeCaseToCamelCase('test_string'));
118+
self::assertSame('testString', StringUtils::snakeCaseToCamelCase('test@string', '@'));
119+
}
120+
102121
public function testToAcronym()
103122
{
104123
self::assertEquals(

0 commit comments

Comments
 (0)