Search by

jamrul / cloudflare-r2

jamrulwithlearn

A lightweight PHP wrapper for Cloudflare R2 object storage built on the AWS S3 SDK. Works with plain PHP and Laravel.

dev-main 2026-10-01 14:55 UTC

This package is auto-updated.

Last update: 2026-11-02 19:10:12 UTC


README

A lightweight PHP wrapper for Cloudflare R2 object storage, built on the AWS S3 SDK. Works with plain PHP, any framework, and Laravel (auto-discovered).

Features

  • Upload files from $_FILES, a local path, or raw content
  • Replace, delete (single or bulk) and list files
  • Public URLs and temporary signed URLs for private files
  • Automatic unique, sanitized file names
  • Real MIME type detection (does not trust the client)
  • Optional read / write / delete permission check
  • Laravel service provider and publishable config
  • No .env dependency inside the library: pass your config to the constructor

Requirements

  • PHP 7.4 or higher
  • ext-json, ext-fileinfo
  • aws/aws-sdk-php 3.337 or higher

Installation

composer require jamrul/cloudflare-r2

Configuration

Create an R2 API token in the Cloudflare dashboard (R2 > Manage R2 API Tokens) with Object Read & Write permission, then add these values to your .env:

R2_ACCOUNT_ID=
R2_ACCESS_KEY_ID=
R2_SECRET_ACCESS_KEY=
R2_BUCKET_NAME=
R2_PUBLIC_DOMAIN=
R2_DEFAULT_FOLDER=
R2_ENABLE_PERMISSION_CHECK=false
Key Required Description
R2_ACCOUNT_ID Yes Your Cloudflare account ID
R2_ACCESS_KEY_ID Yes R2 API token access key
R2_SECRET_ACCESS_KEY Yes R2 API token secret
R2_BUCKET_NAME Yes Bucket name
R2_PUBLIC_DOMAIN No Public domain of the bucket (custom domain or r2.dev URL). Leave empty for private buckets
R2_DEFAULT_FOLDER No Folder used when you do not pass one, for example uploads
R2_ENABLE_PERMISSION_CHECK No Set to true to enable checkPermissions(). Keep it off in production

Usage with plain PHP

<?php

require __DIR__ . '/vendor/autoload.php';

use Jamrul\Cloudflare\R2Service;

$r2 = new R2Service(
    'your-account-id',
    'your-access-key-id',
    'your-secret-access-key',
    'your-bucket-name',
    'https://cdn.example.com', // public domain (optional)
    'uploads'                  // default folder (optional)
);

// Upload from a form
if (!empty($_FILES['file'])) {
    $result = $r2->uploadFile($_FILES['file']);
    echo $result['url'];
}

To load credentials from a .env file, install vlucas/phpdotenv and read the values yourself:

Dotenv\Dotenv::createImmutable(__DIR__)->safeLoad();

$r2 = new R2Service(
    $_ENV['R2_ACCOUNT_ID'] ?? null,
    $_ENV['R2_ACCESS_KEY_ID'] ?? null,
    $_ENV['R2_SECRET_ACCESS_KEY'] ?? null,
    $_ENV['R2_BUCKET_NAME'] ?? null,
    $_ENV['R2_PUBLIC_DOMAIN'] ?? '',
    $_ENV['R2_DEFAULT_FOLDER'] ?? '',
    filter_var($_ENV['R2_ENABLE_PERMISSION_CHECK'] ?? false, FILTER_VALIDATE_BOOLEAN)
);

Usage with Laravel

The service provider is registered automatically. Add the values above to your .env and inject the service:

use Illuminate\Http\Request;
use Jamrul\Cloudflare\R2Service;

public function store(Request $request, R2Service $r2)
{
    $file = $request->file('image');

    $result = $r2->uploadFromPath(
        $file->getRealPath(),
        'avatars',
        $file->getClientOriginalName()
    );

    return $result['url'];
}

You can also resolve it with app('r2').

To publish the config file to config/r2.php:

php artisan vendor:publish --tag=r2-config

Available methods

Method Description
uploadFile(array $file, ?string $folder = null) Upload a $_FILES entry
uploadFromPath(string $path, ?string $folder = null, ?string $customName = null) Upload a file from disk
uploadContent(string $content, string $fileName, ?string $folder = null, string $contentType = 'application/octet-stream') Upload raw content (generated PDF, JSON, and so on)
updateFile(string $oldKey, array $newFile, ?string $folder = null) Upload a new file, then delete the old one
deleteFile(string $key) Delete one file
deleteFiles(array $keys) Delete many files (batched by 1000)
fileExists(string $key) Check whether a file exists
getUrl(string $key) Public URL (empty string if no public domain is set)
temporaryUrl(string $key, int $minutes = 60) Signed URL for private files
listFiles(?string $folder = null) List files in a folder (handles pagination)
getImages(?string $folder = null) Alias of listFiles()
checkPermissions() Test read, write and delete access

Upload methods return:

[
    'key'       => 'uploads/1735700000_a1b2c3_photo.jpg',
    'file_name' => 'photo.jpg',
    'url'       => 'https://cdn.example.com/uploads/1735700000_a1b2c3_photo.jpg',
    'size'      => 20480,
    'type'      => 'image/jpeg',
]

Store the key in your database. You need it to delete, replace or sign the file later.

Error handling

Failures throw \RuntimeException (the original AWS exception is available through getPrevious()). Missing configuration throws \InvalidArgumentException.

try {
    $r2->uploadFile($_FILES['file']);
} catch (\RuntimeException $e) {
    echo $e->getMessage();
}

Demo page

examples/index.php is a ready-made test page for upload, replace, delete and list. Copy it next to your .env, run composer require vlucas/phpdotenv, and open it in the browser.

The demo has no authentication. Use it only for local testing and never deploy it publicly.

Security notes

  • Never commit your .env file or API credentials.
  • Validate file type and size in your own application before uploading.
  • Use temporaryUrl() instead of a public domain for private files.
  • Keep R2_ENABLE_PERMISSION_CHECK off in production. The check writes and deletes a test file every time it runs.

License

Released under the MIT License.