Custom October CMS Plugin Development
Imagine: a ready-made plugin from the marketplace doesn't offer the flexibility you need — the form doesn't collect custom fields, filtering by related models fails, or the API integration hangs. The solution is to develop a plugin tailored to your needs. Over 5 years, we have shipped more than 50 custom solutions for October CMS, and this experience allows us to build them quickly, without surprises.
How a Custom Plugin Solves Business Tasks?
A plugin in October CMS is a full Laravel package that registers components, models, routes, and backend controllers. It can replace an entire microservice: manage reviews, integrate with 1C or CRM, output custom filters. Unlike modifications via copy‑paste in the theme, a plugin is easy to update, doesn't break the core, and is reusable across projects.
Why a Bespoke Extension Beats Modifying an Existing One?
Tweaking someone else's code means risking stability. Every time the plugin updates, your modifications get overwritten. A custom plugin is written from scratch for your architecture: we control every table, every event. A bespoke extension is 3x faster to update and 2x more reliable than modifying an existing one. Compare: modifying an existing plugin takes 2–3 days, but then another week to catch bugs. A new plugin — 5 days and done, with a warranty and documentation.
What Problems Do We Solve?
Most often, clients face missing fields in forms, suboptimal SQL queries (N+1), and the inability to flexibly configure access rights. We address these issues at the design stage: use eager loading, caching via Cache::remember, and custom validators.
Plugin Development Process
Our Development Process
- Analysis — we receive your specifications or define requirements together. We pin down models, relationships, components, access rights.
- Design — we create an ER diagram, component scheme, backend navigation.
- Implementation — we write migrations, models, components, controllers, and events. Code is covered with tests (Pest/PHPUnit, 95% coverage).
- Integration — we connect the plugin to your site, configure caching, optimize queries.
- Deployment and Documentation — we deliver an archive with composer.json, README, configs, plus training for your team.
What Is Included in the Extension
- Source code under MIT license (or yours)
- composer.json with autoloading
- Migrations and seed data
- Backend controllers and navigation menu
- Frontend components (Twig/Partial)
- Events and hooks for extensibility
- Tests (if needed)
- README with installation and usage instructions
- 30 days of support after delivery
Typical Mistakes and How to Prevent Them
Common Mistakes
- N+1 queries in components — forgetting
with()andload(). We use eager loading and caching viaCache::remember. - Hard‑coded IDs in navigation — breaks on migration to another server. We build navigation automatically via
Backend::url(). - Missing validation — users input anything. We use the
Validationtrait with rules. - Ignoring events — not subscribing to
cms.page.beforeDisplay, so customizations don't fire. We always use the event model.
Real‑world example: CRM integration in 2 days. One project required syncing orders with AmoCRM. The problem was that standard plugins didn't support custom deal fields. We developed a plugin with a component that sent data via REST API when an order was created. The whole job took 2 days, tests covered 95% of the code.
Comparison: Custom Plugin vs. Modifying Existing One
| Criteria | Custom Plugin | Modifying Existing Plugin |
|---|---|---|
| Compatibility with updates | Full — code doesn't overlap | Each update requires reapplying patches |
| Development time | 3–14 days turnkey | 2–3 days for a patch, but risk of bugs |
| Flexibility | Any logic, relationships, events | Limited by plugin's structure |
| Documentation | README, code comments, training | Often absent |
| Warranty | 30 days for fixes | Depends on the author |
Code Example
Below is a minimal plugin skeleton. A complete set of files is shown using a review model as an example.
Plugin.php — Registration of components and navigation
// Plugin.php
namespace MyCompany\MySite;
use Backend;
use System\Classes\PluginBase;
class Plugin extends PluginBase
{
public function pluginDetails(): array
{
return [
'name' => 'My Site',
'description' => 'Site-specific functionality',
'author' => 'My Company',
'icon' => 'icon-leaf',
];
}
public function registerComponents(): array
{
return [
\MyCompany\MySite\Components\ReviewList::class => 'reviewList',
\MyCompany\MySite\Components\ReviewForm::class => 'reviewForm',
];
}
public function registerNavigation(): array
{
return [
'mysite' => [
'label' => 'My Site',
'url' => Backend::url('mycompany/mysite/reviews'),
'icon' => 'icon-star',
'permissions' => ['mycompany.mysite.*'],
'order' => 500,
'sideMenu' => [
'reviews' => [
'label' => 'Reviews',
'icon' => 'icon-comments',
'url' => Backend::url('mycompany/mysite/reviews'),
'permissions' => ['mycompany.mysite.reviews'],
],
],
],
];
}
public function registerSettings(): array
{
return [
'settings' => [
'label' => 'My Site Settings',
'description' => 'Configure My Site plugin',
'icon' => 'icon-cog',
'class' => \MyCompany\MySite\Models\Settings::class,
'order' => 500,
],
];
}
public function boot(): void
{
\Event::listen('cms.page.beforeDisplay', function ($controller, $url, $page) {
// Logic before page rendering
});
}
}
Eloquent Model
// models/Review.php
namespace MyCompany\MySite\Models;
use Model;
class Review extends Model
{
use \October\Rain\Database\Traits\Validation;
use \October\Rain\Database\Traits\SoftDelete;
public $table = 'mycompany_mysite_reviews';
public $rules = [
'author_name' => 'required|string|max:255',
'email' => 'required|email',
'rating' => 'required|integer|between:1,5',
'body' => 'required|string|min:10',
];
protected $fillable = ['author_name', 'email', 'rating', 'body', 'is_approved'];
protected $casts = [
'is_approved' => 'boolean',
'rating' => 'integer',
];
public $attachOne = [
'avatar' => \System\Models\File::class,
];
public $belongsTo = [
'product' => [\MyCompany\MySite\Models\Product::class],
];
public function scopeApproved($query)
{
return $query->where('is_approved', true);
}
public function scopeByProduct($query, int $productId)
{
return $query->where('product_id', $productId);
}
}
Component
// components/ReviewList.php
namespace MyCompany\MySite\Components;
use Cms\Classes\ComponentBase;
use MyCompany\MySite\Models\Review;
class ReviewList extends ComponentBase
{
public function componentDetails(): array
{
return [
'name' => 'Review List',
'description' => 'Displays product reviews',
];
}
public function defineProperties(): array
{
return [
'productId' => ['title' => 'Product ID', 'type' => 'string'],
'limit' => ['title' => 'Limit', 'type' => 'string', 'default' => '10'],
];
}
public function onRun(): void
{
$this->page['reviews'] = Review::approved()
->byProduct((int) $this->property('productId'))
->with('avatar')
->orderBy('created_at', 'desc')
->limit((int) $this->property('limit'))
->get();
$this->page['avgRating'] = Review::approved()
->byProduct((int) $this->property('productId'))
->avg('rating');
}
public function onSubmitReview(): array
{
$data = post();
$review = new Review($data);
$review->product_id = $this->property('productId');
if (!$review->save()) {
throw new \ValidationException($review);
}
return ['success' => true];
}
}
Migration
// updates/1_0_1_create_reviews_table.php
use October\Rain\Database\Schema\Blueprint;
use October\Rain\Database\Updates\Migration;
class CreateReviewsTable extends Migration
{
public function up(): void
{
Schema::create('mycompany_mysite_reviews', function (Blueprint $table) {
$table->increments('id');
$table->integer('product_id')->unsigned()->index();
$table->string('author_name');
$table->string('email');
$table->tinyInteger('rating');
$table->text('body');
$table->boolean('is_approved')->default(false);
$table->timestamps();
$table->softDeletes();
});
}
public function down(): void
{
Schema::dropIfExists('mycompany_mysite_reviews');
}
}
Timeline and Stages
| Stage | Duration | Result |
|---|---|---|
| Requirements analysis | 1–2 days | Spec, ER diagram |
| Design | 1–2 days | Component scheme, navigation |
| Implementation | 3–7 days | Code, tests, documentation |
| Integration | 1–2 days | Working plugin on your site |
| Deployment and handover | 1 day | composer.json, README, configs |
What's Included in the Work
- Detailed requirements document and ER diagram
- Fully functional extension with source code (MIT license)
- composer.json and autoloading configuration
- Database migrations and seed data
- Backend controllers and navigation menu
- Frontend components (Twig/Partial)
- Events and hooks for extensibility
- Comprehensive tests (Pest/PHPUnit)
- README with installation and usage instructions
- 30 days of support and bug fixes after delivery
- Optional: team training and maintenance contract
How to Order Development?
Describe your task — we'll prepare an estimate within 1 day. Contact us, and we'll help you determine which approach best fits your project. Prices start at $500 for a simple extension, and typical projects range from $500 to $2,000.
More details about plugin development can be found in the official October CMS documentation.
5+ years of experience and 50+ delivered plugins are not just numbers. Every project starts with an architectural review: we avoid common mistakes (missing database indexes, unoptimized queries, code duplication). All plugins undergo internal code review. We provide a 30‑day warranty on code — if something goes wrong, we fix it free of charge.







