Download as pdf or txt
Download as pdf or txt
You are on page 1of 64

Workshop

II
PHP
v3.0

1
Agenda

1. Object oriented work with database


2. Active Record
3. Twitter
4. Exercises

2
Object oriented
work with
database

3
Goal of the workshop

The goal of this workshop is to learn how to integrate a database with the object oriented programming.
This can be done in many different ways.
The most popular project templates that solve this problem are:
Row Data Gateway,
Active Record,
Data Mapper.
During the workshop your classes will implement the Active Record template so this template will be
described in detail at the beginning.

4
Arrays in database vs. class

The key to understanding how we work with the database is to understand the relationship between our
classes and the arrays in the database.

In most approaches we have the following assumptions:


For each array in our database there is one class that represents it.
The class has the same name as the array and the attributes corresponding to the table columns.
Each object in this class is a representation of one row (record) from an array.
An object can be in two states: synchronized and unsynchronized.

5
Synchronizing objects

An object is synchronized with the database if the An object is not synchronized with the database
data held in its attributes is the same as the data if the data held in its attributes is not the same as
in the corresponding row. the data in the corresponding row or it does not
have a row that suits it.
Synchronization takes place in case of:
loading a row from the database to an object, The object is synchronized in case of:
saving the object to the database. creating a new object (there is no
corresponding order),
If one of our objects is not synchronized with
deletion of a row from the database,
the database at the end of our program's
operation, the changes contained therein will change of any attribute after the last
synchronization.
not be saved to the database!

6
Active Record

7
Active Record

Active Record is one of the simpler patterns, which is used to communicate with the database. For each
table that we use in our program we implement a separate class. The class has attributes corresponding
to the columns of our array. Additionally, the class implements methods for setting and collecting all its
attributes (getters and setters) and methods for communication with the database.
Usually these are:
update() - saves the object to the array (as changes to a pre-existing row of this array),
save() - saves object into an array (as a new row),
delete() - removes an object from the array (i.e. removes a row with the same id as the object).
An object of this class will represent one row in our array. Our object is used both to keep data and
communication with the database as well as to implement all the logic needed further in the program.

8
Active Record

In addition, Active Record usually implements a range of static methods to help us find or load more
data.
Usually they are:
loadAll() - loads all rows from the array, creates a new object on the basis of each row, then
returns arrays with all created objects,
loadById(id) - reads one row from the array (with specified id) and returns the object which is the
representation of this row,
deleteAll() - deletes all data from the table.
Very often loadAll() and loadById(id) methods occur in different variants (e.g. it searches for data
from a column, e.g. loadByName(name) etc.).

9
Active Record - example

As an example we can show the storage of users We want our class to enable to:
in the database. write a new user to the database,
We want our class to store the following data: edit an existing user,
User ID (set by the database - usually delete an existing user,
auto_increment), load the user by his ID,
User name, load the user by his email address (needed for
Password (hashed), logging in),
User’s e-mail (unique in our system). load all users,
change all its attributes.

10
Users array

There is a Users array in the database. It is described as follows:


+------------------+--------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+------------------+--------------+------+-----+---------+----------------+
| id | int(11) | NO | PRI | NULL | auto_increment |
| email | varchar(255) | NO | UNI | NULL | |
| username | varchar(255) | NO | | NULL | |
| hash_pass | varchar(60) | NO | | NULL | |
+------------------+--------------+------+-----+---------+----------------+

11
Users array

There is a Users array in the database. It is described as follows:


+------------------+--------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+------------------+--------------+------+-----+---------+----------------+
| id | int(11) | NO | PRI | NULL | auto_increment |
| email | varchar(255) | NO | UNI | NULL | |
| username | varchar(255) | NO | | NULL | |
| hash_pass | varchar(60) | NO | | NULL | |
+------------------+--------------+------+-----+---------+----------------+
The main key

12
Users array

There is a Users array in the database. It is described as follows:


+------------------+--------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+------------------+--------------+------+-----+---------+----------------+
| id | int(11) | NO | PRI | NULL | auto_increment |
| email | varchar(255) | NO | UNI | NULL | |
| username | varchar(255) | NO | | NULL | |
| hash_pass | varchar(60) | NO | | NULL | |
+------------------+--------------+------+-----+---------+----------------+
The column is unique, we assume that there will be no 2 users with the same email
address ( Key - UNI)

