PHP Language Basics

Programming Paradigms In PHP

The goal is not to declare one style universally superior. Choose the smallest structure that makes current behavior and likely changes understandable. A paradigm is a tool for managing complexity, not a badge of maturity.

Procedural Code Makes Execution Order Visible

Procedural code runs statements step by step:

PHP example
<?php

declare(strict_types=1);

$products = [
    ['name' => 'Notebook', 'price_cents' => 1299],
    ['name' => 'Pen', 'price_cents' => 199],
];

$totalCents = 0;

foreach ($products as $product) {
    echo $product['name'], PHP_EOL;
    $totalCents += $product['price_cents'];
}

echo "Total: $totalCents cents\n";

This is appropriate for a small command whose sequence is the main idea: load input, process records, print a result.

Procedural does not mean unstructured. The script can use clear variables, guard clauses, typed helper functions, and explicit error handling. The problem begins when one long file combines unrelated validation, calculations, storage, network calls, and presentation with no named boundaries.

Extract A Function When A Rule Deserves A Name

A calculation can move into a function:

PHP example
<?php

declare(strict_types=1);

function totalCents(array $items): int
{
    $total = 0;

    foreach ($items as $item) {
        $total += $item['price_cents'];
    }

    return $total;
}

The caller still controls the workflow:

PHP example
$total = totalCents($products);
echo "Total: $total cents\n";

Extraction is useful when behavior repeats, needs focused testing, or obscures the higher-level flow. Do not create a function merely to move one obvious line elsewhere. A good helper adds vocabulary or isolates a meaningful policy.

Functional Style Emphasises Input And Output

Functional-style PHP favors functions that receive all required data and return new values without changing hidden state.

PHP example
<?php

declare(strict_types=1);

function productNames(array $products): array
{
    return array_map(
        fn (array $product): string => $product['name'],
        $products
    );
}

Given the same input, this function returns the same output. It does not print, mutate a global, or depend on call history. That predictability makes it easy to test and combine.

PHP is not a purely functional language, and application code must eventually perform side effects such as reading files, querying databases, and sending responses. The useful habit is to keep calculations and transformations separate from those effects where practical.

Build A Transformation Pipeline Carefully

Array functions can express a sequence of transformations:

PHP example
<?php

declare(strict_types=1);

$products = [
    ['name' => 'Notebook', 'price_cents' => 1299, 'active' => true],
    ['name' => 'Pen', 'price_cents' => 199, 'active' => false],
    ['name' => 'Mug', 'price_cents' => 899, 'active' => true],
];

$activeProducts = array_filter(
    $products,
    fn (array $product): bool => $product['active']
);

$activeNames = array_map(
    fn (array $product): string => $product['name'],
    array_values($activeProducts)
);

print_r($activeNames);

Each step has a defined input and result. array_values() repairs the list keys preserved by array_filter().

Do not compress a long business workflow into a nested expression merely to look functional. Named intermediate variables reveal data shape and make debugging easier.

Separate Pure Logic From Side Effects

This function calculates a label:

PHP example
<?php

declare(strict_types=1);

function formatCents(int $cents): string
{
    return 'GBP ' . number_format($cents / 100, 2, '.', ',');
}

Printing is a side effect owned by the caller:

PHP example
$label = formatCents(1299);
echo $label, PHP_EOL;

Other common side effects include changing a file, database, session, cache, global variable, or remote service. Side effects are necessary, but they are easier to reason about when concentrated at visible boundaries rather than mixed into every calculation.

A practical command often has a functional core and an imperative shell: the outer workflow performs input and output, while inner functions validate and transform values.

Closures Configure Small Behavior

A closure can carry configuration into a callback:

PHP example
<?php

declare(strict_types=1);

$minimumPriceCents = 500;

$isEligible = fn (array $product): bool =>
    $product['price_cents'] >= $minimumPriceCents;

$eligibleProducts = array_filter($products, $isEligible);

The callback describes one selection rule and captures the threshold by value. This is useful for local transformations.

