Modular B2B Vehicle Platform with Laravel
Build a modular Laravel 13 vehicle marketplace with Filament admin and Livewire.
Introduction
30 Second Summary
Wenn du auf mobile.de oder AutoScout24 nach einem Gebrauchtwagen suchst, siehst du Tausende strukturierte Fahrzeugeintraege mit Marke, Kilometerstand, Preis und Kraftstoff. Hinter solchen Plattformen steckt eine modulare Software-Architektur, bei der jedes Feature (Haendlerverwaltung, Kontaktanfragen, Fahrzeuglistings) als eigenstaendiges Modul funktioniert.
In diesem Projekt baust du das Fundament einer solchen B2B-Fahrzeughandelsplattform. Du richtest eine modulare Laravel 13 Applikation mit DDEV ein, erstellst ein eigenstaendiges Vehicle-Modul mit branchenrelevanten Datenfeldern und baust sowohl ein Admin-Panel als auch eine oeffentliche Fahrzeugliste.
What You'll Build
Du oeffnest dein Filament-Admin-Panel, legst ein Fahrzeug mit allen Details an, und siehst es Sekunden spaeter auf der oeffentlichen Listing-Seite erscheinen. Das ist der Moment, in dem deine modulare Plattform zum Leben erwacht.
By the end of this project, you'll have:
- Ein Vehicle-Modul, das du per Artisan-Command aktivieren oder deaktivieren kannst. Es bringt eigene Routes, Migrations, Models und Views mit, komplett unabhaengig vom Rest der Applikation.
- Ein Filament-Admin-Panel, in dem du Fahrzeuge anlegst, bearbeitest und nach Marke, Kraftstoff oder Zustand filterst. 50 realistische Seed-Datensaetze stehen sofort zum Testen bereit.
- Eine oeffentliche Fahrzeugliste mit Livewire-Component und Tailwind CSS-Kartenansicht, auf der Buyer die eingestellten Fahrzeuge mit Preis, Kilometerstand und Baujahr durchsehen.
- Secret Mission: Erweiterte Mehrfach-Filter mit Live-Updates. Buyer koennen nach mehreren Marken, Preisbereichen und Kilometerstand gleichzeitig filtern, und die URL-Parameter sind teilbar.
Gibt es Voraussetzungen?
Du brauchst DDEV installiert und lauffaehig, PHP 8.3+ mit Composer, und grundlegende Laravel-Kenntnisse (Routes, Models, Migrations). Das Projekt laeuft komplett lokal und verursacht keine Kosten.
Before We Start
Before We Start...
Before we dive in, let's lock in what you're about to build and why a modular architecture is the right foundation for a B2B platform that needs to scale with new features over time.
Set Up DDEV, Laravel 13, Filament v5, and the Module System
Jede modulare Plattform braucht ein solides Fundament. Ohne eine saubere Entwicklungsumgebung mit der richtigen Datenbank und dem Modul-System kann kein modularer Code entstehen.
In diesem Schritt richtest du DDEV mit PostgreSQL 16 ein, installierst Laravel 13 und FilamentPHP v5 fuer das Admin-Panel, und konfigurierst nwidart/laravel-modules als Modul-System.
In this step, get ready to:
- DDEV-Projekt mit PostgreSQL 16 erstellen und Laravel 13 installieren.
- FilamentPHP v5 installieren und einen Admin-User anlegen.
- nwidart/laravel-modules v13 mit Autoloading via composer-merge-plugin einrichten.
Create the DDEV project and install Laravel 13
DDEV containerisiert deine gesamte Entwicklungsumgebung. Du bekommst PHP, Composer und PostgreSQL in einem reproduzierbaren Setup.
- Erstelle einen neuen Projektordner auf deinem Desktop und wechsle hinein:
cd ~/Desktop
mkdir fahrzeughandel && cd fahrzeughandel
- Konfiguriere DDEV fuer ein Laravel-Projekt mit PostgreSQL 16:
ddev config --project-type=laravel --docroot=public --database=postgres:16
Was macht dieser Befehl?
- --project-type=laravel teilt DDEV mit, dass es ein Laravel-Projekt ist und passt die Konfiguration entsprechend an.
- --docroot=public setzt den Webserver-Root auf den public-Ordner, wie Laravel es erwartet.
- --database=postgres:16 startet einen PostgreSQL 16 Container statt des Standard-MySQL.
- Starte die DDEV-Container:
ddev start
DDEV laedt die Container-Images und startet Webserver sowie Datenbank. Das kann beim ersten Mal ein paar Minuten dauern.
- Installiere Laravel 13 im Projektverzeichnis:
ddev composer create-project laravel/laravel .
Was passiert hier?
DDEV fuehrt Composer innerhalb des Web-Containers aus. Der Punkt am Ende sorgt dafuer, dass Laravel direkt ins aktuelle Verzeichnis installiert wird. DDEV setzt automatisch die Datenbank-Credentials in der .env-Datei (Host: db, User: db, Passwort: db, Datenbank: db).
- Was erwartest du zu sehen, wenn du die App im Browser oeffnest? Pruefe es:
ddev launch
Dein Browser oeffnet sich mit der Laravel-Welcome-Page. Das bestaetigt, dass DDEV, PHP und PostgreSQL korrekt zusammenarbeiten.
Browser zeigt eine Fehlerseite?
- Pruefe mit ddev status ob alle Container laufen.
- Falls die .env fehlt oder kein App-Key gesetzt ist, fuehre folgendes aus:
ddev exec cp .env.example .env && ddev exec php artisan key:generate
Brauche Hilfe: warum startet meine Laravel-App nicht korrekt?
Install FilamentPHP v5 and set up the admin panel
FilamentPHP generiert ein komplettes Admin-Panel mit CRUD-Oberflaechen aus PHP-Klassen. Du brauchst keine einzige Zeile Frontend-Code dafuer zu schreiben.
- Installiere FilamentPHP v5:
ddev composer require filament/filament:"^5.0"
- Initialisiere das Admin-Panel:
ddev exec php artisan filament:install --panels
Was richtet dieser Befehl ein?
Filament erstellt einen AdminPanelProvider unter app/Providers/Filament/AdminPanelProvider.php. Dieser Provider registriert das Panel unter der URL /admin und steuert, welche Resources und Pages dort erscheinen.
- Fuehre die Datenbank-Migrationen aus, damit die User-Tabelle existiert:
ddev exec php artisan migrate
- Erstelle einen Admin-User fuer das Filament-Panel:
ddev exec php artisan make:filament-user
Der Befehl fragt nach Name, E-Mail und Passwort. Waehle Werte, die du dir merken kannst (z.B. Admin, admin@example.com, password).
- Oeffne das Admin-Panel und logge dich mit deinen Credentials ein:
ddev launch /admin
Du siehst die Filament-Login-Seite. Nach dem Login erscheint das leere Dashboard. Hier werden spaeter die Fahrzeug-Resources erscheinen.
Login-Seite erscheint nicht?
- Pruefe ob die Migration gelaufen ist mit ddev exec php artisan migrate:status. Die users-Tabelle sollte als Ran angezeigt werden.
- Falls ein 404 erscheint, pruefe ob app/Providers/Filament/AdminPanelProvider.php existiert.
Noch Probleme? Hilf mir, mein Filament-Panel zu debuggen.
Install and configure nwidart/laravel-modules v13
Das Modul-System ist das Herzstuck deiner Architektur. Jedes Feature lebt in seinem eigenen Modul mit eigenem ServiceProvider, eigenen Routes und eigenen Migrations.
- Installiere nwidart/laravel-modules:
ddev composer require nwidart/laravel-modules
- Publiziere die Konfigurationsdatei des Pakets:
ddev exec php artisan vendor:publish --provider="Nwidart\Modules\LaravelModulesServiceProvider" --tag="config"
Was bewirkt das?
Der Befehl kopiert die Standard-Konfiguration nach config/modules.php. Dort steuerst du den Pfad fuer Module, das Namespace-Schema und weitere Optionen.
- Installiere das wikimedia/composer-merge-plugin fuer das Modul-Autoloading:
ddev composer require wikimedia/composer-merge-plugin
Warum ein Merge-Plugin?
Jedes Modul bringt eine eigene composer.json mit seinen Abhaengigkeiten mit. Das Merge-Plugin sorgt dafuer, dass Composer diese Modul-Dateien automatisch einbezieht, Abhaengigkeiten installiert und das PSR-4 Autoloading der Module funktioniert.
- Oeffne composer.json im Projektroot und fuege die merge-plugin-Konfiguration in die extra-Section ein:
"extra": {
"merge-plugin": {
"include": [
"Modules/*/composer.json"
]
}
}
Falls extra bereits existiert
Laravel und Filament haben moeglicherweise bereits einen extra-Block in deiner composer.json. Fuege den merge-plugin-Schluessel als zusaetzlichen Eintrag innerhalb des bestehenden extra-Blocks ein. Ersetze den Block nicht komplett.
- Regeneriere das Autoloading, damit die neue Konfiguration greift:
ddev composer dump-autoload
- Was erwartest du bei einer leeren Modulliste zu sehen? Pruefe es:
ddev exec php artisan module:list
Du siehst eine leere Tabelle. Das Modul-System ist bereit, aber es existieren noch keine Module.
Befehl module:list nicht gefunden?
- Pruefe mit ddev exec php artisan list | grep module ob die Module-Commands registriert sind.
- Falls die Commands fehlen, fuehre ddev exec php artisan package:discover aus und versuche es erneut.
Brauche Hilfe: mein artisan module:list Befehl funktioniert nicht.
Dein Fundament steht. DDEV, Laravel, Filament und das Modul-System arbeiten zusammen. Im naechsten Schritt erstellst du dein erstes Vehicle-Modul und siehst die modulare Architektur in Aktion.
Create the Vehicle Module and Explore Its Structure
Your development environment is running with DDEV, PostgreSQL, and the module system installed. The module system is empty though, so the modular architecture remains theoretical.
In this step, you generate your first module and see how nwidart/laravel-modules organizes code into self-contained units. Each module gets its own routes, views, models, migrations, and service providers.
In this step, get ready to:
- Generate the Vehicle module and explore its folder structure.
- Understand the module.json and VehicleServiceProvider files.
- Register a test route that renders a Blade view at /vehicles.
Generate the Vehicle module
The module:make command scaffolds an entire module with its own MVC structure, routes, and configuration files.
- Generate the Vehicle module by running this command:
ddev exec php artisan module:make Vehicle
What does this command do?
This generates a complete module scaffold at Modules/Vehicle/ with its own controllers, providers, routes, views, config, database folders, and metadata files.
The module is automatically enabled. Its status is tracked in modules_statuses.json at the project root.
- Confirm the module was created and is enabled by running:
ddev exec php artisan module:list
You should see the Vehicle module listed with status "Enabled".
Module list is still empty?
- Make sure the module:make command completed without errors. Run ddev composer dump-autoload to refresh autoloading.
- Verify the Modules/Vehicle/ directory exists on disk. If it does not, re-run the command and watch for permission errors.
Help me troubleshoot why my Vehicle module is not showing up.
- Open the Modules/Vehicle/ folder in your editor to explore the generated structure.
You should see folders including app/, config/, database/, resources/, routes/, and metadata files like module.json.
What is inside the module?
Each module mirrors a mini-Laravel app. It has its own app/Providers/ for service registration, app/Http/Controllers/ for request handling, database/ for migrations and seeders, and routes/ for web and API routes.
This structure means you can enable, disable, or extract a module into its own package without touching the rest of your application.
Understand module.json and set up a test route
The module.json file is the identity card of your module. It tells the module system which Service Providers to boot and how to reference the module.
- Open Modules/Vehicle/module.json in your editor and inspect its contents:
{
"name": "Vehicle",
"alias": "vehicle",
"description": "",
"keywords": [],
"priority": 0,
"providers": [
"Modules\\Vehicle\\Providers\\VehicleServiceProvider"
],
"files": []
}
What does module.json control?
- The name field identifies the module for artisan commands like module:enable and module:disable.
- The providers array lists the service providers that get booted when the module is active. The VehicleServiceProvider registers routes, views, and migrations for this module.
- The priority field controls the boot order when multiple modules depend on each other.
Now set up a simple test route so you can confirm the module serves web content. The generated routes/web.php contains controller-based CRUD routes that you do not need yet.
- Open Modules/Vehicle/routes/web.php and replace its entire contents with this simple route:
<?php
use Illuminate\Support\Facades\Route;
// Public vehicle listing route
Route::get('/vehicles', function () {
return view('vehicle::index');
})->name('vehicles.index');
What does this route do?
- The vehicle::index syntax tells Laravel to load the index.blade.php view from the Vehicle module's registered view namespace. The VehicleServiceProvider registers this namespace automatically.
- Using a closure route keeps things simple for testing. Later you will replace this with a Livewire component.
- Open Modules/Vehicle/resources/views/index.blade.php and replace its contents with a simple test view:
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<title>Vehicle Module</title>
</head>
<body>
<h1>Vehicle Module funktioniert!</h1>
<p>Diese Seite wird vom Vehicle-Modul gerendert.</p>
</body>
</html>
- Save both files.
Before you test, what do you expect to see when you visit /vehicles in your browser?
- Open the route in your browser by running:
ddev launch /vehicles
You should see the heading "Vehicle Module funktioniert!" rendered in the browser.
Seeing a 404 error?
- Check if the route is registered by running ddev exec php artisan route:list --path=vehicles. If nothing appears, the VehicleServiceProvider may not be loading routes correctly.
- Verify the module is enabled by running ddev exec php artisan module:list. If it shows "Disabled", enable it with ddev exec php artisan module:enable Vehicle.
- Check for syntax errors in your routes/web.php by running ddev exec php artisan route:clear.
Help me debug why my /vehicles route returns a 404.
✔️ Awesome, I've got everything!
Great. Double check you have saved both Modules/Vehicle/routes/web.php and Modules/Vehicle/resources/views/index.blade.php.
ⓧ I'd like to double check the full code
Here are the two files you edited in this step. Compare them against your code:
<?php
use Illuminate\Support\Facades\Route;
// Public vehicle listing route
Route::get('/vehicles', function () {
return view('vehicle::index');
})->name('vehicles.index');
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<title>Vehicle Module</title>
</head>
<body>
<h1>Vehicle Module funktioniert!</h1>
<p>Diese Seite wird vom Vehicle-Modul gerendert.</p>
</body>
</html>
Your first module is alive and serving content. Next up, you will fill it with a real data model: a Vehicle migration, model, and seeder with automobile-specific fields.
Build the Data Model — Migration, Model, and Seeder
Your Vehicle module has its own routes and views, but the public listing page needs actual data to display. Without a solid data model with industry-relevant fields, the platform has nothing to offer buyers.
In this step, you will build the database layer for the Vehicle module. You will create a migration with automobile-specific fields modeled after platforms like mobile.de and AutoScout24, an Eloquent model, a factory that generates realistic German-market vehicle data, and a seeder that populates the database with 50 sample listings.
In this step, get ready to:
- Generate the model, migration, factory, and seeder files with a single artisan command.
- Define automobile-specific columns in the migration and configure the Eloquent model.
- Implement a factory with realistic vehicle data and seed 50 records into PostgreSQL.
Generate the model, migration, factory, and seeder files
The nwidart/laravel-modules package provides artisan generators that scaffold all database-layer files in one command.
- Generate the Vehicle model along with its migration, factory, and seeder by running:
ddev exec php artisan module:make-model Vehicle Vehicle --migration --factory --seed
What does this command do?
This single command creates four files inside your Vehicle module:
- The Eloquent model at Modules/Vehicle/app/Models/Vehicle.php.
- A migration file in Modules/Vehicle/database/migrations/.
- A factory at Modules/Vehicle/database/factories/VehicleFactory.php.
- A seeder at Modules/Vehicle/database/seeders/VehicleSeeder.php.
- Confirm the files were created by running:
ls Modules/Vehicle/app/Models/ Modules/Vehicle/database/migrations/ Modules/Vehicle/database/factories/ Modules/Vehicle/database/seeders/
You should see Vehicle.php in Models, a timestamped migration file, VehicleFactory.php, and VehicleSeeder.php.
Command not found or module error?
- Ensure DDEV is running with ddev status.
- Verify the Vehicle module is enabled with ddev exec php artisan module:list.
Help me fix my module:make-model command.
Define the vehicle migration schema and configure the model
The generated migration contains an empty up() method. You need to fill it with automobile-specific columns that mirror what platforms like mobile.de use.
- Open the migration file in Modules/Vehicle/database/migrations/ (its name starts with a timestamp and ends in _create_vehicles_table.php).
- Replace the contents of the up() method with this schema definition:
Schema::create('vehicles', function (Blueprint $table) {
$table->id();
$table->string('make');
$table->string('model');
$table->integer('year');
$table->integer('mileage');
$table->decimal('price', 10, 2);
$table->enum('fuel_type', ['petrol', 'diesel', 'electric', 'hybrid']);
$table->enum('transmission', ['manual', 'automatic']);
$table->enum('condition', ['new', 'used', 'demo']);
$table->string('color');
$table->integer('power_hp');
$table->text('description')->nullable();
$table->boolean('is_published')->default(true);
$table->timestamps();
});
What do these columns represent?
- make and model store the brand and model name (e.g. BMW, 3er).
- fuel_type, transmission, and condition use enums to restrict values to valid industry options.
- price uses decimal(10, 2) for precise currency values up to 99,999,999.99.
- is_published defaults to true so new vehicles appear on the public listing immediately.
Now configure the Eloquent model so Laravel knows which fields are mass-assignable.
- Open Modules/Vehicle/app/Models/Vehicle.php and replace its entire contents with:
<?php
namespace Modules\Vehicle\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Modules\Vehicle\Database\Factories\VehicleFactory;
class Vehicle extends Model
{
use HasFactory;
// Allow mass assignment for all vehicle fields
protected $fillable = [
'make',
'model',
'year',
'mileage',
'price',
'fuel_type',
'transmission',
'condition',
'color',
'power_hp',
'description',
'is_published',
];
// Tell Laravel where to find this module's factory
protected static function newFactory(): VehicleFactory
{
return VehicleFactory::new();
}
}
Why newFactory()?
Models inside modules live outside Laravel's default App\Models namespace. The newFactory() method explicitly tells Laravel where to find the factory class for this model, preventing a "class not found" error during seeding.
- Save both files (the migration and the model).
- Run the migration to create the vehicles table by running:
ddev exec php artisan module:migrate Vehicle
You should see output confirming the migration ran successfully, with a line mentioning create_vehicles_table.
Migration failed?
- Check that your DDEV containers are running with ddev status.
- Verify there are no syntax errors in your migration file (missing semicolons or unmatched brackets).
- Ensure you placed the Schema::create block inside the existing up() method, not outside the class.
Help me debug my Vehicle module migration error.
Implement the factory and seeder
The table exists but is empty. You need a factory that generates realistic German-market vehicle data and a seeder that creates 50 sample listings for testing.
- Open Modules/Vehicle/database/factories/VehicleFactory.php and replace its contents with:
<?php
namespace Modules\Vehicle\Database\Factories;
use Illuminate\Database\Eloquent\Factories\Factory;
use Modules\Vehicle\Models\Vehicle;
class VehicleFactory extends Factory
{
protected $model = Vehicle::class;
public function definition(): array
{
// Map real German-market brands to their models
$brands = [
'BMW' => ['3er', '5er', 'X3', 'X5'],
'Mercedes-Benz' => ['C-Klasse', 'E-Klasse', 'GLC', 'A-Klasse'],
'Volkswagen' => ['Golf', 'Passat', 'Tiguan', 'Polo'],
'Audi' => ['A3', 'A4', 'Q5', 'A6'],
];
$make = fake()->randomElement(array_keys($brands));
return [
'make' => $make,
'model' => fake()->randomElement($brands[$make]),
'year' => fake()->numberBetween(2015, 2026),
'mileage' => fake()->numberBetween(0, 200000),
'price' => fake()->randomFloat(2, 5000, 120000),
'fuel_type' => fake()->randomElement(['petrol', 'diesel', 'electric', 'hybrid']),
'transmission' => fake()->randomElement(['manual', 'automatic']),
'condition' => fake()->randomElement(['new', 'used', 'demo']),
'color' => fake()->randomElement(['Schwarz', 'Weiss', 'Silber', 'Blau', 'Rot']),
'power_hp' => fake()->numberBetween(90, 450),
'description' => fake()->optional(0.7)->sentence(),
'is_published' => fake()->boolean(90),
];
}
}
What does this factory generate?
Each call to Vehicle::factory()->create() picks a random brand from the $brands array, then selects a matching model name. This ensures realistic brand/model combinations like "BMW 3er" or "Volkswagen Golf" instead of random strings.
The fake()->boolean(90) on is_published means 90% of generated vehicles are published. This gives you a mix of visible and hidden listings to test filtering later.
- Open Modules/Vehicle/database/seeders/VehicleSeeder.php and replace its contents with:
<?php
namespace Modules\Vehicle\Database\Seeders;
use Illuminate\Database\Seeder;
use Modules\Vehicle\Models\Vehicle;
class VehicleSeeder extends Seeder
{
public function run(): void
{
// Generate 50 realistic vehicle listings
Vehicle::factory()->count(50)->create();
}
}
- Open Modules/Vehicle/database/seeders/DatabaseSeeder.php (the module's master seeder) and replace its contents with:
<?php
namespace Modules\Vehicle\Database\Seeders;
use Illuminate\Database\Seeder;
class VehicleDatabaseSeeder extends Seeder
{
public function run(): void
{
// Call the VehicleSeeder to populate the vehicles table
$this->call(VehicleSeeder::class);
}
}
- Save all three files.
Before you run the seeder, what do you think will happen? Will 50 records appear, or could something go wrong with autoloading?
- Seed the database with 50 vehicle records by running:
ddev exec php artisan module:seed Vehicle
- Verify that 50 records exist in the vehicles table by running:
ddev psql -c "SELECT count(*) FROM vehicles;"
You should see 50 as the count result. Your database now has realistic vehicle listings ready for the Filament admin panel and the public listing page.
Seeing 0 rows or a seeder error?
- If you see a "class not found" error mentioning the factory, verify that Vehicle.php has the newFactory() method returning VehicleFactory::new().
- If you see a namespace error, check that your factory namespace is Modules\Vehicle\Database\Factories and not Database\Factories.
- Run ddev composer dump-autoload to refresh the class map if autoloading seems stale.
Help me fix my Vehicle module seeder error.
✔️ Awesome, I've got everything!
Great. Double check you have saved all files and that ddev psql -c "SELECT count(*) FROM vehicles;" returns 50.
ⓧ I'd like to double check the full code
Here are the key files you should have at this point. Scroll through each and compare against your code.
Vehicle model (Modules/Vehicle/app/Models/Vehicle.php):
<?php
namespace Modules\Vehicle\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Modules\Vehicle\Database\Factories\VehicleFactory;
class Vehicle extends Model
{
use HasFactory;
// Allow mass assignment for all vehicle fields
protected $fillable = [
'make',
'model',
'year',
'mileage',
'price',
'fuel_type',
'transmission',
'condition',
'color',
'power_hp',
'description',
'is_published',
];
// Tell Laravel where to find this module's factory
protected static function newFactory(): VehicleFactory
{
return VehicleFactory::new();
}
}
VehicleFactory (Modules/Vehicle/database/factories/VehicleFactory.php):
<?php
namespace Modules\Vehicle\Database\Factories;
use Illuminate\Database\Eloquent\Factories\Factory;
use Modules\Vehicle\Models\Vehicle;
class VehicleFactory extends Factory
{
protected $model = Vehicle::class;
public function definition(): array
{
// Map real German-market brands to their models
$brands = [
'BMW' => ['3er', '5er', 'X3', 'X5'],
'Mercedes-Benz' => ['C-Klasse', 'E-Klasse', 'GLC', 'A-Klasse'],
'Volkswagen' => ['Golf', 'Passat', 'Tiguan', 'Polo'],
'Audi' => ['A3', 'A4', 'Q5', 'A6'],
];
$make = fake()->randomElement(array_keys($brands));
return [
'make' => $make,
'model' => fake()->randomElement($brands[$make]),
'year' => fake()->numberBetween(2015, 2026),
'mileage' => fake()->numberBetween(0, 200000),
'price' => fake()->randomFloat(2, 5000, 120000),
'fuel_type' => fake()->randomElement(['petrol', 'diesel', 'electric', 'hybrid']),
'transmission' => fake()->randomElement(['manual', 'automatic']),
'condition' => fake()->randomElement(['new', 'used', 'demo']),
'color' => fake()->randomElement(['Schwarz', 'Weiss', 'Silber', 'Blau', 'Rot']),
'power_hp' => fake()->numberBetween(90, 450),
'description' => fake()->optional(0.7)->sentence(),
'is_published' => fake()->boolean(90),
];
}
}
VehicleSeeder (Modules/Vehicle/database/seeders/VehicleSeeder.php):
<?php
namespace Modules\Vehicle\Database\Seeders;
use Illuminate\Database\Seeder;
use Modules\Vehicle\Models\Vehicle;
class VehicleSeeder extends Seeder
{
public function run(): void
{
// Generate 50 realistic vehicle listings
Vehicle::factory()->count(50)->create();
}
}
VehicleDatabaseSeeder (Modules/Vehicle/database/seeders/DatabaseSeeder.php):
<?php
namespace Modules\Vehicle\Database\Seeders;
use Illuminate\Database\Seeder;
class VehicleDatabaseSeeder extends Seeder
{
public function run(): void
{
// Call the VehicleSeeder to populate the vehicles table
$this->call(VehicleSeeder::class);
}
}
Your Vehicle module now has a complete data layer with 50 realistic listings in PostgreSQL. Next up, you will build a Filament resource that turns this data into a full admin CRUD interface with filtering and search.
Build the Filament Resource for Vehicle Management
Your vehicles exist in the database, but nobody can manage them yet. Without a UI, adding or editing a listing means writing raw SQL.
Filament solves this by turning an Eloquent model into a full admin interface in minutes. In this step you will create a VehicleResource inside the Vehicle module that gives you a searchable table, filters, and create/edit forms for every vehicle field.
In this step, get ready to:
- Create a Filament Resource and its Page classes inside the Vehicle module.
- Register the module's resource path in the AdminPanelProvider.
- Define table columns, filters, and a form schema for full CRUD.
Create the VehicleResource and register it in the AdminPanelProvider
Filament does not auto-discover resources inside modules. You need to create the resource class manually and then tell the AdminPanelProvider where to find it.
- Create the directory structure for the resource and its pages by running these commands:
ddev exec mkdir -p Modules/Vehicle/app/Filament/Resources/VehicleResource/Pages
- Create the VehicleResource skeleton by adding this file at Modules/Vehicle/app/Filament/Resources/VehicleResource.php:
<?php
namespace Modules\Vehicle\Filament\Resources;
use Filament\Forms;
use Filament\Forms\Form;
use Filament\Resources\Resource;
use Filament\Tables;
use Filament\Tables\Table;
use Modules\Vehicle\Models\Vehicle;
class VehicleResource extends Resource
{
// Bind this resource to the Vehicle model
protected static ?string $model = Vehicle::class;
protected static ?string $navigationIcon = 'heroicon-o-truck';
public static function form(Form $form): Form
{
return $form->schema([]);
}
public static function table(Table $table): Table
{
return $table->columns([]);
}
public static function getPages(): array
{
return [
'index' => VehicleResource\Pages\ListVehicles::route('/'),
'create' => VehicleResource\Pages\CreateVehicle::route('/create'),
'edit' => VehicleResource\Pages\EditVehicle::route('/{record}/edit'),
];
}
}
What does this code do?
This is the resource skeleton that tells Filament which model to manage and which pages (list, create, edit) are available. The form() and table() methods are empty for now. You will fill them in the next substep.
The getPages() array maps URL segments to Page classes that you will create next.
- Create the ListVehicles page at Modules/Vehicle/app/Filament/Resources/VehicleResource/Pages/ListVehicles.php:
<?php
namespace Modules\Vehicle\Filament\Resources\VehicleResource\Pages;
use Filament\Actions;
use Filament\Resources\Pages\ListRecords;
use Modules\Vehicle\Filament\Resources\VehicleResource;
class ListVehicles extends ListRecords
{
// Connects this page to the VehicleResource
protected static string $resource = VehicleResource::class;
protected function getHeaderActions(): array
{
return [Actions\CreateAction::make()];
}
}
- Create the CreateVehicle page at Modules/Vehicle/app/Filament/Resources/VehicleResource/Pages/CreateVehicle.php:
<?php
namespace Modules\Vehicle\Filament\Resources\VehicleResource\Pages;
use Filament\Resources\Pages\CreateRecord;
use Modules\Vehicle\Filament\Resources\VehicleResource;
class CreateVehicle extends CreateRecord
{
// Connects this page to the VehicleResource
protected static string $resource = VehicleResource::class;
}
- Create the EditVehicle page at Modules/Vehicle/app/Filament/Resources/VehicleResource/Pages/EditVehicle.php:
<?php
namespace Modules\Vehicle\Filament\Resources\VehicleResource\Pages;
use Filament\Actions;
use Filament\Resources\Pages\EditRecord;
use Modules\Vehicle\Filament\Resources\VehicleResource;
class EditVehicle extends EditRecord
{
// Connects this page to the VehicleResource
protected static string $resource = VehicleResource::class;
protected function getHeaderActions(): array
{
return [Actions\DeleteAction::make()];
}
}
Why three separate page classes?
Filament uses one Page class per screen: listing, creating, and editing. Each extends a Filament base class that provides the full UI logic automatically. You only need to point them at the correct resource.
Now register the module's resource directory in the AdminPanelProvider so Filament can discover VehicleResource.
- Open app/Providers/Filament/AdminPanelProvider.php.
- Find the existing discoverResources line:
->discoverResources(in: app_path('Filament/Resources'), for: 'App\\Filament\\Resources')
- Add a second discoverResources call directly below it, so both lines are present:
->discoverResources(in: app_path('Filament/Resources'), for: 'App\\Filament\\Resources')
->discoverResources(in: base_path('Modules/Vehicle/app/Filament/Resources'), for: 'Modules\\Vehicle\\Filament\\Resources')
What does discoverResources do?
The in: parameter points to the directory on disk. The for: parameter tells Filament the PHP namespace those classes live under. Filament scans the directory, finds classes that extend Resource, and registers them in the admin panel automatically.
- Save AdminPanelProvider.php.
- Open the admin panel in your browser by running:
ddev launch /admin
You should see a Vehicles entry in the left sidebar with a truck icon. Clicking it shows an empty table because table() returns no columns yet.
Don't see Vehicles in the sidebar?
- Confirm the namespace in VehicleResource.php is exactly Modules\Vehicle\Filament\Resources. A typo means Filament cannot match the class to the discovery path.
- Make sure the for: string in AdminPanelProvider uses double backslashes (\\) since it is inside a PHP string.
- Run ddev exec php artisan optimize:clear to flush cached config and routes, then reload the page.
Still stuck? Why isn't my VehicleResource showing in the Filament sidebar?
Define table columns, filters, and the form schema
The resource exists but shows nothing useful. Now you will fill in the table with columns and filters so you can browse and search the 50 seeded vehicles, then add the form schema so create and edit work.
- In VehicleResource.php, replace the entire table() method with the following:
public static function table(Table $table): Table
{
return $table
->columns([
Tables\Columns\TextColumn::make('make')
->searchable()->sortable(),
Tables\Columns\TextColumn::make('model')
->searchable()->sortable(),
Tables\Columns\TextColumn::make('year')
->sortable(),
Tables\Columns\TextColumn::make('mileage')
->numeric()->sortable(),
Tables\Columns\TextColumn::make('price')
->money('EUR')->sortable(),
Tables\Columns\TextColumn::make('fuel_type')
->badge(),
Tables\Columns\TextColumn::make('condition')
->badge(),
])
->filters([
// Dynamic options populated from existing DB values
Tables\Filters\SelectFilter::make('make')
->options(fn () => Vehicle::distinct()->pluck('make', 'make')->toArray()),
Tables\Filters\SelectFilter::make('fuel_type')
->options(['petrol' => 'Petrol', 'diesel' => 'Diesel', 'electric' => 'Electric', 'hybrid' => 'Hybrid']),
Tables\Filters\SelectFilter::make('condition')
->options(['new' => 'New', 'used' => 'Used', 'demo' => 'Demo']),
])
->actions([
Tables\Actions\EditAction::make(),
])
->bulkActions([
Tables\Actions\BulkActionGroup::make([
Tables\Actions\DeleteBulkAction::make(),
]),
]);
}
What does this code do?
- Seven TextColumn entries map each vehicle field to a table column. searchable() adds a search input for make and model. sortable() enables click-to-sort headers.
- Three SelectFilter entries let you narrow results by make, fuel type, or condition. The make filter pulls its options dynamically from the database so every brand in your seed data appears.
- The actions array adds an Edit button per row. bulkActions adds a checkbox-based bulk delete.
- Save VehicleResource.php and refresh the admin panel at /admin/vehicles.
You should see all 50 vehicles in a paginated table with sortable columns. Click the filter icon to test filtering by make or fuel type.
Seeing a class not found error?
- Check that the use statements at the top of VehicleResource.php include both Filament\Tables and Modules\Vehicle\Models\Vehicle.
- Run ddev composer dump-autoload if the autoloader has not picked up the new files.
Still stuck? Help me fix a class not found error in my Filament VehicleResource table
- Now replace the empty form() method with the full schema so that creating and editing vehicles works:
public static function form(Form $form): Form
{
return $form
->schema([
Forms\Components\TextInput::make('make')
->required()->maxLength(255),
Forms\Components\TextInput::make('model')
->required()->maxLength(255),
Forms\Components\TextInput::make('year')
->numeric()->required(),
Forms\Components\TextInput::make('mileage')
->numeric()->required(),
Forms\Components\TextInput::make('price')
->numeric()->required()->prefix('\u20ac'),
Forms\Components\TextInput::make('power_hp')
->numeric()->required()->label('Power (HP)'),
Forms\Components\Select::make('fuel_type')
->options(['petrol' => 'Petrol', 'diesel' => 'Diesel', 'electric' => 'Electric', 'hybrid' => 'Hybrid'])
->required(),
Forms\Components\Select::make('transmission')
->options(['manual' => 'Manual', 'automatic' => 'Automatic'])
->required(),
Forms\Components\Select::make('condition')
->options(['new' => 'New', 'used' => 'Used', 'demo' => 'Demo'])
->required(),
Forms\Components\TextInput::make('color')
->required()->maxLength(255),
Forms\Components\Textarea::make('description')
->columnSpanFull(),
Forms\Components\Toggle::make('is_published')
->default(true),
]);
}
What does this code do?
- Each TextInput maps to a string or numeric database column. required() adds server-side validation.
- Three Select fields present the enum values (fuel_type, transmission, condition) as dropdowns.
- The Textarea for description spans the full width. The Toggle for is_published defaults to true so new vehicles are visible immediately.
- Save VehicleResource.php.
Before you test, what do you expect to see when you click the Edit button on a vehicle row?
- In the admin panel at /admin/vehicles, click the pencil icon on any vehicle row.
You should see a form pre-filled with that vehicle's data: make, model, year, mileage, price, power, fuel type, transmission, condition, color, description, and the published toggle. Change the mileage, click Save, and confirm the table now shows the updated value.
Form fields showing but save fails?
- Make sure the Vehicle model's $fillable array in Modules/Vehicle/app/Models/Vehicle.php includes every field name used in the form (make, model, year, mileage, price, fuel_type, transmission, condition, color, power_hp, description, is_published).
- Check that the column names in the form match your migration column names exactly. A mismatch like fuelType instead of fuel_type causes silent save failures.
Still stuck? Help me debug why my Filament form saves fail for the Vehicle resource
✔️ Awesome, I've got everything!
Great. Double-check you have saved all files, then confirm the table at /admin/vehicles shows 50 rows and that editing a vehicle works.
ⓧ I'd like to double check the full code
Your VehicleResource.php was built across three chunks above. Scroll up and confirm these are present, in order:
- The class declaration with $model, $navigationIcon, and the getPages() method.
- The complete table() method with 7 columns, 3 SelectFilters, actions, and bulkActions.
- The complete form() method with 12 form components.
Also confirm your AdminPanelProvider.php has both discoverResources lines (the original for App\Filament\Resources and the new one for Modules\Vehicle\Filament\Resources).
Your admin panel now lets you browse, filter, create, and edit vehicles without touching a single line of SQL. Next up, you will build the public-facing vehicle listing so that buyers can see what is available.
Build the Public Vehicle Listing with Livewire
Die Plattform hat jetzt Daten und ein Admin-Panel, aber Buyer sehen noch nichts. Die oeffentliche Listing-Seite ist der Kern des Produkts: hier kommen Kaeufer und Angebote zusammen.
In diesem Schritt baust du eine Livewire-Component, die alle publizierten Fahrzeuge paginiert laedt und in einem Tailwind CSS-Karten-Layout anzeigt. Dafuer erstellst du ein App-Layout und stellst die Route auf die Full-Page-Component um.
In this step, get ready to:
- Eine Livewire-Component VehicleList mit Pagination erstellen.
- Eine Blade-View mit Tailwind-Karten-Layout fuer die Fahrzeugliste bauen.
- Ein App-Layout erstellen und die Route /vehicles auf die Livewire-Component umstellen.
Create the VehicleList Livewire component
Livewire Full-Page-Components sind Components, die direkt als Route gerendert werden. Sie brauchen ein Layout (das HTML-Grundgeruest) und eine Blade-View fuer den eigentlichen Inhalt.
- Erstelle die benoetigten Verzeichnisse fuer die Livewire-Component und ihre View, indem du diesen Befehl ausfuehrst:
ddev exec mkdir -p Modules/Vehicle/app/Livewire Modules/Vehicle/resources/views/livewire
- Pruefe, dass die Verzeichnisse existieren:
ddev exec ls Modules/Vehicle/app/ Modules/Vehicle/resources/views/
Du solltest Livewire unter app/ und livewire unter views/ in der Ausgabe sehen.
- Erstelle die Component-Datei Modules/Vehicle/app/Livewire/VehicleList.php mit folgendem Inhalt:
<?php
namespace Modules\Vehicle\Livewire;
use Livewire\Component;
use Livewire\WithPagination;
use Modules\Vehicle\Models\Vehicle;
class VehicleList extends Component
{
use WithPagination;
public function render()
{
// Load only published vehicles, 12 per page
$vehicles = Vehicle::where('is_published', true)
->paginate(12);
return view('vehicle::livewire.vehicle-list', [
'vehicles' => $vehicles,
]);
}
}
What does this code do?
- use WithPagination aktiviert Livewires Pagination-Features, sodass Seitenwechsel ohne vollstaendigen Page-Reload funktionieren.
- Vehicle::where('is_published', true)->paginate(12) laedt nur veroeffentlichte Fahrzeuge und zeigt 12 pro Seite.
- vehicle::livewire.vehicle-list referenziert die Blade-View im Vehicle-Modul ueber den Modul-View-Namespace.
Create the Blade view with Tailwind card layout
- Erstelle die zugehoerige Blade-View unter Modules/Vehicle/resources/views/livewire/vehicle-list.blade.php mit diesem Tailwind-Karten-Layout:
<div>
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<h1 class="text-3xl font-bold text-gray-900 mb-8">Fahrzeuge</h1>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
@foreach($vehicles as $vehicle)
<div class="bg-white rounded-lg shadow-md overflow-hidden">
<div class="p-6">
<!-- Vehicle title -->
<h2 class="text-xl font-semibold text-gray-900">
{{ $vehicle->make }} {{ $vehicle->model }}
</h2>
<!-- Price -->
<p class="text-2xl font-bold text-blue-600 mt-2">
{{ number_format($vehicle->price, 0, ',', '.') }} €
</p>
<!-- Key details -->
<div class="mt-4 space-y-1 text-sm text-gray-600">
<p>Baujahr: {{ $vehicle->year }}</p>
<p>Kilometerstand: {{ number_format($vehicle->mileage, 0, ',', '.') }} km</p>
<p>Kraftstoff: {{ ucfirst($vehicle->fuel_type) }}</p>
</div>
</div>
</div>
@endforeach
</div>
<!-- Pagination links -->
<div class="mt-8">
{{ $vehicles->links() }}
</div>
</div>
</div>
What does this view do?
- Das aeussere <div> ist das Single-Root-Element, das jede Livewire-Component braucht.
- grid-cols-1 md:grid-cols-2 lg:grid-cols-3 zeigt je nach Bildschirmgroesse 1 bis 3 Karten nebeneinander.
- number_format($vehicle->price, 0, ',', '.') formatiert den Preis im deutschen Zahlenformat (Punkt als Tausendertrenner).
- $vehicles->links() rendert die Pagination-Navigation automatisch.
- Speichere beide Dateien (VehicleList.php und vehicle-list.blade.php).
- Pruefe, dass die View-Datei existiert:
ddev exec ls Modules/Vehicle/resources/views/livewire/
Du solltest vehicle-list.blade.php in der Ausgabe sehen.
Datei nicht sichtbar?
- Stelle sicher, dass du die Verzeichnisse mit dem mkdir -p Befehl weiter oben erstellt hast.
- Pruefe die Schreibweise: livewire (klein) im Views-Pfad und vehicle-list.blade.php (kebab-case) als Dateiname.
Hilfe noetig? Warum findet Livewire meine View nicht?
Create the app layout and update the route
Livewire Full-Page-Components brauchen ein Layout als HTML-Rahmen. Standardmaessig sucht Livewire nach resources/views/layouts/app.blade.php.
- Generiere das Livewire-Layout, indem du diesen Befehl ausfuehrst:
ddev exec php artisan livewire:layout
What does this command do?
Dieser Artisan-Befehl erstellt die Datei resources/views/layouts/app.blade.php mit einem Standard-HTML-Geruest. Es enthaelt @livewireStyles, @livewireScripts und einen {{ $slot }} Platzhalter, in dem die Component gerendert wird.
- Oeffne die generierte Datei resources/views/layouts/app.blade.php und ersetze den gesamten Inhalt mit diesem erweiterten Layout:
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{ $title ?? 'B2B Fahrzeughandel' }}</title>
@vite(['resources/css/app.css', 'resources/js/app.js'])
@livewireStyles
</head>
<body class="bg-gray-100">
<!-- Navigation header -->
<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 items-center">
<a href="/" class="text-xl font-bold text-gray-900">B2B Fahrzeughandel</a>
<a href="/vehicles" class="text-gray-600 hover:text-gray-900">Fahrzeuge</a>
</div>
</div>
</nav>
<!-- Component content renders here -->
<main>
{{ $slot }}
</main>
@livewireScripts
</body>
</html>
What does this layout do?
- @vite(['resources/css/app.css', 'resources/js/app.js']) laedt Tailwind CSS und JavaScript ueber den Vite-Bundler.
- @livewireStyles und @livewireScripts laden die Assets, die Livewire fuer reaktive Updates braucht.
- {{ $slot }} ist der Platzhalter, in den Livewire die Component-View einfuegt.
- Speichere resources/views/layouts/app.blade.php.
- Oeffne jetzt Modules/Vehicle/routes/web.php und ersetze den gesamten Inhalt mit dieser Route:
<?php
use Illuminate\Support\Facades\Route;
use Modules\Vehicle\Livewire\VehicleList;
// Public vehicle listing as full-page Livewire component
Route::livewire('/vehicles', VehicleList::class)
->name('vehicles.index');
What does this route do?
Route::livewire('/vehicles', VehicleList::class) registriert die VehicleList-Component als Full-Page-Component. Wenn ein Buyer /vehicles aufruft, rendert Livewire die Component innerhalb des App-Layouts.
In Livewire v4 ist Route::livewire() der empfohlene Weg fuer Full-Page-Components. Er ersetzt das aeltere Route::get()-Pattern.
- Speichere Modules/Vehicle/routes/web.php.
- Aktualisiere den Autoloader, damit die neue Livewire-Klasse gefunden wird:
ddev composer dump-autoload
Bevor du die Seite aufrufst: Was erwartest du zu sehen, wenn du /vehicles im Browser oeffnest? Wie viele Karten sollten auf der ersten Seite erscheinen?
- Oeffne die Fahrzeugliste im Browser, indem du diesen Befehl ausfuehrst:
ddev launch /vehicles
Du siehst eine gestylte Fahrzeugliste mit Kartenansicht. Jede Karte zeigt Marke, Modell, Preis, Kilometerstand, Baujahr und Kraftstoff.
Unter den Karten erscheinen Pagination-Links, weil 50 Fahrzeuge auf 12er-Seiten verteilt sind (5 Seiten insgesamt).
Leere Seite oder Fehler?
- Wenn du eine leere weisse Seite siehst, pruefe ob Vite laeuft. Fuehre ddev exec npm run build aus, um die CSS-Assets zu kompilieren.
- Wenn du einen 404-Fehler bekommst, pruefe ob Modules/Vehicle/routes/web.php die richtige Route enthaelt und ob der VehicleServiceProvider die Routes registriert.
- Wenn du einen Class-not-found-Fehler siehst, fuehre ddev composer dump-autoload aus, damit der Autoloader die neue Klasse findet.
Hilfe noetig? Hilf mir bei der Fehlersuche fuer meine Livewire-Fahrzeugliste
✔️ Awesome, I've got everything!
Sehr gut. Pruefe nochmal, dass du alle Dateien gespeichert hast.
ⓧ I'd like to double check the full code
Hier sind die vier Dateien, die du in diesem Schritt erstellt oder geaendert hast. Scrolle nach oben und vergleiche jede mit deiner Version:
- Modules/Vehicle/app/Livewire/VehicleList.php (Component mit WithPagination und paginate(12)).
- Modules/Vehicle/resources/views/livewire/vehicle-list.blade.php (Blade-View mit Tailwind-Grid und Pagination-Links).
- resources/views/layouts/app.blade.php (Layout mit @livewireStyles, @livewireScripts und Navigation).
- Modules/Vehicle/routes/web.php (Route::livewire auf VehicleList::class).
Die oeffentliche Fahrzeugliste steht. Buyer koennen jetzt alle eingestellten Fahrzeuge mit Preis, Kilometerstand und Baujahr sehen und durch die Seiten blaettern. Deine modulare Plattform hat damit sowohl eine Admin-Verwaltung als auch eine oeffentliche Buyer-Experience.
Secret mission
Erweiterte Filterung mit Mehrfachselektion
Eine Fahrzeugliste ohne Filter ist wie ein Telefonbuch ohne Alphabet. In dieser Secret Mission erweiterst du die VehicleList-Component um Mehrfach-Markenfilter, Preis- und Kilometerstand-Bereiche. Alle Filter arbeiten kombiniert mit AND-Logik und aktualisieren die Liste live via Livewire. Die Filter-Zustaende werden als URL-Query-Parameter gespeichert, sodass Buyer ihre Suche per Link teilen koennen.
Clean Up Your Resources
Clean Up Your Resources
This project runs entirely locally using DDEV containers. There are no cloud resources and no ongoing costs.
Decide whether to keep your resources running, pause them to come back later, or delete them entirely.
Resources you used:
- DDEV containers (Laravel app server and PostgreSQL 16 database).
- Local project files (the folder containing your .ddev/ directory and Modules/ folder).
Keep everything running
No action needed. Choose this if you are still actively building on the platform or plan to add more modules.
- Your DDEV containers will stop when you shut down your computer.
- Run ddev start from your project folder to bring everything back up after a reboot.
- All project files and database data remain intact on disk.
Pause - I'll come back to this later
Shut down running containers to free up memory and CPU while keeping all data intact.
- Stop the DDEV containers by running:
ddev stop
- This stops the Docker containers but preserves your database and all project files.
- When you are ready to continue, navigate back into your project folder and run ddev start.
Delete - I don't want to use this again
Remove all DDEV containers, the database, and your project files completely.
- Delete the DDEV project and its database by running this from inside your project folder:
ddev delete -O
What does this command do?
The -O flag skips creating a database snapshot before deletion. This removes the Docker containers and the PostgreSQL database entirely.
Your local project files on disk are NOT removed by this command. You need to delete them separately.
- Delete the project folder from your Desktop by running:
macOS
cd ~/Desktop
rm -rf your-project-folder
Windows
cd ~\Desktop
Remove-Item -Recurse -Force your-project-folder
- Replace your-project-folder with the actual name of your project directory.
- After both commands, your machine is back to its original state.
Nice Work!
Nice Work!
You just built a modular B2B vehicle trading platform from scratch using Laravel 13, FilamentPHP v5, and nwidart/laravel-modules. You have a scalable, module-based architecture that mirrors how professional platforms like Magento structure their features.
You've learned how to:
- Set up a modular Laravel 13 application with DDEV, PostgreSQL, and nwidart/laravel-modules v13, where each feature lives in its own self-contained module with independent routes, migrations, and service providers.
- Build a complete Vehicle module with an industry-specific data model and a Filament v5 Resource for full CRUD management with filters, so dealers can create and manage listings from an admin panel.
- Create a public Livewire-powered vehicle listing page that displays seeded inventory to buyers with pagination and card-based layouts, forming the core B2B buyer experience.
- Secret Mission: Implement advanced multi-select filters with live updates and shareable URL parameters, so buyers can combine brand, price range, and mileage filters without a page reload.
Ready to quiz yourself?