Laravel Chat RESTful API | Chat App 02

Laravel Chat API

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

Building Laravel & Vue.js Chat App 00 | Introduction
Laravel Login And Register RESTful API | Chat app 01
Laravel Chat RESTful API | Chat App 02 
Laravel WebSockets Chat | Chat App 03
Vue.js Chat App Frontend | Chat App 04
Vue 3 With Laravel Echo WebSocket | Chat App 05

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