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(): allowsUser::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:
| Concept | Restaurant Analogy |
|---|---|
index.php | Front door + Host who manages everything |
Request::capture() | Guest walking in and saying their order |
Router | The waiter who matches the guest to the kitchen |
Controller | The chef who prepares the meal |
Model (User.php) | The fridge/storage where the ingredients (data) are |
View | The plate the food is served on (HTML) |
Response::send() | Waiter delivering food to the guest (browser) |
Container | The 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
UserControlleritself, 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 manuallyneweverything. - When no dependencies are involved,
bind()is optional — container can auto-resolve basic classes.
🏎️ Analogy: Building a Car with Dependencies
| Object | Depends On |
|---|---|
Car | Tire |
Tire | Rubber |
Rubber | Glue |
Glue | TreeJuice |
🧰 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! 💯
