PHP



Create a local PHP Server


php -S localhost:<port> -> php -S localhost:8000 # -S should be uppercase
php -S localhost:8000 -t public
#-t public -> Sets the document root (the folder PHP serves) to the public/ directory.

What is PHP



How PHP works (simple)


  1. Browser requests a URL (e.g. https://example.com/ → server receives GET /).
  2. Web server (Apache, Nginx, etc.) checks the document root for configured index files (e.g. index.php, index.html).
  3. If an index file is found and PHP is configured, the server hands the .php file to the PHP handler (via mod_php or PHP-FPM).
  4. PHP parses and compiles the script into opcodes (cached by OPcache for performance), executes it, and returns the output (HTML, JSON, etc.) to the web server → then to the browser.
  5. If no index file exists and directory listing is enabled, the server returns a file listing (a security risk).
  6. Use server config (e.g. Options -Indexes in Apache or autoindex off in Nginx) to prevent directory listings.

PHP Fundamentals



echo, print_r vs var_dump



PHP Types



Type juggling


<?php
$qty = 20;
if($qty == '20') {
    echo 'Equal'; //Equal
}

$total = 100;
$qty = "20 pieces";
$total = $total + $qty;

echo $total; // 120 //PHP casts the string “20 pieces” as an integer 20 before calculating the sum

Functions


<?php

function sum($a, $b, int ...$numbers): int
{
    return array_sum($numbers);
}

Advanced Functions



Anonymous functions



Arrow functions (php 7.4)



Variable functions


<?php

$f = 'strlen';
echo $f('Hello'); # strlen('Hello) -> 5

Variable constructs(language construct)



Advanced Array Operations


<?php

$lengths = [10, 20, 30];
// calculate areas
$areas = array_map(
	fn ($length) => $length * $length,
	$lengths
);
print_r($areas);
<?php

$numbers = [1, 2, 3, 4, 5];
$odd_numbers = array_filter(
	$numbers,
	fn ($number) => $number % 2 === 1
);
print_r($odd_numbers);

# Using callback as a method of a class
array_filter($numbers,[new Odd(), 'isOdd']) //if isOdd is a public method of Odd class
array_filter($numbers,['Odd', 'isOdd']) //if isOdd is a static method of Odd class
array_filter($numbers,new Odd()) //if you want to pass __invoke() as the callback function
<?php

$numbers = [10,20,30];
$total  = array_reduce(
    $numbers,
    fn ($previous, $current) => $previous + $current
);
echo $total; // 60

State Management




# Setting a cookie
setcookie ( 
    string $name , 
    string $value = "" , 
    array $options = [] //expires, path, domain, secure, httponly and samesite
    ) : bool

setcookie('name','value',
          [	
            'expires' => time() + 7 * 86400,  // 7 days from now,
          	'path' => '/',
        	'secure' => true,
        	'httponly' => true,
        	'samesite' => 'Lax'
          ]);

# samesite can take a value of None, Lax, or Strict
# if $secure is set to true , the cookie should be transmitted over a secured HTTP (HTTPS) connection from the web browser.
# if $httponly is true, the cookie can be accessed only via the HTTP protocol, not JavaScript.

# Reading a cookie
    if (isset($_COOKIE['cookie_name'])) {...)
# Deleting a cookie
    unset($_COOKIE['cookie_name']);
	setcookie('cookie_name', null, time()-3600); //can be deleted by setting a past date as the expiry date

Session


<?php

# get session data stored location
# /tmp folder of the web server
echo ini_get('session.save_path'); //OR
echo session_save_path();

# store values
// store scalar value
$_SESSION['user'] = 'admin';
// store an array
$_SESSION['roles'] = ['administrator', 'approver', 'editor'];

# deleting session values
session_destroy();

# This  session_destroy() deletes all data associated with the current session. However, it does not unset data in the  $_SESSION array and cookie.

# To completely destroy the session data, you need to unset the variable in  $_SESSION array and remove the PHPSESSID cookie like this:

// remove cookie
if(isset($_COOKIE[session_name()])){
    setcookie(session_name(),'',time() - 3600, '/');
}

// unset data in $_SESSION
$_SESSION[] = array();

// destroy the session
session_destroy();

Processing Forms


# ex 1
<?php
if (isset($_POST['name'], $_POST['email'])) {
	$name = htmlspecialchars($_POST['name']);
	$email = htmlspecialchars($_POST['email']);

	// show the $name and $email
	echo "Thanks $name for your subscription.<br>";
	echo "Please confirm it in your inbox of the email $email.";
} else {
	echo 'You need to provide your name and email address.';
}

# ex 2
<form action="<?php echo htmlspecialchars($_SERVER['PHP_SELF']) ?>" method="post">

Form validation flow


Validate → Sanitize → Escape

Validate

Sanitize

Escape


isset() vs filter_has_var()


<?php
	$_POST['email'] = 'example@phptutorial.net';
	if(isset($_POST['email'])) // return true

	$_POST['email'] = 'example@phptutorial.net'; //manually set email
	if(filter_has_var(INPUT_POST, 'email')) //return false as it checks only actual request body

filter_var() for sanitize and validate data


//filter_var ( mixed $value , int $filter = FILTER_DEFAULT , array|int $options = 0 ) : mixed

if (filter_has_var(INPUT_GET, 'id')) {
	// sanitize id
	// remove all characters except the digits, +,and - signs from the id variable.
	$clean_id = filter_var($_GET['id'], FILTER_SANITIZE_NUMBER_INT);

	// validate id with options, here options array is optional
    $id = filter_var($clean_id, FILTER_VALIDATE_INT, ['options' => ['min_range' => 10]]);

filter_input() for sanitize and validate user inputs (HTTP)


<?php
// filter_input ( int $type , string $var_name , int $filter = FILTER_DEFAULT , array|int $options = 0 ) : mixed
$term_html = filter_input(INPUT_GET, 'term', FILTER_SANITIZE_SPECIAL_CHARS); //value for showing on the 

filter_input vs. filter_var


$validated = filter_var($_POST['email'] ?? '', FILTER_VALIDATE_EMAIL);

//OR just
$validated = filter_input(INPUT_POST, 'email', FILTER_VALIDATE_EMAIL);

CSRF



1️⃣ You’re logged into your bank



2️⃣ You visit a malicious website


<form action="https://yourbank.com/transfer-fund" method="POST" id="attackForm">
	<input type="hidden" name="amount" value="10000" />
	<input type="hidden" name="to_account" value="attacker123" />
</form>

<script>
	document.getElementById("attackForm").submit(); // auto-submit when the page loads
</script>

3️⃣ What happens next



4️⃣ Why CSRF works



How websites defend against it


session_start();
$_SESSION['token'] = bin2hex(random_bytes(35));

// in the form
<input type="hidden" name="token" value="<?= $_SESSION['token'] ?? '' ?>">

//Check the submitted token with the one stored in the $_SESSION to prevent the CSRF attacks.

PRG (Post-Redirect-Get)


When a user submits a form (like a registration form or payment form):

The PRG Solution

<?php
session_start();

if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    // Process form (e.g., save to DB)
    $_SESSION['message'] = 'Form submitted successfully!';

    // Redirect to the same page using GET
    header('Location: ' . $_SERVER['PHP_SELF']);
    exit;
}
?>

<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><title>PRG Example</title></head>
<body>
  <?php
  if (!empty($_SESSION['message'])) {
      echo '<p>' . htmlspecialchars($_SESSION['message']) . '</p>';
      unset($_SESSION['message']); // clear after showing
  }
  ?>

  <form method="post">
      <input type="text" name="name" placeholder="Enter name" required>
      <button type="submit">Submit</button>
  </form>
</body>
</html>

File Upload


<form enctype="multipart/form-data" action="upload.php" method="post">
    <input type="file" accept="image/png, image/jpeg" name="file"> //single file
	<input type="file" name="files[]" id="files" multiple /> //multiple file
</form>

#from php
$_FILES['file'];

password_hash and password_verify


password_hash(
     string $password,
     string|int|null $algo,
     array $options = []
): string

//PASSWORD_DEFAULT -> bcrypt
//PASSWORD_BCRYPT -> CRYPT_BLOWFISH
//PASSWORD_ARGON2I -> Argon2i
//PASSWORD_ARGON2ID -> Argon2id

<?php

$password = 'Password1';
echo password_hash($password, PASSWORD_DEFAULT);

password_verify(string $password, string $hash): bool
// $password is a plain text password to match.
// $hash is a hash created by the password_hash() function.

Working with Files


$f = fopen($filename, 'r'); //open the readme.txt for reading.
fclose($f); //close the file

if(file_exists($filename)){...}
# check if the file readme.txt exists in the current directory:
# $filename can be also a path to a directory. In this case, the file_exists() function returns true if the directory exists.

if (is_file($filename)) {...}
# check if a path is a file (not a directory) and exists

if (is_readable($filename)) {...}
# check if a file exists and readable

if (is_writable($filename)) {...}
# check if a file exists and writable
$content = file_get_contents(
	$filename = $filename,
    $use_include_path = false , 
    $context = null ,
	$offset = 5,
	$maxlen = 20
);
<?php

$lines = file(
    'https://www.php.net/robots.txt',
    FILE_SKIP_EMPTY_LINES | FILE_IGNORE_NEW_LINES
);
//FILE_USE_INCLUDE_PATH -> Search for the file in the include path.
//FILE_IGNORE_NEW_LINES -> Skip the newline at the end of the array element.
//FILE_SKIP_EMPTY_LINES -> Skip empty lines in the file.

# If your system or network uses a proxy server (for internet access control, filtering, or security), direct connections to external URLs (like php.net) may be blocked.
# use stream_context_create() function in such cases

CSV (comma-separated values)



Writing to a CSV file


fputcsv ( resource $handle , array $fields , string $delimiter = "," , string $enclosure = '"' , string $escape_char = "\\" ) : int|false

# ex
<?php

$data = [
	['Symbol', 'Company', 'Price'],
	['GOOG', 'Google Inc.', '800'],
];

$filename = 'stock.csv';
$f = fopen($filename, 'w');// open csv file for writing

if ($f === false) {
	die('Error opening the file ' . $filename);
}

// write each row at a time to a file
foreach ($data as $row) {
	fputcsv($f, $row);
}
fclose($f);// close the file

Reading from a CSV file


fgetcsv ( resource $stream , int $length = 0 , string $separator = "," , string $enclosure = '"' , string $escape = "\\" ) : array

# ex
<?php

$filename = './stock.csv';
$data = [];

// open the file
$f = fopen($filename, 'r');

if ($f === false) {
	die('Cannot open the file ' . $filename);
}

// read each line in CSV file at a time
while (($row = fgetcsv($f)) !== false) {
	$data[] = $row;
}

// close the file
fclose($f);
<?php

#get the permissions set on a particular file
$permissions = fileperms('readme.txt');

# changing file permission
$filename = './readme.txt';
chmod($filename, 0644);

Working with Directories


<?php
# Use the opendir() function to open a directory and get the directory handle
$dh = opendir('./public');

# Use the readdir() function to read the entries in a directory specified by a directory handle.
if ($dh) {
	while ($e = readdir($dh)) {
		if ($e !== '.' && $e !== '..') {
			echo $e , PHP_EOL;
		}
	}
}

# Use getcwd() to get the current directory
echo getcwd();

# Use chdir() to change the current directory
chdir('./dev');
echo getcwd(); //dev

# Use the mkdir() function to create a new directory.
mkdir('./public/img')) //parent directory public must exist.
mkdir($dir, 0644); //can pass permissions, default is 0777, or ou can use chmod() function to change it

