Laravel Integration

Introduction

This guide shows you how to integrate Feedframer into your Laravel application using Laravel's HTTP client, caching, and best practices.

Installation & Setup

Step 1: Add API Key to Environment

Add your Feedframer API key to .env:

FEEDFRAMER_API_KEY=your_api_key_here

Step 2: Create Configuration File

Create config/services.php entry (or add to existing file):

// config/services.php

return [
    // ... other services

    'feedframer' => [
        'api_key' => env('FEEDFRAMER_API_KEY'),
        'base_url' => env('FEEDFRAMER_BASE_URL', 'https://feedframer.com'),
    ],
];

Basic Usage

Simple HTTP Request

use Illuminate\Support\Facades\Http;

$response = Http::get('https://feedframer.com/api/v1/me', [
    'api_key' => config('services.feedframer.api_key'),
    'page[size]' => 12,
]);

$posts = $response->json()['data'];

return view('instagram.feed', compact('posts'));

Controller Example

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Http;

class InstagramFeedController extends Controller
{
    public function index()
    {
        $response = Http::get('https://feedframer.com/api/v1/me', [
            'api_key' => config('services.feedframer.api_key'),
            'filter[type]' => 'IMAGE',
            'page[size]' => 12,
            'sort' => '-published_at',
        ]);

        if ($response->failed()) {
            return view('instagram.feed', [
                'posts' => [],
                'error' => 'Unable to load Instagram posts'
            ]);
        }

        $data = $response->json();

        return view('instagram.feed', [
            'posts' => $data['data'],
            'pagination' => $data['meta'],
        ]);
    }
}

Blade Template

<!-- resources/views/instagram/feed.blade.php -->

<div class="instagram-feed">
    @if(isset($error))
        <div class="alert alert-error">{{ $error }}</div>
    @endif

    <div class="grid grid-cols-3 gap-4">
        @foreach($posts as $post)
            <div class="instagram-post">
                <img src="{{ $post['mediaUrl'] }}"
                     alt="{{ Str::limit($post['caption'], 100) }}">

                <div class="post-caption">
                    {{ $post['caption'] }}
                </div>

                <div class="post-stats">
                    <span>❤️ {{ $post['likeCount'] }}</span>
                    <span>💬 {{ $post['commentsCount'] }}</span>
                </div>

                <a href="{{ $post['permalink'] }}"
                   target="_blank"
                   class="view-on-instagram">
                    View on Instagram
                </a>
            </div>
        @endforeach
    </div>
</div>

Service Class Pattern

Create a dedicated service class for cleaner code:

Create Service

<?php

namespace App\Services;

use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Log;

class FeedframerService
{
    protected string $apiKey;
    protected string $baseUrl;

    public function __construct()
    {
        $this->apiKey = config('services.feedframer.api_key');
        $this->baseUrl = config('services.feedframer.base_url');
    }

    public function getPosts(array $params = [])
    {
        try {
            $response = Http::get("{$this->baseUrl}/api/v1/me", array_merge([
                'api_key' => $this->apiKey,
            ], $params));

            if ($response->successful()) {
                return $response->json();
            }

            $this->logError($response);
            return null;

        } catch (\Exception $e) {
            Log::error('Feedframer API error: ' . $e->getMessage());
            return null;
        }
    }

    public function getImagePosts(int $limit = 12)
    {
        return $this->getPosts([
            'filter[type]' => 'IMAGE',
            'page[size]' => $limit,
            'sort' => '-published_at',
        ]);
    }

    public function getVideoPosts(int $limit = 12)
    {
        return $this->getPosts([
            'filter[type]' => 'VIDEO',
            'page[size]' => $limit,
            'sort' => '-published_at',
        ]);
    }

    public function getReels(int $limit = 12)
    {
        return $this->getPosts([
            'filter[type]' => 'REELS',
            'page[size]' => $limit,
            'sort' => '-published_at',
        ]);
    }

    protected function logError($response)
    {
        $error = $response->json()['errors'][0] ?? null;

        if ($error) {
            Log::error('Feedframer API Error', [
                'code' => $error['code'],
                'detail' => $error['detail'],
                'status' => $error['status'],
            ]);
        }
    }
}

Use Service in Controller

<?php

namespace App\Http\Controllers;

use App\Services\FeedframerService;

class InstagramFeedController extends Controller
{
    public function __construct(
        protected FeedframerService $feedframer
    ) {}

    public function index()
    {
        $data = $this->feedframer->getImagePosts(12);

        return view('instagram.feed', [
            'posts' => $data['data'] ?? [],
        ]);
    }

    public function videos()
    {
        $data = $this->feedframer->getVideoPosts(12);

        return view('instagram.videos', [
            'posts' => $data['data'] ?? [],
        ]);
    }
}

Caching

Cache API responses to reduce API calls and improve performance:

Basic Caching

use Illuminate\Support\Facades\Cache;

