Cookbook

Storage

Configure disks, read/write files, generate URLs, and handle errors

This guide covers the framework's storage layer: configuring disks, reading/writing files, safe path handling, URL generation, and error handling.

Overview

  • Storage core: Glueful\Storage\StorageManager wraps Flysystem and manages multiple disks based on config/storage.php.
  • Path safety: Glueful\Storage\PathGuard validates and normalizes paths (prevents traversal, null bytes, and disallows absolute paths by default).
  • URLs: Glueful\Storage\Support\UrlGenerator formats public URLs using disk base_url/cdn_base_url.
  • Errors: Glueful\Storage\Exceptions\StorageException wraps Flysystem exceptions with machine‑readable reason + suggested HTTP status.
  • DI: Provided via the StorageProvider with a string alias storage.

Configuration

Edit config/storage.php to define your disks.

Basic Configuration

return [
    // Default disk (respects STORAGE_DEFAULT_DISK, or falls back to STORAGE_DRIVER)
    'default' => env('STORAGE_DEFAULT_DISK', env('STORAGE_DRIVER', 'uploads')),

    // PathGuard configuration
    'path_guard' => [
        'allow_absolute' => false,      // Reject absolute paths by default
        'max_path_length' => 4096,      // Maximum path length
        // Traversal ('..') and null bytes ("\0") are always rejected
    ],

    'disks' => [
        // Local filesystem
        'uploads' => [
            'driver' => 'local',
            'root' => config('app.paths.uploads'),
            'visibility' => 'private', // 'private' or 'public'
            'base_url' => config('app.urls.cdn'), // Public URL base
        ],

        // In-memory filesystem (great for testing)
        'memory' => [
            'driver' => 'memory',
        ],

        // S3-compatible storage
        's3' => [
            'driver' => 's3',
            'key' => env('S3_ACCESS_KEY_ID'),
            'secret' => env('S3_SECRET_ACCESS_KEY'),
            'region' => env('S3_REGION', 'us-east-1'),
            'bucket' => env('S3_BUCKET'),
            'prefix' => env('S3_PREFIX', ''), // Optional path prefix
            'endpoint' => env('S3_ENDPOINT'),
            'use_path_style_endpoint' => true,
            'cdn_base_url' => env('S3_CDN_BASE_URL'),
        ],

        // Azure Blob Storage
        'azure' => [
            'driver' => 'azure',
            'connection_string' => env('AZURE_CONNECTION_STRING'),
            'container' => env('AZURE_CONTAINER'),
            'prefix' => env('AZURE_PREFIX', ''),
            // Alternative: provide a prebuilt adapter
            // 'adapter' => $customAzureAdapter,
        ],

        // Google Cloud Storage
        'gcs' => [
            'driver' => 'gcs',
            'key_file' => env('GCS_KEY_FILE'),
            'project_id' => env('GCS_PROJECT_ID'),
            'bucket' => env('GCS_BUCKET'),
            'prefix' => env('GCS_PREFIX', ''),
        ],
    ],
];

Supported drivers: local, memory, s3, azure, gcs.

  • Cloud drivers require their Flysystem adapter packages. If missing, StorageManager throws an instructive error.

Public URLs (base_url/cdn_base_url)

Set base_url (or cdn_base_url) on a disk to enable UrlGenerator to build public links:

// config/storage.php
return [
    'default' => env('STORAGE_DEFAULT_DISK', env('STORAGE_DRIVER', 'uploads')),
    'disks' => [
        'uploads' => [
            'driver' => 'local',
            'root' => config('app.paths.uploads'),
            'visibility' => 'public',
            'base_url' => config('app.urls.cdn'), // e.g. https://cdn.example.com
        ],
        's3' => [
            'driver' => 's3',
            'bucket' => env('S3_BUCKET'),
            'region' => env('S3_REGION', 'us-east-1'),
            'cdn_base_url' => env('S3_CDN_BASE_URL'), // takes precedence if set
            // optional signing hints used in the example below
            'signed_urls' => true,
            'signed_ttl' => (int) env('S3_SIGNED_URL_TTL', 3600),
        ],
    ],
];

