Skip to content

Pending Asset Manager

The PendingAssetManager class provides a high-level API for managing pending assets. It handles storing, retrieving, and deleting pending assets, as well as managing their lifecycle and expiration.

Overview

PendingAssetManager acts as a facade over the configured PendingStorageInterface implementation, providing a simple and consistent interface for working with pending assets.

Namespace: Maniaba\AssetConnect\Pending\PendingAssetManager

Creating an Instance

use Maniaba\AssetConnect\Pending\PendingAssetManager;

// Using default storage from configuration
$manager = PendingAssetManager::make();

// Using custom storage
$customStorage = new MyCustomPendingStorage();
$manager = PendingAssetManager::make($customStorage);

Methods

make()

Creates a new instance of PendingAssetManager.

public static function make(?PendingStorageInterface $storage = null): PendingAssetManager

Parameters: - $storage - Optional custom pending storage implementation. If null, uses the storage configured in app/Config/Asset.php

Returns: New PendingAssetManager instance

Example:

// Default storage
$manager = PendingAssetManager::make();

// Custom storage
use App\Storage\S3PendingStorage;
$manager = PendingAssetManager::make(new S3PendingStorage());

store()

Stores a pending asset. If the asset doesn't have an ID, generates a new one and stores both file and metadata. If the asset already has an ID, updates only the metadata.

public function store(PendingAsset $pendingAsset, ?int $ttlSeconds = null): void

Parameters: - $pendingAsset - The pending asset to store - $ttlSeconds - Optional TTL in seconds (overrides default if provided)

Throws: - PendingAssetException - If unable to store the asset - RandomException - If unable to generate unique ID

Examples:

Store new pending asset

use Maniaba\AssetConnect\Pending\PendingAsset;
use Maniaba\AssetConnect\Pending\PendingAssetManager;

$pending = PendingAsset::createFromFile('/path/to/photo.jpg');
$pending->usingName('Profile Photo');

$manager = PendingAssetManager::make();
$manager->store($pending);

// ID is now available
$pendingId = $pending->id;

Store with custom TTL

$pending = PendingAsset::createFromFile('/path/to/document.pdf');

// Store with 1 hour TTL instead of default 24 hours
$manager = PendingAssetManager::make();
$manager->store($pending, 3600);

Update metadata only

// Fetch existing pending asset
$manager = PendingAssetManager::make();
$pending = $manager->fetchById($existingId);

// Update metadata
$pending->withCustomProperty('status', 'reviewed');
$pending->usingName('Updated Name');

// Store - only updates metadata.json, file remains unchanged
$manager->store($pending);

fetchById()

Fetches a pending asset by its ID. Returns null if not found or expired.

public function fetchById(string $id): ?PendingAsset

Parameters: - $id - Unique identifier of the pending asset

Returns: PendingAsset object if found and not expired, null otherwise

Throws: - PendingAssetException - If unable to read metadata

Example:

$manager = PendingAssetManager::make();

$pending = $manager->fetchById('a1b2c3d4e5f6');

if ($pending === null) {
    // Asset not found or expired
    echo "Pending asset not found";
} else {
    // Use the asset
    echo $pending->name;
    echo $pending->file_name;
}

Automatic Expiration Handling:

The method automatically checks if the asset has expired by comparing created_at + ttl with the current time. If expired: 1. Attempts to delete the expired asset 2. Returns null

$manager = PendingAssetManager::make();
$pending = $manager->fetchById($id);

// If returned null, either:
// 1. ID doesn't exist
// 2. Asset has expired (and was automatically deleted)
if ($pending === null) {
    return response()->json(['error' => 'Asset not found or expired'], 404);
}

deleteById()

Deletes a pending asset by its ID.

public function deleteById(string $id): bool

Parameters: - $id - Unique identifier of the pending asset to delete

Returns: true if deleted successfully, false otherwise

Example:

$manager = PendingAssetManager::make();

$success = $manager->deleteById($pendingId);

if ($success) {
    echo "Asset deleted successfully";
} else {
    echo "Failed to delete asset or asset not found";
}

Note: When using addAssetFromPending() with a pending ID, the ID is consumed immediately after ownership validation. The pending storage entry and token are deleted, and the real asset is stored from a temporary source file.

Expired pending assets are rejected when a known pending ID is fetched, and AssetConnect then attempts to delete that ID. DefaultPendingStorage does not list remote storage buckets to discover expired entries. For S3-compatible storage, use storage lifecycle rules on the pending prefix if you need background cleanup for unconsumed pending assets.

Complete Usage Examples

Example 1: Simple Upload Flow

// Upload endpoint
public function upload()
{
    $file = $this->request->getFile('photo');

    $pending = PendingAsset::createFromFile($file);
    $pending->usingName('User Photo');

    $manager = PendingAssetManager::make();
    $manager->store($pending);

    return $this->response->setJSON([
        'pending_id' => $pending->id
    ]);
}

// Confirm endpoint
public function confirm()
{
    $pendingId = $this->request->getPost('pending_id');

    try {
        // Add to entity by ID. This consumes and deletes the pending entry.
        $user->addAssetFromPending($pendingId)
            ->toAssetCollection(Photos::class);
    } catch (\Maniaba\AssetConnect\Exceptions\AssetException) {
        return $this->response->setStatusCode(404);
    }

    return $this->response->setJSON(['success' => true]);
}

Example 2: Multi-step Upload with Metadata Editing

