A minimal MVC structure using Illuminate components (routing, database, support, etc.)

Here’s a complete guide to create a minimal MVC structure using Illuminate components (routing, database, support, etc.) and display user data from a users table.

In my private Git repo, you can see directory named ‘custom-mvc-packages’.

✅ Step-by-Step Minimal MVC Setup

🧩 1. Project Setup

Run this in a new folder:

composer init
composer require illuminate/routing illuminate/events illuminate/container illuminate/database illuminate/support

📁 2. Directory Structure

project/
│
├── index.php
├── routes.php
├── config/
│   └── database.php
├── controllers/
│   └── UserController.php
├── models/
│   └── User.php
├── views/
│   └── users.php
└── vendor/

⚙️ 3. Database Config – config/database.php

<?php

return [
    'driver'    => 'mysql',
    'host'      => 'localhost',
    'database'  => 'packages_test',
    'username'  => 'root',
    'password'  => 'PASSWORD',
    'charset'   => 'utf8',
    'collation' => 'utf8_unicode_ci',
    'prefix'    => '',
];

📦 4. Model – models/User.php

<?php

use Illuminate\Database\Eloquent\Model;

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

🧠 5. Controller – controllers/UserController.php

<?php
require_once __DIR__ . '/../models/User.php';
class UserController
{
    public function index()
    {
        $users = User::all();
        // Pass $users to the view
        require __DIR__ . '/../views/users.php';
    }
}

🖼️ 6. View – views/users.php

<!DOCTYPE html>
<html>

<head>
    <title>User List</title>
</head>

<body>
    <h1>Users</h1>
    <ul>
        <?php foreach ($users as $user): ?>
        <li><?php echo $user->name?> (<?php echo $user->email?>)</li>
        <?php endforeach; ?>
    </ul>
</body>

</html>

📍 7. Routes – routes.php

<?php

$router->get('/users', [UserController::class, 'index']);

🚀 8. Front Controller – index.php

<?php

use Illuminate\Container\Container;
use Illuminate\Database\Capsule\Manager as Capsule;
use Illuminate\Events\Dispatcher;
use Illuminate\Http\Request;
use Illuminate\Routing\Router;

require_once __DIR__ . '/vendor/autoload.php';

// Setup container
$container = new Container;
$request   = Request::capture();

// Setup Eloquent ORM
$capsule = new Capsule($container);
$capsule->addConnection(require __DIR__ . '/config/database.php');
$capsule->setAsGlobal();
$capsule->bootEloquent();

// Setup router
$events = new Dispatcher($container);
$router = new Router($events, $container);

// Make sure UserController is imported or defined
require_once __DIR__ . '/controllers/UserController.php';

// Bind controller
$container->bind('UserController', function () {
    return new UserController();
});

// Load routes
require __DIR__ . '/routes.php';

// Dispatch request
$response = $router->dispatch($request);
$response->send();

✅ 9. Run the App

Use PHP’s built-in server:

php -S localhost:8000

Visit: http://localhost:8000/users


🧠 What’s Happening?

  • index.php is the front controller.
  • Illuminate components handle routing, DB, DI, events.
  • MVC structure is manually built:
    • Routes → Controller → Model → View.

🔍 index.php – Detailed Explanation

This is your Front Controller, which acts as the single entry point to your entire application. It wires up everything — database, routing, controllers, and sends a response.

<?php

// Use necessary classes
use Illuminate\Container\Container;
use Illuminate\Events\Dispatcher;
use Illuminate\Routing\Router;
use Illuminate\Database\Capsule\Manager as Capsule;
use Illuminate\Http\Request;

require __DIR__ . '/vendor/autoload.php';

🔹 Step 1: Autoload Classes

require __DIR__ . '/vendor/autoload.php';

Loads all installed Illuminate packages and your own class files via Composer.


🔹 Step 2: Create a Dependency Container

$container = new Container;

Think of the container as a toolbox that knows how to build and manage your classes. Laravel’s core uses this container to do dependency injection.


🔹 Step 3: Capture the Current HTTP Request

$request = Request::capture();

It captures the current request (e.g., /users, query string, method, etc.) into a Request object.


🔹 Step 4: Configure Eloquent ORM

$capsule = new Capsule;
$capsule->addConnection(require __DIR__ . '/config/database.php');
$capsule->setAsGlobal();
$capsule->bootEloquent();

Here, you’re setting up Eloquent (Laravel’s ORM):

  • addConnection(...): loads DB config.
  • setAsGlobal(): allows User::all() etc. to work globally.
  • bootEloquent(): initializes the ORM.

This is like plugging in a database adapter to your app.


🔹 Step 5: Set Up the Router

$events = new Dispatcher($container);
$router = new Router($events, $container);
  • Dispatcher: handles internal Laravel events (not needed in basic routing but required by Router).
  • Router: the core class that manages all route definitions and dispatching.

🔹 Step 6: Bind Controllers (for DI)

$container->bind('UserController', function () {
    return new UserController();
});

This tells the container how to build a controller when it’s needed. It enables dependency injection if your controller needs things like services, DB access, etc.


🔹 Step 7: Load the Routes

