Sign In
Sign In

Creating a Telegram Bot with Laravel 12

Creating a Telegram Bot with Laravel 12
Hostman Team
Technical writer
Laravel
24.09.2025
Reading time: 14 min

In this detailed guide, we will dive into the process of building our own Telegram bot using the power of PHP and the elegance of the Laravel framework. Thanks to its well-thought-out architecture, expressive syntax, and extensive ecosystem, Laravel is an ideal platform for building and later managing complex bots. We will go through the entire path together: from the very first step, registering a bot in Telegram, to deploying and writing its logic on a server running Ubuntu 24.04.

Why Laravel?

It is possible to write complex logic in “pure” PHP, but this is slow and not always efficient. This is where Laravel comes in: a PHP framework that provides a ready-made structure and a huge number of convenient development tools.

Laravel organizes your PHP code into an elegant and scalable system. It takes care of all the routine work:

  • Routing: easily directs an incoming request from Telegram to the correct part of the code (controller).
  • Working with the database: allows you to effortlessly save information about users, their messages, or settings.
  • Processing HTTP requests: provides convenient tools for sending response messages back to the Telegram API.
  • Queues and scheduled tasks: makes it possible to perform long operations (for example, mailings) in the background without slowing down bot responses.

Registering the Bot and Obtaining a Token

Before writing even a single line of code, we must perform a preparatory but very important action: register our future digital assistant in the Telegram ecosystem and obtain a unique access key, the API token.

  1. Launch your Telegram client. In the search bar, enter the username @BotFather and select the official management bot marked with the blue verification checkmark.

  2. Initiate a dialogue: start communicating with it by sending the command /newbot. This tells it that you intend to create a new bot.

  3. Choose a name and a unique username. BotFather will ask you to provide two names sequentially. The first is the public name that users will see (it can be anything). The second is the technical username, which must be absolutely unique within all of Telegram and must end with the suffix bot (for example, MySuperLaravelBot or TestProject_bot).

  4. Save the access token. As soon as you pick a unique username, BotFather will congratulate you on the successful creation of your bot and will send you a message containing the token. This token is a long random sequence of letters and numbers. It is your secret key for interacting with the Telegram Bot API. Copy it and save it in a secure, inaccessible place. Treat it like the password to a very important account.

Congratulations, you now have a bot in Telegram!

Installing the Required Software

Update system packages:

sudo apt update && sudo apt upgrade -y

To get access to the latest PHP packages, we need to add the repository maintained by Ondrej Sury. Use this command to add the repository:

sudo add-apt-repository ppa:ondrej/php
sudo apt update

Add the PPA for modern PHP versions, then install PHP 8.2 and the required extensions:

sudo apt install -y php8.2 php8.2-fpm php8.2-mbstring php8.2-xml php8.2-sqlite3 php8.2-curl php8.2-zip php8.2-intl php8.2-bcmath

Install Nginx and Composer:

sudo apt install -y nginx composer

Check the version with:

php -v

The output should look something like “PHP 8.2.x”.

Setting Up Laravel and the Telegraph SDK

Now that you have the token, you can proceed to set up the Laravel project. We will use the package Telegraph: defstudio/telegraph. It provides convenient artisan commands for registering bots and webhooks.

  1. Navigate to the websites folder:

cd /var/www
  1. Create a new Laravel project:

composer create-project laravel/laravel telegram-bot
  1. Go to the project folder:

cd telegram-bot
  1. Install the SDK via Composer:

composer require defstudio/telegraph:1.57.1
  1. For the package to work, Telegraph requires running its migrations to create the telegraph_bots and telegraph_chats tables, which will store bot data and associated chats. Run the migrations:

php artisan vendor:publish --tag="telegraph-migrations"  
php artisan migrate
  1. Publish the configuration file. This command will create the file config/telegraph.php where all your bot’s settings will be stored:

php artisan vendor:publish --tag="telegraph-config"
  1. Open the .env file in the root of your project:

nano .env
  1. Add the token you received from BotFather:

TELEGRAPH_BOT_TOKEN=your_token

Changing Bot Permissions and Privacy

Before our bot can send and, more importantly, receive messages in groups, we need to change some of its settings through @BotFather.

  1. Give the bot permission to join Telegram groups. Send the command /setjoingroups in the chat with @BotFather. It will ask you which bot you want to change the setting for. Select your bot from the list and click “Enable”.

  2. Disable privacy mode. This is a key step. By default, a bot added to a group can only see messages that start with the / symbol or are direct replies to its messages. To allow our bot to read all messages in the chat and respond to them, privacy mode must be disabled. Send the command /setprivacy to the same @BotFather, select the bot, and change its status to “Disabled”.

Registering the Bot in Telegraph

Now we need to “introduce” the Telegraph package to our bot and the chat where we will be sending messages.

To register a new bot in the application, Telegraph provides a convenient interactive wizard. Launch it:

php artisan telegraph:new-bot

This command starts a wizard that will guide you through the bot creation process. During the setup, you will have the option to:

  • create a new bot,
  • add the bot token,
  • enter the bot’s name (optional),
  • add a new chat with its ID,
  • configure a webhook for the bot.

This method allows you to quickly register a bot directly from the command line.

Registering a Chat in Telegraph

To send messages to a specific chat, Telegraph needs to know its unique identifier (chat_id).

  1. First, send any message to your bot.
  2. Then open the following link in your browser, substituting your token:
https://api.telegram.org/bot<YOUR_TOKEN>/getUpdates

You will see a JSON response, where you can easily find the chat object, and inside it, the id field. Copy this numeric value.

To make the chat known to Telegraph, it must be registered:

php artisan telegraph:new-chat

Enter the Telegram chat ID. Optionally, you can specify a name for the chat.

The chat is successfully added to the database.

Testing Message Sending

Once your bot and at least one chat are set up, you can send messages using the Telegraph package.

Sending a Simple Message

Open the file routes/web.php:

nano routes/web.php

Add the following code:

use DefStudio\Telegraph\Models\TelegraphChat;

Route::get('/send-telegram', function () {
  $chat = TelegraphChat::where('chat_id', 'your_chat_id')->first();
  $chat->message('Hello!')->send();
  return response()->json(['Message sent successfully'], 200);
});

