PHP Development

Demystifying PHP: Building a Job Portal Website with Modern Programming Paradigms

By the Domain India teamPublished 8 min read
Knowledge base article
Contents (9 sections)

A job portal is a good practice project for modern PHP: it needs clear data models, a database, user accounts, file uploads for résumés and a small API. This guide builds the core of one with current PHP 8 features and the habits that keep it secure, and explains which popular "advanced" techniques belong on a server you control rather than on shared hosting.

Key takeaways

Model jobs with small typed classes, keep all SQL in one repository class that uses PDO with prepared statements, and route requests through thin controllers (MVC). Hash passwords with password_hash(), regenerate the session ID at login, add a CSRF token to every form and escape all output with htmlspecialchars(). Validate uploads by content, give them random names and keep them outside the web root. Long-running tools such as WebSocket or event-loop servers need a VPS; on shared hosting, use cron and ordinary requests instead.

1. What you'll build

  • Employers post jobs; candidates browse, search and apply with a résumé.
  • A JSON endpoint lists open jobs for other sites or a mobile app.
  • Plain PHP with no framework, so every part is visible. In a real project, a framework such as Laravel or Symfony gives you routing, validation and security features ready-made.

Use a supported PHP version: 8.3 or newer is the safe choice in 2026.

2. Model the data with typed classes

PHP 8 lets you declare a class's properties in the constructor. readonly stops them being changed by accident, and an enum replaces magic strings:

php
<?php
declare(strict_types=1);

enum JobType: string {
    case FullTime = 'full_time';
    case PartTime = 'part_time';
    case Internship = 'internship';
}

final class Job {
    public function __construct(
        public readonly ?int $id,
        public readonly string $title,
        public readonly string $description,
        public readonly int $salaryMonthly,
        public readonly JobType $type,
    ) {}
}

The matching table:

sql
CREATE TABLE jobs (
  id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
  title VARCHAR(200) NOT NULL,
  description TEXT NOT NULL,
  salary_monthly INT UNSIGNED NOT NULL,
  type VARCHAR(20) NOT NULL,
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  INDEX (title)
) DEFAULT CHARSET = utf8mb4;

3. One database connection, one place for SQL

Keep credentials out of your code, in a config file outside the public folder, and create the PDO connection once:

php
function db(): PDO {
    static $pdo = null;
    if ($pdo === null) {
        $cfg = require __DIR__ . '/../config.php';
        $pdo = new PDO(
            "mysql:host={$cfg['host']};dbname={$cfg['db']};charset=utf8mb4",
            $cfg['user'], $cfg['pass'],
            [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
             PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
             PDO::ATTR_EMULATE_PREPARES => false]
        );
    }
    return $pdo;
}

Put every query in a repository class. Each value goes in through a ? placeholder, never by joining strings, which is what prevents SQL injection:

php
final class JobRepository {
    public function __construct(private PDO $pdo) {}

    public function add(Job $job): int {
        $stmt = $this->pdo->prepare(
            'INSERT INTO jobs (title, description, salary_monthly, type) VALUES (?, ?, ?, ?)');
        $stmt->execute([$job->title, $job->description, $job->salaryMonthly, $job->type->value]);
        return (int) $this->pdo->lastInsertId();
    }

    public function search(string $term, int $limit = 20): array {
        $stmt = $this->pdo->prepare(
            'SELECT id, title, salary_monthly, type FROM jobs WHERE title LIKE ? ORDER BY created_at DESC LIMIT ?');
        $stmt->bindValue(1, $term . '%');
        $stmt->bindValue(2, $limit, PDO::PARAM_INT);
        $stmt->execute();
        return $stmt->fetchAll();
    }
}

Let the database do searching and sorting with indexes. Loading every job into a PHP array to search it, for example with a hand-written binary search, stops working as soon as the table grows. For more, see Preventing SQL injection in PHP and Node.js.

4. Structure requests with MVC

A controller reads the request, calls the repository and chooses a view. It holds no SQL and no HTML:

php
final class JobController {
    public function __construct(private JobRepository $jobs) {}

    public function index(): void {
        $term = trim($_GET['q'] ?? '');
        $jobs = $this->jobs->search($term);
        require __DIR__ . '/../views/jobs/index.php';
    }
}

In the view, escape every value you print, which stops cross-site scripting:

php
<?php foreach ($jobs as $job): ?>
  <h2><?= htmlspecialchars($job['title'], ENT_QUOTES, 'UTF-8') ?></h2>
<?php endforeach; ?>

5. User accounts done safely

php
function register(PDO $pdo, string $email, string $password): void {
    $hash = password_hash($password, PASSWORD_DEFAULT);
    $pdo->prepare('INSERT INTO users (email, password_hash) VALUES (?, ?)')
        ->execute([$email, $hash]);
}

function login(PDO $pdo, string $email, string $password): bool {
    $stmt = $pdo->prepare('SELECT id, password_hash FROM users WHERE email = ?');
    $stmt->execute([$email]);
    $user = $stmt->fetch();
    if (!$user || !password_verify($password, $user['password_hash'])) {
        return false;
    }
    session_regenerate_id(true);
    $_SESSION['user_id'] = $user['id'];
    return true;
}

