Phase 1 — Module 1.10: Installing Themes & Plugins — Concepts & Best Practices

Part 1 — Themes

What is a Theme?

A theme controls the visual appearance of your WordPress site. It determines:

  • Layout of every page
  • Typography, colors, spacing
  • How posts, pages, archives look
  • Header and footer structure
  • Navigation placement

A theme should only control appearance — it should never add permanent functionality. If a theme adds functionality (contact forms, custom post types, sliders) and you switch themes, that functionality disappears. That is bad practice.

Theme      →  controls HOW things look
Plugin     →  controls WHAT things do

Types of Themes

1. Classic Themes
   - Use PHP template files
   - Have functions.php, header.php, footer.php etc.
   - Support Menus and Widgets the traditional way
   - What we will build in Phase 2

2. Block Themes (FSE — Full Site Editing)
   - Use HTML template files with block markup
   - Edited via Site Editor (Appearance → Editor)
   - No classic Menus/Widgets panel
   - Twenty Twenty-Three, Twenty Twenty-Four are block themes
   - More modern but complex for beginners

3. Child Themes
   - Built on top of a parent theme
   - Inherits all parent styles and functionality
   - You only override what you want to change
   - Safe from parent theme updates

For this course we build a Classic Theme from scratch.


Installing a Theme — 3 Methods

Method 1 — From WordPress.org Repository (Free Themes)

Go to Appearance → Themes → Add New

+--------------------------------------------------+
|  Add Themes                                      |
|--------------------------------------------------|
|  Search themes...  [                    ] [🔍]   |
|                                                  |
|  Featured  Popular  Latest  Favorites  Filter    |
|--------------------------------------------------|
|  [Astra]  [OceanWP]  [GeneratePress]  [Neve]    |
+--------------------------------------------------+

Search for a theme → hover over it → click Install → click Activate.

Method 2 — Upload ZIP File (Premium Themes)

When you buy a premium theme from ThemeForest, Elegant Themes, etc. — you get a ZIP file.

Go to Appearance → Themes → Add New → Upload Theme

+------------------------------------------+
|  Upload Theme                            |
|------------------------------------------|
|  If you have a theme in a .zip format,   |
|  you may install it by uploading it here.|
|                                          |
|  [ Choose File ]  no file selected       |
|                                          |
|  [ Install Now ]                         |
+------------------------------------------+

Choose the ZIP file → Install Now → Activate.

Method 3 — Manual Upload via FTP/File Manager

For developers — copy the theme folder directly into:

wp-content/themes/your-theme-name/

Then go to Appearance → Themes and activate it from there.

This is how we will activate our custom StreamVault theme in Phase 2.


Theme Best Practices

✓ Always use a child theme if modifying an existing theme
✓ Never edit theme files directly from Appearance → Theme Editor
✓ Test themes on staging before applying to production
✓ Check theme last updated date — outdated themes are security risks
✓ Check ratings and active installations before installing
✓ A theme should never add permanent functionality
✗ Never install themes from unknown sources (pirated themes contain malware)
✗ Never have more than 2-3 themes installed (inactive themes are security risks)

Deleting Unused Themes

Go to Appearance → Themes → click on an inactive theme → click Delete (bottom right corner of the popup).

Always delete themes you are not using. Even inactive themes can have vulnerabilities that get exploited.

Keep:
✓ Your active theme
✓ One default WordPress theme (as fallback)

Delete:
✗ Every other inactive theme

Part 2 — Plugins

What is a Plugin?

A plugin adds or extends functionality to WordPress without modifying core files. Plugins use the hooks system (Actions & Filters) to plug into WordPress at specific points.

Examples:
- Contact Form 7    →  adds contact forms
- WooCommerce       →  adds e-commerce
- Yoast SEO         →  adds SEO tools
- WP Super Cache    →  adds caching
- Akismet           →  adds spam protection

A plugin can be:

Single file:   wp-content/plugins/hello.php
Folder based:  wp-content/plugins/woocommerce/woocommerce.php

Installing a Plugin — 3 Methods

Method 1 — From WordPress.org Repository

Go to Plugins → Add New

+--------------------------------------------------+
|  Add Plugins                                     |
|--------------------------------------------------|
|  Search plugins...  [               ] [🔍]       |
|                                                  |
|  Featured  Popular  Recommended  Favorites       |
|--------------------------------------------------|
|  [Contact Form 7]  [Yoast SEO]  [WooCommerce]   |
+--------------------------------------------------+

Search → Install Now → Activate.

Method 2 — Upload ZIP File

Go to Plugins → Add New → Upload Plugin

Same as theme upload — choose ZIP → Install → Activate.

Method 3 — Manual via FTP

Copy plugin folder to:

wp-content/plugins/your-plugin-name/

Then go to Plugins → Installed Plugins and activate it.


Plugin States — Active vs Inactive

Installed + Active    →  Running, affecting your site
Installed + Inactive  →  Not running, but files still on server
Not installed         →  Does not exist

An inactive plugin does nothing — its code does not run at all. But its files still sit on your server taking up space and potentially having security vulnerabilities.


Plugins Screen — Reading It Correctly

Go to Plugins → Installed Plugins

