Foundation · 45 MINUTES · VERSION 1
PHP functions, validation, and useful errors
Build a reusable task validator, return accepted fields or clear errors, and keep display and storage outside the function.
Reviewed 23 Oct 2026Your setup may differ. Windows, Ubuntu, and optional tool bundles →
What you will learn
- Define a function with an input parameter and a returned result.
- Separate calculation from display and storage.
- Validate unknown task data and return useful field errors.
Theory: one named job
A function gives a name to a piece of work. A parameter is its named input; an argument is the value you pass when calling it. return gives a result to the caller and leaves that function. Printing with echo sends text to output; it is not the same as returning a value.
Keep validation separate from display and storage. A function that returns a structured result can be called from a CLI program, tested with dummy data, or adapted to an application. It should not silently create a database record while checking input. In Laravel request handlers, use framework validation rather than replacing it with a custom validator from this exercise.
An outer array declaration checks only that the value is an array; it does not guarantee the types of every field. JSON may decode to an object-shaped array, a list, a string, a number, or null. This lesson accepts unknown input as mixed and checks the shape deliberately. It returns an array with either a normalized task or errors.
Worked example: validate a proposed task
Required: PHP 8.4 or a compatible newer runtime. No database, framework, server, or Python is needed. Save as task_validation.php in your practice folder:
<?php
declare(strict_types=1);
function validateTask(mixed $input): array
{
if (! is_array($input)) {
return ['task' => null, 'errors' => ['task' => 'Provide a task object.']];
}
$errors = [];
$title = $input['title'] ?? null;
$status = $input['status'] ?? null;
if (! is_string($title)) {
$errors['title'] = 'Title must be text.';
} else {
$title = trim($title);
if ($title === '' || strlen($title) > 80) {
$errors['title'] = 'Title must contain 1 to 80 bytes after trimming.';
}
}
if (! is_string($status) || ! in_array($status, ['todo', 'in_progress', 'done'], true)) {
$errors['status'] = 'Status must be todo, in_progress, or done.';
}
if ($errors !== []) {
return ['task' => null, 'errors' => $errors];
}
return ['task' => ['title' => $title, 'status' => $status], 'errors' => []];
}
$inputs = [
['title' => ' Write tests ', 'status' => 'todo', 'is_admin' => true],
['title' => ' ', 'status' => 'blocked'],
'not a task',
];
foreach ($inputs as $index => $input) {
$result = validateTask($input);
echo 'Case '.($index + 1).': '.json_encode($result, JSON_THROW_ON_ERROR).PHP_EOL;
}
Run php task_validation.php in Windows PowerShell/Command Prompt or Ubuntu Bash from your practice folder. Use /getting-started to select your installed PHP executable if needed. Laragon/XAMPP are optional, and the PHP file is identical on these systems.
Expected output:
Case 1: {"task":{"title":"Write tests","status":"todo"},"errors":[]}
Case 2: {"task":null,"errors":{"title":"Title must contain 1 to 80 bytes after trimming.","status":"Status must be todo, in_progress, or done."}}
Case 3: {"task":null,"errors":{"task":"Provide a task object."}}
The function's variables are local to that call. $errors starts empty every time. A failed input returns no task, so a caller cannot mistake partly valid fields for a fully accepted record. Valid input returns a newly constructed array containing only allowed fields. The extra is_admin field never reaches that result. This illustrates selecting accepted fields; it does not replace application authorization.
The title is trimmed only after checking that it is text. The status must match an allowed string exactly. The caller owns printing. Calling this validator alone does not print anything or save data. JSON_THROW_ON_ERROR raises an exception if encoding fails, such as invalid UTF-8; the next lesson will explain exception handling. This example assumes ordinary valid UTF-8 strings when encoding results.
The length rule counts bytes with strlen, not user-perceived Unicode characters. That is the deliberate contract here. A real product should choose its text-length rule and test different languages. The default trim removes a defined set of common edge whitespace, not every Unicode whitespace character.
Guided practice
- Run all three original cases. Identify which function branch produced each result.
- Add
['title' => 'Read', 'status' => 'done']. Expect a normalized task with no errors. - Add an empty array, null, a numeric title, an array title, and a Boolean status. None should return an accepted task. Non-array input gets a task-level error; array inputs get field errors.
- Test 80 ASCII
xcharacters in a title, then 81. The first is valid; the second returns a title error. Count after trimming. - Call the function twice with the same input. Verify the same return value each time and that the original array is unchanged. The function should not print during either call.
Independent exercise
Move the earlier task-report counting loop into countTasksByStatus(array $tasks): array. Initialize counters inside the function, return them after the loop, and leave display outside. Keep the same rule: array rows with missing/unsupported/non-string status count as unknown. The function still assumes each row is an array; state this contract in your explanation.
Check empty input, all-done input, and missing/unsupported/non-string status values. Check that repeated calls start fresh and do not change input. Then write a caller that validates proposed tasks with validateTask, collects accepted tasks, and reports errors separately. Invalid proposals must not be saved or counted as accepted work. No file or database write is needed.
Common mistakes
- Using
echoinstead ofreturn: a caller needs a value it can inspect. - Reading a field before checking its type: string functions cannot safely accept arbitrary arrays.
- Returning partly accepted data alongside errors: make the result contract clear.
- Copying every input field into the result: construct the approved field set explicitly.
- Using global counters: each report call should start with its own counters.
Self-check — not graded
- What is the difference between a parameter and an argument?
- Does an array parameter guarantee a string title?
- Why keep printing outside the function?
Interview practice
“How would you design a small validation boundary?” Describe accepted types/fields/ranges, normalization, structured errors, side effects, tests, and how framework request validation fits. Explain that accepting a task shape does not grant permission to create or edit a task.
English practice and glossary
- Function: a named piece of work.
- Parameter: a name for input in a function definition.
- Argument: a value passed to a function call.
- Return value: the result given back to the caller.
- Local scope: variables available inside a particular function call.
- Side effect: a change outside the returned result, such as writing a file.
Write: “The function accepts __. It returns __ when __. The caller is responsible for __.” Explain one failing input without reading the answer aloud word for word.
Recap and next step
Give a function one clear job, return a predictable result, and validate unknown input before using it. Keep display and storage separate. Next, learn how to investigate failures and handle exceptions deliberately. Official sources and review dates are listed below.
Reveal exercise hints and self-check answers
- A parameter is the named input in the definition. An argument is the actual value passed by a caller.
- No. Field types still need checks.
- The same result can be tested and reused without capturing unwanted output or running a storage action.
Exercise outline: place the earlier initialization and task loop inside countTasksByStatus, then return $counts. Keep its display loop in the caller. Verify an empty result is todo 0/in_progress 0/done 0/unknown 0. Two done tasks produce done 2. Missing status, blocked, null, and array status each increment unknown. Do not add static or global counters. In the validation caller, accept a task only when errors is empty and task is not null. Preserve all error details for user feedback and do not claim a task was stored.