From 3303676bdf1a06fc21d2ab92b7dbd627b7b20d7a Mon Sep 17 00:00:00 2001 From: Seweryn Czabanowski Date: Wed, 6 Dec 2023 20:46:29 +0100 Subject: [PATCH 1/8] Update README --- ReadMe.md | 118 ++++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 84 insertions(+), 34 deletions(-) diff --git a/ReadMe.md b/ReadMe.md index 6b16ef0..b6a4726 100644 --- a/ReadMe.md +++ b/ReadMe.md @@ -1,47 +1,85 @@ -## PhP course +

PHP Course: Build a Blogging Platform

-This project is meant to teach PhP fundamentals by creating a blog, step by step. It uses the [Bootstrap](https://getbootstrap.com/docs/4.6/getting-started/introduction/) framework and stores its data in [JSON](https://en.wikipedia.org/wiki/JSON) files. +> This project is a step-by-step guide to building a blogging platform using PHP. It's designed to teach PHP fundamentals through practical application. The project uses the [Bootstrap](https://getbootstrap.com/docs/4.6/getting-started/introduction/) framework and stores data in [JSON](https://en.wikipedia.org/wiki/JSON) files. -This is how the final app looks like: +## Preview of the Final Application + +Here's a preview of the final application: ![Travel Blog Screenshot](public/img/Screenshot.jpg) -Follow these steps to continuously build a server-side Blog web app with PhP. +## Table of Contents + +- [Preview of the Final Application](#preview-of-the-final-application) +- [Table of Contents](#table-of-contents) +- [Getting Started](#getting-started) + - [1. Data model](#1-data-model) + - [2. Test data](#2-test-data) + - [3. Controllers](#3-controllers) + - [4. Views](#4-views) + - [5. UI framework](#5-ui-framework) + - [6. Layout template](#6-layout-template) + - [7. Router](#7-router) + - [8. 'Create post' feature](#8-create-post-feature) + - [9. 'Post detail' view](#9-post-detail-view) + - [10. 'Delete post' feature](#10-delete-post-feature) + - [11. 'Edit existing post' feature](#11-edit-existing-post-feature) + - [12. 'Create and edit users' feature](#12-create-and-edit-users-feature) + +## Getting Started + +To start the project, follow these steps: + +1. Clone the repository to your local machine. +2. Open the project in your IDE. +3. Open a terminal and run `php -S localhost:8000 -t public`. +4. Open `http://localhost:8000` in your browser. ### 1. Data model + Create 3 model classes in the 'model' folder: `Post`, `User` and `Category`. -### 2. Test data +### 2. Test data + Write scripts in the `scripts` folder that generate test data in JSON format in the `data` folder. ### 3. Controllers + Create controllers in the `controllers` folder that load and display the JSON data. ### 4. Views + Create PHP/HTML views in the `views` folder. Write a `view` function that loads data from the corresponding JSON file and loads the proper view template to display it. ### 5. UI framework + Chose a UI framework. We recommend:
+ 1. [Bootstrap](https://getbootstrap.com/) or [Material Design Lite](https://getmdl.io/started/index.html) 2. search for a demo layout page and copy its HTML code to the `views\posts.php` file.
-For example: [AdminLTE starter page](https://adminlte.io/themes/v3/starter.html) + For example: [AdminLTE starter page](https://adminlte.io/themes/v3/starter.html) 3. open the `controllers\posts.php` page in your browser and check the result. 4. correct all the CSS, JS and image references using CDN links (for starters). 5. remove all unnecessary UI elements and place your own labels. ### 6. Layout template + Create a `layout template`. To do so, follow these steps: -1. create a `views\partials` sub-directory. + +1. create a `views\partials` sub-directory. 2. cut out the code of HTML ``, top navigation, sidebar and footer and paste it into their corresponding PhP files. 3. `require` all partials in the `loadView()` function. 4. create a `public` folder and `css`, `img`, `js` and `webfonts` sub-folders. Download the corresponding resources to these directories and modify the links to use the local copies. 5. tell the PhP server to use the `public` directory with these parameters:
-`php -S localhost:8080 -t public` - -### 7. Router -Implement a basic router: + `php -S localhost:8080 -t public` + +### 7. Router + +Implement a basic router: + 1. inside `public\index.php`, insert this code:
- ``` + + ```php $uri = parse_url($_SERVER['REQUEST_URI'])['path']; $routes = [ '/' => '../controllers/posts.php', @@ -57,22 +95,25 @@ Implement a basic router: loadView("404"); } ``` + 2. add an error page: `view\404.php` 3. add dynamic titles by adding a `$title` parameter to the `loadView($view, $title)` function. Pass a title in the corresponding controller. Example: -`loadView("posts", "Memories of our travels")`
-Display that title in the view by using the `$title` variable: `

` + `loadView("posts", "Memories of our travels")`
+ Display that title in the view by using the `$title` variable: `

` 4. place the proper page links in the top-navbar and sidebar: `Posts` 5. highlight the current nav link in the sidebar by checking the `$view` variable: `` - + ### 8. 'Create post' feature + Implement the `create post` feature. For this: + 1. create the `controllers\posts` directory and move `controllers\posts.php` to `constrollers\posts\index.php`. Correct all necessary links. 2. create the `views\posts` directory and move `views\posts.php` to `views\posts\index.php`. Correct all necessary links. 3. create both `controllers\posts\create.php` and `views\posts\create.php`. 4. in `public\index.php`, add this route to the `$routes`:
-`'/posts/create' => BASE_PATH . '/controllers/posts/create.php'` + `'/posts/create' => BASE_PATH . '/controllers/posts/create.php'` 5. in `views\posts\create.php`, insert a HTML form with these fields: - 1. `title` as simple input field, + 1. `title` as simple input field, 2. `categories[]` as multiple select box and 3. `body` as textarea. You may use a rich text editor like [TinyMCE](https://www.tiny.cloud/). 4. you may add [jQuery Validation](https://jqueryvalidation.org/) to it. @@ -81,14 +122,16 @@ Implement the `create post` feature. For this: 6. in `functions.php`, write a `saveData($key, $newEntry)` function. 7. implement `controllers\posts\store.php`: 1. create a new Post with the fields submitted in the form:
- `$newPost = new Post($_POST['title'], $_POST['body'], $_POST['userId'], $_POST['categories'])` + `$newPost = new Post($_POST['title'], $_POST['body'], $_POST['userId'], $_POST['categories'])` 2. save that post: `saveData('posts', $newPost)` 3. redirect to the posts overview page: `header("location: /posts")` ### 9. 'Post detail' view + Implement the 'post detail' view: + 1. create a new dynamic rule in the router: - ``` + ```php // check if `$uri` starts with 'posts' if (strpos($uri, "/posts/") == 0) { // parse any ID after the slash @@ -99,22 +142,24 @@ Implement the 'post detail' view: } ``` 2. implement `controllers\posts\read.php`:
-Get the current post by its id: `$post = $GLOBALS['posts'][$postId]`. -Load the detail view and pass the current post: -`loadView("posts/read", $post->title, ['post'=>$post])` + Get the current post by its id: `$post = $GLOBALS['posts'][$postId]`. + Load the detail view and pass the current post: + `loadView("posts/read", $post->title, ['post'=>$post])` 3. create `views\posts\read.php`, showing the post's details.
-Add a `close` button to return to the overview. + Add a `close` button to return to the overview. ### 10. 'Delete post' feature + Implement the 'delete post' feature: + 1. add a new rule to the routes:
-`'/posts/delete' => BASE_PATH . '/controllers/posts/delete.php'` + `'/posts/delete' => BASE_PATH . '/controllers/posts/delete.php'` 2. in `views\posts\read.php`, add a 'delete' button that is only visible if the current user is identical to the post's user:
-`if($_SESSION['currentUser'] == $post->userId)` -3. upon button click, show a dialog that asks: 'Do you really want to delete this post?'. -4. in the dialog, implement a small `
` with a hidden ``. + `if($_SESSION['currentUser'] == $post->userId)` +3. upon button click, show a dialog that asks: 'Do you really want to delete this post?'. +4. in the dialog, implement a small `` with a hidden ``. 5. create the `controllers\posts\delete.php` controller and insert this code: - ``` + ```php $postId = $_POST['postId']; unset($GLOBALS['posts'][$postId]); saveData('posts'); @@ -122,11 +167,13 @@ Implement the 'delete post' feature: ``` ### 11. 'Edit existing post' feature + Implement the 'edit existing post' feature: + 1. add a new rule to the routes:
`'/posts/update' => BASE_PATH . '/controllers/posts/update.php'` 2. remember the current user id in a session variable. To do so, insert this code at the beginning of `loadView()`: - ``` + ```php if (!isset($_SESSION['currentUser'])) { session_start(); // start with UserId = 2 @@ -137,7 +184,7 @@ Implement the 'edit existing post' feature: `if($_SESSION['currentUser'] == $post->userId)` 4. add a small `` with a hidden ``. 5. create the `controllers\posts\update.php` controller and insert this code: - ``` + ```php $postId = $_POST['postId']; $post = $GLOBALS['posts'][$postId]; if (!$post) { @@ -146,17 +193,17 @@ Implement the 'edit existing post' feature: loadView("posts/edit", "[Edit] " . $post->title, ['post' => $post]); ``` 6. in `controllers\posts\create.php`, create a new, empty `Post` and pass it to the view: - ``` + ```php $newPost = new Post("", "", null, []); loadView("posts/edit", "New blog post", ['post' => $newPost]); ``` 7. rename `views\posts\create.php` to `edit.php`. Insert these hidden fields right after the ``: - ``` + ```php ``` 8. fill all fields' values with the post's data: - ``` + ```php ``` 9. modify the code in `controllers\posts\save.php`: - ``` + + ```php $isExistingPost = $_POST['isExistingPost']; // remove temporary field unset($_POST['isExistingPost']); @@ -187,5 +235,7 @@ Implement the 'edit existing post' feature: } header("location: /posts"); ``` + ### 12. 'Create and edit users' feature + Description will follow... From 4b18ec9a720411caadc8261d686c4df870d2af09 Mon Sep 17 00:00:00 2001 From: Seweryn Czabanowski Date: Wed, 6 Dec 2023 20:46:56 +0100 Subject: [PATCH 2/8] Whitespace --- controllers/posts/create.php | 2 +- views/posts/index.php | 79 ++++++++++++++++++------------------ views/users/index.php | 21 +++++----- 3 files changed, 51 insertions(+), 51 deletions(-) diff --git a/controllers/posts/create.php b/controllers/posts/create.php index 54c526d..befb0ae 100644 --- a/controllers/posts/create.php +++ b/controllers/posts/create.php @@ -1,3 +1,3 @@ $newPost]); \ No newline at end of file +loadView("posts/edit", "New blog post", ['post' => $newPost]); diff --git a/views/posts/index.php b/views/posts/index.php index 9258b90..361b442 100644 --- a/views/posts/index.php +++ b/views/posts/index.php @@ -2,18 +2,20 @@ $colors = array(1 => 'maroon', 2 => 'warning', 3 => 'indigo', 4 => 'navy', 5 => 'success', 6 => 'gray'); ?>
@@ -21,8 +23,7 @@

- + Create post @@ -36,34 +37,34 @@
$post) : - $author = $GLOBALS['users'][$post->userId]; ?> -
-
-
-
- User Image - - title ?> - - name ?> + $author = $GLOBALS['users'][$post->userId]; ?> +
+
+
+
+ User Image + + title ?> + + name ?> on created) ?> -
-
- categories)) { - foreach ($post->categories as $categoryId) { ?> - - - +
+
+ categories)) { + foreach ($post->categories as $categoryId) { ?> + + + -
-
-
- body ?> -
-
-
+ } ?> +
+
+
+ body ?> +
+
+
diff --git a/views/users/index.php b/views/users/index.php index 1a33d9f..b82684d 100644 --- a/views/users/index.php +++ b/views/users/index.php @@ -20,15 +20,15 @@
@@ -36,5 +36,4 @@

-
- +
\ No newline at end of file From 55decb373d633f31a047c8c561195faa831c7667 Mon Sep 17 00:00:00 2001 From: Seweryn Czabanowski Date: Wed, 6 Dec 2023 20:48:42 +0100 Subject: [PATCH 3/8] Improve design --- views/partials/html-head.php | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/views/partials/html-head.php b/views/partials/html-head.php index 2e65367..68abe0d 100644 --- a/views/partials/html-head.php +++ b/views/partials/html-head.php @@ -1,5 +1,6 @@ + @@ -13,20 +14,19 @@ - -
+ +
\ No newline at end of file From 41f67470a8db2e8f9abae0192e375bb59db7fd54 Mon Sep 17 00:00:00 2001 From: Seweryn Czabanowski Date: Wed, 6 Dec 2023 21:00:36 +0100 Subject: [PATCH 4/8] Implement hash algorithm --- controllers/users/save.php | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/controllers/users/save.php b/controllers/users/save.php index 8fa2370..cc6fd25 100644 --- a/controllers/users/save.php +++ b/controllers/users/save.php @@ -1,5 +1,5 @@ fields + // Check if password was provided and hash it + if (!empty($_POST['password'])) { + $_POST['password'] = password_hash($_POST['password'], PASSWORD_DEFAULT); + } + + // Update the existing user with the fields $GLOBALS['users'][$userId] = array_merge((array)$user, $_POST); saveData('users'); } @@ -18,4 +23,4 @@ $newUser = new User($_POST['name'], $_POST['email'], $_POST['password'], $_POST['avatarUrl']); saveData('users', $newUser); } -header("location: /users"); \ No newline at end of file +header("location: /users"); From 7bdd8186012110210dafaa607762c299b8fe70e5 Mon Sep 17 00:00:00 2001 From: Seweryn Czabanowski Date: Wed, 6 Dec 2023 21:00:44 +0100 Subject: [PATCH 5/8] New test data entry --- data/users.json | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/data/users.json b/data/users.json index fb6a8a6..aa78f30 100644 --- a/data/users.json +++ b/data/users.json @@ -22,5 +22,13 @@ "password": "p@$$w0rd23!3", "created": 1701457197, "avatarUrl": "\/img\/avatar.png" + }, + "4": { + "id": 4, + "name": "BobBob", + "email": "bob@email.com", + "password": "$2y$10$39OtZNDIwycfnQrggvu8rOzj\/APxMLtlpNHl.QLsZJhITUkmj0L\/m", + "created": 1701892785, + "avatarUrl": "\/img\/avatar.png" } } \ No newline at end of file From a703a45bc67e13563f1d4997befd8317b8f866af Mon Sep 17 00:00:00 2001 From: Seweryn Czabanowski Date: Wed, 6 Dec 2023 21:00:55 +0100 Subject: [PATCH 6/8] Add helper functions and documentation for later --- model/Category.php | 24 +++++++++++++++++++++--- model/Post.php | 41 +++++++++++++++++++++++++++++++++-------- model/User.php | 31 ++++++++++++++++--------------- 3 files changed, 70 insertions(+), 26 deletions(-) diff --git a/model/Category.php b/model/Category.php index 0affcf6..a3c2075 100644 --- a/model/Category.php +++ b/model/Category.php @@ -13,12 +13,30 @@ class Category /** * Creates a new blog category - * @param $name + * @param string $name */ - public function __construct($name) + public function __construct(string $name) { $this->name = $name; $this->id = self::$id_counter; self::$id_counter++; } -} \ No newline at end of file + + /** + * Get the ID of the category + * @return int + */ + public function getId(): int + { + return $this->id; + } + + /** + * Get the name of the category + * @return string + */ + public function getName(): string + { + return $this->name; + } +} diff --git a/model/Post.php b/model/Post.php index 2b2c1e2..1310d1b 100644 --- a/model/Post.php +++ b/model/Post.php @@ -15,13 +15,14 @@ class Post public $created; /** - * Creates a new blog entry - * @param $title - * @param $body - * @param $userId - * @param $categories + * Creates a new blog post + * @param string $title + * @param string $body + * @param int $userId + * @param array $categories + * @return void */ - public function __construct($title, $body, $userId, $categories = [1, 3]) + public function __construct(string $title, string $body, int $userId, array $categories = [1, 3]) { $this->title = $title; $this->body = $body; @@ -33,9 +34,33 @@ public function __construct($title, $body, $userId, $categories = [1, 3]) Post::$idCounter++; } - public static function setIdCounter($idCounter) + + /** + * Set the ID counter for the Post class + * + * @param int $idCounter The new value for the ID counter + * @return void + */ + public static function setIdCounter(int $idCounter): void { self::$idCounter = $idCounter; } -} \ No newline at end of file + /** + * Get the categories of the post + * @return array + */ + public function getCategories(): array + { + return $this->categories; + } + + /** + * Set the categories of the post + * @param array $categories + */ + public function setCategories(array $categories): void + { + $this->categories = $categories; + } +} diff --git a/model/User.php b/model/User.php index b6080af..48d192a 100644 --- a/model/User.php +++ b/model/User.php @@ -1,4 +1,5 @@ name = $name; $this->email = $email; - $this->password = $password; -// TODO: encrypt password -// $this->password = openssl_encrypt($password, self::$cipher, self::$encryption_key, 0, self::$encryption_iv); + $this->password = password_hash($password, PASSWORD_DEFAULT); $this->avatarUrl = $avatarUrl; $this->id = self::$idCounter; @@ -36,7 +33,13 @@ public function __construct($name, $email, $password, $avatarUrl = "/img/avatar. self::$idCounter++; } - public static function fromObject($user) : User { + /** + * Creates a new user from an object + * @param $user + * @return User + */ + public static function fromObject($user): User + { $instance = new self($user->name, $user->email, $user->password, $user->avatarUrl); $instance->id = $user->id; return $instance; @@ -45,9 +48,8 @@ public static function fromObject($user) : User { * Returns the decrypted password * @return false|string */ - public function getPassword() + public function getPassword(): string { -// $decrypted = openssl_decrypt($this->password, self::$cipher, self::$encryption_key, 0, self::$encryption_iv); return $this->password; } @@ -55,5 +57,4 @@ public static function setIdCounter($idCounter) { self::$idCounter = $idCounter; } - -} \ No newline at end of file +} From 165f79d620ca6d2b09c561ce1db038ff7597e0d3 Mon Sep 17 00:00:00 2001 From: Seweryn Czabanowski Date: Wed, 6 Dec 2023 21:01:14 +0100 Subject: [PATCH 7/8] Remove password values in inputs --- views/users/edit.php | 107 +++++++++++++++++++++++-------------------- 1 file changed, 57 insertions(+), 50 deletions(-) diff --git a/views/users/edit.php b/views/users/edit.php index 1528f05..11ebe55 100644 --- a/views/users/edit.php +++ b/views/users/edit.php @@ -10,72 +10,60 @@
-
+
- User profile picture
+ User profile picture
- +
- +
- +
- -
-
- -
- - -
-
-
+
+
+
- -
-
- - -
-
-
-
- - -
-
+ +
+
+ + +
+
+
+
+ + +
+
@@ -101,36 +89,55 @@ function togglePassword(icon) { } function initForm() { - $.validator.setDefaults({ignore: ''}) + $.validator.setDefaults({ + ignore: '' + }) $('#userForm').validate({ rules: { - name: {required: true, minlength: 4}, - email: {required: true, email: true}, - password: {required: true, minlength: 6}, - password2: {required: true, minlength: 6, equalTo: '#password'}, + name: { + required: true, + minlength: 4 + }, + email: { + required: true, + email: true + }, + password: { + required: true, + minlength: 6 + }, + password2: { + required: true, + minlength: 6, + equalTo: '#password' + }, }, messages: { - name: {required: "Please enter a username"}, + name: { + required: "Please enter a username" + }, email: { required: "Please enter an email address", email: "Please enter a valid email address" }, - password: {required: "Please enter a password"}, + password: { + required: "Please enter a password" + }, password2: { required: "Please re-type the password", equalTo: "Passwords must match", } }, errorElement: 'span', - errorPlacement: function (error, element) { + errorPlacement: function(error, element) { error.addClass('invalid-feedback') element.closest('div').append(error) }, - highlight: function (element, errorClass, validClass) { + highlight: function(element, errorClass, validClass) { $(element).addClass('is-invalid') }, - unhighlight: function (element, errorClass, validClass) { + unhighlight: function(element, errorClass, validClass) { $(element).removeClass('is-invalid') } }) From 236fb6164c445cb8e64c848c4af6ea1d5d7768db Mon Sep 17 00:00:00 2001 From: Seweryn Czabanowski Date: Wed, 6 Dec 2023 21:03:38 +0100 Subject: [PATCH 8/8] Update to include null and int for id (for dev purposes) --- model/Post.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/model/Post.php b/model/Post.php index 1310d1b..2b080e8 100644 --- a/model/Post.php +++ b/model/Post.php @@ -18,11 +18,11 @@ class Post * Creates a new blog post * @param string $title * @param string $body - * @param int $userId + * @param int|null $userId * @param array $categories * @return void */ - public function __construct(string $title, string $body, int $userId, array $categories = [1, 3]) + public function __construct(string $title, string $body, int|null $userId, array $categories = [1, 3]) { $this->title = $title; $this->body = $body;