// Step 1: Upload file
public function uploadFile()
{
    $file = $this->request->getFile('file');

    $pending = PendingAsset::createFromFile($file);

    $manager = PendingAssetManager::make();
    $manager->store($pending);

    return $this->response->setJSON([
        'pending_id' => $pending->id,
        'file_name' => $pending->file_name,
        'size' => $pending->size
    ]);
}

// Step 2: Edit metadata
public function updateMetadata()
{
    $pendingId = $this->request->getPost('pending_id');
    $name = $this->request->getPost('name');
    $alt = $this->request->getPost('alt');

    $manager = PendingAssetManager::make();
    $pending = $manager->fetchById($pendingId);

    if (!$pending) {
        return $this->response->setStatusCode(404);
    }

    // Update metadata (file remains unchanged)
    $pending->usingName($name)
        ->withCustomProperty('alt', $alt);

    $manager->store($pending);

    return $this->response->setJSON(['success' => true]);
}

// Step 3: Confirm and attach
public function confirmUpload()
{
    $pendingId = $this->request->getPost('pending_id');

    try {
        $user->addAssetFromPending($pendingId)
            ->toAssetCollection(Images::class);
    } catch (\Maniaba\AssetConnect\Exceptions\AssetException) {
        return $this->response->setStatusCode(404);
    }

    return $this->response->setJSON(['success' => true]);
}

Example 3: Batch Upload

public function batchUpload()
{
    $result = PendingAsset::createFromRequest('photos');

    if (empty($result['photos'])) {
        return $this->response->setStatusCode(400)
            ->setJSON(['error' => 'No files uploaded']);
    }

    $manager = PendingAssetManager::make();
    $pendingIds = [];

    foreach ($result['photos'] as $pending) {
        $manager->store($pending);

        $pendingIds[] = $pending->id;
    }

    return $this->response->setJSON([
        'pending_ids' => $pendingIds
    ]);
}

public function confirmBatch()
{
    $pendingIds = $this->request->getPost('pending_ids');

    foreach ($pendingIds as $pendingId) {
        try {
            $product->addAssetFromPending($pendingId)
                ->toAssetCollection(ProductImages::class);
        } catch (\Maniaba\AssetConnect\Exceptions\AssetException) {
            continue; // Skip expired, invalid, or already consumed IDs
        }
    }

    return $this->response->setJSON(['success' => true]);
}

Error Handling

Handling Expired Assets

$manager = PendingAssetManager::make();
$pending = $manager->fetchById($pendingId);

if ($pending === null) {
    return $this->response->setStatusCode(410) // 410 Gone
        ->setJSON([
            'error' => 'This upload has expired. Please upload again.',
            'code' => 'ASSET_EXPIRED'
        ]);
}

Handling Storage Errors

try {
    $manager = PendingAssetManager::make();
    $manager->store($pending);
} catch (PendingAssetException $e) {
    log_message('error', 'Failed to store pending asset: ' . $e->getMessage());

    return $this->response->setStatusCode(500)
        ->setJSON([
            'error' => 'Failed to store file. Please try again.',
            'code' => 'STORAGE_ERROR'
        ]);
}

Validation Before Storing

try {
    // createFromRequest automatically validates uploaded files
    $result = PendingAsset::createFromRequest('photo');

    if (empty($result['photo'])) {
        throw new \RuntimeException('No file uploaded');
    }

    $pending = $result['photo'][0];

    // Additional validation
    if ($pending->size > 10 * 1024 * 1024) { // 10MB
        throw new \RuntimeException('File too large');
    }

    $allowedMimes = ['image/jpeg', 'image/png', 'image/gif'];
    if (!in_array($pending->mime_type, $allowedMimes)) {
        throw new \RuntimeException('Invalid file type');
    }

    // Now safe to store
    $manager = PendingAssetManager::make();
    $manager->store($pending);

} catch (\Exception $e) {
    return $this->response->setStatusCode(400)
        ->setJSON(['error' => $e->getMessage()]);
}

Best Practices

1. Pending Assets Are Auto-Cleaned

// Pending IDs are consumed and removed before the real asset is stored
$user->addAssetFromPending($pendingId)
    ->toAssetCollection(Photos::class);
// File is automatically removed from pending storage

2. Check for Expiration

// Good
$pending = $manager->fetchById($id);
if (!$pending) {
    return response()->json(['error' => 'Expired'], 410);
}

// Bad
$pending = $manager->fetchById($id);
$user->addAssetFromPending($pending)->toAssetCollection(Photos::class); // May be null!

3. Use Appropriate TTL

// Short-lived uploads (e.g., profile picture)
$manager->store($pending, 1800); // 30 minutes

// Long-lived uploads (e.g., document approval workflow)
$manager->store($pending, 86400 * 7); // 7 days

Note: Default pending storage avoids bucket listing. Use storage lifecycle rules or an application-side index if you need scheduled cleanup of unconsumed expired pending assets.

Configuration

The default pending storage is configured in app/Config/Asset.php:

use Maniaba\AssetConnect\Pending\DefaultPendingStorage;

class Asset extends BaseConfig
{
    public string $pendingStorage = DefaultPendingStorage::class;
    public ?string $pendingStorageDisk = null; // falls back to defaultProtectedStorage
    public string $pendingStoragePrefix = 'assets_pending';
}

To use a custom storage implementation:

use App\Storage\CustomPendingStorage;

class Asset extends BaseConfig
{
    public string $pendingStorage = CustomPendingStorage::class;
}

See Also