Replace your_chat_id with your actual chat ID.

Run the server for testing:

php artisan serve

Now, when you go to http://127.0.0.1:8000/send-telegram, the bot should send a message to your Telegram chat.

You can test it with:

curl -I http://127.0.0.1:8000/send-telegram

Formatting Message Text

In Telegraph, you can send messages in Markdown and HTML formats:

$chat->markdown("*Hello! This message is in...*\n\n_Markdown!_")->send();
$chat->html("<strong>And this message</strong>\n\nHTML!")->send();

It is especially useful for high-load bots to send messages via Laravel’s queue system so as not to delay the execution of the main code. To organize interaction with Telegraph through the queue system, you can use the dispatch() method:

Telegraph::message('Hello')->dispatch('your_queue_name');

Sending a message without sound:

$chat->message("shhh 🤫, a silent message")->silent()->send();

The bot successfully sent messages.

For more detailed information about all Telegraph features, you can always check the documentation.

Setting Up an Nginx Web Server

The built-in server launched with php artisan serve works great for development, but for real-world use you need a proper web server.

On Ubuntu, Nginx traditionally uses /etc/nginx/sites-available and /etc/nginx/sites-enabled. This is where we’ll store the site configuration and manage enabling or disabling virtual hosts.

Open the Nginx configuration file for the project:

nano /etc/nginx/sites-available/telegram-bot

Insert the following:

server {
    listen 80 default_server;
    listen [::]:80 default_server;
    server_name _;

    root /var/www/telegram-bot/public;    # <-- if the project is elsewhere, replace the path
    index index.php index.html;

    location / {
        try_files $uri $uri/ /index.php?$query_string;
    }

    location ~* \.(jpg|jpeg|png|gif|ico|css|js|svg|woff|woff2|ttf|eot)$ {
        try_files $uri =404;
        access_log off;
        expires max;
    }

    location ~ \.php$ {
        include snippets/fastcgi-php.conf;
        fastcgi_split_path_info ^(.+\.php)(/.+)$;

        # on Ubuntu the socket is usually in /run/php/
        fastcgi_pass unix:/run/php/php8.2-fpm.sock;  # <-- check your PHP version
        fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
        include fastcgi_params;
    }

    location ~ /\.ht {
        deny all;
    }

    access_log /var/log/nginx/telegram-bot.access.log;
    error_log  /var/log/nginx/telegram-bot.error.log;
}

Carefully check that the paths in root and fastcgi_pass are correct. The root argument must point to the public folder of the project, and fastcgi_pass must point to the PHP socket.

Check that the socket exists:

ls -l /run/php/

You should see the file php8.2-fpm.sock.

Create a symlink:

sudo ln -s /etc/nginx/sites-available/telegram-bot /etc/nginx/sites-enabled/telegram-bot

By default, /etc/nginx/sites-enabled contains default, which also specifies default_server. Remove its symlink to avoid duplicates:

sudo rm /etc/nginx/sites-enabled/default

Test the configuration:

sudo nginx -t

If you see syntax is ok and test is successful, reload Nginx:

sudo systemctl reload nginx

Change the owner and group for the following directories so that Nginx can handle them properly:

chown -R www-data:www-data /var/www/telegram-bot/storage/framework/views/
chown -R www-data:www-data /var/www/telegram-bot/storage/logs/
chown -R www-data:www-data /var/www/telegram-bot/public/
chown -R www-data:www-data /var/www/telegram-bot/database/

Receiving Updates from Telegram in Laravel

You need to define how Telegram will deliver updates to your bot.

In production, webhooks are recommended. They let you receive updates (messages, commands, etc.) in real time through your app’s URL, without constantly polling the API. Telegram will send HTTP requests to the webhook you provide every time an event occurs with your bot, for example when a user sends a message.

Creating a Webhook Handler

Create a folder for handlers:

mkdir app/Handlers

Create a file CustomWebhookHandler.php inside Handlers:

nano app/Handlers/CustomWebhookHandler.php

Insert:

<?php

namespace App\Handlers;

use Illuminate\Support\Stringable;
use DefStudio\Telegraph\Handlers\WebhookHandler;

class CustomWebhookHandler extends WebhookHandler
{
    protected function handleChatMessage(Stringable $text): void
    {
        // the received message is sent back to the chat
        $this->chat->html("You wrote: $text")->send();
    }
}

Next, we need to tell Telegraph to use this handler for incoming requests.

Open the config:

nano config/telegraph.php

Find the webhook parameter and change it to:

'webhook' => [
    'handler' => App\Handlers\CustomWebhookHandler::class,
],

Now, every plain message sent to the bot in Telegram will be processed by the handleChatMessage method of CustomWebhookHandler. The bot will reply with the same message in chat.

By default, webhooks can only process requests from known chats, i.e., those stored in the database as TelegraphChat models. All others are rejected.

To allow processing messages from unknown chats, update config/telegraph.php like this:

'security' => [

    /*
     * if enabled, allows callback queries from unregistered chats
     */
    'allow_callback_queries_from_unknown_chats' => true,

    /*
     * if enabled, allows messages and commands from unregistered chats
     */
    'allow_messages_from_unknown_chats' => true,

    /*
     * if enabled, store unknown chats as new TelegraphChat models
     */
    'store_unknown_chats_in_db' => true,
],

Setting the Webhook

Because we’re working locally, we need to expose our Laravel server to the internet over HTTPS. Telegram does not support plain HTTP.

During development, you can use services like ngrok to create a temporary domain for your local server. By default, Laravel runs on port 8000. This is the simplest way to give Telegram a public HTTPS URL to your local Nginx.

In production, you should use a reliable VPS and your own domain.

Method 1: Using Your Own Domain (Recommended)

Switching to your own domain is an important step in running your bot in production. A domain gives stability and user trust. Unlike temporary tunnels, a domain doesn’t change as long as it is valid.

Buy a domain and create an A record pointing to your server’s IP.

Install a free SSL certificate with Certbot, which automatically updates the Nginx configuration:

sudo apt install certbot python3-certbot-nginx -y
sudo certbot --nginx -d yourdomain.com
sudo systemctl reload nginx

Update .env with the new link:

nano .env

Change:

APP_URL=https://yourdomain.com

Set the webhook:

