Introduction to Laravel
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:
composer create-project laravel/laravel school
cd school
php artisan servecreate-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
| Path | What lives there |
|---|---|
routes/web.php | Routes 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.php | The front controller; the web server's document root is public/ |
.env | Settings 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:
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:
| Command | Does |
|---|---|
php artisan serve | Starts the development server |
php artisan make:model Student -m | Creates a model and a migration for it |
php artisan make:controller StudentController | Creates a controller |
php artisan migrate | Runs migrations that have not run yet |
php artisan route:list | Lists every route |
php artisan tinker | Opens an interactive PHP shell inside your app |
php artisan --version | Shows 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:
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
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:
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 rowIsha 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
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
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:
@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:
<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 withhtmlspecialchars(), 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 ... @endforelsefor lists that may be empty. @csrfadds 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:
<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.envand your code could be downloaded. - Committing
.envto Git. It holds passwords and theAPP_KEY. Commit.env.exampleinstead. - Forgetting
@csrfin 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 nameorlaravel new name, and start it withphp artisan serve. - Routes in
routes/web.phpmap URLs to controller actions; route model binding turns{student}into aStudentobject 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,@csrfprotects forms, and components such as<x-layout>share page structure. php artisancreates 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
// 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(),
]);
}<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?
Laravel 13 supports PHP 8.3 to 8.5.
2. What does {{ $student->name }} do in a Blade template?
Double curly braces escape output; {!! !!} would print raw HTML.
3. What happens when the route /students/{student} is called with an id that does not exist and the action is show(Student $student)?
Route model binding looks up the model and answers 404 when it is missing.
4. Why must POST forms in Laravel contain @csrf?
Without the token Laravel rejects the POST with 419 Page Expired.
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