# Use the rmdir() function to remove a directory.
rmdir('./public/assets'); //need to have sufficient permissions && the directory needs to be empty

# Use the is_dir() function to check if a path is a directory and that directory exists in the file system.
is_dir('./public')

# Use the closedir() function once you are done with the directory.
closedir($dh);
//Using the PHP glob() function to calculate the total size of PHP files
echo array_sum(array_map('filesize', glob('./src/*.php')));
<?php

echo '1.' , dirname("/htdocs/public") , PHP_EOL; 	#1./htdocs
echo '2.' , dirname("/htdocs/") , PHP_EOL; 			# 2.\
echo '3.' , dirname(".") , PHP_EOL; 				# 3..
echo '4.' , dirname("C:\\") , PHP_EOL; 				# 4.C:\
echo '5.' , dirname("/htdocs/public/css/dev", 2); 	# 5./htdocs/public
<?php

echo "1) ".basename("/htdocs/index.php", ".php").PHP_EOL; 	# 1) index
echo "2) ".basename("/htdocs/index.php").PHP_EOL; 			#2) index.php
echo "3) ".basename("/htdocs/public").PHP_EOL; 				# 3) public
echo "4) ".basename("/htdocs/").PHP_EOL; 					# 4) htdocs
echo "5) ".basename(".").PHP_EOL; 							# 5) .
echo "6) ".basename("/"); 									# 6)
<?php

