PHP Development

The Ultimate Comprehensive Guide for Mastering PHP Core Features

Published · Updated 14 min read
Knowledge base article
Contents (13 sections)

This is a cookbook, not a syllabus. Each section is a job you actually have to do in a PHP application, and each recipe inside it is a short, complete snippet you can paste into a file and run, followed by the mistake that usually goes with it and what changes when the code runs on shared hosting. Every snippet was executed on PHP 8.3 before this article was published; nothing here depends on a framework.

TL;DR

Ten recipes cover most of what a working PHP site needs. Start every file with declare(strict_types=1). Keep state in sessions, never in cookies you trust. Talk to MySQL only through PDO prepared statements. Validate uploads by content, not by extension, and store them outside public_html. Use curl with timeouts and json_decode(..., flags: JSON_THROW_ON_ERROR). Hash passwords with password_hash, sign anything you hand to the browser with hash_hmac, encrypt secrets at rest with OpenSSL AES-256-GCM. Log errors, never display them in production. Run maintenance from the command line, not from a URL. Check OPcache is on. On Domain India cPanel hosting you choose the PHP version yourself in MultiPHP Manager; use 8.2 or newer for anything written today.

1. Start the file right

Task: make PHP tell you about mistakes instead of hiding them.

php
<?php
declare(strict_types=1);

error_reporting(E_ALL);
ini_set('display_errors', PHP_SAPI === 'cli' ? '1' : '0');
ini_set('log_errors', '1');
date_default_timezone_set('Asia/Kolkata');

function add(int $a, int $b): int { return $a + $b; }

echo add(2, 3), PHP_EOL;        // 5
try {
    echo add("2", 3);           // TypeError under strict_types
} catch (TypeError $e) {
    echo 'Caught: ', $e->getMessage(), PHP_EOL;
}

The gotcha. strict_types applies to calls made from the file that declares it, not to the functions defined in it. Put the declaration at the top of every file, not just the entry point.

On shared hosting. display_errors is off on Domain India servers (checked on the PHP 8.3 build) so that stack traces never reach visitors, and log_errors is on. Read them from the account's error log in cPanel instead, and turn display on only in a local copy. If you need the setting for a debugging session, How to enable display_errors shows the per-directory way.

Which PHP am I on? php -v on the command line and <?php echo PHP_VERSION; in a page can disagree, because the web server and the shell can use different builds. On Domain India cPanel hosting every account picks its own version in MultiPHP Manager (5.x for legacy sites up to 8.5; new accounts default to 8.3). The steps are in How to change PHP versions in cPanel. Everything below assumes 8.2 or newer.

2. Keep state between requests

Task: start a session that cannot be hijacked by a copied cookie.

php
<?php
declare(strict_types=1);

session_set_cookie_params([
    'lifetime' => 0,          // until the browser closes
    'path'     => '/',
    'secure'   => true,       // HTTPS only
    'httponly' => true,       // JavaScript cannot read it
    'samesite' => 'Lax',      // not sent on cross-site POSTs
]);
session_start();

// After a successful login: new id, so a pre-login cookie is useless
function loginUser(int $userId): void {
    session_regenerate_id(true);
    $_SESSION['user_id']  = $userId;
    $_SESSION['login_at'] = time();
}

loginUser(42);
echo 'Session ', session_id() !== '' ? 'ok' : 'missing', ' for user ', $_SESSION['user_id'], PHP_EOL;

Task: show a message once, on the next page only (a "flash").

php
<?php
declare(strict_types=1);
session_start();

function flash(string $key, ?string $message = null): ?string {
    if ($message !== null) {           // set
        $_SESSION['_flash'][$key] = $message;
        return null;
    }
    $value = $_SESSION['_flash'][$key] ?? null;   // read once
    unset($_SESSION['_flash'][$key]);
    return $value;
}

flash('notice', 'Profile saved.');
echo flash('notice'), PHP_EOL;          // Profile saved.
var_dump(flash('notice'));              // NULL — already consumed

