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! πŸ’―