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
Aspectsymfony/yamlyaml extension (PECL)
Installationcomposer requireCompile/apt
PortabilityEvery environmentNeeds system installation
PerformanceSlowerFaster
FeaturesFull + validationStandard
MaintenanceActive (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 — especially yes, 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
AspectYAMLJSONINI / .env
Readability★★★★★★★★☆☆★★★★☆
Comments
Data typesAutomaticExplicitStrings only
Nested structureLimited
ValidationNeeds schemaNeeds schemaN/A
Built-in PHP parser✓ (parse_ini_file)
Best forConfiguration, CI/CDAPI, storageSimple 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/yaml is the best YAML library for PHP — pure PHP with no extra extensions, portable, actively maintained by the Symfony team. Install with composer require symfony/yaml.
  • YAML data type traps: yes/no/on/off are 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 <<: *name for 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 accessconfig.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/.env for simple environment variables.
  • Don’t build YAML manually via string concatenation — always use Yaml::dump() so escaping and formatting are handled correctly.

← Previous: JSON   Next: MySQL →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact