Laravel: My First Real Framework

SS Saurav Sitaula

Before React, before Spring Boot, there was Laravel. How I went from writing spaghetti PHP to understanding MVC architecture, Blade templates, and why frameworks exist. A journey back to early 2019 when everything clicked.

The PHP Spaghetti Era

Before I discovered Laravel, my PHP code looked like this:

<?php
// index.php - 500 lines of chaos

$conn = mysqli_connect("localhost", "root", "", "mydb");

if ($_SERVER["REQUEST_METHOD"] == "POST") {
    $name = $_POST["name"];
    $email = $_POST["email"];
    // No validation, no escaping, SQL injection paradise
    $sql = "INSERT INTO users (name, email) VALUES ('$name', '$email')";
    mysqli_query($conn, $sql);
}

$result = mysqli_query($conn, "SELECT * FROM users");
?>

<!DOCTYPE html>
<html>
<head>
    <title>My App</title>
</head>
<body>
    <h1>Users</h1>
    <?php while($row = mysqli_fetch_assoc($result)): ?>
        <p><?php echo $row['name']; ?> - <?php echo $row['email']; ?></p>
    <?php endwhile; ?>
    
    <form method="POST">
        <input name="name" placeholder="Name">
        <input name="email" placeholder="Email">
        <button type="submit">Add User</button>
    </form>
</body>
</html>

HTML, PHP, SQL, all in one file. No structure. No organization. A security nightmare.

But it worked. So I kept doing it.

The Breaking Point

My “app” grew to 15 PHP files. Each one a mix of database queries, business logic, and HTML. Finding anything took forever. Changing one thing broke three others.

A senior developer reviewed my code:

“Have you considered using a framework?”

“What’s a framework?”

Silence.

Enter Laravel

He suggested Laravel. “It’s PHP, but organized. You’ll hate it at first, then wonder how you lived without it.”

He was right on both counts.

Installation: Welcome to Composer

In my raw PHP days, “installing” something meant downloading a zip file and including it with require. Laravel uses Composer, PHP’s package manager.

# Install Composer first (one-time)
curl -sS https://getcomposer.org/installer | php
mv composer.phar /usr/local/bin/composer

# Create new Laravel project
composer create-project --prefer-dist laravel/laravel myapp "5.6.*"

I specifically used Laravel 5.6 — it was the stable version in early 2019.

The download took forever. So many dependencies. What is all this stuff?

The Project Structure That Terrified Me

myapp/
├── app/
│   ├── Http/
│   │   ├── Controllers/
│   │   └── Middleware/
│   ├── Models/
│   └── Providers/
├── config/
├── database/
│   ├── migrations/
│   └── seeds/
├── public/
├── resources/
│   └── views/
├── routes/
│   └── web.php
├── storage/
├── vendor/
├── .env
└── composer.json

My brain: Where do I put my code? What are migrations? What’s middleware? Why are there so many folders?

But here’s the thing: each folder has ONE purpose. Unlike my single-file chaos, Laravel separates concerns:

  • routes/ — URL definitions
  • app/Http/Controllers/ — Request handling logic
  • resources/views/ — HTML templates (Blade)
  • database/migrations/ — Database structure
  • config/ — Configuration files
  • .env — Environment variables (passwords, API keys)

My First Route

In routes/web.php:

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

That’s it. Visit /, return the welcome view. No index.php?page=home. No URL parsing. Just clean routes.

Adding my own:

Route::get('/hello', function () {
    return 'Hello, World!';
});

Route::get('/users', function () {
    return view('users.index');
});

Route::get('/users/{id}', function ($id) {
    return "User ID: " . $id;
});

The {id} syntax captures URL parameters. /users/5$id = 5. Magic.

Blade Templates: PHP’s JSX Moment

Remember my inline PHP mess? Blade fixed that.

Create resources/views/users/index.blade.php:

<!DOCTYPE html>
<html>
<head>
    <title>Users</title>
</head>
<body>
    <h1>All Users</h1>
    
    @foreach($users as $user)
        <div class="user-card">
            <h3>{{ $user->name }}</h3>
            <p>{{ $user->email }}</p>
        </div>
    @endforeach
    
    @if(count($users) === 0)
        <p>No users found.</p>
    @endif
</body>
</html>

Key differences from raw PHP:

  • {{ $var }} instead of <?php echo htmlspecialchars($var); ?>
  • @foreach / @endforeach instead of <?php foreach(): ?>
  • @if / @endif instead of <?php if(): ?>

The {{ }} syntax automatically escapes output. No more XSS vulnerabilities because I forgot htmlspecialchars().

Layouts: Stop Repeating Yourself

Every page needs <html>, <head>, navigation, footer. With raw PHP, I was copying this everywhere.

Blade has layouts.

Create resources/views/layouts/app.blade.php:

<!DOCTYPE html>
<html>
<head>
    <title>@yield('title') - My App</title>
    <link rel="stylesheet" href="/css/app.css">
</head>
<body>
    <nav>
        <a href="/">Home</a>
        <a href="/users">Users</a>
        <a href="/about">About</a>
    </nav>
    
    <main class="container">
        @yield('content')
    </main>
    
    <footer>
        &copy; 2019 My App
    </footer>
    
    <script src="/js/app.js"></script>
</body>
</html>

Now any page can extend this:

{{-- resources/views/users/index.blade.php --}}

@extends('layouts.app')

@section('title', 'Users')

@section('content')
    <h1>All Users</h1>
    
    @foreach($users as $user)
        <div class="user-card">
            <h3>{{ $user->name }}</h3>
            <p>{{ $user->email }}</p>
        </div>
    @endforeach