Store only the user ID in the session, never the password hash. session_regenerate_id(true) at login blocks session fixation. Every form that changes data also needs a CSRF token: create one with bin2hex(random_bytes(32)), store it in the session, put it in a hidden field and compare it with hash_equals() when the form is submitted.

6. Résumé uploads

Uploads are the riskiest part of a job portal. Check the real file type, not the name the browser sends, and never keep the original filename:

php
$file = $_FILES['resume'] ?? null;
if (!$file || $file['error'] !== UPLOAD_ERR_OK || $file['size'] > 2 * 1024 * 1024) {
    throw new RuntimeException('Upload a PDF under 2 MB.');
}
$mime = (new finfo(FILEINFO_MIME_TYPE))->file($file['tmp_name']);
if ($mime !== 'application/pdf') {
    throw new RuntimeException('Only PDF files are accepted.');
}
$name = bin2hex(random_bytes(16)) . '.pdf';
move_uploaded_file($file['tmp_name'], __DIR__ . '/../storage/resumes/' . $name);

Save the files in a folder outside public_html, and serve them only to the employer who owns the job, through a PHP script that checks permissions first.

7. A small JSON API

php
header('Content-Type: application/json; charset=utf-8');
$repo = new JobRepository(db());
echo json_encode(['jobs' => $repo->search($_GET['q'] ?? '')], JSON_THROW_ON_ERROR);

To call another API, for example for salary data, use cURL or an HTTP client library such as Guzzle, with a timeout, and check the HTTP status before you use the response. For a full walkthrough, see How to integrate third-party APIs with PHP.

8. Async, events and real-time notifications

Libraries such as ReactPHP and WebSocket servers built on it run as long-lived processes that listen on their own port. That is a good fit for a VPS, where you control processes and ports, but not for shared hosting, where PHP runs per request and you can't keep a server process listening. On shared hosting, get the same result the simple way:

  • Email the employer when someone applies, straight from the request, with PHP mail().
  • Queue slow work in a database table and process it with a cron job every few minutes.
  • Show new applications by having the page poll a small JSON endpoint every 30 to 60 seconds.

9. Running this on Domain India

Measured on our shared servers in September 2026:

  • PHP and databases. cPanel offers PHP 5.1 to 8.5, with 8.3 the default for new accounts; DirectAdmin offers PHP up to 8.3. Databases are MariaDB, managed in phpMyAdmin.
  • Disabled functions. Functions that start programs or open raw sockets are disabled, so Composer cannot run on the server. Run composer install on your computer or in CI and upload the project with its vendor folder. See PHP disabled functions on shared hosting.
  • Email. SMTP from PHP fails on cPanel because the socket functions it needs are disabled. Use mail() with the -f envelope sender, as shown in Using PHPMailer for contact forms. On cPanel an account can send 200 messages per hour.
  • cURL. On cPanel, curl_exec() normally works. On most DirectAdmin sites it is disabled, but file_get_contents() on an https:// URL works.
  • Background work. Use cron jobs; see How to set up a cron job.

If you need long-running processes, WebSockets or your own PHP settings, a self-managed VPS gives you full root access.

cPanel Starter
₹125/mo + GST
  • 25 GB NVMe SSD Storage
  • 50 GB Monthly Bandwidth
  • 1 Website
  • 10 Email Accounts
See plan details
VPS Starter
₹552.65/mo + GST
  • 1 vCPU
  • 2 GB DDR4 RAM
  • 64 GB NVMe SSD Storage
  • 2 TB Monthly Bandwidth
See plan details

The cards show live Domain India prices, excluding 18% GST.

Which PHP version should I use for a new project in 2026?

PHP 8.3 or newer. Older versions no longer receive security fixes. On Domain India cPanel hosting, new accounts use PHP 8.3 by default and you can change the version per domain.

How do I prevent SQL injection in PHP?

Use PDO or mysqli prepared statements and pass every value through a placeholder. Never build SQL by joining user input into the query string.

How should I store user passwords in PHP?

Hash them with password_hash() using PASSWORD_DEFAULT and check them with password_verify(). Never store plain passwords or use md5 or sha1 for passwords.

Can I run a ReactPHP or WebSocket server on shared hosting?

No. They need a long-running process listening on its own port, which shared hosting doesn't allow. Use a VPS, or use cron jobs and polling on shared hosting.

Can I run Composer on Domain India shared hosting?

No. Composer needs proc_open, which is disabled on shared servers. Run composer install on your own computer or in CI and upload the project including the vendor folder.

How do I send application emails from PHP on cPanel hosting?

Use PHP's mail() with the -f envelope sender set to an address on your domain. SMTP connections from PHP fail on cPanel because the socket functions they need are disabled.

Ready to build? Start on cPanel hosting for a classic PHP and MariaDB site, or choose a VPS if you need long-running processes. Questions about what your plan supports? Open a support ticket.

Host your PHP project

PHP 8.3 by default, MariaDB databases, phpMyAdmin and weekly backups with JetBackup.

See cPanel plans

Was this article helpful?

Your answer helps us decide what to improve next.

Still need help? Open a support ticket and our team will reply.

Prefer an app? Add this site to your home screen.Get the app
Build a Job Portal in Modern PHP 8: OOP, MVC, Security