The gotcha. Cannot send session cookie - headers already sent means something produced output before session_start(): a blank line before <?php, a UTF-8 byte-order mark, or an echo in an included file. The fix is to remove the output, not to buffer around it. Navigating PHP session and header errors walks through the usual sources.

On shared hosting. Sessions live as files in the account's own temporary directory, one per visitor, and are cleaned by PHP's garbage collector. A site that stores large arrays in $_SESSION for thousands of visitors will hit disk and inode limits before it hits CPU; store an id and load the rest from the database.

3. Talk to MySQL safely

Task: connect once, with exceptions on and real prepared statements.

php
<?php
declare(strict_types=1);

function db(): PDO {
    static $pdo = null;
    if ($pdo === null) {
        $dsn = getenv('DB_DSN') ?: 'mysql:host=localhost;dbname=cpaneluser_app;charset=utf8mb4';
        $pdo = new PDO($dsn, getenv('DB_USER') ?: 'cpaneluser_app', getenv('DB_PASS') ?: '', [
            PDO::ATTR_ERRMODE            => PDO::ERRMODE_EXCEPTION,
            PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
            PDO::ATTR_EMULATE_PREPARES   => false,
        ]);
    }
    return $pdo;
}

Task: never put a value in a query string.

php
<?php
declare(strict_types=1);
// Demo uses SQLite so it runs anywhere; swap the DSN for MySQL in production.
$pdo = new PDO('sqlite::memory:', options: [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC]);
$pdo->exec('CREATE TABLE users (id INTEGER PRIMARY KEY, email TEXT UNIQUE, name TEXT)');

$insert = $pdo->prepare('INSERT INTO users (email, name) VALUES (:email, :name)');
$insert->execute(['email' => '[email protected]', 'name' => "Meera O'Brien"]);

$userInput = "[email protected]' OR '1'='1";          // hostile input
$select = $pdo->prepare('SELECT id, name FROM users WHERE email = :email');
$select->execute(['email' => $userInput]);
var_dump($select->fetch());                           // false — nothing matched, injection failed

$select->execute(['email' => '[email protected]']);
print_r($select->fetch());                            // the real row, apostrophe intact

Task: make two writes succeed or fail together.

php
<?php
declare(strict_types=1);
$pdo = new PDO('sqlite::memory:', options: [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC]);
$pdo->exec('CREATE TABLE accounts (id INTEGER PRIMARY KEY, balance INTEGER NOT NULL CHECK (balance >= 0))');
$pdo->exec('INSERT INTO accounts (id, balance) VALUES (1, 500), (2, 0)');

function transfer(PDO $pdo, int $from, int $to, int $paise): void {
    $pdo->beginTransaction();
    try {
        $debit  = $pdo->prepare('UPDATE accounts SET balance = balance - :p WHERE id = :id');
        $credit = $pdo->prepare('UPDATE accounts SET balance = balance + :p WHERE id = :id');
        $debit->execute(['p' => $paise, 'id' => $from]);
        $credit->execute(['p' => $paise, 'id' => $to]);
        $pdo->commit();
    } catch (Throwable $e) {
        $pdo->rollBack();
        throw $e;
    }
}

transfer($pdo, 1, 2, 300);
try { transfer($pdo, 1, 2, 300); } catch (PDOException $e) { echo "Rolled back: insufficient balance\n"; }
print_r($pdo->query('SELECT id, balance FROM accounts')->fetchAll());   // 200 and 300, not negative

The gotcha. PDO::ATTR_EMULATE_PREPARES => false matters: with emulation on, PDO builds the SQL string itself and a wrong charset can reopen the injection door. Set the charset in the DSN and turn emulation off.

On shared hosting. The host is localhost, and the database and user names carry the cPanel account prefix (cpaneluser_app). Keep credentials in a file above public_html or in an environment variable, never in a file the web server can serve. Connecting to MySQL with PHP in cPanel and DirectAdmin covers creating the user.

4. Handle files and uploads

Task: write a file so a crash halfway never leaves a half-written one.

php
<?php
declare(strict_types=1);