php artisan telegraph:set-webhook

Now the bot is ready for production.

Method 2: Using Ngrok

Register an ngrok account and install ngrok on Linux. Run:

ngrok http 80

The terminal will display a public HTTPS URL. Copy the tunnel link.

Update .env with this link:

nano .env

Change:

APP_URL=https://your-tunnel-link

Set the webhook:

php artisan telegraph:set-webhook

Now the bot is ready to receive updates through ngrok.

Testing the Bot

Send any message to your bot in Telegram. If everything is set up correctly, it should reply immediately.

If you encounter errors, note the step where they occur. Check the logs in the terminal running the tunnel and local server. Laravel errors can be found in storage/logs/laravel.log.

Supporting Commands

Let’s teach the bot to react to specific commands such as /start, and also show interactive buttons.

To handle commands, define their logic inside CustomWebhookHandler.

Open:

nano app/Handlers/CustomWebhookHandler.php

Update it as follows:

<?php

namespace App\Handlers;

use DefStudio\Telegraph\Keyboard\Button;
use DefStudio\Telegraph\Keyboard\Keyboard;
use Illuminate\Support\Stringable;
use DefStudio\Telegraph\Handlers\WebhookHandler;

class CustomWebhookHandler extends WebhookHandler
{
    protected function handleChatMessage(Stringable $text): void
    {
        $this->chat->markdown("You wrote: $text")->send();
    }

    /**
     * @var string[] List of allowed commands
     */
    protected array $allowedCommands = [
        'start',
        'buttons',
        'chatid', // command from the base class
    ];

    /**
     * Parent method for filtering commands
     */
    protected function handleCommand(Stringable $text): void
    {
        [$command, $parameter] = $this->parseCommand($text);

        // if the command is not in our whitelist,
        // handle it as an unknown command
        if (!in_array($command, $this->allowedCommands)) {
            $this->handleUnknownCommand($text);
            return;
        }

        // otherwise call the parent command handler
        parent::handleCommand($text);
    }

    public function start(): void
    {
        $this->chat->markdown("*Hello!* I’m a bot running on Laravel!")->send();
    }

    public function buttons(): void
    {
        $this->chat->message('This is a message with buttons. Choose an action:')
            ->keyboard(Keyboard::make()->buttons([
                Button::make("🗑️ Delete message")->action("delete"),
                Button::make("📖 Share wisdom")->action("read"),
                Button::make("👀 Open link")->url('https://hostman.com/'),
            ])->chunk(2))->send();
    }

    /**
     * Handles pressing the button with action("delete")
     */
    public function delete(): void
    {
        $this->reply('Message deleted');
        $this->chat->deleteMessage($this->messageId)->send();
    }

    /**
     * Handles pressing the button with action("read")
     */
    public function read(): void
    {
        $wisdomText = "40% of performance problems are solved by code optimization. The remaining 60% are solved by moving to *Hostman*. Be wiser—start with the 60%.";

        $this->reply('Wisdom revealed!', true);
        $this->chat->edit($this->messageId)->markdown($wisdomText)->send();
    }
}

We added two commands:

  • /start displays a greeting message.
  • /buttons shows an interface with buttons that trigger different behaviors.

Pressing the buttons will trigger the corresponding methods (delete, read), while the button with a URL will simply open the link in the browser.

For example, testing the “Share wisdom” button should return the custom wisdom message.

Conclusion

In this guide, we demonstrated the full cycle of building a Telegram bot with the Laravel framework and the Telegraph package. You now have a working foundation that can:

  • Send messages,
  • Handle commands,
  • Display interactive keyboards.

These are the building blocks for creating functional bots capable of solving real-world tasks, from automating routine operations to establishing new communication channels with users.

Laravel
24.09.2025
Reading time: 14 min

Similar

Laravel

Developing an Admin Panel in Laravel

