😘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)

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

Project 1 (custom-cms)

1. Tạo controller HomeController, LoginController, DashboardController

app\Controllers\DashboardController.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
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
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
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
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
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

<!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

</body>
</html>

app\views\dashboard.php

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

app\views\home.php

home.php

app\views\login.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
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
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
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
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
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
use App\App;
session_start();
require_once '../vendor/autoload.php';
$app = new App;

composer.json

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

2. Chi tiết bài viết

app\Router.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
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
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

app\Models\User.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
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

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; ?>

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

app\Controllers\PostController.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
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;
  }
}

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

app\Controllers\PostController.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
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
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
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;
  }
}

6. Create new Post

app\Controllers\PostController.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

<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
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

app\Controllers\PostController.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

<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
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
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
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;
  }
}
$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
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
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));
  }
}

Project 2 (php-crud-mvc)

1. Load controller, method, params

public\index.php

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

app\init.php

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

app\config\config.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
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
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
class homeController extends Controller
{
	public function index(): void
	{
		$data['title'] = 'Lionel Home';
		echo $data['title'];
	}
}

2. Show data mahasiswa to index.php

app\controllers\mahasiswaController.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
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
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
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
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

<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

<!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

<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
require_once 'config/config.php';
require_once 'core/Database.php';
require_once 'core/App.php';
require_once 'core/Controller.php';

Last updated

Was this helpful?