> For the complete documentation index, see [llms.txt](https://learnphp.gitbook.io/learnphp/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://learnphp.gitbook.io/learnphp/advanced/du-an-xay-dung-mvc-php-ok.md).

# Dự án xây dựng MVC PHP (ok)

1\. <https://github.com/phendan/custom-cms>\
2\. <https://github.com/rizkhal/php-crud-mvc>\
3\. <https://github.com/khalidhagane/school_manager> (Dự án này chỉnh sửa lại code để chạy trên lva1.com lva1-school\_manager.rar)

4. <https://github.com/AbdAllAH-ElRhmany/future_link/tree/main> (Đã có chỉnh sửa sang tiếng anh)

{% file src="/files/dlZWDeILgsWav15GzOPQ" %}

{% file src="/files/q7ksJcn4TI7xQAKWU2hM" %}

{% file src="/files/MrGjUTorM9LtxZS0Scw8" %}

{% file src="/files/Ke1iGzwHgPbaQ2pvzJyi" %}

## Project 1  ([custom-cms](https://github.com/phendan/custom-cms))

### 1. Tạo controller HomeController, LoginController, DashboardController

app\Controllers\DashboardController.php

```php
<?php
namespace App\Controllers;
use App\BaseController;
use App\Request;
class DashboardController extends BaseController {
  public function index(Request $request): void
  {
    $this->view->render("dashboard",
      [
        "user" => $this->user
      ]
    );
  }
}
```

app\Controllers\HomeController.php

```php
<?php
namespace App\Controllers;
use App\BaseController;
use App\Request;
class HomeController extends BaseController {
  public function index(Request $request): void
  {
    $this->view->render("home");
  }
}
```

app\Controllers\LoginController.php

```php
<?php
namespace App\Controllers;
use App\BaseController;
use App\Request;
class LoginController extends BaseController
{
  public function index(Request $request)
  {
    if($this->user->isLoggedIn()) {
      $this->redirectTo(path: "dashboard");
    }
    if ($request->getMethod() != "POST") {
      $this->view->render("login");
      return;
    }
    try {
      $inputForm = $request->getInput();
      $this->user->login($inputForm);
      $this->redirectTo(path: "dashboard");
    } catch (\Throwable $th) {
      throw $th;
    }
  }
}

```

app\Helpers\Str.php

```php
<?php
namespace App\Helpers;
class Str
{
  public static function toPascalCase(string $subject): string
  {
    return str_replace('_', '', ucwords($subject, '_'));
  }
  public static function toCamelCase(string $subject): string
  {
    return lcfirst(self::toPascalCase($subject));
  }
}

```

app\Models\Database.php

```php
<?php
namespace App\Models;
use PDO;
use PDOStatement;
class Database
{
  private $host = "localhost";
  private $dbname = "forum";
  private $username = "root";
  private $password = "";
  private PDO $db;
  private PDOStatement $statement;
  public function __construct()
  {
    $this->db = new PDO(
      dsn: "mysql:host={$this->host};dbname={$this->dbname}",
      username: $this->username,
      password: $this->password
    );
  }
  public function query($sql,$values=[]): static {
    $this->statement = $this->db->prepare(query: $sql);
    $this->statement->execute(params: $values);
    return $this;
  }
  public function results():array {
    return $this->statement->fetchAll(PDO::FETCH_ASSOC);
  }
  public function first():array {
    return $this->results()[0];
  }
  public function count():int {
    return $this->statement->rowCount();
  }
}

```

app\Models\User.php

```php
<?php
namespace App\Models;
use App\Helpers\Str;
class User
{
  private Database $db;
  private $id;
  private $email;
  private $firstName;
  private $lastName;
  private $password;
  public function __construct(Database $db)
  {
    $this->db = $db;
  }
  public function find(string|int $identifier): bool
  {
    $column = is_int(value: $identifier) ? "id" : "email";
    $sql = "SELECT * FROM `users` WHERE `{$column}` = :identifier";
    $userQuery = $this->db->query($sql, ["identifier" => $identifier]);
    if (!$userQuery->count()) return false;
    $userData = $userQuery->first();
    foreach ($userData as $column => $value) {
      $columnCamel = Str::toCamelCase($column);
      $this->{$columnCamel} = $value;
    }
    return true;
  }
  public function isLoggedIn(): bool {
    return isset($_SESSION['userId']);
  }
  public function login(array $userData): void
  {
    if (!$this->find($userData["email"])) {
      throw new \Exception(message: "Email Not Corrent 😌");
    }
    $_SESSION['userId'] = $this->getId();
  }
  public function getId(): int
  {
    return (int)($this->id ?? $_SESSION['userId']);
  }
  public function getFirstName():string {
    return $this->firstName;
  }
}

```

app\views\partials\header.php

```php
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Custom CMS</title>
  <link rel="stylesheet" href="https://lva3.com/styles/app.css">
  <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
```

app\views\partials\footer.php

```php
</body>
</html>
```

app\views\dashboard.php

```php
Hello: <?= $user->getFirstName(); ?>
```

app\views\home.php

```php
home.php
```

app\views\login.php

```php
<div class="container">
  <h1>Login</h1>
  <form method="post" novalidate>
    <div>
      <label for="email" class="form-label">Email</label>
      <input type="email" id="email" name="email" class="form-control">
    </div>
    <div>
      <label for="password" class="form-label">Password</label>
      <input type="password" id="password" name="password" class="form-control">
    </div>
    <input type="submit" value="Sign In" class="mt-3 btn btn-success">
  </form>
</div>
```

app\App.php

```php
<?php
namespace App;
class App
{
  public function __construct()
  {
    $router = new Router();
    $requestedControlller = $router->getRequestedController();
    $requestedMethod = $router->getRequestedMethod();
    $controller = new $requestedControlller();
    $params = $router->getParams();
    $request = new Request(pageParams: $params);
    $controller->{$requestedMethod}($request);
  }
}
```

app\BaseController.php

```php
<?php
namespace App;
use App\Models\Database;
use App\Models\User;
abstract class BaseController {
  protected View $view;
  protected User $user;
  protected Database $db;
  public function __construct() {
    $this->db = new Database();
    $this->user = new User(db: $this->db);
    $this->view = new View(user: $this->user);
    if ($this->user->isLoggedIn()) {
      $this->user->find($this->user->getId());
    }
  }
  abstract public function index(Request $request);
  public function redirectTo(string $path): never {
    header(header: "Location:".$path);
    exit();
  }
}
```

app\Request.php

```php
<?php
namespace App;
class Request {
  private $pageParams;
  private $getParams;
  private $postParams;
  private $fileParams;
  public function __construct($pageParams) {
    $this->pageParams = $pageParams;
    $this->getParams = $_GET;
    $this->postParams = $_POST;
    $this->fileParams = $_FILES;
  }
  public function getMethod(): string {
    return $_SERVER['REQUEST_METHOD'];
  }
  public function getInput(string $kind="post") {
    $input = match($kind) {
      "page" => $this->pageParams,
      "get" => $this->getParams,
      "post" => $this->postParams,
      "file" => $this->fileParams
    };
    return $input;
  }
}
```

app\Router.php

```php
<?php
namespace App;
use App\Controllers\HomeController;
class Router {
  private $controller = HomeController::class;
  private $method = "index";
  private $params = [];
  public function __construct() {
    $url = $this->parseUrl();
    if(!isset($url[0])) return;
    $requestedController = "App\\Controllers\\" . ucfirst(string: $url[0]) . "Controller";
    $this->controller = $requestedController;
  }
  public function getRequestedController(): string {
    return $this->controller;
  }
  public function getRequestedMethod(): string {
    return $this->method;
  }
  public function getParams(): array {
    return $this->params;
  }
  public function parseUrl(): array {
    if(!isset($_GET['url'])) return [];
    $url = rtrim(string: $_GET['url'], characters: "/");
    $url = explode(separator: "/",string: $url);
    return $url;
  }
}
```

app\View\.php

```php
<?php
namespace App;
use App\Models\User;
class View {
  private User $user;
  public function __construct(User $user) {
    $this->user = $user;
  }
  public function render(string $view,$data=[]): void {
    extract($data);
    require_once("../app/views/partials/header.php");
    require_once("../app/views/{$view}.php");
    require_once("../app/views/partials/footer.php");
  }
}
```

public\\.htaccess

```
Options -MultiViews
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^(.+)$ index.php?url=$1 [QSA,L]

```

public\index.php

```php
<?php
use App\App;
session_start();
require_once '../vendor/autoload.php';
$app = new App;

```

composer.json

```json
{
  "autoload": {
    "psr-4": {
      "App\\": "app/"
    }
  }
}
```

{% file src="/files/vHXkxjhR7WrMUihSFZ0G" %}

### 2. Chi tiết bài viết&#x20;

<figure><img src="/files/81ofBAWLCIWyHEL3J5HZ" alt=""><figcaption></figcaption></figure>

app\Router.php

```php
<?php
namespace App;
use App\Controllers\HomeController;
class Router {
  private $controller = HomeController::class;
  private $method = "index";
  private $params = [];
  public function __construct() {
    $url = $this->parseUrl();
    if(!isset($url[0])) return;
    $requestedController = "App\\Controllers\\" . ucfirst(string: $url[0]) . "Controller";
    $this->controller = $requestedController;
    unset($url[0]);
    $this->params = array_values(array: $url);
  }
  public function getRequestedController(): string {
    return $this->controller;
  }
  public function getRequestedMethod(): string {
    return $this->method;
  }
  public function getParams(): array {
    return $this->params;
  }
  public function parseUrl(): array {
    if(!isset($_GET['url'])) return [];
    $url = rtrim(string: $_GET['url'], characters: "/");
    $url = explode(separator: "/",string: $url);
    return $url;
  }
}
```

app\Models\Post.php

```php
<?php
namespace App\Models;
use App\Helpers\Str;
use DateTime;
class Post
{
  private Database $db;
  private $id;
  private $userId;
  private string $title;
  private string  $slug;
  private  string $body;
  private $createdAt;
  public function __construct(Database $db, $data = [])
  {
    $this->db = $db;
    $this->fill($data);
  }
  public function fill($data)
  {
    foreach ($data as $column => $value) {
      $columnCamel = Str::toCamelCase($column);
      $this->{$columnCamel} = $value;
    }
  }
  public function find(int $identifier): bool
  {
    $sql = "SELECT * FROM `posts` WHERE `id` = :id";
    $postQuery = $this->db->query($sql, ['id' => $identifier]);
    if(!$postQuery->count()) {
      return false;
    }
    $post = $postQuery->first();
    $this->fill($post);
    return true;
  }
  public function getTitle(): string
  {
    return $this->title;
  }
}

```

app\Controllers\PostController.php

```php
<?php
namespace App\Controllers;
use App\BaseController;
use App\Models\Post;
use App\Request;
class PostController extends BaseController {
  public function index(Request $request): void
  {
    $identifier = $request->getInput('page')[0];
    $post = new Post(db: $this->db);
    $post->find($identifier);
    $this->view->render("posts/index",[
      "post" => $post
    ]);
  }
}
```

### 3. Show bài viết của 1 người đăng nhập ra dashboad

<figure><img src="/files/PXBSLipRdcOCxCkekdWw" alt=""><figcaption></figcaption></figure>

app\Models\User.php

```php
<?php
namespace App\Models;
use App\Helpers\Str;
class User
{
  private Database $db;
  private $id;
  private $email;
  private $firstName;
  private $lastName;
  private $password;
  public function __construct(Database $db)
  {
    $this->db = $db;
  }
  public function find(string|int $identifier): bool
  {
    $column = is_int(value: $identifier) ? "id" : "email";
    $sql = "SELECT * FROM `users` WHERE `{$column}` = :identifier";
    $userQuery = $this->db->query($sql, ["identifier" => $identifier]);
    if (!$userQuery->count()) return false;
    $userData = $userQuery->first();
    foreach ($userData as $column => $value) {
      $columnCamel = Str::toCamelCase($column);
      $this->{$columnCamel} = $value;
    }
    return true;
  }
  public function isLoggedIn(): bool {
    return isset($_SESSION['userId']);
  }
  public function login(array $userData): void
  {
    if (!$this->find($userData["email"])) {
      throw new \Exception(message: "Email Not Corrent 😌");
    }
    $_SESSION['userId'] = $this->getId();
  }
  public function getId(): int
  {
    return (int)($this->id ?? $_SESSION['userId']);
  }
  public function getFirstName():string {
    return $this->firstName;
  }
  public function getPosts():array {
    $sql = "SELECT * FROM `posts` WHERE `user_id` = :user_id";
    $userQuery = $this->db->query($sql,['user_id'=>$this->getId()]);
    if(!$userQuery->count()) {
      return [];
    }
    $posts = [];
    foreach ($userQuery->results() as $value) {
      $posts[] = new Post($this->db, $value);
    }
    return $posts; 
  }
}

```

app\Models\Post.php

```php
<?php
namespace App\Models;
use App\Helpers\Str;
use DateTime;
class Post
{
  private Database $db;
  private $id;
  private $userId;
  private string $title;
  private string  $slug;
  private  string $body;
  private $createdAt;
  public function __construct(Database $db, $data = [])
  {
    $this->db = $db;
    $this->fill($data);
  }
  public function fill($data)
  {
    foreach ($data as $column => $value) {
      $columnCamel = Str::toCamelCase($column);
      $this->{$columnCamel} = $value;
    }
  }
  public function find(int $identifier): bool
  {
    $sql = "SELECT * FROM `posts` WHERE `id` = :id";
    $postQuery = $this->db->query($sql, ['id' => $identifier]);
    if(!$postQuery->count()) {
      return false;
    }
    $post = $postQuery->first();
    $this->fill($post);
    return true;
  }
  public function getTitle(): string
  {
    return $this->title;
  }
  public function getId():int {
    return $this->id;
  }
  public function getSlug():string {
    return $this->slug;
  }
}

```

app\Views\dashboard.php

```php
Hello: <?= $user->getFirstName(); ?>
<h2>Your Posts</h2>
<?php foreach ($user->getPosts() as $post): ?>
  <div class="list-group mb-3">
    <a class="list-group-item list-group-item-action w-50" href="/post/<?php echo $post->getId(); ?>/<?php echo $post->getSlug(); ?>"><?php echo $post->getTitle(); ?></a>
    <div class="d-flex">
    <a class="btn btn-danger w-25 list-group-flush" href="/post/delete/<?php echo $post->getId(); ?>">Delete</a>
    <a class="btn btn-warning w-25 list-group-flush" href="/post/edit/<?php echo $post->getId(); ?>">Edit</a>
    </div>
  </div>
<?php endforeach; ?>
```

{% file src="/files/VZFY3zGP5MQu5ZHJX1kt" %}

### 4. Show bài viết ra form chỉnh sửa

<figure><img src="/files/FE344rrIcLUBuTUzRB8S" alt=""><figcaption></figcaption></figure>

app\Controllers\PostController.php

```php
<?php
namespace App\Controllers;
use App\BaseController;
use App\Models\Post;
use App\Request;
class PostController extends BaseController {
  public function index(Request $request): void
  {
    $idifitier = $request->getInput("page")[0];
    $post = new Post(db: $this->db);
    $post->find($idifitier);
    $this->view->render("posts/index", [
      "post" => $post
    ]);
  }
  public function edit(Request $request) {
    $idifitier = $request->getInput("page")[1];
    $post = new Post(db: $this->db);
    $post->find($idifitier);
    $this->view->render("posts/edit",[
      "post" => $post
    ]);
  }
}
```

app\Models\Post.php

```php
<?php
namespace App\Models;
use App\Helpers\Str;
class Post {
  private Database $db;
  private $id;
  private $userId;
  private string $title;
  private string $slug;
  private string $body;
  private $createdAt;
  public function __construct(Database $db,$data=[]) {
    $this->db = $db;
    $this->fill($data);
  }
  public function fill($data): void {
    foreach ($data as $column => $value) {
      $columnCamel = Str::toCamelCase($column);
      $this->{$columnCamel} = $value;
    }
  }
  public function find(int $idifitier): bool {
    $sql = "SELECT * FROM `posts` WHERE `id` = :id";
    $postQuery = $this->db->query($sql,['id'=>$idifitier]);
    if(!$postQuery->count()) {
      return false;
    }
    $postData = $postQuery->first();
    $this->fill($postData);
    return true;
  }
  public function getTitle(): string {
    return $this->title;
  }
  public function getId():int {
    return $this->id;
  }
  public function getSlug():string {
    return $this->slug;
  }
  public function getBody(): string {
    return $this->body;
  }
}
```

{% file src="/files/ejZJYR9dJzj10JZNyrjt" %}

### 5. Cập nhật bài viết và chuyển sang bài viết vừa cập nhật

<figure><img src="/files/s80M8w7LsOuJn5ZdKuFq" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/D2ANpL0wkIsKlYqfsZs9" alt=""><figcaption></figcaption></figure>

app\Controllers\PostController.php

```php
<?php
namespace App\Controllers;
use App\BaseController;
use App\Models\Post;
use App\Request;
class PostController extends BaseController {
  public function index(Request $request): void
  {
    $identifier = $request->getInput('page')[0];
    $post = new Post(db: $this->db);
    $post->find($identifier);
    $this->view->render("posts/index",[
      "post" => $post
    ]);
  }
  public function edit(Request $request) {
    $identifier = $request->getInput('page')[1];
    $post = new Post(db: $this->db);
    $post->find($identifier);
    if ($request->getMethod() !== 'POST') {
      $this->view->render('/posts/edit', [
        'post' => $post
      ]);
      return;
    }
    $formInput = $request->getInput();
    if (!$post->update(postData: $formInput)) {
      die("Chưa cập nhật bài viết");
    }
    $this->redirectTo("/post/{$post->getId()}/{$post->getSlug()}");
  }
}
```

app\Router.php

```php
<?php
namespace App;
use App\Controllers\HomeController;
class Router {
  private $controller = HomeController::class;
  private $method = "index";
  private $params = [];
  public function __construct() {
    $url = $this->parseUrl();
    if(!isset($url[0])) return;
    $requestedController = "App\\Controllers\\" . ucfirst(string: $url[0]) . "Controller";
    $this->controller = $requestedController;
    unset($url[0]);
    if(isset($url[1]) && method_exists(object_or_class: $this->controller,method: $url[1])) {
      $this->method = $url[1];
    }
    $this->params = array_values(array: $url);
    unset($url[1]);
  }
  public function getRequestedController(): string {
    return $this->controller;
  }
  public function getRequestedMethod(): string {
    return $this->method;
  }
  public function getParams(): array {
    return $this->params;
  }
  public function parseUrl(): array {
    if(!isset($_GET['url'])) return [];
    $url = rtrim(string: $_GET['url'], characters: "/");
    $url = explode(separator: "/",string: $url);
    return $url;
  }
}
```

app\Models\Post.php

```php
<?php
namespace App\Models;
use App\Helpers\Str;
use DateTime;
class Post
{
  private Database $db;
  private $id;
  private $userId;
  private string $title;
  private string  $slug;
  private  string $body;
  private $createdAt;
  public function __construct(Database $db, $data = [])
  {
    $this->db = $db;
    $this->fill($data);
  }
  public function fill($data)
  {
    foreach ($data as $column => $value) {
      $columnCamel = Str::toCamelCase($column);
      $this->{$columnCamel} = $value;
    }
  }
  public function find(int $identifier): bool
  {
    $sql = "SELECT * FROM `posts` WHERE `id` = :id";
    $postQuery = $this->db->query($sql, ['id' => $identifier]);
    if (!$postQuery->count()) {
      return false;
    }
    $post = $postQuery->first();
    $this->fill($post);
    return true;
  }
  public function getTitle(): string
  {
    return $this->title;
  }
  public function getId(): int
  {
    return $this->id;
  }
  public function getSlug(): string
  {
    return $this->slug;
  }
  public function getBody(): string
  {
    return $this->body;
  }
  public function update(array $postData)
  {
    $sql = "UPDATE `posts` SET `title` = :title, `slug` = :slug, `body` =:body WHERE `id` = :id";
    $slug = Str::slug($postData['title']);
    $postData = [
      'id' => $this->getId(),
      'title' => $postData['title'],
      'slug' => $slug,
      'body' => $postData['body']
    ];
    $editQuery = $this->db->query($sql, $postData);
    $this->fill($postData);
    return (bool) $editQuery->count();
  }
}
```

app\Helpers\Str.php

```php
<?php
namespace App\Helpers;
class Str
{
  public static function toPascalCase(string $subject): string
  {
    return str_replace('_', '', ucwords($subject, '_'));
  }
  public static function toCamelCase(string $subject): string
  {
    return lcfirst(self::toPascalCase($subject));
  }
  public static function slug(string $string)
  {
    $disallowedCharacters = '/[^\-\s\pN\pL]+/';
    $spacesDuplicateHyphens = '/[\-\s]+/';
    $slug = mb_strtolower($string, 'UTF-8');
    $slug = preg_replace($disallowedCharacters, '', $slug);
    $slug = preg_replace($spacesDuplicateHyphens, '-', $slug);
    $slug = trim($slug, '-');
    return $slug;
  }
}

```

{% file src="/files/elgYJzdd8waFFFmEU0jw" %}

### 6. Create new Post

<figure><img src="/files/VczCUbWGCwZS15aT6SeO" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/XyPhCg8RKSTgtHYYj8Gy" alt=""><figcaption></figcaption></figure>

app\Controllers\PostController.php

```php
<?php
namespace App\Controllers;
use App\BaseController;
use App\Models\Post;
use App\Request;
use Exception;
class PostController extends BaseController {
  public function index(Request $request): void
  {
    $idifitier = $request->getInput("page")[0];
    $post = new Post(db: $this->db);
    $post->find($idifitier);
    $this->view->render("posts/index", [
      "post" => $post
    ]);
  }
  public function edit(Request $request) {
    $identifier = $request->getInput("page")[1];
    $post = new Post(db: $this->db);
    $post->find($identifier);
    if($request->getMethod() != "POST") {
      $this->view->render("posts/edit", [
        "post"=> $post
      ]);
      return;
    }
    $inputForm = $request->getInput();
    if (!$post->update(postData: $inputForm)) {
      die("Not Update");
    }
    $this->redirectTo("/post/{$post->getId()}/{$post->getSlug()}");
  }
  public function create(Request $request) {
    if($request->getMethod() != "POST") {
      $this->view->render("posts/create");
      return;
    }
    $formInput = $request->getInput();
    $fileInput = $request->getInput('file');
    $post = new Post($this->db);
    try {
      $post->add(userId: $this->user->getId(), postData: $formInput, image: $fileInput['image']);
      $this->redirectTo('/dashboard');
    } catch (Exception $e) {
      $this->view->render('posts/create', [
        'errors' => [
          'root' => $e->getMessage()
        ]
      ]);
    }
  }
}
```

app\views\posts\create.php

```php
<div class="container">
  <h1>Write Your Post</h1>
  <form method="post" enctype="multipart/form-data">
    <div>
      <label for="title" class="form-label">Title</label>
      <input type="text" id="title" name="title" class="form-control">
    </div>
    <div>
      <label for="body" class="form-label">Post Body</label>
      <textarea name="body" id="body" class="form-control"></textarea>
    </div>
    <div>
      <label for="image" class="form-label">Image</label>
      <input type="file" id="image" name="image" class="form-control">
    </div>
    <input type="submit" value="Create Post" class="mt-3 btn btn-success">
  </form>
</div>
```

app\Models\Post.php

```php
<?php
namespace App\Models;
use App\Helpers\Str;
class Post
{
  private Database $db;
  private $id;
  private $userId;
  private string $title;
  private $slug;
  private $body;
  private $createdAt;
  public function __construct(Database $db, $data = [])
  {
    $this->db = $db;
    $this->fill($data);
  }
  public function fill($data): void
  {
    foreach ($data as $column => $value) {
      $columnCamel = Str::toCamelCase($column);
      $this->{$columnCamel} = $value;
    }
  }
  public function find(int $idifitier): bool
  {
    $sql = "SELECT * FROM `posts` WHERE `id` = :id";
    $postQuery = $this->db->query($sql, ['id' => $idifitier]);
    if (!$postQuery->count()) {
      return false;
    }
    $postData = $postQuery->first();
    $this->fill($postData);
    return true;
  }
  public function getTitle(): string
  {
    return $this->title;
  }
  public function getId(): int
  {
    return $this->id;
  }
  public function getSlug(): string
  {
    return $this->slug;
  }
  public function getBody(): string {
    return $this->body;
  }
  public function update(array $postData) {
    $sql = "UPDATE `posts` SET `title`=:title, `slug`=:slug,`body` =:body WHERE `id` = :id";
    $slug = Str::slug($postData['title']);
    $postData = [
      'id' => $this->getId(),
      'title'=> $postData['title'],
      'slug'=> $slug,
      'body'=> $postData['body'],
    ];
    $updateQuery = $this->db->query($sql, $postData);
    $this->fill($postData);
    return (bool) ($updateQuery->count());
  }
  public function add(int $userId, array $postData, array $image) {
    $sql = "INSERT INTO `posts` (`user_id`, `title`, `slug`, `body`, `created_at`) VALUES (:userId, :title, :slug, :body, :createdAt)";
    $slug = Str::slug($postData['title']);
    $this->db->query($sql, [
      'userId' => $userId,
      'title' => $postData['title'],
      'slug' => $slug,
      'body' => $postData['body'],
      'createdAt' => time()
    ]);
  }
}

```

### 7. Upload image in post

<figure><img src="/files/X05MtIHSN2gIux16L2i2" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/YIOu7gRyUMHYBlkrlBnV" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/bPYYWBQg79vg8yP3EDkA" alt=""><figcaption></figcaption></figure>

app\Controllers\PostController.php

```php
<?php
namespace App\Controllers;
use App\BaseController;
use App\Models\Post;
use App\Request;
use Exception;
class PostController extends BaseController {
  public function index(Request $request): void
  {
    $idifitier = $request->getInput("page")[0];
    $post = new Post(db: $this->db);
    $post->find($idifitier);
    $this->view->render("posts/index", [
      "post" => $post
    ]);
  }
  public function edit(Request $request) {
    $identifier = $request->getInput("page")[1];
    $post = new Post(db: $this->db);
    $post->find($identifier);
    if($request->getMethod() != "POST") {
      $this->view->render("posts/edit", [
        "post"=> $post
      ]);
      return;
    }
    $inputForm = $request->getInput();
    if (!$post->update(postData: $inputForm)) {
      die("Not Update");
    }
    $this->redirectTo("/post/{$post->getId()}/{$post->getSlug()}");
  }
  public function create(Request $request) {
    if($request->getMethod() != "POST") {
      $this->view->render("posts/create");
      return;
    }
    $formInput = $request->getInput();
    $fileInput = $request->getInput('file');
    $post = new Post($this->db);
    try {
      $post->add(userId: $this->user->getId(), postData: $formInput, image: $fileInput['image']);
      $this->redirectTo('/dashboard');
    } catch (Exception $e) {
      $this->view->render('posts/create', [
        'errors' => [
          'root' => $e->getMessage()
        ]
      ]);
    }
  }
}
```

app\views\posts\create.php

```php
<div class="container">
  <h1>Write Your Post</h1>
  <form method="post" enctype="multipart/form-data">
    <div>
      <label for="title" class="form-label">Title</label>
      <input type="text" id="title" name="title" class="form-control">
    </div>
    <div>
      <label for="body" class="form-label">Post Body</label>
      <textarea name="body" id="body" class="form-control"></textarea>
    </div>
    <div>
      <label for="image" class="form-label">Image</label>
      <input type="file" id="image" name="image" class="form-control">
    </div>
    <input type="submit" value="Create Post" class="mt-3 btn btn-success">
  </form>
</div>
```

app\Models\Post.php

```php
<?php
namespace App\Models;
use App\Helpers\Str;
use App\Config;
class Post
{
  private Database $db;
  private $id;
  private $userId;
  private string $title;
  private $slug;
  private $body;
  private $createdAt;
  public function __construct(Database $db, $data = [])
  {
    $this->db = $db;
    $this->fill($data);
  }
  public function fill($data): void
  {
    foreach ($data as $column => $value) {
      $columnCamel = Str::toCamelCase($column);
      $this->{$columnCamel} = $value;
    }
  }
  public function find(int $idifitier): bool
  {
    $sql = "SELECT * FROM `posts` WHERE `id` = :id";
    $postQuery = $this->db->query($sql, ['id' => $idifitier]);
    if (!$postQuery->count()) {
      return false;
    }
    $postData = $postQuery->first();
    $this->fill($postData);
    return true;
  }
  public function getTitle(): string
  {
    return $this->title;
  }
  public function getId(): int
  {
    return $this->id;
  }
  public function getSlug(): string
  {
    return $this->slug;
  }
  public function getBody(): string {
    return $this->body;
  }
  public function update(array $postData) {
    $sql = "UPDATE `posts` SET `title`=:title, `slug`=:slug,`body` =:body WHERE `id` = :id";
    $slug = Str::slug($postData['title']);
    $postData = [
      'id' => $this->getId(),
      'title'=> $postData['title'],
      'slug'=> $slug,
      'body'=> $postData['body'],
    ];
    $updateQuery = $this->db->query($sql, $postData);
    $this->fill($postData);
    return (bool) ($updateQuery->count());
  }
  public function add(int $userId, array $postData, array $image) {
    $sql = "INSERT INTO `posts` (`user_id`, `title`, `slug`, `body`, `created_at`) VALUES (:userId, :title, :slug, :body, :createdAt)";
    $slug = Str::slug($postData['title']);
    $this->db->query($sql, [
      'userId' => $userId,
      'title' => $postData['title'],
      'slug' => $slug,
      'body' => $postData['body'],
      'createdAt' => time()
    ]);
    $sql = "SELECT MAX(`id`) AS 'id' FROM `posts` WHERE `user_id` = :user_id";
    $postQuery = $this->db->query($sql, ['user_id' => $userId]);
    $postId = $postQuery->first()['id'];
    $fileStorage = new FileStorage(file: $image);
    $fileStorage->saveIn(Config::get('app.uploadFolder'));
    $imageName = $fileStorage->getGeneratedName();
    $sql = "INSERT INTO `post_images` (`post_id`, `filename`, `created_at`) VALUES (:post_id, :filename, :created_at)";
    $this->db->query($sql, [
      'post_id' => $postId,
      'filename' => $imageName,
      'created_at' => time()
    ]);
  }
}

```

app\Models\Post.php

```php
<?php
namespace App\Models;
use App\Models\Database;
use App\Helpers\Str;
use App\Models\User;
use App\Config;
use App\Models\FileStorage;
class Post
{
  private Database $db;
  private string $id;
  private string $userId;
  private string $title;
  private string $slug;
  private string $body;
  private string $createdAt;
  public function __construct(Database $db, ?array $data = [])
  {
    $this->db = $db;
    $this->fill($data);
  }
  public function find(int $identifier): bool
  {
    $sql = "SELECT * FROM `posts` WHERE `id` = :identifier";
    $postQuery = $this->db->query($sql, ['identifier' => $identifier]);
    if (!$postQuery->count()) {
      return false;
    }
    $this->fill($postQuery->first());
    return true;
  }
  public function fill(array $data)
  {
    foreach ($data as $field => $value) {
      $this->{Str::toCamelCase($field)} = $value;
    }
  }
  public function create(int $userId, array $postData, array $image)
  {
    $sql = "
            INSERT INTO `posts`
            (`user_id`, `title`, `slug`, `body`, `created_at`)
            VALUES (:userId, :title, :slug, :body, :createdAt)
        ";
    $slug = Str::slug($postData['title']);
    $this->db->query($sql, [
      'userId' => $userId,
      'title' => $postData['title'],
      'slug' => $slug,
      'body' => $postData['body'],
      'createdAt' => time()
    ]);
    $sql = "SELECT MAX(`id`) AS 'id' FROM `posts` WHERE `user_id` = :user_id";
    $postQuery = $this->db->query($sql, ['user_id' => $userId]);
    $postId = $postQuery->first()['id'];
    $fileStorage = new FileStorage($image);
    $fileStorage->saveIn(Config::get('app.uploadFolder'));
    $imageName = $fileStorage->getGeneratedName();
    $sql = "
            INSERT INTO `post_images`
            (`post_id`, `filename`, `created_at`)
            VALUES (:post_id, :filename, :created_at)
        ";
    $this->db->query($sql, [
      'post_id' => $postId,
      'filename' => $imageName,
      'created_at' => time()
    ]);
  }
  public function edit(array $postData): bool
  {
    $sql = "
            UPDATE `posts`
            SET `title` = :title, `slug` = :slug, `body` = :body
            WHERE `id` = :id
        ";
    $slug = Str::slug($postData['title']);
    $postData = [
      'id' => $this->getId(),
      'title' => $postData['title'],
      'slug' => $slug,
      'body' => $postData['body']
    ];
    $editQuery = $this->db->query($sql, $postData);
    $this->fill($postData);
    return (bool) $editQuery->count();
  }
  public function delete(): bool
  {
    $images = $this->getImages();
    foreach ($images as $image) {
      FileStorage::delete($image);
    }
    $sql = "DELETE FROM `posts` WHERE `id` = :id";
    $deleteQuery = $this->db->query($sql, ['id' => $this->getId()]);
    return (bool) $deleteQuery->count();
  }
  public function like(int $userId): bool
  {
    $sql = "
            INSERT INTO `post_likes`
            (`user_id`, `post_id`, `created_at`)
            VALUES (:user_id, :post_id, :created_at)
        ";
    $likeQuery = $this->db->query($sql, [
      'user_id' => $userId,
      'post_id' => $this->getId(),
      'created_at' => time()
    ]);
    return (bool) $likeQuery->count();
  }
  public function dislike(int $userId): bool
  {
    $sql = "DELETE FROM `post_likes` WHERE `post_id` = :post_id AND `user_id` = :user_id";
    $deleteQuery = $this->db->query($sql, [
      'post_id' => $this->getId(),
      'user_id' => $userId
    ]);
    return (bool) $deleteQuery->count();
  }
  public function getTotalLikes(): int
  {
    $sql = "SELECT COUNT(`id`) as 'like_count' FROM `post_likes` WHERE `post_id` = :post_id";
    $likesQuery = $this->db->query($sql, [
      'post_id' => $this->getId()
    ]);
    return (int) $likesQuery->first()['like_count'];
  }
  public function isLikedBy(int $userId): bool
  {
    $sql = "SELECT 1 FROM `post_likes` WHERE `post_id` = :post_id AND `user_id` = :user_id";
    $likeQuery = $this->db->query($sql, [
      'post_id' => $this->getId(),
      'user_id' => $userId
    ]);
    return (bool) $likeQuery->count();
  }
  public function getId(): int
  {
    return (int) $this->id;
  }
  public function getUserId(): int
  {
    return (int) $this->userId;
  }
  public function getTitle(): string
  {
    return $this->title;
  }
  public function getSlug(): string
  {
    return $this->slug;
  }
  public function getBody(): string
  {
    return $this->body;
  }
  public function getCreatedAt(): string
  {
    return date('D, d.m.Y H:i:s', $this->createdAt);
  }
  public function getUser(): User
  {
    $user = new User($this->db);
    $user->find($this->getUserId());
    return $user;
  }
  public function getImages(): array
  {
    $sql = "SELECT `filename` FROM `post_images` WHERE `post_id` = :post_id";
    $query = $this->db->query($sql, ['post_id' => $this->getId()]);
    $images = array_map(function ($image) {
      return DIRECTORY_SEPARATOR . Config::get('app.uploadFolder') . DIRECTORY_SEPARATOR . $image['filename'];
    }, $query->results());
    return $images;
  }
}

```

app\Config.php

```php
<?php
namespace App;
class Config
{
  private static array $options = [
    'app' => [
      'uploadFolder' => 'images'
    ],
    'database' => [
      'host' => 'localhost',
      'name' => 'forum',
      'username' => 'root',
      'password' => '',
      'charset' => 'utf8mb4'
    ]
  ];
  public static function get(string $selector)
  {
    $elements = explode(separator: '.', string: $selector);
    $dataset = self::$options;
    foreach ($elements as $element) {
      $dataset = $dataset[$element];
    }
    return $dataset;
  }
}

```

<figure><img src="/files/XeY45BbpTSPvuYXXiJTL" alt=""><figcaption></figcaption></figure>

```
$fileInput = $request->getInput('file');
echo '<pre>';
var_export($fileInput);
echo '</pre>';
die("gg");
array (
  'image' => 
  array (
    'name' => '5.jpg',
    'full_path' => '5.jpg',
    'type' => 'image/jpeg',
    'tmp_name' => 'C:\\xampp82\\tmp\\php49D1.tmp',
    'error' => 0,
    'size' => 312563,
  ),
)
```

app\Models\FileStorage.php

```php
<?php
namespace App\Models;
use App\Helpers\Str;
use Exception;
class FileStorage
{
  private array $file;
  private string $extension;
  private string $currentLocation;
  private string $generatedName;
  public function __construct(array $file)
  {
    $this->file = $file;
    $this->extension = strtolower(string: pathinfo(path: $this->file['name'], flags: PATHINFO_EXTENSION));
    $this->currentLocation = $this->file['tmp_name'];
    $this->generatedName = Str::token() . '.' . $this->extension;
  }
  public function getGeneratedName(): string
  {
    return $this->generatedName;
  }
  public function saveIn(string $folder): void
  {
    $destination = "{$folder}/{$this->generatedName}";
    if (!move_uploaded_file(from: $this->currentLocation, to: $destination)) {
      throw new Exception(message: 'We encountered an error uploading the file.');
    }
  }
  public static function delete(string $path): bool
  {
    return unlink(filename: ltrim(string: $path, characters: DIRECTORY_SEPARATOR));
  }
}

```

app\Helpers\Str.php

```php
<?php
namespace App\Helpers;
class Str
{
  public static function toPascalCase(string $subject): string
  {
    return str_replace('_', '', ucwords($subject, '_'));
  }
  public static function toCamelCase(string $subject): string
  {
    return lcfirst(self::toPascalCase($subject));
  }
  public static function slug(string $string)
  {
    $disallowedCharacters = '/[^\-\s\pN\pL]+/';
    $spacesDuplicateHyphens = '/[\-\s]+/';
    $slug = mb_strtolower(string: $string, encoding: 'UTF-8');
    $slug = preg_replace(pattern: $disallowedCharacters, replacement: '', subject: $slug);
    $slug = preg_replace(pattern: $spacesDuplicateHyphens, replacement: '-', subject: $slug);
    $slug = trim(string: $slug, characters: '-');
    return $slug;
  }
  public static function token(): string
  {
    return bin2hex(string: random_bytes(length: 16));
  }
}

```

{% file src="/files/gO0bvuZdvoYSOn11aVTA" %}

## Project 2  ([php-crud-mvc](https://github.com/rizkhal/php-crud-mvc))

### 1.  Load controller, method, params&#x20;

<figure><img src="/files/Y3aePixeDOVNyikaIrHb" alt=""><figcaption></figcaption></figure>

public\index.php

```php
<?php
require '../app/init.php';
$app = new App();
$app->run();
```

app\init.php

```php
<?php
require_once 'config/config.php';
require_once 'core/App.php';
require_once 'core/Controller.php';
```

app\config\config.php

```php
<?php
define('BASEURL', 'https://lva2.com');
define('DB_HOST', 'localhost');
define('DB_USER', 'root');
define('DB_PASS', '');
define('DB_NAME', 'lva1');
```

app\core\App.php

```php
<?php
class App
{
	protected $controller = 'homeController';
	protected $method = 'index';
	protected $params = [];
	public function __construct()
	{
		$url = $this->parseURL();
		if (file_exists(filename: '../app/controllers/' . $url[0] . '.php')) {
			$this->controller = $url[0];
			unset($url[0]);
		} else {
			die("{$url[0]} not found");
		}
		require_once '../app/controllers/' . $this->controller . '.php';
		$this->controller = new $this->controller;
		if (isset($url[1])) {
			if (method_exists(object_or_class: $this->controller, method: $url[1])) {
				$this->method = $url[1];
				unset($url[1]);
			}
		}
		if (!empty($url)) {
			$this->params = array_values(array: $url);
		}
	}
	public function parseURL()
	{
		if (isset($_GET['url'])) {
			$url 	= explode(separator: '/', string: filter_var(value: trim(string: $_GET['url']), filter: FILTER_SANITIZE_URL));
			$url[0] = $url[0] . 'Controller';
		} else {
			$url[0] = 'homeController';
		}
		return $url;
	}
	public function run()
	{
		return call_user_func_array(callback: [$this->controller, $this->method], args: $this->params);
	}
}

```

app\core\Controller.php

```php
<?php
class Controller
{
	public function view($view, $data = [])
	{
		require_once '../app/views/' . $view . '.php';
	}
	public function model($model)
	{
		require_once '../app/models/' . $model . '.php';
		return new $model;
	}
	public function redirect($url)
	{
		header('Location: ' . BASEURL . '/' . $url);
		exit;
	}
}
```

app\controllers\homeController.php

```php
<?php
class homeController extends Controller
{
	public function index(): void
	{
		$data['title'] = 'Lionel Home';
		echo $data['title'];
	}
}

```

{% file src="/files/DRhe9wP9GFwvhiYH4ux3" %}

### 2. Show data mahasiswa to index.php

<figure><img src="/files/6oZZjJVzRbR1ntmiCRqt" alt=""><figcaption></figcaption></figure>

app\controllers\mahasiswaController.php

```php
<?php
class mahasiswaController extends Controller {
  public function index() {
    $data['title'] = 'Halaman mahasiswa';
		$data['judul'] = 'Data mahasiswa';
		$data['mahasiswa'] = $this->model('Mahasiswa')->getAll();
    $this->view('templates/header', $data);
		$this->view('mahasiswa/index', $data);
		$this->view('templates/footer');
  }
}
```

app\core\App.php

```php
<?php
class App
{
	protected $controller = 'homeController';
	protected $method = 'index';
	protected $params = [];
	public function __construct()
	{
		$url = $this->parseURL();
		if (file_exists(filename: '../app/controllers/' . $url[0] . '.php')) {
			$this->controller = $url[0];
			unset($url[0]);
		} else {
			die("{$url[0]} not found");
		}
		require_once '../app/controllers/' . $this->controller . '.php';
		$this->controller = new $this->controller;
		if (isset($url[1])) {
			if (method_exists(object_or_class: $this->controller, method: $url[1])) {
				$this->method = $url[1];
				unset($url[1]);
			}
		}
		if (!empty($url)) {
			$this->params = array_values(array: $url);
		}
	}
	public function parseURL()
	{
		if (isset($_GET['url'])) {
			$url 	= explode(separator: '/', string: filter_var(value: trim(string: $_GET['url']), filter: FILTER_SANITIZE_URL));
			$url[0] = $url[0] . 'Controller';
		} else {
			$url[0] = 'homeController';
		}
		return $url;
	}
	public function run()
	{
		return call_user_func_array(callback: [$this->controller, $this->method], args: $this->params);
	}
}

```

app\core\Controller.php

```php
<?php
class Controller
{
	public function view($view, $data = [])
	{
		require_once '../app/views/' . $view . '.php';
	}
	public function model($model)
	{
		require_once '../app/models/' . $model . '.php';
		return new $model;
	}
	public function redirect($url)
	{
		header('Location: ' . BASEURL . '/' . $url);
		exit;
	}
}
```

app\core\Database.php

```php
<?php
class Database {
  private $host 	= DB_HOST;
  private $dbname 	= DB_NAME;
	private $username 	= DB_USER;
	private $password 	= DB_PASS;
  private $db;
  private $st;
  public function __construct()
	{
    try {
			$this->db = new PDO(dsn: "mysql:host={$this->host};dbname={$this->dbname}", username: $this->username, password: $this->password);
		} catch (PDOException $e) {
			print_r(value: $e->getMessage());
			die;
		}
  }
  public function query($query): void
	{
		$this->st = $this->db->prepare($query);
	}
  public function execute(): void
	{
		$this->st->execute();
	}
  public function resultSet(): array
	{
		$this->execute();
		return $this->st->fetchAll(PDO::FETCH_ASSOC);
	}
  public function single(): array
	{
		$this->execute();
		return $this->st->fetch(PDO::FETCH_ASSOC);
	}
}
```

app\models\Mahasiswa.php

```php
<?php
class Mahasiswa {
  private $db;
	private $table = 'mahasiswa';
  public function __construct()
	{
		$this->db = new Database;
	}
  public function getAll()
	{
		$this->db->query("SELECT * FROM {$this->table}");
		return $this->db->resultSet();
	}
}
```

app\views\mahasiswa\index.php

```php
<div class="container">
	<div class="jumbotron mt-4">
		<h1 class="display-4"><?= $data['judul']; ?></h1>
		<a href="<?= BASEURL ?>/mahasiswa/create" class="btn btn-sm btn-primary">
			Add
		</a>
		<div class="table-responsive mt-5">
			<table class="table">
				<thead>
					<tr>
						<th scope="col">#</th>
						<th scope="col">Name</th>
						<th scope="col">Email</th>
						<th scope="col">Action</th>
					</tr>
				</thead>
				<tbody>
					<?php foreach ($data['mahasiswa'] as $key => $value): ?>
						<tr>
							<th scope="row"><?= ++$key; ?></th>
							<td><?= $value['name']; ?></td>
							<td><?= $value['email']; ?></td>
							<td style=" display: flex; gap: 10px; ">
								<a href="<?= BASEURL; ?>/mahasiswa/edit/<?= $value['id']; ?>" class="btn btn-xs btn-info">Edit</a>
								<form action="<?= BASEURL; ?>/mahasiswa/destroy/<?= $value['id'] ?>" method="post">
									<button class="btn btn-xs btn-danger">Delete</button>
								</form>
							</td>
						</tr>
					<?php endforeach; ?>
				</tbody>
			</table>
		</div>
	</div>
</div>
```

app\views\templates\header.php

```php
<!DOCTYPE html>
<html lang="en">
<head>
	<meta charset="UTF-8">
	<title><?= $data['title'] ?></title>
	<link rel="stylesheet" href="<?= BASEURL; ?>/assets/css/bootstrap.min.css">
</head>
<body>
<nav class="navbar navbar-expand-lg navbar-light bg-light">
  <div class="container">
  		<a class="navbar-brand" href="#">LOGO</a>
	  <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
	    <span class="navbar-toggler-icon"></span>
	  </button>
	  <div class="collapse navbar-collapse" id="navbarNav">
	    <ul class="navbar-nav">
	      <li class="nav-item active">
	        <a class="nav-link" href="<?= BASEURL;  ?>">Home <span class="sr-only">(current)</span></a>
	      </li>
	      <li class="nav-item">
	        <a class="nav-link" href="<?= BASEURL;  ?>/mahasiswa">Mahasiswa</a>
	      </li>
	      <li class="nav-item">
	        <a class="nav-link" href="<?= BASEURL;  ?>/about">About</a>
	      </li>
	    </ul>
	  </div>
  </div>
</nav>
```

app\views\templates\footer.php

```php
<script src="https://lva1.com/assets/js/jquery.slim.min.js"></script>
<script src="https://lva1.com/assets/js/popper.min.js"></script>
<script src="https://lva1.com/assets/js/bootstrap.min.js"></script>
</body>
</html>
```

app\init.php

```php
<?php
require_once 'config/config.php';
require_once 'core/Database.php';
require_once 'core/App.php';
require_once 'core/Controller.php';
```

{% file src="/files/RuCR3INLvaxd139YbAhB" %}


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://learnphp.gitbook.io/learnphp/advanced/du-an-xay-dung-mvc-php-ok.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