function writeAtomic(string $path, string $contents): void {
    $tmp = tempnam(dirname($path), '.tmp-');
    if ($tmp === false || file_put_contents($tmp, $contents, LOCK_EX) === false) {
        throw new RuntimeException("Cannot write $path");
    }
    if (!rename($tmp, $path)) {           // rename is atomic on the same filesystem
        @unlink($tmp);
        throw new RuntimeException("Cannot replace $path");
    }
}

$file = sys_get_temp_dir() . '/settings.json';
writeAtomic($file, json_encode(['theme' => 'dark'], JSON_PRETTY_PRINT));
echo file_get_contents($file), PHP_EOL;

Task: accept an upload without trusting anything the browser said.

php
<?php
declare(strict_types=1);

function storeUpload(array $file, string $destDir, int $maxBytes = 2_000_000): string {
    if (($file['error'] ?? UPLOAD_ERR_NO_FILE) !== UPLOAD_ERR_OK) {
        throw new RuntimeException('Upload failed with code ' . $file['error']);
    }
    if ($file['size'] > $maxBytes) {
        throw new RuntimeException('File too large');
    }
    $mime = (new finfo(FILEINFO_MIME_TYPE))->file($file['tmp_name']);   // sniff the bytes
    $allowed = ['image/jpeg' => 'jpg', 'image/png' => 'png', 'application/pdf' => 'pdf'];
    if (!isset($allowed[$mime])) {
        throw new RuntimeException("Type $mime not allowed");
    }
    $name = bin2hex(random_bytes(16)) . '.' . $allowed[$mime];   // never the client's filename
    $dest = rtrim($destDir, '/') . '/' . $name;
    if (!move_uploaded_file($file['tmp_name'], $dest) && !rename($file['tmp_name'], $dest)) {
        throw new RuntimeException('Cannot move upload');
    }
    return $name;
}

// Demo without a browser: fake the $_FILES entry with a real 1×1 PNG
$tmp = tempnam(sys_get_temp_dir(), 'up');
file_put_contents($tmp, base64_decode('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg=='));
$stored = storeUpload(['error' => 0, 'size' => filesize($tmp), 'tmp_name' => $tmp], sys_get_temp_dir());
echo 'Stored as ', $stored, PHP_EOL;

The gotcha. $_FILES['photo']['type'] and the file extension are both supplied by the client. A PHP script renamed avatar.png passes both checks; finfo reads the real bytes. Store uploads in a directory outside public_html and serve them through a script, or at least deny PHP execution in the upload folder.

On shared hosting. Two limits apply before your code runs: upload_max_filesize and post_max_size. When they are exceeded, $_FILES arrives empty and $file['error'] is UPLOAD_ERR_INI_SIZE. Raise them per account as shown in PHP upload filesize limit.

5. Call an API

Task: GET JSON with a timeout, and fail clearly.

php
<?php
declare(strict_types=1);

function getJson(string $url, int $timeout = 10): array {
    $ch = curl_init($url);
    curl_setopt_array($ch, [
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_FOLLOWLOCATION => true,
        CURLOPT_CONNECTTIMEOUT => 5,
        CURLOPT_TIMEOUT        => $timeout,
        CURLOPT_HTTPHEADER     => ['Accept: application/json', 'User-Agent: example-app/1.0'],
    ]);
    $body   = curl_exec($ch);
    $status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
    $err    = curl_error($ch);
    curl_close($ch);

    if ($body === false)   throw new RuntimeException("Request failed: $err");
    if ($status >= 400)    throw new RuntimeException("HTTP $status from $url");
    return json_decode($body, associative: true, flags: JSON_THROW_ON_ERROR);
}

$data = getJson('https://domainindia.com/api/public/domains/tlds');
$in = array_values(array_filter($data['tlds'], fn($t) => $t['tld'] === 'in'))[0];
printf(".in registers at ₹%s and renews at ₹%s%s", $in['registrationPrice'], $in['renewalPrice'], PHP_EOL);

Task: POST JSON with a bearer token.

php
<?php
declare(strict_types=1);

