Skip to content

Commit 2e3c4d8

Browse files
authored
Performance improvements
1 parent c373c01 commit 2e3c4d8

10 files changed

Lines changed: 169 additions & 10 deletions

File tree

src/Api/ApiClient.php

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -424,7 +424,12 @@ protected function buildHttpClientConfig(): array
424424
];
425425
}
426426

427-
return array_merge_recursive($clientConfig, $authConfig);
427+
if (isset($authConfig['headers'])) {
428+
$clientConfig['headers'] = array_merge($clientConfig['headers'], $authConfig['headers']);
429+
unset($authConfig['headers']);
430+
}
431+
432+
return array_merge($clientConfig, $authConfig);
428433
}
429434

430435
/**

src/Asset/AssetFinalizerTrait.php

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -186,7 +186,7 @@ protected function finalizeSource(): string
186186
{
187187
$source = $this->asset->publicId(true);
188188

189-
if (! preg_match('/^https?:\//i', $source)) {
189+
if (stripos($source, 'http:/') !== 0 && stripos($source, 'https:/') !== 0) {
190190
$source = rawurldecode($source);
191191
}
192192

@@ -212,10 +212,14 @@ protected function finalizeVersion(): ?string
212212

213213
if (empty($version) && $this->urlConfig->forceVersion
214214
&& ! empty($this->asset->location)
215-
&& ! preg_match('/^https?:\//', $this->asset->publicId())
216-
&& ! preg_match('/^v\d+/', $this->asset->publicId())
217215
) {
218-
$version = '1';
216+
$publicId = $this->asset->publicId();
217+
if (strncmp($publicId, 'http:/', 6) !== 0
218+
&& strncmp($publicId, 'https:/', 7) !== 0
219+
&& ! preg_match('/^v\d+/', $publicId)
220+
) {
221+
$version = '1';
222+
}
219223
}
220224

221225
return $version ? 'v' . $version : null;

src/Asset/AuthToken.php

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,13 @@ public function isEnabled(): bool
8585
*/
8686
public function configuration(mixed $configuration): static
8787
{
88-
$tempConfiguration = new Configuration($configuration, false); // TODO: improve performance here
88+
if ($configuration instanceof Configuration) {
89+
$this->config = clone $configuration->authToken;
90+
91+
return $this;
92+
}
93+
94+
$tempConfiguration = new Configuration($configuration, false);
8995

9096
$this->config = $tempConfiguration->authToken;
9197

src/Asset/BaseAsset.php

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -266,7 +266,15 @@ public function importJson(array|string $json): static
266266
*/
267267
public function configuration(Configuration|array $configuration): static
268268
{
269-
$tempConfiguration = new Configuration($configuration, true); // TODO: improve performance here
269+
if ($configuration instanceof Configuration) {
270+
$this->cloud = clone $configuration->cloud;
271+
$this->urlConfig = clone $configuration->url;
272+
$this->logging = clone $configuration->logging;
273+
274+
return $this;
275+
}
276+
277+
$tempConfiguration = new Configuration($configuration, true);
270278
$this->cloud = $tempConfiguration->cloud;
271279
$this->urlConfig = $tempConfiguration->url;
272280
$this->logging = $tempConfiguration->logging;
@@ -372,7 +380,7 @@ public function jsonSerialize(bool $includeEmptyKeys = false, bool $includeEmpty
372380
if (! $includeEmptySections && empty(array_values($section)[0])) {
373381
continue;
374382
}
375-
$json = array_merge($json, $section);
383+
$json += $section;
376384
}
377385

378386
return $json;

src/Asset/Descriptor/AssetDescriptor.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -162,7 +162,7 @@ public function setSuffix(?string $suffix): static
162162
return $this;
163163
}
164164

165-
if (preg_match('/[.\/]/', $suffix)) {
165+
if (str_contains($suffix, '.') || str_contains($suffix, '/')) {
166166
throw new \UnexpectedValueException(static::class . '::$suffix must not include . or /');
167167
}
168168

src/Configuration/Configuration.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -331,7 +331,7 @@ public function jsonSerialize(
331331
if (! $includeEmptySections && empty(array_values($section)[0])) {
332332
continue;
333333
}
334-
$json = array_merge($json, $section);
334+
$json += $section;
335335
}
336336

337337
return $json;

tests/Unit/Admin/OAuthTest.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,12 @@ public function testOauthTokenAdminApi()
4343
'Authorization' => ['Bearer ' . self::FAKE_OAUTH_TOKEN]
4444
]
4545
);
46+
47+
self::assertArrayHasKey(
48+
'User-Agent',
49+
$lastRequest->getHeaders(),
50+
'User-Agent header must be present alongside Authorization when using OAuth token'
51+
);
4652
}
4753