+----------------------------------------------------------------+
|  All(3)  Active(1)  Inactive(2)  Update Available(1)          |
|----------------------------------------------------------------|
|  [ ] Plugin Name          Description                          |
|----------------------------------------------------------------|
|  [✓] Akismet Anti-Spam    Akismet checks your comments...     |
|      Activate | Edit | Delete                                  |
|                                                                |
|  [ ] Hello Dolly          This is not just a plugin...        |
|      Deactivate | Edit | Delete                               |
|                                                                |
|  [ ] WP Super Cache       Very fast caching plugin...         |
|      Deactivate | Edit | Delete          Update Available ⚠️   |
+----------------------------------------------------------------+

Each plugin row shows:

  • Plugin name and description
  • Author and version
  • Action links — Activate/Deactivate, Edit, Delete
  • Update notice if a new version is available

Activating & Deactivating

Activate — click "Activate" under the plugin name. The plugin starts running immediately.

Deactivate — click "Deactivate". Plugin stops running but files remain. Use this when:

  • Troubleshooting a site issue (deactivate one by one to find the problem)
  • Temporarily disabling a feature
  • Before deleting a plugin

Delete — first deactivate, then delete. This removes all plugin files. Some plugins also remove their database tables on deletion if they have an uninstall hook.


Auto Updates for Plugins

In the Plugins list, you can enable auto-updates per plugin:

Akismet Anti-Spam    |  Enable auto-updates
Hello Dolly          |  Enable auto-updates

Click "Enable auto-updates" for security plugins like Akismet. Be careful with auto-updates for complex plugins like WooCommerce — updates can sometimes break things and should be tested first.


Must-Have Plugins for Development

Install these right now for StreamVault development:


1. Query Monitor

Go to Plugins → Add New → search "Query Monitor" → Install → Activate.

What it does:
- Shows all database queries on every page load
- Shows which hooks fired and in what order
- Shows PHP errors and warnings
- Shows template file being used
- Shows HTTP API calls
- Shows memory usage and page load time

After activating — visit your site frontend. You will see a black admin bar at the top with Query Monitor data. Click on it to expand.

This is your most important debugging tool throughout this course.


2. Classic Editor (Optional)

Go to Plugins → Add New → search "Classic Editor" → Install → Activate.

What it does:
- Replaces the Block Editor (Gutenberg) with the old TinyMCE editor
- Simpler interface for content writing
- Better compatibility with older plugins

This is optional — use it if you find the Block Editor confusing. We will learn the Block Editor properly in Phase 9.


3. Advanced Custom Fields (ACF)

Go to Plugins → Add New → search "Advanced Custom Fields" → Install → Activate.

What it does:
- Adds a UI for creating custom fields (post meta)
- Much easier than writing meta box code manually
- We use this in Phase 4 for movie details
  (release year, rating, trailer URL, etc.)

4. Regenerate Thumbnails

Go to Plugins → Add New → search "Regenerate Thumbnails" → Install → Activate.

What it does:
- When you change image sizes in Settings → Media
  or add new custom image sizes in functions.php,
  old uploaded images do not get the new sizes
- This plugin regenerates all image sizes for
  every image in your Media Library
- We will use this after setting up custom image
  sizes for StreamVault in Phase 2

Plugin Conflicts

Sometimes two plugins conflict with each other — they both try to do the same thing or their code interferes.

How to identify a plugin conflict:

Step 1: Something breaks on your site
Step 2: Go to Plugins → Installed Plugins
Step 3: Deactivate ALL plugins
Step 4: Check if the problem is fixed
Step 5: If yes — reactivate plugins ONE BY ONE
Step 6: After each activation, check if problem returns
Step 7: The plugin that causes the problem to return is the culprit

Plugin Best Practices

✓ Only install plugins from wordpress.org or trusted developers
✓ Check last updated date — avoid plugins not updated in 2+ years
✓ Check active installations and ratings before installing
✓ Read the reviews — look for recent negative reviews about bugs
✓ Always test plugin updates on staging before applying to production
✓ Delete plugins you are not using — do not just deactivate
✓ Keep all plugins updated — outdated plugins are the #1 cause
  of WordPress hacks

✗ Never install nulled (pirated) plugins — they contain malware
✗ Do not install too many plugins — each plugin adds overhead
✗ Never install two plugins that do the same thing

How Many Plugins is Too Many?

There is no hard rule — it depends on plugin quality. A well-coded plugin with 1 database query is better than a poorly coded plugin with 50 queries.

General guideline:

Under 10 plugins    →  Great
10 to 20 plugins    →  Fine if they are all necessary
20 to 30 plugins    →  Getting heavy, audit regularly
30+ plugins         →  Likely has redundancy, clean up

Use Query Monitor to check how many database queries each page load is making. Under 20 queries is ideal.


Hands On — Plugin Setup for StreamVault

Install and activate these plugins right now:

1. Query Monitor        →  debugging tool
2. Advanced Custom Fields →  custom fields UI
3. Regenerate Thumbnails  →  image size management

After installing all three — go to your site frontend and look at the Query Monitor bar at the top. Click on "Queries" to see all database queries WordPress is making on the homepage.


Part 3 — Theme vs Plugin — When to Use Which