function postJson(string $url, array $payload, string $token): array {
    $ch = curl_init($url);
    curl_setopt_array($ch, [
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_POST           => true,
        CURLOPT_POSTFIELDS     => json_encode($payload, JSON_THROW_ON_ERROR),
        CURLOPT_TIMEOUT        => 15,
        CURLOPT_HTTPHEADER     => [
            'Content-Type: application/json',
            'Accept: application/json',
            'Authorization: Bearer ' . $token,
        ],
    ]);
    $body = curl_exec($ch);
    $status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
    curl_close($ch);
    if ($body === false || $status >= 400) throw new RuntimeException("HTTP $status");
    return json_decode($body, true, flags: JSON_THROW_ON_ERROR);
}

$echo = postJson('https://httpbin.org/post', ['order' => 1001, 'amount_paise' => 57500], 'demo-token');
echo 'Server saw: ', $echo['data'], PHP_EOL;

The gotcha. Without CURLOPT_TIMEOUT a slow third party holds a PHP worker for as long as the server allows, and on a busy page that turns one slow API into a site outage. Always set both timeouts, and never call an external API on every page view when a cached result from a minute ago would do.

On shared hosting. Outbound HTTPS is allowed and allow_url_fopen is on, but prefer curl anyway: it gives you timeouts, status codes and error messages that file_get_contents() hides. A longer treatment, including retries and webhooks, is in How to integrate third-party APIs with PHP.

6. Fail loudly in development, quietly in production

Task: one handler that logs everything and shows the visitor a plain page.

php
<?php
declare(strict_types=1);

final class AppException extends RuntimeException {}

set_exception_handler(function (Throwable $e): void {
    error_log(sprintf('[%s] %s in %s:%d', $e::class, $e->getMessage(), $e->getFile(), $e->getLine()));
    if (PHP_SAPI !== 'cli') {
        http_response_code(500);
        echo 'Something went wrong. The error has been logged.';
    } else {
        fwrite(STDERR, "Fatal: {$e->getMessage()}\n");
    }
});

// Warnings become exceptions too, so nothing slips past unnoticed
set_error_handler(function (int $no, string $str, string $file, int $line): bool {
    throw new ErrorException($str, 0, $no, $file, $line);
});

try {
    $config = json_decode('{bad json', true, flags: JSON_THROW_ON_ERROR);
} catch (JsonException $e) {
    echo 'Config problem handled: ', $e->getMessage(), PHP_EOL;
}

try {
    echo intdiv(10, 0);
} catch (DivisionByZeroError $e) {
    echo 'Maths problem handled: ', $e->getMessage(), PHP_EOL;
} finally {
    echo "finally always runs\n";
}

The gotcha. Catching Exception misses Error subclasses such as TypeError and DivisionByZeroError. Catch Throwable at the top level and specific classes below it.

On shared hosting. error_log() writes to the account's PHP error log, which cPanel shows under Metrics › Errors; the file is also readable in File Manager. Comprehensive guide to debugging PHP code covers Xdebug and log reading in depth.

7. Hash, sign, encrypt

Task: store a password.

php
<?php
declare(strict_types=1);

$hash = password_hash('correct horse battery staple', PASSWORD_DEFAULT);   // bcrypt today, upgrades itself later
var_dump(password_verify('correct horse battery staple', $hash));         // true
var_dump(password_verify('wrong', $hash));                                // false

// On each successful login, upgrade old hashes transparently
if (password_needs_rehash($hash, PASSWORD_DEFAULT)) {
    $hash = password_hash('correct horse battery staple', PASSWORD_DEFAULT);
}
echo strlen($hash), " characters, store in a VARCHAR(255)\n";

Task: hand the browser a value it cannot tamper with (a signed download link).

php
<?php
declare(strict_types=1);
$secret = 'load-this-from-env-not-from-code';

function signUrl(string $path, int $expires, string $secret): string {
    $sig = hash_hmac('sha256', "$path|$expires", $secret);
    return "$path?exp=$expires&sig=$sig";
}
function verifyUrl(string $path, int $expires, string $sig, string $secret): bool {
    if ($expires < time()) return false;
    return hash_equals(hash_hmac('sha256', "$path|$expires", $secret), $sig);   // constant-time
}

