<?php
require_once (dirname(__FILE__) . '/CraurCsvWriter.class.php');
require_once (dirname(__FILE__) . '/CraurCsvReader.class.php');
class Craur
{
protected $data = null;
/**
* Create a new `Craur` from a given JSON-string.
*
* @example
* $node = Craur::createFromJson('{"book": {"authors": ["Hans", "Paul"]}}');
* $authors = $node->get('book.authors[]');
* assert(count($authors) == 2);
*
* @return Craur
*/
static function createFromJson($json_string)
{
$data = @json_decode($json_string, true);
if (!$data)
{
throw new Exception('Invalid json: ' . $json_string);
}
return new Craur($data);
}
/**
* Create a new `Craur` from a given XML-string.
*
* @example
* $node = Craur::createFromXml('<book><author>Hans</author><author>Paul</author></book>');
* $authors = $node->get('book.author[]');
* assert(count($authors) == 2);
*
* @return Craur
*/
static function createFromXml($xml_string, $encoding = 'utf-8')
{
$xml_string = preg_replace('/[\x1-\x8\xB-\xC\xE-\x1F]/', '', $xml_string);
if ($encoding != 'utf-8')
{
$xml_string = iconv($encoding, 'utf-8', $xml_string);
}
$node = new DOMDocument('1.0', 'utf-8');
$is_loaded = $node->loadXML($xml_string, LIBXML_NOCDATA | LIBXML_NOWARNING | LIBXML_NOERROR);
if (!$is_loaded)
{
throw new Exception('Invalid xml: ' . $xml_string);
}
$data = self::convertDomNodeToDataArray($node);
$xpath = new DOMXPath($node);
$root_node_name = $node->documentElement->nodeName;
$namespaces = array();
foreach ($xpath->query('namespace::*') as $namespace_node)
{
$namespace_name = $namespace_node->nodeName;
if ($namespace_name !== 'xmlns:xml')
{
$namespaces[$namespace_name] = $namespace_node->nodeValue;
}
}
$namespaces = array_reverse($namespaces, true);
foreach ($namespaces as $namespace_name => $namespace_uri)
{
$data[$root_node_name]['@' . $namespace_name] = $namespace_uri;
}
return new Craur($data);
}
/**
* Create a new `Craur` from a given HTML-string.
*
* @example
* $node = Craur::createFromHtml('<html><head><title>Hans</title></head><body>Paul</body></html>');
* assert($node->get('html.head.title') == 'Hans');
* assert($node->get('html.body') == 'Paul');
*
* @return Craur
*/
static function createFromHtml($html_string, $encoding = 'utf-8')
{
$html_string = preg_replace('/[\x1-\x8\xB-\xC\xE-\x1F]/', '', $html_string);
if ($encoding != 'utf-8')
{
$html_string = iconv($encoding, 'utf-8', $html_string);
}
$node = new DOMDocument('1.0', 'utf-8');
/*
* FIXME: Can we check if that was enabled in first place?
*/
libxml_use_internal_errors(true);
$node->loadHTML($html_string);
$error = libxml_get_last_error();
libxml_use_internal_errors(false);
if ($error)
{
throw new Exception('Invalid html (' . trim($error->message) . ', line: ' . $error->line . ', col: ' . $error->column . '): ' . $html_string);
}
$data = self::convertDomNodeToDataArray($node);
/*
* We don't need to parse for namespaces here (like in the xml case),
* because namespaces are just attributes in html!
*/
return new Craur($data);
}
static function convertDomNodeToDataArray(DomNode $node)
{
$data = array();
$values = array();
$has_value = false;
if ($node->hasChildNodes())
{
foreach ($node->childNodes as $child_node)
{
/*
* A html dom node always contains one dom document type child
* node with no content (DOMDocumentType#internalSubset is for
* example <!DOCTYPE html>). Ignore it!
*/
if ($child_node instanceof DOMDocumentType)
{
continue ;
}
if ($child_node->nodeType === XML_TEXT_NODE)
{
$has_value = true;
$values[] = $child_node->nodeValue;
}
else
{
$key = $child_node->nodeName;
if (isset($data[$key]))
{
if (!is_array($data[$key]) || !isset($data[$key][0]))
{
$data[$key] = array($data[$key]);
}
$data[$key][] = self::convertDomNodeToDataArray($child_node);
}
else
{
$data[$key] = self::convertDomNodeToDataArray($child_node);
}
}
}
}
if ($node->hasAttributes())
{
foreach ($node->attributes as $attribute_node)
{
$key = '@' . $attribute_node->nodeName;
$data[$key] = $attribute_node->nodeValue;
}
}
if ($has_value)
{
$value = implode('', $values);
if (trim($value))
{
if (empty($data))
{
$data = $value;
}
else
{
$data['@'] = $value;
}
}
}
return $data;
}
/**
* Will load the csv file and fill the objects according to the given `$field_mappings`.
*
* @example
* // If the file loooks like this:
* // Book Name;Book Year;Author Name
* // My Book;2012;Hans
* // My Book;2012;Paul
* // My second Book;2010;Erwin
* $shelf = Craur::createFromCsvFile('fixtures/books.csv', array(
* 'book[].name',
* 'book[].year',
* 'book[].author[].name',
* ));
* assert(count($shelf->get('book[]')) === 2);
* foreach ($shelf->get('book[]') as $book)
* {
* assert(in_array($book->get('name'), array('My Book', 'My second Book')));
* foreach ($book->get('author[]') as $author)
* {
* assert(in_array($author->get('name'), array('Hans', 'Paul', 'Erwin')));
* }
* }
*
* @return Craur
*/
static function createFromCsvFile($file_path, array $field_mappings)
{
$file_handle = null;
if (file_exists($file_path))
{
$file_handle = fopen($file_path, "r");
}
if (!$file_handle)
{
throw new Exception('Cannot open file at ' . $file_path);
}
$craur = CraurCsvReader::createFromCsvFileHandle($file_handle, $field_mappings);
fclose($file_handle);
return $craur;
}
public function __construct(array $data)
{
$this->data = $data;
}
/**
* Return multiple values at once. If a given path is not set, one can use
* the `$default_values` array to specify a default. If a path is not set
* and no default value is given an exception will be thrown.
*
* @param {String[String]} $paths_map A map of values `$paths_map[$key_in_values]=$path_in_craur`
* @param {String[String]} $default_values A map of default values `$paths_map[$key_in_values]=$default_value`
* @param {mixed} $default_value A value which can be used as default if even the `$default_values` do not have a key for the path
*
* @example
* $node = Craur::createFromJson('{"book": {"name": "MyBook", "authors": ["Hans", "Paul"]}}');
*
* $values = $node->getValues(
* array(
* 'name' => 'book.name',
* 'book_price' => 'price',
* 'first_author' => 'book.authors'
* ),
* array(
* 'book_price' => 20
* )
* );
*
* assert($values['name'] == 'MyBook');
* assert($values['book_price'] == '20');
* assert($values['first_author'] == 'Hans');
*
* @return $values[String][]
*/
public function getValues(array $paths_map, array $default_values = array(), $default_value = null)
{
$values = array();
foreach ($paths_map as $value_key => $path)
{
if (array_key_exists($value_key, $default_values))
{
/*
* Yay, we have a default value!
*/
$values[$value_key] = $this->get($path, $default_values[$value_key]);
}
else
{
if (func_num_args() < 3)
{
/*
* If we have no default_value parameter supplied
*/
$values[$value_key] = $this->get($path);
}
else
{
$values[$value_key] = $this->get($path, $default_value);
}
}
}
return $values;
}
/**
* Works similar to `Craur#getValues`, but can use a callable as filter
* object. Before returning the value, the function evaluates
* `$filter($value)` and returns this instead.
*
* If the `$filter` throws an exception, the value won't be added to the
* result.
*
* @example
* $node = Craur::createFromJson('{"book": {"name": "MyBook", "authors": ["Hans", "Paul"]}}');
*
* $values = $node->getValuesWithFilters(
* array(
* 'name' => 'book.name',
* 'book_price' => 'price',
* 'first_author' => 'book.authors'
* ),
* array(
* 'name' => 'strtolower',
* 'first_author' => 'strtoupper',
* ),
* array(
* 'book_price' => 20
* )
* );
*
* assert($values['name'] == 'mybook');
* assert($values['book_price'] == '20');
* assert($values['first_author'] == 'HANS');
*
* @return $values[String][]
*/
public function getValuesWithFilters(array $paths_map, array $filters, array $default_values = array(), $default_value = null)
{
$values = array();
foreach ($paths_map as $value_key => $path)
{
$has_filter = array_key_exists($value_key, $filters);
if ($has_filter)
{
$filter = $filters[$value_key];
if (array_key_exists($value_key, $default_values))
{
/*
* Yay, we have a default value!
*/
$values[$value_key] = $this->getWithFilter($path, $filter, $default_values[$value_key]);
}
else
{
if (func_num_args() < 4)
{
/*
* If we have no default_value parameter supplied
*/
$values[$value_key] = $this->getWithFilter($path, $filter);
}
else
{
$values[$value_key] = $this->getWithFilter($path, $filter, $default_value);
}
}
}
else
{
if (array_key_exists($value_key, $default_values))
{
/*
* Yay, we have a default value!
*/
$values[$value_key] = $this->get($path, $default_values[$value_key]);
}
else
{
if (func_num_args() < 4)
{
/*
* If we have no default_value parameter supplied
*/
$values[$value_key] = $this->get($path);
}
else
{
$values[$value_key] = $this->get($path, $default_value);
}
}
}
}
return $values;
}
/**
* Returns the value at a given path in the object. If the given path does
* not exist and an explicit `$default_value` is set: the `$default_value`
* will be returned.
*
* @param {String} $path The path to the value (e.g. `book.name` or `book.authors[]`)
* @param {mixed} $default_value The default value, which will be returned if the path has no value
*
* @example
* $node = Craur::createFromJson('{"book": {"name": "MyBook", "authors": ["Hans", "Paul"]}}');
*
* $book = $node->get('book');
* assert($book->get('name') == 'MyBook');
* assert($book->get('price', 20) == 20);
*
* $authors = $node->get('book.authors[]');
* assert(count($authors) == 2);
*
* @return mixed
*/
public function get($path, $default_value = null)
{
$current_node = $this->data;
$return_multiple = false;
if (substr($path, -2) === '[]')
{
$return_multiple = true;
$path = substr($path, 0, strlen($path) - 2);
}
/*
* 1. Find the data for the path
*/
foreach (explode('.', $path) as $part)
{
if (is_array($current_node) && !isset($current_node[$part]) && isset($current_node[0]))
{
/*
* We have a non associative array, maybe we want just the
* first element?
*/
$current_node = $current_node[0];
}
if (!isset($current_node[$part]))
{
if (func_num_args() < 2)
{
/*
* If we have no default_value parameter supplied
*/
throw new Exception('Path not found: ' . $path);
}
return $default_value;
}
$current_node = $current_node[$part];
}
/*
* 2. Now return the value
*/
if (!$return_multiple)
{
/*
* If we expect just one value!
*/
if (is_array($current_node) && empty($current_node))
{
if (func_num_args() < 2)
{
/*
* If we have no default_value parameter supplied
*/
throw new Exception('Path not found: ' . $path);
}
return $default_value;
}
if (is_array($current_node) && isset($current_node[0]))
{
/*
* We have something like
*
* current_node = [
* "value",
* "value2"
* ]
*
* let's use the first value!
*/
$current_node = $current_node[0];
}
/*
* It's no array, so let's return it
*/
if (!is_array($current_node))
{
return $current_node;
}
/*
* Associative array - let's return it as Craur node!
*/
return new Craur($current_node);
}
/*
* If we expect multiple values!
*/
if (is_array($current_node) && empty($current_node))
{
return array();
}
if (is_array($current_node) && isset($current_node[0]))
{
/*
* Return each value!
*/
$results = array();
foreach ($current_node as $result_data)
{
if (!is_array($result_data))
{
$result_data = array('@' => $result_data);
}
$results[] = new Craur($result_data);
}
return $results;
}
/*
* It's no array yet, because it's just one element. Let's return
* an array with one item!
*/
if (is_array($current_node))
{
return array(new Craur($current_node));
}
return array($current_node);
}
/**
* Works similar to `Craur#get`, but can use a callable as filter object.
* Before returning the value, the function evaluates `$filter($value)`
* and returns this instead.
*
* If the `$filter` throws an exception, the value won't be added to the
* result.
*
* @example
* function isACheapBook(Craur $value)
* {
* if ($value->get('price') > 20)
* {
* throw new Exception('Is no cheap book!');
* }
* return $value;
* }
*
* $node = Craur::createFromJson('{"books": [{"name":"A", "price": 30}, {"name": "B", "price": 10}, {"name": "C", "price": 15}]}');
* $cheap_books = $node->getWithFilter('books[]', 'isACheapBook');
* assert(count($cheap_books) == 2);
* assert($cheap_books[0]->get('name') == 'B');
* assert($cheap_books[1]->get('name') == 'C');
*
* @return mixed
*/
public function getWithFilter($path, $filter, $default_value = null)
{
$has_default_value = (func_num_args() > 2);
$return_multiple = false;
if (substr($path, -2) === '[]')
{
$return_multiple = true;
$path = substr($path, 0, strlen($path) - 2);
}
if (!is_callable($filter))
{
throw new Exception('Cannot use ' . gettype($filter) . ' as filter, only callables allowed!');
}
try
{
$values_without_filter = $this->get($path . '[]');
$values = array();
foreach ($values_without_filter as $value_without_filter)
{
try
{
$value = $filter($value_without_filter);
if (!$return_multiple)
{
return $value;
}
$values[] = $value;
}
catch (Exception $exception)
{
/*
* Ok, no match!
*/
}
}
if ($return_multiple)
{
return $values;
}
throw new Exception('No element for this path found (after filtering)');
}
catch (Exception $exception)
{
if ($has_default_value)
{
return $default_value;
}
throw new Exception('Path not found!');
}
}
public function __toString()
{
if (isset($this->data['@']))
{
return $this->data['@'];
}
throw new Exception('Cannot convert to string, since value is missing!');
}
/**
* Return the object as a json string. Can be loaded from `Craur::createFromJson`.
*
* @return {String}
*/
public function toJsonString()
{
return json_encode($this->data);
}
/**
* Return the object as a xml string. Can be loaded from `Craur::createFromXml`.
*
* @return {String}
*/
public function toXmlString()
{
return $this->convertNodeDataToXml($this->data);
}
protected function convertNodeDataToXml(array $data)
{
if (isset($data['@']) && count($data) === 1)
{
return htmlspecialchars($data['@']);
}
$result_buffer = array();
foreach ($data as $key => $value)
{
if (substr($key, 0, 1) === '@')
{
/*
* Ok, just an attribute (we made them in a recursion before
* this element)
*/
if ($key === '@')
{
/*
* Nice, we finally have the value:
*/
$result_buffer[] = htmlspecialchars($value);
}
continue;
}
if (is_array($value) && isset($value[0]))
{
/*
* Multiple elements!
*/
foreach ($value as $sub_value)
{
$tmp_data = array();
$tmp_data[$key] = $sub_value;
$result_buffer[] = $this->convertNodeDataToXml($tmp_data);
}
continue;
}
$has_inner_value = false;
$result_buffer[] = '<' . htmlspecialchars($key);
$attributes = array();
$has_also_non_attributes = false;
if (is_array($value))
{
foreach ($value as $sub_key => $sub_value)
{
if (substr($sub_key, 0, 1) == '@' && strlen($sub_key) > 1)
{
$result_buffer[] = ' ' . substr($sub_key, 1) . '="' . htmlspecialchars($sub_value) . '"';
}
else
{
$has_also_non_attributes = true;
}
}
}
else
{
$has_also_non_attributes = true;
}
if (!$has_also_non_attributes)
{
$result_buffer[] = '/>';
continue;
}
$result_buffer[] = '>';
/*
* Just one!
*/
if (is_array($value))
{
/*
* Multiple elements!
*/
$result_buffer[] = $this->convertNodeDataToXml($value);
}
else
{
$result_buffer[] = htmlspecialchars($value);
}
$result_buffer[] = '</' . htmlspecialchars($key) . '>';
}
return implode('', $result_buffer);
}
/**
* Will store the csv file with the objects content according to the given `$field_mappings`.
*
* @example
* $data = array(
* 'book' => array(
* array(
* 'name' => 'My Book',
* 'year' => '2012',
* 'author' => array(
* array('name' => 'Hans'),
* array('name' => 'Paul')
* )
* ),
* array(
* 'name' => 'My second Book',
* 'year' => '2010',
* 'author' => array(
* array('name' => 'Erwin')
* )
* )
* )
* );
*
* $shelf = new Craur($data);
* $shelf->saveToCsvFile('fixtures/temp_csv_file.csv', array(
* 'book[].name',
* 'book[].year',
* 'book[].author[].name',
* ));
*
* // csv file will look like this now:
* // book[].name;book[].year;book[].author[].name
* // "My Book";2012;Hans
* // "My Book";2012;Paul
* // "My second Book";2010;Erwin
*
* assert(json_encode(array($data)) == Craur::createFromCsvFile('fixtures/temp_csv_file.csv', array(
* 'book[].name',
* 'book[].year',
* 'book[].author[].name',
* ))->toJsonString());
*
* unlink('fixtures/temp_csv_file.csv');
*
* @return void
*/
public function saveToCsvFile($csv_file_path, array $field_mappings)
{
/*
* Clean the file
*/
file_put_contents($csv_file_path, '');
$file_handle = fopen($csv_file_path, 'w');
fputcsv($file_handle, $field_mappings, ';');
$writer = new CraurCsvWriter($this, $field_mappings);
$writer->writeToCsvFileHandle($file_handle);
fclose($file_handle);
}
}