If many operations need the same growing set of state and rules, passing several values into many closures may become harder to manage. That can be a signal to consider a different structure, including an object later.

Object-Oriented Code Groups State And Behavior

An object represents a concept with state and operations that belong together. A minimal shape might be:

PHP example
<?php

declare(strict_types=1);

final class Basket
{
    public function __construct(private array $items)
    {
    }

    public function totalCents(): int
    {
        return totalCents($this->items);
    }
}

The point is not the class syntax; the later OOP track teaches classes, properties, visibility, interfaces, composition, and architecture in depth.

The design pressure is what matters. A class may help when a basket's items and several related rules must travel together, invalid state must be prevented, or different collaborators operate on the same concept.

For one calculation over one passed array, a function is usually simpler.

State Changes The Tradeoff

Stateless helpers are straightforward: arguments enter and a result leaves. Stateful behavior remembers earlier actions.

A basket that supports adding items, removing items, applying discounts, and calculating shipping has state transitions and invariants. An object may provide one owner for that state.

A static variable or global array can also retain state, but ownership is hidden. Tests and callers may affect one another. Prefer explicit state passed between functions or encapsulated deliberately rather than shared process state.

Declarative And Imperative Descriptions

Imperative code says how to perform each step:

PHP example
$names = [];

foreach ($products as $product) {
    $names[] = $product['name'];
}

A more declarative transformation says what result is wanted:

PHP example
$names = array_column($products, 'name');

Neither is always better. The loop handles complex branching and diagnostics naturally. The array function is concise for a standard transformation. Choose the version that exposes the rule and edge cases most clearly.

PHP Applications Commonly Mix Styles

A realistic application may use:

  • a procedural front controller or CLI entry point;
  • function-based validation and formatting;
  • objects for domain concepts and infrastructure;
  • closures for callbacks and configuration;
  • declarative framework routes or validation rules.

Consistency matters within a local responsibility, but forcing the entire system into one paradigm can create awkward code. Evaluate dependencies, state, testability, and change pressure.

Refactor In Response To Evidence

Signals that a procedural block may need helper functions:

  • the same rule appears more than once;
  • a block has a clear name and inputs;
  • tests need to exercise the rule without running the whole script;
  • the top-level workflow is obscured by low-level detail.

Signals that related functions and data may need an object later:

  • many functions accept the same state repeatedly;
  • valid state must be protected across operations;
  • several behaviors belong to one domain concept;
  • interchangeable implementations need a shared contract.

Signals that an abstraction is premature:

  • there is one short use case;
  • future variation is only imagined;
  • the abstraction has a vague name such as Manager or Processor;
  • understanding the code now requires more jumping than before.

Evaluate A Design With Concrete Questions

Ask:

  1. Where does each input come from?
  2. Which function or object owns each rule?
  3. Which values can change over time?
  4. Where do side effects occur?
  5. Can the core rule be tested without unrelated output or storage?
  6. Does the chosen structure reduce duplication or clarify an invariant?
  7. What specific next change becomes easier?

If the answer to the last question is unclear, a more elaborate paradigm may not be justified.

Common Paradigm Mistakes

Symptom Likely cause
One script mixes every responsibility Procedural flow lacks named boundaries
Helper is difficult to test Calculation performs unrelated side effects
Array pipeline is unreadable Too many transformations were nested
Many functions pass the same growing state Ownership may need a stronger structure
Tiny script has many classes Object model was introduced before design pressure
Globals coordinate behavior State ownership is hidden
Refactor changes output unexpectedly No behavior checks protected the transformation
Abstraction has a vague name It does not represent a stable responsibility

What You Should Be Able To Do

After this lesson, you should be able to recognise procedural, functional-style, declarative, and object-oriented shapes; extract a named helper for a real rule; keep pure transformations separate from output and storage effects; use readable array pipelines; identify when state ownership creates pressure for an object; explain why a simple script may remain procedural; and justify a refactor with a concrete change it makes safer or clearer.

