├── LICENSE ├── README.md ├── composer.json ├── config └── cloudflare_images.php └── src ├── CloudflareApi.php ├── CloudflareImagesServiceProvider.php ├── Exceptions ├── CloudflareImageNotFound.php ├── CloudflareImagesApiException.php ├── CloudflareImagesApiUnexpectedError.php ├── CloudflareSignatureTokenNotProvided.php ├── CloudflareSignedUrlNotSupportedForCustomIds.php ├── IncorrectKeyOrAccountProvided.php ├── NoImageDeliveryUrlProvided.php └── NoKeyOrAccountProvided.php ├── Facades └── CloudflareApi.php ├── Helpers └── SignedUrlGenerator.php ├── Http ├── Clients │ ├── ImagesApiClient.php │ └── ImagesVariantsApiClient.php ├── Entities │ ├── ArrayableEntity.php │ ├── DirectUploadInfo.php │ ├── Image.php │ ├── ImageVariant.php │ └── ImageVariants.php └── Responses │ ├── BaseResponse.php │ ├── DetailsResponse.php │ └── ListResponse.php └── Testing └── Fakes ├── ImagesApiClientFake.php └── ImagesVariantsApiClientFake.php /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 godundmytro 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Cloudflare Images 2 | 3 | Provides access to Cloudflare Images API for Laravel projects 4 | 5 | [![Stable Version][badge_stable]][link_packagist] 6 | [![Unstable Version][badge_unstable]][link_packagist] 7 | [![Total Downloads][badge_downloads]][link_packagist] 8 | [![License][badge_license]][link_license] 9 | 10 | ## Table of contents 11 | 12 | * [Installation](#installation) 13 | * [Configuration](#configuration) 14 | * [Using](#using) 15 | 16 | ## Installation 17 | 18 | To get the latest version of `Laravel CloudflareImages`, simply require the project using [Composer](https://getcomposer.org): 19 | 20 | ```bash 21 | $ composer require dedmytro/laravel-cloudflare-images 22 | ``` 23 | 24 | Or manually update `require` block of `composer.json` and run `composer update`. 25 | 26 | ```json 27 | { 28 | "require": { 29 | "dedmytro/laravel-cloudflare-images": "^0.2" 30 | } 31 | } 32 | ``` 33 | 34 | ## Configuration 35 | 36 | Add environment variables to your .env file: 37 | 38 | ```dotenv 39 | CLOUDFLARE_IMAGES_ACCOUNT='your-account-id' 40 | CLOUDFLARE_IMAGES_KEY='your-api-key' 41 | CLOUDFLARE_IMAGES_DELIVERY_URL='https://imagedelivery.net/ZWd9g1K8vvvVv_Yyyy_XXX' 42 | CLOUDFLARE_IMAGES_DEFAULT_VARIATION='your-default-variation' 43 | CLOUDFLARE_IMAGES_SIGNATURE_TOKEN='your-signature-token' 44 | ``` 45 | 46 | or publish config and set up vars there 47 | 48 | ```php 49 | return [ 50 | 'account'=> env('CLOUDFLARE_IMAGES_ACCOUNT'), 51 | 'key'=> env('CLOUDFLARE_IMAGES_KEY'), 52 | 'delivery_url' => env('CLOUDFLARE_IMAGES_DELIVERY_URL'), 53 | 'default_variation' => env('CLOUDFLARE_IMAGES_DEFAULT_VARIATION'), 54 | 'signature_token' => env('CLOUDFLARE_IMAGES_SIGNATURE_TOKEN') 55 | ]; 56 | ``` 57 | 58 | `CLOUDFLARE_IMAGES_KEY` - is an `API Token`. To create a new one go to [User Api Tokens](https://dash.cloudflare.com/profile/api-tokens) on Cloudflare dashboard 59 | 60 | `CLOUDFLARE_IMAGES_ACCOUNT` - is an `Account ID` on the Overview page 61 | 62 | `CLOUDFLARE_IMAGES_DELIVERY_URL` - is an `Image Delivery URL` on the Overview page 63 | 64 | `CLOUDFLARE_IMAGES_DEFAULT_VARIATION` - is a variation on the Variants page 65 | 66 | `CLOUDFLARE_IMAGES_SIGNATURE_TOKEN` - is a token from the Images -> Keys page 67 | 68 | 69 | ## Using 70 | 71 | ### Direct upload 72 | 73 | The Direct upload is feature of Cloudflare Images to upload image directly from frontend but without sharing your api key. Once you get this url you can use 74 | inside your html 75 | 76 | `
` 77 | 78 | **IMPORTANT: You can use this url only once!** 79 | 80 | ```php 81 | use DeDmytro\CloudflareImages\Facades\CloudflareApi; 82 | 83 | $response = CloudflareApi::images()->directUploadUrl() 84 | $response->result->id; // Your uploaded image ID 85 | $response->result->uploadURL; // One-time uploadUrl 86 | 87 | ``` 88 | 89 | ### Upload 90 | 91 | Call `upload()` method and pass file as local file path or `UploadedFile` instance. As a result of upload you'll get `DetailsResponse` instance with uploaded 92 | image details, so you can save it locally. 93 | 94 | ```php 95 | use DeDmytro\CloudflareImages\Facades\CloudflareApi; 96 | use DeDmytro\CloudflareImages\Http\Responses\DetailsResponse; 97 | use DeDmytro\CloudflareImages\Http\Entities\Image; 98 | 99 | /* @var $file \Illuminate\Http\UploadedFile|string */ 100 | 101 | /* @var $response DetailsResponse*/ 102 | $response = CloudflareApi::images()->upload($file) 103 | 104 | /* @var $image Image*/ 105 | $image = $response->result 106 | 107 | $image->id; 108 | $image->filename; 109 | $image->variants->thumbnail; //Depends on your Cloudflare Images Variants setting 110 | $image->variants->original; //Depends on your Cloudflare Images Variants setting 111 | 112 | 113 | ``` 114 | 115 | ### List 116 | 117 | To list existing images you should use `list()` method which also has pagination and accept `$page` and `$perPage` arguments. 118 | 119 | ```php 120 | use DeDmytro\CloudflareImages\Facades\CloudflareApi; 121 | 122 | /* @var $response ListResponse*/ 123 | $response = CloudflareApi::images()->list() 124 | //OR 125 | $response = CloudflareApi::images()->list($page, $perPage) 126 | 127 | foreach($response->result as $image){ 128 | $image->id; 129 | $image->filename; 130 | $image->variants->thumbnail; //Depends on your Cloudflare Images Variants setting 131 | $image->variants->original; //Depends on your Cloudflare Images Variants setting 132 | } 133 | 134 | ``` 135 | 136 | ### Details 137 | 138 | To get existing image details you should use `get($id)` method where `$id` is image identifier you received when you list or upload the image. 139 | 140 | ```php 141 | use DeDmytro\CloudflareImages\Facades\CloudflareApi; 142 | 143 | $response = CloudflareApi::images()->get($id) 144 | 145 | $image = $response->result; 146 | $image->id; 147 | $image->filename; 148 | $image->variants->thumbnail; //Depends on your Cloudflare Images Variants setting 149 | $image->variants->original; //Depends on your Cloudflare Images Variants setting 150 | 151 | 152 | ``` 153 | 154 | ### Delete 155 | 156 | To delete existing image you should use `delete($id)` method where `$id` is image identifier you received when you list or upload the image. 157 | 158 | ```php 159 | use DeDmytro\CloudflareImages\Facades\CloudflareApi; 160 | 161 | $response = CloudflareApi::images()->delete($id) 162 | $response->success 163 | 164 | ``` 165 | 166 | ### Public url 167 | 168 | To generate image url locally call method `url($id)` and pass image ID. Don't forget to set up 169 | 170 | ```dotenv 171 | CLOUDFLARE_IMAGES_DELIVERY_URL= 172 | CLOUDFLARE_IMAGES_DEFAULT_VARIATION= 173 | ``` 174 | 175 | ```php 176 | use DeDmytro\CloudflareImages\Facades\CloudflareApi; 177 | 178 | $url = CloudflareApi::images()->url($id) 179 | ``` 180 | 181 | ```html 182 | 183 | ``` 184 | 185 | ### Signed url 186 | 187 | To generate signed image url locally call method `signedUrl($id, $expires = 3600)` and pass image ID and expiration time in seconds. Don't forget to set up 188 | 189 | ```dotenv 190 | CLOUDFLARE_IMAGES_DELIVERY_URL= 191 | CLOUDFLARE_IMAGES_DEFAULT_VARIATION= 192 | CLOUDFLARE_IMAGES_SIGNATURE_TOKEN= 193 | ``` 194 | 195 | ```php 196 | use DeDmytro\CloudflareImages\Facades\CloudflareApi; 197 | 198 | $url = CloudflareApi::images()->signedUrl($id, $expires) 199 | ``` 200 | 201 | ```html 202 | 203 | 204 | ``` 205 | 206 | 207 | [badge_downloads]: https://img.shields.io/packagist/dt/dedmytro/laravel-cloudflare-images.svg?style=flat-square 208 | 209 | [badge_license]: https://img.shields.io/packagist/l/dedmytro/laravel-cloudflare-images.svg?style=flat-square 210 | 211 | [badge_stable]: https://img.shields.io/github/v/release/dedmytro/laravel-cloudflare-images?label=stable&style=flat-square 212 | 213 | [badge_unstable]: https://img.shields.io/badge/unstable-dev--main-orange?style=flat-square 214 | 215 | [link_license]: LICENSE 216 | 217 | [link_packagist]: https://packagist.org/packages/dedmytro/laravel-cloudflare-images 218 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dedmytro/laravel-cloudflare-images", 3 | "description": "Cloudflare API client and filesystem for Laravel", 4 | "type": "library", 5 | "license": "MIT", 6 | "keywords": [ 7 | "laravel", 8 | "cloudflare", 9 | "cloudflare api", 10 | "cloudflare images" 11 | ], 12 | "authors": [ 13 | { 14 | "name": "Hodun Dmytro", 15 | "email": "godundmytro@gmail.com" 16 | } 17 | ], 18 | "support": { 19 | "issues": "https://github.com/DeDmytro/laravel-cloudflare-images/issues", 20 | "source": "https://github.com/DeDmytro/laravel-cloudflare-images" 21 | }, 22 | "require": { 23 | "php": "^7.4|^8.0", 24 | "illuminate/support": "^7.0|^8.0|^9.0|^10.0|^11.0", 25 | "guzzlehttp/guzzle": "^6.2.1|^7.0" 26 | }, 27 | "require-dev": { 28 | "mockery/mockery": "^1.0", 29 | "orchestra/testbench": "^6.0|^9.0", 30 | "phpunit/phpunit": "^10.0" 31 | }, 32 | "autoload": { 33 | "psr-4": { 34 | "DeDmytro\\CloudflareImages\\": "src" 35 | } 36 | }, 37 | "autoload-dev": { 38 | "psr-4": { 39 | "Tests\\": "tests" 40 | } 41 | }, 42 | "config": { 43 | "preferred-install": "dist", 44 | "sort-packages": true 45 | }, 46 | "minimum-stability": "stable", 47 | "prefer-stable": true, 48 | "extra": { 49 | "laravel": { 50 | "providers": [ 51 | "DeDmytro\\CloudflareImages\\CloudflareImagesServiceProvider" 52 | ], 53 | "aliases": { 54 | "CloudflareImages": "DeDmytro\\CloudflareImages\\Facades\\CloudflareApi" 55 | } 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /config/cloudflare_images.php: -------------------------------------------------------------------------------- 1 | env('CLOUDFLARE_IMAGES_ACCOUNT'), 5 | 'key' => env('CLOUDFLARE_IMAGES_KEY'), 6 | 'delivery_url' => env('CLOUDFLARE_IMAGES_DELIVERY_URL'), // https://imagedelivery.net/ZWd9g1K8vvvVv_Yyyy_XXX 7 | 'default_variation' => env('CLOUDFLARE_IMAGES_DEFAULT_VARIATION', 'public'), // One of defined image variation on Cloudlflare Images Variations 8 | 'signature_token' => env('CLOUDFLARE_IMAGES_SIGNATURE_TOKEN'), 9 | ]; 10 | -------------------------------------------------------------------------------- /src/CloudflareApi.php: -------------------------------------------------------------------------------- 1 | bootPublishes(); 15 | } 16 | 17 | /** 18 | * Register package resources 19 | */ 20 | public function register(): void 21 | { 22 | $this->registerConfig(); 23 | $this->registerFacade(); 24 | } 25 | 26 | /** 27 | * Boot publishable resources 28 | */ 29 | protected function bootPublishes(): void 30 | { 31 | $this->publishes([ 32 | __DIR__ . '/../config/cloudflare_images.php' => $this->app->configPath('cloudflare_images.php'), 33 | ], 'config'); 34 | } 35 | 36 | /** 37 | * Register related config 38 | */ 39 | protected function registerConfig(): void 40 | { 41 | $this->mergeConfigFrom(__DIR__ . '/../config/cloudflare_images.php', 'cloudflare_images'); 42 | } 43 | 44 | /** 45 | * Register related facade 46 | */ 47 | protected function registerFacade(): void 48 | { 49 | $this->app->bind('cloudflareImages', function ($app) { 50 | return new CloudflareApi(); 51 | }); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/Exceptions/CloudflareImageNotFound.php: -------------------------------------------------------------------------------- 1 | andReturn(new ImagesApiClientFake()); 38 | self::shouldReceive('variants')->andReturn(new ImagesVariantsApiClientFake()); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Helpers/SignedUrlGenerator.php: -------------------------------------------------------------------------------- 1 | signatureToken = config('cloudflare_images.signature_token'); 39 | 40 | $this->imageUrl = $imageUrl; 41 | } 42 | 43 | /** 44 | * @param string $imageUrl 45 | * 46 | * @return self 47 | */ 48 | public static function fromDeliveryUrl(string $imageUrl): self 49 | { 50 | return new self($imageUrl); 51 | } 52 | 53 | /** 54 | * Set the expiration time for the signed URL 55 | * 56 | * @param int $seconds 57 | * 58 | * @return $this 59 | */ 60 | final public function setExpiration(int $seconds): self 61 | { 62 | $this->expiry = $seconds; 63 | 64 | return $this; 65 | } 66 | 67 | /** 68 | * Generate the signed URL 69 | * 70 | * @return string 71 | */ 72 | final public function generate(): string 73 | { 74 | $urlParts = parse_url($this->imageUrl); 75 | $urlPath = $urlParts['path']; 76 | 77 | // Attach the expiration value to the `url` 78 | $expiry = time() + $this->expiry; 79 | $queryParams['exp'] = $expiry; 80 | 81 | // Generate the string to sign (including query parameters) 82 | $stringToSign = $urlPath . '?' . http_build_query($queryParams); 83 | 84 | // Generate the signature 85 | $mac = hash_hmac('sha256', $stringToSign, $this->signatureToken); 86 | 87 | // And attach it to the `url` 88 | $queryParams['sig'] = $mac; 89 | 90 | // Rebuild the URL with the signature 91 | return $urlParts['scheme'] . '://' . $urlParts['host'] . $urlPath . '?' . http_build_query($queryParams); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/Http/Clients/ImagesApiClient.php: -------------------------------------------------------------------------------- 1 | httpClient = Http::withToken($key)->baseUrl("https://api.cloudflare.com/client/v4/accounts/$account/images"); 41 | } 42 | 43 | /** 44 | * Upload file and return details 45 | * 46 | * @param string|UploadedFile $file 47 | * @param string $filename 48 | * @param bool $requiredSignedUrl 49 | * @param array $metadata 50 | * @param string|null $customId 51 | * 52 | * @throws CloudflareSignedUrlNotSupportedForCustomIds 53 | * @return DetailsResponse 54 | */ 55 | public function upload($file, string $filename = '', bool $requiredSignedUrl = false, array $metadata = [], string $customId = null): DetailsResponse 56 | { 57 | if ($file instanceof UploadedFile) { 58 | $path = $file->getRealPath(); 59 | } else { 60 | $path = $file; 61 | } 62 | 63 | $body = [ 64 | 'file' => [ 65 | 'Content-type' => 'multipart/form-data', 66 | 'name' => 'file', 67 | 'contents' => fopen($path, 'rb'), 68 | 'filename' => $filename ?: basename($path), 69 | ], 70 | 'requireSignedURLs' => var_export($requiredSignedUrl, true), 71 | 'metadata' => \GuzzleHttp\json_encode($metadata), 72 | ]; 73 | 74 | if ($customId) { 75 | if ($requiredSignedUrl) { 76 | throw new CloudflareSignedUrlNotSupportedForCustomIds(); 77 | } 78 | $body['id'] = $customId; 79 | } 80 | 81 | $result = $this->httpClient->asMultipart()->post('v1', $body)->json(); 82 | 83 | return DetailsResponse::fromArray($result)->mapResultInto(Image::class); 84 | } 85 | 86 | /** 87 | * Return list of images 88 | * 89 | * @param int $page 90 | * @param int $perPage 91 | * 92 | * @return ListResponse 93 | */ 94 | public function list(int $page = 1, int $perPage = 50): ListResponse 95 | { 96 | return ListResponse::fromArray($this->httpClient->get('v1', ['page' => $page, 'per_page' => $perPage])->json())->mapResultInto(Image::class, 'images'); 97 | } 98 | 99 | /** 100 | * Return image details by ID 101 | * 102 | * @param string $imageId 103 | * 104 | * @throws CloudflareImageNotFound 105 | * @throws \Throwable 106 | * @return DetailsResponse 107 | */ 108 | public function get(string $imageId): DetailsResponse 109 | { 110 | $result = $this->httpClient->get("v1/$imageId")->json(); 111 | 112 | throw_if(is_null($result), new CloudflareImageNotFound($imageId)); 113 | 114 | return DetailsResponse::fromArray($result)->mapResultInto(Image::class); 115 | } 116 | 117 | /** 118 | * Delete image by ID 119 | * 120 | * @param string $imageId 121 | * 122 | * @throws \Throwable 123 | * @return DetailsResponse 124 | */ 125 | public function delete(string $imageId): DetailsResponse 126 | { 127 | $result = $this->httpClient->delete("v1/$imageId")->json(); 128 | 129 | throw_if(is_null($result), new CloudflareImageNotFound($imageId)); 130 | 131 | return DetailsResponse::fromArray($result); 132 | } 133 | 134 | /** 135 | * Check image exists by ID 136 | * 137 | * @param string $imageId 138 | * 139 | * @throws \Throwable 140 | * @return bool 141 | */ 142 | public function exists(string $imageId): bool 143 | { 144 | $result = $this->httpClient->get("v1/$imageId")->json(); 145 | 146 | if (is_null($result)) { 147 | return false; 148 | } 149 | 150 | return DetailsResponse::fromArray($result)->success; 151 | } 152 | 153 | /** 154 | * Return direct upload information 155 | * Direct upload allows uploading files from frontend without sharing the application api key 156 | * 157 | * @link https://developers.cloudflare.com/images/cloudflare-images/upload-images/direct-creator-upload 158 | * @return DetailsResponse 159 | */ 160 | public function directUploadUrl(): DetailsResponse 161 | { 162 | return DetailsResponse::fromArray($this->httpClient->post('v1/direct_upload')->json())->mapResultInto(DirectUploadInfo::class); 163 | } 164 | 165 | /** 166 | * Return image public url by image id and variation 167 | * 168 | * @param string $imageId 169 | * @param string|null $variation 170 | * 171 | * @throws \Throwable 172 | * @return string 173 | */ 174 | public function url(string $imageId, ?string $variation = null): string 175 | { 176 | $imageDeliveryUrl = config('cloudflare_images.delivery_url'); 177 | 178 | throw_if(empty($imageDeliveryUrl), new NoImageDeliveryUrlProvided()); 179 | 180 | if (! $variation) { 181 | $variation = config('cloudflare_images.default_variation'); 182 | } 183 | 184 | return ltrim(rtrim($imageDeliveryUrl, '/') . '/' . ltrim("$imageId/$variation", '/'), '/'); 185 | } 186 | 187 | /** 188 | * Return signed url by image id and variation 189 | * 190 | * @param string $imageId 191 | * @param string|null $variation 192 | * @param int $expirationSeconds 193 | * 194 | * @throws \Throwable 195 | * @return string 196 | */ 197 | public function signedUrl(string $imageId, ?string $variation = null, int $expirationSeconds = 3600): string 198 | { 199 | return SignedUrlGenerator::fromDeliveryUrl($this->url($imageId,$variation))->setExpiration($expirationSeconds)->generate(); 200 | } 201 | } 202 | -------------------------------------------------------------------------------- /src/Http/Clients/ImagesVariantsApiClient.php: -------------------------------------------------------------------------------- 1 | httpClient = Http::withToken($key)->baseUrl("https://api.cloudflare.com/client/v4/accounts/$account/images/v1"); 40 | } 41 | 42 | /** 43 | * Return list of images variants 44 | * 45 | * @return ListResponse 46 | */ 47 | public function list(): ListResponse 48 | { 49 | return ListResponse::fromArray($this->httpClient->get('variants')->json())->mapResultInto(ImageVariant::class, 'variants'); 50 | } 51 | 52 | /** 53 | * Return variant by ID 54 | * 55 | * @param string $variantId 56 | * 57 | * @return DetailsResponse 58 | */ 59 | public function get(string $variantId): DetailsResponse 60 | { 61 | return DetailsResponse::fromArray($this->httpClient->get("variants/$variantId")->json())->mapResultInto(ImageVariant::class); 62 | } 63 | 64 | /** 65 | * Delete image by ID 66 | * 67 | * @param string $variantId 68 | * 69 | * @return DetailsResponse 70 | */ 71 | public function delete(string $variantId): DetailsResponse 72 | { 73 | return DetailsResponse::fromArray($this->httpClient->delete("variants/$variantId")->json()); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/Http/Entities/ArrayableEntity.php: -------------------------------------------------------------------------------- 1 | id = $id; 28 | $this->uploadURL = $uploadURL; 29 | } 30 | 31 | /** 32 | * Return Photo instance from array 33 | * 34 | * @param array $array 35 | * 36 | * @return self 37 | */ 38 | public static function fromArray(array $array) 39 | { 40 | return new self( 41 | (string) Arr::get($array, 'id'), 42 | (string) Arr::get($array, 'uploadURL'), 43 | ); 44 | } 45 | 46 | /** 47 | * @inheritDoc 48 | */ 49 | final public function toArray(): array 50 | { 51 | return [ 52 | 'id' => $this->id, 53 | 'uploadURL' => $this->uploadURL, 54 | ]; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Http/Entities/Image.php: -------------------------------------------------------------------------------- 1 | id = $id; 36 | $this->filename = $filename; 37 | $this->metadata = $metadata; 38 | $this->requireSignedURLs = $requireSignedURLs; 39 | $this->variants = $variants; 40 | $this->uploaded = Carbon::parse($uploaded); 41 | } 42 | 43 | /** 44 | * Return Image instance from array 45 | * 46 | * @param array $array 47 | * 48 | * @return Image 49 | */ 50 | public static function fromArray(array $array) 51 | { 52 | return new self( 53 | (string) Arr::get($array, 'id'), 54 | (string) Arr::get($array, 'filename'), 55 | (array) Arr::get($array, 'meta'), 56 | (bool) Arr::get($array, 'requireSignedURLs'), 57 | ImageVariants::fromArray((array) Arr::get($array, 'variants')), 58 | Arr::get($array, 'uploaded'), 59 | ); 60 | } 61 | 62 | /** 63 | * @inheritDoc 64 | */ 65 | final public function toArray(): array 66 | { 67 | return [ 68 | 'id' => $this->id, 69 | 'filename' => $this->filename, 70 | 'metadata' => $this->metadata, 71 | 'requireSignedURLs' => $this->requireSignedURLs, 72 | 'variants' => $this->variants->toArray(), 73 | 'uploaded' => $this->uploaded, 74 | ]; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/Http/Entities/ImageVariant.php: -------------------------------------------------------------------------------- 1 | id = $id; 31 | $this->options = $options; 32 | $this->neverRequireSignedURLs = $neverRequireSignedURLs; 33 | } 34 | 35 | /** 36 | * Return Photo instance from array 37 | * 38 | * @param array $array 39 | * 40 | * @return ImageVariant 41 | */ 42 | public static function fromArray(array $array) 43 | { 44 | return new self( 45 | (string) Arr::get($array, 'id'), 46 | (array) Arr::get($array, 'options'), 47 | (bool) Arr::get($array, 'neverRequireSignedURLs'), 48 | ); 49 | } 50 | 51 | /** 52 | * @inheritDoc 53 | */ 54 | final public function toArray(): array 55 | { 56 | return [ 57 | 'id' => $this->id, 58 | 'metadata' => $this->options, 59 | 'requireSignedURLs' => $this->neverRequireSignedURLs, 60 | ]; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/Http/Entities/ImageVariants.php: -------------------------------------------------------------------------------- 1 | variants[$key] = $variant; 21 | } 22 | } 23 | 24 | /** 25 | * Return new instance 26 | * 27 | * @param array $array 28 | * 29 | * @return self 30 | */ 31 | final public static function fromArray(array $array) 32 | { 33 | return new self($array); 34 | } 35 | 36 | /** 37 | * @inheritDoc 38 | */ 39 | final public function toArray(): array 40 | { 41 | return $this->variants; 42 | } 43 | 44 | /** 45 | * Return image variant url by name 46 | * 47 | * @param string $name 48 | * 49 | * @return mixed|string 50 | */ 51 | public function __get(string $name) 52 | { 53 | if (array_key_exists($name, $this->variants)) { 54 | return $this->variants[$name]; 55 | } 56 | 57 | return ''; 58 | } 59 | 60 | /** 61 | * Set/Update variation url by name 62 | * 63 | * @param string $name 64 | * @param $value 65 | */ 66 | public function __set(string $name, $value) 67 | { 68 | $this->variants[$name] = $value; 69 | } 70 | 71 | /** 72 | * Check variation exists when use isset() or empty() on property 73 | * 74 | * @param $name 75 | * 76 | * @return bool 77 | */ 78 | public function __isset($name) 79 | { 80 | return array_key_exists($name, $this->variants); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/Http/Responses/BaseResponse.php: -------------------------------------------------------------------------------- 1 | result = $result; 48 | $this->success = $success; 49 | $this->errors = $errors; 50 | $this->messages = $messages; 51 | } 52 | 53 | /** 54 | * Return new instance 55 | * 56 | * @param array $array 57 | * 58 | * @throws IncorrectKeyOrAccountProvided 59 | * @throws CloudflareImagesApiUnexpectedError 60 | * @return static 61 | */ 62 | final public static function fromArray(array $array) 63 | { 64 | try { 65 | return new static( 66 | Arr::get($array, 'result'), 67 | (bool) Arr::get($array, 'success'), 68 | Arr::get($array, 'errors', []), 69 | Arr::get($array, 'messages', []), 70 | ); 71 | } 72 | catch (TypeError $exception) { 73 | $responseCode = Arr::get($array, 'errors.0.code'); 74 | 75 | if (in_array($responseCode, [self::RESPONSE_CODE_AUTHENTICATION_ERROR, self::RESPONSE_CODE_MISSING_AUTHORIZATION_KEYS], true)) { 76 | throw new IncorrectKeyOrAccountProvided(); 77 | } 78 | 79 | throw new CloudflareImagesApiUnexpectedError(Arr::get($array, 'errors.0.message')); 80 | } 81 | } 82 | 83 | /** 84 | * @inheritDoc 85 | */ 86 | public function toArray(): array 87 | { 88 | return [ 89 | 'success' => $this->success, 90 | 'errors' => $this->errors, 91 | 'messages' => $this->messages, 92 | 'result' => $this->result, 93 | ]; 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/Http/Responses/DetailsResponse.php: -------------------------------------------------------------------------------- 1 | result = $class::fromArray($this->result); 41 | } 42 | 43 | return $this; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Http/Responses/ListResponse.php: -------------------------------------------------------------------------------- 1 | result = array_map(static fn (array $image) => $class::fromArray($image), Arr::get($this->result, $key, [])); 44 | } 45 | 46 | return $this; 47 | } 48 | 49 | /** 50 | * @inheritDoc 51 | */ 52 | final public function toArray(): array 53 | { 54 | return [ 55 | 'success' => $this->success, 56 | 'errors' => $this->errors, 57 | 'messages' => $this->messages, 58 | 'result' => array_map(static fn ($image) => $image->toArray(), $this->result), 59 | ]; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/Testing/Fakes/ImagesApiClientFake.php: -------------------------------------------------------------------------------- 1 | set('cloudflare_images.account', 'fake'); 31 | config()->set('cloudflare_images.key', 'fake'); 32 | config()->set('cloudflare_images.default_variation', 'fake'); 33 | config()->set('cloudflare_images.delivery_url', 'fake'); 34 | 35 | parent::__construct(); 36 | } 37 | 38 | /** 39 | * Upload file and return details 40 | * 41 | * @param string|\Illuminate\Http\UploadedFile $file 42 | * @param bool $requiredSignedUrl 43 | * @param array $metadata 44 | * 45 | * @return DetailsResponse 46 | */ 47 | public function upload($file, string $filename = '', bool $requiredSignedUrl = false, array $metadata = [], string $customId = null): DetailsResponse 48 | { 49 | $result = [ 50 | 'result' => $this->testImageData(Str::afterLast($file, '/')), 51 | 'success' => true, 52 | 'errors' => [], 53 | 'messages' => [], 54 | ]; 55 | 56 | return DetailsResponse::fromArray($result)->mapResultInto(Image::class); 57 | } 58 | 59 | /** 60 | * Return test image data and save id to static 61 | * 62 | * @param string|null $filename 63 | * 64 | * @return array 65 | */ 66 | private function testImageData(string $filename = null): array 67 | { 68 | $id = Str::random(64); 69 | 70 | static::$createdFakeIds[] = $id; 71 | 72 | return [ 73 | 'id' => $id, 74 | 'filename' => $filename ?? Str::random(64), 75 | 'metadata' => [], 76 | 'requireSignedURLs' => false, 77 | 'variants' => [], 78 | 'uploaded' => now()->toDateTimeString(), 79 | ]; 80 | } 81 | 82 | /** 83 | * Return list of images 84 | * 85 | * @param int $page 86 | * @param int $perPage 87 | * 88 | * @return ListResponse 89 | */ 90 | public function list(int $page = 1, int $perPage = 50): ListResponse 91 | { 92 | $result = [ 93 | 'result' => array_map(function ($item) { 94 | return $this->testImageData(); 95 | }, range(1, $perPage)), 96 | 'success' => true, 97 | 'errors' => [], 98 | 'messages' => [], 99 | ]; 100 | 101 | return ListResponse::fromArray($result)->mapResultInto(Image::class, 'images'); 102 | } 103 | 104 | /** 105 | * Return image details by ID 106 | * 107 | * @param string $imageId 108 | * 109 | * @throws \Throwable 110 | * @return DetailsResponse 111 | */ 112 | public function get(string $imageId): DetailsResponse 113 | { 114 | $result = [ 115 | 'result' => $this->testImageData(), 116 | 'success' => true, 117 | 'errors' => [], 118 | 'messages' => [], 119 | ]; 120 | 121 | return DetailsResponse::fromArray($result)->mapResultInto(Image::class); 122 | } 123 | 124 | /** 125 | * Delete image by ID 126 | * 127 | * @param string $imageId 128 | * 129 | * @throws \Throwable 130 | * @return DetailsResponse 131 | */ 132 | public function delete(string $imageId): DetailsResponse 133 | { 134 | $result = [ 135 | 'result' => [], 136 | 'success' => true, 137 | 'errors' => [], 138 | 'messages' => [], 139 | ]; 140 | 141 | return DetailsResponse::fromArray($result); 142 | } 143 | 144 | /** 145 | * Check image exists by ID 146 | * 147 | * @param string $imageId 148 | * 149 | * @throws \Throwable 150 | * @return bool 151 | */ 152 | public function exists(string $imageId): bool 153 | { 154 | return in_array($imageId, static::$createdFakeIds, true); 155 | } 156 | 157 | /** 158 | * Return direct upload information 159 | * Direct upload allows uploading files from frontend without sharing the application api key 160 | * 161 | * @link https://developers.cloudflare.com/images/cloudflare-images/upload-images/direct-creator-upload 162 | * @return DetailsResponse 163 | */ 164 | public function directUploadUrl(): DetailsResponse 165 | { 166 | $result = [ 167 | 'result' => [ 168 | 'id' => Str::random(), 169 | 'uploadURL' => 'https://api.cloudflarefake.com/', 170 | ], 171 | 'success' => true, 172 | 'errors' => [], 173 | 'messages' => [], 174 | ]; 175 | 176 | return DetailsResponse::fromArray($result)->mapResultInto(DirectUploadInfo::class); 177 | } 178 | 179 | /** 180 | * Return image public url by image id and variation 181 | * 182 | * @param string $imageId 183 | * @param string|null $variation 184 | * 185 | * @throws \Throwable 186 | * @return string 187 | */ 188 | public function url(string $imageId, ?string $variation = null): string 189 | { 190 | return Str::random(64); 191 | } 192 | 193 | /** 194 | * Return signed image for private url by image id and variation 195 | * 196 | * @param string $imageId 197 | * @param string|null $variation 198 | * @param int $expirationSeconds * 199 | * 200 | * @throws \Throwable 201 | * @return string 202 | */ 203 | public function signedUrl(string $imageId, ?string $variation = null, int $expirationSeconds = 3600): string 204 | { 205 | return SignedUrlGenerator::fromDeliveryUrl($this->url($imageId, $variation))->setExpiration($expirationSeconds)->generate(); 206 | } 207 | } 208 | -------------------------------------------------------------------------------- /src/Testing/Fakes/ImagesVariantsApiClientFake.php: -------------------------------------------------------------------------------- 1 | set('cloudflare_images.account', 'fake'); 35 | config()->set('cloudflare_images.key', 'fake'); 36 | config()->set('cloudflare_images.default_variation', 'fake'); 37 | config()->set('cloudflare_images.delivery_url', 'fake'); 38 | 39 | parent::__construct(); 40 | } 41 | 42 | /** 43 | * Return list of images variants 44 | * 45 | * @return ListResponse 46 | */ 47 | public function list(): ListResponse 48 | { 49 | $result = [ 50 | 'result' => ['variants' => [$this->testVariationData(), $this->testVariationData(), $this->testVariationData()]], 51 | 'success' => true, 52 | 'errors' => [], 53 | 'messages' => [], 54 | ]; 55 | 56 | return ListResponse::fromArray($result)->mapResultInto(ImageVariant::class, 'variants'); 57 | } 58 | 59 | /** 60 | * Return test variation data and save id to static 61 | * 62 | * @return array 63 | */ 64 | private function testVariationData(): array 65 | { 66 | $id = Str::random(64); 67 | 68 | static::$createdFakeIds[] = $id; 69 | 70 | return [ 71 | 'id' => $id, 72 | 'options' => [], 73 | 'neverRequireSignedURLs' => true, 74 | ]; 75 | } 76 | 77 | /** 78 | * Return variant by ID 79 | * 80 | * @param string $variantId 81 | * 82 | * @return DetailsResponse 83 | */ 84 | public function get(string $variantId): DetailsResponse 85 | { 86 | $result = $this->testVariationData(); 87 | 88 | return DetailsResponse::fromArray($result)->mapResultInto(ImageVariant::class); 89 | } 90 | 91 | /** 92 | * Delete image by ID 93 | * 94 | * @param string $variantId 95 | * 96 | * @return DetailsResponse 97 | */ 98 | public function delete(string $variantId): DetailsResponse 99 | { 100 | $result = [ 101 | 'result' => [], 102 | 'success' => true, 103 | 'errors' => [], 104 | 'messages' => [], 105 | ]; 106 | 107 | return DetailsResponse::fromArray($result); 108 | } 109 | } 110 | --------------------------------------------------------------------------------