Speeding Up Laravel Tests with tmpfs (MySQL in RAM)
If your Laravel test suite is painfully slow and does a lot of database work (multi-tenancy, migrations, seeding), the bottleneck is almost…
This guide walks you through creating a Statamic addon from scratch. We'll build a real addon that adds functionality to your Statamic installation.
By the end of this guide, you'll have:
Use Statamic's built-in command to scaffold your addon:
php please make:addon vendor/my-addon
This creates the basic structure in your addons directory:
addons/
vendor/
my-addon/
src/
ServiceProvider.php
resources/
tests/
composer.json
README.md
Your addon's composer.json is crucial. Here's what Statamic needs:
{
"name": "vendor/my-addon",
"description": "My amazing Statamic addon",
"type": "statamic-addon",
"autoload": {
"psr-4": {
"Vendor\\MyAddon\\": "src"
}
},
"authors": [
{
"name": "Your Name",
"email": "you@example.com"
}
],
"extra": {
"statamic": {
"name": "My Addon",
"description": "My amazing Statamic addon"
},
"laravel": {
"providers": [
"Vendor\\MyAddon\\ServiceProvider"
]
}
},
"require": {
"statamic/cms": "^5.0"
}
}
The extra.statamic section tells Statamic this is an addon. The extra.laravel.providers section registers your service provider with Laravel.
Your service provider is the heart of your addon. It must extend Statamic's AddonServiceProvider:
<?php
namespace Vendor\MyAddon;
use Statamic\Providers\AddonServiceProvider;
class ServiceProvider extends AddonServiceProvider
{
protected $vite = [
'input' => [
'resources/js/cp.js',
'resources/css/cp.css'
],
'publicDirectory' => 'resources/dist',
];
protected $routes = [
'cp' => __DIR__.'/../routes/cp.php',
'web' => __DIR__.'/../routes/web.php',
];
public function bootAddon()
{
// Boot logic here
}
}
Important: Use bootAddon() instead of boot(). This ensures your addon boots after Statamic.
Let's create a custom fieldtype. Run:
php please make:fieldtype ColorPicker --addon=vendor/my-addon
This generates:
Your fieldtype class:
<?php
namespace Vendor\MyAddon\Fieldtypes;
use Statamic\Fields\Fieldtype;
class ColorPicker extends Fieldtype
{
protected $icon = 'color-picker';
protected static $title = 'Color Picker';
public function component(): string
{
return 'color-picker';
}
public function configFieldItems(): array
{
return [
'default' => [
'display' => 'Default Color',
'type' => 'text',
'default' => '#000000',
'width' => 50,
],
'format' => [
'display' => 'Color Format',
'type' => 'select',
'default' => 'hex',
'options' => [
'hex' => 'Hex',
'rgb' => 'RGB',
'hsl' => 'HSL',
],
'width' => 50,
],
];
}
}
Your addon needs these files for asset compilation:
{
"private": true,
"scripts": {
"dev": "vite",
"build": "vite build"
},
"devDependencies": {
"@vitejs/plugin-vue2": "^2.3.1",
"laravel-vite-plugin": "^1.0",
"vite": "^5.0"
}
}
import { defineConfig } from 'vite';
import laravel from 'laravel-vite-plugin';
import vue from '@vitejs/plugin-vue2';
export default defineConfig({
plugins: [
laravel({
input: [
'resources/js/cp.js',
'resources/css/cp.css'
],
publicDirectory: 'resources/dist',
}),
vue(),
],
});
import ColorPicker from './components/fieldtypes/ColorPicker.vue';
Statamic.booting(() => {
Statamic.component('color-picker', ColorPicker);
});
Create routes/cp.php:
<?php
use Illuminate\Support\Facades\Route;
use Vendor\MyAddon\Http\Controllers\CP\DashboardController;
Route::prefix('my-addon')->name('my-addon.')->group(function () {
Route::get('/', [DashboardController::class, 'index'])->name('index');
Route::get('/api/data', [DashboardController::class, 'data'])->name('data');
Route::post('/api/save', [DashboardController::class, 'save'])->name('save');
});
Create your controller:
<?php
namespace Vendor\MyAddon\Http\Controllers\CP;
use Statamic\Http\Controllers\CP\CpController;
use Illuminate\Http\Request;
class DashboardController extends CpController
{
public function index()
{
return view('my-addon::dashboard');
}
public function data()
{
return response()->json([
'success' => true,
'data' => [
'stats' => $this->getStats(),
]
]);
}
public function save(Request $request)
{
$request->validate([
'setting' => 'required|string',
]);
// Save logic here
return response()->json([
'success' => true,
'message' => 'Settings saved successfully'
]);
}
private function getStats()
{
return [
'total' => 42,
'active' => 30,
];
}
}
Add your addon to the Control Panel navigation in your service provider:
use Statamic\Facades\CP\Nav;
public function bootAddon()
{
Nav::extend(function ($nav) {
$nav->content('My Addon')
->section('Tools')
->route('my-addon.index')
->icon('addons')
->active('my-addon.*');
});
}
As of Statamic 5.28+, many components are autoloaded. If you're supporting older versions or want explicit control, register them manually:
protected $tags = [
\Vendor\MyAddon\Tags\MyTag::class,
];
protected $fieldtypes = [
\Vendor\MyAddon\Fieldtypes\ColorPicker::class,
];
protected $modifiers = [
\Vendor\MyAddon\Modifiers\MyModifier::class,
];
protected $widgets = [
\Vendor\MyAddon\Widgets\MyWidget::class,
];
During development, run:
cd addons/vendor/my-addon
npm install
npm run dev
For production builds:
npm run build
Assets are automatically published when users run composer update.
Create a config file that users can publish:
// In your service provider
protected $publishables = [
__DIR__.'/../config/my-addon.php' => 'config/my-addon.php',
];
Create config/my-addon.php:
<?php
return [
'api_key' => env('MY_ADDON_API_KEY'),
'cache_ttl' => 3600,
'features' => [
'advanced_mode' => false,
],
];
Users can publish and customize:
php artisan vendor:publish --tag=my-addon
Your addon comes with PHPUnit configured. Write tests in tests/:
<?php
namespace Vendor\MyAddon\Tests;
class ColorPickerTest extends TestCase
{
/** @test */
public function it_saves_color_value()
{
$field = new \Vendor\MyAddon\Fieldtypes\ColorPicker();
$field->setField(new \Statamic\Fields\Field('color', [
'type' => 'color_picker',
'default' => '#ff0000',
]));
$processed = $field->process('#00ff00');
$this->assertEquals('#00ff00', $processed);
}
}
Run tests:
./vendor/bin/phpunit
Your complete addon structure:
my-addon/
├── config/
│ └── my-addon.php
├── resources/
│ ├── dist/
│ ├── js/
│ │ ├── components/
│ │ │ └── fieldtypes/
│ │ │ └── ColorPicker.vue
│ │ └── cp.js
│ ├── css/
│ │ └── cp.css
│ └── views/
│ └── dashboard.blade.php
├── routes/
│ ├── cp.php
│ └── web.php
├── src/
│ ├── Fieldtypes/
│ │ └── ColorPicker.php
│ ├── Http/
│ │ └── Controllers/
│ │ └── CP/
│ │ └── DashboardController.php
│ └── ServiceProvider.php
├── tests/
│ ├── ColorPickerTest.php
│ └── TestCase.php
├── composer.json
├── package.json
├── phpunit.xml
└── vite.config.js
Run npm install && npm run dev in your addon directory.
Check your PSR-4 namespace in composer.json and run composer dump-autoload.
Ensure your $vite property matches your vite.config.js.
Verify your routes are in the $routes property and files exist.
When ready to share:
Remember: First edition should be the most limited (free tier), with additional editions offering more features.
Give Vroni a GitHub issue, bug report, spec, or rough idea. It reads the repo, plans the change, writes code, runs checks, and works toward a review-ready pull request.
Take a look at vroni.com