A good choice is explainable in terms of current behavior, owned state, visible dependencies, and a specific maintenance cost it avoids. The later object-oriented track teaches class mechanics and architecture. This lesson's purpose is to help you choose structures based on evidence rather than perceived sophistication.

Practice

Task: Choose A Shape

Task

For each situation, choose procedural script, function, or class candidate. Give one concrete reason based on repetition, state, or change pressure.

  1. A one-off CLI command prints three hard-coded product names.
  2. One price-formatting rule appears in five reports.
  3. A basket must add and remove items while preserving a running collection for several operations.
  4. One script maps an array of prices to formatted labels.
  5. A team proposes ProductManager for a single function that uppercases one SKU.

For case 5, explain why the proposed abstraction is or is not justified.

Show solution

Solution

  1. Procedural script: the short sequence is the whole use case and has no repeated rule.
  2. Function: one named formatter removes meaningful duplication and can be tested directly.
  3. Class candidate: item state and several related operations need one explicit owner; detailed class design comes later.
  4. Function: a transformation from one array to another needs no retained state.
  5. Function: a ProductManager class would add indirection without owning state or several related behaviors.

Explanation

The smallest adequate structure wins. Repetition creates pressure for a helper, while related mutable state can create pressure for an object. A vague class around one operation does not improve ownership or express a domain concept.

Task: Pick A Clear Style

Task

Build a short inventory report from this hard-coded list:

PHP example
$products = [
    ['sku' => 'BOOK-001', 'name' => 'Workbook'],
    ['sku' => 'PEN-002', 'name' => 'Pen'],
];

Use a procedural foreach loop for control flow and a function formatInventoryLine(array $product): string for formatting. Print:

BOOK-001 | Workbook
PEN-002 | Pen

After the code, write one sentence naming a concrete future change that could justify considering a class. Do not create a class for the current requirement.

Show solution

Solution

PHP example
<?php

declare(strict_types=1);

function formatInventoryLine(array $product): string
{
    return $product['sku'] . ' | ' . $product['name'];
}

$products = [
    ['sku' => 'BOOK-001', 'name' => 'Workbook'],
    ['sku' => 'PEN-002', 'name' => 'Pen'],
];

foreach ($products as $product) {
    echo formatInventoryLine($product), PHP_EOL;
}

A class might become worth evaluating if a product must preserve valid state while supporting several related operations such as repricing, stock changes, and availability checks.

Explanation

The loop keeps the two-step workflow visible, while the formatter names the only reusable rule. A class would not simplify this static report today.

The future condition is concrete and state-related. It provides an evidence-based reason to revisit the shape rather than introducing an object pre-emptively.

Task: Refactor To Function

Task

Write totalCents(array $items): int and use it for two different baskets:

PHP example
$officeItems = [
    ['name' => 'Notebook', 'price_cents' => 1299],
    ['name' => 'Pen', 'price_cents' => 199],
];

$kitchenItems = [
    ['name' => 'Mug', 'price_cents' => 899],
];

The helper must return a total without printing or changing either array. Print:

Office total: 1498
Kitchen total: 899
Office item count: 2

Hints

  • Initialise a local accumulator inside the function.
  • Pass each array as an argument.
  • count($officeItems) after both calls verifies the original list still has two records.
Show solution

Solution

PHP example
<?php

declare(strict_types=1);

function totalCents(array $items): int
{
    $total = 0;

    foreach ($items as $item) {
        $total += $item['price_cents'];
    }

    return $total;
}

$officeItems = [
    ['name' => 'Notebook', 'price_cents' => 1299],
    ['name' => 'Pen', 'price_cents' => 199],
];

$kitchenItems = [
    ['name' => 'Mug', 'price_cents' => 899],
];

echo 'Office total: ' . totalCents($officeItems) . "\n";
echo 'Kitchen total: ' . totalCents($kitchenItems) . "\n";
echo 'Office item count: ' . count($officeItems) . "\n";

Explanation

One named calculation now serves both callers. Its accumulator is local, and the function returns rather than printing, so it has no output side effect.

The final count confirms the helper did not remove or append office items. Given the same item arrays, it produces the same totals.