$path = 'htdocs/phptutorial/index.php';
$parts = pathinfo($path);
print_r($parts);
#result
Array
(
    [dirname] => htdocs/phptutorial
    [basename] => index.php
    [extension] => php
    [filename] => index
)

pathinfo($path, PATHINFO_BASENAME); //index.php
//PATHINFO_DIRNAME -> Return the directory name
//PATHINFO_BASENAME -> Return the base name
//PATHINFO_EXTENSION -> Return the file extension
//PATHINFO_FILENAME -> Return the file name (without the extension)

Regular Expressions



Common regex patterns


/				start
/ 				end
/g 				global
/gi 			case insensitive
/\d/g 			select single digit
/\d+/g 			select one or more digits
/\d{9}/g 		select 9 digits in a row
/e+/g 			one or more than one e
/a?/g 			a is optional | match a zero or one time.
/a*/g 			match 0 or more, and a is optional
/\d{n,}/        match at least n times
/\d{n,m}/       match Between n and m Times
/./g 			match anything except new line
/\. /g 			match . (dot)
/.\./g 			match any character before .
/\w/g 			match any word character , negative is /\W
/\s/g 			match and white space, negative is /\S
/\w{4}/g 		select any words with or more than 4 characters
/\w{4,5}/g 		select any words with 4 or 5 characters
/[fc]at/g 		words start with f or c but end with at
/[q-z,A-Z]at/g	any character followed by “at”
/(t|T)he/g 		match the words "the" or "The"
/(t|e|r){2,3}/g match the word like "street" "stttreet" "strrret"
/A(?=B)/        matches A only if followed by B
/A(?!B)/        matches A only if not followed by B
(?<!B)A         matches A only if there’s B before it
(?<!B)A         matches A only if there’s no B before it
/^I/g			select beginning with I for whole chunk
/\.$/g 			select end of line with . for whole statements
/\.$/gm 		select end of line with . for multiple line
/(?<=[tT]he)./g	match very first character after the/The
/(?<![tT]he)./g select anything that does not have the/The before
/.(?=at)/g 		select any character followed by at
/.(?!at)/g 		select everything that not followed by at

