I've written a small YAML parser. Currently it only implements a tiny subset of the YAML Specification, because that's all I need, but it could be used as a stepping stone if anyone was interested in implementing more of the YAML Specification:
Yaml Node:
Code: Select all
<?php
class YamlNode implements ArrayAccess, IteratorAggregate
{
/**
* Name $___children to prevent collision with any children actually called "children"
*/
protected $___children = array();
public function __construct($yaml = null)
{
if ($yaml !== null)
{
$this->assimilateData($yaml);
}
}
public function children()
{
return $this->___children;
}
public function addChildNode($name)
{
if (!isset($this->___children[$name]))
{
$this->___children[$name] = new self();
}
return $this->___children[$name];
}
public function addKeyValuePair($key, $value)
{
$this->___children[$key] = $value;
}
public function assimilateData($yaml)
{
$parser = new YamlParser($this);
$parser->assimilateData($yaml);
}
/**
* for IteratorAggregate
*/
public function getIterator()
{
return new ArrayIterator($this->___children);
}
/**
* for ArrayAccess
* {{{
*/
public function offsetExists($offset)
{
if (isset($this->___children[$offset]))
{
return true;
}
return false;
}
public function offsetGet($offset)
{
return $this->___children[$offset];
}
public function offsetSet($offset, $value) {
$this->___children[$offset] = $value;
}
public function offsetUnset($offset)
{
if (isset($this->___children[$offset]))
{
unset($this->___children[$offset]);
}
}
/**
* }}}
*/
/**
* Magic methods
* {{{
*/
private function __get($property)
{
if (!isset($this->___children[$property]))
{
return;
}
return $this->___children[$property];
}
private function __isset($property)
{
if (isset($this->___children[$property]))
{
return true;
}
return false;
}
/**
* }}}
*/
}
Yaml Parser:
Code: Select all
class YamlParser
{
protected $nodes = array();
public function __construct(YamlNode $root)
{
$this->nodes = array(0 => $root);
}
public function assimilateData($yaml)
{
$file = $this->getDataLines($yaml);
foreach ($file as $line)
{
if ($this->isComment($line))
{
continue;
}
if ($this->isChildNode($line))
{
$this->assimilateChildNode($line);
continue;
}
if ($this->isKeyValuePair($line))
{
$this->assimiateKeyValuePair($line);
continue;
}
}
}
protected function assimilateChildNode($line)
{
$name = substr(trim($line), 0, -1);
$level = $this->getLevel($line);
$this->nodes[$level + 1] = $this->nodes[$level]->addChildNode($name);
}
protected function assimiateKeyValuePair($line)
{
preg_match('/^([a-z]{1}[^\s]+):(.+)$/', trim($line), $matches);
$key = $matches[1];
$value = $this->autoType(trim($matches[2]));
$level = $this->getLevel($line);
$this->nodes[$level]->addKeyValuePair($key, $value);
}
protected function getLevel($line)
{
if (preg_match('/^\s+/', $line, $matches))
{
return substr_count($matches[0], ' ');
}
return 0;
}
/**
* must start with a-z and end with ":" (no spaces)
*/
protected function isChildNode($line)
{
$line = trim($line);
return (preg_match('/^[a-z]{1}[^\s]+:$/', $line) === 1);
}
protected function isKeyValuePair($line)
{
$line = trim($line);
return (preg_match('/^[a-z]{1}[^\s]+:.+$/', $line) === 1);
}
protected function isComment($line)
{
return (strpos(trim($line), '#') === 0);
}
protected function getDataLines($yaml)
{
if (strpos($yaml, "\n") === false)
{
return file($yaml);
}
return explode("\n", $yaml);
}
protected function autoType($value) {
$lower_value = strtolower($value);
switch (true) {
// $value is an integer
case ($value === (string)(int)$value):
return (int)$value;
break;
// $value is a float
case ($value === (string)(float)$value):
return (float)$value;
break;
// $value is a boolean true equivalent
case ($lower_value === 'true'):
case ($lower_value === 'on'):
case ($lower_value === 'yes'):
case ($lower_value === 'y'):
case ($lower_value === '+'):
return true;
break;
// $value is a boolean false equivalent
case ($lower_value === 'false'):
case ($lower_value === 'off'):
case ($lower_value === 'no'):
case ($lower_value === 'n'):
case ($lower_value === '-'):
return false;
break;
case ($lower_value === 'null'):
case ($lower_value === '~'):
case ($lower_value === ''):
return null;
break;
// $value is just a string
default:
return $this->stripQuotes($value);
break;
}
}
protected function stripQuotes($string) {
return preg_replace('/(^"(.+)"$)|(^\'(.+)\'$)/', '$2$4', $string);
}
}
sample.yml (example app configuration):
Code: Select all
domain.com:
mode: production
http:
url: http://www.domain.com/index.php
cookie: domain.com
https:
url: https://www.domain.com/index.php
cookie: domain.com
databases:
transactional:
read: type://user:pass@host.tld:port/dbname
write: type://user:pass@socket:/tmp/mysql.sock/dbname
analytical:
read: type://user:pass@host.tld:port/dbname
write: type://user:pass@socket:/tmp/mysql.sock/dbname
sample use:
Code: Select all
$conf = new YamlNode('sample.yml');
// access as an array
var_dump($conf['domain.com']['databases']['transactional']['read']);
// access as object properties
var_dump($conf->{'domain.com'}->databases->transactional->read);
// access mixed
var_dump($conf['domain.com']->databases->transactional->read);
It has been working well for me, and like I said it only implements a small subset of YAML since I don't require all of YAML. Any the key/value pairs key names are a bit more strict since I don't allow spaces. But anyone is free to use it or expand on it and add more YAML Specifications. :)
If you're wondering what the YamlNode::assimilateData() is all about, it is so that I can merge several discrete YAML files into a single YamlNode. Quite handy for modular systems ;-)