@endsection

Change the navigation once in layouts/app.blade.php, every page updates. This was the “component” moment before I knew what components were.

Controllers: Separating Logic from Routes

My routes file was getting crowded. Time to use controllers.

php artisan make:controller UserController

This creates app/Http/Controllers/UserController.php:

<?php

namespace App\Http\Controllers;

use App\User;
use Illuminate\Http\Request;

class UserController extends Controller
{
    public function index()
    {
        $users = User::all();
        return view('users.index', ['users' => $users]);
    }
    
    public function show($id)
    {
        $user = User::findOrFail($id);
        return view('users.show', ['user' => $user]);
    }
    
    public function create()
    {
        return view('users.create');
    }
    
    public function store(Request $request)
    {
        $validated = $request->validate([
            'name' => 'required|max:255',
            'email' => 'required|email|unique:users',
        ]);
        
        User::create($validated);
        
        return redirect('/users')->with('success', 'User created!');
    }
}

And the routes become simple:

Route::get('/users', 'UserController@index');
Route::get('/users/create', 'UserController@create');
Route::post('/users', 'UserController@store');
Route::get('/users/{id}', 'UserController@show');

Or even simpler with resource routing:

Route::resource('users', 'UserController');

This one line creates ALL standard CRUD routes. I couldn’t believe it.

Eloquent: Database Without SQL Strings

Remember my SQL injection paradise? Eloquent is Laravel’s ORM.

The User model (app/User.php):

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class User extends Model
{
    protected $fillable = ['name', 'email', 'password'];
    
    protected $hidden = ['password'];
}

Now I can do:

// Get all users
$users = User::all();

// Find by ID
$user = User::find(1);

// Find or 404
$user = User::findOrFail(1);

// Create
User::create([
    'name' => 'Saurav',
    'email' => 'saurav@example.com',
    'password' => bcrypt('secret')
]);

// Update
$user->name = 'New Name';
$user->save();

// Delete
$user->delete();

// Query builder
$activeUsers = User::where('active', true)
                   ->orderBy('created_at', 'desc')
                   ->take(10)
                   ->get();

No SQL strings. No mysqli_real_escape_string(). Eloquent handles it all.

Migrations: Version Control for Databases

My old approach: Write SQL in phpMyAdmin, hope I remember what I changed.

Laravel migrations:

php artisan make:migration create_users_table

Creates database/migrations/2019_02_10_000000_create_users_table.php:

<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

class CreateUsersTable extends Migration
{
    public function up()
    {
        Schema::create('users', function (Blueprint $table) {
            $table->increments('id');
            $table->string('name');
            $table->string('email')->unique();
            $table->string('password');
            $table->timestamps();  // created_at, updated_at
        });
    }

    public function down()
    {
        Schema::dropIfExists('users');
    }
}

Run migrations:

php artisan migrate

Every team member runs the same migrations. Database changes are versioned. Roll back mistakes with php artisan migrate:rollback. Revolutionary.

The Artisan CLI

php artisan became my best friend:

# Create things
php artisan make:controller ProductController
php artisan make:model Product -m  # -m creates migration too
php artisan make:middleware CheckAdmin

# Database
php artisan migrate
php artisan migrate:rollback
php artisan db:seed

# Development
php artisan serve  # Start dev server on localhost:8000
php artisan tinker  # Interactive PHP shell with Laravel loaded

# Cache
php artisan cache:clear
php artisan config:clear
php artisan view:clear

No more memorizing file locations. Artisan creates properly structured files in the right places.

The .env File: Configuration Done Right

My old approach: Hardcode database passwords in PHP files. Push to GitHub. Panic.

Laravel’s approach:

# .env (not committed to git!)
APP_NAME=MyApp
APP_ENV=local
APP_DEBUG=true

DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=myapp
DB_USERNAME=root
DB_PASSWORD=secret

MAIL_DRIVER=smtp
MAIL_HOST=smtp.mailtrap.io
MAIL_USERNAME=your_username
MAIL_PASSWORD=your_password

Access in code:

$appName = env('APP_NAME');
$dbHost = config('database.connections.mysql.host');

Different .env for development and production. Secrets stay secret.

What I Wish I’d Known Earlier

  1. Read the documentation. Laravel’s docs are excellent. I wasted hours Googling things that were clearly explained in the official docs.

  2. Use Artisan for everything. Don’t manually create files. php artisan make:* ensures proper structure.

  3. Eloquent isn’t magic. It’s running SQL under the hood. Use DB::enableQueryLog() to see what’s actually executing.

  4. Blade components exist. @include and @component let you create reusable pieces. I learned this way too late.

  5. The learning curve is front-loaded. The first week is confusing. The second week is productive. The third week you’re flying.

The Journey Continues

Laravel transformed how I thought about web development. Instead of chaos, I had structure. Instead of copy-pasting, I had reusable components. Instead of SQL injection vulnerabilities, I had Eloquent.

But I’d only scratched the surface. Laravel had authentication built-in. Form handling. Sessions. And something called “notifications” that would change how I thought about user communication.


P.S. — If you’re coming from raw PHP, Laravel will feel like overkill at first. “Why do I need all these folders for a simple app?” Give it two weeks. Build something real. You’ll never go back to spaghetti PHP. I promise.

SS

Saurav Sitaula

Software Architect • Nepal

Back to all posts
Saurav.dev

© 2026 Saurav Sitaula.AstroNeoBrutalism