capturing groups and give names from groups


<?php

$uri = 'posts/25';
$pattern = '{(\w+)/(\d+)}';

if (preg_match($pattern, $uri, $matches)) {
    print_r($matches);
}

# give names for groups using ?<name>
$uri = 'posts/25';
$pattern = '{(?<controller>\w+)/(?<id>\d+)}';

if (preg_match($pattern, $uri, $matches)) {
    print_r($matches);
}
# capturing groups has a number -> 1,2, they can access later by $1, $2

PHP Date & Time


<?php

$current_time = time();
echo date('Y-m-d g:ia', $current_time) . '<br>'; #2021-07-13 5:47am

# adding timestamp
$one_week_later =  $current_time + 7 * 24 * 60 * 60; // 7 days later
echo date('Y-m-d g:ia',$one_week_later);

# subtracting timestamp
$yesterday = $current_time -  24 * 60 * 60; //1 day ago
echo date('Y-m-d g:ia',$yesterday);

# mktime
mktime(
    int $hour,
    int|null $minute = null,
    int|null $second = null,
    int|null $month = null,
    int|null $day = null,
    int|null $year = null
): int|false

date() function


<?php

echo date('Y'); //2025
$created_at = date("Y-m-d H:i:s");
echo $created_at;//2021-07-14 13:03:08 -> usefull for store in Mysql DB

OOP with PHP



Objects & Classes



Constructor and Destructor



constructor promotion (PHP 8.0)



Properties


public ?string $name = null;

Inheritance



Abstract classes



Abstract classes



Polymorphism



Traits



PHP trait’s method conflict resolution



Overriding trait method


trait FileLogger
{
	public function log($msg)
	{
		echo 'File Logger ' . date('Y-m-d h:i:s') . ':' . $msg . '<br/>';
	}
}

trait DatabaseLogger
{
	public function log($msg)
	{
		echo 'Database Logger ' . date('Y-m-d h:i:s') . ':' . $msg . '<br/>';
	}
}

class Logger
{
	use FileLogger, DatabaseLogger{
		FileLogger::log insteadof DatabaseLogger;
	}
}

$logger = new Logger();
$logger->log('this is a test message #1');
$logger->log('this is a test message #2');

Aliasing trait method


class Logger
{
	use FileLogger, DatabaseLogger{
		DatabaseLogger::log as logToDatabase;
		FileLogger::log insteadof DatabaseLogger;
	}
}