This is a common confusion for beginners:

Use a THEME for:
✓ Page layout and structure
✓ Typography and colors
✓ Header, footer, sidebar design
✓ Template files for different content types
✓ CSS and frontend JavaScript

Use a PLUGIN for:
✓ Contact forms
✓ Custom post types (if they should survive theme switching)
✓ E-commerce functionality
✓ SEO tools
✓ Caching
✓ Security features
✓ Any feature that should work regardless of which theme is active

The golden rule:

If the feature should still work when you switch themes → Plugin
If the feature is purely visual/layout → Theme

For StreamVault:

Theme handles:    →  how movies are displayed, layout, design
Plugin handles:   →  watchlist feature, star ratings, custom notifications

Summary

  • Themes control appearance. Plugins add functionality. Never mix them.
  • Three ways to install themes/plugins — WordPress.org repo, ZIP upload, manual FTP.
  • Always delete unused themes and plugins — do not just deactivate.
  • Inactive plugins do not run but their files still pose security risks.
  • Never install nulled/pirated themes or plugins — they always contain malware.
  • Install Query Monitor immediately on every development site.
  • Install Advanced Custom Fields for StreamVault — we use it in Phase 4.
  • Plugin conflicts are diagnosed by deactivating all plugins then reactivating one by one.
  • The golden rule — if a feature should survive theme switching, it belongs in a plugin.


Module 3.1 — What is an Embedding & Why Does it Exist

Start With a Real Problem

Imagine you work at Netflix.

You have 10,000 movies. A user comes in and says:

"I want to watch something like Inception"

How do you find similar movies?

Option 1 — Match by genre tag

Inception tags: ["sci-fi", "thriller", "action"]

Find movies with same tags:
→ The Matrix       ✓ (sci-fi, action)
→ Interstellar     ✓ (sci-fi, thriller)
→ Die Hard         ? (action — but totally different vibe)
→ Avengers         ? (sci-fi, action — but very different)

Genre tags are too broad. Die Hard and Inception are both "action" — but they feel nothing alike.

Option 2 — Match by keywords in description

Inception description: 
"A thief who steals corporate secrets through 
dream-sharing technology..."

Search for movies with words like "thief", "dreams", "technology"
→ Maybe finds some, maybe misses obvious similar movies

Keywords are too literal. "Interstellar" doesn't share many words with "Inception" — but they feel very similar (mind-bending, emotional, sci-fi).

The real problem:

How do you capture the feeling of a movie — not just its tags or keywords — so you can find truly similar ones?

This is exactly the problem embeddings solve. And not just for movies — for any content. Documents, sentences, questions, code, images — anything.


The Big Idea — Turn Meaning Into Numbers

What if you could represent every movie as a point in space?

Movies that feel similar → placed close together. Movies that feel different → placed far apart.

Imagine a simple 2D space:

Mind-bending ↑
             │    • Inception
             │    • Interstellar  
             │    • The Matrix
             │              • Avengers
─────────────┼──────────────────────── → Action-heavy
             │
             │  • The Notebook
             │  • Titanic
             ↓
         Emotional/Romance

Now finding "similar to Inception" is just:

Find the points closest to Inception in this space.

Interstellar and The Matrix are close → recommend them. The Notebook is far away → don't recommend it.

An embedding is exactly this — converting something (a movie, a word, a sentence, a document) into a point in space — represented as a list of numbers.


From 2D to Real Embeddings

In the example above we used 2 dimensions — easy to visualize on paper.

Real embeddings use hundreds or thousands of dimensions:

OpenAI text-embedding-3-small → 1,536 dimensions
OpenAI text-embedding-3-large → 3,072 dimensions
Google's models               → 768 dimensions
Small/fast models             → 384 dimensions

Each dimension captures some aspect of meaning. You can't visualize 1,536 dimensions — but the math works the same way as our simple 2D example.

"cat" as a 1536-dimensional embedding:
[0.23, -0.87, 0.41, 0.92, -0.13, 0.67, ...]
 dim1   dim2  dim3  dim4   dim5  dim6  ...1536 total numbers

This list of 1,536 numbers IS the meaning of "cat" — captured mathematically.


Words vs Sentences vs Documents

Embeddings work at different levels — and this is important for RAG:

Word-level embedding:

"bank" → [0.2, 0.8, 0.4, ...]
Single word represented as a vector

Sentence-level embedding:

"I went to the bank to deposit money"
→ [0.6, 0.3, 0.9, ...]
Whole sentence as one vector
Captures the full meaning of the sentence

Document-level embedding:

Entire paragraph or page
→ [0.4, 0.7, 0.2, ...]
Whole document compressed into one vector

For RAG — we mostly use sentence and paragraph level embeddings. We break documents into chunks and embed each chunk.


The Two Types of Embeddings You'll Use

Type 1 — Token Embeddings (inside the LLM)

These are the embeddings we talked about in Phase 2. They live inside the Transformer. Every token gets an embedding, and Attention updates them as they flow through layers.

You don't directly use these as a developer. They happen internally.

Type 2 — Text Embeddings (for search and RAG)

These are what YOU will use. You send text to an embedding model — it sends back a single vector representing the whole text.

