how is mvc architecture used in php without any framework?
Updated 2020-02-11: Refactoring the answer to include a few best practices and being closer to PHP 7.4.
Thousands words does not compete with a clean example, so here is a simple use case:
Imagine you want to display a page describing a "car" (given a "car id") from an imaginary car vendor: http://example.com/car.php?id=42 (will be http://example.com/car/42 later).
Very basically, you can structure your code with an hierarchy like:
A configuration directory (this isn't part of the MVC architectural pattern):
+ config/
- database.php
<?php
return new PDO(getenv("DB_DSN"), getenv("DB_USER"), getenv("DB_PASSWORD"));
A folder for your document root (scripts acting like Controllers):
+ htdocs/
- car.php
<?php
$carService = new CarService(require "config/database.php");
$car = $carService->getById($_GET["id"]);
require "car.php";
A folder encapsulating your Model/business logic (hint: "Thin Controllers, Fat model"):
+ src/
- CarService.php
<?php
class CarService {
private PDO $database;
public function __construct(PDO $database) {
$this->database = $database;
}
public function getById(int $id): CarEntity {
return $this->database->query(
"SELECT model, year, price " .
"FROM car " .
"WHERE id = $id"
)->fetch(PDO::FETCH_CLASS, CarEntity::class);
}
}
A last folder containing all your Views(/templates):
+ views/
- car.php
<!DOCTYPE html>
<html>
<head>
<title>Car - <?= htmlspecialchars($car->model) ?></title>
</head>
<body>
<h1><?= htmlspecialchars($car->model) ?></h1>
Year: <?= htmlspecialchars($car->year) ?>
Price: <?= htmlspecialchars($car->price) ?>
</body>
</html>
For the code above to work, you will need PHP to be configured with:
include_path="/the/path/to/src:/the/path/to/views"
You might want nice URLs, if using Apache you can achieve this with:
RewriteEngine On
RewriteRule ^/car/(\d+)$ /car.php?id=$1 [L]
This enables writing URLs like http://example.com/car/42 which will be internally converted to http://example.com/car.php?id=42
In the above solution, car.php
is included from the global scope, that is why $car
is directly available, but $carService
too!
One way to restrict what the templates may access is to transform it as a class:
views/CarView.php
:
<?php
class CarView {
private CarEntity $car;
public function __construct(CarEntity $car) {
$this->car = $car;
}
public function __invoke(): void {
?>
<!DOCTYPE html>
<html>
<head>
<title>Car - <?= htmlspecialchars($this->car->model) ?></title>
</head>
<body>
<h1><?= htmlspecialchars($this->car->model) ?></h1>
Year: <?= htmlspecialchars($this->car->year) ?>
Price: <?= htmlspecialchars($this->car->price) ?>
</body>
</html>
<?php
}
}
and then adapting the controller:
htdocs/car.php
:
<?php
$carService = new CarService(require "config/database.php");
$view = new CarView($carService->getById($_GET["id"]));
$view();
Using plain PHP files as templates, nothing prevents you from creating headers.php, footers.php, menu.php,... which you can reuse with include()
/require()
to avoid duplicated HTML.
Using classes, re-usability can be obtained by combining them, for example, a LayoutView
can be responsible for the global layout, and, in turn, calls another View
component:
<?php
class LayoutView {
protected string $lang;
public function __construct(string $lang) {
$this->lang = $lang;
}
// __invoke(): for embracing the "Single Responsibility" principle
public function __invoke(View $view): void {
?>
<!DOCTYPE html>
<html lang="<?= $this->lang ?>">
<head>
<meta charset="utf-8" />
<title><?= htmlentities($view->getTitle()) ?></title>
</head>
<body>
<?php ($view)(); ?>
</body>
</html>
<?php
}
}
and CarView could be implemented like:
views/CarView.php
:
<?php
class CarView implements View {
private CarEntity $car;
public function __construct(CarEntity $car) {
$this->car = $car;
}
public function getTitle(): string {
return $this->car->model;
}
// __invoke(): for embracing the "Single Responsibility" principle
public function __invoke(): void {
?>
<h1><?= htmlspecialchars($this->car->model) ?></h1>
Year: <?= htmlspecialchars($this->car->year) ?>
Price: <?= htmlspecialchars($this->car->price) ?>
<?php
}
}
In turns, the controller would use it like this:
htdocs/car.php
:
<?php
$carService = new CarService(require "config/database.php");
(new LayoutView("en"))(
new CarView($carService->getById($_GET["id"]))
);
This is far from being production-ready code, as other aspects aren't addressed by those examples: dependency injection/inversion of control (IoC), input filtering, class autoloading, namespaces,... The goal of this answer is to focus as much as possible on the main aspect of MVC.
This is very much in the same spirit as Rasmus Lerdorf mentioned on: https://toys.lerdorf.com/the-no-framework-php-mvc-framework.
One should not forget that MVC remains a pattern. Software Patterns are reusable principles to solve common problems, if they would be reusable code, they would have been named "libraries" instead.
Frameworks like Zend Framework, Symfony, Laravel, CakePHP and the likes proposes a structure to adopt an MVC approach but can't enforce it, MVC, as a special case of Separation of concerns needs to be learned and understood to be achieved.
I think generally using one of the common frameworks is probably the way to go. The reason is that many good developers have spent a long time writing, bug-fixing, tweaking and polishing to create something solid for basing your site on. The best thing to do is to find one you like, learn it and stick with it (unless you find a reason not to). When I work with PHP, my choice is generally Zend Framework, but there are also CodeIgniter, Symfony, CakePHP and a bunch of others.
If you still want to use the MVC pattern without an existing framework, you either have the choice of putting your own together or just logically separating each concern out from each other - this is the core tenet of MVC, the frameworks just help you achieve it.
Rasmus Lerdorf wrote about his minimal approach to the MVC pattern in PHP in 2006. Might be worth a read. You may also be interested in a mini-framework such as F3::PHP (PHP 5.3+ only) - looks pretty promising.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With