Skip to content

Commit 920cd89

Browse files
author
Sebastian Bürgin-Fix
authored
Merge pull request #91 from codebar-ag/feature-laravel13
Feature laravel13
2 parents 5abe662 + 714b582 commit 920cd89

42 files changed

Lines changed: 2321 additions & 840 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/ISSUE_TEMPLATE/bug_report.yml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,17 +29,17 @@ We're sorry to hear you have a problem. Can you help us solve it by providing th
2929
attributes:
3030
label: PHP Version
3131
description: What version of PHP are you running? Please be as specific as possible
32-
placeholder: "8.4.0"
33-
value: "8.4.0"
32+
placeholder: "8.5.0"
33+
value: "8.5.0"
3434
validations:
3535
required: true
3636
- type: input
3737
id: laravel-version
3838
attributes:
3939
label: Laravel Version
4040
description: What version of Laravel are you running? Please be as specific as possible
41-
placeholder: "12.0.0"
42-
value: "12.0.0"
41+
placeholder: "13.0.0"
42+
value: "13.0.0"
4343
validations:
4444
required: true
4545
- type: dropdown

.github/workflows/composer-audit.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ jobs:
2828
- name: Setup PHP
2929
uses: shivammathur/setup-php@2.37.0
3030
with:
31-
php-version: '8.4'
31+
php-version: '8.5'
3232
coverage: none
3333

3434
- name: Resolve dependencies and audit

.github/workflows/fix-php-code-style-issues.yml

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,17 @@ jobs:
1313
- name: Checkout code
1414
uses: actions/checkout@v6.0.2
1515

16-
- name: Fix PHP code style issues
17-
uses: aglipanci/laravel-pint-action@2.6
16+
- name: Setup PHP
17+
uses: shivammathur/setup-php@2.37.0
18+
with:
19+
php-version: '8.5'
20+
coverage: none
21+
22+
- name: Install composer dependencies
23+
uses: ramsey/composer-install@4.0.0
24+
25+
- name: Run Laravel Pint
26+
run: vendor/bin/pint
1827

1928
- name: Commit changes
2029
uses: stefanzweifel/git-auto-commit-action@v7.1.0

.github/workflows/phpstan.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ jobs:
3232
- name: Setup PHP
3333
uses: shivammathur/setup-php@2.37.0
3434
with:
35-
php-version: '8.4'
35+
php-version: '8.5'
3636
coverage: none
3737

3838
- name: Install composer dependencies

.github/workflows/run-tests.yml

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,18 +14,18 @@ jobs:
1414
max-parallel: 1
1515
matrix:
1616
os: [ ubuntu-latest ]
17-
php: [ 8.2, 8.3, 8.4 ]
18-
laravel: [ 12.* ]
17+
php: [ 8.3, 8.4, 8.5 ]
18+
laravel: [ 13.* ]
1919
stability: [ prefer-lowest, prefer-stable ]
2020

2121
name: P${{ matrix.php }} - L${{ matrix.laravel }} - ${{ matrix.stability }} - ${{ matrix.os }}
2222

2323
steps:
2424
- name: Checkout code
25-
uses: actions/checkout@v4
25+
uses: actions/checkout@v6.0.2
2626

2727
- name: Setup PHP
28-
uses: shivammathur/setup-php@v2
28+
uses: shivammathur/setup-php@2.37.0
2929
with:
3030
php-version: ${{ matrix.php }}
3131
extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, bcmath, soap, intl, gd, exif, iconv, imagick, fileinfo
@@ -41,11 +41,11 @@ jobs:
4141
composer require "laravel/framework:${{ matrix.laravel }}" --no-interaction --no-update
4242
composer update --${{ matrix.stability }} --prefer-dist --no-interaction
4343
44-
- name: Execute tests
44+
- name: Prepare phpunit config
4545
run: cp phpunit.xml.dist phpunit.xml
4646

4747
- name: Execute tests
48-
run: vendor/bin/pest
48+
run: vendor/bin/pest --no-coverage
4949
env:
5050
CLOUDINARY_CLOUD_NAME: ${{ secrets.CLOUDINARY_CLOUD_NAME }}
5151
CLOUDINARY_API_KEY: ${{ secrets.CLOUDINARY_API_KEY }}

CHANGELOG.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,25 @@
22

33
All notable changes to `laravel-cloudinary` will be documented in this file.
44

