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
- Pending Assets - Overview of pending assets functionality
- DefaultPendingStorage - Default protected storage implementation
- Custom Pending Storage - Creating custom storage implementations