Skip to content

Feature laravel13#91

Merged
StanBarrows merged 9 commits into
mainfrom
feature-laravel13
Apr 3, 2026
Merged

Feature laravel13#91
StanBarrows merged 9 commits into
mainfrom
feature-laravel13

Conversation

@StanBarrows
Copy link
Copy Markdown
Contributor

No description provided.

Copilot AI review requested due to automatic review settings April 3, 2026 14:43
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR updates the package for Laravel 13 / Flysystem v3, refactors the Cloudinary adapter into smaller components, and modernizes the test suite and tooling to align with the new Flysystem exception/return-type expectations.

Changes:

  • Upgrade support matrix and tooling: Laravel 13, PHP 8.3–8.5, Pest 4, Testbench 11, Larastan 3.9; update CI workflows accordingly.
  • Refactor FlysystemCloudinaryAdapter to use new collaborators (CloudinaryDiskOptions, CloudinaryPathNormalizer, CloudinaryResponseMapper, CloudinaryResourceOperations, etc.) and to throw Flysystem v3 exceptions for failure cases.
  • Restructure tests: split the previous adapter test into focused unit/feature tests; add guarded live integration tests and update docs/config examples.

Reviewed changes

Copilot reviewed 42 out of 42 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
src/FlysystemCloudinaryServiceProvider.php Passes resolved disk options into the adapter when registering the Storage driver.
src/FlysystemCloudinaryAdapter.php Major refactor to Flysystem v3 behavior (exceptions, attributes, shallow listing), and delegates work to new helper classes.
src/Concerns/InteractsWithCloudinaryMetadata.php Extracts metadata retrieval behavior used by the adapter.
src/CloudinaryUrlBuilder.php Centralizes URL building and explicit-url selection.
src/CloudinaryStringUploadSource.php Encapsulates temp-file creation for string uploads.
src/CloudinaryResponseMapper.php Centralizes response normalization and attribute mapping.
src/CloudinaryResponseLogger.php Centralizes dispatching response log events.
src/CloudinaryResourceOperations.php Encapsulates explicit/destroy resource-type iteration and logging.
src/CloudinaryPathNormalizer.php Normalizes logical vs folder-prefixed Cloudinary public IDs.
src/CloudinaryListResponseAssembler.php Builds shallow list results as FileAttributes/DirectoryAttributes.
src/CloudinaryDiskOptions.php Resolves disk/published config and merges upload options deterministically.
src/CloudinaryAdminFolderLocator.php Implements paginated folder existence checks via Admin API.
tests/Pest.php Adds integration helper include, groups integration tests, and sets adapter fixtures for feature adapter tests.
tests/cloudinary_integration.php Adds helper to skip live integration tests unless real credentials are present.
tests/Integration/CloudinaryTest.php Updates integration assertions to Flysystem v3 exception behavior and adds additional cases.
tests/Feature/FlysystemCloudinaryServiceProviderTest.php Verifies the Storage driver registers and yields the expected adapter.
tests/Feature/AdapterTest.php Removes monolithic adapter test in favor of split feature tests.
tests/Feature/Adapter/UrlTest.php Feature tests for URL building behavior.
tests/Feature/Adapter/UploadWriteTest.php Feature tests for write/update and metadata behavior.
tests/Feature/Adapter/RenameCopyTest.php Feature tests for rename/copy behavior and failure exceptions.
tests/Feature/Adapter/ReadListHasTest.php Feature tests for readStream/listing/has behavior.
tests/Feature/Adapter/DeleteDirectoryTest.php Feature tests for delete/deleteDirectory behaviors and folder prefixing.
tests/Unit/InteractsWithCloudinaryMetadataTest.php Unit coverage for metadata mapping and exception wrapping.
tests/Unit/FlysystemCloudinaryResponseLogTest.php Unit test for event payload storage.
tests/Unit/CloudinaryUrlBuilderTest.php Unit tests for URL builder behaviors and event dispatching.
tests/Unit/CloudinaryStringUploadSourceTest.php Unit tests for temp source creation for string uploads.
tests/Unit/CloudinaryResponseMapperTest.php Unit tests for normalization/mapping edge cases.
tests/Unit/CloudinaryResponseLoggerTest.php Unit test for dispatching response log events.
tests/Unit/CloudinaryResourceOperationsTest.php Unit tests for resource-type iteration and destroy semantics.
tests/Unit/CloudinaryPathNormalizerTest.php Unit tests for folder prefixing/logical path conversion.
tests/Unit/CloudinaryListResponseAssemblerTest.php Unit tests for assembling shallow lists to Flysystem attributes.
tests/Unit/CloudinaryDiskOptionsTest.php Unit tests for disk/published config resolution and merge order.
tests/Unit/CloudinaryAdminFolderLocatorTest.php Unit tests for paginated folder existence checks.
README.md Updates Laravel 13/Flysystem 3 docs, exception semantics, testing commands, and config examples.
phpunit.xml.dist Switches to FILESYSTEM_DISK placeholder and keeps Cloudinary env placeholders.
composer.json Bumps platform/framework/tooling versions; updates scripts to Pest/PHPStan/Pint.
CHANGELOG.md Documents 13.0.0 changes and behavioral differences.
.github/workflows/run-tests.yml Updates test matrix (PHP/Laravel), config prep step, and test command.
.github/workflows/phpstan.yml Bumps PHP version used for static analysis.
.github/workflows/fix-php-code-style-issues.yml Switches to explicit PHP setup + composer install + pint run.
.github/workflows/composer-audit.yml Bumps PHP version used for dependency audit.
.github/ISSUE_TEMPLATE/bug_report.yml Updates placeholders for PHP/Laravel versions.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/CloudinaryUrlBuilder.php Outdated
Comment on lines +39 to +46
try {
$response = $this->resourceOps->explicit($this->cloudinary, $prefixed);
} catch (NotFound) {
return false;
}

