YAML #
YAML (YAML Ain’t Markup Language) is a data serialization format designed to be easy for humans to read and write — unlike JSON, which suits machines better, YAML uses whitespace indentation and minimal syntax without excessive curly braces or quotes. YAML is the standard choice for configuration files (Docker Compose, Kubernetes, GitHub Actions, Ansible all use YAML), and it’s increasingly used as an alternative to .env for more structured configuration. PHP doesn’t have a rich built-in YAML parser — there’s the yaml extension (PECL) and libraries like symfony/yaml which are far more popular and portable. This article covers the YAML syntax you need to understand, how to parse and generate YAML from PHP, data type traps that often surprise people, and idiomatic application configuration patterns.
YAML Syntax — What You Need to Understand #
Before parsing from PHP, understand YAML syntax so you can read and write configuration files correctly.
Basic Data Types #
# Comments use the hash sign
# Strings — quotes optional
name: Budi Santoso
city: "Jakarta" # double quotes allowed
province: 'West Java' # single quotes allowed
# Integers and Floats
age: 28
height: 172.5
temperature: -5
# Booleans — WARNING: many values are treated as true/false!
active: true
inactive: false
yes: yes # true!
no: no # false!
on: on # true!
off: off # false!
# Null
empty: null
also_empty: ~ # tilde = null
# Dates — automatically parsed as date objects by some parsers
birth: 1995-08-17
time: 2024-03-15T14:30:00+07:00
Data Structures — Mappings and Sequences #
# Mapping (like an associative array / object)
database:
host: localhost
port: 3306
name: myapp_db
user: root
password: "secret123"
# Sequence (like an indexed array)
fruits:
- apple
- mango
- orange
# Sequence of mappings (array of objects)
products:
- id: 1
name: Laptop
price: 15000000
- id: 2
name: Monitor
price: 5000000
# Flow style — inline (like JSON)
coordinates: {lat: -6.2088, lng: 106.8456}
tags: [php, web, backend]
# Nested — 2-space indentation (convention, not a requirement)
application:
server:
host: 0.0.0.0
port: 8080
workers: 4
database:
primary:
host: db-primary.example.com
port: 5432
replica:
host: db-replica.example.com
port: 5432
Multi-line Strings #
# Literal block (|) — preserves newlines
description: |
First line.
Second line.
Third line with indentation.
# Folded block (>) — newlines become spaces (except blank lines)
summary: >
This is a long paragraph
wrapped across several lines
but will be joined into one line.
A new paragraph starts after a blank line.
# Strings containing special characters — need quotes
path: "C:\\Users\\Budi\\Documents"
regex: "^\\d{4}-\\d{2}-\\d{2}$"
quotes: 'She said "hello"'
Anchors and Aliases — Avoiding Duplication #
# Anchor (&name) defines a reusable block
# Alias (*name) references the anchor
# Define a base configuration
_base_db: &base_db
host: localhost
port: 5432
charset: utf8
# Use it in several places with aliases
development:
database:
<<: *base_db # merge anchor — copy all keys from base_db
name: myapp_dev
user: dev_user
production:
database:
<<: *base_db # copy from base_db again
host: db.production.example.com # override host
name: myapp_prod
user: prod_user
password: "strong_password"
# Anchor for scalar values
_timeout: &timeout 30
api:
connect_timeout: *timeout
read_timeout: *timeout
write_timeout: *timeout
Installing a YAML Parser #
PHP doesn’t have adequate built-in YAML support. There are two main options:
# Option 1: symfony/yaml — pure PHP, no extension needed
# Most recommended: portable, actively maintained, full-featured
composer require symfony/yaml
# Option 2: yaml extension (PECL) — faster, needs system installation
sudo apt install php8.3-yaml
# or via PECL:
pecl install yaml
| Aspect | symfony/yaml | yaml extension (PECL) |
|---|---|---|
| Installation | composer require | Compile/apt |
| Portability | Every environment | Needs system installation |
| Performance | Slower | Faster |
| Features | Full + validation | Standard |
| Maintenance | Active (Symfony) | Less active |
Use symfony/yaml for almost every case — easier to deploy and more reliable.
Parsing YAML with symfony/yaml #
<?php
use Symfony\Component\Yaml\Yaml;
use Symfony\Component\Yaml\Exception\ParseException;
// Parse a YAML string
$yaml = <<<YAML
name: Budi Santoso
age: 28
active: true
address:
city: Jakarta
postalCode: "10110"
hobbies:
- reading
- coding
- hiking
YAML;
$data = Yaml::parse($yaml);
echo $data['name']; // Budi Santoso
echo $data['age']; // 28
var_dump($data['active']); // bool(true)
echo $data['address']['city']; // Jakarta
echo $data['hobbies'][1]; // coding
// Parse from a file
$config = Yaml::parseFile('/path/to/config.yaml');
// With error handling
try {
$config = Yaml::parseFile('config.yaml');
} catch (ParseException $e) {
throw new \RuntimeException(
"Failed to parse config.yaml: " . $e->getMessage(),
previous: $e
);
}
Important Parsing Flags #
<?php
use Symfony\Component\Yaml\Yaml;
$yaml = "
active: true
value: 3.14
date: 2024-03-15
empty: ~
code: '007'
";
// Default parsing
$data = Yaml::parse($yaml);
var_dump($data['active']); // bool(true)
var_dump($data['value']); // float(3.14)
var_dump($data['code']); // string(3) "007"
// PARSE_DATETIME — parse dates into \DateTime objects
$data = Yaml::parse($yaml, Yaml::PARSE_DATETIME);
var_dump($data['date']); // object(DateTime)
// PARSE_OBJECT_FOR_MAP — mappings become stdClass instead of arrays
$data = Yaml::parse($yaml, Yaml::PARSE_OBJECT_FOR_MAP);
echo $data->active; // 1 (true)
echo $data->code; // 007
// PARSE_CONSTANT — allow PHP constants in YAML
// Useful for PHP-level configuration
$yamlWithConstant = "log_level: !php/const PHP_INT_MAX";
$data = Yaml::parse($yamlWithConstant, Yaml::PARSE_CONSTANT);
var_dump($data['log_level']); // int(9223372036854775807)
Dumping PHP to YAML #
<?php
use Symfony\Component\Yaml\Yaml;
$data = [
'application' => [
'name' => 'Online Store',
'version' => '2.1.0',
'debug' => false,
],
'database' => [
'host' => 'localhost',
'port' => 3306,
'name' => 'store_db',
'user' => 'root',
'password' => 'secret',
],
'cache' => [
'driver' => 'redis',
'ttl' => 3600,
],
'features' => ['cart', 'wishlist', 'review'],
];
// Dump to a YAML string
$yaml = Yaml::dump($data);
echo $yaml;
/*
application:
name: 'Online Store'
version: 2.1.0
debug: false
database:
host: localhost
port: 3306
...
*/
// Control inline depth — short arrays are inlined at a certain depth
$yaml = Yaml::dump($data, indent: 2, inline: 3);
// inline: 3 means structures at depth >= 3 are inlined as flow style
// Save to a file
file_put_contents('config.yaml', Yaml::dump($data, 2, 2));
// Dump with flags
$yaml = Yaml::dump($data, 2, 2,
Yaml::DUMP_MULTI_LINE_LITERAL_BLOCK // long strings use block style |
| Yaml::DUMP_EMPTY_ARRAY_AS_SEQUENCE // [] instead of {}
);
YAML Data Type Traps #
YAML has several automatic type conversions that often surprise people — especially values that look like strings but get interpreted as other types:
# Trap 1: values that look boolean but aren't in YAML 1.2
# In YAML 1.1 (widely used):
yes: yes # → true
no: no # → false
on: on # → true
off: off # → false
y: y # → true (!) in some parsers
n: n # → false (!)
# In YAML 1.2 (stricter):
# only true/false count as booleans; yes/no/on/off are strings
<?php
use Symfony\Component\Yaml\Yaml;
// This yields DIFFERENT results depending on the YAML version!
$yaml = "active: yes";
$data = Yaml::parse($yaml);
var_dump($data['active']); // bool(true) — not the string "yes"!
// Solution: quote values that are genuinely strings
$yaml = "active: 'yes'";
$data = Yaml::parse($yaml);
var_dump($data['active']); // string(3) "yes" — with quotes, it stays a string
// Trap 2: octal numbers
// 0755 in YAML 1.1 is interpreted as octal = 493 decimal!
$yaml = "permission: 0755";
$data = Yaml::parse($yaml);
var_dump($data['permission']); // int(493) not int(755)!
// Solution: quote as a string if you genuinely need the string "0755"
$yaml = "permission: '0755'";
// Trap 3: values that look like floats
$yaml = "version: 1.0";
$data = Yaml::parse($yaml);
var_dump($data['version']); // float(1.0) — not the string "1.0"
// Trap 4: strings that look like null
$yaml = "name: ~";
$data = Yaml::parse($yaml);
var_dump($data['name']); // NULL — tilde = null in YAML!
// Trap 5: ISO dates
$yaml = "birth: 1995-08-17";
$data = Yaml::parse($yaml);
var_dump($data['birth']); // string(10) "1995-08-17" in symfony/yaml by default
// but could be a DateTime in other parsers or with the PARSE_DATETIME flag
Always quote ambiguous string values — especiallyyes,no,on,off,true,false, numbers with leading zeros, and ISO 8601 dates. Single quotes ('value') are safest because there are no escape sequences inside them.
YAML for Application Configuration #
The most common pattern: one YAML file per environment, loaded when the application boots.
Configuration File Structure #
# config/app.yaml
app:
name: "Nusantara Online Store"
version: "2.1.0"
url: "https://store.example.com"
debug: false
timezone: "Asia/Jakarta"
locale: "id_ID"
server:
host: "0.0.0.0"
port: 8080
workers: 4
timeout: 30
database:
default: mysql
connections:
mysql:
host: "${DB_HOST}" # value from an environment variable
port: 3306
name: "${DB_NAME}"
user: "${DB_USER}"
password: "${DB_PASSWORD}"
charset: utf8mb4
options:
strict: true
timezone: "+07:00"
cache:
default: redis
stores:
redis:
host: "${REDIS_HOST}"
port: 6379
db: 0
ttl: 3600
array:
driver: array
mail:
driver: smtp
host: "${MAIL_HOST}"
port: 587
encryption: tls
from:
address: "[email protected]"
name: "Online Store"
logging:
level: info
channel: daily
path: "storage/logs/app.log"
max_files: 14
features:
payments:
- bank_transfer
- credit_card
- gopay
- ovo
shipping:
- jne
- pos
- sicepat
ConfigLoader — A Class for Reading Configuration #
<?php
use Symfony\Component\Yaml\Yaml;
class ConfigLoader
{
private array $config = [];
public function __construct(private string $baseDir)
{
}
public function load(string $environment = 'production'): static
{
// Load the base configuration
$baseFile = $this->baseDir . '/app.yaml';
if (file_exists($baseFile)) {
$this->config = Yaml::parseFile($baseFile);
}
// Override with the environment configuration
$envFile = $this->baseDir . "/app.{$environment}.yaml";
if (file_exists($envFile)) {
$envConfig = Yaml::parseFile($envFile);
$this->config = $this->mergeRecursive($this->config, $envConfig);
}
// Interpolate environment variables
$this->config = $this->interpolateEnv($this->config);
return $this;
}
public function get(string $key, mixed $default = null): mixed
{
// Access with dot notation: 'database.connections.mysql.host'
$parts = explode('.', $key);
$value = $this->config;
foreach ($parts as $segment) {
if (!is_array($value) || !array_key_exists($segment, $value)) {
return $default;
}
$value = $value[$segment];
}
return $value;
}
public function getRequired(string $key): mixed
{
$value = $this->get($key);
if ($value === null) {
throw new \RuntimeException("Configuration '$key' is required but not found");
}
return $value;
}
private function mergeRecursive(array $base, array $override): array
{
foreach ($override as $key => $value) {
if (is_array($value) && isset($base[$key]) && is_array($base[$key])) {
$base[$key] = $this->mergeRecursive($base[$key], $value);
} else {
$base[$key] = $value;
}
}
return $base;
}
private function interpolateEnv(mixed $value): mixed
{
if (is_string($value)) {
// Replace ${VAR_NAME} with the environment variable value
return preg_replace_callback(
'/\$\{([A-Z_][A-Z0-9_]*)\}/',
function(array $m): string {
$envVal = getenv($m[1]);
if ($envVal === false) {
throw new \RuntimeException(
"Environment variable '{$m[1]}' is not set"
);
}
return $envVal;
},
$value
);
}
if (is_array($value)) {
return array_map(fn($v) => $this->interpolateEnv($v), $value);
}
return $value;
}
}
// Usage
$config = (new ConfigLoader(__DIR__ . '/config'))
->load(getenv('APP_ENV') ?: 'production');
echo $config->get('app.name'); // Nusantara Online Store
echo $config->get('database.connections.mysql.host'); // value from DB_HOST
echo $config->get('server.port', 8080); // 8080
$config->getRequired('app.secret_key'); // RuntimeException if missing
Multi-Document YAML #
One YAML file can contain several documents, separated by ---:
# deployment.yaml
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: myapp
spec:
replicas: 3
template:
spec:
containers:
- name: myapp
image: myapp:latest
---
apiVersion: v1
kind: Service
metadata:
name: myapp-service
spec:
type: LoadBalancer
ports:
- port: 80
<?php
use Symfony\Component\Yaml\Yaml;
// Parse multi-document YAML (produces an array of documents)
$content = file_get_contents('deployment.yaml');
$documents = [];
// Split manually by '---'
foreach (preg_split('/^---$/m', $content, -1, PREG_SPLIT_NO_EMPTY) as $part) {
if (trim($part) !== '') {
$documents[] = Yaml::parse($part);
}
}
foreach ($documents as $i => $doc) {
echo "Document $i: kind=" . $doc['kind'] . "\n";
}
// Document 0: kind=Deployment
// Document 1: kind=Service
YAML vs JSON vs INI — When to Use Which #
flowchart TD
A{Which format\nis right?} --> B{Read directly\nby humans?}
B -- Yes --> C{Complex\nstructure?}
B -- No --> D[JSON\nGood for APIs\nand storage]
C -- Yes --> E[YAML\nComplex config\nwith comments]
C -- No --> F{Only\nkey=value?}
F -- Yes --> G[INI / .env\nSimple and universal]
F -- No --> E
style E fill:#dcfce7
style D fill:#dbeafe
style G fill:#fef9c3| Aspect | YAML | JSON | INI / .env |
|---|---|---|---|
| Readability | ★★★★★ | ★★★☆☆ | ★★★★☆ |
| Comments | ✓ | ✗ | ✓ |
| Data types | Automatic | Explicit | Strings only |
| Nested structure | ✓ | ✓ | Limited |
| Validation | Needs schema | Needs schema | N/A |
| Built-in PHP parser | ✗ | ✓ | ✓ (parse_ini_file) |
| Best for | Configuration, CI/CD | API, storage | Simple env variables |
Use YAML for:
✓ Application configuration files read by developers
✓ CI/CD pipelines (GitHub Actions, GitLab CI)
✓ Infrastructure as Code (Ansible, Kubernetes)
✓ Configuration needing comments and hierarchical structure
Use JSON for:
✓ API communication (request/response)
✓ Structured data storage
✓ Package manifests (composer.json)
✓ When parsing speed matters
Use INI/.env for:
✓ Sensitive environment variables (passwords, API keys)
✓ Minimal configuration without nested structure
Common YAML Anti-Patterns #
<?php
use Symfony\Component\Yaml\Yaml;
// ✗ Anti-pattern 1: storing boolean/null values as unquoted strings
// This is NOT the string "true" — it's the boolean true!
$yaml = "debug: true";
$data = Yaml::parse($yaml);
var_dump($data['debug']); // bool(true), not the string "true"
// ✓ If you genuinely need a string: use quotes
$yaml = "debug: 'true'";
// ✗ Anti-pattern 2: storing passwords/secrets directly in YAML committed to Git
# config/production.yaml — DON'T DO THIS
# database:
# password: "SuperSecret123!"
// ✓ Use environment variable references
$yaml = "
database:
password: '\${DB_PASSWORD}'
";
// The actual value lives in .env, which isn't committed
// ✗ Anti-pattern 3: ignoring parse errors
$config = Yaml::parseFile('config.yaml');
// If the file doesn't exist or the YAML is invalid, a ParseException is thrown uncaught
// ✓ Handle parse errors informatively
try {
$config = Yaml::parseFile('config.yaml');
} catch (\Symfony\Component\Yaml\Exception\ParseException $e) {
throw new \RuntimeException(
"Invalid configuration at line {$e->getParsedLine()}: {$e->getMessage()}"
);
} catch (\RuntimeException $e) {
throw new \RuntimeException("Configuration file not found: config.yaml");
}
// ✗ Anti-pattern 4: using tabs for indentation
// YAML doesn't allow tabs as indentation — only spaces!
// Make sure your editor is configured not to insert tabs in YAML files
// ✗ Anti-pattern 5: regenerating YAML from PHP arrays containing user data
// User data can contain characters that break YAML
$data = ['comment' => ": this contains an unquoted colon"];
$yaml = Yaml::dump($data);
// symfony/yaml handles this correctly (adds quotes automatically)
// but not all libraries do — always use a library, never build YAML manually
Summary #
symfony/yamlis the best YAML library for PHP — pure PHP with no extra extensions, portable, actively maintained by the Symfony team. Install withcomposer require symfony/yaml.- YAML data type traps:
yes/no/on/offare booleans, tilde (~) is null, numbers with leading zeros are octal. Always quote ambiguous string values with single quotes ('value').- Anchors (
&name) and aliases (*name) prevent configuration duplication — define once, use many times with<<: *namefor merging.- Environment variable interpolation — don’t store passwords in YAML committed to Git. Use
${VAR_NAME}placeholders and interpolate from the environment at runtime.- Dot notation for nested access —
config.get('database.connections.mysql.host')is far cleaner than chained array access.- Multi-documents are separated with
---— useful for Kubernetes or Ansible files containing several resources in one file.- YAML vs JSON vs INI — YAML for hierarchical human-readable configuration; JSON for APIs and storage; INI/
.envfor simple environment variables.- Don’t build YAML manually via string concatenation — always use
Yaml::dump()so escaping and formatting are handled correctly.