$url = signUrl('/files/invoice-1001.pdf', time() + 600, $secret);
parse_str(parse_url($url, PHP_URL_QUERY), $q);
var_dump(verifyUrl('/files/invoice-1001.pdf', (int)$q['exp'], $q['sig'], $secret));   // true
var_dump(verifyUrl('/files/invoice-1002.pdf', (int)$q['exp'], $q['sig'], $secret));   // false

Task: encrypt a secret you must be able to read back (an API key at rest).

php
<?php
declare(strict_types=1);

$key = random_bytes(32);                          // generate once, keep it outside the web root

function encrypt(string $plain, string $key): string {
    $iv  = random_bytes(12);                      // 96-bit nonce for GCM, never reused with the same key
    $tag = '';
    $cipher = openssl_encrypt($plain, 'aes-256-gcm', $key, OPENSSL_RAW_DATA, $iv, $tag);
    if ($cipher === false) throw new RuntimeException('Encryption failed');
    return base64_encode($iv . $tag . $cipher);
}
function decrypt(string $stored, string $key): string {
    $raw = base64_decode($stored, true);
    if ($raw === false || strlen($raw) < 28) throw new RuntimeException('Corrupt value');
    [$iv, $tag, $cipher] = [substr($raw, 0, 12), substr($raw, 12, 16), substr($raw, 28)];
    $plain = openssl_decrypt($cipher, 'aes-256-gcm', $key, OPENSSL_RAW_DATA, $iv, $tag);
    if ($plain === false) throw new RuntimeException('Tampered or wrong key');   // GCM tag check failed
    return $plain;
}

$stored = encrypt('sk_live_example_key', $key);
echo decrypt($stored, $key), PHP_EOL;
try { decrypt(substr($stored, 0, -4) . 'AAAA', $key); } catch (RuntimeException $e) { echo $e->getMessage(), PHP_EOL; }

The gotcha. md5() and sha1() are not password hashes, and a plain == comparison of signatures leaks timing information; use password_verify and hash_equals. Tokens (password reset, API keys) come from random_bytes, never from rand() or uniqid(). AES-GCM authenticates as well as encrypts, so a modified value is rejected instead of decrypting to garbage.

On shared hosting. Everything above is core PHP plus the OpenSSL extension, which is loaded on every PHP version we offer. The sodium extension is not enabled on our shared servers by default, which is why this recipe uses OpenSSL. Keep the key in a file outside public_html with permissions 600, and never commit it.

8. Dates and money without surprises

Task: work in Indian time, store in UTC.

php
<?php
declare(strict_types=1);

$ist = new DateTimeZone('Asia/Kolkata');
$ordered = new DateTimeImmutable('2026-09-06 23:45', $ist);
$renewal = $ordered->add(new DateInterval('P1Y'));

echo 'Store: ',  $ordered->setTimezone(new DateTimeZone('UTC'))->format(DATE_ATOM), PHP_EOL;
echo 'Show:  ',  $renewal->format('j M Y, g:i a'), ' IST', PHP_EOL;
echo 'Days left: ', (new DateTimeImmutable('now', $ist))->diff($renewal)->days, PHP_EOL;

Task: keep money as integers and print it the Indian way.

php
<?php
declare(strict_types=1);

function inr(int $paise): string {
    $rupees = intdiv($paise, 100);
    $p      = $paise % 100;
    $s      = (string)$rupees;
    if (strlen($s) > 3) {                               // 1,00,000 grouping
        $last3 = substr($s, -3);
        $rest  = preg_replace('/\B(?=(\d{2})+(?!\d))/', ',', substr($s, 0, -3));
        $s     = "$rest,$last3";
    }
    return '₹' . $s . ($p ? sprintf('.%02d', $p) : '');
}

echo inr(57500), ' ', inr(115000), ' ', inr(12345678), ' ', inr(99), PHP_EOL;   // ₹575 ₹1,150 ₹1,23,456.78 ₹0.99
$withGst = (int) round(57500 * 1.18);
echo 'With 18% GST: ', inr($withGst), PHP_EOL;