In this guide, we’ll show how to develop an admin panel for a small project using Laravel’s built-in tools. This process helps you better understand the framework’s structure, improve your Laravel skills, and gain control over every part of the application. To follow this guide, you will need: Basic knowledge of the PHP programming language Know how to use Composer for dependency management Know how to set up and prepare a local development environment We will use the following environment: Operating System: Ubuntu 22.04 PHP: 8.3.15 Laravel: 9.x Composer: 2.7.8 npm: 10.8.2 Node: 18.20.5 Preparing the Environment Creating a User Create a user named lara and add it to the sudo and www-data groups: adduser lara usermod -aG sudo lara usermod -aG www-data lara su - lara Installing PHP 8.3 and Extensions for Laravel Add the PHP PPA: sudo add-apt-repository ppa:ondrej/php -y sudo apt update Install PHP and extensions: sudo apt install php8.3 php8.3-cli php8.3-fpm php8.3-mysql php8.3-curl php8.3-gd php8.3-mbstring php8.3-bcmath php8.3-xml php8.3-zip php8.3-intl php8.3-soap php8.3-redis -y Installing Composer Update package list: sudo apt update Download the installer script: cd /usr/local/bin sudo curl -sS https://getcomposer.org/installer -o composer-setup.php Verify the file integrity: HASH=$(curl -sS https://composer.github.io/installer.sig) php -r "if (hash_file('sha384', 'composer-setup.php') === '$HASH') { echo 'Installer verified'; } else { echo 'Installer corrupt'; unlink('composer-setup.php'); } echo PHP_EOL;" Install Composer: sudo php composer-setup.php --install-dir=/usr/local/bin --filename=composer sudo composer self-update 2.7.8 Check the installation: composer --version Installing Node.js and npm Remove old versions: sudo apt purge nodejs -y sudo apt autoremove -y sudo apt update Install Node.js 18.x: curl -fsSL https://deb.nodesource.com/setup_18.x | sudo -E bash - sudo apt install -y nodejs Check versions: node -v npm -v Creating a Working Directory Create a folder for the project: cd /var sudo mkdir www sudo chown lara:www-data www sudo chmod 775 www cd www Installing and Configuring Nginx Install Nginx: sudo apt install -y nginx sudo ufw allow 'Nginx Full' Configure a virtual host: sudo nano /etc/nginx/sites-available/lara.com Add these directives: server { listen 80; server_name <server_IP_address_or_domain_name>; root /var/www/laravel-admin/public; index index.php index.html index.htm; location / { try_files $uri $uri/ /index.php?$query_string; } location ~ \.php$ { include snippets/fastcgi-php.conf; fastcgi_pass unix:/var/run/php/php8.3-fpm.sock; } location ~ /\.ht { deny all; } } Enable the configuration: sudo ln -s /etc/nginx/sites-available/lara.com /etc/nginx/sites-enabled/ sudo unlink /etc/nginx/sites-enabled/default Restart the services: sudo systemctl restart nginx sudo systemctl restart php8.3-fpm Installing Laravel Create a project: composer create-project laravel/laravel:^9.0 laravel-admin Configure permissions: sudo find /var/www/laravel-admin -type f -exec chmod 644 {} \; sudo find /var/www/laravel-admin -type d -exec chmod 755 {} \; cd laravel-admin/ sudo chown -R lara:www-data . sudo find . -type f -exec chmod 664 {} \; sudo find . -type d -exec chmod 775 {} \; sudo chgrp -R www-data storage bootstrap/cache sudo chmod -R ug+rwx storage bootstrap/cache Installing and Configuring SQLite Install the SQLite extension: sudo apt install php8.3-sqlite3 Set up the connection. In the .env file, modify these lines: DB_CONNECTION=sqlite DB_DATABASE=/var/www/laravel-admin/database/database.sqlite Create a database: touch database/database.sqlite Change the owner and group for the database.sqlite file: sudo chown lara:www-data /var/www/laravel-admin/database/database.sqlite Run migrations: php artisan migrate The php artisan command must be executed from the project root: /var/www/laravel-admin. Setting Up a Provider Providers are an important part of Laravel. They are special classes used to configure and initialize application components. They allow you to define how different elements of the system should be organized and linked. Laravel automatically loads the providers listed in the config/app.php file, which ensures flexibility and modularity in managing the application. What does RouteServiceProvider do? RouteServiceProvider is a system provider responsible for routing in the application. Its tasks include: Loading route files for the web application, API, and other parts of the system. Binding models to routes. Configuring namespaces for controllers. This provider helps organize routes by splitting them into groups (for example, for the admin panel and the user-facing part) and defining common rules for the routes, such as middleware, prefixes, or constraints. Configuring RouteServiceProvider The RouteServiceProvider.php file is located in the app/Providers folder. We will change the value of the HOME constant and edit the boot method: class RouteServiceProvider extends ServiceProvider { public const HOME = '/dashboard'; public function boot() { $this->configureRateLimiting(); $this->routes(function () { Route::middleware('api') ->prefix('api') ->group(base_path('routes/api.php')); Route::middleware('web') ->group(base_path('routes/web.php')); Route::middleware(['web', 'auth']) ->namespace('App\Http\Controllers\Dashboard') ->prefix('dashboard') ->group(base_path('routes/dashboard.php')); }); } // Remaining provider code } Main Changes and Settings HOME property: Defines the redirect route for authenticated users after successful login. In this case, users will be redirected to /dashboard. This is convenient for centralized redirect management: if the route changes, it’s enough to update only the constant value. public const HOME = '/dashboard'; boot method: The boot method is where the main routing configuration takes place. Here, the routes method is used, which allows defining routing rules for different parts of the application. Routes for the Web Application Route::middleware('web')     ->group(base_path('routes/web.php')); Client-side routes are included from the routes/web.php file. They only use the web middleware, which provides support for sessions, CSRF protection, and other web features. Routes for the Admin Panel Route::middleware(['web', 'auth'])     ->namespace('App\Http\Controllers\Dashboard')     ->prefix('dashboard')     ->group(base_path('routes/dashboard.php')); For the admin panel routes, the following are added: web and auth middleware, restricting access to authenticated users only. Namespace: App\Http\Controllers\Dashboard. Prefix dashboard, making them accessible via URLs like /dashboard/.... Route file location: routes/dashboard.php. Why is this Important? This structure makes the application modular and scalable: Splitting routes into separate files simplifies management. Namespaces allow different controllers to be used for different parts of the application. Adding middleware and prefixes at the route group level improves security and simplifies configuration. Updating Routes Create a file routes/dashboard.php and add the following route: use Illuminate\Support\Facades\Route; Route::get('/', function () { return view('dashboard.app'); })->name('dashboard.home'); Route description Route::get('/'): This route responds to GET requests at the /dashboard address (since the RouteServiceProvider sets the prefix dashboard for the admin panel). Callback function: The callback returns the view dashboard.app. This is the base template of the admin panel, created earlier, and inherits its structure from master.blade.php. Route naming: ->name('dashboard.home'). Naming allows you to reference the route by dashboard.home in templates, controllers, or when generating links. Example in a Blade template: <a href="{{ route('dashboard.home') }}">Home</a> Purpose of the route: This is the main page of the admin panel. This route is used to render the base admin panel template, which can then be customized and filled with the necessary content. Installing laravel/ui To use standard frontend styles and authentication in Laravel, you need to install the laravel/ui package. This package provides basic components such as authentication, login and registration pages, and also allows the use of frontend frameworks such as Bootstrap or Vue.js. Installing the laravel/ui package Install the package via Composer: composer require laravel/ui After installing the package, run the command to generate a frontend skeleton using Bootstrap: php artisan ui bootstrap This command must be run from the project root: /var/www/laravel-admin. It will create the necessary files to integrate Bootstrap into the project, including templates and styles. Setting up Authentication Once you have installed and set up laravel/ui, you can generate standard authentication pages (login and registration). Run the command: php artisan ui bootstrap --auth Agree to recreate the Controller.php file. This command will create the necessary controllers, routes, and views for authentication: Controllers for user registration and login. Views for login, registration, and password reset pages. Routes that provide access to these pages. Installing Dependencies Install the required JavaScript dependencies and compile resources using npm: npm install && npm run build If the installation fails, run the command again. Removing Unnecessary Files and Routes After installing the laravel/ui package and configuring authentication, Laravel automatically adds some files and routes that are not needed when developing an admin panel. To clean them up, do the following: HomeController The default HomeController.php is often unused since its functionality is replaced by your own controllers. To delete it: rm app/Http/Controllers/HomeController.php home Route in routes/web.php Laravel automatically creates a home route: Route::get('/home', [App\Http\Controllers\HomeController::class, 'index'])->name('home'); You can remove it from routes/web.php, since the authentication redirect route is now defined by the HOME constant in RouteServiceProvider. Updating the $redirectTo Property In the controllers responsible for authentication and authorization, replace the redirect path after successful login/registration with the constant RouteServiceProvider::HOME. Find and open the controllers located in app/Http/Controllers/Auth/: ConfirmPasswordController.php LoginController.php RegisterController.php ResetPasswordController.php VerificationController.php Import the namespace in each controller: use App\Providers\RouteServiceProvider; Then update the $redirectTo property. By default, Laravel sets it to /home: protected $redirectTo = '/home'; Change it to: protected $redirectTo = RouteServiceProvider::HOME; Example for LoginController.php: namespace App\Http\Controllers\Auth; use App\Http\Controllers\Controller; use App\Providers\RouteServiceProvider; use Illuminate\Foundation\Auth\AuthenticatesUsers; class LoginController extends Controller { use AuthenticatesUsers; protected $redirectTo = RouteServiceProvider::HOME; // Rest of the controller code } Do the same in all other controllers where $redirectTo is defined. Viewing the Pages After completing the steps above, test the login and registration pages: Clear the application cache: php artisan config:clear Run the php artisan command from the project root: /var/www/laravel-admin. Open a browser and go to: http://<Server_Public_IP>/login to access the login page. http://<Server_Public_IP>/register to register a new user. Now, basic authentication in your Laravel project is ready. You can use it as a foundation for further customization and expansion. Creating the File and Folder Structure for the Admin Panel Create a folder for the admin panel templates: mkdir -p resources/views/dashboard/layouts Creating a Base Template Create the base template resources/views/dashboard/layouts/master.blade.php: <!doctype html> <html lang="{{ str_replace('_', '-', app()->getLocale()) }}"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="csrf-token" content="{{ csrf_token() }}"> <title>Dashboard</title> <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet"> @stack('style') </head> <body class="container-fluid" style="height: 100%; margin: 0;"> <div class="wrapper row" style="height: 100%;"> <nav id="sidebar" class="p-3 bg-dark d-flex flex-column col-2" style="height: 100vh;"> @include('dashboard.layouts.sidebar') </nav> <div id="content" class="col" style="height: 100vh; overflow-y: auto;"> @include('dashboard.layouts.nav') <div class="container-fluid mt-4 mb-4"> @if(Session::has('global')) <div class="alert alert-success"> {{ Session::get('global') }} </div> @endif @yield('content') </div> </div> </div> <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script> @stack('scripts') </body> </html> Key elements of the template: {{ csrf_token() }}: Generates a unique token to protect against CSRF attacks. @stack('style') and @stack('scripts'): Allow adding styles and scripts from child templates via @push. @include('dashboard.layouts.nav'): Includes the Blade template with the top navigation. <script src="https://cdn.jsdelivr.net/..."></script>: Connects Bootstrap via CDN for quick use of ready-made frontend styles and components. {{ Session::get('global') }}: Displays notifications stored in the global session variable. Creating Supporting Templates Sidebar menu: resources/views/dashboard/layouts/sidebar.blade.php: <ul class="nav flex-column"> <li class="nav-item"> <a class="nav-link text-white" href="{{ route('dashboard.home') }}">Home</a> </li> </ul> Top navigation: resources/views/dashboard/layouts/nav.blade.php: <nav class="navbar navbar-expand-lg navbar-light bg-light">     <div class="container-fluid">         <span class="navbar-brand">Dashboard</span>     </div> </nav> Creating the Main Page Template Create resources/views/dashboard/app.blade.php: @extends('dashboard.layouts.master') @section('content')     <p>How to Create an Admin Panel in Laravel: Step-by-Step Guide</p> @endsection This file extends the base template master.blade.php and defines unique content for each page. After completing all the previous steps, do the following to test authentication: Clear caches and temporary data from the project root: php artisan optimize:clear Register your first user via the route: http://<Server_Public_IP>/register Then, you will be able to log in. Preparing the Post Model Creating the Model and Migration Files Create the Post model file along with its migration file (using the -m flag) with the command: php artisan make:model Post -m Laravel will automatically create two files: Model file app/Models/Post.php: namespace App\Models; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; class Post extends Model { use HasFactory; } Migration file database/migrations/YYYY_MM_DD_create_posts_table.php: use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; return new class extends Migration { public function up() { Schema::create('posts', function (Blueprint $table) { $table->id(); $table->timestamps(); }); } public function down() { Schema::dropIfExists('posts'); } }; Editing the Migration File Add title and content columns inside the migration file database/migrations/YYYY_MM_DD_create_posts_table.php: $table->string('title'); $table->text('content'); Final version of the migration file: return new class extends Migration { public function up() { Schema::create('posts', function (Blueprint $table) { $table->id(); $table->string('title'); $table->text('content'); $table->timestamps(); }); } public function down() { Schema::dropIfExists('posts'); } }; Run the migration to create the table in the database: php artisan migrate Editing the Post Model In the app/Models/Post.php file, add the following property: protected $fillable = ['title', 'content']; Final version of the Post model: class Post extends Model { use HasFactory; protected $fillable = ['title', 'content']; } $fillable lists the fields that can be mass-assigned, e.g., via Post::create(). HasFactory allows using factories for testing. Creating a Resource Controller Generate a controller for the Post model with: php artisan make:controller Dashboard/PostController -r --model=Post Laravel will automatically create the file app/Http/Controllers/Dashboard/PostController.php: namespace App\Http\Controllers\Dashboard; use App\Http\Controllers\Controller; use App\Models\Post; use Illuminate\Http\Request; class PostController extends Controller { public function index() { // } public function create() { // } public function store(Request $request) { // } public function show(Post $post) { // } public function edit(Post $post) { // } public function update(Request $request, Post $post) { // } public function destroy(Post $post) { // } } What is a Resource Controller? A resource controller in Laravel is a special type of controller that automatically generates empty methods for standard CRUD operations (Create, Read, Update, Delete) on a model. It simplifies the process of creating routes and controllers for managing resources. The methods follow a RESTful approach and include: index: display a list of records create: show the form to create a new record store: save a new record in the database edit: show the form to edit a record update: update a record in the database destroy: delete a record Updating the Controller Code Remove the show method and update the remaining methods in app/Http/Controllers/Dashboard/PostController.php: class PostController extends Controller { public function index() { $posts = Post::latest()->paginate(10); return view('dashboard.posts.index', compact('posts')); } public function create() { return view('dashboard.posts.create'); } public function store(Request $request) { $request->validate([ 'title' => 'required|string|max:255', 'content' => 'required', ]); Post::create($request->only('title', 'content')); return redirect()->route('posts.index')->with('global', 'Post created successfully.'); } public function edit(Post $post) { return view('dashboard.posts.edit', compact('post')); } public function update(Request $request, Post $post) { $request->validate([ 'title' => 'required|string|max:255', 'content' => 'required', ]); $post->update($request->only('title', 'content')); return redirect()->route('posts.index')->with('global', 'Post updated successfully.'); } public function destroy(Post $post) { $post->delete(); return redirect()->route('posts.index')->with('global', 'Post deleted successfully.'); } } Adding Routes In routes/dashboard.php, add the resource controller route: use App\Http\Controllers\Dashboard\PostController; Route::resource('posts', PostController::class); This route will automatically create access to all resource controller methods (index, create, store, etc.). Adding a Sidebar Link In resources/views/dashboard/layouts/sidebar.blade.php, add a link to manage posts: <li class="nav-item"> <a class="nav-link text-white" href="{{ route('posts.index') }}">Posts</a> </li> Final version of sidebar.blade.php: <ul class="nav flex-column"> <li class="nav-item"> <a class="nav-link text-white" href="{{ route('dashboard.home') }}">Home</a> </li> <li class="nav-item"> <a class="nav-link text-white" href="{{ route('posts.index') }}">Posts</a> </li> </ul> Explanation of {{ route('posts.index') }} When you define a resource route: Route::resource('posts', PostController::class); Laravel automatically generates named routes for the controller methods: posts.index: display the list of posts (index method). posts.create: show the form to create a post (create method). posts.store: save a new post (store method). posts.edit: show the form to edit a post (edit method). posts.update: update a post (update method). posts.destroy: delete a post (destroy method). Now, the admin panel menu will include a “Posts” item that leads to the posts management page. Request Class: Creation and Usage The Request class in Laravel is responsible for handling incoming HTTP requests. It provides: Centralized validation: Validation rules are defined in one place, improving readability and maintainability. Security: Ensures that only validated data reaches the controller. Logical isolation: Removes extra logic from controllers, making the code cleaner and easier to understand. What does the Request class do? Data validation: You define validation rules directly in the Request class (e.g., requiring that the title field is mandatory and of a certain length). Authorization: You can specify who is allowed to perform the request. If the authorize() method returns false, the request will be rejected with status 403. Clean data: The validated() method returns only the data that passed validation. This prevents unwanted input from being stored or used in your application logic. Logic isolation: All rules related to the request are isolated in the Request class, reducing controller complexity. Creating a Custom Request Class To create a custom Request class for the Post model, run: php artisan make:request PostRequest Laravel will create the file app/Http/Requests/PostRequest.php: namespace App\Http\Requests; use Illuminate\Foundation\Http\FormRequest; class PostRequest extends FormRequest { public function authorize() { return false; } public function rules() { return [ // ]; } } Update the authorize and rules methods: class PostRequest extends FormRequest { /** * Determines if the user is authorized to make this request. */ public function authorize() { return true; // Replace with actual check if authorization is required. } /** * Defines validation rules for the request data. */ public function rules() { return [ 'title' => 'required|string|max:255', 'content' => 'required', ]; } /** * Custom validation error messages. */ public function messages() { return [ 'title.required' => 'The "Title" field is required.', 'content.required' => 'The "Content" field is required.', ]; } } Sometimes it’s useful to create two different request classes for create and update operations: PostStoreRequest for validating data when creating a post. PostUpdateRequest for validating data when updating a post. This allows you to apply different validation rules. For example: When creating a record, the title field might be required. When updating, the title could be optional if it isn’t being changed. Using the PostRequest in the Controller In app/Http/Controllers/Dashboard/PostController.php: Import the namespace: use App\Http\Requests\PostRequest; Update the store and update methods: namespace App\Http\Controllers\Dashboard; use App\Http\Controllers\Controller; use App\Models\Post; use App\Http\Requests\PostRequest; class PostController extends Controller { // Other methods... public function store(PostRequest $request) { Post::create($request->validated()); return redirect()->route('posts.index')->with('global', 'Post created successfully.'); } public function update(PostRequest $request, Post $post) { $post->update($request->validated()); return redirect()->route('posts.index')->with('global', 'Post updated successfully.'); } // Other methods... } The $request->validated() method returns only the fields that passed validation. If validation fails, Laravel automatically halts execution and returns an error (e.g., if title is empty when it’s required). If validation succeeds, validated() returns a clean array of safe data that you can use for creating or updating a record. This ensures your application never processes invalid or unsafe data. Creating Blade Templates 1. Create a Directory for Post Views: mkdir -p resources/views/dashboard/posts 2. Index View resources/views/dashboard/posts/index.blade.php: @extends('dashboard.layouts.master') @section('content') <div class="d-flex justify-content-between align-items-center"> <h1>Posts</h1> <a href="{{ route('posts.create') }}" class="btn btn-primary">Create Post</a> </div> <table class="table mt-4"> <thead> <tr> <th>#</th> <th>Title</th> <th>Actions</th> </tr> </thead> <tbody> @foreach($posts as $post) <tr> <td>{{ $post->id }}</td> <td>{{ $post->title }}</td> <td> <a href="{{ route('posts.edit', $post) }}" class="btn btn-warning btn-sm">Edit</a> <form action="{{ route('posts.destroy', $post) }}" method="POST" class="d-inline-block"> @csrf @method('DELETE') <button class="btn btn-danger btn-sm" onclick="return confirm('Are you sure?')">Delete</button> </form> </td> </tr> @endforeach </tbody> </table> {{ $posts->links() }} @endsection 3. Create View resources/views/dashboard/posts/create.blade.php: @extends('dashboard.layouts.master') @section('content') <h1>Create Post</h1> <form action="{{ route('posts.store') }}" method="POST"> @csrf <div class="mb-3"> <label for="title" class="form-label">Title</label> <input type="text" class="form-control" id="title" name="title" value="{{ old('title') }}"> @error('title') <p class="text-danger">{{ $message }}</p> @enderror </div> <div class="mb-3"> <label for="content" class="form-label">Content</label> <textarea class="form-control" id="content" name="content" rows="5">{{ old('content') }}</textarea> @error('content') <p class="text-danger">{{ $message }}</p> @enderror </div> <button type="submit" class="btn btn-success">Save</button> </form> @endsection 4. Edit View resources/views/dashboard/posts/edit.blade.php: @extends('dashboard.layouts.master') @section('content') <h1>Edit Post</h1> <form action="{{ route('posts.update', $post) }}" method="POST"> @csrf @method('PUT') <div class="mb-3"> <label for="title" class="form-label">Title</label> <input type="text" class="form-control" id="title" name="title" value="{{ old('title', $post->title) }}"> @error('title') <p class="text-danger">{{ $message }}</p> @enderror </div> <div class="mb-3"> <label for="content" class="form-label">Content</label> <textarea class="form-control" id="content" name="content" rows="5">{{ old('content', $post->content) }}</textarea> @error('content') <p class="text-danger">{{ $message }}</p> @enderror </div> <button type="submit" class="btn btn-success">Update</button> </form> @endsection Blade elements: @extends: Used to inherit a base template (e.g., master.blade.php), which defines the common layout (header, menu, styles, scripts). @section: Defines the content that will be injected into the @yield sections of the base template. {{ old('title') }}: Retrieves the previously entered form value if validation fails and the page reloads. @error: Displays validation error messages for a specific field. After creating these templates: Try adding new posts via the Create Post form. Edit them with the Edit Post form. Check that validation errors display correctly. Displaying Posts on the Client Side Create a controller for handling client-side requests: php artisan make:controller PostController In app/Http/Controllers/PostController.php: namespace App\Http\Controllers; use App\Models\Post; use Illuminate\Http\Request; class PostController extends Controller { public function show() { $posts = Post::latest()->paginate(10); return view('posts.index', compact('posts')); } } Adding the Route In routes/web.php: use App\Http\Controllers\PostController; Route::get('/', [PostController::class, 'show'])->name('posts.index'); This replaces the default welcome route: // Old Route::get('/', function () {     return view('welcome'); }); Client-side Blade Template Create the posts directory: mkdir -p resources/views/posts Create resources/views/posts/index.blade.php: @extends('layouts.app') @section('content') <div class="container mt-4"> <h1>Posts</h1> @foreach($posts as $post) <div class="card mb-3"> <div class="card-body"> <h5 class="card-title">{{ $post->title }}</h5> <p class="card-text">{{ $post->content }}</p> </div> </div> @endforeach <div class="mt-4"> {{ $posts->links() }} </div> </div> @endsection Now, the homepage (http://localhost:8000/) will display the list of posts with titles and content. Conclusion You have built a basic admin panel and client-side frontend in Laravel! Your project now includes: Admin panel: Manage posts via a CRUD interface. Client-side: Display posts to users. Flexibility & scalability: Custom Request classes and models. Future-ready: You can easily extend the app with categories, tags, search, and filters.  
22 September 2025 · 25 min to read
Laravel

