Introduction to Laravel

PHP · Lesson 46 of 50 10 min read Updated

Laravel is the most popular PHP framework. A framework is a ready-made structure for web apps: routing, database access, templates, validation, logins, email and testing are already built, so you write only the parts special to your app. Laravel is free and open source, and was created by Taylor Otwell in 2011.

In this lesson you will install Laravel and rebuild the student list from earlier lessons with routes, a controller, Blade templates, a migration, an Eloquent model and a validated form. Because you already built a router and PDO queries by hand, you will recognise what Laravel does for you.

Versions and requirements

Laravel releases one major version a year. The current version is Laravel 13, released on 17 March 2026. It needs PHP 8.3 or newer and supports up to PHP 8.5. Each major version gets bug fixes for 18 months and security fixes for two years. Check the Laravel documentation for your version, because many online tutorials are written for older ones.

You need PHP, Composer (see Namespaces & Composer) and, for front-end build tools, Node.js with npm.

Creating a project

The plain Composer way works everywhere:

Terminal
composer create-project laravel/laravel school
cd school
php artisan serve

create-project downloads Laravel and about a hundred packages it depends on into school/vendor, copies .env.example to .env, generates a secret APP_KEY, creates an SQLite database file and runs the first migrations. php artisan serve starts a development server at http://127.0.0.1:8000, where you will see Laravel's welcome page.

The documentation also offers the Laravel installer: install it once with composer global require laravel/installer, then run laravel new school, which asks a few questions such as which starter kit to use. On Windows and macOS, the free Laravel Herd app installs PHP, Composer and the installer in one go. After npm install, composer run dev starts the web server, a queue worker and the Vite front-end server together.

A tour of the project

PathWhat lives there
routes/web.phpRoutes for web pages
app/Http/Controllers/Controllers
app/Models/Eloquent models (one class per table)
resources/views/Blade templates (*.blade.php)
database/migrations/Migrations: PHP files that create and change tables
public/index.phpThe front controller; the web server's document root is public/
.envSettings and secrets for this computer, never committed to Git
config/Configuration files that read values from .env
vendor/Composer packages, including Laravel itself

Notice the same ideas as in A Simple MVC Router: one front controller in public/, routes in one place, and separate folders for models, views and controllers.

By default a new project uses SQLite, a database stored in the single file database/database.sqlite, so it works with no database server. To use MySQL from XAMPP instead, create an empty database in phpMyAdmin and change these lines in .env, then run php artisan migrate:

Text
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=school
DB_USERNAME=root
DB_PASSWORD=

Artisan: Laravel's command line

artisan is a PHP script in the project root that runs Laravel's commands. You will use it all the time:

CommandDoes
php artisan serveStarts the development server
php artisan make:model Student -mCreates a model and a migration for it
php artisan make:controller StudentControllerCreates a controller
php artisan migrateRuns migrations that have not run yet
php artisan route:listLists every route
php artisan tinkerOpens an interactive PHP shell inside your app
php artisan --versionShows the Laravel version, such as Laravel Framework 13.39.0

Migrations and Eloquent models

Run php artisan make:model Student -m. It creates app/Models/Student.php and a migration file whose name starts with the date, such as database/migrations/2026_10_30_103551_create_students_table.php. A migration describes a table in PHP, so every developer and every server can build the same database with one command. Fill in the columns in its up() method:

PHP
public function up(): void
{
    Schema::create('students', function (Blueprint $table) {
        $table->id();                                  // BIGINT UNSIGNED AUTO_INCREMENT primary key
        $table->string('name', 100);
        $table->string('city', 60);
        $table->unsignedTinyInteger('marks');
        $table->timestamps();                          // created_at and updated_at
    });
}

Then run php artisan migrate, and Laravel creates the table. Its down() method, which drops the table, lets you undo the migration with php artisan migrate:rollback.

Eloquent is Laravel's ORM (Object-Relational Mapper): each table has a model class, and each row becomes an object. By convention the Student model uses the students table. To protect against mass assignment (a visitor sending extra fields such as is_admin), list the fields that create() may fill:

PHP
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Attributes\Fillable;
use Illuminate\Database\Eloquent\Model;