$posts = Cache::remember('instagram_posts', 3600, function () {
    return Http::get('https://feedframer.com/api/v1/me', [
        'api_key' => config('services.feedframer.api_key'),
        'page[size]' => 12,
    ])->json();
});

Service with Caching

public function getPosts(array $params = [], bool $cached = true)
{
    if (!$cached) {
        return $this->fetchPosts($params);
    }

    $cacheKey = 'feedframer_posts_' . md5(json_encode($params));

    return Cache::remember($cacheKey, 3600, function () use ($params) {
        return $this->fetchPosts($params);
    });
}

protected function fetchPosts(array $params)
{
    $response = Http::get("{$this->baseUrl}/api/v1/me", array_merge([
        'api_key' => $this->apiKey,
    ], $params));

    return $response->successful() ? $response->json() : null;
}

Cache Invalidation

// Clear specific cache
Cache::forget('feedframer_posts_' . md5(json_encode($params)));

// Clear all Feedframer caches
Cache::flush(); // Not recommended - clears all cache

// Or use cache tags (Redis/Memcached only)
Cache::tags(['feedframer'])->flush();

GraphQL Integration

GraphQL Query

use Illuminate\Support\Facades\Http;

$query = <<<'GRAPHQL'
query {
  posts(first: 12, type: "IMAGE") {
    data {
      id
      caption
      mediaUrl
      publishedAt
      instagramAccount {
        username
      }
    }
  }
}
GRAPHQL;

$response = Http::post('https://feedframer.com/graphql', [
    'query' => $query,
], [
    'api_key' => config('services.feedframer.api_key'),
]);

$posts = $response->json()['data']['posts']['data'];

GraphQL Service Method

public function queryGraphQL(string $query, array $variables = [])
{
    $response = Http::post("{$this->baseUrl}/graphql", [
        'query' => $query,
        'variables' => $variables,
    ], [
        'api_key' => $this->apiKey,
    ]);

    if ($response->successful()) {
        $data = $response->json();

        if (isset($data['errors'])) {
            Log::error('GraphQL errors', $data['errors']);
            return null;
        }

        return $data['data'];
    }

    return null;
}

// Usage
$query = 'query { posts(first: 12) { data { id caption } } }';
$data = $this->feedframer->queryGraphQL($query);
$posts = $data['posts']['data'];

Filtering

Filter by Type

// Images only
$images = $this->feedframer->getPosts([
    'filter[type]' => 'IMAGE',
    'page[size]' => 24,
]);

// Videos only
$videos = $this->feedframer->getPosts([
    'filter[type]' => 'VIDEO',
    'page[size]' => 12,
]);

Filter by Date Range

use Carbon\Carbon;

$posts = $this->feedframer->getPosts([
    'filter[publishedAfter]' => Carbon::now()->subDays(30)->toIso8601String(),
    'filter[publishedBefore]' => Carbon::now()->toIso8601String(),
    'page[size]' => 50,
]);

Pagination

Cursor-Based Pagination

public function getPaginatedPosts(Request $request)
{
    $params = [
        'page[size]' => 12,
    ];

    if ($request->has('cursor')) {
        $params['page[cursor]'] = $request->input('cursor');
    }

    $data = $this->feedframer->getPosts($params);

    return view('instagram.feed', [
        'posts' => $data['data'],
        'nextCursor' => $data['meta']['next_cursor'],
        'prevCursor' => $data['meta']['prev_cursor'],
    ]);
}

Blade Pagination Links

@if($nextCursor)
    <a href="?cursor={{ $nextCursor }}" class="btn">Load More</a>
@endif

Rate Limiting

Check Rate Limits

$response = Http::get('https://feedframer.com/api/v1/me', [
    'api_key' => config('services.feedframer.api_key'),
]);

$limit = $response->header('X-RateLimit-Limit');
$remaining = $response->header('X-RateLimit-Remaining');

Log::info("Rate limit: {$remaining}/{$limit} remaining");

Handle Rate Limit Errors

public function getPosts(array $params = [])
{
    $response = Http::get("{$this->baseUrl}/api/v1/me", array_merge([
        'api_key' => $this->apiKey,
    ], $params));

    if ($response->status() === 429) {
        Log::warning('Rate limit exceeded');

        // Return cached data if available
        return Cache::get('last_successful_posts');
    }

    if ($response->successful()) {
        $data = $response->json();
        Cache::put('last_successful_posts', $data, 3600);
        return $data;
    }

    return null;
}

Error Handling

Comprehensive Error Handling

public function getPosts(array $params = [])
{
    try {
        $response = Http::timeout(10)->get("{$this->baseUrl}/api/v1/me", array_merge([
            'api_key' => $this->apiKey,
        ], $params));

        if ($response->successful()) {
            return $response->json();
        }

        // Handle specific error codes
        $error = $response->json()['errors'][0] ?? null;

        if ($error) {
            match ($error['code']) {
                'INVALID_API_KEY' => Log::error('Invalid Feedframer API key'),
                'RATE_LIMIT_EXCEEDED' => Log::warning('Feedframer rate limit exceeded'),
                default => Log::error('Feedframer API error: ' . $error['detail']),
            };
        }

        return null;

    } catch (\Illuminate\Http\Client\ConnectionException $e) {
        Log::error('Connection to Feedframer failed: ' . $e->getMessage());
        return null;
    }
}