5+
## 13.0.0 - 2026-04-03
6+
7+
- Laravel 13 support
8+
- PHP 8.3, 8.4, and 8.5 support (PHP 8.2 dropped)
9+
- Development tooling: Orchestra Testbench 11, Pest 4, Larastan 3.9
10+
- Packagist `dev-main` branch alias `13.x-dev` for Composer until `v13.0.0` is tagged
11+
- **Flysystem 3:** `listContents()` now returns `FileAttributes` / `DirectoryAttributes` so `Storage::files()`, `Storage::directories()`, and related APIs work (fixes [#64](https://github.com/codebar-ag/laravel-flysystem-cloudinary/issues/64), [#80](https://github.com/codebar-ag/laravel-flysystem-cloudinary/issues/80))
12+
- **Flysystem 3:** `read()` / `readStream()` throw `UnableToReadFile` on failure; `readStream()` returns a `resource`; `copy()` throws `UnableToCopyFile`; `delete()` throws `UnableToDeleteFile` when destroy fails; `createDirectory()` / `deleteDirectory()` throw the corresponding Flysystem exceptions
13+
- Apply configured folder prefix to `move()`, `createDirectory()`, `deleteDirectory()`, and fix `directoryExists()` for root-level paths
14+
- Fix string uploads: rewind temp stream after `fwrite()` before Cloudinary `upload()`
15+
- `createDir()` catches `ApiError` as well as `RateLimited`
16+
- Composer scripts: `analyse` (PHPStan), `test` / `test-coverage` via Pest with `--exclude-group=integration`; `format` uses Pint
17+
- Integration tests: `integration` group, skip when placeholder Cloudinary env is used
18+
- **Adapter internals:** Extracted `CloudinaryPathNormalizer`, `CloudinaryDiskOptions`, `CloudinaryResponseMapper`, `CloudinaryResourceOperations`, and `CloudinaryResponseLogger`; disk options (folder, upload preset, merge options, secure URL preference) are resolved from the Laravel disk config in `FlysystemCloudinaryServiceProvider` with fallback to `config('flysystem-cloudinary.*')` when the adapter is constructed without disk options
19+
- **API:** Added `lastUploadMetadata()`, `lastCopySucceeded()`, and `lastDeleteSucceeded()`; public `$meta`, `$copied`, and `$deleted` remain for backward compatibility
20+
- **Adapter slimming:** `CloudinaryStringUploadSource`, `CloudinaryAdminFolderLocator`, `CloudinaryUrlBuilder`, `CloudinaryListResponseAssembler`, `CloudinaryDiskOptions::uploadOptionsFor()`, and `Concerns\InteractsWithCloudinaryMetadata` further shrink `FlysystemCloudinaryAdapter` and move branching out of the adapter file
21+
- **Security:** `phpunit.xml` no longer ships real Cloudinary API secrets; default env is placeholder-only. Live API integration tests run only when real `CLOUDINARY_*` values are exported (see `tests/cloudinary_integration.php`)
22+
- **Tests:** Dedicated Pest files per collaborator (`CloudinaryResourceOperations`, `CloudinaryResponseMapper`, `CloudinaryResponseLogger`, `CloudinaryUrlBuilder`, `CloudinaryListResponseAssembler`, `CloudinaryAdminFolderLocator`, `CloudinaryDiskOptions`, `FlysystemCloudinaryResponseLog`, `FlysystemCloudinaryServiceProvider`); extra integration cases for missing files, directories, and rename
23+
524
## 2.0.0 - 2022-11-20
625

726
laravel-flysystem v3 upgrade:

README.md

Lines changed: 56 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@ additional parameters to your url 😉
2020

2121
| Package | PHP | Laravel | Flysystem |
2222
|-----------|-------------|-----------|-------------|
23-
| v12.0 | ^8.2 - ^8.4 | 12.x | 3.25.1 |
23+
| v13.0 | 8.3.*–8.5.* | 13.x | 3.x |
24+
| v12.0 | ^8.2 - ^8.4 | 12.x | 3.x |
2425
| v11.0 | ^8.2 - ^8.3 | 11.x | 3.0 |
2526
| v4.0 | ^8.2 - ^8.3 | 11.x | 3.0 |
2627
| v3.0 | 8.2 | 10.x | 3.0 |
@@ -59,13 +60,33 @@ configuration:
5960
Add the following environment variables to your `.env` file:
6061

6162
```shell
62-
FILESYSTEM_DRIVER=cloudinary
63+
FILESYSTEM_DISK=cloudinary
6364

6465
CLOUDINARY_CLOUD_NAME=my-cloud-name
6566
CLOUDINARY_API_KEY=my-api-key
6667
CLOUDINARY_API_SECRET=my-api-secret
6768
```
6869

70+
Older Laravel apps may still use `FILESYSTEM_DRIVER`; Laravel 9+ prefers `FILESYSTEM_DISK`.
71+
72+
## Flysystem 3 and Laravel 13
73+
74+
This package registers a **League Flysystem v3** adapter with Laravel’s `Storage` facade.
75+
76+
- **Exceptions:** On failure, `read` / `readStream` throw `UnableToReadFile`; `copy` throws `UnableToCopyFile`; `delete` throws `UnableToDeleteFile`; `createDirectory` / `deleteDirectory` throw `UnableToCreateDirectory` / `UnableToDeleteDirectory` (see [CHANGELOG](CHANGELOG.md)).
77+
- **`deleteDirectory`:** Cloudinary’s Admin API only deletes **empty** folders. The adapter first destroys **shallow-listed files** under the logical path, then calls `delete_folder`—aligned with the legacy `deleteDir()` behaviour. Listing is shallow; deeply nested trees may need extra steps depending on how assets are organised.
78+
- **`listContents`:** **Shallow** listing only; the `$deep` argument is ignored. Each Admin API `assets` call uses `max_results` => 500 **without** `next_cursor` pagination, so very large prefixes may not return a complete list.
79+
- **`write` / `writeStream` vs `update` / `updateStream`:** Only `write` and `writeStream` set `lastUploadMetadata()` and the public `$meta` property. `update` and `updateStream` return the normalized metadata `array` from the upload (or `false` on failure) but **do not** update `lastUploadMetadata()`—it keeps the value from the last `write` / `writeStream`. Use the return value of `update` / `updateStream` when you need fresh metadata.
80+
- **Other helpers:** `lastCopySucceeded()` and `lastDeleteSucceeded()` (and legacy public `$copied` / `$deleted`) reflect the outcome of the latest `copy` / `delete` calls on this adapter instance.
81+
82+
### Cloudinary folder modes
83+
84+
This adapter lists assets with the Admin API using **public ID `prefix`** and manages folders with **`subFolders` / `create_folder` / `delete_folder`**, which matches **legacy fixed folder mode** and typical public-ID paths. If your Cloudinary product environment uses **dynamic folder mode** only, some behaviours may differ; see [Folder modes](https://cloudinary.com/documentation/folder_modes) and the [Admin API](https://cloudinary.com/documentation/admin_api#folders).
85+
86+
### Continuous integration and integration tests
87+
88+
The test suite includes optional **integration** tests that call the live Cloudinary API. They run only when `CLOUDINARY_CLOUD_NAME`, `CLOUDINARY_API_KEY`, and `CLOUDINARY_API_SECRET` are set to real values (for example via GitHub Actions secrets). The default `composer test` command excludes the `integration` group; run `vendor/bin/pest` without `--exclude-group` to include them locally.
89+
6990
## 🏗 File extension problem
7091

7192
Let's look at the following example:
@@ -143,8 +164,8 @@ use Illuminate\Support\Facades\Storage;
143164

144165
Storage::disk('cloudinary')->getAdapter()->getUrl([
145166
'path' => 'meow',
146-
'options => ['w_250', 'h_250', 'c_thumb',]
147-
);
167+
'options' => ['w_250', 'h_250', 'c_thumb'],
168+
]);
148169
```
149170

150171
You can find all options in
@@ -180,10 +201,10 @@ You can pass all parameters as an array to the `put` method:
180201
use Illuminate\Support\Facades\Storage;
181202

182203
Storage::disk('cloudinary')->put('meow', $contents, [
183-
'options' [
204+
'options' => [
184205
'notification_url' => 'https://mysite.example.com/notify_endpoint',
185206
'async' => true,
186-
]
207+
],
187208
]);
188209
```
189210

@@ -260,14 +281,40 @@ return [
260281
];
261282
```
262283

263-
## 🚧 Testing
284+
## 🚧 Testing and static analysis
264285

265-
Run the tests:
286+
Default test run (Pest, **excludes** the `integration` group that calls the live Cloudinary API):
266287

267288
```shell
268289
composer test
269290
```
270291

292+
PHPStan with Larastan:
293+
294+
```shell
295+
composer analyse
296+
```
297+
298+
Pest with code coverage (also excludes `integration`):
299+
300+
```shell
301+
composer test-coverage
302+
```
303+
304+
Apply the project code style (Laravel Pint):
305+
306+
```shell
307+
composer format
308+
```
309+
310+
To run **all** tests including integration tests, use real `CLOUDINARY_CLOUD_NAME`, `CLOUDINARY_API_KEY`, and `CLOUDINARY_API_SECRET` in the environment, then:
311+
312+
```shell
313+
vendor/bin/pest
314+
```
315+
316+
The same credentials can be supplied as GitHub Actions secrets for CI (see [.github/workflows/run-tests.yml](.github/workflows/run-tests.yml)).
317+
271318
## 📝 Changelog
272319

273320
Please see [CHANGELOG](CHANGELOG.md) for more information on what has changed recently.
@@ -278,7 +325,7 @@ Please see [CONTRIBUTING](.github/CONTRIBUTING.md) for details.
278325

279326
## 🧑‍💻 Security Vulnerabilities
280327

281-
Please review [our security policy](../../security/policy) on how to report security vulnerabilities.
328+
Please see [.github/SECURITY.md](.github/SECURITY.md) for how to report security vulnerabilities.
282329

283330
## 🙏 Credits
284331

composer.json

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "codebar-ag/laravel-flysystem-cloudinary",
3-
"description": "Cloudinary Flysystem v1 integration with Laravel",
3+
"description": "Cloudinary Flysystem 3 integration with Laravel",
44
"keywords": [
55
"laravel",
66
"codebar-ag",
@@ -19,18 +19,18 @@
1919
}
2020
],
2121
"require": {
22-
"php": "8.2.*|8.3.*|8.4.*",
22+
"php": "8.3.*|8.4.*|8.5.*",
2323
"guzzlehttp/guzzle": "^7.8",
24-
"illuminate/contracts": "^12.0",
24+
"illuminate/contracts": "^13.0",
2525
"cloudinary/cloudinary_php": "^3.1",
2626
"nesbot/carbon": "^3.8",
2727
"spatie/laravel-package-tools": "^1.19"
2828
},
2929
"require-dev": {
3030
"laravel/pint": "^1.21",
31-
"larastan/larastan": "^v3.1",
32-
"orchestra/testbench": "^10.0",
33-
"pestphp/pest": "^3.7",
31+
"larastan/larastan": "^3.9",
32+
"orchestra/testbench": "^11.0",
33+
"pestphp/pest": "^4.0",
3434
"phpstan/extension-installer": "^1.4",
3535
"phpstan/phpstan-deprecation-rules": "^2.0",
3636
"phpstan/phpstan-phpunit": "^2.0",
@@ -47,19 +47,24 @@
4747
}
4848
},
4949
"scripts": {
50-
"psalm": "vendor/bin/psalm",
51-
"test": "./vendor/bin/testbench package:test --parallel --no-coverage --exclude-group=Integration",
52-
"test-coverage": "vendor/bin/phpunit --coverage-html coverage --exclude-group=Integration",
53-
"format": "vendor/bin/php-cs-fixer fix --allow-risky=yes"
50+
"analyse": "vendor/bin/phpstan analyse",
51+
"test": "vendor/bin/pest --no-coverage --exclude-group=integration",
52+
"test-coverage": "vendor/bin/pest --coverage --exclude-group=integration",
53+
"format": "vendor/bin/pint"
5454
},
5555
"config": {
5656
"sort-packages": true,
5757
"allow-plugins": {
58+
"composer/package-versions-deprecated": false,
5859
"pestphp/pest-plugin": true,
59-
"phpstan/extension-installer": true
60+
"phpstan/extension-installer": true,
61+
"dealerdirect/phpcodesniffer-composer-installer": true
6062
}
6163
},
6264
"extra": {
65+
"branch-alias": {
66+
"dev-main": "13.x-dev"
67+
},
6368
"laravel": {
6469
"providers": [
6570
"CodebarAg\\FlysystemCloudinary\\FlysystemCloudinaryServiceProvider"

phpunit.xml.dist

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
</logging>
2222
<php>
2323
<env name="APP_KEY" value="base64:F+mHMDBbavrsp/I3WYA5lDSwDJJI/0wQG4eM3csq/lo="/>
24-
<env name="FILESYSTEM_DRIVER" value="cloudinary"/>
24+
<env name="FILESYSTEM_DISK" value="cloudinary"/>
2525
<env name="CLOUDINARY_CLOUD_NAME" value="cloudinary_cloud_name"/>
2626
<env name="CLOUDINARY_API_KEY" value="cloudinary_api_key"/>
2727
<env name="CLOUDINARY_API_SECRET" value="cloudinary_api_secret"/>
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
<?php
2+
3+
namespace CodebarAg\FlysystemCloudinary;
4+
5+
use Cloudinary\Cloudinary;
6+
use Throwable;
7+
8+
final class CloudinaryAdminFolderLocator
9+
{
10+
public function folderExists(Cloudinary $cloudinary, string $prefixedPath): bool
11+
{
12+
$pos = strrpos($prefixedPath, '/');
13+
$needle = $pos === false ? '' : substr($prefixedPath, 0, $pos);
14+
15+
try {
16+
$folders = [];
17+
$nextCursor = null;
18+
do {
19+
$response = (array) $cloudinary->adminApi()->subFolders($needle, [
20+
'max_results' => 500,
21+
'next_cursor' => $nextCursor,
22+
]);
23+
24+
$folders = array_merge($folders, $response['folders'] ?? []);
25+
$nextCursor = $response['next_cursor'] ?? null;
26+
} while (! empty($nextCursor));
27+
28+
foreach ($folders as $folder) {
29+
if (($folder['path'] ?? '') === $prefixedPath) {
30+
return true;
31+
}
32+
}
33+
} catch (Throwable) {
34+
return false;
35+
}
36+
37+
return false;
38+
}
39+
}

0 commit comments

Comments
 (0)