Using Cockpit with Laravel
Learn how to integrate Cockpit CMS as a headless content management system for your Laravel applications.
- Overview
- Project Setup
- Prerequisites
- Installation
- Environment Configuration
- Creating the Cockpit Service
- Service Provider
- Configuration File
- Cockpit Service Class
- Laravel Integration
- Facade
- Blade Components
- Content Models
- Building a Blog Example
- Routes
- Controllers
- Views
- Artisan Commands
- API Development
- Caching Strategies
- Cache Warming
- Testing
- Best Practices
- 1. Error Handling
- 2. Performance Optimization
- 3. Configuration Management
- Deployment
- Environment Variables
- Cache Configuration
- Troubleshooting
- Common Issues
Overview
Cockpit CMS provides an excellent headless CMS solution that integrates seamlessly with Laravel. This guide covers:
- Setting up a Laravel service provider for Cockpit
- Creating a fluent API client with caching
- Building Blade components for content rendering
- Implementing content synchronization
- Creating custom Artisan commands
- Building RESTful APIs with Cockpit data
Project Setup
Prerequisites
- PHP 8.1+ with required extensions
- Laravel 10+ installed
- Cockpit CMS instance running
- Composer for dependency management
Installation
Create a new Laravel project or use an existing one:
composer create-project laravel/laravel my-cockpit-app
cd my-cockpit-app
Environment Configuration
Add Cockpit configuration to your .env
file:
# Cockpit Configuration
COCKPIT_API_URL=http://localhost:8080/api
COCKPIT_API_KEY=your-api-key-here
COCKPIT_CACHE_TTL=3600
Creating the Cockpit Service
Service Provider
Create app/Providers/CockpitServiceProvider.php
:
<?php
namespace App\Providers;
use App\Services\CockpitService;
use Illuminate\Support\ServiceProvider;
class CockpitServiceProvider extends ServiceProvider
{
public function register(): void
{
$this->app->singleton(CockpitService::class, function ($app) {
return new CockpitService(
config('services.cockpit.api_url'),
config('services.cockpit.api_key'),
config('services.cockpit.cache_ttl', 3600)
);
});
$this->app->alias(CockpitService::class, 'cockpit');
}
public function boot(): void
{
$this->publishes([
__DIR__.'/../../config/cockpit.php' => config_path('cockpit.php'),
], 'cockpit-config');
}
}
Configuration File
Create config/cockpit.php
:
<?php
return [
'api_url' => env('COCKPIT_API_URL', 'http://localhost:8080/api'),
'api_key' => env('COCKPIT_API_KEY'),
'cache_ttl' => env('COCKPIT_CACHE_TTL', 3600),
'cache_prefix' => 'cockpit_',
'timeout' => 30,
];
Update config/services.php
:
'cockpit' => [
'api_url' => env('COCKPIT_API_URL'),
'api_key' => env('COCKPIT_API_KEY'),
'cache_ttl' => env('COCKPIT_CACHE_TTL', 3600),
],
Cockpit Service Class
Create app/Services/CockpitService.php
:
<?php
namespace App\Services;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Http;
use Illuminate\Http\Client\PendingRequest;
use Illuminate\Support\Collection;
class CockpitService
{
protected string $apiUrl;
protected string $apiKey;
protected int $cacheTtl;
protected PendingRequest $client;
public function __construct(string $apiUrl, string $apiKey, int $cacheTtl = 3600)
{
$this->apiUrl = rtrim($apiUrl, '/');
$this->apiKey = $apiKey;
$this->cacheTtl = $cacheTtl;
$this->client = Http::withHeaders([
'api-key' => $this->apiKey,
'Content-Type' => 'application/json',
])->timeout(config('cockpit.timeout', 30));
}
/**
* Get collection items
*/
public function getItems(string $collection, array $options = []): Collection
{
$cacheKey = $this->getCacheKey('items', $collection, $options);
return Cache::remember($cacheKey, $this->cacheTtl, function () use ($collection, $options) {
$response = $this->client->get("{$this->apiUrl}/content/items/{$collection}", $options);
if (!$response->successful()) {
throw new \Exception("Failed to fetch items from collection: {$collection}");
}
return collect($response->json());
});
}
/**
* Get single item by ID
*/
public function getItem(string $collection, string $id, bool $useCache = true): ?array
{
if (!$useCache) {
return $this->fetchItem($collection, $id);
}
$cacheKey = $this->getCacheKey('item', $collection, ['id' => $id]);
return Cache::remember($cacheKey, $this->cacheTtl, function () use ($collection, $id) {
return $this->fetchItem($collection, $id);
});
}
/**
* Get singleton content
*/
public function getSingleton(string $singleton, bool $useCache = true): ?array
{
if (!$useCache) {
return $this->fetchSingleton($singleton);
}
$cacheKey = $this->getCacheKey('singleton', $singleton);
return Cache::remember($cacheKey, $this->cacheTtl, function () use ($singleton) {
return $this->fetchSingleton($singleton);
});
}
/**
* Clear cache for specific content
*/
public function clearCache(?string $type = null, ?string $name = null): void
{
if ($type === null) {
Cache::flush();
return;
}
$pattern = config('cockpit.cache_prefix') . "{$type}:{$name}:*";
Cache::forget($pattern);
}
/**
* Get asset URL
*/
public function getAssetUrl(string $assetId, array $params = []): string
{
$baseUrl = str_replace('/api', '', $this->apiUrl);
$query = http_build_query($params);
return "{$baseUrl}/api/assets/image/{$assetId}" . ($query ? "?{$query}" : '');
}
protected function fetchItem(string $collection, string $id): ?array
{
$response = $this->client->get("{$this->apiUrl}/content/item/{$collection}/{$id}");
if (!$response->successful()) {
return null;
}
return $response->json();
}
protected function fetchSingleton(string $singleton): ?array
{
$response = $this->client->get("{$this->apiUrl}/content/item/{$singleton}");
if (!$response->successful()) {
return null;
}
return $response->json();
}
protected function getCacheKey(string $type, string $name, array $params = []): string
{
$prefix = config('cockpit.cache_prefix', 'cockpit_');
$paramKey = md5(json_encode($params));
return "{$prefix}{$type}:{$name}:{$paramKey}";
}
public function getClient(): PendingRequest
{
return $this->client;
}
}
Laravel Integration
Facade
Create app/Facades/Cockpit.php
:
<?php
namespace App\Facades;
use Illuminate\Support\Facades\Facade;
class Cockpit extends Facade
{
protected static function getFacadeAccessor(): string
{
return 'cockpit';
}
}
Register the facade in config/app.php
:
'aliases' => Facade::defaultAliases()->merge([
'Cockpit' => App\Facades\Cockpit::class,
])->toArray(),
Blade Components
Create app/View/Components/CockpitImage.php
:
<?php
namespace App\View\Components;
use Closure;
use Illuminate\Contracts\View\View;
use Illuminate\View\Component;
class CockpitImage extends Component
{
public ?string $src;
public function __construct(
public ?array $asset,
public ?int $width = null,
public ?int $height = null,
public int $quality = 80,
public ?string $alt = null,
public ?string $class = null,
) {
$this->src = $this->generateUrl();
}
public function render(): View|Closure|string
{
if (!$this->src) {
return '';
}
return view('components.cockpit-image');
}
protected function generateUrl(): ?string
{
if (!$this->asset || !isset($this->asset['_id'])) {
return null;
}
$params = [];
if ($this->width) $params['w'] = $this->width;
if ($this->height) $params['h'] = $this->height;
if ($this->quality) $params['q'] = $this->quality;
return app('cockpit')->getAssetUrl($this->asset['_id'], $params);
}
}
Create resources/views/components/cockpit-image.blade.php
:
<img
src="{{ $src }}"
alt="{{ $alt ?? $asset['title'] ?? '' }}"
@if($width) width="{{ $width }}" @endif
@if($height) height="{{ $height }}" @endif
@if($class) class="{{ $class }}" @endif
{{ $attributes }}
>
Content Models
Create model classes for your Cockpit content:
<?php
namespace App\Models\Cockpit;
use Illuminate\Support\Facades\Cockpit;
abstract class CockpitModel
{
protected static string $collection = '';
protected array $attributes = [];
public function __construct(array $attributes = [])
{
$this->attributes = $attributes;
}
public static function find(string $id): ?static
{
$data = Cockpit::getItem(static::$collection, $id);
return $data ? new static($data) : null;
}
public static function all(): \Illuminate\Support\Collection
{
return Cockpit::getItems(static::$collection)->map(fn($item) => new static($item));
}
public function __get($key)
{
return $this->attributes[$key] ?? null;
}
public function __set($key, $value)
{
$this->attributes[$key] = $value;
}
public function toArray(): array
{
return $this->attributes;
}
}
Create app/Models/Cockpit/Post.php
:
<?php
namespace App\Models\Cockpit;
use Carbon\Carbon;
class Post extends CockpitModel
{
protected static string $collection = 'posts';
public function getPublishedAtAttribute(): Carbon
{
return Carbon::createFromTimestamp($this->_created);
}
public function getImageUrlAttribute(): ?string
{
if (!$this->featured_image) {
return null;
}
return app('cockpit')->getAssetUrl($this->featured_image['_id'], [
'w' => 800,
'h' => 400,
'q' => 80
]);
}
public static function published(): \Illuminate\Support\Collection
{
return Cockpit::getItems(static::$collection, [
'filter' => ['published' => true]
])->map(fn($item) => new static($item));
}
public static function findBySlug(string $slug): ?static
{
$items = Cockpit::getItems(static::$collection, [
'filter' => [
'slug' => $slug,
'published' => true
],
'limit' => 1
]);
$post = $items->first();
return $post ? new static($post) : null;
}
}
Building a Blog Example
Routes
Update routes/web.php
:
<?php
use App\Http\Controllers\BlogController;
use App\Http\Controllers\PageController;
use Illuminate\Support\Facades\Route;
Route::get('/', [PageController::class, 'home'])->name('home');
Route::get('/blog', [BlogController::class, 'index'])->name('blog.index');
Route::get('/blog/{slug}', [BlogController::class, 'show'])->name('blog.show');
Route::get('/search', [BlogController::class, 'search'])->name('blog.search');
Controllers
Create app/Http/Controllers/BlogController.php
:
<?php
namespace App\Http\Controllers;
use App\Models\Cockpit\Post;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cockpit;
class BlogController extends Controller
{
public function index(Request $request)
{
$page = $request->get('page', 1);
$perPage = 12;
$skip = ($page - 1) * $perPage;
$posts = Cockpit::getItems('posts', [
'filter' => ['published' => true],
'sort' => ['_created' => -1],
'limit' => $perPage,
'skip' => $skip
])->map(fn($post) => new Post($post));
// For pagination, you'd need to get the total count
// This is a simplified version
$total = Cockpit::getItems('posts', [
'filter' => ['published' => true],
'fields' => ['_id' => 1]
])->count();
return view('blog.index', [
'posts' => $posts,
'pagination' => [
'total' => $total,
'per_page' => $perPage,
'current_page' => $page,
'last_page' => ceil($total / $perPage)
]
]);
}
public function show(string $slug)
{
$post = Post::findBySlug($slug);
if (!$post) {
abort(404);
}
return view('blog.show', compact('post'));
}
public function search(Request $request)
{
$query = $request->get('q');
if (!$query) {
return redirect()->route('blog.index');
}
$posts = Cockpit::getItems('posts', [
'filter' => [
'$and' => [
['published' => true],
['$or' => [
['title' => ['$regex' => $query, '$options' => 'i']],
['content' => ['$regex' => $query, '$options' => 'i']],
['excerpt' => ['$regex' => $query, '$options' => 'i']]
]]
]
],
'sort' => ['_created' => -1],
'limit' => 20
])->map(fn($post) => new Post($post));
return view('blog.search', compact('posts', 'query'));
}
}
Create app/Http/Controllers/PageController.php
:
<?php
namespace App\Http\Controllers;
use App\Models\Cockpit\Post;
use Illuminate\Support\Facades\Cockpit;
class PageController extends Controller
{
public function home()
{
$settings = Cockpit::getSingleton('site_settings');
$posts = Cockpit::getItems('posts', [
'filter' => ['published' => true],
'sort' => ['_created' => -1],
'limit' => 6
])->map(fn($post) => new Post($post));
return view('home', compact('settings', 'posts'));
}
}
Views
Create resources/views/layouts/app.blade.php
:
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>@yield('title', config('app.name'))</title>
@vite(['resources/css/app.css', 'resources/js/app.js'])
</head>
<body>
<nav class="bg-white shadow-sm">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="flex justify-between h-16">
<div class="flex">
<a href="{{ route('home') }}" class="flex items-center">
{{ config('app.name') }}
</a>
</div>
<div class="flex items-center space-x-4">
<a href="{{ route('blog.index') }}">Blog</a>
<form action="{{ route('blog.search') }}" method="GET" class="flex">
<input type="search" name="q" placeholder="Search..."
value="{{ request('q') }}"
class="px-3 py-1 border rounded-l">
<button type="submit" class="px-4 py-1 bg-blue-500 text-white rounded-r">
Search
</button>
</form>
</div>
</div>
</div>
</nav>
<main class="py-8">
@yield('content')
</main>
</body>
</html>
Create resources/views/blog/index.blade.php
:
@extends('layouts.app')
@section('title', 'Blog')
@section('content')
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<h1 class="text-3xl font-bold mb-8">Blog</h1>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
@foreach($posts as $post)
<article class="bg-white rounded-lg shadow overflow-hidden">
@if($post->featured_image)
<x-cockpit-image
:asset="$post->featured_image"
:width="400"
:height="200"
class="w-full h-48 object-cover"
/>
@endif
<div class="p-6">
<h2 class="text-xl font-semibold mb-2">
<a href="{{ route('blog.show', $post->slug) }}"
class="hover:text-blue-600">
{{ $post->title }}
</a>
</h2>
<p class="text-gray-600 mb-4">{{ $post->excerpt }}</p>
<div class="text-sm text-gray-500">
<span>By {{ $post->author }}</span>
<time class="ml-2">{{ $post->published_at->format('M d, Y') }}</time>
</div>
</div>
</article>
@endforeach
</div>
{{-- Simple pagination --}}
@if($pagination['last_page'] > 1)
<div class="mt-8 flex justify-center space-x-2">
@for($i = 1; $i <= $pagination['last_page']; $i++)
<a href="?page={{ $i }}"
class="px-3 py-1 {{ $i == $pagination['current_page'] ? 'bg-blue-500 text-white' : 'bg-gray-200' }} rounded">
{{ $i }}
</a>
@endfor
</div>
@endif
</div>
@endsection
Create resources/views/blog/show.blade.php
:
@extends('layouts.app')
@section('title', $post->title)
@section('content')
<article class="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8">
<header class="mb-8">
<h1 class="text-4xl font-bold mb-4">{{ $post->title }}</h1>
@if($post->featured_image)
<x-cockpit-image
:asset="$post->featured_image"
:width="800"
:height="400"
class="w-full rounded-lg mb-6"
/>
@endif
<div class="text-gray-600">
<span>By {{ $post->author }}</span>
<time class="ml-4">{{ $post->published_at->format('F d, Y') }}</time>
</div>
</header>
<div class="prose prose-lg max-w-none">
{!! $post->content !!}
</div>
@if($post->tags)
<footer class="mt-8 pt-8 border-t">
<div class="flex flex-wrap gap-2">
@foreach($post->tags as $tag)
<span class="px-3 py-1 bg-gray-100 text-gray-700 rounded-full text-sm">
{{ $tag }}
</span>
@endforeach
</div>
</footer>
@endif
</article>
@endsection
Artisan Commands
Create custom commands for content management:
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Cockpit;
class CockpitCacheClear extends Command
{
protected $signature = 'cockpit:cache-clear {type?} {name?}';
protected $description = 'Clear Cockpit cache';
public function handle(): int
{
$type = $this->argument('type');
$name = $this->argument('name');
Cockpit::clearCache($type, $name);
$this->info('Cockpit cache cleared successfully.');
return Command::SUCCESS;
}
}
<?php
namespace App\Console\Commands;
use App\Models\Cockpit\Post;
use Illuminate\Console\Command;
class CockpitImportPosts extends Command
{
protected $signature = 'cockpit:import-posts {--published}';
protected $description = 'Import posts from Cockpit CMS';
public function handle(): int
{
$filter = [];
if ($this->option('published')) {
$filter['published'] = true;
}
$posts = Cockpit::getItems('posts', [
'filter' => $filter
])->map(fn($post) => new Post($post));
$this->info("Found {$posts->count()} posts");
// Process posts as needed
foreach ($posts as $post) {
$this->line("- {$post->title}");
}
return Command::SUCCESS;
}
}
API Development
Create API endpoints that serve Cockpit content:
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\Cockpit\Post;
use Illuminate\Http\Request;
class PostApiController extends Controller
{
public function index(Request $request)
{
$page = $request->get('page', 1);
$perPage = $request->get('per_page', 10);
$skip = ($page - 1) * $perPage;
$posts = Cockpit::getItems('posts', [
'filter' => ['published' => true],
'sort' => ['_created' => -1],
'limit' => $perPage,
'skip' => $skip
]);
$total = Cockpit::getItems('posts', [
'filter' => ['published' => true],
'fields' => ['_id' => 1]
])->count();
return response()->json([
'data' => $posts->map(fn($post) => [
'id' => $post['_id'],
'title' => $post['title'],
'slug' => $post['slug'],
'excerpt' => $post['excerpt'],
'author' => $post['author'],
'published_at' => $post['_created'],
'image_url' => $post['featured_image']
? app('cockpit')->getAssetUrl($post['featured_image']['_id'])
: null,
]),
'meta' => [
'total' => $total,
'per_page' => $perPage,
'current_page' => $page,
'last_page' => ceil($total / $perPage),
]
]);
}
public function show(string $slug)
{
$post = Post::findBySlug($slug);
if (!$post) {
return response()->json(['message' => 'Post not found'], 404);
}
return response()->json([
'data' => [
'id' => $post->_id,
'title' => $post->title,
'slug' => $post->slug,
'content' => $post->content,
'excerpt' => $post->excerpt,
'author' => $post->author,
'tags' => $post->tags,
'published_at' => $post->_created,
'image_url' => $post->image_url,
]
]);
}
}
Caching Strategies
Cache Warming
Create a scheduled job to warm the cache:
<?php
namespace App\Jobs;
use App\Models\Cockpit\Post;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
class WarmCockpitCache implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public function handle(): void
{
// Warm posts cache
Cockpit::getItems('posts', [
'filter' => ['published' => true],
'sort' => ['_created' => -1],
'limit' => 100
]);
// Warm individual post cache
$recentPosts = Cockpit::getItems('posts', [
'filter' => ['published' => true],
'sort' => ['_created' => -1],
'limit' => 20,
'fields' => ['_id' => 1, 'slug' => 1]
]);
foreach ($recentPosts as $post) {
Post::find($post['_id']);
}
}
}
Schedule in app/Console/Kernel.php
:
protected function schedule(Schedule $schedule): void
{
$schedule->job(new WarmCockpitCache)->hourly();
}
Testing
Create tests for your Cockpit integration:
<?php
namespace Tests\Feature;
use App\Services\CockpitService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Http;
use Tests\TestCase;
class CockpitServiceTest extends TestCase
{
protected function setUp(): void
{
parent::setUp();
// Mock Cockpit API responses
Http::fake([
'*/api/content/items/posts*' => Http::response([
['_id' => '1', 'title' => 'Test Post', 'slug' => 'test-post'],
]),
'*/api/content/item/posts/1' => Http::response([
'_id' => '1',
'title' => 'Test Post',
'slug' => 'test-post',
'content' => 'Test content',
]),
]);
}
public function test_can_fetch_collection_items()
{
$service = app(CockpitService::class);
$items = $service->getItems('posts');
$this->assertCount(1, $items);
$this->assertEquals('Test Post', $items[0]['title']);
}
public function test_can_fetch_single_item()
{
$service = app(CockpitService::class);
$item = $service->getItem('posts', '1');
$this->assertNotNull($item);
$this->assertEquals('Test Post', $item['title']);
}
public function test_filter_options_work()
{
$service = app(CockpitService::class);
$items = $service->getItems('posts', [
'filter' => ['published' => true],
'sort' => ['_created' => -1],
'limit' => 10
]);
$this->assertInstanceOf(\Illuminate\Support\Collection::class, $items);
}
}
Best Practices
1. Error Handling
Always handle API failures gracefully:
try {
$posts = Post::published()->get();
} catch (\Exception $e) {
Log::error('Failed to fetch posts from Cockpit', [
'error' => $e->getMessage()
]);
// Return cached or default data
$posts = collect();
}
2. Performance Optimization
Use field selection and caching:
// Only fetch required fields
$posts = Cockpit::getItems('posts', [
'fields' => ['_id' => 1, 'title' => 1, 'slug' => 1, 'excerpt' => 1, '_created' => 1],
'limit' => 10
]);
// Cache expensive queries
$popularPosts = Cache::remember('popular_posts', 3600, function () {
return Cockpit::getItems('posts', [
'filter' => [
'published' => true,
'views' => ['$gt' => 1000]
],
'sort' => ['views' => -1],
'limit' => 5
]);
});
3. Configuration Management
Use config files for all Cockpit settings:
// config/cockpit.php
return [
'collections' => [
'posts' => [
'cache_ttl' => 3600,
'per_page' => 12,
],
'pages' => [
'cache_ttl' => 7200,
'per_page' => 20,
],
],
];
Deployment
Environment Variables
Set these in your production .env
:
COCKPIT_API_URL=https://your-cockpit-instance.com/api
COCKPIT_API_KEY=your-production-api-key
COCKPIT_CACHE_TTL=7200
Cache Configuration
Optimize cache for production:
// Use Redis for better performance
CACHE_DRIVER=redis
REDIS_HOST=127.0.0.1
REDIS_PASSWORD=null
REDIS_PORT=6379
Troubleshooting
Common Issues
-
API Connection Errors
- Verify API URL and key in
.env
- Check network connectivity
- Ensure Cockpit instance is accessible
- Verify API URL and key in
-
Cache Issues
- Clear Laravel cache:
php artisan cache:clear
- Clear Cockpit cache:
php artisan cockpit:cache-clear
- Clear Laravel cache:
-
Performance Issues
- Enable query caching
- Use field selection to reduce payload
- Implement pagination for large datasets
This integration provides a robust foundation for using Cockpit CMS with Laravel, leveraging Laravel's powerful features while maintaining the flexibility of a headless CMS.