The gotcha. 0.1 + 0.2 !== 0.3 in floating point, so a price stored as a float will eventually produce an invoice that is one paisa off. Store paise as integers and convert only for display.

9. Run PHP from the terminal and from cron

Task: a script with arguments, an exit code and no HTML.

php
<?php
declare(strict_types=1);
// Usage: php cleanup.php --days=30 [--dry-run]
$opts = getopt('', ['days:', 'dry-run']);
$days = (int)($opts['days'] ?? 30);
$dry  = array_key_exists('dry-run', $opts);

if ($days < 1) { fwrite(STDERR, "days must be >= 1\n"); exit(2); }

$cutoff = time() - $days * 86400;
$dir = sys_get_temp_dir() . '/app-cache';
@mkdir($dir);
touch("$dir/old.tmp", $cutoff - 10);   // demo file that is older than the cutoff
$removed = 0;
foreach (new DirectoryIterator($dir) as $f) {
    if ($f->isFile() && $f->getMTime() < $cutoff) {
        $dry || unlink($f->getPathname());
        $removed++;
    }
}
echo ($dry ? 'Would remove ' : 'Removed '), $removed, " file(s) older than $days days\n";
exit(0);

The gotcha. A maintenance job reachable at https://example.in/cron.php can be run by anyone, at any rate. Move it to the command line and, if it must stay web-reachable, require a long random token.

On shared hosting. In cPanel Cron Jobs, call the interpreter explicitly so the job runs under the version you chose in MultiPHP Manager, for example /usr/local/bin/php /home/cpaneluser/scripts/cleanup.php --days=30, and redirect output to a log file rather than letting cron email it. Run it as often as it needs, not every minute; account CPU limits apply to cron as much as to page views.

10. Make it fast

Task: confirm OPcache is doing its job.

php
<?php
declare(strict_types=1);

$status = function_exists('opcache_get_status') ? opcache_get_status(false) : false;
if ($status === false || empty($status['opcache_enabled'])) {
    echo "OPcache is OFF — every request recompiles every file\n";
} else {
    printf("OPcache ON: %d scripts cached, %.1f%% hit rate\n",
        $status['opcache_statistics']['num_cached_scripts'],
        $status['opcache_statistics']['opcache_hit_rate']);
}

Task: process a large file without loading it into memory.

php
<?php
declare(strict_types=1);

function lines(string $path): Generator {
    $h = fopen($path, 'rb');
    try {
        while (($line = fgets($h)) !== false) yield rtrim($line, "\r\n");
    } finally {
        fclose($h);
    }
}

$csv = sys_get_temp_dir() . '/orders.csv';
file_put_contents($csv, implode("\n", array_map(fn($i) => "$i,order-$i," . ($i * 100), range(1, 200_000))));

$total = 0;
foreach (lines($csv) as $line) {
    $total += (int) explode(',', $line)[2];
}
printf("Sum %d using %.1f MB peak\n", $total, memory_get_peak_usage(true) / 1_048_576);

The gotcha. file() and file_get_contents() on a 200 MB export need more than 200 MB of RAM and stop with Allowed memory size exhausted. Streams and generators keep the footprint flat regardless of file size. If you do hit the limit, PHP memory limit explains the safe way to raise it.

On shared hosting. OPcache is enabled on Domain India's PHP builds; the check above tells you if a custom php.ini turned it off. Cache expensive results (API responses, rendered fragments) in files or the database with a timestamp; the biggest wins on shared hosting come from doing less work per request, not from a faster server.

11. Modern PHP worth adopting today

Everything below runs on 8.1 or newer. The old form still works; the new one is shorter and lets PHP catch more mistakes for you.

php
<?php
declare(strict_types=1);

enum Status: string {
    case Active = 'active';
    case Suspended = 'suspended';
    public function label(): string {
        return match ($this) { self::Active => 'Active', self::Suspended => 'Suspended' };
    }
}

final readonly class Invoice {
    public function __construct(public int $id, public int $totalPaise, public Status $status) {}
}