$this->logger->log($response);

Copy link

Copilot AI Apr 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CloudinaryResourceOperations::explicit() already logs the successful ApiResponse before returning it. Logging again here causes duplicate FlysystemCloudinaryResponseLog events for the same response (and forces callers/tests to expect 2 dispatches). Consider removing this extra log (and the $logger dependency) so each API response is logged exactly once by the component that performs the call.

Copilot uses AI. Check for mistakes.
}

$folder = trim(trim($this->folder), '/');

Copy link

Copilot AI Apr 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

prefixed() always concatenates "{$folder}/{$path}" when a folder is configured, which yields a trailing slash when $path is empty (e.g. listing root with a configured folder becomes folder/). Cloudinary Admin API folder and prefix parameters are typically slash-sensitive; returning folder (no trailing slash) for an empty path would be safer and avoids inconsistent identifiers.

Suggested change
if ($path === '') {
return $folder;
}

Copilot uses AI. Check for mistakes.
Comment on lines +52 to +54
expect($builder->urlViaExplicit('file'))->toBe('https://secure');
Event::assertDispatched(FlysystemCloudinaryResponseLog::class, 2);
});
Copy link

Copilot AI Apr 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This assertion currently expects 2 FlysystemCloudinaryResponseLog dispatches, but the implementation logs the same explicit ApiResponse twice (once inside CloudinaryResourceOperations::explicit() and again in CloudinaryUrlBuilder::urlViaExplicit()). If the duplicate logging is removed, update this expectation to 1 dispatch to match the intended "one event per API response" behavior.