// You send this text:
"What are the side effects of aspirin?"

// Embedding model sends back:
[0.23, -0.87, 0.41, 0.92, -0.13, 0.67, ... 1536 numbers]

// This vector captures the full meaning of the question

This is what we'll use in Phase 4 (Vector Databases) and Phase 5 (RAG).


Why Embeddings Are Better Than Keywords

Let's make this very concrete.

You have a document with this sentence:

"The medication reduced inflammation significantly."

A user asks:

"Does this drug help with swelling?"

Keyword search fails:

Document words: "medication", "reduced", "inflammation", "significantly"
Query words:    "drug", "help", "swelling"

Matching words: ZERO
Result: No match found ✗

Zero overlap in words. Traditional search returns nothing.

Embedding search succeeds:

"medication" ≈ "drug"          (similar meaning)
"inflammation" ≈ "swelling"    (same medical concept)
"reduced significantly" ≈ "help" (similar intent)

Embedding of document sentence: [0.8, 0.3, 0.7, ...]
Embedding of user question:     [0.7, 0.4, 0.6, ...]

These vectors are CLOSE in space → high similarity → match found ✓

This is the superpower of embeddings.

Embeddings capture meaning — not just words. So you can find similar content even when completely different words are used.

This is called semantic search — search by meaning, not keywords.


How Embedding Models Are Trained

You might wonder — how does the model learn that "medication" and "drug" should have similar embeddings?

The training process works like this:

The model reads billions of sentences. It learns — words and phrases that appear in similar contexts should have similar embeddings.

Training data examples:

"The doctor prescribed medication for pain"
"The doctor prescribed a drug for pain"

"She had inflammation in her knee"
"She had swelling in her knee"

After seeing millions of examples like this — the model learns:

"medication" and "drug" → appear in same contexts → similar embeddings
"inflammation" and "swelling" → appear in same contexts → similar embeddings
"medication" and "pizza" → almost never same context → very different embeddings

Nobody told the model these words are related. It figured it out purely from patterns in text.


Embeddings Are Static vs Contextual

This is an important distinction:

Static Embeddings (old way):

"bank" always → [0.4, 0.7, 0.2, ...]

Same vector every time — regardless of context.
"river bank" → same vector as "money bank"

Contextual Embeddings (modern way — what LLMs use):

"I walked to the river bank"
→ "bank" gets embedding influenced by "river"
→ [0.2, 0.9, 0.1, ...] (river meaning)

"I went to the bank to get money"  
→ "bank" gets embedding influenced by "money"
→ [0.8, 0.1, 0.6, ...] (financial meaning)

Different context → different embedding → different meaning captured

Modern embedding models for RAG produce contextual embeddings — the whole sentence influences the meaning of each part.


A Practical Example — What Embeddings Look Like in Code

Here's how you actually generate embeddings using the OpenAI API in JavaScript:


    const response = await fetch("https://api.openai.com/v1/embeddings", {
    method: "POST",
    headers: {
        "Content-Type": "application/json",
        "Authorization": `Bearer ${process.env.OPENAI_API_KEY}`
    },
    body: JSON.stringify({
        model: "text-embedding-3-small",  // embedding model
        input: "What are the side effects of aspirin?"
    })
    });

    const data = await response.json();
    const embedding = data.data[0].embedding;

    console.log(embedding.length);     // 1536
    console.log(embedding.slice(0,5)); // [0.23, -0.87, 0.41, 0.92, -0.13]
    // ... 1531 more numbers

That array of 1,536 numbers IS the meaning of your sentence.

Store it. Compare it. Search with it.


The Three Things You Do With Embeddings

1. Store them

Convert your documents/chunks into embeddings and store them in a Vector Database. (Phase 4)

Document chunk → Embedding → Store in Vector DB

2. Compare them

Convert a user's question into an embedding and compare it against stored embeddings. (Phase 5)

User question → Embedding → Compare with stored embeddings

3. Find the closest ones

Return the document chunks whose embeddings are closest to the question embedding.

Closest embeddings → Most relevant chunks → Feed to LLM → Answer

This is the entire RAG pipeline — and it all runs on embeddings.


The Mental Model

Think of embeddings like GPS coordinates — but for meaning instead of location.

GPS coordinates:
New York  → [40.7128° N, 74.0060° W]
London    → [51.5074° N, 0.1278° W]

Close coordinates = close locations

Embeddings:
"cat"     → [0.9, 0.8, 0.1, 0.05, ...]
"dog"     → [0.8, 0.9, 0.1, 0.06, ...]
"pizza"   → [0.1, 0.2, 0.9, 0.80, ...]

Close embeddings = similar meaning

When you ask "find me something similar to this question" — you're really asking "find the GPS coordinates closest to these coordinates in 1,536-dimensional space."


3-Line Summary

  1. An embedding converts text into a list of numbers (a vector) that captures its meaning — text with similar meaning gets similar numbers, so you can find related content mathematically.
  2. Embeddings are better than keyword search because they capture meaning not words — "medication" and "drug" have similar embeddings even though they're different words.
  3. As a developer you'll use embeddings in two steps — embed your documents and store them, then embed user questions and find the closest stored embeddings — this is the core of RAG.

