Introduction to the Project
The Online Movie Booking System is a comprehensive, full-stack web application designed to streamline the process of browsing movies, selecting showtimes, choosing seats, and booking tickets online. This platform provides a seamless experience for movie enthusiasts to discover films, check showtimes, and reserve seats from anywhere, anytime.
The system features role-based access control with three distinct user types: Admin, Theater Managers, and Customers. It includes real-time seat selection, multiple payment options, automated ticket generation, and comprehensive reporting for business intelligence.
Key Features
Admin Features
- Dashboard Overview: Comprehensive statistics on bookings, revenue, and user activity
- Theater Management: Add, edit, and manage theaters and screens
- Movie Management: Add movies, manage showtimes, and control pricing
- User Management: Manage customers and theater managers
- Booking Management: View and manage all bookings across theaters
- Revenue Reports: Generate detailed financial reports
- Promotions: Create and manage discount codes and offers
- System Settings: Configure ticket pricing, taxes, and fees
Theater Manager Features
- Screen Management: Manage screens, seating layouts, and amenities
- Showtime Management: Schedule movie shows with time slots
- Pricing Rules: Set different prices for showtimes (matinee, evening, weekend)
- Booking Overview: View bookings for their theater
- Seat Management: Manage seat availability and maintenance
- Reports: Generate theater-specific reports
- Staff Management: Manage theater staff (optional)
Customer Features
- Movie Discovery: Browse current and upcoming movies
- Search & Filter: Find movies by genre, language, format (2D/3D/IMAX)
- Theater Selection: Choose preferred theater and location
- Showtime Selection: View available showtimes for selected movie
- Seat Selection: Interactive seat map with real-time availability
- Booking Management: View, modify, or cancel bookings
- Ticket History: Access past bookings and e-tickets
- Reviews & Ratings: Rate and review watched movies
- Favorites: Create watchlist of favorite movies
- Loyalty Program: Earn points and redeem rewards
General Features
- Real-time Seat Availability: Live seat status updates
- Multiple Payment Options: Credit card, PayPal, digital wallets
- E-ticket Generation: PDF tickets with QR codes
- Email/SMS Confirmations: Automatic booking confirmations
- Reminder Notifications: Reminders before showtime
- Movie Trailers: Watch trailers directly on the platform
- Cast & Crew Information: Detailed movie information
- User Reviews: Read and write movie reviews
- Location-based Theater Search: Find nearby theaters
- Multi-language Support: Interface in multiple languages
Technology Stack
- Frontend: HTML5, CSS3, JavaScript (ES6+), Bootstrap 5
- Backend: PHP 8.0+ (Core PHP with OOP approach)
- Database: MySQL 5.7+
- Additional Libraries:
- QR Code generation for tickets
- TCPDF for PDF ticket generation
- Chart.js for analytics
- Bootstrap 5 for responsive UI
- Font Awesome for icons
- jQuery for AJAX operations
- Select2 for enhanced dropdowns
- DataTables for advanced tables
- Moment.js for date handling
- PHPMailer for email notifications
- Stripe/PayPal SDK for payments
Project File Structure
movie-booking-system/ │ ├── assets/ │ ├── css/ │ │ ├── style.css │ │ ├── dashboard.css │ │ ├── seat-layout.css │ │ ├── responsive.css │ │ └── dark-mode.css │ ├── js/ │ │ ├── main.js │ │ ├── booking.js │ │ ├── seat-selection.js │ │ ├── payment.js │ │ ├── validation.js │ │ └── charts.js │ ├── images/ │ │ ├── movies/ │ │ ├── theaters/ │ │ └── avatars/ │ └── plugins/ │ ├── datatables/ │ ├── select2/ │ └── qrcode/ │ ├── includes/ │ ├── config.php │ ├── Database.php │ ├── functions.php │ ├── auth.php │ ├── Movie.php │ ├── Theater.php │ ├── Booking.php │ ├── Seat.php │ ├── Payment.php │ ├── User.php │ ├── Review.php │ └── helpers/ │ ├── DateHelper.php │ ├── SeatHelper.php │ └── TicketHelper.php │ ├── admin/ │ ├── dashboard.php │ ├── manage_movies.php │ ├── add_movie.php │ ├── edit_movie.php │ ├── manage_theaters.php │ ├── add_theater.php │ ├── manage_screens.php │ ├── manage_showtimes.php │ ├── manage_bookings.php │ ├── manage_users.php │ ├── manage_promotions.php │ ├── revenue_reports.php │ └── settings.php │ ├── manager/ │ ├── dashboard.php │ ├── screens.php │ ├── add_screen.php │ ├── showtimes.php │ ├── add_showtime.php │ ├── bookings.php │ ├── seat_maintenance.php │ ├── pricing.php │ ├── reports.php │ └── profile.php │ ├── customer/ │ ├── index.php │ ├── movies.php │ ├── movie_details.php │ ├── theaters.php │ ├── showtimes.php │ ├── select_seats.php │ ├── checkout.php │ ├── payment.php │ ├── booking_confirmation.php │ ├── my_bookings.php │ ├── booking_details.php │ ├── ticket.php │ ├── reviews.php │ ├── write_review.php │ ├── profile.php │ └── wishlist.php │ ├── api/ │ ├── get_showtimes.php │ ├── get_seats.php │ ├── hold_seats.php │ ├── release_seats.php │ ├── confirm_booking.php │ ├── process_payment.php │ ├── apply_promo.php │ └── search.php │ ├── includes/ │ └── notifications/ │ ├── email_templates/ │ │ ├── booking_confirmation.php │ │ ├── booking_cancellation.php │ │ ├── reminder.php │ │ └── payment_receipt.php │ └── sms_templates/ │ ├── uploads/ │ ├── movies/ │ ├── theaters/ │ └── avatars/ │ ├── tickets/ │ └── (generated PDF tickets) │ ├── vendor/ │ ├── index.php ├── login.php ├── register.php ├── forgot_password.php ├── reset_password.php ├── logout.php ├── .env ├── .gitignore ├── composer.json └── sql/ └── database.sql
Database Schema
File: sql/database.sql
-- Create Database
CREATE DATABASE IF NOT EXISTS `movie_booking_system`;
USE `movie_booking_system`;
-- Users Table
CREATE TABLE `users` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`user_id` VARCHAR(20) UNIQUE NOT NULL,
`email` VARCHAR(100) UNIQUE NOT NULL,
`password` VARCHAR(255) NOT NULL,
`first_name` VARCHAR(50) NOT NULL,
`last_name` VARCHAR(50) NOT NULL,
`phone` VARCHAR(20),
`date_of_birth` DATE,
`gender` ENUM('male', 'female', 'other') NULL,
`address` TEXT,
`city` VARCHAR(100),
`state` VARCHAR(50),
`country` VARCHAR(50),
`postal_code` VARCHAR(20),
`profile_picture` VARCHAR(255) DEFAULT 'default.png',
`role` ENUM('admin', 'manager', 'customer') NOT NULL DEFAULT 'customer',
`theater_id` INT(11), -- for managers assigned to specific theater
`status` ENUM('active', 'inactive', 'suspended') DEFAULT 'active',
`email_verified` BOOLEAN DEFAULT FALSE,
`phone_verified` BOOLEAN DEFAULT FALSE,
`verification_token` VARCHAR(255),
`reset_token` VARCHAR(255),
`reset_expires` DATETIME,
`loyalty_points` INT DEFAULT 0,
`total_bookings` INT DEFAULT 0,
`last_login` DATETIME,
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
`updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
INDEX `idx_email` (`email`),
INDEX `idx_role` (`role`),
INDEX `idx_status` (`status`)
);
-- Theaters Table
CREATE TABLE `theaters` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`name` VARCHAR(255) NOT NULL,
`address` TEXT NOT NULL,
`city` VARCHAR(100) NOT NULL,
`state` VARCHAR(50),
`country` VARCHAR(50),
`postal_code` VARCHAR(20),
`phone` VARCHAR(20),
`email` VARCHAR(100),
`website` VARCHAR(255),
`latitude` DECIMAL(10,8),
`longitude` DECIMAL(11,8),
`total_screens` INT DEFAULT 1,
`amenities` TEXT,
`image` VARCHAR(255),
`status` ENUM('active', 'inactive', 'maintenance') DEFAULT 'active',
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
`updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
INDEX `idx_city` (`city`),
INDEX `idx_status` (`status`)
);
-- Screens Table
CREATE TABLE `screens` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`theater_id` INT(11) NOT NULL,
`screen_number` VARCHAR(20) NOT NULL,
`name` VARCHAR(100),
`capacity` INT NOT NULL,
`rows` INT NOT NULL,
`columns` INT NOT NULL,
`screen_type` ENUM('standard', 'imax', '3d', '4dx', 'vip') DEFAULT 'standard',
`audio_system` VARCHAR(100),
`projection_type` VARCHAR(100),
`has_wheelchair_access` BOOLEAN DEFAULT TRUE,
`has_vip_seats` BOOLEAN DEFAULT FALSE,
`status` ENUM('active', 'maintenance', 'inactive') DEFAULT 'active',
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
`updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
FOREIGN KEY (`theater_id`) REFERENCES `theaters`(`id`) ON DELETE CASCADE,
UNIQUE KEY `unique_screen` (`theater_id`, `screen_number`)
);
-- Seat Types Table
CREATE TABLE `seat_types` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`name` VARCHAR(50) NOT NULL,
`description` TEXT,
`price_multiplier` DECIMAL(3,2) DEFAULT 1.00,
`color_code` VARCHAR(7) DEFAULT '#28a745',
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`)
);
-- Insert default seat types
INSERT INTO `seat_types` (`name`, `description`, `price_multiplier`, `color_code`) VALUES
('Standard', 'Regular seating', 1.00, '#28a745'),
('Premium', 'Extra legroom and comfort', 1.50, '#ffc107'),
('VIP', 'Luxury recliner seats', 2.00, '#dc3545'),
('Couple', 'Two seats together with no divider', 1.80, '#17a2b8'),
('Wheelchair', 'Accessible seating', 1.00, '#6c757d');
-- Seats Table
CREATE TABLE `seats` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`screen_id` INT(11) NOT NULL,
`seat_type_id` INT(11) DEFAULT 1,
`row_label` VARCHAR(5) NOT NULL,
`seat_number` VARCHAR(5) NOT NULL,
`seat_column` INT NOT NULL,
`seat_row` INT NOT NULL,
`x_coordinate` INT,
`y_coordinate` INT,
`status` ENUM('active', 'maintenance', 'blocked') DEFAULT 'active',
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
`updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
FOREIGN KEY (`screen_id`) REFERENCES `screens`(`id`) ON DELETE CASCADE,
FOREIGN KEY (`seat_type_id`) REFERENCES `seat_types`(`id`),
UNIQUE KEY `unique_seat` (`screen_id`, `row_label`, `seat_number`)
);
-- Movies Table
CREATE TABLE `movies` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`title` VARCHAR(255) NOT NULL,
`original_title` VARCHAR(255),
`description` TEXT,
`poster` VARCHAR(255),
`backdrop` VARCHAR(255),
`trailer_url` VARCHAR(255),
`duration` INT NOT NULL, -- in minutes
`release_date` DATE,
`end_date` DATE,
`language` VARCHAR(50),
`subtitles` VARCHAR(50),
`genre` VARCHAR(255),
`director` VARCHAR(255),
`cast` TEXT,
`producer` VARCHAR(255),
`writer` VARCHAR(255),
`music_director` VARCHAR(255),
`cinematographer` VARCHAR(255),
`rating` ENUM('G', 'PG', 'PG-13', 'R', 'NC-17') DEFAULT 'PG-13',
`critic_rating` DECIMAL(3,1) DEFAULT 0.0,
`user_rating` DECIMAL(3,1) DEFAULT 0.0,
`total_ratings` INT DEFAULT 0,
`imdb_id` VARCHAR(20),
`imdb_rating` DECIMAL(3,1),
`metacritic_score` INT,
`rotten_tomatoes` INT,
`awards` TEXT,
`status` ENUM('now_showing', 'coming_soon', 'ended') DEFAULT 'coming_soon',
`featured` BOOLEAN DEFAULT FALSE,
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
`updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
INDEX `idx_status` (`status`),
INDEX `idx_release` (`release_date`),
INDEX `idx_featured` (`featured`),
FULLTEXT `idx_search` (`title`, `description`, `cast`, `director`)
);
-- Showtimes Table
CREATE TABLE `showtimes` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`movie_id` INT(11) NOT NULL,
`screen_id` INT(11) NOT NULL,
`theater_id` INT(11) NOT NULL,
`show_date` DATE NOT NULL,
`start_time` TIME NOT NULL,
`end_time` TIME NOT NULL,
`show_type` ENUM('2d', '3d', 'imax', '4dx') DEFAULT '2d',
`language` VARCHAR(50),
`subtitles` VARCHAR(50),
`base_price` DECIMAL(10,2) NOT NULL,
`price_multiplier` DECIMAL(3,2) DEFAULT 1.00,
`is_holiday` BOOLEAN DEFAULT FALSE,
`is_weekend` BOOLEAN DEFAULT FALSE,
`is_special` BOOLEAN DEFAULT FALSE,
`special_event` VARCHAR(255),
`available_seats` INT NOT NULL,
`total_seats` INT NOT NULL,
`status` ENUM('active', 'cancelled', 'completed', 'sold_out') DEFAULT 'active',
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
`updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
FOREIGN KEY (`movie_id`) REFERENCES `movies`(`id`) ON DELETE CASCADE,
FOREIGN KEY (`screen_id`) REFERENCES `screens`(`id`),
FOREIGN KEY (`theater_id`) REFERENCES `theaters`(`id`),
INDEX `idx_datetime` (`show_date`, `start_time`),
INDEX `idx_status` (`status`)
);
-- Bookings Table
CREATE TABLE `bookings` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`booking_id` VARCHAR(20) UNIQUE NOT NULL,
`user_id` INT(11) NOT NULL,
`showtime_id` INT(11) NOT NULL,
`theater_id` INT(11) NOT NULL,
`movie_id` INT(11) NOT NULL,
`booking_date` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
`show_date` DATE NOT NULL,
`show_time` TIME NOT NULL,
`total_amount` DECIMAL(10,2) NOT NULL,
`discount_amount` DECIMAL(10,2) DEFAULT 0.00,
`tax_amount` DECIMAL(10,2) DEFAULT 0.00,
`final_amount` DECIMAL(10,2) NOT NULL,
`currency` VARCHAR(3) DEFAULT 'USD',
`number_of_seats` INT NOT NULL,
`seat_ids` TEXT, -- comma-separated seat IDs
`seat_labels` TEXT, -- comma-separated seat labels (e.g., A1, A2)
`payment_method` VARCHAR(50),
`payment_id` VARCHAR(255),
`payment_status` ENUM('pending', 'completed', 'failed', 'refunded') DEFAULT 'pending',
`booking_status` ENUM('pending', 'confirmed', 'cancelled', 'completed', 'no_show') DEFAULT 'pending',
`qr_code` VARCHAR(255), -- path to QR code image
`ticket_pdf` VARCHAR(255), -- path to PDF ticket
`special_requests` TEXT,
`cancellation_reason` TEXT,
`cancelled_at` DATETIME,
`checked_in` BOOLEAN DEFAULT FALSE,
`checked_in_at` DATETIME,
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
`updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
FOREIGN KEY (`user_id`) REFERENCES `users`(`id`),
FOREIGN KEY (`showtime_id`) REFERENCES `showtimes`(`id`),
FOREIGN KEY (`theater_id`) REFERENCES `theaters`(`id`),
FOREIGN KEY (`movie_id`) REFERENCES `movies`(`id`),
INDEX `idx_booking_id` (`booking_id`),
INDEX `idx_user` (`user_id`),
INDEX `idx_showtime` (`showtime_id`),
INDEX `idx_status` (`booking_status`),
INDEX `idx_payment` (`payment_status`)
);
-- Booking Seats Junction Table (to track individual seats in booking)
CREATE TABLE `booking_seats` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`booking_id` INT(11) NOT NULL,
`seat_id` INT(11) NOT NULL,
`seat_type_id` INT(11) NOT NULL,
`price` DECIMAL(10,2) NOT NULL,
`status` ENUM('active', 'cancelled', 'refunded') DEFAULT 'active',
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
FOREIGN KEY (`booking_id`) REFERENCES `bookings`(`id`) ON DELETE CASCADE,
FOREIGN KEY (`seat_id`) REFERENCES `seats`(`id`),
FOREIGN KEY (`seat_type_id`) REFERENCES `seat_types`(`id`),
UNIQUE KEY `unique_booking_seat` (`booking_id`, `seat_id`)
);
-- Temporary Seat Holds (for pending bookings)
CREATE TABLE `seat_holds` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`showtime_id` INT(11) NOT NULL,
`seat_id` INT(11) NOT NULL,
`user_id` INT(11) NOT NULL,
`session_id` VARCHAR(255) NOT NULL,
`hold_until` DATETIME NOT NULL,
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
FOREIGN KEY (`showtime_id`) REFERENCES `showtimes`(`id`) ON DELETE CASCADE,
FOREIGN KEY (`seat_id`) REFERENCES `seats`(`id`) ON DELETE CASCADE,
FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE CASCADE,
INDEX `idx_showtime_seat` (`showtime_id`, `seat_id`),
INDEX `idx_hold_until` (`hold_until`)
);
-- Reviews Table
CREATE TABLE `reviews` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`user_id` INT(11) NOT NULL,
`movie_id` INT(11) NOT NULL,
`booking_id` INT(11), -- to ensure only watched movies can be reviewed
`rating` TINYINT NOT NULL CHECK (rating >= 1 AND rating <= 10),
`title` VARCHAR(255),
`review` TEXT,
`spoiler` BOOLEAN DEFAULT FALSE,
`likes` INT DEFAULT 0,
`dislikes` INT DEFAULT 0,
`status` ENUM('pending', 'approved', 'rejected') DEFAULT 'pending',
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
`updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
FOREIGN KEY (`user_id`) REFERENCES `users`(`id`),
FOREIGN KEY (`movie_id`) REFERENCES `movies`(`id`) ON DELETE CASCADE,
FOREIGN KEY (`booking_id`) REFERENCES `bookings`(`id`),
UNIQUE KEY `unique_user_movie` (`user_id`, `movie_id`),
INDEX `idx_movie` (`movie_id`),
INDEX `idx_rating` (`rating`)
);
-- Payments Table
CREATE TABLE `payments` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`booking_id` INT(11) NOT NULL,
`transaction_id` VARCHAR(255) UNIQUE NOT NULL,
`amount` DECIMAL(10,2) NOT NULL,
`currency` VARCHAR(3) DEFAULT 'USD',
`payment_method` VARCHAR(50) NOT NULL,
`payment_status` ENUM('pending', 'completed', 'failed', 'refunded') DEFAULT 'pending',
`payment_details` JSON,
`refund_amount` DECIMAL(10,2),
`refund_reason` TEXT,
`refunded_at` DATETIME,
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
`updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
FOREIGN KEY (`booking_id`) REFERENCES `bookings`(`id`) ON DELETE CASCADE,
INDEX `idx_transaction` (`transaction_id`),
INDEX `idx_status` (`payment_status`)
);
-- Promotions/Coupons Table
CREATE TABLE `promotions` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`code` VARCHAR(50) UNIQUE NOT NULL,
`description` TEXT,
`discount_type` ENUM('percentage', 'fixed') NOT NULL,
`discount_value` DECIMAL(10,2) NOT NULL,
`min_booking_amount` DECIMAL(10,2) DEFAULT 0,
`max_discount` DECIMAL(10,2),
`usage_limit` INT,
`usage_count` INT DEFAULT 0,
`per_user_limit` INT DEFAULT 1,
`applicable_movies` TEXT, -- comma-separated movie IDs
`applicable_theaters` TEXT, -- comma-separated theater IDs
`applicable_days` VARCHAR(50), -- comma-separated days (0-6)
`start_date` DATETIME NOT NULL,
`end_date` DATETIME NOT NULL,
`status` ENUM('active', 'inactive', 'expired') DEFAULT 'active',
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
`updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
INDEX `idx_code` (`code`),
INDEX `idx_dates` (`start_date`, `end_date`),
INDEX `idx_status` (`status`)
);
-- Loyalty Points Transactions
CREATE TABLE `loyalty_transactions` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`user_id` INT(11) NOT NULL,
`booking_id` INT(11),
`points` INT NOT NULL,
`type` ENUM('earned', 'redeemed', 'expired', 'adjusted') NOT NULL,
`description` VARCHAR(255),
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE CASCADE,
FOREIGN KEY (`booking_id`) REFERENCES `bookings`(`id`) ON DELETE SET NULL,
INDEX `idx_user` (`user_id`)
);
-- Notifications Table
CREATE TABLE `notifications` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`user_id` INT(11) NOT NULL,
`type` ENUM('booking', 'reminder', 'promotion', 'system') NOT NULL,
`title` VARCHAR(255) NOT NULL,
`message` TEXT NOT NULL,
`data` JSON,
`is_read` BOOLEAN DEFAULT FALSE,
`read_at` DATETIME,
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE CASCADE,
INDEX `idx_user_read` (`user_id`, `is_read`)
);
-- Theater Managers Assignment
ALTER TABLE `users` ADD CONSTRAINT `fk_user_theater` FOREIGN KEY (`theater_id`) REFERENCES `theaters`(`id`) ON DELETE SET NULL;
-- Insert Default Admin
INSERT INTO `users` (`user_id`, `email`, `password`, `first_name`, `last_name`, `role`, `email_verified`)
VALUES ('ADMIN001', '[email protected]', '$2y$10$YourHashedPasswordHere', 'System', 'Administrator', 'admin', TRUE);
-- Insert Sample Theaters
INSERT INTO `theaters` (`name`, `address`, `city`, `state`, `country`, `phone`, `email`, `total_screens`) VALUES
('Cinema City Downtown', '123 Main Street', 'New York', 'NY', 'USA', '+1-212-555-1234', '[email protected]', 8),
('Cinema City Uptown', '456 Broadway', 'New York', 'NY', 'USA', '+1-212-555-5678', '[email protected]', 6),
('Movie Palace', '789 Hollywood Blvd', 'Los Angeles', 'CA', 'USA', '+1-323-555-9012', '[email protected]', 10);
-- Insert Sample Movies
INSERT INTO `movies` (`title`, `description`, `duration`, `release_date`, `genre`, `director`, `cast`, `rating`, `status`) VALUES
('The Last Voyage', 'An epic adventure across the oceans', 148, '2025-03-15', 'Adventure, Drama', 'James Cameron', 'Leonardo DiCaprio, Kate Winslet', 'PG-13', 'now_showing'),
('Mystery of the Lost City', 'Archaeologists discover an ancient civilization', 125, '2025-03-01', 'Action, Adventure', 'Steven Spielberg', 'Harrison Ford, Phoebe Waller-Bridge', 'PG-13', 'now_showing'),
('Laugh Out Loud', 'A hilarious comedy about family reunions', 105, '2025-04-05', 'Comedy', 'Todd Phillips', 'Zach Galifianakis, Bradley Cooper', 'R', 'coming_soon'),
('Space Explorers', 'Astronauts on a mission to Mars', 142, '2025-03-20', 'Sci-Fi', 'Christopher Nolan', 'Matthew McConaughey, Anne Hathaway', 'PG-13', 'now_showing'),
('The Love Story', 'A romantic tale of two hearts', 118, '2025-02-14', 'Romance', 'Nancy Meyers', 'Meryl Streep, Steve Carell', 'PG-13', 'ended');
Core PHP Classes
Database Class
File: includes/Database.php
<?php
/**
* Database Class
* Handles all database connections and operations using PDO with singleton pattern
*/
class Database {
private static $instance = null;
private $connection;
private $statement;
private $host;
private $dbname;
private $username;
private $password;
/**
* Private constructor for singleton pattern
*/
private function __construct() {
$this->host = DB_HOST;
$this->dbname = DB_NAME;
$this->username = DB_USER;
$this->password = DB_PASS;
try {
$this->connection = new PDO(
"mysql:host={$this->host};dbname={$this->dbname};charset=utf8mb4",
$this->username,
$this->password,
[
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false
]
);
} catch (PDOException $e) {
die("Database connection failed: " . $e->getMessage());
}
}
/**
* Get database instance (Singleton)
*/
public static function getInstance() {
if (self::$instance === null) {
self::$instance = new self();
}
return self::$instance;
}
/**
* Prepare and execute query with parameters
*/
public function query($sql, $params = []) {
try {
$this->statement = $this->connection->prepare($sql);
$this->statement->execute($params);
return $this->statement;
} catch (PDOException $e) {
$this->logError($e->getMessage(), $sql, $params);
throw new Exception("Database query failed: " . $e->getMessage());
}
}
/**
* Get single row
*/
public function getRow($sql, $params = []) {
$result = $this->query($sql, $params);
return $result->fetch();
}
/**
* Get multiple rows
*/
public function getRows($sql, $params = []) {
$result = $this->query($sql, $params);
return $result->fetchAll();
}
/**
* Get single value
*/
public function getValue($sql, $params = []) {
$result = $this->query($sql, $params);
return $result->fetchColumn();
}
/**
* Insert data and return last insert ID
*/
public function insert($table, $data) {
$columns = implode(', ', array_keys($data));
$placeholders = ':' . implode(', :', array_keys($data));
$sql = "INSERT INTO {$table} ({$columns}) VALUES ({$placeholders})";
$this->query($sql, $data);
return $this->connection->lastInsertId();
}
/**
* Insert multiple rows
*/
public function insertMultiple($table, $data) {
if (empty($data)) {
return false;
}
$columns = implode(', ', array_keys($data[0]));
$placeholders = [];
$values = [];
foreach ($data as $index => $row) {
$rowPlaceholders = [];
foreach (array_keys($row) as $key) {
$placeholder = ":{$key}_{$index}";
$rowPlaceholders[] = $placeholder;
$values[$placeholder] = $row[$key];
}
$placeholders[] = '(' . implode(', ', $rowPlaceholders) . ')';
}
$sql = "INSERT INTO {$table} ({$columns}) VALUES " . implode(', ', $placeholders);
$this->query($sql, $values);
return $this->connection->lastInsertId();
}
/**
* Update data
*/
public function update($table, $data, $where, $whereParams = []) {
$set = [];
foreach (array_keys($data) as $column) {
$set[] = "{$column} = :{$column}";
}
$sql = "UPDATE {$table} SET " . implode(', ', $set) . " WHERE {$where}";
$params = array_merge($data, $whereParams);
return $this->query($sql, $params)->rowCount();
}
/**
* Delete data
*/
public function delete($table, $where, $params = []) {
$sql = "DELETE FROM {$table} WHERE {$where}";
return $this->query($sql, $params)->rowCount();
}
/**
* Begin transaction
*/
public function beginTransaction() {
return $this->connection->beginTransaction();
}
/**
* Commit transaction
*/
public function commit() {
return $this->connection->commit();
}
/**
* Rollback transaction
*/
public function rollback() {
return $this->connection->rollBack();
}
/**
* Get last insert ID
*/
public function lastInsertId() {
return $this->connection->lastInsertId();
}
/**
* Log database errors
*/
private function logError($message, $sql, $params) {
$logFile = __DIR__ . '/../logs/database.log';
$logDir = dirname($logFile);
if (!is_dir($logDir)) {
mkdir($logDir, 0755, true);
}
$timestamp = date('Y-m-d H:i:s');
$logMessage = "[{$timestamp}] Error: {$message}\n";
$logMessage .= "SQL: {$sql}\n";
$logMessage .= "Params: " . json_encode($params) . "\n";
$logMessage .= "------------------------\n";
file_put_contents($logFile, $logMessage, FILE_APPEND);
}
/**
* Prevent cloning of the instance
*/
private function __clone() {}
/**
* Prevent unserializing of the instance
*/
public function __wakeup() {}
}
?>
Configuration File
File: includes/config.php
<?php
/**
* Configuration File
* Loads environment variables and sets up constants
*/
// Start session if not started
if (session_status() == PHP_SESSION_NONE) {
session_start();
}
// Load environment variables from .env file
function loadEnv($path) {
if (!file_exists($path)) {
return false;
}
$lines = file($path, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
foreach ($lines as $line) {
if (strpos(trim($line), '#') === 0) {
continue;
}
list($name, $value) = explode('=', $line, 2);
$name = trim($name);
$value = trim($value);
if (!array_key_exists($name, $_ENV)) {
$_ENV[$name] = $value;
putenv(sprintf('%s=%s', $name, $value));
}
}
return true;
}
// Load environment variables
loadEnv(__DIR__ . '/../.env');
// Database Configuration
define('DB_HOST', getenv('DB_HOST') ?: 'localhost');
define('DB_NAME', getenv('DB_NAME') ?: 'movie_booking_system');
define('DB_USER', getenv('DB_USER') ?: 'root');
define('DB_PASS', getenv('DB_PASS') ?: '');
// Application Configuration
define('APP_NAME', getenv('APP_NAME') ?: 'Movie Booking System');
define('APP_URL', getenv('APP_URL') ?: 'http://localhost/movie-booking-system');
define('APP_VERSION', getenv('APP_VERSION') ?: '1.0.0');
define('DEBUG_MODE', getenv('DEBUG_MODE') === 'true');
// Security Configuration
define('SESSION_TIMEOUT', getenv('SESSION_TIMEOUT') ?: 3600); // 1 hour
define('BCRYPT_ROUNDS', 12);
define('CSRF_TOKEN_NAME', 'csrf_token');
// Upload Configuration
define('UPLOAD_DIR', __DIR__ . '/../uploads/');
define('TICKET_DIR', __DIR__ . '/../tickets/');
define('MAX_FILE_SIZE', getenv('MAX_FILE_SIZE') ?: 5 * 1024 * 1024); // 5MB
define('ALLOWED_EXTENSIONS', ['jpg', 'jpeg', 'png', 'gif']);
// Pagination
define('ITEMS_PER_PAGE', getenv('ITEMS_PER_PAGE') ?: 20);
// Date/Time Configuration
date_default_timezone_set(getenv('TIMEZONE') ?: 'America/New_York');
define('DATE_FORMAT', 'Y-m-d');
define('TIME_FORMAT', 'H:i');
define('DATETIME_FORMAT', 'Y-m-d H:i:s');
// Booking Settings
define('SEAT_HOLD_MINUTES', getenv('SEAT_HOLD_MINUTES') ?: 10); // minutes to hold seats
define('MAX_SEATS_PER_BOOKING', getenv('MAX_SEATS_PER_BOOKING') ?: 10);
define('CANCELLATION_HOURS', getenv('CANCELLATION_HOURS') ?: 2); // hours before show
define('LOYALTY_POINTS_PER_BOOKING', getenv('LOYALTY_POINTS_PER_BOOKING') ?: 10);
// Tax Settings
define('TAX_RATE', getenv('TAX_RATE') ?: 0.08); // 8% tax
define('CONVENIENCE_FEE', getenv('CONVENIENCE_FEE') ?: 1.50); // per ticket
// Notification Settings
define('ENABLE_REMINDERS', getenv('ENABLE_REMINDERS') === 'true');
define('REMINDER_HOURS', getenv('REMINDER_HOURS') ?: 24); // hours before show
define('ENABLE_SMS', getenv('ENABLE_SMS') === 'true');
// Payment Settings
define('ENABLE_PAYMENTS', getenv('ENABLE_PAYMENTS') === 'true');
define('PAYMENT_GATEWAY', getenv('PAYMENT_GATEWAY') ?: 'stripe');
define('STRIPE_KEY', getenv('STRIPE_KEY') ?: '');
define('STRIPE_SECRET', getenv('STRIPE_SECRET') ?: '');
define('PAYPAL_CLIENT_ID', getenv('PAYPAL_CLIENT_ID') ?: '');
define('PAYPAL_SECRET', getenv('PAYPAL_SECRET') ?: '');
// Error Reporting
if (DEBUG_MODE) {
error_reporting(E_ALL);
ini_set('display_errors', 1);
} else {
error_reporting(0);
ini_set('display_errors', 0);
}
// Include required files
require_once __DIR__ . '/Database.php';
require_once __DIR__ . '/functions.php';
require_once __DIR__ . '/auth.php';
require_once __DIR__ . '/Movie.php';
require_once __DIR__ . '/Theater.php';
require_once __DIR__ . '/Booking.php';
require_once __DIR__ . '/Seat.php';
require_once __DIR__ . '/Payment.php';
require_once __DIR__ . '/Review.php';
// Initialize database connection
$db = Database::getInstance();
// Set timezone for MySQL
$db->query("SET time_zone = ?", [date('P')]);
?>
Helper Functions
File: includes/functions.php
```php
<?php
/**
- Helper Functions
- Common utility functions used throughout the application
*/
/**
- Sanitize input data
*/
function sanitize($input) {
if (is_array($input)) {
return array_map('sanitize', $input);
}
return htmlspecialchars(trim($input), ENT_QUOTES, 'UTF-8');
}
/**
- Generate CSRF token
*/
function generateCSRFToken() {
if (!isset($_SESSION[CSRF_TOKEN_NAME])) {
$_SESSION[CSRF_TOKEN_NAME] = bin2hex(random_bytes(32));
}
return $_SESSION[CSRF_TOKEN_NAME];
}
/**
- Verify CSRF token
*/
function verifyCSRFToken($token) {
if (!isset($_SESSION[CSRF_TOKEN_NAME]) || $token !== $_SESSION[CSRF_TOKEN_NAME]) {
return false;
}
return true;
}
/**
- Redirect to URL
*/
function redirect($url) {
header("Location: " . APP_URL . $url);
exit();
}
/**
- Format amount with currency
*/
function formatAmount($amount, $currency = null) {
if ($currency === null) {
$currency = CURRENCY ?? 'USD';
} $symbols = [
'USD' => '$',
'EUR' => '€',
'GBP' => '£',
'JPY' => '¥',
'INR' => '₹',
'CAD' => 'C$',
'AUD' => 'A$'
]; $symbol = $symbols[$currency] ?? '$'; return $symbol . ' ' . number_format($amount, 2);
}
/**
- Format date
*/
function formatDate($date, $format = null) {
if ($format === null) {
$format = DATE_FORMAT;
} if ($date instanceof DateTime) {
return $date->format($format);
} return date($format, strtotime($date));
}
/**
- Format time
*/
function formatTime($time, $format = null) {
if ($format === null) {
$format = TIME_FORMAT;
} return date($format, strtotime($time));
}
/**
- Format datetime
*/
function formatDateTime($datetime, $format = null) {
if ($format === null) {
$format = DATETIME_FORMAT;
} return date($format, strtotime($datetime));
}
/**
- Get time ago string
*/
function timeAgo($datetime) {
$time = strtotime($datetime);
$now = time();
$diff = $now - $time; if ($diff < 60) {
return $diff . ' seconds ago';
} elseif ($diff < 3600) {
return floor($diff / 60) . ' minutes ago';
} elseif ($diff < 86400) {
return floor($diff / 3600) . ' hours ago';
} elseif ($diff < 2592000) {
return floor($diff / 86400) . ' days ago';
} elseif ($diff < 31536000) {
return floor($diff / 2592000) . ' months ago';
} else {
return floor($diff / 31536000) . ' years ago';
}
}
/**
- Generate unique booking ID
*/
function generateBookingId() {
$prefix = 'BK';
$date = date('Ymd');
$random = strtoupper(substr(uniqid(), -6));
return $prefix . $date . $random;
}
/**
- Generate unique user ID
*/
function generateUserId($role) {
$prefix = strtoupper(substr($role, 0, 2));
$date = date('Y');
$random = str_pad(mt_rand(1, 9999), 4, '0', STR_PAD_LEFT);
return $prefix . $date . $random;
}
/**
- Calculate end time based on start time and movie duration
*/
function calculateEndTime($startTime, $durationMinutes) {
return date('H:i:s', strtotime($startTime) + ($durationMinutes * 60));
}
/**
- Check if showtime is in the past
*/
function isPastShowtime($showDate, $showTime) {
$showDateTime = strtotime($showDate . ' ' . $showTime);
return $showDateTime < time();
}
/**
- Get day of week from date
*/
function getDayOfWeek($date) {
return date('w', strtotime($date));
}
/**
- Check if date is weekend
*/
function isWeekend($date) {
$day = getDayOfWeek($date);
return ($day == 0 || $day == 6); // 0 = Sunday, 6 = Saturday
}
/**
- Calculate final price with taxes and fees
*/
function calculateFinalPrice($basePrice, $quantity = 1, $taxRate = null, $convenienceFee = null) {
if ($taxRate === null) {
$taxRate = TAX_RATE;
} if ($convenienceFee === null) {
$convenienceFee = CONVENIENCE_FEE;
} $subtotal = $basePrice * $quantity;
$tax = $subtotal * $taxRate;
$fee = $convenienceFee * $quantity;
$total = $subtotal + $tax + $fee; return [
'subtotal' => $subtotal,
'tax' => $tax,
'fee' => $fee,
'total' => $total
];
}
/**
- Generate star rating HTML
*/
function generateStarRating($rating, $maxRating = 5) {
$fullStars = floor($rating);
$halfStar = ($rating - $fullStars) >= 0.5;
$emptyStars = $maxRating - $fullStars - ($halfStar ? 1 : 0); $html = ''; // Full stars
for ($i = 0; $i < $fullStars; $i++) {
$html .= '';
} // Half star
if ($halfStar) {
$html .= '';
} // Empty stars
for ($i = 0; $i < $emptyStars; $i++) {
$html .= '';
} return $html;
}
/**
- Get movie runtime in hours and minutes
*/
function formatRuntime($minutes) {
$hours = floor($minutes / 60);
$mins = $minutes % 60; if ($hours > 0) {
return $hours . 'h ' . $mins . 'm';
} return $mins . ' minutes';
}
/**
- Generate QR code for ticket
*/
function generateTicketQRCode($bookingId, $data) {
require_once DIR . '/../vendor/autoload.php'; use Endroid\QrCode\QrCode;
use Endroid\QrCode\Writer\PngWriter; $qrCode = QrCode::create($data);
$writer = new PngWriter();
$result = $writer->write($qrCode); $filename = 'qr_' . $bookingId . '.png';
$path = TICKET_DIR . $filename; $result->saveToFile($path); return $filename;
}
/**
- Generate PDF ticket
*/
function generateTicketPDF($bookingData, $seatsData) {
require_once DIR . '/../vendor/autoload.php'; $pdf = new TCPDF(PDF_PAGE_ORIENTATION, PDF_UNIT, PDF_PAGE_FORMAT, true, 'UTF-8', false); // Set document information
$pdf->SetCreator(APP_NAME);
$pdf->SetAuthor(APP_NAME);
$pdf->SetTitle('Movie Ticket - ' . $bookingData['booking_id']);
$pdf->SetSubject('Movie Ticket'); // Remove header/footer
$pdf->setPrintHeader(false);
$pdf->setPrintFooter(false); // Add a page
$pdf->AddPage(); // Set font
$pdf->SetFont('helvetica', '', 12); // Ticket content
$html = '' . APP_NAME . '';
$html .= 'Movie Ticket';
$html .= 'Booking ID: ' . $bookingData['booking_id'] . '';
$html .= 'Movie: ' . $bookingData['movie_title'] . '';
$html .= 'Theater: ' . $bookingData['theater_name'] . '';
$html .= 'Screen: ' . $bookingData['screen_name'] . '';
$html .= 'Date: ' . formatDate($bookingData['show_date']) . '';
$html .= 'Time: ' . formatTime($bookingData['show_time']) . '';
$html .= 'Seats: ' . implode(', ', $seatsData) . '';
$html .= 'Total Amount: ' . formatAmount($bookingData['final_amount']) . ''; // Add QR code
$qrFile = TICKET_DIR . 'qr_' . $bookingData['booking_id'] . '.png';
if (file_exists($qrFile)) {
$html .= '';
} $pdf->writeHTML($html, true, false, true, false, ''); $filename = 'ticket_' . $bookingData['booking_id'] . '.pdf';
$path = TICKET_DIR . $filename; $pdf->Output($path, 'F'); return $filename;
}
/**
- Send email notification
*/
function sendEmail($to, $subject, $template, $data = []) {
// Load email template
$templateFile = DIR . "/notifications/email_templates/{$template}.php"; if (!file_exists($templateFile)) {
logError("Email template not found: {$template}");
return false;
} // Extract data for template
extract($data);
ob_start();
include $templateFile;
$message = ob_get_clean(); // Headers
$headers = 'MIME-Version: 1.0', 'Content-type: text/html; charset=utf-8', 'From: ' . APP_NAME . ' <' . (BUSINESS_EMAIL ?? '[email protected]') . '>', 'Reply-To: ' . (BUSINESS_EMAIL ?? '[email protected]'), 'X-Mailer: PHP/' . phpversion(); return mail($to, $subject, $message, implode("\r\n", $headers));
}
/**
- Send SMS notification
*/
function sendSMS($to, $message) {
if (!ENABLE_SMS) {
return false;
} // Implement SMS sending logic (Twilio, etc.)
// This is a placeholder
return true;
}
/**
- Log error
*/
function logError($message, $context = []) {
$logFile = DIR . '/../logs/error.log';
$logDir = dirname($logFile); if (!is_dir($logDir)) {
mkdir($logDir, 0755, true);
} $timestamp = date('Y-m-d H:i:s');
$contextStr = !empty($context) ? ' ' . json_encode($context) : '';
$logMessage = "[{$timestamp}] {$message}{$contextStr}\n"; file_put_contents($logFile, $logMessage, FILE_APPEND);
}
/**
- Get user IP address
*/
function getUserIP() {
if (!empty($_SERVER['HTTP_CLIENT_IP'])) {
return $_SERVER['HTTP_CLIENT_IP'];
} elseif (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
return $_SERVER['HTTP_X_FORWARDED_FOR'];
} else {
return $_SERVER['REMOTE_ADDR'];
}
}
/**
- Generate random string
*/
function generateRandomString($length = 32) {
return bin2hex(random_bytes($length / 2));
}
/**
- Validate email
*/
function validateEmail($email) {
return filter_var($email, FILTER_VALIDATE_EMAIL);
}
/**
- Validate phone number
*/
function validatePhone($phone) {
return preg_match('/^[0-9-()\/+\s]+$/', $phone);
}
/**
- Generate pagination
*/
function paginate($currentPage, $totalPages, $url) {
if ($totalPages <= 1) {
return '';
} $html = '- Previous
- Previous
- ' . $i . '
- ' . $i . '
- Next
- Next
}
/**
- Get time slots for display
*/
function getTimeSlotsForDisplay($showtimes) {
$html = '';
foreach ($showtimes as $showtime) {
$html .= '
' . formatTime($showtime['start_time']) . ' - ' . formatTime($showtime['end_time']) . '
<span class="badge bg-info ms-1">' . strtoupper($showtime['show_type']) . '</span>
<span class="badge bg-success ms-1">' . formatAmount($showtime['base_price']) . '</span>
<span class="badge bg-secondary ms-1">' . $showtime['available_seats'] . ' seats</span>
</button>';
}
return $html;
}
/**
* Upload file
*/
function uploadFile($file, $targetDir, $allowedTypes = null) {
if ($allowedTypes === null) {
$allowedTypes = ALLOWED_EXTENSIONS;
}
// Check for errors
if ($file['error'] !== UPLOAD_ERR_OK) {
return ['success' => false, 'error' => 'Upload failed with error code: ' . $file['error']];
}
// Check file size
if ($file['size'] > MAX_FILE_SIZE) {
return ['success' => false, 'error' => 'File size exceeds limit'];
}
// Check file type
$extension = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
if (!in_array($extension, $allowedTypes)) {
return ['success' => false, 'error' => 'File type not allowed'];
}
// Generate unique filename
$filename = uniqid() . '_' . time() . '.' . $extension;
$targetPath = $targetDir . '/' . $filename;
// Create directory if not exists
if (!is_dir($targetDir)) {
mkdir($targetDir, 0755, true);
}
// Upload file
if (move_uploaded_file($file['tmp_name'], $targetPath)) {
return [
'success' => true,
'filename' => $filename,
'original_name' => $file['name'],
'path' => $targetPath
];
}
return ['success' => false, 'error' => 'Failed to move uploaded file'];
}
/**
* Delete file
*/
function deleteFile($path) {
if (file_exists($path)) {
return unlink($path);
}
return false;
}
?>
Authentication Class
File: includes/auth.php
<?php
/**
* Authentication Class
* Handles user authentication, registration, and session management
*/
class Auth {
private $db;
/**
* Constructor
*/
public function __construct() {
$this->db = Database::getInstance();
}
/**
* Register new user
*/
public function register($data) {
try {
// Check if email already exists
$existing = $this->db->getRow(
"SELECT id FROM users WHERE email = ?",
[$data['email']]
);
if ($existing) {
return ['success' => false, 'error' => 'Email already registered'];
}
// Hash password
$hashedPassword = password_hash($data['password'], PASSWORD_BCRYPT, ['cost' => BCRYPT_ROUNDS]);
// Generate user ID
$userId = generateUserId($data['role'] ?? 'customer');
// Generate verification token
$verificationToken = generateRandomString();
// Prepare user data
$userData = [
'user_id' => $userId,
'email' => $data['email'],
'password' => $hashedPassword,
'first_name' => $data['first_name'],
'last_name' => $data['last_name'],
'phone' => $data['phone'] ?? null,
'role' => $data['role'] ?? 'customer',
'verification_token' => $verificationToken
];
// Insert user
$newUserId = $this->db->insert('users', $userData);
if ($newUserId) {
// Send verification email
$this->sendVerificationEmail($data['email'], $verificationToken);
return [
'success' => true,
'user_id' => $newUserId,
'message' => 'Registration successful. Please check your email to verify your account.'
];
}
return ['success' => false, 'error' => 'Registration failed'];
} catch (Exception $e) {
logError('Registration error: ' . $e->getMessage(), $data);
return ['success' => false, 'error' => 'Registration failed: ' . $e->getMessage()];
}
}
/**
* Login user
*/
public function login($email, $password, $remember = false) {
try {
// Get user
$user = $this->db->getRow(
"SELECT * FROM users WHERE email = ? AND status = 'active'",
[$email]
);
if (!$user) {
return ['success' => false, 'error' => 'Invalid email or password'];
}
// Check if email verified
if (!$user['email_verified']) {
return ['success' => false, 'error' => 'Please verify your email before logging in'];
}
// Verify password
if (!password_verify($password, $user['password'])) {
return ['success' => false, 'error' => 'Invalid email or password'];
}
// Check if password needs rehash
if (password_needs_rehash($user['password'], PASSWORD_BCRYPT, ['cost' => BCRYPT_ROUNDS])) {
$newHash = password_hash($password, PASSWORD_BCRYPT, ['cost' => BCRYPT_ROUNDS]);
$this->db->update('users', ['password' => $newHash], 'id = :id', ['id' => $user['id']]);
}
// Set session
$_SESSION['user_id'] = $user['id'];
$_SESSION['user_email'] = $user['email'];
$_SESSION['user_name'] = $user['first_name'] . ' ' . $user['last_name'];
$_SESSION['user_role'] = $user['role'];
$_SESSION['user_id_display'] = $user['user_id'];
$_SESSION['logged_in'] = true;
$_SESSION['login_time'] = time();
// Update last login
$this->db->update(
'users',
['last_login' => date('Y-m-d H:i:s')],
'id = :id',
['id' => $user['id']]
);
// Set remember me cookie
if ($remember) {
$this->setRememberMe($user['id']);
}
// If manager, get theater info
if ($user['role'] === 'manager') {
$_SESSION['theater_id'] = $user['theater_id'];
}
return ['success' => true, 'user' => $user];
} catch (Exception $e) {
logError('Login error: ' . $e->getMessage());
return ['success' => false, 'error' => 'Login failed'];
}
}
/**
* Set remember me cookie
*/
private function setRememberMe($userId) {
$token = generateRandomString(64);
$expires = time() + (86400 * 30); // 30 days
// Store token in database (you would need a remember_tokens table)
// For now, just set cookie
setcookie('remember_token', $token, $expires, '/', '', false, true);
}
/**
* Logout user
*/
public function logout() {
// Clear session
$_SESSION = array();
// Clear session cookie
if (ini_get('session.use_cookies')) {
$params = session_get_cookie_params();
setcookie(
session_name(),
'',
time() - 42000,
$params['path'],
$params['domain'],
$params['secure'],
$params['httponly']
);
}
// Clear remember me cookie
setcookie('remember_token', '', time() - 3600, '/');
// Destroy session
session_destroy();
}
/**
* Check if user is logged in
*/
public function isLoggedIn() {
return isset($_SESSION['logged_in']) && $_SESSION['logged_in'] === true;
}
/**
* Get current user
*/
public function getCurrentUser() {
if (!$this->isLoggedIn()) {
return null;
}
return $this->db->getRow(
"SELECT * FROM users WHERE id = ?",
[$_SESSION['user_id']]
);
}
/**
* Check if user has role
*/
public function hasRole($role) {
return isset($_SESSION['user_role']) && $_SESSION['user_role'] === $role;
}
/**
* Require login
*/
public function requireLogin() {
if (!$this->isLoggedIn()) {
$_SESSION['error'] = 'Please login to access this page';
redirect('/login.php');
}
}
/**
* Require role
*/
public function requireRole($role) {
$this->requireLogin();
if (!$this->hasRole($role)) {
$_SESSION['error'] = 'You do not have permission to access this page';
// Redirect based on role
if ($this->hasRole('admin')) {
redirect('/admin/dashboard.php');
} elseif ($this->hasRole('manager')) {
redirect('/manager/dashboard.php');
} else {
redirect('/customer/index.php');
}
}
}
/**
* Verify email
*/
public function verifyEmail($token) {
$user = $this->db->getRow(
"SELECT id FROM users WHERE verification_token = ?",
[$token]
);
if ($user) {
$this->db->update(
'users',
['email_verified' => true, 'verification_token' => null],
'id = :id',
['id' => $user['id']]
);
return true;
}
return false;
}
/**
* Send verification email
*/
private function sendVerificationEmail($email, $token) {
$subject = "Verify your email - " . APP_NAME;
$data = [
'verification_link' => APP_URL . "/verify.php?token=" . $token
];
return sendEmail($email, $subject, 'verification', $data);
}
/**
* Forgot password
*/
public function forgotPassword($email) {
$user = $this->db->getRow(
"SELECT id FROM users WHERE email = ?",
[$email]
);
if ($user) {
$token = generateRandomString();
$expires = date('Y-m-d H:i:s', strtotime('+1 hour'));
$this->db->update(
'users',
['reset_token' => $token, 'reset_expires' => $expires],
'id = :id',
['id' => $user['id']]
);
// Send reset email
$subject = "Password Reset - " . APP_NAME;
$data = [
'reset_link' => APP_URL . "/reset_password.php?token=" . $token
];
return sendEmail($email, $subject, 'password_reset', $data);
}
return false;
}
/**
* Reset password
*/
public function resetPassword($token, $password) {
$user = $this->db->getRow(
"SELECT id FROM users WHERE reset_token = ? AND reset_expires > NOW()",
[$token]
);
if ($user) {
$hashedPassword = password_hash($password, PASSWORD_BCRYPT, ['cost' => BCRYPT_ROUNDS]);
$this->db->update(
'users',
['password' => $hashedPassword, 'reset_token' => null, 'reset_expires' => null],
'id = :id',
['id' => $user['id']]
);
return true;
}
return false;
}
/**
* Update user profile
*/
public function updateProfile($userId, $data) {
try {
$updateData = [
'first_name' => $data['first_name'],
'last_name' => $data['last_name'],
'phone' => $data['phone'] ?? null,
'address' => $data['address'] ?? null,
'city' => $data['city'] ?? null,
'state' => $data['state'] ?? null,
'country' => $data['country'] ?? null,
'postal_code' => $data['postal_code'] ?? null
];
// Handle profile picture upload
if (isset($_FILES['profile_picture']) && $_FILES['profile_picture']['error'] == 0) {
$uploadDir = UPLOAD_DIR . 'avatars/';
$result = uploadFile($_FILES['profile_picture'], $uploadDir);
if ($result['success']) {
$updateData['profile_picture'] = $result['filename'];
}
}
$this->db->update('users', $updateData, 'id = :id', ['id' => $userId]);
return ['success' => true, 'message' => 'Profile updated successfully'];
} catch (Exception $e) {
logError('Profile update error: ' . $e->getMessage());
return ['success' => false, 'error' => 'Failed to update profile'];
}
}
/**
* Change password
*/
public function changePassword($userId, $currentPassword, $newPassword) {
$user = $this->db->getRow("SELECT password FROM users WHERE id = ?", [$userId]);
if (!password_verify($currentPassword, $user['password'])) {
return ['success' => false, 'error' => 'Current password is incorrect'];
}
$hashedPassword = password_hash($newPassword, PASSWORD_BCRYPT, ['cost' => BCRYPT_ROUNDS]);
$this->db->update('users', ['password' => $hashedPassword], 'id = :id', ['id' => $userId]);
return ['success' => true, 'message' => 'Password changed successfully'];
}
/**
* Add loyalty points
*/
public function addLoyaltyPoints($userId, $points, $bookingId = null, $description = '') {
$this->db->insert('loyalty_transactions', [
'user_id' => $userId,
'booking_id' => $bookingId,
'points' => $points,
'type' => 'earned',
'description' => $description
]);
$this->db->update(
'users',
['loyalty_points' => $this->db->getValue("SELECT loyalty_points + ? FROM users WHERE id = ?", [$points, $userId])],
'id = :id',
['id' => $userId]
);
}
/**
* Redeem loyalty points
*/
public function redeemLoyaltyPoints($userId, $points, $description = '') {
$currentPoints = $this->db->getValue("SELECT loyalty_points FROM users WHERE id = ?", [$userId]);
if ($currentPoints < $points) {
return ['success' => false, 'error' => 'Insufficient loyalty points'];
}
$this->db->insert('loyalty_transactions', [
'user_id' => $userId,
'points' => -$points,
'type' => 'redeemed',
'description' => $description
]);
$this->db->update(
'users',
['loyalty_points' => $this->db->getValue("SELECT loyalty_points - ? FROM users WHERE id = ?", [$points, $userId])],
'id = :id',
['id' => $userId]
);
return ['success' => true, 'message' => 'Points redeemed successfully'];
}
}
// Initialize Auth
$auth = new Auth();
?>
Movie Class
File: includes/Movie.php
<?php
/**
* Movie Class
* Handles all movie-related operations
*/
class Movie {
private $db;
/**
* Constructor
*/
public function __construct() {
$this->db = Database::getInstance();
}
/**
* Get movie by ID
*/
public function getMovie($id) {
return $this->db->getRow(
"SELECT * FROM movies WHERE id = ?",
[$id]
);
}
/**
* Get all movies with filters
*/
public function getMovies($filters = []) {
$sql = "SELECT * FROM movies WHERE 1=1";
$params = [];
if (!empty($filters['status'])) {
$sql .= " AND status = :status";
$params['status'] = $filters['status'];
}
if (!empty($filters['genre'])) {
$sql .= " AND genre LIKE :genre";
$params['genre'] = '%' . $filters['genre'] . '%';
}
if (!empty($filters['language'])) {
$sql .= " AND language = :language";
$params['language'] = $filters['language'];
}
if (!empty($filters['featured'])) {
$sql .= " AND featured = 1";
}
if (!empty($filters['search'])) {
$sql .= " AND (title LIKE :search OR description LIKE :search OR cast LIKE :search OR director LIKE :search)";
$params['search'] = '%' . $filters['search'] . '%';
}
if (!empty($filters['release_date'])) {
$sql .= " AND release_date <= :release_date AND (end_date IS NULL OR end_date >= :release_date)";
$params['release_date'] = $filters['release_date'];
}
$sql .= " ORDER BY featured DESC, release_date DESC";
// Pagination
$page = $filters['page'] ?? 1;
$limit = $filters['limit'] ?? ITEMS_PER_PAGE;
$offset = ($page - 1) * $limit;
$sql .= " LIMIT :limit OFFSET :offset";
$params['limit'] = $limit;
$params['offset'] = $offset;
return $this->db->getRows($sql, $params);
}
/**
* Get now showing movies
*/
public function getNowShowing($limit = null) {
$sql = "SELECT DISTINCT m.* FROM movies m
JOIN showtimes s ON m.id = s.movie_id
WHERE m.status = 'now_showing'
AND s.show_date >= CURDATE()
AND s.status = 'active'
ORDER BY m.release_date DESC";
if ($limit) {
$sql .= " LIMIT " . intval($limit);
}
return $this->db->getRows($sql);
}
/**
* Get coming soon movies
*/
public function getComingSoon($limit = null) {
$sql = "SELECT * FROM movies
WHERE status = 'coming_soon'
ORDER BY release_date ASC";
if ($limit) {
$sql .= " LIMIT " . intval($limit);
}
return $this->db->getRows($sql);
}
/**
* Get movies by theater
*/
public function getMoviesByTheater($theaterId, $date = null) {
if (!$date) {
$date = date('Y-m-d');
}
return $this->db->getRows(
"SELECT DISTINCT m.* FROM movies m
JOIN showtimes s ON m.id = s.movie_id
WHERE s.theater_id = :theater_id
AND s.show_date = :date
AND s.status = 'active'
ORDER BY m.title ASC",
[
'theater_id' => $theaterId,
'date' => $date
]
);
}
/**
* Add new movie
*/
public function addMovie($data) {
try {
$movieData = [
'title' => $data['title'],
'original_title' => $data['original_title'] ?? null,
'description' => $data['description'] ?? null,
'duration' => $data['duration'],
'release_date' => $data['release_date'],
'end_date' => $data['end_date'] ?? null,
'language' => $data['language'] ?? 'English',
'subtitles' => $data['subtitles'] ?? null,
'genre' => $data['genre'] ?? null,
'director' => $data['director'] ?? null,
'cast' => $data['cast'] ?? null,
'producer' => $data['producer'] ?? null,
'writer' => $data['writer'] ?? null,
'music_director' => $data['music_director'] ?? null,
'cinematographer' => $data['cinematographer'] ?? null,
'rating' => $data['rating'] ?? 'PG-13',
'status' => $data['status'] ?? 'coming_soon',
'featured' => $data['featured'] ?? false
];
// Handle poster upload
if (isset($_FILES['poster']) && $_FILES['poster']['error'] == 0) {
$uploadDir = UPLOAD_DIR . 'movies/';
$result = uploadFile($_FILES['poster'], $uploadDir);
if ($result['success']) {
$movieData['poster'] = $result['filename'];
}
}
// Handle backdrop upload
if (isset($_FILES['backdrop']) && $_FILES['backdrop']['error'] == 0) {
$uploadDir = UPLOAD_DIR . 'movies/';
$result = uploadFile($_FILES['backdrop'], $uploadDir);
if ($result['success']) {
$movieData['backdrop'] = $result['filename'];
}
}
$id = $this->db->insert('movies', $movieData);
if ($id) {
return ['success' => true, 'id' => $id, 'message' => 'Movie added successfully'];
}
return ['success' => false, 'error' => 'Failed to add movie'];
} catch (Exception $e) {
logError('Add movie error: ' . $e->getMessage(), $data);
return ['success' => false, 'error' => 'Failed to add movie: ' . $e->getMessage()];
}
}
/**
* Update movie
*/
public function updateMovie($id, $data) {
try {
$updateData = [
'title' => $data['title'],
'original_title' => $data['original_title'] ?? null,
'description' => $data['description'] ?? null,
'duration' => $data['duration'],
'release_date' => $data['release_date'],
'end_date' => $data['end_date'] ?? null,
'language' => $data['language'] ?? 'English',
'subtitles' => $data['subtitles'] ?? null,
'genre' => $data['genre'] ?? null,
'director' => $data['director'] ?? null,
'cast' => $data['cast'] ?? null,
'producer' => $data['producer'] ?? null,
'writer' => $data['writer'] ?? null,
'music_director' => $data['music_director'] ?? null,
'cinematographer' => $data['cinematographer'] ?? null,
'rating' => $data['rating'] ?? 'PG-13',
'status' => $data['status'] ?? 'coming_soon',
'featured' => $data['featured'] ?? false
];
// Handle poster upload
if (isset($_FILES['poster']) && $_FILES['poster']['error'] == 0) {
$uploadDir = UPLOAD_DIR . 'movies/';
$result = uploadFile($_FILES['poster'], $uploadDir);
if ($result['success']) {
// Delete old poster
$oldMovie = $this->getMovie($id);
if ($oldMovie && $oldMovie['poster']) {
deleteFile(UPLOAD_DIR . 'movies/' . $oldMovie['poster']);
}
$updateData['poster'] = $result['filename'];
}
}
// Handle backdrop upload
if (isset($_FILES['backdrop']) && $_FILES['backdrop']['error'] == 0) {
$uploadDir = UPLOAD_DIR . 'movies/';
$result = uploadFile($_FILES['backdrop'], $uploadDir);
if ($result['success']) {
// Delete old backdrop
$oldMovie = $this->getMovie($id);
if ($oldMovie && $oldMovie['backdrop']) {
deleteFile(UPLOAD_DIR . 'movies/' . $oldMovie['backdrop']);
}
$updateData['backdrop'] = $result['filename'];
}
}
$updated = $this->db->update('movies', $updateData, 'id = :id', ['id' => $id]);
if ($updated !== false) {
return ['success' => true, 'message' => 'Movie updated successfully'];
}
return ['success' => false, 'error' => 'Failed to update movie'];
} catch (Exception $e) {
logError('Update movie error: ' . $e->getMessage(), $data);
return ['success' => false, 'error' => 'Failed to update movie: ' . $e->getMessage()];
}
}
/**
* Delete movie
*/
public function deleteMovie($id) {
try {
// Check if movie has showtimes
$showtimes = $this->db->getValue(
"SELECT COUNT(*) FROM showtimes WHERE movie_id = ?",
[$id]
);
if ($showtimes > 0) {
return ['success' => false, 'error' => 'Cannot delete movie with existing showtimes'];
}
// Get movie to delete poster/backdrop
$movie = $this->getMovie($id);
if ($movie['poster']) {
deleteFile(UPLOAD_DIR . 'movies/' . $movie['poster']);
}
if ($movie['backdrop']) {
deleteFile(UPLOAD_DIR . 'movies/' . $movie['backdrop']);
}
$deleted = $this->db->delete('movies', 'id = ?', [$id]);
if ($deleted) {
return ['success' => true, 'message' => 'Movie deleted successfully'];
}
return ['success' => false, 'error' => 'Failed to delete movie'];
} catch (Exception $e) {
logError('Delete movie error: ' . $e->getMessage());
return ['success' => false, 'error' => 'Failed to delete movie: ' . $e->getMessage()];
}
}
/**
* Update movie rating
*/
public function updateRating($movieId) {
$stats = $this->db->getRow(
"SELECT AVG(rating) as avg_rating, COUNT(*) as total
FROM reviews
WHERE movie_id = ? AND status = 'approved'",
[$movieId]
);
$this->db->update(
'movies',
[
'user_rating' => round($stats['avg_rating'] ?? 0, 1),
'total_ratings' => $stats['total'] ?? 0
],
'id = :id',
['id' => $movieId]
);
}
/**
* Get movie showtimes
*/
public function getShowtimes($movieId, $date = null, $theaterId = null) {
if (!$date) {
$date = date('Y-m-d');
}
$sql = "SELECT s.*, t.name as theater_name, t.city, sc.screen_number, sc.screen_type
FROM showtimes s
JOIN theaters t ON s.theater_id = t.id
JOIN screens sc ON s.screen_id = sc.id
WHERE s.movie_id = :movie_id
AND s.show_date = :date
AND s.status = 'active'
AND t.status = 'active'";
$params = [
'movie_id' => $movieId,
'date' => $date
];
if ($theaterId) {
$sql .= " AND s.theater_id = :theater_id";
$params['theater_id'] = $theaterId;
}
$sql .= " ORDER BY s.start_time ASC";
return $this->db->getRows($sql, $params);
}
/**
* Get movie reviews
*/
public function getReviews($movieId, $status = 'approved') {
return $this->db->getRows(
"SELECT r.*, u.first_name, u.last_name, u.profile_picture
FROM reviews r
JOIN users u ON r.user_id = u.id
WHERE r.movie_id = ? AND r.status = ?
ORDER BY r.created_at DESC",
[$movieId, $status]
);
}
/**
* Search movies
*/
public function search($query) {
return $this->db->getRows(
"SELECT * FROM movies
WHERE MATCH(title, description, cast, director) AGAINST(? IN NATURAL LANGUAGE MODE)
AND status IN ('now_showing', 'coming_soon')
ORDER BY featured DESC, release_date DESC
LIMIT 20",
[$query]
);
}
/**
* Get movies count
*/
public function getCount($filters = []) {
$sql = "SELECT COUNT(*) FROM movies WHERE 1=1";
$params = [];
if (!empty($filters['status'])) {
$sql .= " AND status = :status";
$params['status'] = $filters['status'];
}
return $this->db->getValue($sql, $params);
}
/**
* Get upcoming releases
*/
public function getUpcomingReleases($days = 30) {
$startDate = date('Y-m-d');
$endDate = date('Y-m-d', strtotime("+{$days} days"));
return $this->db->getRows(
"SELECT * FROM movies
WHERE status = 'coming_soon'
AND release_date BETWEEN ? AND ?
ORDER BY release_date ASC",
[$startDate, $endDate]
);
}
/**
* Get recommended movies based on user's history
*/
public function getRecommendations($userId, $limit = 10) {
// Get user's watched genres
$genres = $this->db->getRows(
"SELECT DISTINCT m.genre FROM bookings b
JOIN movies m ON b.movie_id = m.id
WHERE b.user_id = ? AND b.booking_status = 'completed'
ORDER BY b.created_at DESC
LIMIT 5",
[$userId]
);
if (empty($genres)) {
// If no history, return popular movies
return $this->getPopularMovies($limit);
}
$genreConditions = [];
$params = [];
foreach ($genres as $index => $g) {
if (!empty($g['genre'])) {
$genreConditions[] = "genre LIKE :genre{$index}";
$params["genre{$index}"] = '%' . $g['genre'] . '%';
}
}
if (empty($genreConditions)) {
return $this->getPopularMovies($limit);
}
$sql = "SELECT * FROM movies
WHERE status = 'now_showing'
AND (" . implode(' OR ', $genreConditions) . ")
ORDER BY user_rating DESC, release_date DESC
LIMIT " . intval($limit);
return $this->db->getRows($sql, $params);
}
/**
* Get popular movies
*/
public function getPopularMovies($limit = 10) {
return $this->db->getRows(
"SELECT m.*, COUNT(b.id) as booking_count
FROM movies m
LEFT JOIN bookings b ON m.id = b.movie_id
WHERE m.status = 'now_showing'
GROUP BY m.id
ORDER BY booking_count DESC, m.user_rating DESC
LIMIT ?",
[$limit]
);
}
}
?>
Booking Class
File: includes/Booking.php
<?php
/**
* Booking Class
* Handles all booking-related operations
*/
class Booking {
private $db;
/**
* Constructor
*/
public function __construct() {
$this->db = Database::getInstance();
}
/**
* Create new booking
*/
public function createBooking($userId, $data) {
try {
$this->db->beginTransaction();
// Get showtime details
$showtime = $this->db->getRow(
"SELECT s.*, m.title as movie_title, m.duration,
t.name as theater_name, sc.screen_number
FROM showtimes s
JOIN movies m ON s.movie_id = m.id
JOIN theaters t ON s.theater_id = t.id
JOIN screens sc ON s.screen_id = sc.id
WHERE s.id = ? AND s.status = 'active'",
[$data['showtime_id']]
);
if (!$showtime) {
throw new Exception('Invalid showtime');
}
// Check if showtime is in the past
if (isPastShowtime($showtime['show_date'], $showtime['start_time'])) {
throw new Exception('Cannot book past showtime');
}
// Get seat details
$seatIds = explode(',', $data['seat_ids']);
$seats = $this->db->getRows(
"SELECT s.*, st.name as seat_type_name, st.price_multiplier
FROM seats s
JOIN seat_types st ON s.seat_type_id = st.id
WHERE s.id IN (" . implode(',', array_fill(0, count($seatIds), '?')) . ")
AND s.screen_id = ?",
array_merge($seatIds, [$showtime['screen_id']])
);
if (count($seats) != count($seatIds)) {
throw new Exception('Invalid seats selected');
}
// Check if seats are available
foreach ($seatIds as $seatId) {
if (!$this->isSeatAvailable($data['showtime_id'], $seatId)) {
throw new Exception('Some seats are no longer available');
}
}
// Calculate prices
$basePrice = $showtime['base_price'];
$totalAmount = 0;
$seatPrices = [];
foreach ($seats as $seat) {
$seatPrice = $basePrice * $seat['price_multiplier'];
$totalAmount += $seatPrice;
$seatPrices[$seat['id']] = $seatPrice;
}
// Apply promotion if any
$discountAmount = 0;
if (!empty($data['promo_code'])) {
$promoResult = $this->applyPromoCode($data['promo_code'], $totalAmount, $showtime);
if ($promoResult['success']) {
$discountAmount = $promoResult['discount'];
$promoId = $promoResult['promo_id'];
}
}
// Calculate tax and fees
$priceDetails = calculateFinalPrice($totalAmount - $discountAmount, count($seatIds));
// Generate booking ID
$bookingId = generateBookingId();
// Insert booking
$bookingData = [
'booking_id' => $bookingId,
'user_id' => $userId,
'showtime_id' => $data['showtime_id'],
'theater_id' => $showtime['theater_id'],
'movie_id' => $showtime['movie_id'],
'show_date' => $showtime['show_date'],
'show_time' => $showtime['start_time'],
'total_amount' => $totalAmount,
'discount_amount' => $discountAmount,
'tax_amount' => $priceDetails['tax'],
'final_amount' => $priceDetails['total'],
'number_of_seats' => count($seatIds),
'seat_ids' => implode(',', $seatIds),
'seat_labels' => implode(',', array_column($seats, 'row_label', 'seat_number')),
'payment_status' => 'pending',
'booking_status' => 'pending'
];
$bookingDbId = $this->db->insert('bookings', $bookingData);
// Insert booking seats
$bookingSeatsData = [];
foreach ($seatIds as $seatId) {
$bookingSeatsData[] = [
'booking_id' => $bookingDbId,
'seat_id' => $seatId,
'seat_type_id' => $this->db->getValue("SELECT seat_type_id FROM seats WHERE id = ?", [$seatId]),
'price' => $seatPrices[$seatId]
];
}
$this->db->insertMultiple('booking_seats', $bookingSeatsData);
// Remove seat holds
$this->db->delete('seat_holds', 'showtime_id = ? AND seat_id IN (' . implode(',', $seatIds) . ')',
array_merge([$data['showtime_id']], $seatIds));
// Update available seats in showtime
$this->db->update(
'showtimes',
['available_seats' => $showtime['available_seats'] - count($seatIds)],
'id = :id',
['id' => $data['showtime_id']]
);
// If sold out, update status
if ($showtime['available_seats'] - count($seatIds) == 0) {
$this->db->update(
'showtimes',
['status' => 'sold_out'],
'id = :id',
['id' => $data['showtime_id']]
);
}
$this->db->commit();
return [
'success' => true,
'booking_id' => $bookingId,
'booking_db_id' => $bookingDbId,
'message' => 'Booking created successfully'
];
} catch (Exception $e) {
$this->db->rollback();
logError('Booking error: ' . $e->getMessage(), $data);
return ['success' => false, 'error' => $e->getMessage()];
}
}
/**
* Check if seat is available for showtime
*/
public function isSeatAvailable($showtimeId, $seatId) {
// Check if seat is already booked
$booked = $this->db->getValue(
"SELECT COUNT(*) FROM booking_seats bs
JOIN bookings b ON bs.booking_id = b.id
WHERE b.showtime_id = ? AND bs.seat_id = ? AND b.booking_status NOT IN ('cancelled')",
[$showtimeId, $seatId]
);
if ($booked > 0) {
return false;
}
// Check if seat is held
$held = $this->db->getValue(
"SELECT COUNT(*) FROM seat_holds
WHERE showtime_id = ? AND seat_id = ? AND hold_until > NOW()",
[$showtimeId, $seatId]
);
return $held == 0;
}
/**
* Hold seats for booking
*/
public function holdSeats($showtimeId, $userId, $seatIds, $sessionId) {
try {
$holdUntil = date('Y-m-d H:i:s', strtotime('+' . SEAT_HOLD_MINUTES . ' minutes'));
// Check if seats are available
foreach ($seatIds as $seatId) {
if (!$this->isSeatAvailable($showtimeId, $seatId)) {
return ['success' => false, 'error' => 'Some seats are no longer available'];
}
}
// Remove old holds for this session
$this->db->delete('seat_holds', 'session_id = ? AND showtime_id = ?', [$sessionId, $showtimeId]);
// Insert new holds
$holdData = [];
foreach ($seatIds as $seatId) {
$holdData[] = [
'showtime_id' => $showtimeId,
'seat_id' => $seatId,
'user_id' => $userId,
'session_id' => $sessionId,
'hold_until' => $holdUntil
];
}
$this->db->insertMultiple('seat_holds', $holdData);
return ['success' => true, 'hold_until' => $holdUntil];
} catch (Exception $e) {
logError('Hold seats error: ' . $e->getMessage());
return ['success' => false, 'error' => 'Failed to hold seats'];
}
}
/**
* Release held seats
*/
public function releaseSeats($showtimeId, $sessionId) {
return $this->db->delete('seat_holds', 'showtime_id = ? AND session_id = ?', [$showtimeId, $sessionId]);
}
/**
* Get booking by ID
*/
public function getBooking($bookingId) {
return $this->db->getRow(
"SELECT b.*, u.first_name, u.last_name, u.email, u.phone,
m.title as movie_title, m.poster,
t.name as theater_name, t.address as theater_address,
sc.screen_number,
s.start_time, s.end_time, s.show_date
FROM bookings b
JOIN users u ON b.user_id = u.id
JOIN movies m ON b.movie_id = m.id
JOIN theaters t ON b.theater_id = t.id
JOIN showtimes s ON b.showtime_id = s.id
JOIN screens sc ON s.screen_id = sc.id
WHERE b.booking_id = ? OR b.id = ?",
[$bookingId, $bookingId]
);
}
/**
* Get user bookings
*/
public function getUserBookings($userId, $status = null, $limit = null) {
$sql = "SELECT b.*, m.title as movie_title, m.poster,
t.name as theater_name,
s.start_time, s.end_time, s.show_date
FROM bookings b
JOIN movies m ON b.movie_id = m.id
JOIN theaters t ON b.theater_id = t.id
JOIN showtimes s ON b.showtime_id = s.id
WHERE b.user_id = :user_id";
$params = ['user_id' => $userId];
if ($status) {
$sql .= " AND b.booking_status = :status";
$params['status'] = $status;
}
$sql .= " ORDER BY s.show_date DESC, s.start_time DESC";
if ($limit) {
$sql .= " LIMIT :limit";
$params['limit'] = $limit;
}
return $this->db->getRows($sql, $params);
}
/**
* Get upcoming bookings for user
*/
public function getUpcomingBookings($userId) {
$today = date('Y-m-d');
$now = date('H:i:s');
return $this->db->getRows(
"SELECT b.*, m.title as movie_title, m.poster,
t.name as theater_name,
s.start_time, s.end_time, s.show_date
FROM bookings b
JOIN movies m ON b.movie_id = m.id
JOIN theaters t ON b.theater_id = t.id
JOIN showtimes s ON b.showtime_id = s.id
WHERE b.user_id = :user_id
AND b.booking_status = 'confirmed'
AND (s.show_date > :today OR (s.show_date = :today AND s.start_time > :now))
ORDER BY s.show_date ASC, s.start_time ASC",
[
'user_id' => $userId,
'today' => $today,
'now' => $now
]
);
}
/**
* Get booking history for user
*/
public function getBookingHistory($userId) {
$today = date('Y-m-d');
$now = date('H:i:s');
return $this->db->getRows(
"SELECT b.*, m.title as movie_title, m.poster,
t.name as theater_name,
s.start_time, s.end_time, s.show_date
FROM bookings b
JOIN movies m ON b.movie_id = m.id
JOIN theaters t ON b.theater_id = t.id
JOIN showtimes s ON b.showtime_id = s.id
WHERE b.user_id = :user_id
AND b.booking_status IN ('completed', 'cancelled', 'no_show')
ORDER BY s.show_date DESC, s.start_time DESC",
['user_id' => $userId]
);
}
/**
* Confirm booking after payment
*/
public function confirmBooking($bookingId, $paymentId = null) {
$updateData = [
'booking_status' => 'confirmed',
'payment_status' => 'completed'
];
if ($paymentId) {
$updateData['payment_id'] = $paymentId;
}
$updated = $this->db->update(
'bookings',
$updateData,
'id = :id',
['id' => $bookingId]
);
if ($updated) {
// Get booking details for ticket generation
$booking = $this->getBooking($bookingId);
// Generate QR code
$qrData = "Booking ID: {$booking['booking_id']}\n";
$qrData .= "Movie: {$booking['movie_title']}\n";
$qrData .= "Date: {$booking['show_date']}\n";
$qrData .= "Time: {$booking['show_time']}\n";
$qrData .= "Seats: {$booking['seat_labels']}";
$qrFile = generateTicketQRCode($booking['booking_id'], $qrData);
// Generate PDF ticket
$seatList = explode(',', $booking['seat_labels']);
$pdfFile = generateTicketPDF($booking, $seatList);
// Update booking with ticket files
$this->db->update(
'bookings',
[
'qr_code' => $qrFile,
'ticket_pdf' => $pdfFile
],
'id = :id',
['id' => $bookingId]
);
// Send confirmation email
$this->sendConfirmationEmail($bookingId);
// Add loyalty points
$auth = new Auth();
$auth->addLoyaltyPoints(
$booking['user_id'],
LOYALTY_POINTS_PER_BOOKING,
$bookingId,
'Points earned from booking ' . $booking['booking_id']
);
return true;
}
return false;
}
/**
* Cancel booking
*/
public function cancelBooking($bookingId, $userId, $role, $reason = null) {
try {
$booking = $this->getBooking($bookingId);
if (!$booking) {
return ['success' => false, 'error' => 'Booking not found'];
}
// Check permissions
if ($role === 'customer' && $booking['user_id'] != $userId) {
return ['success' => false, 'error' => 'Unauthorized to cancel this booking'];
}
// Check if cancellation is allowed (at least X hours before show)
if ($role === 'customer') {
$showDateTime = strtotime($booking['show_date'] . ' ' . $booking['show_time']);
$hoursUntilShow = ($showDateTime - time()) / 3600;
if ($hoursUntilShow < CANCELLATION_HOURS) {
return ['success' => false, 'error' => 'Bookings can only be cancelled ' . CANCELLATION_HOURS . ' hours before showtime'];
}
}
$this->db->beginTransaction();
// Update booking status
$this->db->update(
'bookings',
[
'booking_status' => 'cancelled',
'cancellation_reason' => $reason,
'cancelled_at' => date('Y-m-d H:i:s')
],
'id = :id',
['id' => $booking['id']]
);
// Get seat IDs
$seatIds = explode(',', $booking['seat_ids']);
// Update showtime available seats
$this->db->update(
'showtimes',
['available_seats' => $this->db->getValue("SELECT available_seats + ? FROM showtimes WHERE id = ?",
[count($seatIds), $booking['showtime_id']])],
'id = :id',
['id' => $booking['showtime_id']]
);
// If showtime was sold out, set back to active
$this->db->update(
'showtimes',
['status' => 'active'],
'id = :id AND available_seats > 0',
['id' => $booking['showtime_id']]
);
// Process refund if paid
if ($booking['payment_status'] === 'completed' && $booking['payment_id']) {
$payment = new Payment();
$payment->refundPayment($booking['payment_id'], $booking['final_amount']);
}
$this->db->commit();
// Send cancellation email
$this->sendCancellationEmail($booking['id']);
return ['success' => true, 'message' => 'Booking cancelled successfully'];
} catch (Exception $e) {
$this->db->rollback();
logError('Cancel booking error: ' . $e->getMessage());
return ['success' => false, 'error' => 'Failed to cancel booking'];
}
}
/**
* Check-in booking
*/
public function checkIn($bookingId) {
$booking = $this->getBooking($bookingId);
if (!$booking || $booking['booking_status'] !== 'confirmed') {
return ['success' => false, 'error' => 'Invalid booking'];
}
// Check if showtime is today
if ($booking['show_date'] != date('Y-m-d')) {
return ['success' => false, 'error' => 'Check-in only available on show date'];
}
$this->db->update(
'bookings',
[
'checked_in' => true,
'checked_in_at' => date('Y-m-d H:i:s'),
'booking_status' => 'completed'
],
'id = :id',
['id' => $booking['id']]
);
return ['success' => true, 'message' => 'Check-in successful'];
}
/**
* Apply promo code
*/
public function applyPromoCode($code, $amount, $showtime) {
$promo = $this->db->getRow(
"SELECT * FROM promotions
WHERE code = ? AND status = 'active'
AND start_date <= NOW() AND end_date >= NOW()",
[$code]
);
if (!$promo) {
return ['success' => false, 'error' => 'Invalid promo code'];
}
// Check usage limit
if ($promo['usage_limit'] && $promo['usage_count'] >= $promo['usage_limit']) {
return ['success' => false, 'error' => 'Promo code usage limit exceeded'];
}
// Check minimum amount
if ($promo['min_booking_amount'] && $amount < $promo['min_booking_amount']) {
return ['success' => false, 'error' => 'Minimum booking amount not met'];
}
// Check applicable movies
if ($promo['applicable_movies']) {
$movieIds = explode(',', $promo['applicable_movies']);
if (!in_array($showtime['movie_id'], $movieIds)) {
return ['success' => false, 'error' => 'Promo not applicable for this movie'];
}
}
// Check applicable theaters
if ($promo['applicable_theaters']) {
$theaterIds = explode(',', $promo['applicable_theaters']);
if (!in_array($showtime['theater_id'], $theaterIds)) {
return ['success' => false, 'error' => 'Promo not applicable for this theater'];
}
}
// Calculate discount
$discount = 0;
if ($promo['discount_type'] === 'percentage') {
$discount = $amount * ($promo['discount_value'] / 100);
if ($promo['max_discount'] && $discount > $promo['max_discount']) {
$discount = $promo['max_discount'];
}
} else {
$discount = $promo['discount_value'];
}
// Update usage count
$this->db->update(
'promotions',
['usage_count' => $promo['usage_count'] + 1],
'id = :id',
['id' => $promo['id']]
);
return [
'success' => true,
'discount' => $discount,
'promo_id' => $promo['id']
];
}
/**
* Get booked seats for showtime
*/
public function getBookedSeats($showtimeId) {
return $this->db->getRows(
"SELECT seat_id FROM booking_seats bs
JOIN bookings b ON bs.booking_id = b.id
WHERE b.showtime_id = ? AND b.booking_status NOT IN ('cancelled')",
[$showtimeId]
);
}
/**
* Get held seats for showtime
*/
public function getHeldSeats($showtimeId) {
return $this->db->getRows(
"SELECT seat_id FROM seat_holds
WHERE showtime_id = ? AND hold_until > NOW()",
[$showtimeId]
);
}
/**
* Clean expired holds
*/
public function cleanExpiredHolds() {
return $this->db->delete('seat_holds', 'hold_until <= NOW()');
}
/**
* Get booking statistics
*/
public function getStatistics($theaterId = null, $startDate = null, $endDate = null) {
if (!$startDate) {
$startDate = date('Y-m-d', strtotime('-30 days'));
}
if (!$endDate) {
$endDate = date('Y-m-d');
}
$sql = "SELECT
COUNT(*) as total_bookings,
SUM(CASE WHEN booking_status = 'confirmed' THEN 1 ELSE 0 END) as confirmed,
SUM(CASE WHEN booking_status = 'cancelled' THEN 1 ELSE 0 END) as cancelled,
SUM(CASE WHEN booking_status = 'completed' THEN 1 ELSE 0 END) as completed,
SUM(final_amount) as total_revenue,
SUM(number_of_seats) as total_tickets,
AVG(final_amount) as avg_booking_value
FROM bookings
WHERE DATE(created_at) BETWEEN :start_date AND :end_date";
$params = [
'start_date' => $startDate,
'end_date' => $endDate
];
if ($theaterId) {
$sql .= " AND theater_id = :theater_id";
$params['theater_id'] = $theaterId;
}
return $this->db->getRow($sql, $params);
}
/**
* Get daily revenue for chart
*/
public function getDailyRevenue($theaterId = null, $days = 30) {
$endDate = date('Y-m-d');
$startDate = date('Y-m-d', strtotime("-{$days} days"));
$sql = "SELECT
DATE(created_at) as date,
COUNT(*) as bookings,
SUM(final_amount) as revenue,
SUM(number_of_seats) as tickets
FROM bookings
WHERE DATE(created_at) BETWEEN :start_date AND :end_date
AND booking_status IN ('confirmed', 'completed')";
$params = [
'start_date' => $startDate,
'end_date' => $endDate
];
if ($theaterId) {
$sql .= " AND theater_id = :theater_id";
$params['theater_id'] = $theaterId;
}
$sql .= " GROUP BY DATE(created_at) ORDER BY date ASC";
return $this->db->getRows($sql, $params);
}
/**
* Send confirmation email
*/
private function sendConfirmationEmail($bookingId) {
$booking = $this->getBooking($bookingId);
$data = [
'booking' => $booking,
'customer_name' => $booking['first_name'] . ' ' . $booking['last_name'],
'movie_title' => $booking['movie_title'],
'theater' => $booking['theater_name'],
'screen' => $booking['screen_number'],
'date' => formatDate($booking['show_date']),
'time' => formatTime($booking['show_time']),
'seats' => $booking['seat_labels'],
'amount' => formatAmount($booking['final_amount']),
'booking_id' => $booking['booking_id'],
'ticket_link' => APP_URL . '/customer/ticket.php?id=' . $booking['booking_id']
];
return sendEmail(
$booking['email'],
'Booking Confirmation - ' . APP_NAME,
'booking_confirmation',
$data
);
}
/**
* Send cancellation email
*/
private function sendCancellationEmail($bookingId) {
$booking = $this->getBooking($bookingId);
$data = [
'booking' => $booking,
'customer_name' => $booking['first_name'] . ' ' . $booking['last_name'],
'movie_title' => $booking['movie_title'],
'date' => formatDate($booking['show_date']),
'time' => formatTime($booking['show_time']),
'booking_id' => $booking['booking_id']
];
return sendEmail(
$booking['email'],
'Booking Cancelled - ' . APP_NAME,
'booking_cancellation',
$data
);
}
/**
* Send reminders for upcoming bookings
*/
public function sendReminders() {
$reminderHours = REMINDER_HOURS;
$targetTime = date('Y-m-d H:i:s', strtotime("+{$reminderHours} hours"));
$targetDate = date('Y-m-d', strtotime($targetTime));
$targetHour = date('H', strtotime($targetTime));
$bookings = $this->db->getRows(
"SELECT b.*, u.email, u.first_name, u.last_name, u.phone,
m.title as movie_title, t.name as theater_name
FROM bookings b
JOIN users u ON b.user_id = u.id
JOIN movies m ON b.movie_id = m.id
JOIN theaters t ON b.theater_id = t.id
WHERE b.show_date = :date
AND HOUR(b.show_time) = :hour
AND b.booking_status = 'confirmed'",
[
'date' => $targetDate,
'hour' => $targetHour
]
);
foreach ($bookings as $booking) {
$data = [
'booking' => $booking,
'customer_name' => $booking['first_name'] . ' ' . $booking['last_name'],
'movie_title' => $booking['movie_title'],
'theater' => $booking['theater_name'],
'date' => formatDate($booking['show_date']),
'time' => formatTime($booking['show_time']),
'seats' => $booking['seat_labels']
];
sendEmail(
$booking['email'],
'Reminder: Your movie starts soon - ' . APP_NAME,
'reminder',
$data
);
if (ENABLE_SMS && $booking['phone']) {
$message = "Reminder: Your movie '{$booking['movie_title']}' starts at " .
formatTime($booking['show_time']) . " today. Enjoy the show!";
sendSMS($booking['phone'], $message);
}
}
return count($bookings);
}
}
?>
Seat Class
File: includes/Seat.php
<?php
/**
* Seat Class
* Handles all seat-related operations
*/
class Seat {
private $db;
/**
* Constructor
*/
public function __construct() {
$this->db = Database::getInstance();
}
/**
* Get seat layout for screen
*/
public function getSeatLayout($screenId) {
return $this->db->getRows(
"SELECT s.*, st.name as seat_type_name, st.color_code, st.price_multiplier
FROM seats s
JOIN seat_types st ON s.seat_type_id = st.id
WHERE s.screen_id = ?
ORDER BY s.seat_row, s.seat_column",
[$screenId]
);
}
/**
* Generate seats for a new screen
*/
public function generateSeats($screenId, $rows, $columns, $config = []) {
try {
$seats = [];
$rowLetters = range('A', 'Z');
for ($r = 0; $r < $rows; $r++) {
$rowLabel = $rowLetters[$r];
for ($c = 1; $c <= $columns; $c++) {
$seatNumber = $rowLabel . $c;
$seatType = $this->determineSeatType($r, $c, $rows, $columns, $config);
$seats[] = [
'screen_id' => $screenId,
'seat_type_id' => $seatType,
'row_label' => $rowLabel,
'seat_number' => $seatNumber,
'seat_column' => $c,
'seat_row' => $r + 1,
'x_coordinate' => $c * 10, // Example coordinates for visual layout
'y_coordinate' => $r * 10
];
}
}
return $this->db->insertMultiple('seats', $seats);
} catch (Exception $e) {
logError('Generate seats error: ' . $e->getMessage());
return false;
}
}
/**
* Determine seat type based on position
*/
private function determineSeatType($row, $col, $totalRows, $totalCols, $config) {
// VIP seats (last row, middle)
if ($row == $totalRows - 1 && $col >= floor($totalCols / 3) && $col <= ceil($totalCols * 2 / 3)) {
return 3; // VIP
}
// Premium seats (first few rows, middle)
if ($row < 3 && $col >= floor($totalCols / 4) && $col <= ceil($totalCols * 3 / 4)) {
return 2; // Premium
}
// Couple seats (specific positions)
if (isset($config['couple_seats'])) {
foreach ($config['couple_seats'] as $couple) {
if ($couple['row'] == $row && $couple['col'] == $col) {
return 4; // Couple
}
}
}
// Wheelchair accessible
if (isset($config['wheelchair_positions'])) {
foreach ($config['wheelchair_positions'] as $pos) {
if ($pos['row'] == $row && $pos['col'] == $col) {
return 5; // Wheelchair
}
}
}
// Standard seat
return 1; // Standard
}
/**
* Update seat status
*/
public function updateSeatStatus($seatId, $status) {
return $this->db->update(
'seats',
['status' => $status],
'id = :id',
['id' => $seatId]
);
}
/**
* Update multiple seats status
*/
public function updateMultipleSeatStatus($seatIds, $status) {
$placeholders = implode(',', array_fill(0, count($seatIds), '?'));
$params = array_merge([$status], $seatIds);
$sql = "UPDATE seats SET status = ? WHERE id IN ($placeholders)";
return $this->db->query($sql, $params)->rowCount();
}
/**
* Get seat types
*/
public function getSeatTypes() {
return $this->db->getRows("SELECT * FROM seat_types ORDER BY price_multiplier ASC");
}
/**
* Get seat by ID
*/
public function getSeat($seatId) {
return $this->db->getRow(
"SELECT s.*, st.name as seat_type_name, st.price_multiplier
FROM seats s
JOIN seat_types st ON s.seat_type_id = st.id
WHERE s.id = ?",
[$seatId]
);
}
/**
* Get seats by IDs
*/
public function getSeatsByIds($seatIds) {
$placeholders = implode(',', array_fill(0, count($seatIds), '?'));
return $this->db->getRows(
"SELECT s.*, st.name as seat_type_name, st.price_multiplier
FROM seats s
JOIN seat_types st ON s.seat_type_id = st.id
WHERE s.id IN ($placeholders)
ORDER BY s.seat_row, s.seat_column",
$seatIds
);
}
/**
* Get seat availability for showtime
*/
public function getSeatAvailability($showtimeId) {
$booking = new Booking();
// Get showtime details
$showtime = $this->db->getRow(
"SELECT s.*, sc.id as screen_id, sc.rows, sc.columns
FROM showtimes s
JOIN screens sc ON s.screen_id = sc.id
WHERE s.id = ?",
[$showtimeId]
);
if (!$showtime) {
return null;
}
// Get all seats for screen
$allSeats = $this->getSeatLayout($showtime['screen_id']);
// Get booked seats
$bookedSeats = $booking->getBookedSeats($showtimeId);
$bookedSeatIds = array_column($bookedSeats, 'seat_id');
// Get held seats
$heldSeats = $booking->getHeldSeats($showtimeId);
$heldSeatIds = array_column($heldSeats, 'seat_id');
// Mark availability
foreach ($allSeats as &$seat) {
if (in_array($seat['id'], $bookedSeatIds)) {
$seat['availability'] = 'booked';
} elseif (in_array($seat['id'], $heldSeatIds)) {
$seat['availability'] = 'held';
} else {
$seat['availability'] = 'available';
}
}
return [
'showtime' => $showtime,
'seats' => $allSeats
];
}
/**
* Get seat map HTML for display
*/
public function getSeatMapHTML($showtimeId) {
$availability = $this->getSeatAvailability($showtimeId);
if (!$availability) {
return '<p class="text-center">Seat information not available</p>';
}
$seats = $availability['seats'];
$showtime = $availability['showtime'];
// Group seats by row
$rows = [];
foreach ($seats as $seat) {
$rows[$seat['row_label']][] = $seat;
}
$html = '<div class="seat-map-container">';
$html .= '<div class="screen-indicator text-center mb-4">';
$html .= '<div class="screen"></div>';
$html .= '<span class="text-muted">SCREEN</span>';
$html .= '</div>';
$html .= '<div class="seat-map">';
foreach ($rows as $rowLabel => $rowSeats) {
$html .= '<div class="seat-row d-flex justify-content-center mb-2">';
$html .= '<div class="row-label me-3 fw-bold">' . $rowLabel . '</div>';
foreach ($rowSeats as $seat) {
$status = $seat['availability'];
$disabled = ($status !== 'available') ? 'disabled' : '';
$statusClass = 'seat-' . $status;
$html .= '<div class="seat ' . $statusClass . ' m-1" ' . $disabled . '>';
$html .= '<input type="checkbox" class="seat-checkbox" name="seats[]" ';
$html .= 'value="' . $seat['id'] . '" data-label="' . $seat['seat_number'] . '" ';
$html .= 'data-price="' . ($showtime['base_price'] * $seat['price_multiplier']) . '" ';
$html .= $disabled . '>';
$html .= '<div class="seat-inner">';
$html .= '<span>' . $seat['seat_number'] . '</span>';
$html .= '</div>';
$html .= '</div>';
}
$html .= '</div>';
}
$html .= '</div>'; // close seat-map
// Legend
$html .= '<div class="seat-legend d-flex justify-content-center mt-4">';
$html .= '<div class="legend-item me-4"><span class="seat-available"></span> Available</div>';
$html .= '<div class="legend-item me-4"><span class="seat-selected"></span> Selected</div>';
$html .= '<div class="legend-item me-4"><span class="seat-booked"></span> Booked</div>';
$html .= '<div class="legend-item"><span class="seat-held"></span> Temporarily Held</div>';
$html .= '</div>';
$html .= '</div>'; // close seat-map-container
return $html;
}
/**
* Validate seat selection
*/
public function validateSeatSelection($showtimeId, $seatIds) {
$booking = new Booking();
foreach ($seatIds as $seatId) {
if (!$booking->isSeatAvailable($showtimeId, $seatId)) {
return false;
}
}
return true;
}
/**
* Calculate total price for selected seats
*/
public function calculateSeatPrices($showtimeId, $seatIds) {
$showtime = $this->db->getRow("SELECT base_price FROM showtimes WHERE id = ?", [$showtimeId]);
if (!$showtime) {
return 0;
}
$seats = $this->getSeatsByIds($seatIds);
$total = 0;
foreach ($seats as $seat) {
$total += $showtime['base_price'] * $seat['price_multiplier'];
}
return $total;
}
}
?>
Frontend Pages
Main Landing Page
File: index.php
<?php require_once 'includes/config.php'; $movie = new Movie(); // Get now showing movies $nowShowing = $movie->getNowShowing(8); // Get coming soon movies $comingSoon = $movie->getComingSoon(4); // Get featured movies $featured = $movie->getMovies(['featured' => true, 'limit' => 6]); ?> <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title><?php echo APP_NAME; ?> - Book Movie Tickets Online</title> <!-- Bootstrap 5 CSS --> <link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet"> <!-- Font Awesome --> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css"> <!-- Custom CSS --> <link rel="stylesheet" href="assets/css/style.css"> <!-- Owl Carousel for movie sliders --> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/OwlCarousel2/2.3.4/assets/owl.carousel.min.css"> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/OwlCarousel2/2.3.4/assets/owl.theme.default.min.css"> </head> <body> <!-- Navigation --> <nav class="navbar navbar-expand-lg navbar-dark bg-dark fixed-top"> <div class="container"> <a class="navbar-brand" href="index.php"> <i class="fas fa-film me-2"></i> <?php echo APP_NAME; ?> </a> <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav"> <span class="navbar-toggler-icon"></span> </button> <div class="collapse navbar-collapse" id="navbarNav"> <ul class="navbar-nav ms-auto"> <li class="nav-item"> <a class="nav-link active" href="index.php">Home</a> </li> <li class="nav-item"> <a class="nav-link" href="customer/movies.php">Movies</a> </li> <li class="nav-item"> <a class="nav-link" href="customer/theaters.php">Theaters</a> </li> <?php if ($auth->isLoggedIn()): ?> <li class="nav-item dropdown"> <a class="nav-link dropdown-toggle" href="#" id="userDropdown" role="button" data-bs-toggle="dropdown"> <i class="fas fa-user-circle me-1"></i> <?php echo htmlspecialchars($_SESSION['user_name']); ?> </a> <ul class="dropdown-menu dropdown-menu-end"> <li> <a class="dropdown-item" href="<?php echo $_SESSION['user_role'] == 'admin' ? 'admin/dashboard.php' : ($_SESSION['user_role'] == 'manager' ? 'manager/dashboard.php' : 'customer/dashboard.php'); ?>"> <i class="fas fa-tachometer-alt me-2"></i>Dashboard </a> </li> <li> <a class="dropdown-item" href="customer/my_bookings.php"> <i class="fas fa-ticket-alt me-2"></i>My Bookings </a> </li> <li><hr class="dropdown-divider"></li> <li> <a class="dropdown-item text-danger" href="logout.php"> <i class="fas fa-sign-out-alt me-2"></i>Logout </a> </li> </ul> </li> <?php else: ?> <li class="nav-item"> <a class="nav-link" href="login.php">Login</a> </li> <li class="nav-item"> <a class="btn btn-outline-light ms-2" href="register.php">Register</a> </li> <?php endif; ?> </ul> </div> </div> </nav> <!-- Hero Section with Search --> <section class="hero-section text-white d-flex align-items-center" style="background: linear-gradient(rgba(0,0,0,0.7), rgba(0,0,0,0.7)), url('assets/images/hero-bg.jpg'); background-size: cover; background-position: center; height: 500px; margin-top: 76px;"> <div class="container text-center"> <h1 class="display-3 fw-bold mb-4">Book Your Movie Tickets Online</h1> <p class="lead mb-5">Watch the latest movies in stunning quality. Choose your favorite theater and book seats in advance.</p> <div class="row justify-content-center"> <div class="col-md-8"> <form action="customer/search.php" method="GET" class="search-form"> <div class="input-group input-group-lg"> <input type="text" class="form-control" name="q" placeholder="Search for movies, theaters..." required> <button type="submit" class="btn btn-primary"> <i class="fas fa-search me-2"></i>Search </button> </div> </form> </div> </div> </div> </section> <!-- Now Showing Section --> <section class="py-5"> <div class="container"> <div class="d-flex justify-content-between align-items-center mb-4"> <h2 class="fw-bold">Now Showing</h2> <a href="customer/movies.php?status=now_showing" class="btn btn-outline-primary"> View All <i class="fas fa-arrow-right ms-2"></i> </a> </div> <div class="owl-carousel movie-carousel owl-theme"> <?php foreach ($nowShowing as $movie): ?> <div class="movie-card"> <div class="card h-100 border-0 shadow-sm"> <div class="position-relative"> <img src="uploads/movies/<?php echo $movie['poster'] ?: 'default.jpg'; ?>" class="card-img-top" alt="<?php echo htmlspecialchars($movie['title']); ?>"> <?php if ($movie['featured']): ?> <span class="badge bg-warning position-absolute top-0 end-0 m-2"> <i class="fas fa-star me-1"></i>Featured </span> <?php endif; ?> </div> <div class="card-body"> <h6 class="card-title"><?php echo htmlspecialchars($movie['title']); ?></h6> <div class="movie-meta small text-muted mb-2"> <span><i class="far fa-clock me-1"></i><?php echo formatRuntime($movie['duration']); ?></span> <span class="mx-2">|</span> <span><i class="fas fa-tag me-1"></i><?php echo $movie['language'] ?? 'English'; ?></span> </div> <div class="rating mb-2"> <?php echo generateStarRating($movie['user_rating'] / 2); ?> <small class="text-muted ms-2">(<?php echo $movie['total_ratings']; ?>)</small> </div> <a href="customer/movie_details.php?id=<?php echo $movie['id']; ?>" class="btn btn-sm btn-outline-primary w-100"> Book Now </a> </div> </div> </div> <?php endforeach; ?> </div> </div> </section> <!-- Coming Soon Section --> <section class="py-5 bg-light"> <div class="container"> <div class="d-flex justify-content-between align-items-center mb-4"> <h2 class="fw-bold">Coming Soon</h2> <a href="customer/movies.php?status=coming_soon" class="btn btn-outline-primary"> View All <i class="fas fa-arrow-right ms-2"></i> </a> </div> <div class="row g-4"> <?php foreach ($comingSoon as $movie): ?> <div class="col-md-3"> <div class="card h-100 border-0 shadow-sm"> <img src="uploads/movies/<?php echo $movie['poster'] ?: 'default.jpg'; ?>" class="card-img-top" alt="<?php echo htmlspecialchars($movie['title']); ?>"> <div class="card-body"> <h6 class="card-title"><?php echo htmlspecialchars($movie['title']); ?></h6> <p class="small text-muted mb-2"> <i class="far fa-calendar me-1"></i>Releases: <?php echo formatDate($movie['release_date']); ?> </p> <a href="customer/movie_details.php?id=<?php echo $movie['id']; ?>" class="btn btn-sm btn-outline-secondary w-100"> Learn More </a> </div> </div> </div> <?php endforeach; ?> </div> </div> </section> <!-- Features Section --> <section class="py-5"> <div class="container"> <h2 class="text-center fw-bold mb-5">Why Choose Us</h2> <div class="row g-4"> <div class="col-md-3"> <div class="text-center"> <div class="feature-icon bg-primary text-white rounded-circle mx-auto mb-3 d-flex align-items-center justify-content-center" style="width: 80px; height: 80px;"> <i class="fas fa-ticket-alt fa-2x"></i> </div> <h5>Easy Booking</h5> <p class="text-muted">Book your tickets in just a few clicks</p> </div> </div> <div class="col-md-3"> <div class="text-center"> <div class="feature-icon bg-success text-white rounded-circle mx-auto mb-3 d-flex align-items-center justify-content-center" style="width: 80px; height: 80px;"> <i class="fas fa-chair fa-2x"></i> </div> <h5>Choose Your Seat</h5> <p class="text-muted">Select your preferred seats in advance</p> </div> </div> <div class="col-md-3"> <div class="text-center"> <div class="feature-icon bg-warning text-white rounded-circle mx-auto mb-3 d-flex align-items-center justify-content-center" style="width: 80px; height: 80px;"> <i class="fas fa-credit-card fa-2x"></i> </div> <h5>Secure Payments</h5> <p class="text-muted">Multiple payment options with 100% security</p> </div> </div> <div class="col-md-3"> <div class="text-center"> <div class="feature-icon bg-info text-white rounded-circle mx-auto mb-3 d-flex align-items-center justify-content-center" style="width: 80px; height: 80px;"> <i class="fas fa-gift fa-2x"></i> </div> <h5>Loyalty Points</h5> <p class="text-muted">Earn points with every booking</p> </div> </div> </div> </div> </section> <!-- Call to Action --> <section class="py-5 bg-primary text-white"> <div class="container text-center py-4"> <h2 class="fw-bold mb-4">Ready for a Movie Night?</h2> <p class="lead mb-4">Book your tickets now and enjoy the latest blockbusters</p> <?php if (!$auth->isLoggedIn()): ?> <a href="register.php" class="btn btn-light btn-lg px-5 me-2">Sign Up Now</a> <a href="customer/movies.php" class="btn btn-outline-light btn-lg px-5">Browse Movies</a> <?php else: ?> <a href="customer/movies.php" class="btn btn-light btn-lg px-5">Browse Movies</a> <?php endif; ?> </div> </section> <!-- Footer --> <footer class="bg-dark text-white py-5"> <div class="container"> <div class="row"> <div class="col-md-4 mb-4"> <h5><i class="fas fa-film me-2"></i><?php echo APP_NAME; ?></h5> <p class="text-white-50">Your ultimate destination for booking movie tickets online. Experience the magic of cinema with ease.</p> <div class="social-links"> <a href="#" class="text-white me-2"><i class="fab fa-facebook fa-lg"></i></a> <a href="#" class="text-white me-2"><i class="fab fa-twitter fa-lg"></i></a> <a href="#" class="text-white me-2"><i class="fab fa-instagram fa-lg"></i></a> <a href="#" class="text-white me-2"><i class="fab fa-youtube fa-lg"></i></a> </div> </div> <div class="col-md-2 mb-4"> <h6>Quick Links</h6> <ul class="list-unstyled"> <li><a href="index.php" class="text-white-50">Home</a></li> <li><a href="customer/movies.php" class="text-white-50">Movies</a></li> <li><a href="customer/theaters.php" class="text-white-50">Theaters</a></li> <li><a href="#contact" class="text-white-50">Contact</a></li> </ul> </div> <div class="col-md-3 mb-4"> <h6>Support</h6> <ul class="list-unstyled"> <li><a href="#" class="text-white-50">FAQ</a></li> <li><a href="#" class="text-white-50">Terms of Service</a></li> <li><a href="#" class="text-white-50">Privacy Policy</a></li> <li><a href="#" class="text-white-50">Refund Policy</a></li> </ul> </div> <div class="col-md-3 mb-4"> <h6>Contact Us</h6> <ul class="list-unstyled text-white-50"> <li><i class="fas fa-map-marker-alt me-2"></i>123 Cinema Street, Movie City</li> <li><i class="fas fa-phone me-2"></i>+1 (555) 123-4567</li> <li><i class="fas fa-envelope me-2"></i>[email protected]</li> </ul> </div> </div> <hr class="border-secondary"> <div class="row"> <div class="col-12 text-center"> <p class="text-white-50 mb-0">© <?php echo date('Y'); ?> <?php echo APP_NAME; ?>. All rights reserved.</p> </div> </div> </div> </footer> <!-- Scripts --> <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/OwlCarousel2/2.3.4/owl.carousel.min.js"></script> <script> $(document).ready(function() { $('.movie-carousel').owlCarousel({ loop: true, margin: 20, nav: true, dots: false, navText: [ '<i class="fas fa-chevron-left"></i>', '<i class="fas fa-chevron-right"></i>' ], responsive: { 0: { items: 1 }, 600: { items: 2 }, 1000: { items: 4 } } }); }); </script> <style> .hero-section { margin-top: 76px; } .movie-card { transition: transform 0.3s ease; } .movie-card:hover { transform: translateY(-10px); } .feature-icon { width: 80px; height: 80px; } .owl-nav { position: absolute; top: -50px; right: 0; } .owl-prev, .owl-next { background: #f8f9fa !important; color: #333 !important; border-radius: 50% !important; width: 40px; height: 40px; display: inline-flex !important; align-items: center; justify-content: center; } .owl-prev:hover, .owl-next:hover { background: #0d6efd !important; color: white !important; } .search-form .input-group { border-radius: 50px; overflow: hidden; } .search-form input { border: none; padding: 15px 25px; } .search-form button { padding: 15px 30px; border: none; } </style> </body> </html>
Environment Configuration
File: .env
# Database Configuration DB_HOST=localhost DB_NAME=movie_booking_system DB_USER=root DB_PASS= # Application Configuration APP_NAME=Movie Booking System APP_URL=http://localhost/movie-booking-system APP_VERSION=1.0.0 DEBUG_MODE=true # Security SESSION_TIMEOUT=3600 BCRYPT_ROUNDS=12 # Upload Configuration MAX_FILE_SIZE=5242880 # 5MB in bytes # Pagination ITEMS_PER_PAGE=20 # Date/Time TIMEZONE=America/New_York DATE_FORMAT=Y-m-d TIME_FORMAT=H:i # Booking Settings SEAT_HOLD_MINUTES=10 MAX_SEATS_PER_BOOKING=10 CANCELLATION_HOURS=2 LOYALTY_POINTS_PER_BOOKING=10 # Tax Settings TAX_RATE=0.08 CONVENIENCE_FEE=1.50 # Notification Settings ENABLE_REMINDERS=true REMINDER_HOURS=24 ENABLE_SMS=false # Payment Settings ENABLE_PAYMENTS=true PAYMENT_GATEWAY=stripe STRIPE_KEY=your_stripe_key STRIPE_SECRET=your_stripe_secret PAYPAL_CLIENT_ID=your_paypal_client_id PAYPAL_SECRET=your_paypal_secret # SMS Settings (Twilio) TWILIO_SID=your_twilio_sid TWILIO_TOKEN=your_twilio_token TWILIO_PHONE=your_twilio_phone
File: .gitignore
# Environment variables .env # Dependencies /vendor/ node_modules/ # IDE files .vscode/ .idea/ *.sublime-* # OS files .DS_Store Thumbs.db # Logs /logs/ *.log # Uploads /uploads/ !/uploads/.gitkeep # Tickets /tickets/ !/tickets/.gitkeep # Cache /cache/ !/cache/.gitkeep # Composer composer.lock # Temp files *.tmp *.temp
File: composer.json
{
"name": "movie-booking-system/application",
"description": "Online Movie Booking System",
"type": "project",
"require": {
"php": ">=7.4",
"ext-pdo": "*",
"ext-json": "*",
"ext-mbstring": "*",
"ext-curl": "*",
"ext-gd": "*",
"phpmailer/phpmailer": "^6.8",
"twilio/sdk": "^7.0",
"stripe/stripe-php": "^13.0",
"tecnickcom/tcpdf": "^6.6",
"endroid/qr-code": "^4.8"
},
"require-dev": {
"phpunit/phpunit": "^9.0"
},
"autoload": {
"psr-4": {
"MovieSystem\\": "src/"
}
},
"scripts": {
"test": "phpunit tests",
"post-install-cmd": [
"chmod -R 755 uploads/",
"chmod -R 755 tickets/",
"chmod -R 755 logs/",
"chmod -R 755 cache/"
]
}
}
How to Use the Project (Step-by-Step Guide)
Prerequisites
- Web Server: XAMPP, WAMP, MAMP, or any PHP-enabled server (PHP 7.4+)
- Database: MySQL 5.7+ or MariaDB
- Composer: For dependency management
- Browser: Modern web browser (Chrome, Firefox, Edge, etc.)
Installation Steps
Step 1: Set Up Local Server
- Download and install XAMPP from https://www.apachefriends.org/
- Launch XAMPP Control Panel
- Start Apache and MySQL services
Step 2: Install Composer Dependencies
- Download and install Composer from https://getcomposer.org/
- Navigate to your project directory in terminal
- Run:
composer install
Step 3: Create Project Folder
- Navigate to
C:\xampp\htdocs\(Windows) or/Applications/XAMPP/htdocs/(Mac) - Create a new folder named
movie-booking-system - Create all the folders and files as shown in the Project File Structure
Step 4: Set Up Database
- Open browser and go to
http://localhost/phpmyadmin - Click on "New" to create a new database
- Name the database
movie_booking_systemand selectutf8_general_ci - Click on "Import" tab
- Click "Choose File" and select the
database.sqlfile from thesqlfolder - Click "Go" to import the database structure and sample data
Step 5: Configure Environment
- Rename
.env.exampleto.envin the project root - Update database credentials if different from default:
DB_HOST=localhost DB_NAME=movie_booking_system DB_USER=root DB_PASS=
- Update application URL:
APP_URL=http://localhost/movie-booking-system
- Configure payment gateway if using online payments
- Configure email settings if using email notifications
Step 6: Set Folder Permissions
Create the following folders and ensure they are writable:
uploads/movies/uploads/avatars/tickets/logs/cache/
On Windows, right-click folders → Properties → Security → give Write permission to Users
On Mac/Linux, run: chmod -R 755 uploads/ tickets/ logs/ cache/
Step 7: Create Admin Password Hash
- Go to
http://localhost/movie-booking-system/register.php - Register a test user (e.g., email:
[email protected], password:Admin@123) - Open phpMyAdmin, go to the
userstable - Find the user and change role to 'admin'
- Set
email_verifiedto 1
Step 8: Test the Installation
- Open browser and go to
http://localhost/movie-booking-system/ - You should see the landing page with movies
- Test different user types: Admin Login:
- Email:
[email protected] - Password:
Admin@123(or the password you set) Customer Registration: - Register as a new customer
- Browse movies and book tickets
System Walkthrough
For Customers:
- Browse Movies - View now showing and coming soon movies
- Search - Find movies by title, genre, or theater
- Movie Details - View movie information, trailer, cast, and reviews
- Select Showtime - Choose theater, date, and showtime
- Select Seats - Interactive seat map with real-time availability
- Checkout - Review booking details and apply promo codes
- Payment - Complete payment via integrated gateway
- View Tickets - Access e-tickets with QR codes
- My Bookings - View upcoming and past bookings
- Write Reviews - Rate and review watched movies
For Theater Managers:
- Dashboard - View today's shows and booking statistics
- Screen Management - Add and manage screens with seat layouts
- Showtime Management - Schedule movie shows with pricing
- Booking Overview - View all bookings for their theater
- Check-in - Scan QR codes for entry
- Reports - Generate theater-specific reports
For Admins:
- Dashboard - View system-wide statistics
- Movie Management - Add, edit, and manage movies
- Theater Management - Add and manage theaters and screens
- User Management - Manage customers and theater managers
- Promotions - Create and manage discount codes
- Revenue Reports - View financial reports and analytics
- System Settings - Configure global settings
Cron Jobs Setup
Set up the following cron jobs for automated tasks:
# Clean expired seat holds (every minute) * * * * * php /path/to/project/cron/clean_holds.php # Send booking reminders (every hour) 0 * * * * php /path/to/project/cron/send_reminders.php # Update showtime status (daily at midnight) 0 0 * * * php /path/to/project/cron/update_showtimes.php
Security Best Practices
- Change default admin password immediately after installation
- Use HTTPS in production with SSL certificate
- Regular backups of database and uploads
- Input validation on both client and server side
- SQL injection prevention using prepared statements
- XSS prevention using
htmlspecialchars() - CSRF tokens for all forms
- Password hashing using bcrypt
- Rate limiting for login attempts
- Session security with proper timeout
- File upload validation and malware scanning
Performance Optimizations
- Database indexing on frequently queried columns
- Query caching for repeated requests
- Image optimization for movie posters
- Lazy loading for images and content
- Pagination for large datasets
- Minified CSS and JavaScript for production
- CDN for static assets
- Browser caching headers
Conclusion
The Online Movie Booking System is a comprehensive, feature-rich platform that provides a seamless experience for movie enthusiasts to discover films, select showtimes, and book tickets online. With its intuitive interface, real-time seat selection, multiple payment options, and automated ticket generation, this system offers everything needed for a modern cinema booking experience.
This application demonstrates:
- Secure user authentication with email verification
- Real-time seat availability with hold mechanism
- Interactive seat selection with visual seat maps
- Multiple payment gateway integration
- Automated ticket generation with QR codes
- Email and SMS notifications for confirmations and reminders
- Loyalty points system for customer retention
- Role-based access control for different user types
- Comprehensive reporting and analytics
- Responsive design for all devices
- Modular code structure following OOP principles
The system is built to be scalable and extensible, allowing easy addition of new features such as food and beverage ordering, membership programs, or integration with third-party services. Whether you're managing a single-screen cinema or a large multiplex chain, this booking system provides a solid foundation that can be customized to meet specific business requirements.
With proper deployment, regular maintenance, and security updates, this application can serve as a reliable and efficient platform for movie ticket booking operations.