Let’s create our chat controller for getting chats and sending messages with validation rules.
Before creating the real-time chat with WebSockets, we should create some functions for storing chat data and retrieving these data in addition to validating these data for securing our chat app.
Course content
Chat Tables
Let’s create three tables for chatting. The first table is for chat and the second one is for storing the messages and the third is the pivot table between the chats and the users for storing the chat participants
Now, Let’s create the chats table migration and model.
php artisan make:model Chat -m
<?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->bigIncrements('id');
$table->boolean('private')->default(true);
$table->boolean('direct_message')->default(false);
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('chats');
}
};
Let’s explain
- The
private
column is for a public chat or private - The
direct_message
column is for saving the message in the database or not
Let’s create the ChatMessages
table migration and model.
php artisan make:model ChatMessages -m
<?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('chat_messages', function (Blueprint $table) {
$table->bigIncrements('id');
$table->text('message');
$table->bigInteger('chat_id')->unsigned();
$table->bigInteger('user_id')->unsigned()->nullable();
$table->string('type')->default('text');
$table->text('data')->nullable();
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('chat_messages');
}
};
message
the column is for the message body.- chat_id the column is the foreigner key for the chats table and the user_id for the users’ table
- the message can be text, image, URL, etc so we add the
type
column. - the data column for any metadata for the message to be stored as JSON in the database. here we will save “seen by” and the message’s status
Finally, let’s create the chat_user table.
php artisan make:migration chat_user
<?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('chat_user', function (Blueprint $table) {
$table->bigIncrements('id');
$table->bigInteger('chat_id')->unsigned();
$table->bigInteger('user_id')->unsigned();
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('chat_user');
}
};
Chat Tables Relationships
Let’s define the relationships between models. We will start with the User Model. One user can have many chats and one chat can have many users so we will define it as many-to-many relationships with a pivot table.
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
class User extends Authenticatable
{
//
public function chats(): BelongsToMany
{
return $this->belongsToMany(Chat::class);
}
}
That’s all we need for the User model but we have a lot to do for the Chat model app\Models\Chat.php
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
class Chat extends Model
{
use HasFactory;
protected $fillable = ['data', 'direct_message'];
protected $casts = [
'data' => 'array',
'direct_message' => 'boolean',
'private' => 'boolean',
];
public function participants(): BelongsToMany
{
return $this->belongsToMany(User::class);
}
public function messages(): HasMany
{
return $this->hasMany(ChatMessages::class);
}
public function isParticipant($user_id)
{
$data = $this->participants->where('id', $user_id)->first();
if(!empty($data) ){
return true;
}
return false;
}
public function makePrivate($isPrivate = true)
{
$this->private = $isPrivate;
$this->save();
return $this;
}
}
We have defined the relationship with users in participants()
function, to get all conversation participants for a specific chat. We can get all the chat messages via messages()
.
We have created two more functions, isParticipant()
to check if the user is a participant in a chat, and makePrivate()
to make the chat private or public.
Finally, the ChatMessages
model app\Models\ChatMessages.php
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class ChatMessages extends Model
{
use HasFactory;
protected $fillable = ['message', 'chat_id','user_id','type','data'];
public function chat(): BelongsTo
{
return $this->belongsTo(Chat::class);
}
public function sender(): BelongsTo
{
return $this->belongsTo(User::class, 'user_id');
}
}
If we thought about the relationship of the chat message, we will find that the message belongs to one chat, so we created chat()
function and belongs to the user who sent it, so we made sender()
.
Chat Controller
This is the controller for everything about chat but first, we need to create the form requests that will be used for validation and the API resources for response.
Validation
Let’s code our request classes.
php artisan make:request Chat\CreateChatRequest
<?php
namespace App\Http\Requests\Chat;
use Illuminate\Foundation\Http\FormRequest;
class CreateChatRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\Rule|array|string>
*/
public function rules(): array
{
return [
'users' => ['required','array'],
'users.*' => ['sometimes','exists:users,id'],
'isPrivate' => ['required','boolean'],
];
}
}
As you can here when we create a new chat we will send the users an array of user ids and if the chat is private or public.
php artisan make:request Chat\SendTextMessageRequest
These are the validation rules for sending a chat message.
<?php
namespace App\Http\Requests\Chat;
use Illuminate\Foundation\Http\FormRequest;
class SendTextMessageRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\Rule|array|string>
*/
public function rules(): array
{
return [
'message' => ['required','max:500'],
'chat_id' => ['required'],
];
}
}
API Resources
With API resources we can transform our data and customize the attributes for a better response JSON
php artisan make:resource ChatResource
<?php
namespace App\Http\Resources;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
class ChatResource extends JsonResource
{
/**
* Transform the resource into an array.
*
* @return array<string, mixed>
*/
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'private' => $this->private,
'direct_message' => $this->direct_message,
'created_at' => $this->created_at,
'participants' => $this->participants,
];
}
}
We need to create another one for messages
php artisan make:resource MassageResource
<?php
namespace App\Http\Resources;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
class MassageResource extends JsonResource
{
/**
* Transform the resource into an array.
*
* @return array<string, mixed>
*/
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'message' => $this->message,
'chat_id' => $this->chat_id,
'user_id' => $this->user_id,
'data' => json_decode($this->data),
'created_at' => $this->created_at,
'sender' => $this->sender,
];
}
}
The controller
Wow, that’s a lot of work, It’s time for the chat controller
php artisan make:controller ChatController
We will import all the classes we will use in the chat controller to be like that.
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Models\User;
use App\Models\Chat;
use App\Http\Requests\Chat\CreateChatRequest;
use App\Http\Requests\Chat\SendTextMessageRequest;
use App\Models\ChatMessages;
use App\Events\ChatMessageSent;
use App\Events\ChatMessageStatus;
use App\Http\Resources\ChatResource;
use App\Http\Resources\MassageResource;
class ChatController extends Controller
{
}
After that let’s add the first function for creating a chat. The chat here will be between 2 users.
public function createChat(CreateChatRequest $request){
$users = $request->users;
// check if they had a chat before
$chat = $request->user()->chats()->whereHas('participants',function($q) use($users){
$q->where('user_id', $users[0]);
})->first();
//if not, create a new one
if(empty($chat)){
array_push( $users,$request->user()->id);
$chat = Chat::create()->makePrivate($request->isPrivate);
$chat->participants()->attach($users);
}
$success = true;
return response()->json( [
'chat' => new ChatResource($chat),
'success' =>$success
],200);
}
In this function, we check if the users had a chat before returning it instead of creating a new one.
Note that the users’ array $request->users
doesn’t include the authenticated user we add the user to it array_push( $users,$request->user()->id);
that way is safer to protect our app from hackers to create conversations for others.
Note, You can create a chat group but first remove the “check if they had a chat before” code.
Let’s get all conversations of one user
public function getChats(Request $request){
$user = $request->user();
$chats = $user->chats()->with('participants')->get();
$success = true;
return response()->json( [
'chats' => $chats,
'success' => $success
],200);
}
Let’s create the function for sending a message, keep in mind that we should check if the sender is a participant in this conversation or not, then we create a new message with the status sent
Note, the status can be sent, delivered, or seen.
Note, In this function, we will trigger an event via WebSocket for real-time notification in the next tutorial.
public function sendTextMessage(SendTextMessageRequest $request){
$chat = Chat::find($request->chat_id);
if($chat->isParticipant($request->user()->id)){
$message = ChatMessages::create([
'message' => $request->message,
'chat_id' => $request->chat_id,
'user_id' => $request->user()->id,
'data' => json_encode(['seenBy'=>[],'status'=>'sent']) //status = sent, delivered,seen
]);
$success = true;
$message = new MassageResource($message);
return response()->json( [
"message"=> $message,
"success"=> $success
],200);
}else{
return response()->json([
'message' => 'not found'
], 404);
}
}
When the users receive the message, they will send a request to change the message status. so we can add them to seenBy
array.
public function messageStatus(Request $request,ChatMessages $message){
if($message->chat->isParticipant($request->user()->id)){
$messageData = json_decode($message->data);
array_push($messageData->seenBy,$request->user()->id);
$messageData->seenBy = array_unique($messageData->seenBy);
//Check if all participant have seen or not
if(count($message->chat->participants)-1 < count( $messageData->seenBy)){
$messageData->status = 'delivered';
}else{
$messageData->status = 'seen';
}
$message->data = json_encode($messageData);
$message->save();
$message = new MassageResource($message);
return response()->json([
'message' => $message,
'success' => true
], 200);
}else{
return response()->json([
'message' => 'Not found',
'success' => false
], 404);
}
}
Note, in this function we will broadcast an event for changing the message status on the Frontend via WebSockets.
Now, let’s get a chat by id
public function getChatById(Chat $chat,Request $request){
if($chat->isParticipant($request->user()->id)){
$messages = $chat->messages()->with('sender')->orderBy('created_at','asc')->paginate('150');
return response()->json( [
'chat' => new ChatResource($chat),
'messages' => MassageResource::collection($messages)->response()->getData(true)
],200);
}else{
return response()->json([
'message' => 'not found'
], 404);
}
}
The last function we need is to search for a user by email so can start a chat with them, We will use like
for a quick search with suggestions and we will limit the result to 3 only.
public function searchUsers(Request $request){
$users = User::where('email','like',"%{$request->email}%")->limit(3)->get();
return response()->json( [
'users'=> $users ,
],200);
}
Finally the routes of our chat app routes\api.php
<?php
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
use App\Http\Controllers\Auth\RegisterController;
use App\Http\Controllers\Auth\LoginController;
use App\Http\Controllers\ChatController;
//auth
Route::post('register',[RegisterController::class, 'register']);
Route::post('login',[LoginController::class, 'login']);
Route::middleware('auth:sanctum')->get('/user', function (Request $request) {
return $request->user();
});
Route::middleware(['auth:sanctum'])->group(function () {
Route::get('/chat/get-chats',[ChatController::class, 'getChats']);
Route::post('/chat/create-chat',[ChatController::class, 'createChat']);
Route::get('/chat/get-chat-by-id/{chat}',[ChatController::class, 'getChatById']);
Route::post('/chat/send-text-message',[ChatController::class, 'sendTextMessage']);
Route::post('/chat/search-user',[ChatController::class, 'searchUsers']);
Route::get('/chat/message-status/{message}',[ChatController::class, 'messageStatus']);
});
That’s all,
The next tutorial is Laravel WebSockets Chat | Chat App 03