Based on laravel.com/learn/php-fundamentals PHP Version: 8.5.4 (Homebrew) All examples tested and verified in PhpStorm
A variable stores data that we can use later while the program is running.
Always starts with a $ dollar sign.
$status = 404;
$name = "Sharif";| Type | Description | Example |
|---|---|---|
int |
Whole numbers | $age = 30; |
float |
Decimal numbers | $price = 9.99; |
string |
Text | $name = "Sharif"; |
bool |
True or false | $isActive = true; |
null |
Exists but has no value | $data = null; |
array |
Collection of values | $colors = ['red', 'blue']; |
object |
Instance of a class | $user = new User(); |
PHP figures out the type automatically — you do not declare it. The same variable can hold different types at different times.
$data = "hello"; // String
$data = 3.14; // Float
$data = true; // BooleanDouble quotes allow string interpolation — variables are replaced with their values. Single quotes treat everything as plain text.
$animal = 'cat';
echo "It's a big $animal"; // Output: It's a big cat
echo 'It\'s a big $animal'; // Output: It's a big $animalUse curly braces when variable is next to other text with no space:
echo "It's a big {$animal}s"; // Output: It's a big catsPHP automatically converts types based on the operator used.
$status = "404" + 4; // Output: 408 (+ makes PHP think "number")
$status = 404 . "4"; // Output: "4044" (. makes PHP think "text")declare(strict_types=1). PHP 7 was completely silent.
Enforces strict types when calling functions with type declarations. Does NOT stop type juggling in plain arithmetic.
declare(strict_types=1);
function addNumbers(int $a, int $b): int {
return $a + $b;
}
addNumbers("5", 10); // TypeError in strict mode ❌Values that cannot change during execution. No dollar sign. Written in ALL_CAPS.
const PI = 3.14; // Modern way — preferred in PHP 8
define('PI', 3.14); // Older way — also valid
// Typed constant — PHP 8.3+
const float PI = 3.14159265358979323846;| Variable | Constant | |
|---|---|---|
| Syntax | $PI = 3.14 |
const PI = 3.14 |
| Can change? | ✅ Yes | ❌ Never |
| Dollar sign? | ✅ Yes | ❌ No |
| Naming style | $camelCase |
ALL_CAPS |
A variable that stores a collection of related data. Elements are accessed via an index starting at zero.
$colors = ['red', 'green', 'blue'];
echo $colors[0]; // red
echo $colors[1]; // green
echo $colors[2]; // blueUses named keys instead of numbers.
Key and value are connected with the => arrow operator.
$user = [
'name' => 'Sharif',
'age' => 30,
'city' => 'Dallas',
];
echo $user['name']; // Sharif
echo $user['age']; // 30| Type | Key | Access |
|---|---|---|
| Regular | Number (auto) 0, 1, 2 |
$colors[0] |
| Associative | Word (you choose) | $user['name'] |
A reusable piece of code that avoids repetition and organises logic.
function greet(string $name): string {
return "Hello $name!";
}
echo greet('Sharif'); // Hello Sharif!Always declare types for parameters and return values in real applications.
function addNumbers(int $a, int $b): int {
return $a + $b;
}| Part | Meaning |
|---|---|
int $a |
Parameter must be an integer |
: int |
Function must return an integer |
void |
Function returns nothing |
| What it does | |
|---|---|
echo |
Prints directly to screen, function gives nothing back |
return |
Gives the value back so caller can use it |
Prefer return in real applications — gives flexibility.
Allow parameters to have a fallback value when nothing is passed. Default arguments must always come at the end of the parameter list.
function greet(int $age, string $name = 'you'): string {
return "Hello $name you are $age!";
}
echo greet(30); // Hello you you are 30!
echo greet(30, 'Sharif'); // Hello Sharif you are 30!Pass arguments in any order using the parameter name. Types are declared in the function definition, not at the call site.
function createUser(string $name, int $age, string $city): void {
echo "$name is $age years old from $city";
}
// Order does not matter with named arguments
createUser(age: 30, city: 'Dallas', name: 'Sharif');Repeats an action — used to avoid writing the same code multiple times, and to work through collections of data.
Designed specifically for iterating over arrays and collections. PHP handles the index automatically — no manual management needed.
$colors = ['red', 'blue', 'green'];
foreach ($colors as $color) {
echo $color;
}
// With associative arrays
$user = ['name' => 'Sharif', 'age' => 30];
foreach ($user as $key => $value) {
echo "$key: $value";
}Best when you know exactly how many times to repeat.
$numbers = [1, 2, 3, 4, 5];
$total = count($numbers); // calculate once, reuse — more efficient
for ($i = 0; $i < $total; $i++) {
echo $numbers[$i];
}Best when you repeat until something changes but do not know how many times.
$i = 0;
while ($i < count($numbers)) {
echo $numbers[$i];
$i++;
}Runs at least once even if the condition is false from the start.
$i = 0;
do {
echo $numbers[$i];
$i++;
} while ($i < count($numbers));| Loop | Best Used When |
|---|---|
foreach |
Looping over arrays and collections |
for |
You know the exact number of iterations |
while |
Repeat until something changes |
do-while |
Must run at least once |
A blueprint with properties (data) and methods (behaviour). An object is a created instance of a class.
class House {
public function __construct(
private string $address,
private float $price,
) {}
public function getDescription(): string {
return "$this->address costs $this->price";
}
}
$house = new House('123 Main St', 250000.00);
echo $house->getDescription();| Your Words | Technical Term |
|---|---|
| Data stored in a class | Properties |
| Functions inside a class | Methods |
| Creating an instance | Instantiation |
| The created instance | Object |
Declare and assign properties directly in the constructor. Much cleaner.
// Old way ❌ — verbose
class House {
private string $address;
private float $price;
public function __construct(string $address, float $price) {
$this->address = $address;
$this->price = $price;
}
}
// Modern PHP 8 way ✅ — clean
class House {
public function __construct(
private string $address,
private float $price,
) {}
}| Modifier | Accessible From |
|---|---|
public |
Anywhere — inside, outside, subclasses |
protected |
Only inside the class and its subclasses |
private |
Only inside the class itself |
class BankAccount {
private float $balance; // nobody touches this directly
public function deposit(float $amount): void {
$this->balance += $amount; // controlled access
}
}Refers to the current object. Used to access the class's own properties and methods.
class House {
private string $address;
public function __construct(string $address) {
$this->address = $address; // $this->address = the property
// $address = the parameter
}
}Once set in the constructor, the value can never be changed. Prevents accidental modification — immutable objects.
class House {
public function __construct(
private readonly string $address,
private readonly float $price,
) {}
}Getters — allow outside code to read a private property. Setters — allow outside code to change a private property.
// Useful setter — has validation
public function setPrice(float $price): void {
if ($price < 0) {
throw new InvalidArgumentException('Price cannot be negative');
}
$this->price = $price;
}| Approach | When to Use |
|---|---|
| Getter only | Outside code needs to read but not change |
| Setter with validation | Outside code needs to change but with rules |
| No setter at all | Object should not change after creation |
| Plain getter/setter with no logic | Avoid — adds no value |
Allows a class to extend another class and inherit its properties and methods. Used for specialisation — when something is a more specific version of a generic type.
class Duck {
public function __construct(
private string $name,
private string $color
) {}
public function fly(): string {
return 'I can fly';
}
}
class NonFlyingDuck extends Duck {
public function fly(): string { // Method overriding
return 'I cannot fly';
}
}
$mallard = new Duck('Mallard', 'Black');
$rubber = new NonFlyingDuck('Rubber Duck', 'Yellow');
echo $mallard->fly(); // I can fly
echo $rubber->fly(); // I cannot flyRedefining a parent method in a child class to change its behaviour.
Same method name, different behaviour depending on the object. This is the power of inheritance — one loop, many behaviours.
$shapes = [
new Circle(3.5),
new Rectangle(10, 20),
new Circle(5.0),
];
foreach ($shapes as $shape) {
echo $shape->getArea(); // each calculates differently
}Use parent:: to call the parent class version of a method.
class NonFlyingDuck extends Duck {
public function getDescription(): string {
return parent::getDescription() . " (non-flying)";
}
}Constants with a declared type. Use self:: to reference inside the class.
class Circle extends Shape {
private const float PI = 3.14159265358979323846;
private const string SHAPE = 'Circle';
public function getArea(): float {
return self::PI * $this->radius * $this->radius;
}
}The correct cross-platform way to add a new line. Works on Mac, Windows, Linux.
Prefer over hardcoded "\n".
echo "Hello" . PHP_EOL;Declare a type directly on a property so it can only ever hold that type. Untyped properties accept anything — typed properties throw an error if wrong type assigned.
class User {
// Typed — safe and self-documenting ✅
private string $name;
private int $age;
private float $price = 0.0;
// Untyped — accepts anything ⚠️
private $anything;
}
$user->name = 123; // ❌ TypeError — must be string
$user->age = "old"; // ❌ TypeError — must be int| Untyped | Typed | |
|---|---|---|
| Wrong data assigned | Silent — no error | Throws immediately |
| Readability | Hard to know what is expected | Self documenting |
| PhpStorm support | Limited autocomplete | Full autocomplete |
Cleaner and safer replacement for switch.
Returns a value directly. Uses strict comparison ===.
Throws UnhandledMatchError if no match found and no default.
$result = match($statusCode) {
200 => 'ok',
400, 401 => 'error', // multiple conditions on one line
default => 'unknown'
};switch |
match |
|
|---|---|---|
| Comparison | Loose == — type juggling |
Strict === — type safe |
| Fall-through | Yes — forgot break = bug |
No — impossible |
| Returns value | No — needs extra variable | Yes — directly |
| No match found | Silent — does nothing | Throws error immediately |
| Verbosity | Many lines | Very concise |
// switch — risky ❌
switch ($color) {
case 'red':
$hex = '#FF0000';
break;
case 'green':
$hex = '#00FF00';
break;
default:
$hex = '#000000';
break;
}
// match — clean and safe ✅
$hex = match($color) {
'red' => '#FF0000',
'green' => '#00FF00',
default => '#000000',
};💡 Rule: In PHP 8.x — when you feel like writing
switch, writematchinstead.
Safely chain method calls on values that might be null. If any part of the chain returns null, the whole expression returns null — no error.
The problem without null safe operator:
// Verbose and ugly ❌
$planet = $spaceship->findPlanet();
if ($planet !== null) {
$result = $planet->desc();
} else {
$result = null;
}With null safe operator:
// Clean and safe ✅
$result = $spaceship->findPlanet()?->desc();Combined with null coalescing ??:
// If null at any point, use fallback value
$result = $spaceship->findPlanet()?->desc() ?? 'No planet found';A ? before a return type means the function can return that type OR null.
public function findPlanet(): ?Planet // returns Planet or null
{
if ($found) {
return new Planet(...);
}
return null;
}This is what makes the null safe operator ?-> necessary — when a method
can return null, you need a safe way to chain calls on its result.
Makes every property in the class readonly automatically.
No need to add readonly to each property individually.
// PHP 8.1 — readonly per property
class Planet {
public function __construct(
private readonly bool $hasAlien,
private readonly bool $isSafe,
) {}
}
// PHP 8.2 — readonly the whole class ✅ cleaner
readonly class Planet {
public function __construct(
private bool $hasAlien,
private bool $isSafe,
) {}
}Already covered in Chapter 5 — included here as it was part of this lesson too. Declare, type, and assign properties all in the constructor signature.
class House {
public function __construct(
private string $address,
private float $price,
) {}
}Already covered in Chapter 3 — included here as it was part of this lesson too. Pass arguments in any order using the parameter name.
createUser(age: 30, city: 'Dallas', name: 'Sharif');A tool that downloads and manages code written by other developers so you do not have to write everything yourself. Every language has one.
| Language | Package Manager | Package Repository |
|---|---|---|
| PHP | Composer | packagist.org |
| JavaScript | npm / yarn | npmjs.com |
| Python | pip | pypi.org |
| Ruby | Bundler | rubygems.org |
| Rust | Cargo | crates.io |
Code someone else already wrote that solves a common problem. Instead of writing it yourself, you declare "I need this" and Composer downloads it.
Examples:
guzzlehttp/guzzle— makes HTTP calls to APIs cleanlyphpunit/phpunit— runs your automated testslaravel/framework— the entire Laravel framework
Job 1 — Downloads packages You declare what you need, Composer fetches it from packagist.org automatically.
Job 2 — Manages versions Tracks exactly which version of each package you use so the project works the same on every computer.
Job 3 — Autoloading
Maps all your classes to their file locations so PHP finds them automatically — no manual require statements ever.
{
"name": "sharif/my-project",
"require": {
"guzzlehttp/guzzle": "^7.0"
},
"require-dev": {
"phpunit/phpunit": "^10.0"
},
"autoload": {
"psr-4": {
"App\\": "src/"
}
}
}| Section | Plain English |
|---|---|
require |
Packages needed everywhere — production and local |
require-dev |
Packages only needed on your computer for development |
autoload |
Tells Composer where your PHP files live |
require vs require-dev example:
| Package | Where Needed |
|---|---|
| Guzzle — makes HTTP calls | Production ✅ users need this |
| PHPUnit — runs tests | Development only ❌ users never run tests |
When Composer downloads packages it puts them all in vendor/.
Never touch this folder manually — Composer owns it completely.
If deleted by accident just run composer install to rebuild it entirely.
| Command | When to Use |
|---|---|
composer install |
First setup, or when new packages were added to composer.json |
composer require package/name |
Add a new package to your project |
composer dump-autoload |
Rebuild class map after new PHP files added |
composer update |
Upgrade package versions |
composer self-update |
Update Composer itself |
Without autoloading you had to manually tell every file about every other file:
// Old way — nightmare ❌
require 'Models/User.php';
require 'Models/House.php';
require 'Services/PaymentService.php';
// ... every single file, every single timeIn a project with hundreds of files this becomes impossible to maintain. Move one file and everything breaks.
One simple agreement — name your files and classes consistently and PHP finds everything automatically. No require statements ever.
// Modern way with Composer autoloading ✅
use App\Models\User;
$user = new User(); // PHP finds User.php automaticallyComposer scans your project and builds a map of every class and where it lives. This map is stored in:
vendor/composer/autoload_classmap.php
It looks like this internally:
return array(
'App\\Models\\User' => '/src/Models/User.php',
'App\\Models\\House' => '/src/Models/House.php',
'App\\Services\\Payment' => '/src/Services/PaymentService.php',
);When PHP needs a class it looks up this map and finds the file instantly.
Rebuilds the autoload map completely from scratch.
Why you need it at work:
When you pull new code from your team (git pull), teammates may have added new PHP files. The old cached map does not know about them yet.
git pull origin integration # New PHP files arrive on your machine
composer dump-autoload # Rebuild the map to include new filesWithout dump-autoload — PHP crashes trying to find classes not in the old map.
When to use which command:
| Command | When |
|---|---|
composer install |
New packages were added to composer.json |
composer dump-autoload |
New PHP files were added but no new packages |
Without namespaces, two classes with the same name crash your application.
Imagine two files both containing class User — PHP has no idea which one to use.
Namespaces give every class a unique full address — like a postal address for your code.
User ← just a name, could be anyone
App\Models\User ← unique full address, only one exists
App\Api\User ← different address, completely different class
Always declared as the first line after <?php.
<?php
namespace App\Models; // ← full address of this class
class User {
// ...
}PSR-4 is simply an agreement that says:
Your namespace must mirror your folder structure.
| File Location | Namespace | Class Name |
|---|---|---|
src/Models/User.php |
App\Models |
User |
src/Controllers/HomeController.php |
App\Controllers |
HomeController |
src/Services/PaymentService.php |
App\Services |
PaymentService |
The mapping is defined in composer.json:
"autoload": {
"psr-4": {
"App\\": "src/"
}
}Plain English: "Replace src/ with App\ when building namespaces."
Rule 1 — File name must equal Class name
User.php → class User
HomeController.php → class HomeController
Rule 2 — Namespace must equal Folder path
src/Models/ → namespace App\Models
src/Controllers/ → namespace App\Controllers
Rule 3 — Declare namespace at top of every file
<?php
namespace App\Models; // always first line after <?php
class User {
}Ask yourself two questions every time you create a new PHP file:
Q1 — Where did I save this file?
src/Services/Payment/PaymentService.php
Q2 — Does my namespace mirror that path?
namespace App\Services\Payment; // ✅ mirrors the folder path perfectlyIf both match — PSR-4 is correct. No warnings. Composer finds the class automatically.
When your IDE says "namespace does not follow PSR-4" it means one thing:
Your namespace does not match where your file actually lives.
You have two ways to fix it:
Fix 1 — Move the file to match the namespace
namespace App\Models\PaymentService
→ move file to src/Models/PaymentService.php
Fix 2 — Change the namespace to match the file location
File is in src/Services/
→ change namespace to App\Services
Fix whichever is easier — just make them match.
use App\Models\User;
$user = new User();- PHP sees
App\Models\Userand asks Composer "where is this?" - Composer looks in
autoload_classmap.phpand finds the path - PHP opens
src/Models/User.phpautomatically - PHP finds
class Userinside and uses it $user = new User()works perfectly ✅
<?php
namespace App\Http\Controllers; // file lives in app/Http/Controllers/
use App\Models\User; // needs User from app/Models/User.php
use Illuminate\Http\Request; // needs Request from vendor/ package
class HomeController extends Controller // file must be named HomeController.php
{
}Every line now has meaning. Laravel follows PSR-4 perfectly — every file, every folder.
Namespace = Folder path
Class name = File name
composer.json = Tells Composer where src/ starts
dump-autoload = Rebuilds map after new files added
Namespaces are postal addresses for your classes. PSR-4 is the rule that says your address must match where you actually live.
| Feature | PHP Version | What it Does |
|---|---|---|
| Constructor property promotion | 8.0 | Declare and assign in one line |
| Named arguments | 8.0 | Pass args in any order by name |
Null safe operator ?-> |
8.0 | Safe chaining on nullable values |
Nullable types ?Type |
8.0 | Return type or null |
| Match expression | 8.0 | Safe strict replacement for switch |
| Typed properties | 8.0 | Properties locked to one type |
| Automatic non-numeric warning | 8.0 | Warns on bad arithmetic |
readonly properties |
8.1 | Immutable after construction |
readonly class |
8.2 | All properties readonly automatically |
| Typed class constants | 8.3 | Constants with declared types |
Date: April 2026 | Student: Sharif
Composer is PHP's package manager. It manages your project's external libraries.
Running composer init creates composer.json — your project's recipe file.
Running composer install downloads packages into the vendor/ folder and creates composer.lock.
"require": {
"guzzlehttp/guzzle": "^7.10"
},
"require-dev": {
"phpunit/phpunit": "^13.1"
}| Section | When installed | Use for |
|---|---|---|
require |
Local AND production | Packages the app needs to run |
require-dev |
Local only | Testing tools, debuggers |
In production, run composer install --no-dev to skip dev packages — faster, smaller, more secure.
Composer generates vendor/autoload.php — a class map that links namespaces to file paths based on PSR-4 rules in composer.json.
require_once __DIR__ . "/vendor/autoload.php";This one line tells PHP: "Whenever I use a class, find it automatically using Composer's map."
Without it, every new WeatherService() would fail with "class not found."
"autoload": {
"psr-4": {
"Sudiptasharif\\WeatherApp\\": "src/"
}
}This maps the namespace Sudiptasharif\WeatherApp to the src/ folder.
readonly class WeatherService { }- Makes all properties immutable — they cannot be changed after the constructor sets them
- Safe, predictable, communicates intent clearly
- Trying to change a property after construction throws an error
public function __construct(
private string $apiKey = 'your-key',
private string $apiUrl = 'https://...'
) {}The old way required 3 steps:
- Declare the property
- Accept it as a constructor parameter
- Assign it with
$this->property = $value
Constructor promotion does all 3 in one line.
Guzzle is an HTTP client library that makes calling APIs clean and easy.
$client = new Client();
$response = $client->get($this->apiUrl, [
'query' => [
'q' => $city,
'appid' => $this->apiKey,
'units' => 'metric',
]
]);The query array automatically builds URL parameters — cleaner than building URL strings manually.
APIs return data as a JSON string. You must decode it into a PHP array to use it.
$data = json_decode($response->getBody()->getContents(), true);| Call | Returns |
|---|---|
json_decode($json) |
PHP object — access with -> |
json_decode($json, true) |
PHP associative array — access with [] |
The true argument is essential when you want to use array syntax like $data['name'].
Important: getContents() not getContent() — easy mistake to make with Guzzle.
These are global PHP variables automatically available in CLI scripts.
| Variable | What it contains |
|---|---|
$argv |
Array of all command line inputs |
$argc |
Count of how many arguments were passed |
php weather-app.php Irving$argv[0] // "weather-app.php" — always the script name
$argv[1] // "Irving" — first user argument
$argc // 2 — script name + one argumentif ($argc < 2) {
echo "Usage: php weather-app.php <city>" . PHP_EOL;
exit(1);
}Always validate before using $argv[1] — without this the app crashes if no city is provided.
Throwable ← catches EVERYTHING
├── Exception ← expected problems (API failures, invalid input)
└── Error ← serious PHP problems (null pointer, fatal errors)
Using catch (Throwable $e) is broader and safer than catch (Exception $e).
} catch (Throwable $e) {
echo 'Error: Unable to Process Request.' . PHP_EOL; // user sees this
error_log($e->getMessage()); // developer sees this
}error_log() sends to different places depending on the environment:
| Environment | Destination |
|---|---|
| Terminal / CLI | Prints to terminal |
| Apache server | /var/log/apache2/error.log |
| Nginx server | /var/log/nginx/error.log |
| Production (Paycom) | Configured in php.ini |
Key principle: Show users a friendly message. Log the real technical error for developers.
// Bad — hardcoded in class
private string $apiKey = 'xxxxxxxxxxxxxxxxxxxxxxxx'
// Good — loaded from .env file
private string $apiKey = ''
// In .env file (never committed to git)
WEATHER_API_KEY=xxxxxxxxxxxxxxxxxxxxUse vlucas/phpdotenv package to load .env variables.
// Bad — new client created every method call, hard to test
public function getWeather(string $city): array {
$client = new Client();// Good — injected once, reusable, testable
private Client $client;
public function __construct(...) {
$this->client = new Client();
}if ($argc < 2) {
echo "Usage: php weather-app.php <city>" . PHP_EOL;
exit(1);
}Packages like symfony/console provide colored output, formatted tables, progress bars, and proper input handling for command line apps.
- Used proper namespace:
Sudiptasharif\WeatherApp - Used
readonly class— modern PHP 8.2+ feature - Used constructor property promotion correctly
- Added
try/catchwithThrowable— went beyond the tutorial - Added
countryto output — explored the API response independently - Used
PHP_EOLfor cross-platform compatibility - Added PHPUnit as a dev dependency — prepared project for testing
- Separated user-facing error message from technical error log
| Term | Meaning |
|---|---|
| Composer | PHP's package/dependency manager |
| PSR-4 | A standard rule for how namespaces map to file paths |
| Autoloading | Automatically loading class files without manual require |
| Guzzle | PHP HTTP client library for making API calls |
| JSON | Data format APIs use — must be decoded to use in PHP |
| Associative array | PHP array with named keys like $data['name'] |
| readonly | PHP keyword making properties immutable after construction |
| Constructor promotion | Declaring, receiving, and assigning properties in one step |
| Throwable | PHP interface that catches both Exceptions and Errors |
| $argv | Global array of command line arguments |
| $argc | Count of command line arguments passed |
| error_log() | Sends error messages to PHP's error log |
| require-dev | Composer packages only installed in development, not production |
Notes compiled after completing the PHP Fundamentals course final project. Project: CLI Weather App using Composer + Guzzle HTTP + OpenWeather API
Notes by Sharif — learning Laravel the right way, understanding before building.