PHP Classes

File: Spyc.class.php

Recommend this page to a friend!
  Classes of Tom Schaefer   QXML   Spyc.class.php   Download  
File: Spyc.class.php
Role: Auxiliary script
Content type: text/plain
Description: SPYC - A Simple PHP YAML Class
Class: QXML
XML <> Array (preserve attrib.), JSON, etc.
Author: By
Last change:
Date: 15 years ago
Size: 28,131 bytes
 

Contents

Class file image Download
<?php /** * Spyc -- A Simple PHP YAML Class * @version 0.2.3 -- 2006-02-04 * @author Chris Wanstrath <chris@ozmm.org> * @link http://spyc.sourceforge.net/ * @copyright Copyright 2005-2006 Chris Wanstrath * @license http://www.opensource.org/licenses/mit-license.php MIT License * @package Spyc */ /** * A node, used by Spyc for parsing YAML. * @package Spyc */ class YAMLNode { public $parent; public $id; public $data; public $indent; public $children = false; static protected $lastNodeId = 0; /** * The constructor assigns the node a unique ID. * * @return void */ public function __construct() { $this->id = ++self::$lastNodeId; } } /** * The Simple PHP YAML Class. * * This class can be used to read a YAML file and convert its contents * into a PHP array. It currently supports a very limited subsection of * the YAML spec. * * Usage: * <code> * $parser = new Spyc; * $array = $parser->load($file); * </code> * @package Spyc */ class Spyc { /** * Load YAML into a PHP array statically * * The load method, when supplied with a YAML stream (string or file), * will do its best to convert YAML in a file into a PHP array. Pretty * simple. * Usage: * <code> * $array = Spyc::YAMLLoad('lucky.yml'); * print_r($array); * </code> * * @return array * @param string $input Path of YAML file or string containing YAML */ public static function YAMLLoad($input) { $spyc = new Spyc(); return $spyc->load($input); } /** * Dump YAML from PHP array statically * * The dump method, when supplied with an array, will do its best * to convert the array into friendly YAML. Pretty simple. Feel free to * save the returned string as nothing.yml and pass it around. * * Oh, and you can decide how big the indent is and what the wordwrap * for folding is. Pretty cool -- just pass in 'false' for either if * you want to use the default. * * Indent's default is 2 spaces, wordwrap's default is 40 characters. And * you can turn off wordwrap by passing in 0. * * @return string * @param array $array PHP array * @param int $indent Pass in false to use the default, which is 2 * @param int $wordwrap Pass in 0 for no wordwrap, false for default (40) */ public static function YAMLDump($array, $indent = false, $wordwrap = false) { $spyc = new Spyc(); return $spyc->dump($array, $indent, $wordwrap); } /** * Load YAML into a PHP array from an instantiated object * * The load method, when supplied with a YAML stream (string or file path), * will do its best to convert the YAML into a PHP array. Pretty simple. * Usage: * <code> * $parser = new Spyc; * $array = $parser->load('lucky.yml'); * print_r($array); * </code> * * @return array * @param string $input Path of YAML file or string containing YAML */ public function load($input) { // See what type of input we're talking about // If it's not a file, assume it's a string if (!empty($input) && (strpos($input, "\n") === false) && file_exists($input)) { $file = $input; $yaml = file($input); } else { $file = null; $yaml = explode("\n", $input); } // Initiate some objects and values $base = new YAMLNode(); $base->indent = 0; $this->_lastIndent = 0; $this->_lastNode = $base->id; $this->_inBlock = false; $this->_isInline = false; foreach ($yaml as $linenum => $line) { $ifchk = trim($line); // If the line starts with a tab (instead of a space), throw a fit. if (preg_match('/^ *(\t) *+(\w+)/', $line)) { $error = sprintf('ERROR: %sLine %d in your input YAML begins with a tab. YAML only recognizes spaces. Please reformat.', ($file ? "File $file " : ''), $linenum + 1); throw new Exception($error); } if ($this->_inBlock === false && empty($ifchk)) { continue; } else if ($this->_inBlock == true && empty($ifchk)) { $last =& $this->_allNodes[$this->_lastNode]; $last->data[key($last->data)] .= "\n"; } else if ($ifchk{0} != '#' && substr($ifchk, 0, 3) != '---') { // Create a new node and get its indent $node = new YAMLNode(); $node->indent = $this->_getIndent($line); // Check where the node lies in the hierarchy if ($this->_lastIndent == $node->indent) { // If we're in a block, add the text to the parent's data if ($this->_inBlock === true) { $parent =& $this->_allNodes[$this->_lastNode]; $parent->data[key($parent->data)] .= trim($line).$this->_blockEnd; } else { // The current node's parent is the same as the previous node's if (isset($this->_allNodes[$this->_lastNode])) { $node->parent = $this->_allNodes[$this->_lastNode]->parent; } } } else if ($this->_lastIndent < $node->indent) { if ($this->_inBlock === true) { $parent =& $this->_allNodes[$this->_lastNode]; $parent->data[key($parent->data)] .= substr($line, $this->_lastIndent).$this->_blockEnd; } else if ($this->_inBlock === false) { // The current node's parent is the previous node $node->parent = $this->_lastNode; // If the value of the last node's data was > or | we need to // start blocking i.e. taking in all lines as a text value until // we drop our indent. $parent =& $this->_allNodes[$node->parent]; $this->_allNodes[$node->parent]->children = true; if (is_array($parent->data)) { $chk = $parent->data[key($parent->data)]; if ($chk === '>') { $this->_inBlock = true; $this->_blockEnd = ' '; $parent->data[key($parent->data)] = str_replace('>', '', $parent->data[key($parent->data)]); $parent->data[key($parent->data)] .= trim($line).' '; $this->_allNodes[$node->parent]->children = false; $this->_lastIndent = $node->indent; } else if ($chk === '|') { $this->_inBlock = true; $this->_blockEnd = "\n"; $parent->data[key($parent->data)] = str_replace('|', '', $parent->data[key($parent->data)]); $parent->data[key($parent->data)] .= trim($line)."\n"; $this->_allNodes[$node->parent]->children = false; $this->_lastIndent = $node->indent; } } } } else if ($this->_lastIndent > $node->indent) { // Any block we had going is dead now if ($this->_inBlock === true) { $this->_inBlock = false; if ($this->_blockEnd = "\n") { $last =& $this->_allNodes[$this->_lastNode]; $last->data[key($last->data)] = trim($last->data[key($last->data)]); } } // We don't know the parent of the node so we have to find it // foreach ($this->_allNodes as $n) { foreach ($this->_indentSort[$node->indent] as $n) { if ($n->indent == $node->indent) { $node->parent = $n->parent; } } } if ($this->_inBlock === false) { // Set these properties with information from our current node $this->_lastIndent = $node->indent; // Set the last node $this->_lastNode = $node->id; // Parse the YAML line and return its data $node->data = $this->_parseLine($line); // Add the node to the master list $this->_allNodes[$node->id] = $node; // Add a reference to the node in an indent array $this->_indentSort[$node->indent][] =& $this->_allNodes[$node->id]; // Add a reference to the node in a References array if this node // has a YAML reference in it. if ( ((is_array($node->data)) && isset($node->data[key($node->data)]) && (!is_array($node->data[key($node->data)]))) && ((preg_match('/^&([^ ]+)/', $node->data[key($node->data)])) || (preg_match('/^\*([^ ]+)/', $node->data[key($node->data)]))) ) { $this->_haveRefs[] =& $this->_allNodes[$node->id]; } else if ( ((is_array($node->data)) && isset($node->data[key($node->data)]) && (is_array($node->data[key($node->data)]))) ) { // Incomplete reference making code. Ugly, needs cleaned up. foreach ($node->data[key($node->data)] as $d) { if (!is_array($d) && ((preg_match('/^&([^ ]+)/', $d)) || (preg_match('/^\*([^ ]+)/', $d)))) { $this->_haveRefs[] =& $this->_allNodes[$node->id]; } } } } } } unset($node); // Here we travel through node-space and pick out references (& and *) $this->_linkReferences(); // Build the PHP array out of node-space $trunk = $this->_buildArray(); return $trunk; } /** * Dump PHP array to YAML * * The dump method, when supplied with an array, will do its best * to convert the array into friendly YAML. Pretty simple. Feel free to * save the returned string as tasteful.yml and pass it around. * * Oh, and you can decide how big the indent is and what the wordwrap * for folding is. Pretty cool -- just pass in 'false' for either if * you want to use the default. * * Indent's default is 2 spaces, wordwrap's default is 40 characters. And * you can turn off wordwrap by passing in 0. * * @return string * @param array $array PHP array * @param int $indent Pass in false to use the default, which is 2 * @param int $wordwrap Pass in 0 for no wordwrap, false for default (40) */ public function dump($array, $indent = false, $wordwrap = false) { // Dumps to some very clean YAML. We'll have to add some more features // and options soon. And better support for folding. // New features and options. if ($indent === false or !is_numeric($indent)) { $this->_dumpIndent = 2; } else { $this->_dumpIndent = $indent; } if ($wordwrap === false or !is_numeric($wordwrap)) { $this->_dumpWordWrap = 40; } else { $this->_dumpWordWrap = $wordwrap; } // New YAML document $string = "---\n"; // Start at the base of the array and move through it. foreach ($array as $key => $value) { $string .= $this->_yamlize($key, $value, 0); } return $string; } protected $_haveRefs; protected $_allNodes; protected $_lastIndent; protected $_lastNode; protected $_inBlock; protected $_isInline; protected $_dumpIndent; protected $_dumpWordWrap; /** * Attempts to convert a key / value array item to YAML * * @return string * @param $key The name of the key * @param $value The value of the item * @param $indent The indent of the current node */ protected function _yamlize($key, $value, $indent) { if (is_array($value)) { // It has children. What to do? // Make it the right kind of item $string = $this->_dumpNode($key, null, $indent); // Add the indent $indent += $this->_dumpIndent; // Yamlize the array $string .= $this->_yamlizeArray($value, $indent); } else if (!is_array($value)) { // It doesn't have children. Yip. $string = $this->_dumpNode($key, $value, $indent); } return $string; } /** * Attempts to convert an array to YAML * * @return string * @param $array The array you want to convert * @param $indent The indent of the current level */ protected function _yamlizeArray($array, $indent) { if (is_array($array)) { $string = ''; foreach ($array as $key => $value) { $string .= $this->_yamlize($key, $value, $indent); } return $string; } else { return false; } } /** * Returns YAML from a key and a value * * @return string * @param $key The name of the key * @param $value The value of the item * @param $indent The indent of the current node */ protected function _dumpNode($key, $value, $indent) { if (is_object($value)) { if (method_exists($value, '__toString')) { $value = (string) $value; } else { $ref = new ReflectionObject($value); $value = $ref->getName(); } } // do some folding here, for blocks if (strpos($value,"\n")) { $value = $this->_doLiteralBlock($value, $indent); } else { $value = $this->_doFolding($value, $indent); } $spaces = str_repeat(' ', $indent); if (is_int($key)) { // It's a sequence $string = $spaces.'- '.$value."\n"; } else { // It's mapped $string = $spaces.$key.': '.$value."\n"; } return $string; } /** * Creates a literal block for dumping * * @return string * @param $value * @param $indent int The value of the indent */ protected function _doLiteralBlock($value, $indent) { $exploded = explode("\n", $value); $newValue = '|'; $indent += $this->_dumpIndent; $spaces = str_repeat(' ', $indent); foreach ($exploded as $line) { $newValue .= "\n".$spaces.trim($line); } return $newValue; } /** * Folds a string of text, if necessary * * @return string * @param $value The string you wish to fold */ protected function _doFolding($value, $indent) { // Don't do anything if wordwrap is set to 0 if ($this->_dumpWordWrap === 0) { return $value; } if (strlen($value) > $this->_dumpWordWrap) { $indent += $this->_dumpIndent; $indent = str_repeat(' ', $indent); $wrapped = wordwrap($value, $this->_dumpWordWrap, "\n$indent"); $value = ">\n".$indent.$wrapped; } return $value; } /* Methods used in loading */ /** * Finds and returns the indentation of a YAML line * * @return int * @param string $line A line from the YAML file */ protected function _getIndent($line) { preg_match('/^\s{1,}/', $line, $match); if (!empty($match[0])) { $indent = substr_count($match[0], ' '); } else { $indent = 0; } return $indent; } /** * Parses YAML code and returns an array for a node * * @return array * @param string $line A line from the YAML file */ protected function _parseLine($line) { $line = trim($line); $array = array(); if (preg_match('/^-(.*):$/', $line)) { // It's a mapped sequence $key = trim(substr(substr($line,1), 0, -1)); $array[$key] = ''; } else if ($line[0] == '-' && substr($line, 0, 3) != '---') { // It's a list item but not a new stream if (strlen($line) > 1) { $value = trim(substr($line, 1)); // Set the type of the value. Int, string, etc $value = $this->_toType($value); $array[] = $value; } else { $array[] = array(); } } else if (preg_match('/^(.+):/', $line, $key)) { // It's a key/value pair most likely // If the key is in double quotes pull it out if (preg_match('/^(["\'](.*)["\'](\s)*:)/', $line, $matches)) { $value = trim(str_replace($matches[1], '', $line)); $key = $matches[2]; } else { // Do some guesswork as to the key and the value $explode = explode(':', $line); $key = trim($explode[0]); array_shift($explode); $value = trim(implode(':', $explode)); } // Set the type of the value. Int, string, etc $value = $this->_toType($value); if (empty($key)) { $array[] = $value; } else { $array[$key] = $value; } } return $array; } /** * Finds the type of the passed value, returns the value as the new type. * * @param string $value * @return mixed */ protected function _toType($value) { $value = trim($value); if ($value && !('"' == $value[0] || "'" == $value[0])) { $value = preg_replace('/\s*#(.+)$/', '', $value); } if (preg_match('/^("(.*)"|\'(.*)\')/', $value, $matches)) { $value = (string) preg_replace('/(\'\'|\\\\\')/', "'", end($matches)); $value = preg_replace('/\\\\"/', '"', $value); } else if (preg_match('/^\\[\s*\\]$/', $value, $matches)) { $value = array(); } else if (preg_match('/^{}$/', $value, $matches)) { $value = array(); } else if (preg_match('/^\\[(.+)\\]$/', $value, $matches)) { // Inline Sequence // Take out strings sequences and mappings $explode = $this->_inlineEscape($matches[1]); // Propogate value array $value = array(); foreach ($explode as $v) { $value[] = $this->_toType($v); } } else if (strpos($value,': ') !== false && !preg_match('/^{(.+)/', $value)) { // It's a map $array = explode(': ', $value); $key = trim($array[0]); array_shift($array); $value = trim(implode(': ', $array)); $value = $this->_toType($value); $value = array($key => $value); } else if (preg_match("/{(.+)}$/", $value, $matches)) { // Inline Mapping // Take out strings sequences and mappings $explode = $this->_inlineEscape($matches[1]); // Propogate value array $array = array(); foreach ($explode as $v) { $array = $array + $this->_toType($v); } $value = $array; } else if (strtolower($value) == 'null' or $value == '' or $value == '~') { $value = null; } else if (ctype_digit($value)) { $value = (int) $value; } else if (in_array(strtolower($value), array('true', 'on', '+', 'yes', 'y'))) { $value = true; } else if (in_array(strtolower($value), array('false', 'off', '-', 'no', 'n'))) { $value = false; } else if (is_numeric($value)) { $value = (float) $value; } return $value; } /** * Used in inlines to check for more inlines or quoted strings * * @return array */ protected function _inlineEscape($inline) { // There's gotta be a cleaner way to do this... // While pure sequences seem to be nesting just fine, // pure mappings and mappings with sequences inside can't go very // deep. This needs to be fixed. // Check for strings $regex = '/(?:(")|(?:\'))((?(1)[^"]+|[^\']+))(?(1)"|\')/'; if (preg_match_all($regex, $inline, $strings)) { foreach ($strings[0] as $string) { $saved_strings[] = $string; } $inline = preg_replace($regex, 'YAMLString', $inline); } unset($regex); // Check for sequences if (preg_match_all('/\[(.+)\]/U', $inline, $seqs)) { $inline = preg_replace('/\[(.+)\]/U', 'YAMLSeq', $inline); $seqs = $seqs[0]; } // Check for mappings if (preg_match_all('/{(.+)}/U', $inline, $maps)) { $inline = preg_replace('/{(.+)}/U', 'YAMLMap', $inline); $maps = $maps[0]; } $explode = explode(', ', $inline); // Re-add the strings if (!empty($saved_strings)) { $i = 0; foreach ($explode as $key => $value) { if (false !== strpos($value,'YAMLString')) { $explode[$key] = str_replace('YAMLString', $saved_strings[$i], $value); ++$i; } } } // Re-add the sequences if (!empty($seqs)) { $i = 0; foreach ($explode as $key => $value) { if (strpos($value,'YAMLSeq') !== false) { $explode[$key] = str_replace('YAMLSeq', $seqs[$i], $value); ++$i; } } } // Re-add the mappings if (!empty($maps)) { $i = 0; foreach ($explode as $key => $value) { if (strpos($value,'YAMLMap') !== false) { $explode[$key] = str_replace('YAMLMap', $maps[$i], $value); ++$i; } } } return $explode; } /** * Builds the PHP array from all the YAML nodes we've gathered * * @return array */ protected function _buildArray() { $trunk = array(); if (!isset($this->_indentSort[0])) { return $trunk; } foreach ($this->_indentSort[0] as $n) { if (empty($n->parent)) { $this->_nodeArrayizeData($n); // Check for references and copy the needed data to complete them. $this->_makeReferences($n); // Merge our data with the big array we're building $trunk = $this->_array_kmerge($trunk, $n->data); } } return $trunk; } /** * Traverses node-space and sets references (& and *) accordingly * * @return bool */ protected function _linkReferences() { if (is_array($this->_haveRefs)) { foreach ($this->_haveRefs as $node) { if (!empty($node->data)) { $key = key($node->data); // If it's an array, don't check. if (is_array($node->data[$key])) { foreach ($node->data[$key] as $k => $v) { $this->_linkRef($node, $key, $k, $v); } } else { $this->_linkRef($node, $key); } } } } return true; } function _linkRef(&$n, $key, $k = null, $v = null) { if (empty($k) && empty($v)) { // Look for &refs if (preg_match('/^&([^ ]+)/', $n->data[$key], $matches)) { // Flag the node so we know it's a reference $this->_allNodes[$n->id]->ref = substr($matches[0], 1); $this->_allNodes[$n->id]->data[$key] = substr($n->data[$key], strlen($matches[0]) + 1); // Look for *refs } else if (preg_match('/^\*([^ ]+)/', $n->data[$key], $matches)) { $ref = substr($matches[0], 1); // Flag the node as having a reference $this->_allNodes[$n->id]->refKey = $ref; } } else if (!empty($k) && !empty($v)) { if (preg_match('/^&([^ ]+)/', $v, $matches)) { // Flag the node so we know it's a reference $this->_allNodes[$n->id]->ref = substr($matches[0], 1); $this->_allNodes[$n->id]->data[$key][$k] = substr($v, strlen($matches[0]) + 1); // Look for *refs } else if (preg_match('/^\*([^ ]+)/', $v, $matches)) { $ref = substr($matches[0], 1); // Flag the node as having a reference $this->_allNodes[$n->id]->refKey = $ref; } } } /** * Finds the children of a node and aids in the building of the PHP array * * @param int $nid The id of the node whose children we're gathering * @return array */ protected function _gatherChildren($nid) { $return = array(); $node =& $this->_allNodes[$nid]; foreach ($this->_allNodes as $z) { if ($z->parent == $node->id) { // We found a child $this->_nodeArrayizeData($z); // Check for references $this->_makeReferences($z); // Merge with the big array we're returning // The big array being all the data of the children of our parent node $return = $this->_array_kmerge($return, $z->data); } } return $return; } /** * Turns a node's data and its children's data into a PHP array * * * @param array $node The node which you want to arrayize * @return boolean */ protected function _nodeArrayizeData(&$node) { if (is_array($node->data) && $node->children == true) { // This node has children, so we need to find them $childs = $this->_gatherChildren($node->id); // We've gathered all our children's data and are ready to use it $key = key($node->data); $key = empty($key) ? 0 : $key; // If it's an array, add to it of course if (is_array($node->data[$key])) { $node->data[$key] = $this->_array_kmerge($node->data[$key], $childs); } else { $node->data[$key] = $childs; } } else if (!is_array($node->data) && $node->children == true) { // Same as above, find the children of this node $childs = $this->_gatherChildren($node->id); $node->data = array(); $node->data[] = $childs; } // We edited $node by reference, so just return true return true; } /** * Traverses node-space and copies references to / from this object. * * @param object $z A node whose references we wish to make real * @return bool */ protected function _makeReferences(&$z) { // It is a reference if (isset($z->ref)) { $key = key($z->data); // Copy the data to this object for easy retrieval later $this->ref[$z->ref] =& $z->data[$key]; // It has a reference } else if (isset($z->refKey)) { if (isset($this->ref[$z->refKey])) { $key = key($z->data); // Copy the data from this object to make the node a real reference $z->data[$key] =& $this->ref[$z->refKey]; } } return true; } /** * Merges arrays and maintains numeric keys. * * An ever-so-slightly modified version of the array_kmerge() function posted * to php.net by mail at nospam dot iaindooley dot com on 2004-04-08. * * http://us3.php.net/manual/en/function.array-merge.php#41394 * * @param array $arr1 * @param array $arr2 * @return array */ protected function _array_kmerge($arr1, $arr2) { if (!is_array($arr1)) { $arr1 = array(); } if (!is_array($arr2)) { $arr2 = array(); } $keys = array_merge(array_keys($arr1), array_keys($arr2)); $vals = array_merge(array_values($arr1), array_values($arr2)); $ret = array(); foreach ($keys as $key) { list($unused, $val) = each($vals); if (isset($ret[$key]) && is_int($key)) { $ret[] = $val; } else { $ret[$key] = $val; } } return $ret; } } ?>