# Building a Google Photos Clone with Laravel and ittybit [View original](https://ittybit.com/guides/google-photos-clone-laravel) ## Automation Unleashed After establishing the foundation, it was time for the exciting part - bringing ittybit into the equation. What I discovered was that ittybit wasn't just another file storage service with some AI sprinkled on top. It was a complete media processing automation platform that could transform my simple upload flow into a sophisticated, event-driven workflow system. ## Understanding the ittybit Philosophy What impressed me straight away about ittybit was their automation-first approach. Rather than having to manually trigger individual processing tasks, you define workflows that automatically execute when certain events occur - like when new media is created. Their API supported everything from basic thumbnail generation to advanced AI analysis, all orchestrated through JSON workflow definitions. The core concepts were straightforward: * Media objects contain one or more files and serve as the primary containers * Files represent the actual assets (originals, thumbnails, processed versions) * Tasks handle individual processing operations * Automations define event-driven workflows that chain tasks together ## Setting Up the Integration Layer Laravel's HTTP client made the ittybit integration surprisingly elegant. I created a dedicated service that could handle everything from media creation to workflow automation. I could have used the IttyBit PHP SDK, but sometimes I do like a challenge! ```php readonly class IttybitService { public function __construct( private string $apiKey, private string $baseUrl = 'https://api.ittybit.com' ) {} public function createMedia(string $title = null, string $alt = null, array $metadata = []): array { $response = Http::withToken($this->apiKey) ->withHeaders(['Accept-Version' => '2025-08-20']) ->post("{$this->baseUrl}/media", [ 'title' => $title, 'alt' => $alt, 'metadata' => $metadata, ]); if ($response->failed()) { throw new IttybitException( "Media creation failed: " . $response->json('message', 'Unknown error') ); } return $response->json(); } public function createFileFromUrl( string $url, string $mediaId = null, array $options = [] ): array { $payload = array_merge([ 'url' => $url, 'media_id' => $mediaId, ], $options); $response = Http::withToken($this->apiKey) ->withHeaders(['Accept-Version' => '2025-08-20']) ->post("{$this->baseUrl}/files", $payload); if ($response->failed()) { throw new IttybitException( "File creation failed: " . $response->json('message', 'Unknown error') ); } return $response->json(); } public function createAutomation(array $workflow, string $name = null, string $description = null): array { $response = Http::withToken($this->apiKey) ->withHeaders(['Accept-Version' => '2025-08-20']) ->post("{$this->baseUrl}/automations", [ 'name' => $name, 'description' => $description, 'trigger' => [ 'kind' => 'event', 'event' => 'media.created' ], 'workflow' => $workflow, 'status' => 'active' ]); if ($response->failed()) { throw new IttybitException( "Automation creation failed: " . $response->json('message', 'Unknown error') ); } return $response->json(); } } ``` The readonly class ensured my service instances remained immutable - crucial when dealing with external API credentials. The `Accept-Version` header management showed ittybit's thoughtful approach to API versioning. ## Creating Smart Upload Workflows The real magic happened when I started building automated workflows. Instead of manually processing each photo after upload, I could define a workflow that would automatically execute whenever new media was created in IttyBit. ```php class ProcessPhotoWithIttybit { public function __construct( private readonly IttybitService $ittybit, private readonly MediaValidator $validator ) {} public function execute(UploadedFile $file, User $user): Photo { $this->validator->validate($file); $localPath = $file->store('photos/originals'); $publicUrl = Storage::url($localPath); $media = $this->ittybit->createMedia( title: $file->getClientOriginalName(), alt: "Photo uploaded by {$user->name}", metadata: ['user_id' => $user->id, 'source' => 'photo_upload'] ); $ittybitFile = $this->ittybit->createFileFromUrl( url: $publicUrl, mediaId: $media['id'], options: [ 'filename' => $file->getClientOriginalName(), 'folder' => "users/{$user->id}/photos", 'metadata' => ['uploaded_at' => now()->toISOString()] ] ); return Photo::query()->create([ 'user_id' => $user->id, 'original_name' => $file->getClientOriginalName(), 'local_path' => $localPath, 'ittybit_media_id' => $media['id'], 'ittybit_file_id' => $ittybitFile['id'], 'processing_status' => 'processing', 'size' => $file->getSize(), 'mime_type' => $file->getMimeType(), ]); } } ``` ## Building Intelligent Automation Workflows The power of ittybit really shone when I started creating automated workflows. These could chain together multiple processing tasks, with conditional logic to handle different media types appropriately. ```php class IttybitWorkflowBuilder { public function buildPhotoProcessingWorkflow(): array { return [ [ 'kind' => 'description', 'ref' => 'ai_description' ], [ 'kind' => 'nsfw', 'ref' => 'content_check' ], [ 'kind' => 'image', 'width' => 200, 'height' => 200, 'format' => 'webp', 'ref' => 'thumb_small' ], [ 'kind' => 'image', 'width' => 800, 'height' => 800, 'format' => 'webp', 'ref' => 'thumb_large' ], // Conditional processing for videos [ 'kind' => 'conditions', 'conditions' => [ ['prop' => 'media.kind', 'value' => 'video'] ], 'next' => [ [ 'kind' => 'thumbnails', 'ref' => 'video_thumbnails' ], [ 'kind' => 'subtitles', 'ref' => 'auto_subtitles' ] ] ] ]; } public function setupAutomation(string $name): array { $workflow = $this->buildPhotoProcessingWorkflow(); return app(IttybitService::class)->createAutomation( workflow: $workflow, name: $name, description: 'Automatically process uploaded photos and videos with AI analysis and thumbnail generation' ); } } ``` ## Handling Processing Results The beauty of ittybit's automation system was that all processing happened asynchronously, and results were stored within the media and file objects. I could poll for updates or set up webhooks for real-time notifications. ```php class UpdatePhotoWithIttybitResults implements ShouldQueue { use Queueable; public function __construct( private readonly Photo $photo ) {} public function handle(IttybitService $ittybit): void { try { $mediaData = $ittybit->getMedia($this->photo->ittybit_media_id); $processingResults = [ 'ai_description' => null, 'nsfw_score' => null, 'thumbnails' => [], 'video_data' => null, ]; foreach ($mediaData['files'] as $file) { match ($file['object']) { 'intelligence' => $this->extractIntelligenceData($file, $processingResults), 'source' => $this->extractSourceData($file, $processingResults), default => null }; } $this->photo->update([ 'processing_status' => 'completed', 'processing_results' => $processingResults, 'width' => $mediaData['width'], 'height' => $mediaData['height'], 'duration' => $mediaData['duration'], ]); } catch (\Exception $e) { $this->photo->update([ 'processing_status' => 'failed', 'processing_results' => ['error' => $e->getMessage()] ]); throw $e; } } private function extractIntelligenceData(array $file, array &$results): void { if (isset($file['metadata']['description'])) { $results['ai_description'] = $file['metadata']['description']; } if (isset($file['metadata']['nsfw_score'])) { $results['nsfw_score'] = $file['metadata']['nsfw_score']; } } private function extractSourceData(array $file, array &$results): void { if ($file['ref']) { match ($file['ref']) { 'thumb_small', 'thumb_large' => $results['thumbnails'][$file['ref']] = $file['url'], 'video_thumbnails' => $results['video_data']['thumbnails'] = $file['url'], default => null }; } } } ``` ## Frontend Integration with Processing Status With InertiaJS, showing real-time processing status updates felt natural. The processing results flowed seamlessly from ittybit through Laravel to Vue components. ```vue ``` ## Performance and Caching Strategies With ittybit handling the heavy lifting of media processing, I needed to ensure my application remained responsive while managing the asynchronous nature of their workflows. ```php public function getThumbnailUrl(string $size = 'large'): ?string { return Cache::remember( "photo.{$this->id}.thumbnail.{$size}", now()->addDays(7), // Thumbnails rarely change fn() => $this->processing_results['thumbnails']["thumb_{$size}"] ?? null ); } public function scopeProcessed(Builder $query): Builder { return $query->where('processing_status', 'completed'); } public function scopeWithResults(Builder $query): Builder { return $query->whereNotNull('processing_results'); } ``` ## The Developer Experience Transformation What caught me off-guard was how natural the ittybit integration felt. Their automation system aligned perfectly with Laravel's event-driven architecture. Creating workflows felt like writing Laravel policies or form requests - declarative, readable, and maintainable. The separation between media creation and processing meant I could build features incrementally. Start with basic upload functionality, then layer on AI analysis, thumbnail generation, and content moderation as separate concerns. ## Looking Forward As I completed the ittybit integration, I realized I'd built something that was already far more capable than a simple photo storage application. The automated workflows meant every uploaded image was immediately enhanced with AI-generated descriptions, properly sized thumbnails, and content safety analysis - all without blocking the user experience. But this was just the beginning. In the next article, we'll explore how to leverage these processed results to build sophisticated search, organization, and discovery features that make the vast capabilities of modern AI accessible through intuitive user interfaces.