UrlGenerator::url($path, $disk) prefers cdn_base_url if present; otherwise uses base_url. If neither is set, it returns the path unchanged.

Dependency Injection

Resolve services:

use Glueful\Storage\{StorageManager, PathGuard};
use Glueful\Storage\Support\UrlGenerator;

$storage = app(StorageManager::class);      // or app('storage')
$guard   = app(PathGuard::class);          // Path validation service
$urls    = app(UrlGenerator::class);       // URL generator service

The provider also binds a convenient string alias: app('storage').

Common Tasks

Choose a disk

$fs = app(StorageManager::class)->disk();        // default disk
$s3 = app(StorageManager::class)->disk('s3');    // named disk

if (!app(StorageManager::class)->diskExists('s3')) {
    // Log or fall back to default
}

Native Flysystem Operations

The disk() method returns a Flysystem FilesystemOperator, giving you access to all native Flysystem methods (v3):

$disk = app(StorageManager::class)->disk();

// File operations
if ($disk->fileExists('path/to/file.txt')) {
    $content = $disk->read('path/to/file.txt');
    $disk->write('backup/file.txt', $content);
    $disk->delete('path/to/file.txt');
}

// Copy and move files
$disk->copy('source.txt', 'destination.txt');
$disk->move('old-path.txt', 'new-path.txt');

// Directory operations
$disk->createDirectory('logs/2025');
$disk->deleteDirectory('temp');

// File metadata
$size = $disk->fileSize('document.pdf');           // bytes
$modified = $disk->lastModified('document.pdf');   // timestamp
$mime = $disk->mimeType('image.jpg');              // MIME type

// Visibility (permissions)
$disk->setVisibility('public/file.txt', 'public');
$visibility = $disk->visibility('public/file.txt'); // 'public' or 'private'

// Stream operations
$stream = fopen('large-file.dat', 'r');
$disk->writeStream('uploads/large.dat', $stream);
$readStream = $disk->readStream('uploads/large.dat');

Visibility semantics (Local driver)

  • On the Local driver, permissions are mapped via Flysystem’s PortableVisibilityConverter:
    • Files: public = 0644, private = 0600
    • Directories: public = 0755, private = 0700
  • Default disk visibility is private unless overridden in config/storage.php.
  • For non-local drivers, visibility semantics are adapter-defined.

Write and read JSON

$storage = app(StorageManager::class);

$storage->putJson('reports/daily.json', [
    'generated_at' => date(DATE_ATOM),
    'items' => [1, 2, 3],
]);

$data = $storage->getJson('reports/daily.json');

Stream large uploads atomically

$fp = fopen('/path/to/bigfile', 'rb');
app(StorageManager::class)->putStream('uploads/big.dat', $fp);

This uses a temporary file strategy: writes to a .tmp file first, then atomically moves it into place. The temp file is cleaned up on failure.

List contents

foreach (app(StorageManager::class)->listContents('backups', true) as $entry) {
    // $entry is a StorageAttributes instance
    $path = $entry->path();
    $isFile = $entry->isFile();
    $isDir = $entry->isDir();
}

Generate public URLs

$urlGen = app(Glueful\Storage\Support\UrlGenerator::class);

// Generate URL for a file
$url = $urlGen->url('images/logo.png');
// Uses disk base_url/cdn_base_url when configured, else returns the path

// Get disk configuration (useful for signed URLs)
$diskConfig = $urlGen->diskConfig('s3'); // Returns array of disk config

Signed URLs (S3 example)

For time-limited access to private S3 objects, use the AWS SDK to generate a presigned URL. This example reads disk settings via UrlGenerator::diskConfig():

use Glueful\Storage\Support\UrlGenerator;

$cfg = app(UrlGenerator::class)->diskConfig('s3');

