iTranslated by AI
Building a Chat App with Laravel (Up to Model Creation)
This is a detailed article based on the post above.
Creating the Chat App
Now that the boilerplate is ready, let's start building the application step-by-step. The theme for this project is a chat application. The code is primarily based on a Laravel boilerplate implemented using Gemini 2.5 Flash (the version available for free anonymous use as of May 2025).
Requirements
- Everyone enters a single room
- Users can post messages anonymously or while logged in
- Messages from others can be read in real-time (*)
- Messages cannot be edited or deleted; posts older than one day are purged from the server once a day
- The client side also removes messages older than one day from the screen every minute
- Up to 50 messages can be read per request (up to 50 from a specific time or up to 50 of the latest messages. Messages older than one day are not returned)
*Uses SSE (Server-Sent Events)
Goals and Detailed Specifications for This Article
- Initial display only
- No message sending
- No online user count display
- Display up to 50 messages from the database
- Use Laravel defaults for users, authentication, and settings
- Display username if authenticated, otherwise display as anonymous
- Support both dark and light modes based on default user settings
- Up to the database model creation required for the above
Creating the Model
By "model," I am referring to the database model.
Since I'm at it, I'll include some design-style diagrams just to set the mood.
ER Diagram
At this stage, the ER Diagram (which illustrates the relationships between database tables) is quite simple.
Here, the users table is the one used by Laravel's default user authentication.
The chats table is the part we are creating this time.
Class Diagram
The entities in the ER diagram above are mapped to classes in a class diagram.
*Note: While the actual code of the Model-derived classes doesn't exactly follow this structure, this is how it behaves conceptually.
Code Generation
The mapping between entities (tables) in an ER diagram and classes in a class diagram is handled by an ORM, and in Laravel, Eloquent takes on this role.
Below, we generate the boilerplate code for the Eloquent model class and its associated classes.
~/php/example-app $ php artisan make:model Chat --all
INFO Model [app/Models/Chat.php] created successfully.
INFO Factory [database/factories/ChatFactory.php] created successfully.
INFO Migration [database/migrations/????_??_??_??????_create_chats_table.php] created successfully.
INFO Seeder [database/seeders/ChatSeeder.php] created successfully.
INFO Request [app/Http/Requests/StoreChatRequest.php] created successfully.
INFO Request [app/Http/Requests/UpdateChatRequest.php] created successfully.
INFO Controller [app/Http/Controllers/ChatController.php] created successfully.
INFO Policy [app/Policies/ChatPolicy.php] created successfully.
~/php/example-app $
Implementation
We will fill in the implementation based on the generated boilerplate classes.
Migration (Database Schema)
First, let's start with the database schema (migration). In Laravel, it seems the model is determined by the existence of the table first.
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('chats', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->nullable()->constrained()->onDelete('cascade');
$table->string('message', 4096);
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('chats');
}
};
First, we establish a relation to the users table, allowing NULL values and configuring a cascade delete. Next, we define a message column as a string with a maximum of 4096 bytes.
(While we won't apply it just yet) When actually reflected in the database, a table like this will be created:
CREATE TABLE `chats` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
`user_id` bigint(20) unsigned DEFAULT NULL,
`message` varchar(4096) NOT NULL,
`created_at` timestamp NULL DEFAULT NULL,
`updated_at` timestamp NULL DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `chats_user_id_foreign` (`user_id`),
CONSTRAINT `chats_user_id_foreign` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB AUTO_INCREMENT=11 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
Note: This is the result of running show create table chats in MariaDB SQL.
Model
Next is the implementation of the Model itself.
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class Chat extends Model
{
/** @use HasFactory<\Database\Factories\ChatFactory> */
use HasFactory;
protected $fillable = [
'user_id',
'message',
];
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
}
As mentioned earlier, since it seems models are generated based on the database schema, there is no need to describe the model itself in great detail. However, mass assignment (fillable) and the definitions for relationships appear to be necessary.
Factory
Next, we implement how to create dummy data for the model we created (factory). This is used for testing and other purposes.
<?php
namespace Database\Factories;
use Illuminate\Database\Eloquent\Factories\Factory;
use App\Models\User;
use Illuminate\Support\Facades\DB;
/**
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Chat>
*/
class ChatFactory extends Factory
{
protected $model = \App\Models\Chat::class;
/**
* Define the model's default state.
*
* @return array<string, mixed>
*/
public function definition(): array
{
$usersCount = DB::table('users')->count();
$userId = null;
if ($usersCount > 0 && $this->faker->boolean()) {
$userId = User::inRandomOrder()->first()->id;
}
return [
'user_id' => $userId,
'message' => $this->faker->realtext,
];
}
}
Faker is quite convenient.
Seed
This is the code used when creating dummy data (seed). It controls aspects such as how many items to create.
<?php
namespace Database\Seeders;
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use Illuminate\Database\Seeder;
use App\Models\Chat;
class ChatSeeder extends Seeder
{
/**
* Run the database seeds.
*/
public function run(): void
{
Chat::factory()->count(10)->create();
}
}
Applying to the Database (Migration)
I'll apply the changes to the database to verify everything. However, since applying them as-is would result in English data, I'll change the settings before proceeding.
...
#APP_LOCALE=en
APP_LOCALE=ja
#APP_FALLBACK_LOCALE=en
APP_FALLBACK_LOCALE=ja
#APP_FAKER_LOCALE=en_US
APP_FAKER_LOCALE=ja_JP
...
Now, let's apply the migrations.
~/php/example-app $ php artisan migrate:fresh
Dropping all tables .................................................................... 25.79ms DONE
INFO Preparing database.
Creating migration table ............................................................... 16.72ms DONE
INFO Running migrations.
0001_01_01_000000_create_users_table ................................................... 37.85ms DONE
0001_01_01_000001_create_cache_table ................................................... 12.06ms DONE
0001_01_01_000002_create_jobs_table .................................................... 30.28ms DONE
????_??_??_??????_create_chats_table ................................................... 31.66ms DONE
~/php/example-app $
Generating Dummy Data
I'll create the default dummy users provided by default.
~/php/example-app $ php artisan db:seed
INFO Seeding database.
~/php/example-app $
Next, I'll create dummy data for the model we created.
~/php/example-app $ php artisan db:seed --class=ChatSeeder
INFO Seeding database.
~/php/example-app $
Checking the Database
Let's check the contents of the database. We'll use tinker, Laravel's REPL.
~/php/example-app $ php artisan tinker
Psy Shell v0.12.8 (PHP 8.4.2 — cli) by Justin Hileman
> App\Models\Chat::all()
= Illuminate\Database\Eloquent\Collection {#1365
all: [
App\Models\Chat {#6244
id: 1,
user_id: 1,
message: "まれていしてつどうか、まわってるんだん大きなり、天の川の水ぎわに沿そっちに囲かことなり
、姉ねえ、おしまいました。「こどもほんとはなぜかさあ、ぼください」ジョバンニは勢いきなり、まるで運動場う
んどい高い高原で待まって見ました。そして、まもなくなり、姉ねえ」「どうも済すみのようで橋はしらがその黒い
門もんをたてていたのでした。そして、そこらにひざの黒い服ふくなりいろの方へ押おしてくびをたてていしょにし
。",
created_at: "????-??-?? ??:??:??",
updated_at: "????-??-?? ??:??:??",
},
App\Models\Chat {#6245
id: 2,
user_id: 1,
message: "んとうに眼めをみて、あすこ石炭袋せきのどくびにぶっきりんとうのはじめはどうしてしまっ黒
な盤ばんでなしくわくような色をしているようなんだか鼻はなれて来て、あたした。「鳥がおもしおまた、高く星め
ぐったりは、頂いたいよく知りませんです。そしてきて、まだひざまずいぶんいたのでしたもんで行くん、風のよう
に雑作ぞうさえきちっと続つづいてはいっぱさをあけて行かないんだから飛とび乗のりました。そらのようなんだよ
。",
...
},
],
}
> exit
INFO Goodbye.
~/php/example-app $
It seems to be working, so next up is some light testing.
Testing
I haven't mentioned this much so far, but on Termux, xdebug does not work, and you cannot debug. Therefore, test code is essential—and that's an understatement; it's quite critical. Furthermore, since it's Android, unless you have a PC, you (typically) cannot use developer tools in the web browser, which makes debugging even more difficult. The issue with developer tools is a common problem for all web app/service development on Termux (such as node.js).
First, create a boilerplate for a unit test class using the artisan command.
~/php/example-app $ php artisan make:test ChatTest --unit
(Added on 2025/6/7)
Note: This is a Feature test, so --unit is a mistake (I won't fix it because it's a hassle).
Here is the test code with the content filled in.
<?php
namespace Tests\Unit;
use Tests\TestCase;
use App\Models\Chat;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use PHPUnit\Framework\Attributes\Test;
use Illuminate\Foundation\Testing\WithFaker;
class ChatTest extends TestCase
{
use RefreshDatabase;
use WithFaker;
/**
* Verify that an anonymous chat can be created
*/
#[Test]
public function created(): void
{
$chat = Chat::factory()->state(['user_id' => null])->create();
$this->assertDatabaseHas('chats', [
'id' => $chat->id,
'user_id' => null,
'message' => $chat->message,
]);
$this->assertInstanceOf(Chat::class, $chat);
$this->assertNull($chat->user_id);
}
/**
* Verify that a chat with a user can be created
*/
#[Test]
public function created_with_a_user(): void
{
$user = User::factory()->create();
$chat = Chat::factory()->for($user)->create();
$this->assertDatabaseHas('chats', [
'id' => $chat->id,
'user_id' => $user->id,
'message' => $chat->message,
]);
$this->assertEquals($user->id, $chat->user->id);
$this->assertInstanceOf(User::class, $chat->user);
}
/**
* Verify that a message can be updated
*/
#[Test]
public function message_updated(): void
{
$chat = Chat::factory()->create();
$originalMessage = $chat->message;
$updatedMessage = 'Updated message for chat: ' . $this->faker->sentence();
$chat->update(['message' => $updatedMessage]);
$this->assertDatabaseHas('chats', [
'id' => $chat->id,
'message' => $updatedMessage,
]);
$this->assertEquals($updatedMessage, $chat->message);
$this->assertNotEquals($originalMessage, $chat->message);
}
/**
* Verify that it can be deleted
*/
#[Test]
public function deleted(): void
{
$chat = Chat::factory()->create();
$chat->delete();
$this->assertDatabaseMissing('chats', [
'id' => $chat->id,
]);
}
/**
* Verify cascade
*/
#[Test]
public function deleted_when_user_deleted(): void
{
$user = User::factory()->create();
$chat = Chat::factory()->for($user)->create();
$this->assertDatabaseHas('chats', ['id' => $chat->id]);
$user->delete();
// Verify cascade
$this->assertDatabaseMissing('chats', ['id' => $chat->id]);
}
}
The pre-existing test classes will also run simultaneously, but to execute all tests including the one we just created, run the following command:
~/php/example-app $ php artisan test
PASS Tests\Unit\ChatTest
✓ created 0.33s
✓ created with a user 0.03s
✓ message updated 0.02s
✓ deleted 0.02s
✓ deleted when user deleted 0.02s
PASS Tests\Unit\ExampleTest
✓ that true is true 0.33s
PASS Tests\Feature\Auth\AuthenticationTest
...
PASS Tests\Feature\Settings\ProfileUpdateTest
...
✓ user can delete their account 0.03s
✓ correct password must be provided to delete account 0.03s
Tests: 32 passed (76 assertions)
Duration: 3.49s
~/php/example-app $
Since xdebug doesn't work on Termux, it's not possible to generate coverage reports, and this is about as far as we can go with testing.
Discussion