This project is meant to teach PhP fundamentals by creating a blog, step by step. It uses the Bootstrap framework and stores its data in JSON files.
This is how the final app looks like:
Follow these steps to continuously build a server-side Blog web app with PhP.
Create 3 model classes in the 'model' folder: Post
, User
and Category
.
Write scripts in the scripts
folder that generate test data in JSON format in the data
folder.
Create controllers in the controllers
folder that load and display the JSON data.
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.
Chose a UI framework. We recommend:
- Bootstrap or Material Design Lite
- search for a demo layout page and copy its HTML code to the
views\posts.php
file.
For example: AdminLTE starter page - open the
controllers\posts.php
page in your browser and check the result. - correct all the CSS, JS and image references using CDN links (for starters).
- remove all unnecessary UI elements and place your own labels.
Create a layout template
. To do so, follow these steps:
- create a
views\partials
sub-directory. - cut out the code of HTML
<head>
, top navigation, sidebar and footer and paste it into their corresponding PhP files. require
all partials in theloadView()
function.- create a
public
folder andcss
,img
,js
andwebfonts
sub-folders. Download the corresponding resources to these directories and modify the links to use the local copies. - tell the PhP server to use the
public
directory with these parameters:
php -S localhost:8080 -t public
Implement a basic router:
- inside
public\index.php
, insert this code:
$uri = parse_url($_SERVER['REQUEST_URI'])['path']; $routes = [ '/' => '../controllers/posts.php', '/posts' => '../controllers/posts.php', '/categories' => '../controllers/categories.php', '/users' => '../controllers/users.php' ]; if(array_key_exists($uri, $routes)) { require $routes[$uri]; } else { http_response_code(404); loadView("404"); }
- add an error page:
view\404.php
- add dynamic titles by adding a
$title
parameter to theloadView($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:<h1 class="m-0"><?= $title ?></h1>
- place the proper page links in the top-navbar and sidebar:
<a href="/posts" class="nav-link">Posts</a>
- highlight the current nav link in the sidebar by checking the
$view
variable:<a href="/posts" class="nav-link <?= $view=='posts'? 'bg-indigo' : ''?>">
Implement the create post
feature. For this:
- create the
controllers\posts
directory and movecontrollers\posts.php
toconstrollers\posts\index.php
. Correct all necessary links. - create the
views\posts
directory and moveviews\posts.php
toviews\posts\index.php
. Correct all necessary links. - create both
controllers\posts\create.php
andviews\posts\create.php
. - in
public\index.php
, add this route to the$routes
:
'/posts/create' => BASE_PATH . '/controllers/posts/create.php'
- in
views\posts\create.php
, insert a HTML form with these fields:title
as simple input field,categories[]
as multiple select box andbody
as textarea. You may use a rich text editor like TinyMCE.- you may add jQuery Validation to it.
- add
method="post" action="/posts/save"
to the<form>
tag. - add
'/posts/save' => BASE_PATH . '/controllers/posts/store.php'
to the$routes
.
- in
functions.php
, write asaveData($key, $newEntry)
function. - implement
controllers\posts\store.php
:- create a new Post with the fields submitted in the form:
$newPost = new Post($_POST['title'], $_POST['body'], $_POST['userId'], $_POST['categories'])
- save that post:
saveData('posts', $newPost)
- redirect to the posts overview page:
header("location: /posts")
- create a new Post with the fields submitted in the form:
Implement the 'post detail' view:
- create a new dynamic rule in the router:
// check if `$uri` starts with 'posts' if (strpos($uri, "/posts/") == 0) { // parse any ID after the slash $postId = intval(substr($uri, 7)); if ($postId) { require BASE_PATH . '/controllers/posts/read.php'; } }
- 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])
- create
views\posts\read.php
, showing the post's details.
Add aclose
button to return to the overview.
Implement the 'delete post' feature:
- add a new rule to the routes:
'/posts/delete' => BASE_PATH . '/controllers/posts/delete.php'
- 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)
- upon button click, show a dialog that asks: 'Do you really want to delete this post?'.
- in the dialog, implement a small
<form method="post" action="/posts/delete">
with a hidden<input name="postId" type="hidden" value="<?= $post->id ?>">
. - create the
controllers\posts\delete.php
controller and insert this code:$postId = $_POST['postId']; unset($GLOBALS['posts'][$postId]); saveData('posts'); header("location: /posts");
Implement the 'edit existing post' feature:
- add a new rule to the routes:
'/posts/update' => BASE_PATH . '/controllers/posts/update.php'
- remember the current user id in a session variable. To do so, insert this code at the beginning of
loadView()
:if (!isset($_SESSION['currentUser'])) { session_start(); // start with UserId = 2 $_SESSION['currentUser'] = 2; }
- only a post's author should have the right to edit it. So in
views\posts\read.php
, add an 'Edit' button that is only visible if the current user is identical to the post's user:
if($_SESSION['currentUser'] == $post->userId)
- add a small
<form method="post" action="/posts/update">
with a hidden<input name="postId" type="hidden" value="<?= $post->id ?>">
. - create the
controllers\posts\update.php
controller and insert this code:$postId = $_POST['postId']; $post = $GLOBALS['posts'][$postId]; if (!$post) { header("location: /posts"); } loadView("posts/edit", "[Edit] " . $post->title, ['post' => $post]);
- in
controllers\posts\create.php
, create a new, emptyPost
and pass it to the view:$newPost = new Post("", "", null, []); loadView("posts/edit", "New blog post", ['post' => $newPost]);
- rename
views\posts\create.php
toedit.php
. Insert these hidden fields right after the<form>
:<input type="hidden" name="isExistingPost" value="<?=$post->userId ?>"> <input type="hidden" name="id" value="<?= $post->id ?>">
- fill all fields' values with the post's data:
<input name="title" value="<?= $post->title ?>"> <select name="categories[]"> <?php foreach ($GLOBALS['categories'] as $category) : ?> <option value="<?= $category->id ?>" <?= $post->categories && in_array($category->id, $post->categories) ? 'selected' : '' ?> > <?= $category->name ?> </option> <?php endforeach; ?> <textarea name="body"><?= $post->body ?></textarea>
- modify the code in
controllers\posts\save.php
:$isExistingPost = $_POST['isExistingPost']; // remove temporary field unset($_POST['isExistingPost']); if ($isExistingPost) { $postId = $_POST['id']; $post = $GLOBALS['posts'][$postId]; if ($post) { // update the existing post with the <form> fields $GLOBALS['posts'][$postId] = array_merge((array)$post, $_POST); saveData('posts'); } } else { $newPost = new Post($_POST['title'], $_POST['body'], intval($_POST['userId']), $_POST['categories']); saveData('posts', $newPost); } header("location: /posts");
Description will follow...