Create a dating website in PHP: the complete manual

Introduction
Building a dating website represents an excellent project for web developers looking to create dynamic, database-driven applications with complex user interactions. This comprehensive guide will walk you through building a fully-functional dating platform from scratch using PHP and MySQL. We'll cover everything from database design and user authentication to matching algorithms and messaging systems.
This guide assumes you have basic knowledge of PHP, MySQL, HTML, and CSS. We'll be using modern, secure coding practices including prepared statements for database queries, password hashing, and input validation.
Table of Contents
- Project Setup and Configuration
- Database Design and Structure
- User Registration and Authentication
- User Profiles and Preferences
- Search and Matching Algorithms
- Messaging System
- Admin Dashboard
- Security Considerations
- Deployment Preparation
1. Project Setup and Configuration
Directory Structure
First, let's create a logical directory structure for our project:
/dating-site/
│
├── /assets/
│ ├── /css/
│ ├── /js/
│ └── /images/
│
├── /includes/
│ ├── config.php
│ ├── database.php
│ ├── functions.php
│ └── auth.php
│
├── /classes/
│ ├── User.php
│ ├── Profile.php
│ ├── Match.php
│ └── Message.php
│
├── /pages/
│ ├── register.php
│ ├── login.php
│ ├── dashboard.php
│ ├── profile.php
│ ├── search.php
│ ├── matches.php
│ ├── messages.php
│ └── admin/
│
└── index.php
Configuration File
Create includes/config.php to store our application configuration:
define('DB_HOST', 'localhost'); define('DB_NAME', 'dating_site'); define('DB_USER', 'root'); define('DB_PASS', ''); define('SITE_URL', 'localhost/dating-site'); define('SITE_NAME', 'Connectify'); define('UPLOAD_PATH', $_SERVER['DOCUMENT_ROOT'] . '/dating-site/assets/images/uploads/');
// Session configuration ini_set('session.cookie_httponly', 1); ini_set('session.cookie_secure', 0); // Set to 1 if using HTTPS
Database Connection
Create includes/database.php to handle database connections:
require_once 'config.php';
class Database { private $host = DB_HOST; private $db_name = DB_NAME; private $username = DB_USER; private $password = DB_PASS; public $conn;
public function getConnection() {
$this->conn = null;
try {
$this->conn = new PDO("mysql:host=" . $this->host . ";dbname=" . $this->db_name,
$this->username, $this->password);
$this->conn->exec("set names utf8");
$this->conn->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
} catch(PDOException $exception) {
echo "Connection error: " . $exception->getMessage();
}
return $this->conn;
}
}
2. Database Design and Structure
Let's design our database schema with the following tables:
Users Table
CREATE TABLE users ( id INT(11) AUTO_INCREMENT PRIMARY KEY, username VARCHAR(50) UNIQUE NOT NULL, email VARCHAR(100) UNIQUE NOT NULL, password_hash VARCHAR(255) NOT NULL, first_name VARCHAR(50) NOT NULL, last_name VARCHAR(50) NOT NULL, date_of_birth DATE NOT NULL, gender ENUM('male', 'female', 'other') NOT NULL, looking_for ENUM('male', 'female', 'both') NOT NULL, profile_created TIMESTAMP DEFAULT CURRENT_TIMESTAMP, last_login TIMESTAMP NULL, is_active TINYINT(1) DEFAULT 1, is_verified TINYINT(1) DEFAULT 0, verification_token VARCHAR(100), reset_token VARCHAR(100), profile_picture VARCHAR(255) );
Profiles Table
CREATE TABLE profiles ( id INT(11) AUTO_INCREMENT PRIMARY KEY, user_id INT(11) NOT NULL, headline VARCHAR(255), bio TEXT, occupation VARCHAR(100), education VARCHAR(100), location VARCHAR(100), latitude DECIMAL(10, 8), longitude DECIMAL(11, 8), height INT(3), relationship_goal ENUM('friendship', 'casual', 'long-term', 'marriage'), interests TEXT, personality_traits TEXT, smoking ENUM('never', 'occasionally', 'regularly'), drinking ENUM('never', 'occasionally', 'regularly'), has_children ENUM('no', 'yes', 'want'), wants_children ENUM('no', 'yes', 'unsure'), religion VARCHAR(50), political_views VARCHAR(50), updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE );
Photos Table
CREATE TABLE photos ( id INT(11) AUTO_INCREMENT PRIMARY KEY, user_id INT(11) NOT NULL, photo_path VARCHAR(255) NOT NULL, caption VARCHAR(255), is_primary TINYINT(1) DEFAULT 0, upload_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE );
Messages Table
CREATE TABLE messages ( id INT(11) AUTO_INCREMENT PRIMARY KEY, sender_id INT(11) NOT NULL, receiver_id INT(11) NOT NULL, subject VARCHAR(255), message TEXT NOT NULL, is_read TINYINT(1) DEFAULT 0, sent_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (sender_id) REFERENCES users(id) ON DELETE CASCADE, FOREIGN KEY (receiver_id) REFERENCES users(id) ON DELETE CASCADE );
Likes Table
CREATE TABLE likes ( id INT(11) AUTO_INCREMENT PRIMARY KEY, user_id INT(11) NOT NULL, liked_user_id INT(11) NOT NULL, like_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE, FOREIGN KEY (liked_user_id) REFERENCES users(id) ON DELETE CASCADE, UNIQUE KEY unique_like (user_id, liked_user_id) );
Matches Table
CREATE TABLE matches ( id INT(11) AUTO_INCREMENT PRIMARY KEY, user1_id INT(11) NOT NULL, user2_id INT(11) NOT NULL, match_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP, is_active TINYINT(1) DEFAULT 1, FOREIGN KEY (user1_id) REFERENCES users(id) ON DELETE CASCADE, FOREIGN KEY (user2_id) REFERENCES users(id) ON DELETE CASCADE, UNIQUE KEY unique_match (user1_id, user2_id) );
Blocked Users Table
CREATE TABLE blocked_users ( id INT(11) AUTO_INCREMENT PRIMARY KEY, user_id INT(11) NOT NULL, blocked_user_id INT(11) NOT NULL, block_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE, FOREIGN KEY (blocked_user_id) REFERENCES users(id) ON DELETE CASCADE );
3. User Registration and Authentication
Registration System
Create pages/register.php:
session_start(); require_once '../includes/config.php'; require_once '../includes/database.php'; require_once '../includes/functions.php';
$error = ''; $success = '';
if ($_SERVER['REQUEST_METHOD'] == 'POST') { $username = trim($_POST['username']); $email = trim($_POST['email']); $password = $_POST['password']; $confirm_password = $_POST['confirm_password']; $first_name = trim($_POST['first_name']); $last_name = trim($_POST['last_name']); $date_of_birth = $_POST['date_of_birth']; $gender = $_POST['gender']; $looking_for = $_POST['looking_for'];
// Validate inputs
if (empty($username) || empty($email) || empty($password)) {
$error = 'Please fill in all required fields.';
} elseif ($password !== $confirm_password) {
$error = 'Passwords do not match.';
} elseif (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
$error = 'Please enter a valid email address.';
} elseif (strlen($password) < 8) {
$error = 'Password must be at least 8 characters long.';
} else {
$database = new Database();
$db = $database->getConnection();
// Check if username or email already exists
$query = "SELECT id FROM users WHERE username = :username OR email = :email";
$stmt = $db->prepare($query);
$stmt->bindParam(':username', $username);
$stmt->bindParam(':email', $email);
$stmt->execute();
if ($stmt->rowCount() > 0) {
$error = 'Username or email already exists.';
} else {
// Calculate age
$birthDate = new DateTime($date_of_birth);
$today = new DateTime();
$age = $birthDate->diff($today)->y;
if ($age < 18) {
$error = 'You must be at least 18 years old to register.';
} else {
// Create new user
$password_hash = password_hash($password, PASSWORD_DEFAULT);
$verification_token = bin2hex(random_bytes(32));
$query = "INSERT INTO users
(username, email, password_hash, first_name, last_name,
date_of_birth, gender, looking_for, verification_token)
VALUES
(:username, :email, :password_hash, :first_name, :last_name,
:date_of_birth, :gender, :looking_for, :verification_token)";
$stmt = $db->prepare($query);
$stmt->bindParam(':username', $username);
$stmt->bindParam(':email', $email);
$stmt->bindParam(':password_hash', $password_hash);
$stmt->bindParam(':first_name', $first_name);
$stmt->bindParam(':last_name', $last_name);
$stmt->bindParam(':date_of_birth', $date_of_birth);
$stmt->bindParam(':gender', $gender);
$stmt->bindParam(':looking_for', $looking_for);
$stmt->bindParam(':verification_token', $verification_token);
if ($stmt->execute()) {
$user_id = $db->lastInsertId();
// Create empty profile
$query = "INSERT INTO profiles (user_id) VALUES (:user_id)";
$stmt = $db->prepare($query);
$stmt->bindParam(':user_id', $user_id);
$stmt->execute();
// Send verification email (pseudo-code)
// sendVerificationEmail($email, $verification_token);
$success = 'Registration successful! Please check your email to verify your account.';
} else {
$error = 'Registration failed. Please try again.';
}
}
}
}
}
// HTML registration form would follow here...
Authentication System
Create includes/auth.php:
function isLoggedIn() { return isset($_SESSION['user_id']); }
function login($username, $password) { $database = new Database(); $db = $database->getConnection();
$query = "SELECT id, username, password_hash, is_active, is_verified
FROM users
WHERE username = :username OR email = :username";
$stmt = $db->prepare($query);
$stmt->bindParam(':username', $username);
$stmt->execute();
if ($stmt->rowCount() == 1) {
$user = $stmt->fetch(PDO::FETCH_ASSOC);
if (password_verify($password, $user['password_hash'])) {
if (!$user['is_verified']) {
return 'Please verify your email address before logging in.';
}
if (!$user['is_active']) {
return 'Your account has been deactivated.';
}
// Update last login
$query = "UPDATE users SET last_login = NOW() WHERE id = :id";
$stmt = $db->prepare($query);
$stmt->bindParam(':id', $user['id']);
$stmt->execute();
// Set session variables
$_SESSION['user_id'] = $user['id'];
$_SESSION['username'] = $user['username'];
return true;
}
}
return 'Invalid username or password.';
}
function logout() { $_SESSION = array();
if (ini_get("session.use_cookies")) {
$params = session_get_cookie_params();
setcookie(session_name(), '', time() - 42000,
$params["path"], $params["domain"],
$params["secure"], $params["httponly"]
);
}
session_destroy();
}
function requireLogin() { if (!isLoggedIn()) { header('Location: login.php'); exit; } }
Login Page
Create pages/login.php:
session_start(); require_once '../includes/config.php'; require_once '../includes/database.php'; require_once '../includes/auth.php';
$error = '';
if (isLoggedIn()) { header('Location: dashboard.php'); exit; }
if ($_SERVER['REQUEST_METHOD'] == 'POST') { $username = trim($_POST['username']); $password = $_POST['password'];
$result = login($username, $password);
if ($result === true) {
header('Location: dashboard.php');
exit;
} else {
$error = $result;
}
}
// HTML login form would follow...
4. User Profiles and Preferences
Profile Management Class
Create classes/Profile.php:
class Profile { private $conn; private $table_name = "profiles";
public $id;
public $user_id;
public $headline;
public $bio;
public $occupation;
public $education;
public $location;
public $latitude;
public $longitude;
public $height;
public $relationship_goal;
public $interests = array();
public $personality_traits = array();
public $smoking;
public $drinking;
public $has_children;
public $wants_children;
public $religion;
public $political_views;
public function __construct($db) {
$this->conn = $db;
}
public function read() {
$query = "SELECT * FROM " . $this->table_name . " WHERE user_id = :user_id";
$stmt = $this->conn->prepare($query);
$stmt->bindParam(':user_id', $this->user_id);
$stmt->execute();
if ($stmt->rowCount() > 0) {
$row = $stmt->fetch(PDO::FETCH_ASSOC);
$this->id = $row['id'];
$this->headline = $row['headline'];
$this->bio = $row['bio'];
$this->occupation = $row['occupation'];
$this->education = $row['education'];
$this->location = $row['location'];
$this->latitude = $row['latitude'];
$this->longitude = $row['longitude'];
$this->height = $row['height'];
$this->relationship_goal = $row['relationship_goal'];
$this->interests = $row['interests'] ? json_decode($row['interests'], true) : array();
$this->personality_traits = $row['personality_traits'] ? json_decode($row['personality_traits'], true) : array();
$this->smoking = $row['smoking'];
$this->drinking = $row['drinking'];
$this->has_children = $row['has_children'];
$this->wants_children = $row['wants_children'];
$this->religion = $row['religion'];
$this->political_views = $row['political_views'];
return true;
}
return false;
}
public function update() {
$query = "UPDATE " . $this->table_name . "
SET headline = :headline, bio = :bio, occupation = :occupation,
education = :education, location = :location, latitude = :latitude,
longitude = :longitude, height = :height, relationship_goal = :relationship_goal,
interests = :interests, personality_traits = :personality_traits,
smoking = :smoking, drinking = :drinking, has_children = :has_children,
wants_children = :wants_children, religion = :religion,
political_views = :political_views
WHERE user_id = :user_id";
$stmt = $this->conn->prepare($query);
$stmt->bindParam(':headline', $this->headline);
$stmt->bindParam(':bio', $this->bio);
$stmt->bindParam(':occupation', $this->occupation);
$stmt->bindParam(':education', $this->education);
$stmt->bindParam(':location', $this->location);
$stmt->bindParam(':latitude', $this->latitude);
$stmt->bindParam(':longitude', $this->longitude);
$stmt->bindParam(':height', $this->height);
$stmt->bindParam(':relationship_goal', $this->relationship_goal);
$stmt->bindParam(':interests', json_encode($this->interests));
$stmt->bindParam(':personality_traits', json_encode($this->personality_traits));
$stmt->bindParam(':smoking', $this->smoking);
$stmt->bindParam(':drinking', $this->drinking);
$stmt->bindParam(':has_children', $this->has_children);
$stmt->bindParam(':wants_children', $this->wants_children);
$stmt->bindParam(':religion', $this->religion);
$stmt->bindParam(':political_views', $this->political_views);
$stmt->bindParam(':user_id', $this->user_id);
return $stmt->execute();
}
public function calculateAge() {
$query = "SELECT date_of_birth FROM users WHERE id = :user_id";
$stmt = $this->conn->prepare($query);
$stmt->bindParam(':user_id', $this->user_id);
$stmt->execute();
if ($stmt->rowCount() > 0) {
$row = $stmt->fetch(PDO::FETCH_ASSOC);
$birthDate = new DateTime($row['date_of_birth']);
$today = new DateTime();
return $birthDate->diff($today)->y;
}
return null;
}
}
Profile Editing Page
Create pages/profile.php:
session_start(); require_once '../includes/config.php'; require_once '../includes/database.php'; require_once '../includes/auth.php'; require_once '../classes/Profile.php';
requireLogin();
$database = new Database(); $db = $database->getConnection(); $profile = new Profile($db); $profile->user_id = $_SESSION['user_id'];
$success = ''; $error = '';
// Load existing profile data $profile->read();
if ($_SERVER['REQUEST_METHOD'] == 'POST') { $profile->headline = trim($_POST['headline']); $profile->bio = trim($_POST['bio']); $profile->occupation = trim($_POST['occupation']); $profile->education = trim($_POST['education']); $profile->location = trim($_POST['location']); $profile->height = $_POST['height'] ? intval($_POST['height']) : null; $profile->relationship_goal = $_POST['relationship_goal']; $profile->interests = isset($_POST['interests']) ? $_POST['interests'] : array(); $profile->personality_traits = isset($_POST['personality_traits']) ? $_POST['personality_traits'] : array(); $profile->smoking = $_POST['smoking']; $profile->drinking = $_POST['drinking']; $profile->has_children = $_POST['has_children']; $profile->wants_children = $_POST['wants_children']; $profile->religion = trim($_POST['religion']); $profile->political_views = trim($_POST['political_views']);
// Geocode location if provided
if (!empty($profile->location)) {
$geo_data = geocodeAddress($profile->location);
if ($geo_data) {
$profile->latitude = $geo_data['latitude'];
$profile->longitude = $geo_data['longitude'];
}
}
if ($profile->update()) {
$success = 'Profile updated successfully!';
} else {
$error = 'Failed to update profile. Please try again.';
}
}
// HTML profile form would follow...
5. Search and Matching Algorithms
Advanced Search Functionality
Create classes/Match.php:
class Match { private $conn; private $table_users = "users"; private $table_profiles = "profiles";
public function __construct($db) {
$this->conn = $db;
}
public function search($filters, $page = 1, $limit = 20) {
$offset = ($page - 1) * $limit;
$query = "SELECT u.id, u.username, u.first_name, u.last_name, u.date_of_birth,
u.gender, u.profile_picture, p.headline, p.location,
p.occupation, p.relationship_goal
FROM " . $this->table_users . " u
LEFT JOIN " . $this->table_profiles . " p ON u.id = p.user_id
WHERE u.is_active = 1 AND u.is_verified = 1 AND u.id != :current_user_id";
$params = array(':current_user_id' => $filters['current_user_id']);
// Age filter
if (!empty($filters['min_age']) && !empty($filters['max_age'])) {
$min_birthdate = date('Y-m-d', strtotime('-' . $filters['max_age'] . ' years'));
$max_birthdate = date('Y-m-d', strtotime('-' . $filters['min_age'] . ' years'));
$query .= " AND u.date_of_birth BETWEEN :min_birthdate AND :max_birthdate";
$params[':min_birthdate'] = $min_birthdate;
$params[':max_birthdate'] = $max_birthdate;
}
// Gender filter
if (!empty($filters['gender'])) {
$query .= " AND u.gender = :gender";
$params[':gender'] = $filters['gender'];
}
// Location filter
if (!empty($filters['location']) && !empty($filters['distance'])) {
$query .= " AND (6371 * acos(cos(radians(:lat)) * cos(radians(p.latitude)) *
cos(radians(p.longitude) - radians(:lng)) + sin(radians(:lat)) *
sin(radians(p.latitude)))) <= :distance";
$params[':lat'] = $filters['latitude'];
$params[':lng'] = $filters['longitude'];
$params[':distance'] = $filters['distance'];
}
// Relationship goal filter
if (!empty($filters['relationship_goal'])) {
$query .= " AND p.relationship_goal = :relationship_goal";
$params[':relationship_goal'] = $filters['relationship_goal'];
}
// Interests filter
if (!empty($filters['interests'])) {
$interest_conditions = array();
foreach ($filters['interests'] as $index => $interest) {
$param_name = ':interest_' . $index;
$interest_conditions[] = "p.interests LIKE " . $param_name;
$params[$param_name] = '%"' . $interest . '"%';
}
$query .= " AND (" . implode(' OR ', $interest_conditions) . ")";
}
$query .= " ORDER BY u.last_login DESC LIMIT :limit OFFSET :offset";
$params[':limit'] = $limit;
$params[':offset'] = $offset;
$stmt = $this->conn->prepare($query);
foreach ($params as $key => &$value) {
if ($key == ':limit' || $key == ':offset') {
$stmt->bindParam($key, $value, PDO::PARAM_INT);
} else {
$stmt->bindParam($key, $value);
}
}
$stmt->execute();
return $stmt->fetchAll(PDO::FETCH_ASSOC);
}
public function calculateCompatibility($user1_id, $user2_id) {
$score = 0;
$max_score = 0;
// Get both users' profiles
$query = "SELECT p.*, u.gender, u.looking_for, u.date_of_birth
FROM profiles p
JOIN users u ON p.user_id = u.id
WHERE p.user_id IN (:user1_id, :user2_id)";
$stmt = $this->conn->prepare($query);
$stmt->bindParam(':user1_id', $user1_id);
$stmt->bindParam(':user2_id', $user2_id);
$stmt->execute();
$profiles = $stmt->fetchAll(PDO::FETCH_ASSOC);
if (count($profiles) != 2) return 0;
$user1 = $profiles[0]['user_id'] == $user1_id ? $profiles[0] : $profiles[1];
$user2 = $profiles[1]['user_id'] == $user2_id ? $profiles[1] : $profiles[0];
// Check if users are looking for each other's gender
$max_score += 20;
if ($this->checkGenderCompatibility($user1, $user2)) {
$score += 20;
}
// Age compatibility (within 10 years)
$max_score += 15;
$age1 = $this->calculateAgeFromDate($user1['date_of_birth']);
$age2 = $this->calculateAgeFromDate($user2['date_of_birth']);
if (abs($age1 - $age2) <= 10) {
$score += 15;
} elseif (abs($age1 - $age2) <= 15) {
$score += 10;
} elseif (abs($age1 - $age2) <= 20) {
$score += 5;
}
// Relationship goal compatibility
$max_score += 15;
if ($user1['relationship_goal'] == $user2['relationship_goal']) {
$score += 15;
}
// Interests compatibility
$max_score += 20;
$interests1 = $user1['interests'] ? json_decode($user1['interests'], true) : array();
$interests2 = $user2['interests'] ? json_decode($user2['interests'], true) : array();
$common_interests = array_intersect($interests1, $interests2);
if (count($interests1) > 0) {
$interest_score = (count($common_interests) / count($interests1)) * 20;
$score += min($interest_score, 20);
}
// Lifestyle compatibility
$max_score += 15;
if ($user1['smoking'] == $user2['smoking']) $score += 5;
if ($user1['drinking'] == $user2['drinking']) $score += 5;
if ($user1['has_children'] == $user2['has_children']) $score += 5;
// Location proximity
$max_score += 15;
if (!empty($user1['latitude']) && !empty($user2['latitude'])) {
$distance = $this->calculateDistance(
$user1['latitude'], $user1['longitude'],
$user2['latitude'], $user2['longitude']
);
if ($distance <= 50) $score += 15;
elseif ($distance <= 100) $score += 10;
elseif ($distance <= 200) $score += 5;
}
return $max_score > 0 ? round(($score / $max_score) * 100) : 0;
}
private function checkGenderCompatibility($user1, $user2) {
$looking_for1 = $user1['looking_for'];
$looking_for2 = $user2['looking_for'];
$gender1 = $user1['gender'];
$gender2 = $user2['gender'];
if ($looking_for1 == 'both' && $looking_for2 == 'both') return true;
if ($looking_for1 == 'both' && $looking_for2 == $gender1) return true;
if ($looking_for2 == 'both' && $looking_for1 == $gender2) return true;
if ($looking_for1 == $gender2 && $looking_for2 == $gender1) return true;
return false;
}
private function calculateAgeFromDate($date_of_birth) {
$birthDate = new DateTime($date_of_birth);
$today = new DateTime();
return $birthDate->diff($today)->y;
}
private function calculateDistance($lat1, $lon1, $lat2, $lon2) {
$earth_radius = 6371; // kilometers
$dLat = deg2rad($lat2 - $lat1);
$dLon = deg2rad($lon2 - $lon1);
$a = sin($dLat/2) * sin($dLat/2) + cos(deg2rad($lat1)) * cos(deg2rad($lat2)) * sin($dLon/2) * sin($dLon/2);
$c = 2 * asin(sqrt($a));
return $earth_radius * $c;
}
}
Search Page
Create pages/search.php:
session_start(); require_once '../includes/config.php'; require_once '../includes/database.php'; require_once '../includes/auth.php'; require_once '../classes/Match.php';
requireLogin();
$database = new Database(); $db = $database->getConnection(); $match = new Match($db);
$results = array(); $filters = array( 'current_user_id' => $_SESSION['user_id'], 'min_age' => isset($_GET['min_age']) ? intval($_GET['min_age']) : 18, 'max_age' => isset($_GET['max_age']) ? intval($_GET['max_age']) : 99, 'gender' => isset($_GET['gender']) ? $_GET['gender'] : '', 'location' => isset($_GET['location']) ? trim($_GET['location']) : '', 'distance' => isset($_GET['distance']) ? intval($_GET['distance']) : 50, 'relationship_goal' => isset($_GET['relationship_goal']) ? $_GET['relationship_goal'] : '', 'interests' => isset($_GET['interests']) ? $_GET['interests'] : array() );
// Geocode location if provided if (!empty($filters['location'])) { $geo_data = geocodeAddress($filters['location']); if ($geo_data) { $filters['latitude'] = $geo_data['latitude']; $filters['longitude'] = $geo_data['longitude']; } }
$page = isset($_GET['page']) ? intval($_GET['page']) : 1; $limit = 20;
if (!empty($_GET)) { $results = $match->search($filters, $page, $limit);
// Calculate compatibility for each result
foreach ($results as &$result) {
$result['compatibility'] = $match->calculateCompatibility($_SESSION['user_id'], $result['id']);
}
}
// HTML search form and results display would follow...
6. Messaging System
Message Class
Create classes/Message.php:
class Message { private $conn; private $table_name = "messages";
public $id;
public $sender_id;
public $receiver_id;
public $subject;
public $message;
public $is_read;
public $sent_date;
public function __construct($db) {
$this->conn = $db;
}
public function create() {
$query = "INSERT INTO " . $this->table_name . "
(sender_id, receiver_id, subject, message)
VALUES
(:sender_id, :receiver_id, :subject, :message)";
$stmt = $this->conn->prepare($query);
$stmt->bindParam(':sender_id', $this->sender_id);
$stmt->bindParam(':receiver_id', $this->receiver_id);
$stmt->bindParam(':subject', $this->subject);
$stmt->bindParam(':message', $this->message);
return $stmt->execute();
}
public function getConversation($user1_id, $user2_id, $limit = 50) {
$query = "SELECT m.*, u1.username as sender_username, u2.username as receiver_username
FROM " . $this->table_name . " m
JOIN users u1 ON m.sender_id = u1.id
JOIN users u2 ON m.receiver_id = u2.id
WHERE (m.sender_id = :user1_id AND m.receiver_id = :user2_id)
OR (m.sender_id = :user2_id AND m.receiver_id = :user1_id)
ORDER BY m.sent_date DESC
LIMIT :limit";
$stmt = $this->conn->prepare($query);
$stmt->bindParam(':user1_id', $user1_id);
$stmt->bindParam(':user2_id', $user2_id);
$stmt->bindParam(':limit', $limit, PDO::PARAM_INT);
$stmt->execute();
return $stmt->fetchAll(PDO::FETCH_ASSOC);
}
public function getUnreadCount($user_id) {
$query = "SELECT COUNT(*) as unread_count
FROM " . $this->table_name . "
WHERE receiver_id = :user_id AND is_read = 0";
$stmt = $this->conn->prepare($query);
$stmt->bindParam(':user_id', $user_id);
$stmt->execute();
$row = $stmt->fetch(PDO::FETCH_ASSOC);
return $row['unread_count'];
}
public function markAsRead($message_id, $user_id) {
$query = "UPDATE " . $this->table_name . "
SET is_read = 1
WHERE id = :message_id AND receiver_id = :user_id";
$stmt = $this->conn->prepare($query);
$stmt->bindParam(':message_id', $message_id);
$stmt->bindParam(':user_id', $user_id);
return $stmt->execute();
}
public function getConversationPartners($user_id) {
$query = "SELECT DISTINCT u.id, u.username, u.first_name, u.last_name, u.profile_picture,
(SELECT COUNT(*) FROM messages m2
WHERE ((m2.sender_id = u.id AND m2.receiver_id = :user_id)
OR (m2.sender_id = :user_id AND m2.receiver_id = u.id))
AND m2.is_read = 0 AND m2.receiver_id = :user_id) as unread_count,
(SELECT MAX(sent_date) FROM messages m3
WHERE (m3.sender_id = u.id AND m3.receiver_id = :user_id)
OR (m3.sender_id = :user_id AND m3.receiver_id = u.id)) as last_message_date
FROM users u
JOIN messages m ON (m.sender_id = u.id OR m.receiver_id = u.id)
WHERE (m.sender_id = :user_id OR m.receiver_id = :user_id)
AND u.id != :user_id
GROUP BY u.id
ORDER BY last_message_date DESC";
$stmt = $this->conn->prepare($query);
$stmt->bindParam(':user_id', $user_id);
$stmt->execute();
return $stmt->fetchAll(PDO::FETCH_ASSOC);
}
}
Messaging Interface
Create pages/messages.php:
session_start(); require_once '../includes/config.php'; require_once '../includes/database.php'; require_once '../includes/auth.php'; require_once '../classes/Message.php';
requireLogin();
$database = new Database(); $db = $database->getConnection(); $message_obj = new Message($db);
$success = ''; $error = ''; $conversation = array(); $conversation_partners = $message_obj->getConversationPartners($_SESSION['user_id']);
// Get specific conversation if selected $selected_user_id = isset($_GET['user_id']) ? intval($_GET['user_id']) : null;
if ($selected_user_id) { $conversation = $message_obj->getConversation($_SESSION['user_id'], $selected_user_id);
// Mark messages as read
foreach ($conversation as $msg) {
if ($msg['receiver_id'] == $_SESSION['user_id'] && !$msg['is_read']) {
$message_obj->markAsRead($msg['id'], $_SESSION['user_id']);
}
}
}
// Send new message if ($_SERVER['REQUEST_METHOD'] == 'POST' && isset($_POST['send_message'])) { $receiver_id = intval($_POST['receiver_id']); $subject = trim($_POST['subject']); $message_content = trim($_POST['message']);
if (empty($message_content)) {
$error = 'Message cannot be empty.';
} else {
$message_obj->sender_id = $_SESSION['user_id'];
$message_obj->receiver_id = $receiver_id;
$message_obj->subject = $subject;
$message_obj->message = $message_content;
if ($message_obj->create()) {
$success = 'Message sent successfully!';
// Refresh conversation
$conversation = $message_obj->getConversation($_SESSION['user_id'], $receiver_id);
} else {
$error = 'Failed to send message. Please try again.';
}
}
}
$unread_count = $message_obj->getUnreadCount($_SESSION['user_id']);
// HTML messaging interface would follow...
7. Admin Dashboard
Basic Admin Functions
Create pages/admin/dashboard.php:
session_start(); require_once '../../includes/config.php'; require_once '../../includes/database.php'; require_once '../../includes/auth.php';
requireLogin();
// Check if user is admin if (!isAdmin($_SESSION['user_id'])) { header('Location: ../dashboard.php'); exit; }
$database = new Database(); $db = $database->getConnection();
// Get statistics $query = "SELECT COUNT() as total_users, SUM(is_verified) as verified_users, COUNT(DATE(profile_created) = CURDATE()) as new_today, (SELECT COUNT() FROM messages WHERE DATE(sent_date) = CURDATE()) as messages_today, (SELECT COUNT() FROM likes WHERE DATE(like_date) = CURDATE()) as likes_today, (SELECT COUNT() FROM matches WHERE DATE(match_date) = CURDATE()) as matches_today FROM users";
$stmt = $db->prepare($query); $stmt->execute(); $stats = $stmt->fetch(PDO::FETCH_ASSOC);
// Get recent signups $query = "SELECT username, email, profile_created, is_verified FROM users ORDER BY profile_created DESC LIMIT 10";
$stmt = $db->prepare($query); $stmt->execute(); $recent_users = $stmt->fetchAll(PDO::FETCH_ASSOC);
// HTML admin dashboard would follow...
8. Security Considerations
Input Validation and Sanitization
Create includes/functions.php with security functions:
function sanitizeInput($data) { $data = trim($data); $data = stripslashes($data); $data = htmlspecialchars($data, ENT_QUOTES, 'UTF-8'); return $data; }
function validateEmail($email) { return filter_var($email, FILTER_VALIDATE_EMAIL) !== false; }
function validateDate($date, $format = 'Y-m-d') { $d = DateTime::createFromFormat($format, $date); return $d && $d->format($format) === $date; }
function generateCSRFToken() { if (empty($_SESSION['csrf_token'])) { $_SESSION['csrf_token'] = bin2hex(random_bytes(32)); } return $_SESSION['csrf_token']; }
function validateCSRFToken($token) { return isset($_SESSION['csrf_token']) && hash_equals($_SESSION['csrf_token'], $token); }
function checkRateLimit($action, $user_id, $max_attempts = 5, $time_window = 3600) { $database = new Database(); $db = $database->getConnection();
$query = "SELECT COUNT(*) as attempt_count
FROM rate_limits
WHERE user_id = :user_id AND action = :action
AND attempt_time > DATE_SUB(NOW(), INTERVAL :time_window SECOND)";
$stmt = $db->prepare($query);
$stmt->bindParam(':user_id', $user_id);
$stmt->bindParam(':action', $action);
$stmt->bindParam(':time_window', $time_window);
$stmt->execute();
$result = $stmt->fetch(PDO::FETCH_ASSOC);
if ($result['attempt_count'] >= $max_attempts) {
return false;
}
// Log this attempt
$query = "INSERT INTO rate_limits (user_id, action, attempt_time)
VALUES (:user_id, :action, NOW())";
$stmt = $db->prepare($query);
$stmt->bindParam(':user_id', $user_id);
$stmt->bindParam(':action', $action);
$stmt->execute();
return true;
}
File Upload Security
function handleFileUpload($file, $user_id, $max_size = 5242880) { // 5MB $allowed_types = array('image/jpeg', 'image/png', 'image/gif'); $upload_path = UPLOAD_PATH . $user_id . '/';
// Create user directory if it doesn't exist
if (!is_dir($upload_path)) {
mkdir($upload_path, 0755, true);
}
// Validate file
if ($file['error'] !== UPLOAD_ERR_OK) {
throw new Exception('File upload error: ' . $file['error']);
}
if ($file['size'] > $max_size) {
throw new Exception('File size exceeds maximum allowed size.');
}
$finfo = finfo_open(FILEINFO_MIME_TYPE);
$mime_type = finfo_file($finfo, $file['tmp_name']);
finfo_close($finfo);
if (!in_array($mime_type, $allowed_types)) {
throw new Exception('Invalid file type. Only JPG, PNG, and GIF are allowed.');
}
// Generate unique filename
$extension = pathinfo($file['name'], PATHINFO_EXTENSION);
$filename = uniqid() . '.' . $extension;
$destination = $upload_path . $filename;
if (move_uploaded_file($file['tmp_name'], $destination)) {
// Create thumbnail
createThumbnail($destination, $upload_path . 'thumb_' . $filename, 200, 200);
return $filename;
} else {
throw new Exception('Failed to move uploaded file.');
}
}
9. Deployment Preparation
Database Optimization
Add indexes for better performance:
-- Users table indexes CREATE INDEX idx_users_username ON users(username); CREATE INDEX idx_users_email ON users(email); CREATE INDEX idx_users_last_login ON users(last_login); CREATE INDEX idx_users_is_active ON users(is_active);
-- Profiles table indexes
CREATE INDEX idx_profiles_user_id ON profiles(user_id);
CREATE INDEX idx_profiles_location ON profiles(location);
CREATE INDEX idx_profiles_relationship_goal ON profiles(relationship_goal);
-- Messages table indexes CREATE INDEX idx_messages_sender_id ON messages(sender_id); CREATE INDEX idx_messages_receiver_id ON messages(receiver_id); CREATE INDEX idx_messages_sent_date ON messages(sent_date); CREATE INDEX idx_messages_is_read ON messages(is_read);
-- Likes table indexes CREATE INDEX idx_likes_user_id ON likes(user_id); CREATE INDEX idx_likes_liked_user_id ON likes(liked_user_id);
Configuration for Production
Update includes/config.php for production:
// Error reporting ini_set('display_errors', 0); ini_set('log_errors', 1); ini_set('error_log', '/path/to/php-error.log');
// Database configuration for production define('DB_HOST', 'production-db-host'); define('DB_NAME', 'dating_site_prod'); define('DB_USER', 'production_user'); define('DB_PASS', 'strong_password_here'); define('SITE_URL', 'https://your-dating-site.com');
// Security define('ENABLE_HTTPS', true);
Backup Script
Create a backup script backup.php:
require_once 'includes/config.php'; require_once 'includes/database.php';
function backupDatabase($backup_path) { $database = new Database(); $db = $database->getConnection();
$tables = array();
$stmt = $db->query("SHOW TABLES");
while ($row = $stmt->fetch(PDO::FETCH_NUM)) {
$tables[] = $row[0];
}
$backup_file = $backup_path . 'backup-' . date('Y-m-d-H-i-s') . '.sql';
$handle = fopen($backup_file, 'w+');
foreach ($tables as $table) {
// Table structure
$stmt = $db->query("SHOW CREATE TABLE `$table`");
$row = $stmt->fetch(PDO::FETCH_NUM);
fwrite($handle, $row[1] . ";\n\n");
// Table data
$stmt = $db->query("SELECT * FROM `$table`");
while ($row = $stmt->fetch(PDO::FETCH_NUM)) {
$values = array_map(function($value) use ($db) {
if ($value === null) return 'NULL';
return $db->quote($value);
}, $row);
fwrite($handle, "INSERT INTO `$table` VALUES (" . implode(', ', $values) . ");\n");
}
fwrite($handle, "\n");
}
fclose($handle);
return $backup_file;
}
// Run backup daily via cron job if (php_sapi_name() === 'cli') { $backup_path = '/path/to/backups/'; backupDatabase($backup_path); }
This comprehensive guide provides the foundation for building a fully-functional dating website with PHP and MySQL. Remember to:
- Always validate and sanitize user input
- Use prepared statements to prevent SQL injection
- Implement proper password hashing
- Add CSRF protection to forms
- Implement rate limiting to prevent abuse
- Regularly backup your database
- Test thoroughly before deployment
The system can be extended with additional features like video profiles, advanced matching algorithms, mobile apps, and payment integration for premium features.