13
User class

In our code, we create a User (remember to keep all classes in a separate folder e.g. /src).
Create a User.php (file (remember to name the file the same as the class it contains - it will be
important from the Autoloader's point of view - you will learn more about it during classes in advanced
PHP).
class User
Our class will have the following attributes (all
{
private):
private $id;
id private $username;
username private $hashPass;
private $email;
hashPass
}
email

14
User class

A very important attribute of our class is the id attribute.


If an object does not have its row in the database (e.g. if we have created a new object but have not yet
written it into the database, or if we remove an object we have previously loaded from the database), the
value of our id will be -1. We choose this number because SQL will never assign such a primary key.
If an object has a corresponding row in the database, this attribute will hold the value of the primary key,
thanks to which we will know to which row in the table the object will be assigned.
A value other than -1 will be assigned only from the data coming from the database.

15
User - constructor

First, we shall write the constructor.


The constructor in our class will not accept any arguments and will set all its attributes to default (empty)
values. Only the newly created user's id will be set to -1.

public function __construct()


{
$this->id = -1;
$this->username = "";
$this->email = "";
$this->hashPass = "";
}

16
User - constructor

First, we shall write the constructor.


The constructor in our class will not accept any arguments and will set all its attributes to default (empty)
values. Only the newly created user's id will be set to -1.

public function __construct()


{
$this->id = -1;
$this->username = "";
$this->email = "";
$this->hashPass = "";
}

Set id to -1 as this object is not connected to any row in the database.

17
Setters and getters

In the next step, we shall write the appropriate getters and setters. Thanks to them we will be able to
have access to our attributes.
We will not write a setter for the Id attribute. We don't want anyone outside our class to be able to
change this attribute. It could cause errors in our database (remember that the id attribute holds the
primary key or the value -1).

There will also be a setter for the password. It will re-hash our password so that it is ready to be saved in
the database.

public function setPassword(string $newPass): void


{
$newHashedPass = password_hash($newPass, PASSWORD_BCRYPT);

$this->hashPass = $newHashedPass;
}

18
Saving a new object to the database

The next step is to enable saving a new object to the database. For this purpose, we will write the
saveToDB() method. This method will accept one argument - a PDO class object, thanks to which we
will be able to call SQL queries.

At the very beginning of this method we have to check if the object is not in our database yet. We will do
this by checking if its id is -1.

19
Saving a new object to the database
public function saveToDB(PDO $conn): bool
{
if ($this->id == -1) { /* Saving new user to DB */
$stmt = $conn->prepare(
'INSERT INTO Users(username, email, hash_pass)
VALUES (:username, :email, :pass)' );
$result = $stmt->execute([ 'username' => $this->username,
'email' => $this->email,'pass' => $this->hashPass]);
if ($result !== false) {
$this->id = $conn->lastInsertId();
return true;
}
}
return false;
}

20
Saving a new object to the database
public function saveToDB(PDO $conn): bool
{
if ($this->id == -1) { /* Saving new user to DB */
$stmt = $conn->prepare(
'INSERT INTO Users(username, email, hash_pass)
VALUES (:username, :email, :pass)' );
$result = $stmt->execute([ 'username' => $this->username,
'email' => $this->email,'pass' => $this->hashPass]);
if ($result !== false) {
$this->id = $conn->lastInsertId();
return true;
}
}
return false;
}

Save the object to the database only if its id is equal to -1

21
Saving a new object to the database
public function saveToDB(PDO $conn): bool
{
if ($this->id == -1) { /* Saving new user to DB */
$stmt = $conn->prepare(
'INSERT INTO Users(username, email, hash_pass)
VALUES (:username, :email, :pass)' );
$result = $stmt->execute([ 'username' => $this->username,
'email' => $this->email,'pass' => $this->hashPass]);
if ($result !== false) {
$this->id = $conn->lastInsertId();
return true;
}
}
return false;
}

If we managed to save an object to the database, we assign it the primary key as id

22
Use Case: save a new user

In order to test if the method written by us is working properly, we will create a new user registration case.
Such a scenario will look like this:
1. create a new object to the User class,
2. fill in the relevant data using setters,
3. we use the saveToDB() method to save the data to the database.
Then you should check if:
1. There is a new entry in the database?
2. The entry in the database has all the data properly set?
3. The object has the correct id id set?

Remember that the database will not allow you to save two users with the same email!

23
Fetching object from the database

Another method to write will be a method that reads one row from the database and converts it into an
object.
It will be a static method of our class (we will call it on the class and not on the object). All methods that
load objects from the database will be static - we do not need any user (object instance) to load other
users.
This method will retrieve as an argument the PDO class object and the id of the object to be loaded, and
will return User, or null (if the id does not appear in our database).
Other methods that load one user will look similar (we can look up for example by email or name and not
by id).

24
Fetching object from the database
public static function loadUserById(PDO $conn, int $id): ?User
{
$stmt = $conn->prepare('SELECT * FROM Users WHERE id=:id');
$result = $stmt->execute(['id' => $id]);
if ($result === true && $stmt->rowCount() > 0) {
$row = $stmt->fetch(PDO::FETCH_ASSOC);
$loadedUser = new User();
$loadedUser->id = $row['id'];
$loadedUser->username = $row['username'];
$loadedUser->hashPass = $row['hash_pass'];
$loadedUser->email = $row['email'];
return $loadedUser;
}
return null;
}

25
Fetching object from the database
public static function loadUserById(PDO $conn, int $id): ?User
{
$stmt = $conn->prepare('SELECT * FROM Users WHERE id=:id');
$result = $stmt->execute(['id' => $id]);
if ($result === true && $stmt->rowCount() > 0) {
$row = $stmt->fetch(PDO::FETCH_ASSOC);
$loadedUser = new User();
$loadedUser->id = $row['id'];
$loadedUser->username = $row['username'];
$loadedUser->hashPass = $row['hash_pass'];
$loadedUser->email = $row['email'];
return $loadedUser;
}
return null;
}

The function is static - we can use it on the class and not on the object.

26
Fetching object from the database
public static function loadUserById(PDO $conn, int $id): ?User
{
$stmt = $conn->prepare('SELECT * FROM Users WHERE id=:id');
$result = $stmt->execute(['id' => $id]);
if ($result === true && $stmt->rowCount() > 0) {
$row = $stmt->fetch(PDO::FETCH_ASSOC);
$loadedUser = new User();
$loadedUser->id = $row['id'];
$loadedUser->username = $row['username'];
$loadedUser->hashPass = $row['hash_pass'];
$loadedUser->email = $row['email'];
return $loadedUser;
}
return null;
}

We create a new user object and set appropriate parameters.

27
Fetching object from the database
public static function loadUserById(PDO $conn, int $id): ?User
{
$stmt = $conn->prepare('SELECT * FROM Users WHERE id=:id');
$result = $stmt->execute(['id' => $id]);
if ($result === true && $stmt->rowCount() > 0) {
$row = $stmt->fetch(PDO::FETCH_ASSOC);
$loadedUser = new User();
$loadedUser->id = $row['id'];
$loadedUser->username = $row['username'];
$loadedUser->hashPass = $row['hash_pass'];
$loadedUser->email = $row['email'];
return $loadedUser;
}
return null;
}

Since we are inside the class, we have access to private properties, despite the fact that
we are working in the static method.
28
Fetching object from the database
public static function loadUserById(PDO $conn, int $id): ?User
{
$stmt = $conn->prepare('SELECT * FROM Users WHERE id=:id');
$result = $stmt->execute(['id' => $id]);
if ($result === true && $stmt->rowCount() > 0) {
$row = $stmt->fetch(PDO::FETCH_ASSOC);
$loadedUser = new User();
$loadedUser->id = $row['id'];
$loadedUser->username = $row['username'];
$loadedUser->hashPass = $row['hash_pass'];
$loadedUser->email = $row['email'];
return $loadedUser;
}
return null;
}

We return a user object or null.

29
Use Case: dowloading user

In order to test if the method written by us is working properly, we will create a new scenario.
Such a scenario will look like this:
Call the static loadUserById() method by entering its id from the database.
Then you should check if:
the method returns the object and not null?
the object has all the same data as stored in the database?
We can also make a second case to check the action:
Call the static loadUserById() method by entering its id not existing in the database.
Then you should check if:
the method returns null?

30
Fetching many objects

Another method to write will be a method that reads all rows from the database and converts it into an
object.
There may be plenty of such methods, e.g. searching for all users who have a name beginning with a
letter, are born on a given day (if of course we keep the date of birth in the database), etc.
This method will retrieve the PDO class object as an argument and will return an array of User class
objects, or an empty array (if no row meets the requirements).

31
Fetching object from the database
public static function loadAllUsers(PDO $conn): array
{
$ret = []; $sql = "SELECT * FROM Users";
$result = $conn->query($sql);
if ($result !== false && $result->rowCount() != 0) {
foreach ($result as $row) {
$loadedUser = new User();
$loadedUser->id = $row['id'];
$loadedUser->username = $row['username'];
$loadedUser->hashPass = $row['hash_pass'];
$loadedUser->email = $row['email'];
$ret[] = $loadedUser;
}
}
return $ret;
}

32
Fetching object from the database
public static function loadAllUsers(PDO $conn): array
{
$ret = []; $sql = "SELECT * FROM Users";
$result = $conn->query($sql);
if ($result !== false && $result->rowCount() != 0) {
foreach ($result as $row) {
$loadedUser = new User();
$loadedUser->id = $row['id'];
$loadedUser->username = $row['username'];
$loadedUser->hashPass = $row['hash_pass'];
$loadedUser->email = $row['email'];
$ret[] = $loadedUser;
}
}
return $ret;
}

Create an empty array and then fill it with objects loaded from the database.
33
Fetching object from the database
public static function loadAllUsers(PDO $conn): array
{
$ret = []; $sql = "SELECT * FROM Users";
$result = $conn->query($sql);
if ($result !== false && $result->rowCount() != 0) {
foreach ($result as $row) {
$loadedUser = new User();
$loadedUser->id = $row['id'];
$loadedUser->username = $row['username'];
$loadedUser->hashPass = $row['hash_pass'];
$loadedUser->email = $row['email'];
$ret[] = $loadedUser;
}
}
return $ret;
}

Iterate by records returned from the database.


34
Fetching object from the database
public static function loadAllUsers(PDO $conn): array
{
$ret = []; $sql = "SELECT * FROM Users";
$result = $conn->query($sql);
if ($result !== false && $result->rowCount() != 0) {
foreach ($result as $row) {
$loadedUser = new User();
$loadedUser->id = $row['id'];
$loadedUser->username = $row['username'];
$loadedUser->hashPass = $row['hash_pass'];
$loadedUser->email = $row['email'];
$ret[] = $loadedUser;
}
}
return $ret;
}

Create a new user object and set appropriate parameters. Since we are inside the class, we have access
35
Fetching object from the database
public static function loadAllUsers(PDO $conn): array
{
$ret = []; $sql = "SELECT * FROM Users";
$result = $conn->query($sql);
if ($result !== false && $result->rowCount() != 0) {
foreach ($result as $row) {
$loadedUser = new User();
$loadedUser->id = $row['id'];
$loadedUser->username = $row['username'];
$loadedUser->hashPass = $row['hash_pass'];
$loadedUser->email = $row['email'];
$ret[] = $loadedUser;
}
}
return $ret;
}

We put data into the array, which we return at the end.


36
Use Case: loading all users

In order to test if the method written by us is working properly, we will create a new scenario.
Such a scenario will look like this:
Load the static method loadAllUsers().
Then you should check if:
the method returns an array?
the number of objects in the array is the same as the number of rows in the database?
the objects have all the same data as stored in the database (it is enough to check one random
object)?

37
Object modification

The next method to be written will be a method to change the data of an object that already exists in the
database. This will be an extension of the saveToDB() method we have already written.
At the beginning of the saveToDB() method we checked if the object is not yet saved in the database.
To this check, we will add else in which we will write a code updating the data contained in the database.

38
Object modification
public function saveToDB(PDO $conn): bool
{
if ($this->id == -1) { /* ... */ } else {
$stmt = $conn->prepare('UPDATE Users SET username=:username,
email=:email, hash_pass=:hash_pass WHERE id=:id');
$result = $stmt->execute(
[ 'username' => $this->username,
'email' => $this->email,
'hash_pass' => $this->hashPass,
'id' => $this->id,
]);
if ($result === true) { return true; }
}
return false;
}

39
Object modification
public function saveToDB(PDO $conn): bool
{
if ($this->id == -1) { /* ... */ } else {
$stmt = $conn->prepare('UPDATE Users SET username=:username,
email=:email, hash_pass=:hash_pass WHERE id=:id');
$result = $stmt->execute(
[ 'username' => $this->username,
'email' => $this->email,
'hash_pass' => $this->hashPass,
'id' => $this->id,
]);
if ($result === true) { return true; }
}
return false;
}

If the id of the object is different from -1, it means that we work on the object that is in the database, so
we update its data.
40
Object modification
public function saveToDB(PDO $conn): bool
{
if ($this->id == -1) { /* ... */ } else {
$stmt = $conn->prepare('UPDATE Users SET username=:username,
email=:email, hash_pass=:hash_pass WHERE id=:id');
$result = $stmt->execute(
[ 'username' => $this->username,
'email' => $this->email,
'hash_pass' => $this->hashPass,
'id' => $this->id,
]);
if ($result === true) { return true; }
}
return false;
}

We return true or false.

41
Use Case: User modification

In order to test if the method written by us is working properly, we will create a new scenario. Such a
scenario will look like this:
call the static method loadUserById() by entering its id existing in the database,
make changes to the loaded user by means of appropriate getters and setters,
save the changed user to the database.
Then you should check the following:
Does the entry in the database have all the relevant data?
Does the object have the correct id set?
Has a new entry been added to the database?

42
Deleting an object

The next method to be written will be a method that deletes an object from the database.
It should be called on the object that is already written into the database.
At the beginning of the delete() method we will have to check if the object is already stored in the
database. We will do this by checking if its id is different from -1.
If the object is not stored in the database, the method will not do anything.

43
Deleting an object
public function delete(PDO $conn): bool
{
if ($this->id != -1) {
$stmt = $conn->prepare('DELETE FROM Users WHERE id=:id');
$result = $stmt->execute(['id' => $this->id]);
if ($result === true) {
$this->id = -1;
return true;
}
return false;
}
return true;
}

44
Deleting an object
public function delete(PDO $conn): bool
{
if ($this->id != -1) {
$stmt = $conn->prepare('DELETE FROM Users WHERE id=:id');
$result = $stmt->execute(['id' => $this->id]);
if ($result === true) {
$this->id = -1;
return true;
}
return false;
}
return true;
}

Since we have deleted the object, we change its id to -1.

45
Deleting an object
public function delete(PDO $conn): bool
{
if ($this->id != -1) {
$stmt = $conn->prepare('DELETE FROM Users WHERE id=:id');
$result = $stmt->execute(['id' => $this->id]);
if ($result === true) {
$this->id = -1;
return true;
}
return false;
}
return true;
}

If the object was not previously in the database then we can immediately return true.

46
Use Case: deleting a User

In order to test if the method written by us is working properly, we will create a new scenario. Such a
scenario will look like this:
call the static method loadUserById() by entering its id existing in the database,
On the loaded user we use the delete() method.
Then you should check:
Does the object have the id id set to -1?
Has the entry with the given id been removed from the database?

47
Twitter

48
Twitter

The aim of the workshop is to write a full and functional application in the style of Twitter. The application
is to implement the following functionalities:

Users: adding, modifying non-core Comments: Under each tweet, other users
information about oneself, deleting one's should be able to type comments. The
account. The user is to be identified by e-mail maximum length of a comment is 60
(cannot be repeated). characters.
Tweets: Each user can create an unlimited Messages: Each user can send a message to
number of tweets. The maximum tweet length another user.
is 140 characters.

49
Pages to be included in the application

Home page Login page


Page displaying all Tweets that are in the system The website should accept the user's e-mail
(from the newest to the oldest). address and password.
Above them, there should be a form to create a if they are correct, the user is redirected to the
new tweet. homepage,
if not, to the login page, which should then
display an error message about the login or
password,
the login page should also have a link to the
user creation page.

50
Pages to be included in the application
User creation page User display page
The page should download the email and The page is to show all the tweets of a given user (
password. additionally, under each number of comments to a
If there is no such email address in the system given entry).
(table in the database), we register the user There will also be a button on this page, which will
and log in (redirection to the home page).
enable us to send messages to this user.
If there is such an email address, we redirect
to the user creation page (the same page)
and display a message about the occupied
email address.

51
Pages to be included in the application
Tweet display page User edit page
This page should display: The user is to be able to edit information about
a tweet, himself and change the password.
Remember that the user can only edit their
an author of the post,
information.
all comments to each post,
a form for creating a new comment for a
tweet.

52
Pages to be included in the application
Page with messages Page of a single message
The user should be able to view a list of messages All information about the message:
he has received and sent. sender
Sent messages should display the recipient, receiver
the date of sending and the beginning of the
message (first 30 characters). tweet
Received messages should display the
sender, the date of sending and the beginning
of the message (first 30 characters).
Messages, which are not read yet, should be
somehow marked (e.g. bold text containing the
sender).

53
Information

Without logging in, there should be available only:


login page,
new user creation page.

When you have finished working on the workshop (on the day of the workshop), send the Mentor a
message containing the following data:
the address of the repository where your code is located,
list of implemented functionalities,
database dump.

Send the same information when you have completely finished the work on the workshop.

54
Exercises

55
Exercise 1
Preparation
Prepare a folder for the application. Below is an example of a .gitignore file for
Set up a new Git repository on GitHub and a NetBeans:
new database.
Remember to backup the database nbproject/private/
(preferably after each exercise) and create build/
commits, with a description in English (after nbbuild/
each exercise). dist/
Prepare folder named.gitignore and add nbdist/
all the basic data to it: (*.*~ files, directory .nb-gradle/
with your IDE data, if any, etc.). *.*~
Create a file that will be used to connect to the
database.

56
Exercise 2

Exercises with the teacher

During the exercises with the teacher you will create the framework of the application and User class
(based on the diagram from the presentation).

57
Exercise 3
Tweets
It's time to add the main functionality to our Create a class named Tweet.
website - tweets.
Create a table in the database, which will It should contain at least:
keep the entries (Tweets). id: int, private
Remember to create a relationship between userId: int, private
this table and the user table. The user can
have multiple entries, the entry can have only text: string, private
one user. creationDate: date, private

58
Exercise 3 - Tweet class

It is to implement the following functionalities:


Set and get for all attributes (for id - only get). loadAllTweets() method (modeled on the
User class).
Constructor setting id to -1, and all other
data to zero. saveToDB() method (modeled on the User
class).
loadTweetById() method (modeled on the
User class). If you see any other necessary functions, you
can add them.
loadAllTweetsByUserId() method (is to
load all Tweets created by a given user).

59
Exercise 3 - continuation

Modify the home page to display all entries. Modify the home page of the user to display
all entries.
Load them into the table using the
loadAllTweets(), method, and then print Create a page that displays all the information
out the information for each Tweet using the about a single entry.
appropriate getters.
Modify the homepage so that it has a form to
create a new entry.
Remember to use this form on the same
page.
It is supposed to create a new tweet assigned
to the logged User.

60
Exercise 4

Comments
Create a class named Comment.
We add the possibility to write comments
under the entries. It should contain:
Create a comment table in the database id: int, private
(Comments). userId: int, private
Create a relationship between comments and postId: int, private
posts.
creation_date: datetime, private
text: string, private

61
Exercise 4 - continuation
The application is to implement the following functionalities:

Set and get for all attributes (for id - only get). The application is to show comments sorted
from the latest to the oldest.
Constructor setting id to -1, and all other
data to zero. Modify the single entry page so that it displays
all your comments and has a form to create a
loadCommentById() and new comment.
loadAllCommentsByPostId() method.
Comment is to display information about the
saveToDB() method (modeled on the User author (you have to load a given user).
class).

62
Exercise 5
Messages
It's time to send messages. Create a Message class based on previous tasks.
Create a table in the database that will store
the messages.
Connect it to the user table with a multiple to
two relationship. So you have to create two
relationships many to one. The message has
two users, the sender and the recipient, and
the user has many messages.
In the table, create a field that holds
information whether the message has been
read, e.g.:
1 - message was read,
0 - message was not read.

63
Exercise 5 - continuation

Create a page that displays all the messages Add a page that will display information about
the user has received and sent. the message (if it is opened by the recipient,
remember to mark the message as read).
To the page displaying the user, add a button
redirecting to the page with the form to send
messages to this user (it should not be If you have any doubts, ask the teacher.
possible to send messages to oneself!).
Remember that a newly created message
should be marked as unread.

64

You might also like