stfalcon/news-bundle
StfalconNewsBundle is a Symfony bundle that adds a lightweight news section to an existing application. It provides Doctrine-backed news entities, CRUD-style management, slugs, pagination, and Twig templates, with configurable routes for quick integration.
Reframe before you type a command. This is a Symfony 2 bundle. composer require stfalcon/news-bundle will not resolve against a Laravel app, and the bundle's runtime (Symfony kernel bundle registration, Doctrine annotations, Twig, SonataAdmin) has no Laravel equivalent. The day-to-day value is as a reference spec you port in an afternoon, not a dependency. Do not add it to composer.json.
Read it in five minutes without installing anything. Clone the repo and open four files in this order:
Entity/News.php and Entity/Category.php — the field set: title, slug, content/body, image, publish flag, publish date. This is the schema you will recreate in a migration.Admin/NewsAdmin.php — the SonataAdmin configureListFields / configureFormFields calls tell you exactly which fields editors need grouped together and which are list-column-worthy. That is your Filament/Nova form map.Resources/config/routing.yml — the listing + detail route pair (/news, /news/{slug}) and how the slug is bound.Resources/config/services.yml — only needed if you must work inside the legacy Symfony app itself.First use case — port a working news surface in one sitting:
php artisan make:model News -m
php artisan make:filament-resource News
// migration
Schema::create('news', function (Blueprint $table) {
$table->id();
$table->string('title');
$table->string('slug')->unique();
$table->text('excerpt')->nullable();
$table->longText('body');
$table->string('image')->nullable();
$table->boolean('published')->default(false);
$table->timestamp('published_at')->nullable();
$table->timestamps();
});
// app/Models/News.php
protected $casts = ['published' => 'boolean', 'published_at' => 'datetime'];
public function scopeLive($q) {
return $q->where('published', true)->where('published_at', '<=', now());
}
public function getRouteKeyName() { return 'slug'; }
// routes/web.php
Route::get('/news', [NewsController::class, 'index'])->name('news.index');
Route::get('/news/{news}', [NewsController::class, 'show'])->name('news.show');
Then seed three rows, hit /news, and you have validated the whole pattern. The rest of this document is about what to copy and what to deliberately change.
Port the schema faithfully, then extend it. The bundle's model is deliberately flat. Recreate it as-is first so you know you have parity, then add what it lacks: author_id, news_category_id (or a polymorphic taggables join), published_at as your scheduling mechanism, and a status enum once published boolean stops being enough.
Publishing workflow — treat it as the core pattern. The bundle's published flag plus a date is the seed of a real draft/publish/schedule flow. Scope every public query through one place so you can never leak a draft:
// NewsController@index
$news = News::live()->latest('published_at')->paginate(10);
// NewsController@show — 404 on drafts, for free, because of route model binding + the scope check
public function show(News $news) {
abort_unless($news->published && $news->published_at?->isPast(), 404);
return view('news.show', compact('news'));
}
Put that guard in a NewsPolicy rather than inline once you have more than one entry point (admin preview, RSS, API).
Admin resource mirrors the SonataAdmin config. The Filament resource is a near-literal translation of NewsAdmin.php: form fields become TextInput/RichEditor/FileUpload, list columns become TextColumn::make('title')->searchable()->sortable(), and the published boolean becomes a Toggle with a ->afterStateUpdated() hook to set published_at. This is where the bundle earns its keep — the field ordering and list-column choices encode someone's actual editorial experience.
Slug handling — copy the intent, not the implementation. The bundle assumes slugs are set. In Laravel, generate them but keep them editable and never regenerate on update (URLs are contracts):
protected static function booted() {
static::creating(fn ($n) => $n->slug ??= Str::slug($n->title));
}
Then add a redirect/404 monitor — the bundle has no handling for renamed slugs, and that gap is the number one source of SEO damage in a ported news module.
Templates: translate, don't port. The Twig Resources/views/ files give you the information architecture (title, date, hero image, body, back-link). Rebuild them in Blade with your own design system. Do not attempt to reproduce Twig markup — the bundle predates modern semantic HTML conventions and the output will fight your CSS.
Integration points to wire on day one (the bundle doesn't, and you'll want them): a NewsObserver to bust a cache or regenerate a sitemap on save; an RSS/Atom endpoint or feed package; a sitemap entry that includes only live() items; and OG/meta tags in the show view. These are one-liners in Laravel and absent in the bundle — plan for them explicitly or they get deferred forever.
Testing pattern. The scope is the thing worth testing, because everything else is CRUD:
test('drafts are not publicly visible', function () {
$draft = News::factory()->create(['published' => false]);
$this->get("/news/{$draft->slug}")->assertNotFound();
});
test('scheduled posts are hidden until publish time', function () {
$post = News::factory()->create(['published' => true, 'published_at' => now()->addDay()]);
$this->get("/news/{$post->slug}")->assertNotFound();
});
Do not install it. The single most expensive mistake here is a spike that tries to composer require this into a Laravel app, spends a day fighting resolution, and concludes the feature is hard. The feature is not hard; the package is just the wrong artifact. Set a hard 30-minute box on reading the source, then stop.
Symfony version trap. If a colleague asks you to evaluate it for a Symfony app, check the major version first. This targets Symfony 2, which went EOL in 2019 with no security patches. On Symfony 4+ the bundle structure and SonataAdmin versions have moved far enough that it is effectively unrunnable. Same conclusion either way: reference only.
The README lies by omission. The real configuration surface lives in DependencyInjection/Configuration.php, not in the docs. Class-override keys (the bundle expects you to subclass its entities and point config at yours) are the pattern to notice — it is a good reminder that in your Laravel version, never edit a vendor/framework model; extend or override via config/registration. Same discipline, different mechanism.
Entity extension is fragile. The bundle's overridable entities use Doctrine mapping inheritance, and getting @MappedSuperclass vs. single-table inheritance wrong silently drops columns. Irrelevant in Laravel (just write the model), but worth flagging if you are actually inside the legacy Symfony codebase — test with doctrine:schema:validate, not by eyeballing the admin form.
Slug uniqueness will bite. Str::slug($title) collides on repeated titles. Use a unique index plus a suffix strategy (-2, -3), and a unique validation rule on the admin form. The bundle has no visible collision handling, so this is a regression you introduce by being naive, not one you inherit — own it with a test.
Timezone is the quiet bug in every scheduled-publish implementation. Store UTC, compare with now() (UTC in a correctly configured Laravel app), and only convert at the presentation layer. If editors say "it published an hour early," this is why — and it is the same class of bug the bundle's naive datetime comparison has.
published_at as nullable vs. required. If you make it non-nullable with a default of now(), you can never express "published but no date," which breaks imports. Keep it nullable, and treat published = true AND published_at IS NULL as an immediate-publish case in the scope — or reject it in validation. Pick one and document it, because two developers will otherwise pick differently.
Extension points that matter more than the bundle's. The bundle gives you the model and admin screen and stops. The work you will actually be paged for is: image storage/thumbnails (use spatie/laravel-medialibrary, not a bare string column, if images matter), rich text sanitization (never render raw editor HTML without a purifier), search (laravel/scout or a LIKE fallback), and URL redirects on slug change. Budget these; they are not "extra."
Ownership check before you commit. With 6 stars, 0 dependents, and no CI, the moment you copy a line you own it. That is fine for a spec — you were going to write this code anyway — and fatal for a dependency. State that explicitly in the PR description so nobody later asks why you didn't just pull in the package.
How can I help you explore Laravel packages today?