require __DIR__ . '/routes.php';

This file registers routes, like /users, and links them to your controller methods.


🔹 Step 8: Dispatch the Request

$response = $router->dispatch($request);
$response->send();
  • dispatch($request): finds the matching route and executes the controller.
  • send(): sends the response (usually HTML or JSON) back to the browser.

🧠 Analogy: The Restaurant Analogy

Think of your app as a restaurant:

ConceptRestaurant Analogy
index.phpFront door + Host who manages everything
Request::capture()Guest walking in and saying their order
RouterThe waiter who matches the guest to the kitchen
ControllerThe chef who prepares the meal
Model (User.php)The fridge/storage where the ingredients (data) are
ViewThe plate the food is served on (HTML)
Response::send()Waiter delivering food to the guest (browser)
ContainerThe restaurant manager who knows everyone’s job

So when someone visits /users, it’s like:

A guest comes in (Request), asks for “Users List” → The waiter (Router) checks the menu (routes) and sends the request to the chef (UserController) → The chef fetches ingredients (User model) → Prepares the dish (HTML view) → The waiter delivers it (Response).


✅ Summary

This single index.php script wires together your entire minimal MVC system:

  • Handles the request
  • Boots the ORM
  • Registers and runs routes
  • Sends back a clean response

In your index.php, you may have seen this line:

$container->bind('UserController', function () {
    return new UserController();
});

🟢 What does this bind do?

It tells the container (aka the “object manager”) how to create an instance of UserController when it’s needed — especially for route callbacks like:

$router->get('/users', [UserController::class, 'index']);

❓ What if you don’t use bind()?

👉 If your controller has no constructor dependencies (like __construct() with no parameters), then you don’t need to call bind() — Laravel/Illuminate’s container can auto-resolve it.

So this will still work fine:

class UserController
{
    public function index()
    {
        $users = User::all();
        require __DIR__ . '/../views/users.php';
    }
}

Even without:

$container->bind('UserController', function () {
    return new UserController();
});

Because Laravel uses reflection to figure out how to instantiate the class.


❗ But if your controller has constructor dependencies, like:

class UserController
{
    protected $logger;

    public function __construct(Logger $logger)
    {
        $this->logger = $logger;
    }
}

Then you must bind either:

  • The UserController itself, or
  • The dependencies (Logger, in this case).

Otherwise, Laravel’s container will throw an error saying it doesn’t know how to create Logger.


🔧 What I thought (and it’s correct):

Bind is basically needed when we require all other dependent objects & few of its values for the main object.

✅ Yes — when a class (like UserController) needs other services/objects injected into it, the container needs to know how to build it — and bind() is how you provide that recipe.


🧍 Manual Way (without container)

You’re doing everything manually:

$model = new UserModel();
$controller = new UserController($model);

You are manually wiring dependencies. This works fine, but you’re in charge of making sure the right things go into the constructor.


⚙️ Container Way

You say:

$container->bind('UserController', function () {
    return new UserController(new UserModel());
});

Or even simpler, if UserModel is bound too:

$container->bind('UserController', function ($container) {
    return new UserController($container->make('UserModel'));
});

Now, Laravel’s container handles the creation of objects and their dependencies.


✅ Benefits of Using bind() and the Container

  • You don’t have to say new Something() everywhere.
  • You can easily swap dependencies (great for testing).
  • You write cleaner, more decoupled code.
  • You gain automatic dependency resolution.

TL;DR: Your Summary is Spot-On ✅

  • bind() is needed when you want the container to manage complex object creation.
  • Without bind(), you need to manually new everything.
  • When no dependencies are involved, bind() is optional — container can auto-resolve basic classes.

🏎️ Analogy: Building a Car with Dependencies

ObjectDepends On
CarTire
TireRubber
RubberGlue
GlueTreeJuice

🧰 Without Container (Manual Way)

You would write all this by hand:

$treeJuice = new TreeJuice();
$glue = new Glue($treeJuice);
$rubber = new Rubber($glue);
$tire = new Tire($rubber);
$car = new Car($tire);

You’re responsible for the entire chain. Works fine, but gets messy when things grow.


🪄 With Container + bind() or make()

You tell the container just how to make TreeJuice, and maybe Glue.

Then you write:

$container->make(Car::class);

And Laravel says:

“Oh! Car needs a Tire, Tire needs Rubber, Rubber needs Glue, Glue needs TreeJuice. I know how to create each of them, so I’ll recursively build them all and give you a ready-made Car.”

That’s called automatic dependency resolution, and that’s what the container does best.


🧠 Bonus: You Don’t Always Need bind()

If classes look like this:

class Glue {
    public function __construct(TreeJuice $juice) { ... }
}

And there are no manual values or special logic, the container can auto-resolve it using PHP reflection.

You only bind() when:

  • You need to pass fixed values.
  • You want to use an interface with a specific implementation.
  • You need conditional or custom instantiation logic.

✅ Final Summary

Yes — just like a Car depends on a whole chain of parts, when you ask the container for Car, it:

  • Looks at what Car needs.
  • Resolves every sub-dependency.
  • Constructs the whole dependency tree for you.

You nailed it DILIP! 💯