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(), pending assets are automatically cleaned up after successful addition to an entity.

cleanExpiredPendingAssets()

Manually triggers cleanup of all expired pending assets.

public function cleanExpiredPendingAssets(): void

Example:

$manager = PendingAssetManager::make();
$manager->cleanExpiredPendingAssets();

echo "Expired assets cleaned";

Automatic Cleanup:

Expired pending assets are automatically cleaned up by the AssetConnectJob queue job. When assets are processed, the job also handles cleanup of expired pending assets from the default pending storage. This ensures that temporary files don't accumulate over time.

If you need to manually trigger cleanup outside of the queue job, you can use the method shown above.

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');

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

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

    // Add to entity
    $user->addAssetFromPending($pending)
        ->toAssetCollection(Photos::class);

    // Clean up
    $manager->deleteById($pendingId);

    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');

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

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

    $user->addAssetFromPending($pending)
        ->toAssetCollection(Images::class);

    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');
    $manager = PendingAssetManager::make();

    foreach ($pendingIds as $pendingId) {
        $pending = $manager->fetchById($pendingId);

        if (!$pending) {
            continue; // Skip expired or invalid
        }

        $product->addAssetFromPending($pending)
            ->toAssetCollection(ProductImages::class);
    }

    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 assets are automatically cleaned up after successful addition
$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)->save(); // 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: Expired pending assets are automatically cleaned up by the AssetConnectJob queue job when processing assets. No additional setup is required for automatic cleanup.

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;
}

To use a custom storage implementation:

use App\Storage\CustomPendingStorage;

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

See Also