// Guard for availability
if (class_exists(\Aws\S3\S3Client::class) && isset($cfg['bucket'])) {
    $client = new \Aws\S3\S3Client([
        'version' => 'latest',
        'region'  => (string)($cfg['region'] ?? 'us-east-1'),
        // Optional endpoint and credentials
        'endpoint' => $cfg['endpoint'] ?? null,
        'credentials' => isset($cfg['key'], $cfg['secret']) && $cfg['key'] && $cfg['secret']
            ? ['key' => (string)$cfg['key'], 'secret' => (string)$cfg['secret']]
            : null,
    ]);

    $key = 'protected/reports/monthly.pdf';
    $ttl = (int)($cfg['signed_ttl'] ?? 3600);

    $cmd = $client->getCommand('GetObject', [
        'Bucket' => (string)$cfg['bucket'],
        'Key'    => $key,
    ]);
    $request = $client->createPresignedRequest($cmd, "+{$ttl} seconds");
    $signedUrl = (string)$request->getUri();
}

Notes:

  • Ensure the S3 adapter and AWS SDK are installed (league/flysystem-aws-s3-v3).
  • Use cdn_base_url for public assets; use presigned URLs for private objects.

Signed URLs via Uploader Storage

If you’re using the uploader flow and want a presigned link for an S3 object, you can use the uploader’s Flysystem-backed storage adapter directly:

use Glueful\Uploader\Storage\FlysystemStorage;
use Glueful\Storage\{StorageManager};
use Glueful\Storage\Support\UrlGenerator;

$storage = new FlysystemStorage(
    app(StorageManager::class),
    app(UrlGenerator::class),
    's3'
);

$signedUrl = $storage->getSignedUrl('protected/reports/monthly.pdf', 600); // 10 minutes

Notes:

  • Falls back to plain URL if adapter cannot sign (or on error).
  • If expiry is omitted or <= 0, uses storage.disks.s3.signed_ttl.

Helper Functions

// Get absolute path to storage directory
$path = storage_path();                    // /path/to/project/storage
$path = storage_path('logs/app.log');      // /path/to/project/storage/logs/app.log

Path Safety

PathGuard enforces security rules on file paths:

Default Configuration

// config/storage.php
'path_guard' => [
    'allow_absolute' => false,              // Reject absolute paths like /etc/passwd
    'max_path_length' => 4096,              // Maximum allowed path length
    // Traversal ('..') and null bytes ("\0") are always rejected
]

Path Validation Rules

  • No null bytes (\0) - prevents string termination attacks
  • No .. traversal - prevents directory traversal attacks
  • Normalized separators - converts backslashes to forward slashes
  • Removes redundant ./ segments
  • Absolute paths rejected unless explicitly allowed
  • Path length limits enforced

Manual Validation

$guard = app(Glueful\Storage\PathGuard::class);

try {
    // Validates and normalizes the path
    $safe = $guard->validate('reports/2025/../2025/summary.json');
    // Returns: 'reports/2025/summary.json' (normalized)
} catch (\InvalidArgumentException $e) {
    // Path validation failed
}

Error Handling

Most StorageManager operations convert Flysystem exceptions to StorageException with useful metadata:

Basic Error Handling

use Glueful\Storage\Exceptions\StorageException;

try {
    app(StorageManager::class)->getJson('missing/file.json');
} catch (StorageException $e) {
    $reason = $e->reason();      // Machine-readable reason code
    $status = $e->httpStatus();  // Suggested HTTP status code

    // Log with structured data
    $logger = app('logger');
    $logger->error('Storage operation failed', [
        'reason' => $reason,
        'status' => $status,
        'message' => $e->getMessage()
    ]);
}

Complete Error Reason Codes

Reason CodeHTTP StatusDescription
io_read_failed404Unable to read file
io_write_failed500Unable to write file
io_delete_failed500Unable to delete file
io_move_failed500Unable to move/rename file
io_copy_failed500Unable to copy file
dir_create_failed500Unable to create directory
dir_delete_failed500Unable to delete directory
existence_check_failed500Unable to check if file exists
metadata_retrieve_failed500Unable to get file metadata
visibility_set_failed403Unable to set permissions
list_failed500Unable to list directory contents
unknown_error500Unclassified error