Module 3.1 — Complete ✅

Coming up — Module 3.2 — Vectors, Vector Space & Dimensions

Now we go one level deeper. You understand what an embedding IS — next you'll understand exactly how these lists of numbers work mathematically, what "vector space" means, and how the model finds similar embeddings. No heavy math — just clear intuition with real examples.

PHP & Laravel — Zero to Hero Episode 19: Eloquent ORM — Connecting Your Application to a Real Database

What Are We Doing in This Post?

For the last three episodes our posts have been hardcoded arrays inside the controller. Every time you restart the server, the data resets. Nothing is saved permanently.

In this episode we connect everything to a real MySQL database using Eloquent — Laravel's ORM.

Eloquent lets you interact with your database using clean PHP instead of raw SQL. Instead of writing SELECT * FROM posts WHERE id = 5, you write Post::find(5). Instead of INSERT INTO posts ..., you write Post::create([...]). It reads like English and it is far safer than raw queries.

By the end of this episode your blog will be fully database-driven — creating, reading, updating, and deleting real posts stored in MySQL.


What is Eloquent?

Eloquent is an ORM — Object Relational Mapper. It maps database tables to PHP classes and database rows to PHP objects.

Real world analogy: Think of a database table as a spreadsheet. Eloquent is like a smart assistant who sits between you and that spreadsheet. Instead of you writing complex formulas to find, add, or update rows yourself, you tell the assistant in plain language — "find me the post with ID 5", "create a new post with this title", "delete all draft posts". The assistant handles all the spreadsheet operations for you.

Each database table has one corresponding Eloquent Model class. The posts table has a Post model. The users table has a User model. The model is your interface to that table.


Step 1 — Create the Post Model

Run this Artisan command:


    php artisan make:model Post

Laravel creates app/Models/Post.php. Open it:


    <?php

    namespace App\Models;

    use Illuminate\Database\Eloquent\Model;

    class Post extends Model
    {

    }

That is it. An empty class that extends Eloquent's Model. By extending Model, your Post class automatically inherits hundreds of methods for database interaction — create, find, update, delete, query, paginate, and much more.

Eloquent uses conventions to figure out the table name automatically. The Post model maps to the posts table. The User model maps to users. Eloquent pluralizes the model name and lowercases it. You never have to specify the table name unless you break the convention.


Step 2 — Configure Mass Assignment Protection

Before we can create records using Eloquent's create() method, we need to tell it which columns are safe to fill from user input. This is called the $fillable property.


    <?php

    namespace App\Models;

    use Illuminate\Database\Eloquent\Model;

    class Post extends Model
    {
        protected $fillable = [
            'user_id',
            'title',
            'slug',
            'body',
            'status',
            'views',
            'published_at',
        ];
    }

$fillable is a whitelist. Only the columns listed here can be mass-assigned — meaning filled in bulk using an array. This prevents a security vulnerability called mass assignment — where a malicious user could inject extra fields into a form submission to overwrite columns they should not have access to.

For example, without $fillable, a clever user could add is_admin=1 to a form submission and potentially overwrite their own role in the database. With $fillable, only the explicitly listed columns are accepted.


Step 3 — Update the Migration