#[Fillable(['name', 'city', 'marks'])]
class Student extends Model
{
}

Now you can query without writing SQL. With four students saved, you can try this in php artisan tinker:

PHP
use App\Models\Student;

$toppers = Student::where('marks', '>=', 85)->orderByDesc('marks')->get();
foreach ($toppers as $s) {
    echo "{$s->name} ({$s->city}): {$s->marks}\n";
}
echo Student::count(), " students\n";

$aarav = Student::find(2);
$aarav->marks = 91;
$aarav->save();                     // UPDATE students SET marks = 91 ... WHERE id = 2
echo Student::find(2)->marks, "\n";

var_dump(Student::find(99));        // no such row
Output
Isha Gupta (Jaipur): 96
Meera Nair (Kochi): 93
Aarav Sharma (Rohtak): 88
4 students
91
NULL

Behind the scenes Eloquent writes prepared statements, just like your PDO code. Student::create([...]) inserts a row, $student->delete() deletes one, and findOrFail() throws an exception that Laravel turns into a 404 page.

Routes and controllers

Routes go in routes/web.php. Each one maps a method and a URL to a controller action, and name() gives it a name so you can build URLs with route('students.index') instead of typing paths:

PHP
<?php

use App\Http\Controllers\StudentController;
use Illuminate\Support\Facades\Route;

Route::get('/', function () {
    return view('welcome');
});

Route::get('/students', [StudentController::class, 'index'])->name('students.index');
Route::post('/students', [StudentController::class, 'store'])->name('students.store');
Route::get('/students/{student}', [StudentController::class, 'show'])->name('students.show');

The controller, created with php artisan make:controller StudentController, returns views and handles the form:

PHP
<?php

namespace App\Http\Controllers;

use App\Models\Student;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\View\View;

class StudentController extends Controller
{
    public function index(): View
    {
        return view('students.index', [
            'students' => Student::orderByDesc('marks')->get(),
        ]);
    }

    public function show(Student $student): View
    {
        return view('students.show', ['student' => $student]);
    }

    public function store(Request $request): RedirectResponse
    {
        $data = $request->validate([
            'name' => ['required', 'string', 'max:100'],
            'city' => ['required', 'string', 'max:60'],
            'marks' => ['required', 'integer', 'between:0,100'],
        ]);

        Student::create($data);

        return redirect()->route('students.index')->with('status', 'Student added.');
    }
}

In show(Student $student), the placeholder {student} matches a parameter typed with a model, so Laravel loads the row by its id. This is route model binding: /students/99 returns a 404 page if that student does not exist. In store(), if $request->validate() fails, Laravel redirects back to the form with the error messages and old input, and the rest of the method never runs. with('status', ...) stores a one-time flash message in the session.

Blade templates

Blade is Laravel's template language. Templates end in .blade.php and live in resources/views; view('students.index') means resources/views/students/index.blade.php. A shared layout is written as a component in resources/views/components/layout.blade.php:

HTML
@props(['title' => 'School'])
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <title>{{ $title }} | Rohtak Public School</title>
</head>
<body>
    <nav><a href="{{ route('students.index') }}">Students</a></nav>
    <main>
        {{ $slot }}
    </main>
</body>
</html>

The list page uses it as <x-layout>. Whatever is placed between the tags arrives in the layout as $slot:

HTML
<x-layout title="Students">
    <h1>Students</h1>

    @if (session('status'))
        <p>{{ session('status') }}</p>
    @endif

    <ul>
        @forelse ($students as $student)
            <li>
                <a href="{{ route('students.show', $student) }}">{{ $student->name }}</a>
                ({{ $student->city }}): {{ $student->marks }}
            </li>
        @empty
            <li>No students yet.</li>
        @endforelse
    </ul>

    <form method="post" action="{{ route('students.store') }}">
        @csrf
        <input name="name" value="{{ old('name') }}" placeholder="Name">
        <input name="city" value="{{ old('city') }}" placeholder="City">
        <input name="marks" type="number" value="{{ old('marks') }}" placeholder="Marks">
        <button>Add student</button>
        @error('marks')
            <p>{{ $message }}</p>
        @enderror
    </form>
</x-layout>