Copilot uses AI. Check for mistakes.
Comment thread src/CloudinaryResponseMapper.php Outdated
return [
'contents' => $body,
'etag' => Arr::get($response, 'etag'),
'mimetype' => $this->mimeDetector->detectMimeType($logicalPath, $body) ?? 'text/plain',
Copy link

Copilot AI Apr 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FinfoMimeTypeDetector::detectMimeType() expects the file contents as a string (or null depending on implementation), but normalizeUploadOrExplicit() passes $body which can be a stream resource (e.g. writeStream() / updateStream()). This can cause a TypeError at runtime when the adapter is used with real stream resources. Consider guarding with is_string($body) (and passing null/'' otherwise), or using a stream/file-path based detector when $body is a resource.

Suggested change
'mimetype' => $this->mimeDetector->detectMimeType($logicalPath, $body) ?? 'text/plain',
'mimetype' => $this->mimeDetector->detectMimeType($logicalPath, is_string($body) ? $body : null) ?? 'text/plain',

Copilot uses AI. Check for mistakes.
Comment on lines +59 to +73

$adapter->writeStream($publicId, $contents, new Config);

$meta = $adapter->lastUploadMetadata();
$this->assertSame($contents, $meta['contents']);
$this->assertSame('::etag::', $meta['etag']);
$this->assertSame('text/plain', $meta['mimetype']);
$this->assertSame($publicId, $meta['path']);
$this->assertSame(789, $meta['size']);
$this->assertSame(1633860610, $meta['timestamp']);
$this->assertSame('file', $meta['type']);
$this->assertSame(123456, $meta['version']);
$this->assertSame('::version-id::', $meta['versionid']);
$this->assertSame('public', $meta['visibility']);
Event::assertDispatched(FlysystemCloudinaryResponseLog::class, 1);
Copy link

Copilot AI Apr 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test passes a string as the $resource argument to writeStream(), but the Flysystem adapter contract expects a stream resource. Using a real resource (e.g. fopen('php://temp', 'r+') and writing $contents into it) would better reflect real usage and would catch stream-specific issues (like MIME detection / rewind handling).

Suggested change
$adapter->writeStream($publicId, $contents, new Config);
$meta = $adapter->lastUploadMetadata();
$this->assertSame($contents, $meta['contents']);
$this->assertSame('::etag::', $meta['etag']);
$this->assertSame('text/plain', $meta['mimetype']);
$this->assertSame($publicId, $meta['path']);
$this->assertSame(789, $meta['size']);
$this->assertSame(1633860610, $meta['timestamp']);
$this->assertSame('file', $meta['type']);
$this->assertSame(123456, $meta['version']);
$this->assertSame('::version-id::', $meta['versionid']);
$this->assertSame('public', $meta['visibility']);
Event::assertDispatched(FlysystemCloudinaryResponseLog::class, 1);
$resource = fopen('php://temp', 'r+');
fwrite($resource, $contents);
rewind($resource);
try {
$adapter->writeStream($publicId, $resource, new Config);
$meta = $adapter->lastUploadMetadata();
$this->assertSame($contents, $meta['contents']);
$this->assertSame('::etag::', $meta['etag']);
$this->assertSame('text/plain', $meta['mimetype']);
$this->assertSame($publicId, $meta['path']);
$this->assertSame(789, $meta['size']);
$this->assertSame(1633860610, $meta['timestamp']);
$this->assertSame('file', $meta['type']);
$this->assertSame(123456, $meta['version']);
$this->assertSame('::version-id::', $meta['versionid']);
$this->assertSame('public', $meta['visibility']);
Event::assertDispatched(FlysystemCloudinaryResponseLog::class, 1);
} finally {
fclose($resource);
}

Copilot uses AI. Check for mistakes.
Comment on lines +107 to +124
it('can update stream', function () {
$publicId = '::file-path::';
$contents = '::file-contents::';
$mock = $this->mock(Cloudinary::class, function (MockInterface $mock) use ($publicId) {
$mock->shouldReceive('uploadApi->upload')->once()->andReturn(new ApiResponse([
'public_id' => $publicId,
'version' => 123456,
'version_id' => '::version-id::',
'created_at' => '2021-10-10T10:10:10Z',
'bytes' => 789,
'etag' => '::etag::',
'access_mode' => 'public',
], []));
});
$adapter = new FlysystemCloudinaryAdapter($mock);

$meta = $adapter->updateStream($publicId, $contents, new Config);

Copy link

Copilot AI Apr 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test passes a string as the $resource argument to updateStream(), but updateStream() is meant to receive a stream resource. Switching the test to provide an actual resource would exercise the intended code path and help prevent regressions specific to stream uploads.

Copilot uses AI. Check for mistakes.
Comment on lines +574 to +577
if (fwrite($tempFile, $contents) === false) {
throw UnableToReadFile::fromLocation($errorPath);
}
if (rewind($tempFile) === false) {
Copy link

Copilot AI Apr 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

contentsToTempStream() can leak the temporary stream resource on error: when fwrite() or rewind() fails, an exception is thrown without fclose($tempFile). Consider closing the handle before throwing in those failure branches to avoid resource leaks in long-running processes.

Suggested change
if (fwrite($tempFile, $contents) === false) {
throw UnableToReadFile::fromLocation($errorPath);
}
if (rewind($tempFile) === false) {
if (fwrite($tempFile, $contents) === false) {
fclose($tempFile);
throw UnableToReadFile::fromLocation($errorPath);
}
if (rewind($tempFile) === false) {
fclose($tempFile);

Copilot uses AI. Check for mistakes.
@StanBarrows StanBarrows merged commit 920cd89 into main Apr 3, 2026
11 of 13 checks passed
@StanBarrows StanBarrows deleted the feature-laravel13 branch April 3, 2026 15:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants