ibrows/simplecms-bundle
iBrows SimpleCMS Bundle is a Symfony bundle for adding lightweight content management to your application, with simple page and content handling so editors can manage site text without a full CMS.
Before you write a single line, internalize the headline finding: ibrows/simplecms-bundle is a Symfony bundle, not a Laravel package. There is no service provider, no illuminate/* dependency, no publishable config, no migrations. It targets the Symfony 2-era bundle model (IbrowsSimpleCMSBundle class, DependencyInjection/IbrowsSimpleCMSExtension.php, Resources/config/services.yml, Twig templates, Doctrine entities, and Sonata Admin wiring). You cannot composer require it into a Laravel app and get anything usable — expect dependency resolution to fail on modern PHP, and even if it resolved, there is no integration seam.
So the realistic "first use case" for a Laravel developer is reference, not adoption. Read the source to extract the domain model, then build it natively.
Where to look first, in this order:
composer.json — confirm it's Symfony 2.x era, note PSR-0 autoloading and pinned dependencies. This alone tells you whether a spike is worth 30 minutes.Entity/ (or Model/) — typically a Page (slug, title, route, template) and a ContentElement/Block concept (region/position, type, body, page or global scope). This is the reusable asset.Resources/config/routing.xml and the controller(s) — how pages are resolved by slug and rendered.The native Laravel translation of the domain model is small. A page + blocks schema lands in roughly this shape:
// database/migrations/xxxx_create_pages_table.php
Schema::create('pages', function (Blueprint $table) {
$table->id();
$table->string('slug')->unique();
$table->string('title');
$table->string('template')->default('default');
$table->json('meta')->nullable(); // SEO fields
$table->timestamp('published_at')->nullable();
$table->timestamps();
});
Schema::create('blocks', function (Blueprint $table) {
$table->id();
$table->morphs('blockable'); // page-scoped or global
$table->string('region')->default('main');
$table->string('type'); // text, html, image, faq
$table->longText('body')->nullable();
$table->unsignedInteger('position')->default(0);
$table->timestamps();
});
That is the entire "getting started" worth doing with this repository. If your intent is adopt, stop — the license (NOASSERTION) and the zero-maintenance reality are blocking before any code matters.
Pattern 1 — Reference extraction (recommended). Clone the repo, read the entities and admin classes, write down the feature list (page CRUD, slug routing, region-scoped blocks, block types, template selection), then discard the code. You avoid the license/derivative-work ambiguity and get the schema in an afternoon. Do not copy code verbatim; re-express the model in Eloquent/Blade.
Pattern 2 — Polymorphic block rendering. The bundle's core idea is that content is data attached to a page or a region, not hardcoded in templates. In Laravel:
// App\Models\Page.php
public function blocks(): MorphMany
{
return $this->morphMany(Block::class, 'blockable')->orderBy('position');
}
public function renderBlocks(string $region = 'main'): HtmlString
{
return new HtmlString(
$this->blocks->where('region', $region)
->map(fn (Block $b) => view("blocks.{$b->type}", ['block' => $b])->render())
->implode('')
);
}
{{-- resources/views/pages/show.blade.php --}}
@extends("templates.{$page->template}")
@section('content')
{!! $page->renderBlocks('main') !!}
@endsection
Pattern 3 — Block type registry. A small type→view map, mirroring how the bundle associates a content element type with a template, keeps editors constrained and rendering deterministic:
// config/cms.php
return [
'blocks' => [
'text' => 'blocks.text',
'html' => 'blocks.html',
'image' => 'blocks.image',
'faq' => 'blocks.faq',
],
'templates' => ['default', 'landing', 'legal'],
];
Pattern 4 — Slug routing with caching. The bundle resolves pages by slug through a controller. Native equivalent: a catch-all route that looks up a published page and caches the resolution.
Route::get('/{slug}', PageController::class)
->where('slug', '[a-z0-9\-\/]+')
->name('cms.page');
public function __invoke(string $slug)
{
$page = Cache::remember("page:{$slug}", 3600, fn () =>
Page::with('blocks')->where('slug', $slug)
->whereNotNull('published_at')->firstOrFail()
);
return view('pages.show', compact('page'));
}
Pattern 5 — Admin surface. The bundle uses Sonata Admin; in Laravel use Filament or Nova to get the same "list pages, edit blocks" experience in a fraction of the code. A Filament PageResource with a Repeater or relation manager for blocks covers the whole editorial flow.
Pattern 6 — Sidecar (last resort). If for some reason the Symfony app must ship, run the bundle in its own Symfony 2 container and expose content over HTTP/DB to Laravel. This works but inherits every operational and CVE liability. Only defensible if content is read-mostly and you need to ship this quarter.
Pattern 7 — Genuinely on Symfony? Then the normal flow applies: register the bundle in app/AppKernel.php (or config/bundles.php), import @IbrowsSimpleCMSBundle/Resources/config/routing.xml, ensure Doctrine picks up its mapping, and render pages via its controller. That path is out of scope for a Laravel codebase, but it's why the package exists.
NOASSERTION means GitHub could not identify a recognized license, which by default means all rights reserved. A NOASSERTION badge is not permission. Commercial use is a legal question, not an engineering one — get clearance before you even copy snippets, since reading a repo to extract a model carries its own (lower) derivative-work questions.@ORM/@Route annotation readers, Symfony Form type APIs that don't exist in Symfony 5/6/7, and Twig 1.x syntax. Test a throwaway Composer resolve in a disposable container before spending any spike time, but treat failure as the expected outcome.Page::with('blocks')) and cache the resolved page + block payload rather than re-rendering each request. A fragment/full-page cache keyed on page:slug is cheap insurance./{slug} will swallow other routes if registered too early. Register it last, constrain it with a regex, and return 404 for unknown slugs rather than falling through to a controller that assumes a page exists.view("blocks.{$type}") failure. Validate against config('cms.blocks') on save and on render so the failure is a missing type, not an exception from view().How can I help you explore Laravel packages today?