Migrations and Seeders in Laravel: Database Setup

Migrations and seeders are two tools in the PHP framework Laravel that simplify working with databases and solve issues related to inconsistency. In Laravel, migrations function similarly to version control for databases. They allow the development team to define and share a common database schema, ensuring all team members are aware of any changes. Seeders in Laravel are tools used to populate the database with test data. There are several seeder classes available that allow for control over the seeding process. In this article, you will learn how to get started with Laravel and create migrations and seeders. Installing and Setting Up Laravel In this tutorial, we'll use Docker to run a Laravel application. Laravel provides a built-in command-line interface called Laravel Sail for Docker, which is in the Laravel package by default. We will install all necessary software on Windows 10. This guide consolidates instructions from various sources, including Microsoft, Docker, and Laravel. First, we will install WSL (Windows Subsystem for Linux), then Docker, and finally Laravel. The instructions below are specifically for Windows 10. For other operating systems, refer to the official Laravel documentation. Installing WSL WSL (Windows Subsystem for Linux) is a compatibility layer for running Linux applications on Windows. If you have Windows 10 version 2004 or later, you can install WSL by running this command in PowerShell as an administrator: wsl --install By default, the Ubuntu distribution will be used. We need WSL 2, so let's switch to this version. This process will take a few minutes: wsl --set-version Ubuntu 2 To check the version, use the command: wsl.exe -l -v Example output: PS C:\WINDOWS\system32> wsl.exe -l -v  NAME      STATE           VERSION* Ubuntu    Stopped         2 To check your Windows version, press Win+R, and type winver. A window will appear with your system information. If your version is lower than 2004, the wsl --install command will not work. Here's a list of steps for older versions (you'll need at least version 1903 with build 18362): Enable the Windows Subsystem for Linux: Open PowerShell as an administrator and run: dism.exe /online /enable-feature /featurename:Microsoft-Windows-Subsystem-Linux /all /norestart Enable the Virtual Machine feature: Run this command in the same PowerShell window: dism.exe /online /enable-feature /featurename:VirtualMachinePlatform /all /norestart Restart your computer to complete the installation. Download and install the Linux kernel update package from this link. Set WSL 2 as the default version: After rebooting, open PowerShell again as an administrator and run: wsl --set-default-version 2 Install a Linux distribution: Choose your preferred Linux distribution, download it from the Microsoft Store, and install it. After installation, you can access your Linux terminal from the search menu. Installing Docker Go to docker.com, click Get Started, and download Docker Desktop for Windows. During the installation process, make sure to select the option Use WSL 2 instead of HYPER-V. Open Docker, click the gear icon on the top panel to enter the settings menu. In the General tab, you can adjust the basic settings. Here, you can enable WSL 2 and disable unnecessary features like telemetry. After setting these options, move to the Resources tab and enable integration with Ubuntu. After this, click Apply & Restart. Docker will reboot with the new settings. Setting up Laravel Let's create a new Laravel application. Open the Ubuntu terminal and run the following command: curl -s https://laravel.build/example-app | bash Here, example-app is the directory for the new Laravel application. On the first run, the command will download the necessary images, which may take some time. Once the download completes, Laravel Sail will be available. Navigate to the project directory: cd example-app Next, create Laravel Sail containers: ./vendor/bin/sail up The container creation process may take a while, but subsequent Sail launches will be faster. Sail will start the Laravel web application with informational resources that are accessible on localhost. If you encounter the error: The Stream Or File “/Var/Www/Html/Storage/Logs/Laravel.Log” Could Not Be Opened In Append Mode: Failed To Open Stream: Permission Denied You need to change the file permissions for docker.sock using this command: sudo chmod 666 /var/run/docker.sock Working with Migrations As mentioned earlier, migrations are similar to version control. They allow users to create and modify the database schema or roll back changes. Migrations do not define the content of tables (except for constraints). In a Laravel project, migration files are stored in the directory at ./database/migrations/. When you create a Laravel Sail application, it comes with several migrations by default. Let's run these migrations and check the results. Open the Ubuntu terminal and enter the following commands: ./vendor/bin/sail up -d     // Start the application in the background./vendor/bin/sail artisan migrate Output: Migration table created successfully. Migrating: 2014_10_12_000000_create_users_table Migrated: 2014_10_12_000000_create_users_table (1,846.42ms) Migrating: 2014_10_12_100000_create_password_resets_table Migrated: 2014_10_12_100000_create_password_resets_table (1,965.53ms) Migrating: 2019_08_19_000000_create_failed_jobs_table Migrated: 2019_08_19_000000_create_failed_jobs_table (2,196.68ms) Migrating: 2019_12_14_000001_create_personal_access_tokens_table Migrated: 2019_12_14_000001_create_personal_access_tokens_table (3,325.95ms) Migrations in Laravel are executed in the order they are created. This allows Laravel to implement the rollback mechanism correctly. Running migrations in an ad hoc manner can lead to data loss in the database. Next, let's check whether the corresponding tables were created in the database. We can connect to the database using HeidiSQL. The connection parameters can be found in the environment configuration file .env: DB_CONNECTION=mysql DB_HOST=mysql DB_PORT=3306 DB_DATABASE=example_app DB_USERNAME=sail DB_PASSWORD=password You can also set different parameters here. Once connected to the database, you will see its current state. Now, let's create a new migration and see how it works. Creating Migrations First, let's create a migration class. To do this, execute the make:migration command in the Ubuntu terminal: ./vendor/bin/sail artisan make:migration example_migr Output: Created Migration: 2024_06_02_005033_example_migr The name of the migration consists of two parts: a timestamp and a name chosen by the user. The timestamp helps to run migrations in the correct order. Now, open the created file in any editor of your choice: <?php use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; return new class extends Migration { /** * Run the migration * * @return void */ public function up() { // } /** * Rollback the migration * * @return void */ public function down() { // } }; Migrations use two methods: up() and down(). In the up() method, you describe the database changes that need to be executed when the migration is run. The down() method undoes the changes made by up(). Now let's write the migration code to create a table called items with the following columns: id — primary key name — name of the item cost — price of the item count — quantity of the item in stock Here is the migration code for this table: <?php use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; return new class extends Migration { /** * Run the migration. * * @return void */ public function up() { Schema::create('items', function (Blueprint $table) { $table->id(); $table->string('name'); $table->integer('count'); $table->integer('cost'); }); } /** * Rollback the migration * * @return void */ public function down() { Schema::dropIfExists('items'); } }; Schema::create creates a new table. Schema::dropIfExists deletes the table if it exists. Save the changes to the file and run the migration again through the terminal: ./vendor/bin/sail artisan migrate Output: Migrating: 2024_06_02_005033_example_migrMigrated:  2024_06_02_005033_example_migr (658.56ms) Rolling Back Migrations There are several commands to rollback migrations: migrate:rollback — rolls back the last migrate command. You can also rollback a specific number of migrations using the --steps [number] parameter. migrate:reset — rolls back all migrations. migrate:refresh — rolls back all migrations and then runs migrate. migrate:fresh — deletes all tables from the database and then runs migrate. Seeders At this point, the database only contains empty tables. They can be filled using seeders. Seeders are necessary for populating the database with dummy or test data. You can create a seeder using the make:seeder command: ./vendor/bin/sail artisan make:seeder ExampleSeeder Output: Seeder created successfully. All seeders are located in the database/seeders directory. Navigate to it and open ExampleSeeder in your editor: <?php namespace Database\Seeders; use Illuminate\Database\Console\Seeds\WithoutModelEvents; use Illuminate\Database\Seeder; class ExampleSeeder extends Seeder { /** * Run the database seeds. * * @return void */ public function run() { // } } In the run() method, you need to place the code for generating data. Let's add 10 random records to the items table: <?php namespace Database\Seeders; use Illuminate\Database\Console\Seeds\WithoutModelEvents; use Illuminate\Support\Facades\DB; use Illuminate\Database\Seeder; use Illuminate\Support\Str; class ExampleSeeder extends Seeder { /** * Run the database seeds. * * @return void */ public function run() { for ($i = 1; $i <= 10; $i++) { DB::table('items')->insert([ 'id' => $i, 'name' => Str::random(5), 'cost' => rand(5, 20), 'count' => rand(0, 10), ]); } } } Using the insert() command, we add a record to the items table. However, new seeder classes do not run automatically. You need to add its call to the main DatabaseSeeder class: public function run(){    $this->call(ExampleSeeder::class);} After that, we can run the data seeding: ./vendor/bin/sail artisan db:seed Output: Seeding: Database\Seeders\ExampleSeederSeeded: Database\Seeders\ExampleSeeder (841.68ms)Database seeding completed successfully. And here is the result in the database: Conclusion We have explored tools in the Laravel framework, such as migrations and seeders, which simplify working with databases. You can always rent a virtual server for any experiments on Hostman.
16 October 2024 · 9 min to read

Do you have questions,
comments, or concerns?

Our professionals are available to assist you at any moment,
whether you need help or are just unsure of where to start.
Email us
Hostman's Support