4854
/**

tests/Unit/Configuration/ConfigurationTest.php

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
namespace Cloudinary\Test\Unit\Configuration;
1212

13+
use Cloudinary\Asset\Image;
1314
use Cloudinary\Configuration\Configuration;
1415
use Cloudinary\Test\Unit\UnitTestCase;
1516
use Cloudinary\Utils;
@@ -152,4 +153,28 @@ public function testConfigJsonSerialize()
152153
json_encode(Configuration::fromJson($expectedJsonConfig))
153154
);
154155
}
156+
157+
public function testAssetConfigurationIsIndependentFromGlobalConfig()
158+
{
159+
$globalConfig = Configuration::instance();
160+
$originalCloudName = $globalConfig->cloud->cloudName;
161+
$originalSecure = $globalConfig->url->secure;
162+
163+
$image = new Image('sample.png');
164+
165+
// Mutating the asset's config sections must not affect the global configuration
166+
$image->cloud->cloudName = 'mutated_cloud';
167+
$image->urlConfig->secure = ! $originalSecure;
168+
169+
self::assertEquals(
170+
$originalCloudName,
171+
$globalConfig->cloud->cloudName,
172+
'Mutating asset cloud config must not affect the global Configuration'
173+
);
174+
self::assertEquals(
175+
$originalSecure,
176+
$globalConfig->url->secure,
177+
'Mutating asset url config must not affect the global Configuration'
178+
);
179+
}
155180
}

tests/Unit/Upload/OAuthTest.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,12 @@ public function testOauthTokenUploadApi()
4747
'Authorization' => ['Bearer ' . self::FAKE_OAUTH_TOKEN]
4848
]
4949
);
50+
51+
self::assertArrayHasKey(
52+
'User-Agent',
53+
$lastRequest->getHeaders(),
54+
'User-Agent header must be present alongside Authorization when using OAuth token'
55+
);
5056
}
5157

5258
/**

tools/benchmark.php

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
<?php
2+
/**
3+
* Micro-benchmark for hot paths in the Cloudinary PHP SDK.
4+
*
5+
* Usage:
6+
* php tools/benchmark.php
7+
*
8+
* Run on each branch/stash to compare before/after.
9+
*/
10+
11+
require_once __DIR__ . '/../vendor/autoload.php';
12+
13+
use Cloudinary\Asset\Image;
14+
use Cloudinary\Asset\Video;
15+
use Cloudinary\Configuration\Configuration;
16+
17+
// ── Setup ────────────────────────────────────────────────────────────────────
18+
19+
putenv('CLOUDINARY_URL=cloudinary://api_key:api_secret@my_cloud');
20+
Configuration::instance()->init();
21+
Configuration::instance()->url->analytics(false);
22+
23+
const ITERATIONS = 10_000;
24+
const RUNS = 3;
25+
26+
// ── Benchmark helpers ─────────────────────────────────────────────────────────
27+
28+
function bench(string $label, callable $fn): void
29+
{
30+
$times = [];
31+
32+
for ($run = 0; $run < RUNS; $run++) {
33+
$start = hrtime(true);
34+
for ($i = 0; $i < ITERATIONS; $i++) {
35+
$fn();
36+
}
37+
$times[] = (hrtime(true) - $start) / 1e6; // ns → ms
38+
}
39+
40+
$avg = array_sum($times) / count($times);
41+
$min = min($times);
42+
$max = max($times);
43+
44+
printf(
45+
" %-50s avg: %7.2f ms min: %7.2f ms max: %7.2f ms\n",
46+
$label,
47+
$avg,
48+
$min,
49+
$max
50+
);
51+
}
52+
53+
// ── Scenarios ─────────────────────────────────────────────────────────────────
54+
55+
echo str_repeat('', 90) . "\n";
56+
echo sprintf(" Cloudinary PHP SDK benchmark — %d iterations × %d runs\n", ITERATIONS, RUNS);
57+
echo str_repeat('', 90) . "\n";
58+
59+
// 1. Asset construction (exercises configuration() fast path)
60+
bench('new Image($source)', function () {
61+
$img = new Image('sample/image.jpg');
62+
});
63+
64+
// 2. Asset construction + URL generation (exercises finalizeSource, finalizeVersion)
65+
bench('(string) new Image($source)', function () {
66+
$img = (string) new Image('sample/image.jpg');
67+
});
68+
69+
// 3. URL generation on a pre-built asset (isolates toUrl() overhead)
70+
$image = new Image('sample/image.jpg');
71+
bench('$image->toUrl() [pre-built asset]', function () use ($image) {
72+
$url = (string) $image->toUrl();
73+
});
74+
75+
// 4. Asset with suffix (exercises setSuffix + finalizeAssetType)
76+
bench('new Image + setSuffix()', function () {
77+
$img = new Image('sample/image.jpg');
78+
$img->asset->suffix = 'my-seo-name';
79+
$url = (string) $img->toUrl();
80+
});
81+
82+
// 5. Video asset (different asset type path)
83+
bench('(string) new Video($source)', function () {
84+
$img = (string) new Video('sample/video.mp4');
85+
});
86+
87+
// 6. Configuration::jsonSerialize() (exercises array_merge → += fix)
88+
$config = Configuration::instance();
89+
bench('Configuration::jsonSerialize()', function () use ($config) {
90+
$config->jsonSerialize();
91+
});
92+
93+
// 7. Asset construction from Configuration array (slow path, for comparison)
94+
$configArray = $config->jsonSerialize();
95+
bench('new Image($source, $configArray) [array config]', function () use ($configArray) {
96+
$img = new Image('sample/image.jpg', $configArray);
97+
});
98+
99+
echo str_repeat('', 90) . "\n";

0 commit comments

Comments
 (0)