$logger = new Logger();
$logger->log('this is a test message #1');
$logger->logToDatabase('this is a test message #2');

Working with Objects



Serialization in PHP


Serialization is the process of converting a PHP value (object, array, etc.) into a storable string representation that can be saved to a file, database, or transmitted over a network. Unserialization (or deserialization) is the reverse process—reconstructing the original PHP value from that string.


Why Use Serialization?



serialize



unserialize



Complete Example: Session Cache


class UserSession {
    public $userId;
    public $username;
    public $loginTime;
    private $cache; // Large temporary data
    private $apiClient; // Non-serializable resource
    
    public function __construct($userId, $username) {
        $this->userId = $userId;
        $this->username = $username;
        $this->loginTime = time();
        $this->cache = []; // Loaded separately
        $this->apiClient = new ApiClient(); // Resource
    }
    
    public function __sleep() {
        // Save session to disk
        // Exclude cache (too large) and apiClient (resource)        
        // Could perform cleanup here
        $this->cache = null;        
        return ['userId', 'username', 'loginTime'];
    }
    
    public function __wakeup() {
        // Restore session from disk        
        // Reinitialize resources
        $this->cache = [];
        $this->apiClient = new ApiClient();
        
        // Validate session isn't expired
        if (time() - $this->loginTime > 3600) {
            throw new Exception('Session expired');
        }
    }
}

// Usage
$session = new UserSession(123, 'alice');
$_SESSION['user'] = serialize($session); // __sleep() called

// Later request
$session = unserialize($_SESSION['user']); // __wakeup() called

# after PHP 7.4
class ModernUser {
    private $data;
    
    public function __serialize(): array {
        // Return array of data to serialize
        return [
            'data' => $this->data,
            'timestamp' => time()
        ];
    }
    
    public function __unserialize(array $data): void {
        // Receive the serialized data
        $this->data = $data['data'];
        // Can use the timestamp for validation
    }
}

Clone Object



Shallow Copy


$bob = new Person('Bob');

// clone an object
$alex = clone $bob;
$alex->name = 'Alex';

// show both objects
var_dump($bob);
var_dump($alex);

# output
object(Person)#1 (1) {
  ["name"]=> string(3) "Bob"
}
object(Person)#2 (1) {
  ["name"]=> string(4) "Alex"
}

Deep copy with __clone method



Deep copy using serialize and unserialize functions


<?php

function deep_clone($object)
{
	return unserialize(serialize($object));
}

Compare Objects


$p1 = new Point(10, 20);
$p2 = new Point(10, 20);

var_dump($p1 == $p2);  // true  → same class, same property values
var_dump($p1 === $p2); // false → different instances

$p3 = $p1;

var_dump($p1 === $p3); // true  → same reference (identical instance)

Anonymous Class


<?php

$myObject = new class {}; //syntax

# Example
$logger = new class(true) extends SimpleLogger { //pass an argument, and extend from named abstract class
    public function __construct(bool $newLine)
    {
        parent::__construct($newLine);
    }
};

$logger->log('Hello');
$logger->log('Bye');

Namespaces and Autoloading



<?php

namespace Store\Model;
class Customer{}

#index.php
<?php

require 'src/Model/Customer.php';
use Store\Model\Customer;
$customer = new Customer('Bob');
# functions.php
<?php

function load_model($class_name)
{
	$path_to_file = 'models/' . $class_name . '.php';

	if (file_exists($path_to_file)) {
		require $path_to_file;
	}
}
spl_autoload_register('load_model');

# index.php
<?php

require 'functions.php';

$contact = new Contact('john.doe@example.com');

Composer Autoload


  1. classmap
  2. PSR-4
# composer.json
{
	"autoload": {
		"classmap": ["app/models", "app/services"]
    }
}

# run composer dump-autoload and it will generate /vendor/autoload.php
# You just can import /vendor/autoload.php in the index.php 

# index.php
<?php

require_once __DIR__ . '/../vendor/autoload.php';
$user = new User('admin', '$ecurePa$$w0rd1');
# composer.json
{
    "autoload": {
        "psr-4": {
            "Acme\\":"app/Acme"
        }
    }
}
# run the composer dump-autoload command to generate the autoload.php file
# index.php
<?php

require_once __DIR__ . '/../vendor/autoload.php';

use Acme\Auth\User as User;
$user = new User('admin', '$ecurePa$$w0rd1');