A Cooklang recipe parser for PHP.
composer require devapeu/cooklangRequires PHP 8.0 or higher.
use Devapeu\Cooklang\CooklangParser;
$recipe = CooklangParser::parse(<<<COOKLANG
>> servings: 4
Crack the @eggs{3}(any type) into a blender, then add the @flour{125%g},
@milk{250%ml} and @sea salt{1%pinch}, and blitz until smooth.
Pour into a #bowl and leave to stand for ~{15%minutes}.
COOKLANG);
$recipe->meta; // ['servings' => '4']
$recipe->ingredients; // Ingredient[]
$recipe->cookware; // ['bowl']
$recipe->instructions; // Instruction[], ingredients wrapped in <ingredient> tags by default
$recipe->sections; // Section[]CooklangParser::parse() returns a Devapeu\Cooklang\Models\Recipe:
| Property | Type | Description |
|---|---|---|
meta |
array |
Key/value metadata pairs (>> key: value) |
ingredients |
Ingredient[] |
Top-level ingredients (@ingredient) |
cookware |
string[] |
Top-level cookware (#cookware) |
instructions |
Instruction[] |
Top-level instruction sentences |
sections |
Section[] |
Named sections (= Section Name), each with its own ingredients, cookware and instructions |
Each Ingredient has name, quantity, measure, note, and optional.
Each Instruction implements Stringable (so it works directly in string
interpolation, concatenation, echo, etc. with no cast needed), and also exposes
text() for the raw tagged string explicitly and plain() to strip ingredient tags.
By default, @ingredient, #cookware, and ~{timer} references inside
instructions are each wrapped in a matching tag — <ingredient>, <cookware>,
and <timer> respectively (e.g. <ingredient>2 cups of flour</ingredient>) — so
you can style or target them from HTML/CSS/JS.
If you don't want any markup, call plainInstructions() on the Recipe (or Section)
to get all instructions stripped at once, or plain() on an individual Instruction:
echo $recipe->instructions[0]; // "Add <ingredient>2 cups of flour</ingredient> to the mix." (implicit __toString)
$recipe->instructions[0]->text(); // "Add <ingredient>2 cups of flour</ingredient> to the mix." (explicit)
$recipe->instructions[0]->plain(); // "Add 2 cups of flour to the mix."
$recipe->plainInstructions()[0]; // "Add 2 cups of flour to the mix."Recipe, Section, Ingredient, and Instruction all expose toArray(), and
implement JsonSerializable, so you can convert a parsed recipe (recursively,
including its sections) to a plain array or JSON:
$recipe->toArray(); // ['meta' => [...], 'ingredients' => [...], ..., 'sections' => [...]]
json_encode($recipe);Or customize/disable each tag independently at parse time via ParserOptions:
use Devapeu\Cooklang\ParserOptions;
CooklangParser::parse($source, new ParserOptions(
ingredientTag: 'span',
cookwareTag: 'span',
timerTag: null, // disables timer tagging entirely
));- Ingredients:
@name,@name{quantity%measure},@*optional ingredient{} - Cookware:
#cookware - Timers:
~{quantity%measure} - Notes:
@ingredient{}(note) - Sections:
= Section Nameor== Section Name == - Comments:
-- line commentand inline-- trailing comment - Metadata:
>> key: value, or a YAML frontmatter block delimited by---lines (parsed withsymfony/yaml, so nested maps and lists are supported)
composer test