Our posts migration from Episode 16 already has the right structure. But let us verify by checking database/migrations/ for the create_posts_table file. The migration should look like this — if it does not match, update it:


    <?php

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

    return new class extends Migration
    {
        public function up(): void
        {
            Schema::create('posts', function (Blueprint $table) {
                $table->id();
                $table->foreignId('user_id')->nullable()->constrained()->onDelete('cascade');
                $table->string('title');
                $table->string('slug')->unique();
                $table->text('body');
                $table->string('status')->default('draft');
                $table->unsignedInteger('views')->default(0);
                $table->timestamp('published_at')->nullable();
                $table->timestamps();
            });
        }

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

We made user_id nullable for now — since we do not have authentication yet, posts will not be tied to a specific user until Episode 21.

Run a fresh migration to rebuild the database cleanly:


    php artisan migrate:fresh

This drops all existing tables and reruns every migration. Your laravel_myapp database now has clean empty tables.


Step 4 — Seed the Database With Fake Data

Instead of manually adding test posts through a form, let us use Laravel's seeder system to populate the database automatically.

Run:


    php artisan make:seeder PostSeeder

Open database/seeders/PostSeeder.php and update it:


    <?php

    namespace Database\Seeders;

    use Illuminate\Database\Seeder;
    use App\Models\Post;
    use Illuminate\Support\Str;

    class PostSeeder extends Seeder
    {
        public function run(): void
        {
            $posts = [
                [
                    'title'        => 'Getting Started With Laravel',
                    'slug'         => 'getting-started-with-laravel',
                    'body'         => 'Laravel is a powerful PHP framework that makes web development enjoyable. It provides tools for routing, database management, authentication, and much more out of the box. Whether you are building a simple blog or a complex enterprise application, Laravel has the tools you need.',
                    'status'       => 'published',
                    'published_at' => now(),
                ],
                [
                    'title'        => 'Understanding MVC Architecture',
                    'slug'         => 'understanding-mvc-architecture',
                    'body'         => 'MVC separates your application into Models, Views, and Controllers. This separation of concerns keeps your code organized, maintainable, and easy to scale. The Model handles data, the View handles display, and the Controller handles the logic that connects them.',
                    'status'       => 'published',
                    'published_at' => now(),
                ],
                [
                    'title'        => 'Working With Eloquent ORM',
                    'slug'         => 'working-with-eloquent-orm',
                    'body'         => 'Eloquent makes database queries feel like writing plain English. Each database table has a corresponding Model class that you use to interact with that table. You can fetch, create, update, and delete records without writing a single line of raw SQL.',
                    'status'       => 'published',
                    'published_at' => now(),
                ],
                [
                    'title'        => 'Blade Templating Engine Deep Dive',
                    'slug'         => 'blade-templating-engine-deep-dive',
                    'body'         => 'Blade is Laravel\'s powerful templating engine. It gives you clean syntax for displaying variables, loops, conditionals, and template inheritance. Every Blade file is compiled to plain PHP and cached for maximum performance.',
                    'status'       => 'draft',
                    'published_at' => null,
                ],
                [
                    'title'        => 'Laravel Routing Complete Guide',
                    'slug'         => 'laravel-routing-complete-guide',
                    'body'         => 'Laravel\'s routing system is clean and expressive. You can define routes with parameters, constraints, names, and groups. Route::resource generates all seven RESTful routes in a single line.',
                    'status'       => 'published',
                    'published_at' => now(),
                ],
            ];

            foreach ($posts as $post) {
                Post::create($post);
            }
        }
    }

Now register this seeder in database/seeders/DatabaseSeeder.php:


    <?php

    namespace Database\Seeders;

    use Illuminate\Database\Seeder;

    class DatabaseSeeder extends Seeder
    {
        public function run(): void
        {
            $this->call([
                PostSeeder::class,
            ]);
        }
    }

Run the seeder:


    php artisan db:seed

You will see:
INFO  Seeding database.
PostSeeder ............... DONE

Check phpMyAdmin at http://localhost:8080/phpmyadmin — open laravel_myapp, click posts table, click Browse. You will see all five posts inserted.


Step 5 — Update the Controller to Use Eloquent

Now replace all the hardcoded arrays in PostController with real Eloquent queries:


    <?php

    namespace App\Http\Controllers;

    use Illuminate\Http\Request;
    use App\Models\Post;
    use Illuminate\Support\Str;

    class PostController extends Controller
    {
        public function index()
        {
            $posts = Post::latest()->get();
            return view('posts.index', ['posts' => $posts]);
        }

        public function create()
        {
            return view('posts.create');
        }

        public function store(Request $request)
        {
            $request->validate([
                'title'  => 'required|min:3|max:255',
                'body'   => 'required|min:10',
                'author' => 'required|max:100',
            ]);

            Post::create([
                'title'        => $request->input('title'),
                'slug'         => Str::slug($request->input('title')),
                'body'         => $request->input('body'),
                'status'       => 'published',
                'published_at' => now(),
            ]);

            return redirect()->route('posts.index')->with('success', 'Post created successfully!');
        }

        public function show($id)
        {
            $post = Post::findOrFail($id);
            $post->increment('views');
            return view('posts.show', ['post' => $post]);
        }

        public function edit($id)
        {
            $post = Post::findOrFail($id);
            return view('posts.edit', ['post' => $post]);
        }

        public function update(Request $request, $id)
        {
            $post = Post::findOrFail($id);

            $request->validate([
                'title' => 'required|min:3|max:255',
                'body'  => 'required|min:10',
            ]);

            $post->update([
                'title' => $request->input('title'),
                'slug'  => Str::slug($request->input('title')),
                'body'  => $request->input('body'),
            ]);

            return redirect()->route('posts.show', $post->id)->with('success', 'Post updated successfully!');
        }

        public function destroy($id)
        {
            $post = Post::findOrFail($id);
            $post->delete();
            return redirect()->route('posts.index')->with('success', 'Post deleted successfully!');
        }
    }

Let us go through every Eloquent method used here.


Understanding Eloquent Methods

Post::latest()->get()


    <?php

    $posts = Post::latest()->get();

latest() orders results by created_at descending — newest first. get() executes the query and returns a Collection — Laravel's powerful array-like object containing all matching Post models.

Post::create()


    <?php

    Post::create([
        'title' => 'My Post',
        'slug'  => 'my-post',
        'body'  => 'Post content here.',
    ]);

Creates a new row in the posts table with the provided data. Returns the newly created Post model with its auto-assigned id already set.

Post::findOrFail($id)


    <?php

    $post = Post::findOrFail($id);

Finds a post by its primary key. If found, returns the Post model. If not found, automatically throws a 404 error — no manual abort(404) needed. This is much cleaner than Post::find($id) which returns null on failure and requires a manual check.

$post->increment('views')


    <?php

    $post->increment('views');

Increments the views column by 1 directly in the database. Safer than fetching the value, adding 1 in PHP, and saving back — because the increment happens atomically in MySQL.

$post->update()


    <?php

    $post->update([
        'title' => 'Updated Title',
        'body'  => 'Updated content.',
    ]);

Updates only the specified columns on the existing record. updated_at is set automatically by Eloquent.

$post->delete()


    <?php

    $post->delete();

Deletes the record from the database permanently.


Eloquent Query Builder — Filtering and Querying

Eloquent has a full query builder for more complex queries:


    <?php

    Post::all();

    Post::find(5);

    Post::findOrFail(5);

    Post::where('status', 'published')->get();

    Post::where('status', 'published')->orderBy('created_at', 'desc')->get();

    Post::where('status', 'published')->latest()->take(3)->get();

    Post::where('views', '>', 100)->get();

    Post::where('status', 'published')->where('views', '>', 50)->get();

    Post::where('status', 'published')->orWhere('views', '>', 1000)->get();

    Post::count();

    Post::where('status', 'published')->count();

    Post::latest()->first();

    Post::where('slug', 'my-post')->first();

all() — fetch every row. Use carefully on large tables.

find($id) — fetch by primary key, returns null if not found.

where() — add conditions. Chainable.

orderBy('column', 'direction') — sort results.

take($n) — limit results to n rows.

count() — count matching rows without fetching them.

first() — fetch only the first matching row.


Accessing Model Properties

When Eloquent returns a model, each database column becomes a property:


    <?php

    $post = Post::findOrFail(1);

    echo $post->id;
    echo $post->title;
    echo $post->body;
    echo $post->status;
    echo $post->created_at;
    echo $post->updated_at;

In Blade templates you access them the same way:


    <h1>{{ $post->title }}</h1>
    <p>{{ $post->body }}</p>
    <small>{{ $post->created_at->diffForHumans() }}</small>

$post->created_at is not just a plain string — Eloquent automatically casts timestamp columns to Carbon objects. Carbon is a PHP date library that gives you clean methods like diffForHumans() which outputs "3 hours ago", "2 days ago", "1 month ago" — exactly what you see on real blog posts and social media.


Update the Blade Views for Eloquent Data

Update resources/views/posts/index.blade.php — the data is now Eloquent model objects, not arrays, so we use -> instead of []:


    @extends('layouts.app')

    @section('title', 'All Posts — MyBlog')

    @push('styles')
    <style>
        .post-card { background:#fff; border:1px solid #e2e8f0; border-radius:12px; padding:24px; margin-bottom:20px; }
        .post-card h2 { font-size:20px; font-weight:700; margin-bottom:8px; color:#1e293b; }
        .post-card p { font-size:14px; color:#64748b; line-height:1.6; margin-bottom:12px; }
        .post-meta { font-size:12px; color:#94a3b8; display:flex; gap:16px; align-items:center; }
        .badge { display:inline-block; padding:3px 10px; border-radius:20px; font-size:11px; font-weight:600; }
        .badge-published { background:#f0fdf4; color:#166534; }
        .badge-draft { background:#fef9c3; color:#854d0e; }
        .read-more { color:#6366f1; font-weight:600; text-decoration:none; font-size:13px; }
    </style>
    @endpush

    @section('content')

        <div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:32px;">
            <h1 style="font-size:28px; font-weight:800;">All Posts</h1>
            <a href="{{ route('posts.create') }}" style="background:#6366f1; color:#fff; padding:10px 20px; border-radius:8px; text-decoration:none; font-size:14px; font-weight:600;">+ New Post</a>
        </div>

        @forelse($posts as $post)
            <div class="post-card">
                <div style="display:flex; justify-content:space-between; align-items:flex-start; margin-bottom:10px;">
                    <h2>{{ $post->title }}</h2>
                    <span class="badge badge-{{ $post->status }}">{{ ucfirst($post->status) }}</span>
                </div>
                <p>{{ Str::limit($post->body, 120) }}</p>
                <div class="post-meta">
                    <span>{{ $post->created_at->diffForHumans() }}</span>
                    <span>{{ $post->views }} views</span>
                    <a href="{{ route('posts.show', $post->id) }}" class="read-more">Read More →</a>
                </div>
            </div>
        @empty
            <div style="text-align:center; padding:60px; color:#64748b;">
                <p style="font-size:18px; margin-bottom:16px;">No posts yet.</p>
                <a href="{{ route('posts.create') }}" style="color:#6366f1; font-weight:600;">Create your first post</a>
            </div>
        @endforelse

    @endsection

Update resources/views/posts/show.blade.php:


    @extends('layouts.app')

    @section('title', $post->title . ' — MyBlog')

    @section('content')

        <article style="background:#fff; border:1px solid #e2e8f0; border-radius:12px; padding:40px; max-width:700px; margin:0 auto;">

            <span class="badge badge-{{ $post->status }}" style="display:inline-block; padding:3px 10px; border-radius:20px; font-size:11px; font-weight:600; background:#f0fdf4; color:#166534; margin-bottom:16px;">
                {{ ucfirst($post->status) }}
            </span>

            <h1 style="font-size:30px; font-weight:800; margin-bottom:8px; color:#1e293b;">{{ $post->title }}</h1>

            <div style="display:flex; gap:16px; color:#94a3b8; font-size:13px; margin-bottom:32px; padding-bottom:24px; border-bottom:1px solid #e2e8f0;">
                <span>{{ $post->created_at->format('d M Y') }}</span>
                <span>{{ $post->views }} views</span>
            </div>

            <div style="font-size:16px; line-height:1.85; color:#334155;">
                {{ $post->body }}
            </div>

            <div style="margin-top:40px; padding-top:24px; border-top:1px solid #e2e8f0; display:flex; gap:16px;">
                <a href="{{ route('posts.index') }}" style="color:#6366f1; font-weight:600; text-decoration:none;">← All Posts</a>
                <a href="{{ route('posts.edit', $post->id) }}" style="color:#f59e0b; font-weight:600; text-decoration:none;">Edit Post</a>

                <form method="POST" action="{{ route('posts.destroy', $post->id) }}" style="display:inline;" onsubmit="return confirm('Delete this post?')">
                    @csrf
                    @method('DELETE')
                    <button type="submit" style="background:none; border:none; color:#ef4444; font-weight:600; cursor:pointer; font-size:16px; padding:0;">Delete Post</button>
                </form>
            </div>

        </article>

    @endsection

Notice @method('DELETE') — HTML forms only support GET and POST natively. For PUT, PATCH, and DELETE requests from a form, Blade provides @method() which adds a hidden _method field that Laravel reads to determine the actual HTTP method.

Create resources/views/posts/edit.blade.php:


    @extends('layouts.app')

    @section('title', 'Edit Post — MyBlog')

    @section('content')

        <div style="background:#fff; border:1px solid #e2e8f0; border-radius:12px; padding:40px; max-width:600px; margin:0 auto;">

            <h1 style="font-size:24px; font-weight:800; margin-bottom:28px;">Edit Post</h1>

            <form method="POST" action="{{ route('posts.update', $post->id) }}">
                @csrf
                @method('PATCH')

                <div style="margin-bottom:20px;">
                    <label style="display:block; font-size:14px; font-weight:600; margin-bottom:6px;">Title</label>
                    <input type="text" name="title" value="{{ old('title', $post->title) }}"
                        style="width:100%; padding:10px 14px; border:1px solid #e2e8f0; border-radius:8px; font-size:15px;" required>
                </div>

                <div style="margin-bottom:28px;">
                    <label style="display:block; font-size:14px; font-weight:600; margin-bottom:6px;">Body</label>
                    <textarea name="body" rows="8"
                        style="width:100%; padding:10px 14px; border:1px solid #e2e8f0; border-radius:8px; font-size:15px; resize:vertical;" required>{{ old('body', $post->body) }}</textarea>
                </div>

                <div style="display:flex; gap:12px;">
                    <button type="submit"
                        style="background:#6366f1; color:#fff; padding:12px 28px; border:none; border-radius:8px; font-size:15px; font-weight:600; cursor:pointer;">
                        Update Post
                    </button>
                    <a href="{{ route('posts.show', $post->id) }}"
                        style="background:#f1f5f9; color:#475569; padding:12px 28px; border-radius:8px; font-size:15px; font-weight:600; text-decoration:none;">
                        Cancel
                    </a>
                </div>
            </form>

        </div>

    @endsection

old('title', $post->title) — the second argument to old() is a default value. If the form was freshly loaded (not a failed re-submission), it shows the existing post title. If validation failed and the form was re-shown, it shows what the user last typed.


Testing the Full Application

Start the server if it is not running:


    php artisan serve

Visit http://127.0.0.1:8000/posts — you see all five seeded posts with real data from MySQL, view counts, and relative timestamps.

Click any post — the view count increments on every visit. Refresh and watch it go up.

Click Edit Post — update the title and body, submit. You are redirected back to the post with a success message.

Click Delete Post — confirm the dialog. Post is gone from the database.

Go to Create Post — fill in the form and submit. A new post appears in the list, stored permanently in MySQL.

This is a fully working database-driven Laravel application. Create, Read, Update, Delete — all working with real persistent data.


What Did We Learn in This Post?

Eloquent ORM maps database tables to PHP model classes. The Post model maps to the posts table automatically by convention.

$fillable whitelists columns for mass assignment — a critical security protection.

Key Eloquent methods: all(), find(), findOrFail(), create(), update(), delete(), where(), latest(), first(), count(), get(), increment().

findOrFail() automatically returns a 404 response when a record is not found — cleaner than manual null checks.

Timestamp columns like created_at are Carbon objects — diffForHumans() gives you "3 hours ago" style output automatically.

@method('DELETE') and @method('PATCH') in Blade forms let HTML forms send non-GET/POST HTTP methods that Laravel understands.

old('field', $default) in edit forms shows existing data on first load and preserves user input on failed validation.

Seeders populate the database with test data using php artisan db:seed — far faster than manual data entry during development.


What is Coming in Episode 20?

Our application works but it has no input validation error messages shown to the user. Submit a form with invalid data right now — Laravel rejects it silently and redirects back.

Episode 20 covers Laravel Form Validation in depth — validation rules, displaying error messages in Blade, custom error messages, and Form Request classes that keep validation logic out of controllers completely.

See you in the next one.

Module 3.3 — Similarity Search: Cosine, Euclidean & Dot Product

The Problem We're Solving You have 10,000 document chunks — all embedded as vectors — sitting in a vector database. A user asks a questi...