$inv = new Invoice(id: 1001, totalPaise: 57500, status: Status::from('active'));
echo $inv->status->label(), ' invoice #', $inv->id, PHP_EOL;

$customer = null;
echo $customer?->email ?? 'no customer', PHP_EOL;          // nullsafe + null coalescing

$fmt = strtoupper(...);                                    // first-class callable
echo implode(' ', array_map($fmt, ['php', '8.3'])), PHP_EOL;

try { $inv->id = 5; } catch (Error $e) { echo 'readonly: ', $e->getMessage(), PHP_EOL; }
JobOld habitDo this instead
Branch on a valueswitch with breakmatch (strict, returns a value, no fall-through)
Fixed set of optionsclass constants or stringsenum with methods
Value objectsclass with private settersreadonly class
Many optional parameterspositional null, null, truenamed arguments
Optional chainnested isset()?-> and ??
Callbacks'strtoupper' stringstrtoupper(...)
Type checksis_int() by handtyped properties, parameters and return types

Cheat sheet

NeedUseAvoid
Session startsession_set_cookie_params([...]) then session_start()reading $_COOKIE for identity
QueryPDO::prepare() + execute([...])string concatenation, mysql_*
Upload checkfinfo MIME + move_uploaded_file()trusting ['type'] or the extension
HTTP callcurl_* with CURLOPT_TIMEOUTfile_get_contents('https://…')
JSONjson_decode($s, true, flags: JSON_THROW_ON_ERROR)ignoring json_last_error()
Passwordpassword_hash() / password_verify()md5(), sha1(), home-made salts
Random tokenrandom_bytes(), bin2hex()rand(), uniqid(), md5(time())
Signature comparehash_equals()==
Moneyinteger paisefloat rupees
DatesDateTimeImmutable with an explicit zonedate() with the server default
Errors in prodlog_errors=1, display_errors=0@ suppression, die($e)
Big filesfgets() in a generatorfile(), file_get_contents()

Frequently asked questions

Which PHP version should a new project use on Domain India hosting?

8.3, the current default on our cPanel servers. 8.2 to 8.5 are available in MultiPHP Manager; older versions exist only so legacy sites keep running and should not be chosen for new code.

Do I need Composer for any of this?

No. Every recipe uses core PHP and extensions that ship with our builds (PDO, curl, OpenSSL, mbstring, OPcache). When you do need a library, installing PHP libraries with Composer on cPanel hosting shows how without root.

Where do I put database credentials on shared hosting?

In a file one level above public_html (for example /home/cpaneluser/config.php) that your scripts require, or in environment variables set in .htaccess with SetEnv. Never inside a directory the web server serves.

Why does my script work on my laptop but fail on the server?

The usual four: a different PHP version (check MultiPHP Manager), a missing extension (see the list of PHP modules), a function disabled on shared hosting such as exec (see PHP disabled functions on shared hosting), or a case-sensitive filename that Windows or macOS forgave.

Is mysqli wrong?

No, mysqli with prepared statements is fine. PDO is used here because the same code talks to MySQL, MariaDB, SQLite and PostgreSQL, which is also what makes these snippets runnable anywhere.

How do I see errors on a live site without showing them to visitors?

Leave display_errors off, keep log_errors on, and read the account's error log in cPanel. For a short debugging session on one directory, the display_errors article above shows the per-folder override.

Should I use sessions or JWTs for login?

For a normal website on one server, sessions. They are revocable, invisible to JavaScript and need no library. JWTs suit APIs consumed by mobile apps or separate front ends; if you use them, keep them short-lived and never store secrets inside.

What is the fastest single improvement for a slow PHP site?

Confirm OPcache is on (section 10), then cache the slowest query or API call. Those two steps fix more slow sites than any hosting upgrade.

Run this code on hosting that already has the right PHP

PHP 8.2 to 8.5 selectable per account, OPcache on, PDO, curl and OpenSSL included, free SSL. Switch versions any time from MultiPHP Manager.

See cPanel hosting 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
Mastering PHP Core Features: Complete Guide | Domain India