View Composer

Share Instagram posts across multiple views:

// app/Providers/ViewServiceProvider.php

namespace App\Providers;

use Illuminate\Support\ServiceProvider;
use Illuminate\Support\Facades\View;
use App\Services\FeedframerService;

class ViewServiceProvider extends ServiceProvider
{
    public function boot(FeedframerService $feedframer)
    {
        View::composer('partials.instagram-sidebar', function ($view) use ($feedframer) {
            $data = $feedframer->getImagePosts(6);

            $view->with('instagramPosts', $data['data'] ?? []);
        });
    }
}
<!-- resources/views/partials/instagram-sidebar.blade.php -->

<div class="instagram-sidebar">
    <h3>Latest from Instagram</h3>

    <div class="grid grid-cols-2 gap-2">
        @foreach($instagramPosts as $post)
            <a href="{{ $post['permalink'] }}" target="_blank">
                <img src="{{ $post['mediaUrl'] }}"
                     alt="Instagram post"
                     class="rounded">
            </a>
        @endforeach
    </div>
</div>

Scheduled Updates

Fetch and cache posts on a schedule:

// app/Console/Kernel.php

protected function schedule(Schedule $schedule)
{
    $schedule->call(function () {
        $feedframer = app(FeedframerService::class);
        $data = $feedframer->getPosts(['page[size]' => 50]);

        if ($data) {
            Cache::put('instagram_feed', $data, 3600);
        }
    })->hourly();
}

Testing

Mock HTTP Responses

// tests/Feature/InstagramFeedTest.php

use Illuminate\Support\Facades\Http;

test('displays instagram feed', function () {
    Http::fake([
        'feedframer.com/*' => Http::response([
            'username' => 'test_user',
            'name' => 'Test User',
            'posts' => [
                [
                    'id' => '18123456789012345',
                    'caption' => 'Test post',
                    'mediaUrl' => 'https://example.com/image.jpg',
                    'mediaType' => 'IMAGE',
                    'permalink' => 'https://instagram.com/p/ABC123',
                    'timestamp' => '2024-01-15T18:30:00+00:00'
                ],
            ],
            'pagination' => [
                'nextCursor' => null,
                'prevCursor' => null,
                'hasMore' => false,
                'perPage' => 12,
            ],
        ], 200),
    ]);

    $response = $this->get('/instagram');

    $response->assertOk();
    $response->assertSee('Test post');
});

Best Practices

  1. Use Service Classes - Encapsulate API logic in dedicated services
  2. Cache Aggressively - Cache for at least 1 hour to reduce API calls
  3. Handle Errors Gracefully - Always provide fallback content
  4. Log Issues - Log API errors for debugging
  5. Environment Configuration - Store API keys in .env
  6. Rate Limit Awareness - Monitor rate limits and implement caching

Complete Example

Full working example:

// app/Services/FeedframerService.php
<?php

namespace App\Services;

use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Log;

class FeedframerService
{
    protected string $apiKey;
    protected string $baseUrl;
    protected int $cacheMinutes = 60;

    public function __construct()
    {
        $this->apiKey = config('services.feedframer.api_key');
        $this->baseUrl = config('services.feedframer.base_url');
    }

    public function getLatestPosts(int $limit = 12): array
    {
        return Cache::remember("feedframer_latest_{$limit}", $this->cacheMinutes, function () use ($limit) {
            $data = $this->makeRequest([
                'page[size]' => $limit,
                'sort' => '-published_at',
            ]);

            return $data['data'] ?? [];
        });
    }

    protected function makeRequest(array $params = []): ?array
    {
        try {
            $response = Http::timeout(10)
                ->get("{$this->baseUrl}/api/v1/me", array_merge([
                    'api_key' => $this->apiKey,
                ], $params));

            if ($response->successful()) {
                return $response->json();
            }

            $this->handleError($response);
            return null;

        } catch (\Exception $e) {
            Log::error('Feedframer request failed', [
                'message' => $e->getMessage(),
                'params' => $params,
            ]);
            return null;
        }
    }

    protected function handleError($response): void
    {
        $error = $response->json()['errors'][0] ?? null;

        if ($error) {
            Log::error('Feedframer API Error', [
                'code' => $error['code'],
                'detail' => $error['detail'],
            ]);
        }
    }
}
// app/Http/Controllers/InstagramFeedController.php
<?php

namespace App\Http\Controllers;

use App\Services\FeedframerService;

class InstagramFeedController extends Controller
{
    public function index(FeedframerService $feedframer)
    {
        $posts = $feedframer->getLatestPosts(12);

        return view('instagram.feed', compact('posts'));
    }
}
// routes/web.php
Route::get('/instagram', [InstagramFeedController::class, 'index'])->name('instagram');

Next Steps