What a 1-Star Kitchen Taught Me About PHP Fundamentals
Before I ever touched PHP, I learned mise en place the hard way, on a line in a 1-star kitchen. Turns out variables, functions, and scope are the same discipline with a different uniform.
My first week on the line in a 1-star kitchen, a sous chef watched me reach past my own station into someone else's mise en place to grab a lemon I hadn't prepped myself. He didn't yell. He didn't have to. He picked up my unlabeled container of clarified butter, looked at it, looked at me, and asked what was in it. I said butter. He asked what kind, cut how, held at what temperature, prepped how long ago. I didn't have an answer for any of it, because I hadn't written any of it down. He set it back exactly where he found it and said, "Then it's nothing. It's a container with a guess in it."
That sentence has outlived the kitchen. I think about it every time I look at a PHP variable with a name like $data or $temp, holding a value nobody bothered to describe, waiting for some future version of me to guess what it's supposed to be.
Cooking and programming get compared to death in blog posts that never go past the metaphor. I want to go past it, because the discipline underneath both jobs is identical, and PHP's fundamentals make more sense once you've felt the consequence of skipping them somewhere that isn't a compiler.
Mise en Place Is Variable Declaration With Higher Stakes
Mise en place means everything in its place. Before service starts, every ingredient is portioned, labeled, and positioned exactly where your hands expect it to be mid-ticket, because there's no time to go looking for something once the tickets start printing.
A PHP variable is the same promise made to your future self. You're not storing a value for its own sake. You're declaring that this container holds a specific thing, named clearly enough that the next person reading your code, who might be you in six months, doesn't have to guess.
<?php
declare(strict_types=1);
$coversTonight = 42;
$averageTicketTime = 14.5;
$dishName = "Seared Scallops";
$isVegetarian = false;
Four variables, four types PHP can infer on its own: an integer, a float, a string, a boolean. PHP doesn't force you to declare types on variables the way some languages do. That flexibility is exactly why the discipline matters more here, not less. Nothing stops you from naming a boolean $data and a string $flag. The language will run it anyway. Your kitchen will not thank you for it, and neither will the next cook working your station.
That declare(strict_types=1) line at the top isn't decoration. Without it, PHP will quietly coerce types for you where it can, turning the string "42" into the integer 42 if a function expects an int. That sounds convenient until it isn't. A sous chef who lets you eyeball a knife cut "close enough" today will let you eyeball the temperature on a beurre blanc tomorrow, and one of those mistakes costs you presentation. Strict types are the kitchen rule that says a cut is either right or it's redone. I turn this on in every PHP file I write, without exception, and I'd tell you to do the same even if this were the only thing you took from this article.
Every Station Has One Job. So Should Every Function.
A real kitchen brigade doesn't have one cook doing everything. Garde manger handles cold apps. Saucier handles sauces. Poissonnier handles fish. Each station exists because a person who does one job well, over and over, under pressure, produces something consistent. A cook trying to run four stations at once produces something that's technically food, the same way code that tries to validate input, query a database, and format a response in one function technically runs.
<?php
declare(strict_types=1);
function calculatePlatingTime(int $itemCount, float $baseSecondsPerItem): float
{
$complexityMultiplier = $itemCount > 5 ? 1.4 : 1.0;
return $itemCount * $baseSecondsPerItem * $complexityMultiplier;
}
$estimatedSeconds = calculatePlatingTime(itemCount: 6, baseSecondsPerItem: 8.0);
echo $estimatedSeconds; // 67.2
Look at what's declared and where. calculatePlatingTime takes exactly what it needs, an item count and a base time, and returns exactly one thing, a float. It doesn't reach outside itself to grab a global variable. It doesn't touch a database. It doesn't print anything. It does one job, the way a station chef plates one category of dish, and you can test it, reuse it, and reason about it in isolation because of that restraint.
Notice the named arguments in the call, itemCount: 6, baseSecondsPerItem: 8.0. PHP 8 added this, and it matters more than it looks like it should. A ticket that says "6, 8.0" tells the next cook nothing about which number is which. A ticket that says "six covers, eight seconds base" removes the guessing entirely. Name your arguments when a function takes more than one or two, the same way you'd never hand a commis an unlabeled container and expect them to know what's in it.
Default parameter values work the same way a standard recipe works when a specific ingredient isn't called out on the ticket:
function calculatePlatingTime(int $itemCount, float $baseSecondsPerItem = 8.0): float
{
$complexityMultiplier = $itemCount > 5 ? 1.4 : 1.0;
return $itemCount * $baseSecondsPerItem * $complexityMultiplier;
}
Now a caller who doesn't specify a base time gets the kitchen's standard, 8 seconds, without every call site having to restate it. That's not laziness. That's the same reason a recipe card has a default portion size printed on it, so nobody has to re-derive it from scratch every single night.
Scope Is the Difference Between Your Station and the Walk-In
That container of clarified butter cost me a lecture, and the lecture boils down to one rule: your station's mise en place is not the walk-in fridge. Other cooks don't get to reach into it directly. If saucier needs something from your station, they ask you, and you hand it to them, because you're the one who knows what state it's in.
Scope in PHP draws the same line. A variable declared inside a function exists only inside that function, for the duration of that call, and disappears the moment the function returns. It cannot see variables from outside itself unless you explicitly hand them in as parameters.
<?php
declare(strict_types=1);
$kitchenTemperature = 68;
function checkWalkInSafety(): bool
{
// This does NOT see $kitchenTemperature. It has its own scope.
return isset($kitchenTemperature) && $kitchenTemperature < 40;
}
var_dump(checkWalkInSafety()); // bool(false), because $kitchenTemperature is invisible here
PHP does let you break this wall with the global keyword, and I want to be direct about what that is: it's reaching past your own station into someone else's mise en place without asking.
function checkWalkInSafetyGlobal(): bool
{
global $kitchenTemperature;
return $kitchenTemperature < 40;
}
This runs. It also means checkWalkInSafetyGlobal now silently depends on a variable that could be sitting anywhere else in your codebase, modified by anything else that also reaches for it the same way. You can no longer look at the function signature and know what it needs. You have to go read the entire file, maybe the entire application, to find every place that variable gets touched. A kitchen where anyone can walk into any walk-in and rearrange the shelves without telling the station chef isn't organized. It's a liability with a pulse. Pass values in as parameters. Return values out explicitly. Reserve global for the rare, deliberate case, and treat every use of it as a decision you have to defend, not a shortcut you take by default.
Static variables are the one exception worth knowing, because they solve a real problem the right way: persistent state that belongs to one function and nobody else.
function nextTicketNumber(): int
{
static $ticketCount = 0;
$ticketCount++;
return $ticketCount;
}
echo nextTicketNumber(); // 1
echo nextTicketNumber(); // 2
echo nextTicketNumber(); // 3
$ticketCount remembers its value between calls, but only this function can see or touch it. That's the equivalent of a station keeping its own running tally on a private notepad, visible to nobody else on the line, updated only by the person whose job it is to update it.
Control Flow Is a Ticket Rail. Read It Like One.
A ticket rail doesn't process every order the same way. Some tickets fire immediately. Some wait on a table's pace. Some get modified mid-service when a guest changes an order. PHP's control structures exist to model exactly that kind of decision-making, and PHP 8 gave the language a much better tool for it: match.
<?php
declare(strict_types=1);
function stationForDish(string $category): string
{
return match ($category) {
'cold_appetizer', 'charcuterie' => 'Garde Manger',
'sauce', 'reduction' => 'Saucier',
'fish', 'shellfish' => 'Poissonnier',
default => 'Expo',
};
}
echo stationForDish('shellfish'); // Poissonnier
Compare that to the older switch equivalent, which requires a break after every case or execution falls through into the next one, silently, which has caused more production bugs across more PHP codebases than almost any other single language quirk:
function stationForDishSwitch(string $category): string
{
switch ($category) {
case 'cold_appetizer':
case 'charcuterie':
return 'Garde Manger';
case 'sauce':
case 'reduction':
return 'Saucier';
case 'fish':
case 'shellfish':
return 'Poissonnier';
default:
return 'Expo';
}
}
match also uses strict comparison by default, === instead of switch's loose ==, which quietly closes off an entire category of PHP's most infamous gotchas involving strings that look numeric. If you're writing new PHP in 2026 and reaching for switch out of habit, reach for match instead. It reads closer to how an expo calls out stations, and it fails loudly instead of sliding silently into the wrong case.
We recommend reading What Is an API? to continue building context for where these fundamentals are headed. Every function and variable pattern in this piece is the raw material you'll assemble into the request-and-response contract that piece describes, and it's worth having that destination in view before you go much further with the basics.
Arrays Are Prep Trays, and PHP Gives You Two Kinds
A prep tray can hold ingredients in a fixed sequence, first slot to last, the way you'd line up components for plating in the exact order they go down. Or it can hold ingredients labeled by name, where you reach for "the citrus" rather than "the third thing on the left." PHP's arrays cover both patterns with the same data structure.
<?php
declare(strict_types=1);
$platingOrder = ['scallop', 'puree', 'micro greens', 'sauce'];
$stationAssignments = [
'garde_manger' => 'cold apps',
'saucier' => 'sauces',
'poissonnier' => 'fish',
];
foreach ($platingOrder as $step) {
echo "Plate: {$step}\n";
}
foreach ($stationAssignments as $station => $duty) {
echo "{$station} handles {$duty}\n";
}
The first foreach walks an indexed array in sequence, exactly like following a plating order top to bottom. The second walks an associative array, pulling both the key and the value, the way you'd read a station chart and get both the station name and its assigned duty at once.
PHP's array functions let you transform a whole tray at once instead of writing a manual loop for every operation. array_map applies one operation to every item. array_filter keeps only what passes a check. array_reduce folds everything down to a single result:
$rawWeights = [220, 180, 340, 95, 410]; // grams, per portion
$adjustedWeights = array_map(fn($w) => $w * 0.9, $rawWeights); // trim 10% for cook loss
$overPortion = array_filter($adjustedWeights, fn($w) => $w > 200);
$totalWeight = array_reduce($adjustedWeights, fn($carry, $w) => $carry + $w, 0);
echo $totalWeight; // 1121.5
Three operations, three functions, each doing one job, composed together instead of tangled into a single loop that tries to trim, filter, and total in the same pass. That's the same principle from the function section, applied to data instead of logic: one job per station, one job per array operation.
The Health Inspector Is Static Analysis
A real kitchen gets inspected. Someone checks your temperatures, your labeling, your storage, and they don't accept "it's probably fine" as an answer. PHP has the equivalent, and most PHP developers skip it entirely, which is exactly why so much production PHP still ships the kind of mistakes the broader PHP community keeps repeating.
PHPStan and Psalm read your code without running it and flag the equivalent of an unlabeled container: a variable that might be null where you assumed it wouldn't be, a function called with the wrong argument type, a return type that doesn't match what the function returns. Running one of these against even a small PHP project for the first time is a humbling experience. It will find things you were certain were fine.
vendor/bin/phpstan analyse src --level=8
Level 8 is PHPStan's strictest setting. Start lower if you're retrofitting an existing codebase, the same way you'd bring a kitchen up to code one station at a time rather than shutting the whole restaurant down for a single inspection. But start. An inspector who never shows up isn't the same thing as a kitchen that's clean.
The Discipline Doesn't Get Easier. You Get Faster at It.
None of this is complicated. Declare your variables with intent. Give your functions one job. Respect scope the way you'd respect another cook's station. Reach for the control structure that fails loudly instead of the one that fails quiet. That's it. That's the whole list, and it was the whole list on my first night on the line too, delivered by a sous chef holding a container of butter I couldn't account for.
The difference between a cook who's been on the line six months and one who's been on it six years isn't that the fundamentals changed. It's that the fundamentals stopped being something they thought about and became something their hands did without being asked. That's where PHP fundamentals are headed too. Once declaring a strict, well-named variable is automatic instead of effortful, you're ready for the part of this language that separates a script from a system: classes, visibility, and the kind of structure a full webapp needs to survive contact with more than one developer. That's Part 2, and it starts with the same brigade this piece started with, this time with a hierarchy attached to it.