The important Blade rules:

  • {{ $value }} prints a value escaped with htmlspecialchars(), so a name such as <b>Rohan</b> shows as text. {!! $value !!} prints raw HTML; use it only for HTML you created yourself.
  • Directives starting with @ replace PHP control structures: @if, @foreach, and @forelse ... @empty ... @endforelse for lists that may be empty.
  • @csrf adds a hidden token field. Laravel checks it on every POST, which is the CSRF protection from PHP Security Essentials. A form without it gets the error page "419 Page Expired".
  • old('name') refills a field after a failed validation, and @error('marks') shows that field's message, here "The marks field must be between 0 and 100."

For /students/1, the students/show view (<x-layout :title="$student->name"> with a heading and one paragraph) produces this main section:

Output
<main>
    <h1>Meera Nair</h1>
<p>Kochi, 93 marks</p>
</main>

The colon in :title means "this attribute is a PHP expression", not plain text. Older projects build layouts with template inheritance instead of components: the page starts with @extends('layouts.app'), fills @section('content') ... @endsection, and the layout prints it with @yield('content'). Both styles still work.

What to learn next

You have used a small part of Laravel. The next topics are relationships between models (hasMany, belongsTo), authentication with a starter kit, form request classes, sending mail, queues and automated tests. Laravel's own documentation is detailed and well written. Keep your plain PHP knowledge: it is what lets you debug a framework when something goes wrong.

Common mistakes

  • Following a tutorial for an old version. Check which Laravel version a tutorial uses; folder layouts and defaults have changed.
  • Pointing the web server at the project root. The document root must be public/, or .env and your code could be downloaded.
  • Committing .env to Git. It holds passwords and the APP_KEY. Commit .env.example instead.
  • Forgetting @csrf in POST forms. The request fails with 419 Page Expired.
  • Using {!! !!} for user data. That brings back XSS; use {{ }}.

Key points

  • Laravel 13 (March 2026) is the current version and needs PHP 8.3 or newer.
  • Create a project with composer create-project laravel/laravel name or laravel new name, and start it with php artisan serve.
  • Routes in routes/web.php map URLs to controller actions; route model binding turns {student} into a Student object or a 404.
  • Migrations create tables in PHP; Eloquent models query them without SQL, with mass assignment limited by #[Fillable] or $fillable.
  • Blade's {{ }} escapes output, @csrf protects forms, and components such as <x-layout> share page structure.
  • php artisan creates files, runs migrations, lists routes and opens tinker.

Exercise

Add a page that lists the students of one city, such as /cities/Pune. Register a named route cities.show with a {city} placeholder, add a byCity(string $city) action to StudentController that loads the students of that city sorted by marks (highest first), and create the view resources/views/students/city.blade.php with the heading Students from Pune and a message when no student matches. Use the existing layout component.

Show solution
PHP
// routes/web.php
Route::get('/cities/{city}', [StudentController::class, 'byCity'])->name('cities.show');

// app/Http/Controllers/StudentController.php
public function byCity(string $city): View
{
    return view('students.city', [
        'city' => $city,
        'students' => Student::where('city', $city)->orderByDesc('marks')->get(),
    ]);
}
HTML
<x-layout :title="'Students from ' . $city">
    <h1>Students from {{ $city }}</h1>
    <ul>
        @forelse ($students as $student)
            <li>{{ $student->name }}: {{ $student->marks }}</li>
        @empty
            <li>No students from {{ $city }} yet.</li>
        @endforelse
    </ul>
</x-layout>

{city} is a plain string parameter, so Laravel passes the text from the URL to byCity(). Eloquent's where() uses a prepared statement, and Blade's {{ }} escapes the city name, so a URL like /cities/<script> is harmless.

Quick Quiz

1. Which PHP version does Laravel 13 need at minimum?

2. What does {{ $student->name }} do in a Blade template?

3. What happens when the route /students/{student} is called with an id that does not exist and the action is show(Student $student)?

4. Why must POST forms in Laravel contain @csrf?

Learn PHP with a mentor

Free lessons get you started. Our Full Stack Web Development training adds real projects and code reviews from working developers.

  • Project-based learning
  • Mentor code reviews
  • Free counselling call