Parsing Exception Messages

use Glueful\Storage\Support\ExceptionClassifier;

// Parse structured data from exception message
$parsed = ExceptionClassifier::parseFromMessage($e->getMessage());
// Returns: ['reason' => 'io_read_failed', 'http_status' => 404]

Advanced Usage

Custom Disk at Runtime

$disk = app(StorageManager::class)->disk('uploads');

// All Flysystem methods available
$disk->write('file.txt', 'content');
$disk->setVisibility('file.txt', 'public');

// Get underlying adapter for advanced operations
if ($disk instanceof \League\Flysystem\Filesystem) {
    $adapter = $disk->getAdapter();
    // Adapter-specific operations
}

Working with Streams

$disk = app(StorageManager::class)->disk();

// Read large file as stream
$stream = $disk->readStream('large-file.zip');
while (!feof($stream)) {
    $chunk = fread($stream, 8192);
    // Process chunk
}
fclose($stream);

// Write from stream
$input = fopen('php://input', 'r');
$disk->writeStream('uploads/posted.dat', $input);

Atomic Operations

// StorageManager::putStream() is atomic by default
$fp = fopen('critical-data.json', 'r');
app(StorageManager::class)->putStream('config/settings.json', $fp);
// Writes to temp file first, then moves atomically

Testing Tips

Memory Driver for Tests

// config/storage.php (testing environment)
'testing' => [
    'driver' => 'memory',
]
// In your test
$storage = app(StorageManager::class)->disk('testing');
$storage->write('test.txt', 'content');
// No actual filesystem writes!

Local Temp Directory

use Glueful\Storage\{StorageManager, PathGuard};

// Create an isolated disk for the test
$tempDir = sys_get_temp_dir() . '/test-' . uniqid();
mkdir($tempDir);

$sm = new StorageManager([
    'default' => 'test',
    'disks' => [
        'test' => [
            'driver' => 'local',
            'root' => $tempDir,
            'visibility' => 'private',
        ],
    ],
], new PathGuard());

$disk = $sm->disk('test');
$disk->write('example.txt', 'ok');

// Cleanup: delete all entries under the temp root
foreach ($disk->listContents('', true) as $entry) {
    if ($entry->isFile()) {
        $disk->delete($entry->path());
    } elseif ($entry->isDir()) {
        $disk->deleteDirectory($entry->path());
    }
}
rmdir($tempDir);

URL Testing

// Set test URLs
config(['storage.disks.uploads.base_url' => 'https://cdn.test']);

$url = app(UrlGenerator::class)->url('image.jpg', 'uploads');
$this->assertEquals('https://cdn.test/image.jpg', $url);

Reference

Core Classes

  • Glueful\Storage\StorageManager
    • disk(?string $name = null): FilesystemOperator - Get disk instance
    • diskExists(string $name): bool - Check if disk configured
    • putJson(string $path, mixed $data, ?string $disk = null): void
    • getJson(string $path, ?string $disk = null): mixed
    • putStream(string $path, $stream, ?string $disk = null): void - Atomic write
    • listContents(string $path, bool $recursive = false, ?string $disk = null): iterable
  • Glueful\Storage\PathGuard
    • validate(string $path): string - Validate and normalize path
  • Glueful\Storage\Support\UrlGenerator
    • url(string $path, ?string $disk = null): string - Generate public URL
    • diskConfig(string $disk): array - Get disk configuration
  • Glueful\Storage\Exceptions\StorageException
    • reason(): ?string - Get error reason code
    • httpStatus(): ?int - Get suggested HTTP status
    • fromFlysystem(FilesystemException $e, string $path = ''): self

Helper Functions

  • storage_path(string $path = ''): string - Get storage directory path