First commit

This commit is contained in:
Pierre Hubert
2016-11-19 12:08:12 +01:00
commit 990540b2b9
4706 changed files with 931207 additions and 0 deletions

View File

@ -0,0 +1,66 @@
<?php
namespace RocketTheme\Toolbox\ArrayTraits;
/**
* Implements ArrayAccess interface.
*
* @package RocketTheme\Toolbox\ArrayTraits
* @author RocketTheme
* @license MIT
*
* @property array $items
*/
trait ArrayAccess
{
/**
* Whether or not an offset exists.
*
* @param mixed $offset An offset to check for.
* @return bool Returns TRUE on success or FALSE on failure.
*/
public function offsetExists($offset)
{
return isset($this->items[$offset]);
}
/**
* Returns the value at specified offset.
*
* @param mixed $offset The offset to retrieve.
* @return mixed Can return all value types.
*/
public function offsetGet($offset)
{
return isset($this->items[$offset]) ? $this->items[$offset] : null;
}
/**
* Assigns a value to the specified offset.
*
* @param mixed $offset The offset to assign the value to.
* @param mixed $value The value to set.
*/
public function offsetSet($offset, $value)
{
if (is_null($offset)) {
$this->items[] = $value;
} else {
$this->items[$offset] = $value;
}
}
/**
* Unsets an offset.
*
* @param mixed $offset The offset to unset.
*/
public function offsetUnset($offset)
{
// Hack to make Iterator trait work together with unset.
if (isset($this->iteratorUnset) && $offset == key($this->items)) {
$this->iteratorUnset = true;
}
unset($this->items[$offset]);
}
}

View File

@ -0,0 +1,57 @@
<?php
namespace RocketTheme\Toolbox\ArrayTraits;
/**
* Implements getters and setters.
*
* @package RocketTheme\Toolbox\ArrayTraits
* @author RocketTheme
* @license MIT
*/
trait ArrayAccessWithGetters
{
use ArrayAccess;
/**
* Magic setter method
*
* @param mixed $offset Asset name value
* @param mixed $value Asset value
*/
public function __set($offset, $value)
{
$this->offsetSet($offset, $value);
}
/**
* Magic getter method
*
* @param mixed $offset Asset name value
* @return mixed Asset value
*/
public function __get($offset)
{
return $this->offsetGet($offset);
}
/**
* Magic method to determine if the attribute is set
*
* @param mixed $offset Asset name value
* @return boolean True if the value is set
*/
public function __isset($offset)
{
return $this->offsetExists($offset);
}
/**
* Magic method to unset the attribute
*
* @param mixed $offset The name value to unset
*/
public function __unset($offset)
{
$this->offsetUnset($offset);
}
}

View File

@ -0,0 +1,24 @@
<?php
namespace RocketTheme\Toolbox\ArrayTraits;
/**
* Implements Constructor for setting items.
*
* @package RocketTheme\Toolbox\ArrayTraits
* @author RocketTheme
* @license MIT
*
* @property array $items
*/
trait Constructor
{
/**
* Constructor to initialize array.
*
* @param array $items Initial items inside the iterator.
*/
public function __construct(array $items = array())
{
$this->items = $items;
}
}

View File

@ -0,0 +1,24 @@
<?php
namespace RocketTheme\Toolbox\ArrayTraits;
/**
* Implements \Countable interface.
*
* @package RocketTheme\Toolbox\ArrayTraits
* @author RocketTheme
* @license MIT
*
* @property array $items
*/
trait Countable
{
/**
* Implements Countable interface.
*
* @return int
*/
public function count()
{
return count($this->items);
}
}

View File

@ -0,0 +1,51 @@
<?php
namespace RocketTheme\Toolbox\ArrayTraits;
use Symfony\Component\Yaml\Exception\DumpException;
use Symfony\Component\Yaml\Yaml;
/**
* Implements ExportInterface.
*
* @package RocketTheme\Toolbox\ArrayTraits
* @author RocketTheme
* @license MIT
*
* @property array $items
*/
trait Export
{
/**
* Convert object into an array.
*
* @return array
*/
public function toArray()
{
return $this->items;
}
/**
* Convert object into YAML string.
*
* @param int $inline The level where you switch to inline YAML.
* @param int $indent The amount of spaces to use for indentation of nested nodes.
*
* @return string A YAML string representing the object.
* @throws DumpException
*/
public function toYaml($inline = 3, $indent = 2)
{
return Yaml::dump($this->toArray(), $inline, $indent, true, false);
}
/**
* Convert object into JSON string.
*
* @return string
*/
public function toJson()
{
return json_encode($this->toArray());
}
}

View File

@ -0,0 +1,35 @@
<?php
namespace RocketTheme\Toolbox\ArrayTraits;
/**
* Defines Export interface.
*
* @package RocketTheme\Toolbox\ArrayTraits
* @author RocketTheme
* @license MIT
*/
interface ExportInterface
{
/**
* Convert object into an array.
*
* @return array
*/
public function toArray();
/**
* Convert object into YAML string.
*
* @param int $inline
* @param int $indent
* @return string
*/
public function toYaml($inline = 3, $indent = 2);
/**
* Convert object into JSON string.
*
* @return string
*/
public function toJson();
}

View File

@ -0,0 +1,77 @@
<?php
namespace RocketTheme\Toolbox\ArrayTraits;
/**
* Implements \Iterator interface.
*
* @package RocketTheme\Toolbox\ArrayTraits
* @author RocketTheme
* @license MIT
*
* @property array $items
*/
trait Iterator
{
/**
* Hack to make Iterator work together with unset().
*
* @var bool
*/
private $iteratorUnset = false;
/**
* Returns the current element.
*
* @return mixed Can return any type.
*/
public function current()
{
return current($this->items);
}
/**
* Returns the key of the current element.
*
* @return mixed Returns scalar on success, or NULL on failure.
*/
public function key()
{
return key($this->items);
}
/**
* Moves the current position to the next element.
*
* @return void
*/
public function next()
{
if ($this->iteratorUnset) {
// If current item was unset, position is already in the next element (do nothing).
$this->iteratorUnset = false;
} else {
next($this->items);
}
}
/**
* Rewinds back to the first element of the Iterator.
*
* @return void
*/
public function rewind()
{
$this->iteratorUnset = false;
reset($this->items);
}
/**
* This method is called after Iterator::rewind() and Iterator::next() to check if the current position is valid.
*
* @return bool Returns TRUE on success or FALSE on failure.
*/
public function valid()
{
return key($this->items) !== null;
}
}

View File

@ -0,0 +1,190 @@
<?php
namespace RocketTheme\Toolbox\ArrayTraits;
/**
* Implements nested ArrayAccess interface with dot notation.
*
* @package RocketTheme\Toolbox\ArrayTraits
* @author RocketTheme
* @license MIT
*
* @property array $items
*/
trait NestedArrayAccess
{
protected $nestedSeparator = '.';
/**
* Get value by using dot notation for nested arrays/objects.
*
* @example $value = $this->get('this.is.my.nested.variable');
*
* @param string $name Dot separated path to the requested value.
* @param mixed $default Default value (or null).
* @param string $separator Separator, defaults to '.'
* @return mixed Value.
*/
public function get($name, $default = null, $separator = null)
{
$path = explode($separator ?: $this->nestedSeparator, $name);
$current = $this->items;
foreach ($path as $field) {
if (is_object($current) && isset($current->{$field})) {
$current = $current->{$field};
} elseif (is_array($current) && isset($current[$field])) {
$current = $current[$field];
} else {
return $default;
}
}
return $current;
}
/**
* Set value by using dot notation for nested arrays/objects.
*
* @example $data->set('this.is.my.nested.variable', $value);
*
* @param string $name Dot separated path to the requested value.
* @param mixed $value New value.
* @param string $separator Separator, defaults to '.'
* @return $this
*/
public function set($name, $value, $separator = null)
{
$path = explode($separator ?: $this->nestedSeparator, $name);
$current = &$this->items;
foreach ($path as $field) {
if (is_object($current)) {
// Handle objects.
if (!isset($current->{$field})) {
$current->{$field} = array();
}
$current = &$current->{$field};
} else {
// Handle arrays and scalars.
if (!is_array($current)) {
$current = array($field => array());
} elseif (!isset($current[$field])) {
$current[$field] = array();
}
$current = &$current[$field];
}
}
$current = $value;
return $this;
}
/**
* Unset value by using dot notation for nested arrays/objects.
*
* @example $data->undef('this.is.my.nested.variable');
*
* @param string $name Dot separated path to the requested value.
* @param string $separator Separator, defaults to '.'
* @return $this
*/
public function undef($name, $separator = null)
{
if ($name === '') {
$this->items = [];
return $this;
}
$path = explode($separator ?: $this->nestedSeparator, $name);
$var = array_pop($path);
$current = &$this->items;
foreach ($path as $field) {
if (is_object($current)) {
// Handle objects.
if (!isset($current->{$field})) {
return $this;
}
$current = &$current->{$field};
} else {
// Handle arrays and scalars.
if (!is_array($current) || !isset($current[$field])) {
return $this;
}
$current = &$current[$field];
}
}
unset($current[$var]);
return $this;
}
/**
* Set default value by using dot notation for nested arrays/objects.
*
* @example $data->def('this.is.my.nested.variable', 'default');
*
* @param string $name Dot separated path to the requested value.
* @param mixed $default Default value (or null).
* @param string $separator Separator, defaults to '.'
* @return $this
*/
public function def($name, $default = null, $separator = null)
{
$this->set($name, $this->get($name, $default, $separator), $separator);
return $this;
}
/**
* Whether or not an offset exists.
*
* @param mixed $offset An offset to check for.
* @return bool Returns TRUE on success or FALSE on failure.
*/
public function offsetExists($offset)
{
return $this->get($offset) !== null;
}
/**
* Returns the value at specified offset.
*
* @param mixed $offset The offset to retrieve.
* @return mixed Can return all value types.
*/
public function offsetGet($offset)
{
return $this->get($offset);
}
/**
* Assigns a value to the specified offset.
*
* @param mixed $offset The offset to assign the value to.
* @param mixed $value The value to set.
*/
public function offsetSet($offset, $value)
{
if (is_null($offset)) {
$this->items[] = $value;
} else {
$this->set($offset, $value);
}
}
/**
* Unsets variable at specified offset.
*
* @param $offset
*/
public function offsetUnset($offset)
{
if (is_null($offset)) {
$this->items[] = [];
} else {
$this->undef($offset);
}
}
}

View File

@ -0,0 +1,57 @@
<?php
namespace RocketTheme\Toolbox\ArrayTraits;
/**
* Implements getters and setters.
*
* @package RocketTheme\Toolbox\ArrayTraits
* @author RocketTheme
* @license MIT
*/
trait NestedArrayAccessWithGetters
{
use NestedArrayAccess;
/**
* Magic setter method
*
* @param mixed $offset Asset name value
* @param mixed $value Asset value
*/
public function __set($offset, $value)
{
$this->offsetSet($offset, $value);
}
/**
* Magic getter method
*
* @param mixed $offset Asset name value
* @return mixed Asset value
*/
public function __get($offset)
{
return $this->offsetGet($offset);
}
/**
* Magic method to determine if the attribute is set
*
* @param mixed $offset Asset name value
* @return boolean True if the value is set
*/
public function __isset($offset)
{
return $this->offsetExists($offset);
}
/**
* Magic method to unset the attribute
*
* @param mixed $offset The name value to unset
*/
public function __unset($offset)
{
$this->offsetUnset($offset);
}
}

View File

@ -0,0 +1,34 @@
<?php
namespace RocketTheme\Toolbox\ArrayTraits;
/**
* Implements \Serializable interface.
*
* @package RocketTheme\Toolbox\ArrayTraits
* @author RocketTheme
* @license MIT
*
* @property array $items
*/
trait Serializable
{
/**
* Returns string representation of the object.
*
* @return string Returns the string representation of the object.
*/
public function serialize()
{
return serialize($this->items);
}
/**
* Called during unserialization of the object.
*
* @param string $serialized The string representation of the object.
*/
public function unserialize($serialized)
{
$this->items = unserialize($serialized);
}
}

View File

@ -0,0 +1,557 @@
<?php
namespace RocketTheme\Toolbox\Blueprints;
use RocketTheme\Toolbox\ArrayTraits\Export;
use RocketTheme\Toolbox\ArrayTraits\ExportInterface;
use RocketTheme\Toolbox\ArrayTraits\NestedArrayAccessWithGetters;
/**
* The Config class contains configuration information.
*
* @author RocketTheme
*/
abstract class BlueprintForm implements \ArrayAccess, ExportInterface
{
use NestedArrayAccessWithGetters, Export;
/**
* @var array
*/
protected $items;
/**
* @var string
*/
protected $filename;
/**
* @var string
*/
protected $context;
/**
* @var array
*/
protected $overrides = [];
/**
* @var array
*/
protected $dynamic = [];
/**
* Load file and return its contents.
*
* @param string $filename
* @return string
*/
abstract protected function loadFile($filename);
/**
* Get list of blueprint form files (file and its parents for overrides).
*
* @param string|array $path
* @param string $context
* @return array
*/
abstract protected function getFiles($path, $context = null);
/**
* Constructor.
*
* @param string|array $filename
* @param array $items
*/
public function __construct($filename = null, array $items = [])
{
$this->nestedSeparator = '/';
$this->filename = $filename;
$this->items = $items;
}
/**
* Set filename for the blueprint. Can also be array of files for parent lookup.
*
* @param string|array $filename
* @return $this
*/
public function setFilename($filename)
{
$this->filename = $filename;
return $this;
}
/**
* Get the filename of the blueprint.
*
* @return array|null|string
*/
public function getFilename()
{
return $this->filename;
}
/**
* Set context for import@ and extend@.
*
* @param $context
* @return $this
*/
public function setContext($context)
{
$this->context = $context;
return $this;
}
/**
* Set custom overrides for import@ and extend@.
*
* @param array $overrides
* @return $this
*/
public function setOverrides($overrides)
{
$this->overrides = $overrides;
return $this;
}
/**
* Load blueprint.
*
* @return $this
*/
public function load()
{
// Only load and extend blueprint if it has not yet been loaded.
if (empty($this->items) && $this->filename) {
// Get list of files.
$files = $this->getFiles($this->filename);
// Load and extend blueprints.
$data = $this->doLoad($files);
$this->items = (array) array_shift($data);
foreach ($data as $content) {
$this->extend($content, true);
}
}
// Import blueprints.
$this->deepInit($this->items);
return $this;
}
/**
* Initialize blueprints with its dynamic fields.
*
* @return $this
*/
public function init()
{
foreach ($this->dynamic as $key => $data) {
// Locate field.
$path = explode('/', $key);
$current = &$this->items;
foreach ($path as $field) {
if (is_object($current)) {
// Handle objects.
if (!isset($current->{$field})) {
$current->{$field} = array();
}
$current = &$current->{$field};
} else {
// Handle arrays and scalars.
if (!is_array($current)) {
$current = array($field => array());
} elseif (!isset($current[$field])) {
$current[$field] = array();
}
$current = &$current[$field];
}
}
// Set dynamic property.
foreach ($data as $property => $call) {
$action = 'dynamic' . ucfirst($call['action']);
if (method_exists($this, $action)) {
$this->{$action}($current, $property, $call);
}
}
}
return $this;
}
/**
* Get form.
*
* @return array
*/
public function form()
{
return (array) $this->get('form');
}
/**
* Get form fields.
*
* @return array
*/
public function fields()
{
return (array) $this->get('form/fields');
}
/**
* Extend blueprint with another blueprint.
*
* @param BlueprintForm|array $extends
* @param bool $append
* @return $this
*/
public function extend($extends, $append = false)
{
if ($extends instanceof BlueprintForm) {
$extends = $extends->toArray();
}
if ($append) {
$a = $this->items;
$b = $extends;
} else {
$a = $extends;
$b = $this->items;
}
$this->items = $this->deepMerge($a, $b);
return $this;
}
/**
* @param string $name
* @param mixed $value
* @param string $separator
* @param bool $append
* @return $this
*/
public function embed($name, $value, $separator = '/', $append = false)
{
$oldValue = $this->get($name, null, $separator);
if (is_array($oldValue) && is_array($value)) {
if ($append) {
$a = $oldValue;
$b = $value;
} else {
$a = $value;
$b = $oldValue;
}
$value = $this->deepMerge($a, $b);
}
$this->set($name, $value, $separator);
return $this;
}
/**
* Get blueprints by using dot notation for nested arrays/objects.
*
* @example $value = $this->resolve('this.is.my.nested.variable');
* returns ['this.is.my', 'nested.variable']
*
* @param array $path
* @param string $separator
* @return array
*/
public function resolve(array $path, $separator = '/')
{
$fields = false;
$parts = [];
$current = $this['form.fields'];
$result = [null, null, null];
while (($field = current($path)) !== null) {
if (!$fields && isset($current['fields'])) {
if (!empty($current['array'])) {
$result = [$current, $parts, $path ? implode($separator, $path) : null];
// Skip item offset.
$parts[] = array_shift($path);
}
$current = $current['fields'];
$fields = true;
} elseif (isset($current[$field])) {
$parts[] = array_shift($path);
$current = $current[$field];
$fields = false;
} elseif (isset($current['.' . $field])) {
$parts[] = array_shift($path);
$current = $current['.' . $field];
$fields = false;
} else {
break;
}
}
return $result;
}
/**
* Deep merge two arrays together.
*
* @param array $a
* @param array $b
* @return array
*/
protected function deepMerge(array $a, array $b)
{
$bref_stack = array(&$a);
$head_stack = array($b);
do {
end($bref_stack);
$bref = &$bref_stack[key($bref_stack)];
$head = array_pop($head_stack);
unset($bref_stack[key($bref_stack)]);
foreach (array_keys($head) as $key) {
if (!empty($key) && ($key[0] === '@' || $key[strlen($key) - 1] === '@')) {
$list = explode('-', trim($key, '@'), 2);
$action = array_shift($list);
if ($action === 'unset' || $action === 'replace') {
$property = array_shift($list);
if (!$property) {
$bref = ['unset@' => true];
} else {
unset($bref[$property]);
}
continue;
}
}
if (isset($key, $bref[$key]) && is_array($bref[$key]) && is_array($head[$key])) {
$bref_stack[] = &$bref[$key];
$head_stack[] = $head[$key];
} else {
$bref = array_merge($bref, [$key => $head[$key]]);
}
}
} while (count($head_stack));
return $a;
}
/**
* @param array $items
* @param array $path
* @return string
*/
protected function deepInit(array &$items, $path = [])
{
$ordering = '';
$order = [];
$field = end($path) === 'fields';
foreach ($items as $key => &$item) {
// Set name for nested field.
if ($field && isset($item['type'])) {
$item['name'] = $key;
}
// Handle special instructions in the form.
if (!empty($key) && ($key[0] === '@' || $key[strlen($key) - 1] === '@')) {
$list = explode('-', trim($key, '@'), 2);
$action = array_shift($list);
$property = array_shift($list);
switch ($action) {
case 'unset':
unset($items[$key]);
if (empty($items)) {
return null;
}
break;
case 'import':
$this->doImport($item, $path);
unset($items[$key]);
break;
case 'ordering':
$ordering = $item;
unset($items[$key]);
break;
default:
$this->dynamic[implode('/', $path)][$property] = ['action' => $action, 'params' => $item];
}
} elseif (is_array($item)) {
// Recursively initialize form.
$newPath = array_merge($path, [$key]);
$location = $this->deepInit($item, $newPath);
if ($location) {
$order[$key] = $location;
} elseif ($location === null) {
unset($items[$key]);
}
}
}
if ($order) {
// Reorder fields if needed.
$items = $this->doReorder($items, $order);
}
return $ordering;
}
/**
* @param array|string $value
* @param array $path
*/
protected function doImport(&$value, array &$path)
{
$type = !is_string($value) ? !isset($value['type']) ? null : $value['type'] : $value;
$files = $this->getFiles($type, isset($value['context']) ? $value['context'] : null);
if (!$files) {
return;
}
/** @var BlueprintForm $blueprint */
$blueprint = new static($files);
$blueprint->setContext($this->context)->setOverrides($this->overrides)->load();
$name = implode('/', $path);
$this->embed($name, $blueprint->form(), '/', false);
}
/**
* Internal function that handles loading extended blueprints.
*
* @param array $files
* @return array
*/
protected function doLoad(array $files)
{
$filename = array_shift($files);
$content = $this->loadFile($filename);
if (isset($content['extends@'])) {
$key = 'extends@';
} elseif (isset($content['@extends'])) {
$key = '@extends';
} elseif (isset($content['@extends@'])) {
$key = '@extends@';
}
$data = isset($key) ? $this->doExtend($filename, $files, (array) $content[$key]) : [];
if (isset($key)) {
unset($content[$key]);
}
$data[] = $content;
return $data;
}
/**
* Internal function to recursively load extended blueprints.
*
* @param string $filename
* @param array $parents
* @param array $extends
* @return array
*/
protected function doExtend($filename, array $parents, array $extends)
{
if (is_string(key($extends))) {
$extends = [$extends];
}
$data = [];
foreach ($extends as $value) {
// Accept array of type and context or a string.
$type = !is_string($value)
? !isset($value['type']) ? null : $value['type'] : $value;
if (!$type) {
continue;
}
if ($type === '@parent' || $type === 'parent@') {
if (!$parents) {
throw new \RuntimeException("Parent blueprint missing for '{$filename}'");
}
$files = $parents;
} else {
$files = $this->getFiles($type, isset($value['context']) ? $value['context'] : null);
// Detect extend loops.
if ($files && array_intersect($files, $parents)) {
// Let's check if user really meant extends@: parent@.
$index = array_search($filename, $files);
if ($index !== false) {
// We want to grab only the parents of the file which is currently being loaded.
$files = array_slice($files, $index + 1);
}
if ($files !== $parents) {
throw new \RuntimeException("Loop detected while extending blueprint file '{$filename}'");
}
if (!$parents) {
throw new \RuntimeException("Parent blueprint missing for '{$filename}'");
}
}
}
if ($files) {
$data = array_merge($data, $this->doLoad($files));
}
}
return $data;
}
/**
* Internal function to reorder items.
*
* @param array $items
* @param array $keys
* @return array
*/
protected function doReorder(array $items, array $keys)
{
$reordered = array_keys($items);
foreach ($keys as $item => $ordering) {
if ((string)(int) $ordering === (string) $ordering) {
$location = array_search($item, $reordered);
$rel = array_splice($reordered, $location, 1);
array_splice($reordered, $ordering, 0, $rel);
} elseif (isset($items[$ordering])) {
$location = array_search($item, $reordered);
$rel = array_splice($reordered, $location, 1);
$location = array_search($ordering, $reordered);
array_splice($reordered, $location + 1, 0, $rel);
}
}
return array_merge(array_flip($reordered), $items);
}
}

View File

@ -0,0 +1,685 @@
<?php
namespace RocketTheme\Toolbox\Blueprints;
/**
* BlueprintSchema is used to define a data structure.
*
* @package RocketTheme\Toolbox\Blueprints
* @author RocketTheme
* @license MIT
*/
class BlueprintSchema
{
/**
* @var array
*/
protected $items = [];
/**
* @var array
*/
protected $rules = [];
/**
* @var array
*/
protected $nested = [];
/**
* @var array
*/
protected $dynamic = [];
/**
* @var array
*/
protected $filter = ['validation' => true];
/**
* @var array
*/
protected $ignoreFormKeys = ['fields' => 1];
/**
* @var array
*/
protected $types = [];
/**
* Constructor.
*
* @param array $serialized Serialized content if available.
*/
public function __construct($serialized = null)
{
if (is_array($serialized) && !empty($serialized)) {
$this->items = (array) $serialized['items'];
$this->rules = (array) $serialized['rules'];
$this->nested = (array) $serialized['nested'];
$this->dynamic = (array) $serialized['dynamic'];
$this->filter = (array) $serialized['filter'];
}
}
/**
* @param array $types
* @return $this
*/
public function setTypes(array $types)
{
$this->types = $types;
return $this;
}
/**
* Restore Blueprints object.
*
* @param array $serialized
* @return static
*/
public static function restore(array $serialized)
{
return new static($serialized);
}
/**
* Initialize blueprints with its dynamic fields.
*
* @return $this
*/
public function init()
{
foreach ($this->dynamic as $key => $data) {
$field = &$this->items[$key];
foreach ($data as $property => $call) {
$action = 'dynamic' . ucfirst($call['action']);
if (method_exists($this, $action)) {
$this->{$action}($field, $property, $call);
}
}
}
return $this;
}
/**
* Set filter for inherited properties.
*
* @param array $filter List of field names to be inherited.
*/
public function setFilter(array $filter)
{
$this->filter = array_flip($filter);
}
/**
* Get value by using dot notation for nested arrays/objects.
*
* @example $value = $data->get('this.is.my.nested.variable');
*
* @param string $name Dot separated path to the requested value.
* @param mixed $default Default value (or null).
* @param string $separator Separator, defaults to '.'
*
* @return mixed Value.
*/
public function get($name, $default = null, $separator = '.')
{
$name = $separator != '.' ? strtr($name, $separator, '.') : $name;
return isset($this->items[$name]) ? $this->items[$name] : $default;
}
/**
* Set value by using dot notation for nested arrays/objects.
*
* @example $value = $data->set('this.is.my.nested.variable', $newField);
*
* @param string $name Dot separated path to the requested value.
* @param mixed $value New value.
* @param string $separator Separator, defaults to '.'
*/
public function set($name, $value, $separator = '.')
{
$name = $separator != '.' ? strtr($name, $separator, '.') : $name;
$this->items[$name] = $value;
$this->addProperty($name);
}
/**
* Define value by using dot notation for nested arrays/objects.
*
* @example $value = $data->set('this.is.my.nested.variable', true);
*
* @param string $name Dot separated path to the requested value.
* @param mixed $value New value.
* @param string $separator Separator, defaults to '.'
*/
public function def($name, $value, $separator = '.')
{
$this->set($name, $this->get($name, $value, $separator), $separator);
}
/**
* @return array
* @deprecated
*/
public function toArray()
{
return $this->getState();
}
/**
* Convert object into an array.
*
* @return array
*/
public function getState()
{
return [
'items' => $this->items,
'rules' => $this->rules,
'nested' => $this->nested,
'dynamic' => $this->dynamic,
'filter' => $this->filter
];
}
/**
* Get nested structure containing default values defined in the blueprints.
*
* Fields without default value are ignored in the list.
*
* @return array
*/
public function getDefaults()
{
return $this->buildDefaults($this->nested);
}
/**
* Embed an array to the blueprint.
*
* @param $name
* @param array $value
* @param string $separator
* @param bool $merge Merge fields instead replacing them.
* @return $this
*/
public function embed($name, array $value, $separator = '.', $merge = false)
{
if (isset($value['rules'])) {
$this->rules = array_merge($this->rules, $value['rules']);
}
if (!isset($value['form']['fields']) || !is_array($value['form']['fields'])) {
$value['form']['fields'] = [];
}
$name = $separator != '.' ? strtr($name, $separator, '.') : $name;
$form = array_diff_key($value['form'], ['fields' => 1]);
$items = isset($this->items[$name]) ? $this->items[$name] : ['type' => '_root', 'form_field' => false];
$this->items[$name] = [
'form' => $form
] + $items;
$this->addProperty($name);
$prefix = $name ? $name . '.' : '';
$params = array_intersect_key($form, $this->filter);
$location = [$name];
$this->parseFormFields($value['form']['fields'], $params, $prefix, '', $merge, $location);
return $this;
}
/**
* Merge two arrays by using blueprints.
*
* @param array $data1
* @param array $data2
* @param string $name Optional
* @param string $separator Optional
* @return array
*/
public function mergeData(array $data1, array $data2, $name = null, $separator = '.')
{
$nested = $this->getNested($name, $separator);
if (!is_array($nested)) {
$nested = [];
}
return $this->mergeArrays($data1, $data2, $nested);
}
/**
* Get the property with given path.
*
* @param string $path
* @param string $separator
* @return mixed
*/
public function getProperty($path = null, $separator = '.')
{
$name = $this->getPropertyName($path, $separator);
$property = $this->get($name);
$nested = $this->getNested($name);
return $this->getPropertyRecursion($property, $nested);
}
/**
* Returns name of the property with given path.
*
* @param string $path
* @param string $separator
* @return string
*/
public function getPropertyName($path = null, $separator = '.')
{
$parts = explode($separator, $path);
$nested = $this->nested;
$result = [];
while (($part = array_shift($parts)) !== null) {
if (!isset($nested[$part])) {
if (isset($nested['*'])) {
$part = '*';
} else {
return implode($separator, array_merge($result, [$part], $parts));
}
}
$result[] = $part;
$nested = $nested[$part];
}
return implode('.', $result);
}
/**
* Return data fields that do not exist in blueprints.
*
* @param array $data
* @param string $prefix
* @return array
*/
public function extra(array $data, $prefix = '')
{
$rules = $this->nested;
// Drill down to prefix level
if (!empty($prefix)) {
$parts = explode('.', trim($prefix, '.'));
foreach ($parts as $part) {
$rules = isset($rules[$part]) ? $rules[$part] : [];
}
}
return $this->extraArray($data, $rules, $prefix);
}
/**
* Get the property with given path.
*
* @param $property
* @param $nested
* @return mixed
*/
protected function getPropertyRecursion($property, $nested)
{
if (!isset($property['type']) || empty($nested) || !is_array($nested)) {
return $property;
}
if ($property['type'] === '_root') {
foreach ($nested as $key => $value) {
if ($key === '') {
continue;
}
$name = is_array($value) ? $key : $value;
$property['fields'][$key] = $this->getPropertyRecursion($this->get($name), $value);
}
} elseif ($property['type'] === '_parent' || !empty($property['array'])) {
foreach ($nested as $key => $value) {
$name = is_array($value) ? "{$property['name']}.{$key}" : $value;
$property['fields'][$key] = $this->getPropertyRecursion($this->get($name), $value);
}
}
return $property;
}
/**
* Get property from the definition.
*
* @param string $path Comma separated path to the property.
* @param string $separator
* @return array|string|null
* @internal
*/
protected function getNested($path = null, $separator = '.')
{
if (!$path) {
return $this->nested;
}
$parts = explode($separator, $path);
$item = array_pop($parts);
$nested = $this->nested;
foreach ($parts as $part) {
if (!isset($nested[$part])) {
$part = '*';
if (!isset($nested[$part])) {
return [];
}
}
$nested = $nested[$part];
}
return isset($nested[$item]) ? $nested[$item] : (isset($nested['*']) ? $nested['*'] : null);
}
/**
* @param array $nested
* @return array
*/
protected function buildDefaults(array $nested)
{
$defaults = [];
foreach ($nested as $key => $value) {
if ($key === '*') {
// TODO: Add support for adding defaults to collections.
continue;
}
if (is_array($value)) {
// Recursively fetch the items.
$list = $this->buildDefaults($value);
// Only return defaults if there are any.
if (!empty($list)) {
$defaults[$key] = $list;
}
} else {
// We hit a field; get default from it if it exists.
$item = $this->get($value);
// Only return default value if it exists.
if (isset($item['default'])) {
$defaults[$key] = $item['default'];
}
}
}
return $defaults;
}
/**
* @param array $data1
* @param array $data2
* @param array $rules
* @return array
* @internal
*/
protected function mergeArrays(array $data1, array $data2, array $rules)
{
foreach ($data2 as $key => $field) {
$val = isset($rules[$key]) ? $rules[$key] : null;
$rule = is_string($val) ? $this->items[$val] : null;
if (!empty($rule['type']) && $rule['type'][0] === '_'
|| (array_key_exists($key, $data1) && is_array($data1[$key]) && is_array($field) && is_array($val) && !isset($val['*']))
) {
// Array has been defined in blueprints and is not a collection of items.
$data1[$key] = $this->mergeArrays($data1[$key], $field, $val);
} else {
// Otherwise just take value from the data2.
$data1[$key] = $field;
}
}
return $data1;
}
/**
* Gets all field definitions from the blueprints.
*
* @param array $fields Fields to parse.
* @param array $params Property parameters.
* @param string $prefix Property prefix.
* @param string $parent Parent property.
* @param bool $merge Merge fields instead replacing them.
* @param array $formPath
*/
protected function parseFormFields(array $fields, array $params, $prefix = '', $parent = '', $merge = false, array $formPath = [])
{
// Go though all the fields in current level.
foreach ($fields as $key => $field) {
$key = $this->getFieldKey($key, $prefix, $parent);
$newPath = array_merge($formPath, [$key]);
$properties = array_diff_key($field, $this->ignoreFormKeys) + $params;
$properties['name'] = $key;
// Set default properties for the field type.
$type = isset($properties['type']) ? $properties['type'] : '';
if (isset($this->types[$type])) {
$properties += $this->types[$type];
}
// Merge properties with existing ones.
if ($merge && isset($this->items[$key])) {
$properties += $this->items[$key];
}
$isInputField = !isset($properties['input@']) || $properties['input@'];
if (!$isInputField) {
// Remove property if it exists.
if (isset($this->items[$key])) {
$this->removeProperty($key);
}
} elseif (!isset($this->items[$key])) {
// Add missing property.
$this->addProperty($key);
}
if (isset($field['fields'])) {
// Recursively get all the nested fields.
$isArray = !empty($properties['array']);
$newParams = array_intersect_key($properties, $this->filter);
$this->parseFormFields($field['fields'], $newParams, $prefix, $key . ($isArray ? '.*': ''), $merge, $newPath);
} else {
if (!isset($this->items[$key])) {
// Add parent rules.
$path = explode('.', $key);
array_pop($path);
$parent = '';
foreach ($path as $part) {
$parent .= ($parent ? '.' : '') . $part;
if (!isset($this->items[$parent])) {
$this->items[$parent] = ['type' => '_parent', 'name' => $parent, 'form_field' => false];
}
}
}
if ($isInputField) {
$this->parseProperties($key, $properties);
}
}
if ($isInputField) {
$this->items[$key] = $properties;
}
}
}
protected function getFieldKey($key, $prefix, $parent)
{
// Set name from the array key.
if ($key && $key[0] == '.') {
return ($parent ?: rtrim($prefix, '.')) . $key;
}
return $prefix . $key;
}
protected function parseProperties($key, array &$properties)
{
$key = ltrim($key, '.');
if (!empty($properties['data'])) {
$this->dynamic[$key] = $properties['data'];
}
foreach ($properties as $name => $value) {
if (!empty($name) && ($name[0] === '@' || $name[strlen($name) - 1] === '@')) {
$list = explode('-', trim($name, '@'), 2);
$action = array_shift($list);
$property = array_shift($list);
$this->dynamic[$key][$property] = ['action' => $action, 'params' => $value];
}
}
// Initialize predefined validation rule.
if (isset($properties['validate']['rule'])) {
$properties['validate'] += $this->getRule($properties['validate']['rule']);
}
}
/**
* Add property to the definition.
*
* @param string $path Comma separated path to the property.
* @internal
*/
protected function addProperty($path)
{
$parts = explode('.', $path);
$item = array_pop($parts);
$nested = &$this->nested;
foreach ($parts as $part) {
if (!isset($nested[$part]) || !is_array($nested[$part])) {
$nested[$part] = [];
}
$nested = &$nested[$part];
}
if (!isset($nested[$item])) {
$nested[$item] = $path;
}
}
/**
* Remove property to the definition.
*
* @param string $path Comma separated path to the property.
* @internal
*/
protected function removeProperty($path)
{
$parts = explode('.', $path);
$item = array_pop($parts);
$nested = &$this->nested;
foreach ($parts as $part) {
if (!isset($nested[$part]) || !is_array($nested[$part])) {
return;
}
$nested = &$nested[$part];
}
if (isset($nested[$item])) {
unset($nested[$item]);
}
}
/**
* @param $rule
* @return array
* @internal
*/
protected function getRule($rule)
{
if (isset($this->rules[$rule]) && is_array($this->rules[$rule])) {
return $this->rules[$rule];
}
return [];
}
/**
* @param array $data
* @param array $rules
* @param string $prefix
* @return array
* @internal
*/
protected function extraArray(array $data, array $rules, $prefix)
{
$array = [];
foreach ($data as $key => $field) {
$val = isset($rules[$key]) ? $rules[$key] : (isset($rules['*']) ? $rules['*'] : null);
$rule = is_string($val) ? $this->items[$val] : null;
if ($rule || isset($val['*'])) {
// Item has been defined in blueprints.
} elseif (is_array($field) && is_array($val)) {
// Array has been defined in blueprints.
$array += $this->ExtraArray($field, $val, $prefix . $key . '.');
} else {
// Undefined/extra item.
$array[$prefix.$key] = $field;
}
}
return $array;
}
/**
* @param array $field
* @param string $property
* @param array $call
*/
protected function dynamicData(array &$field, $property, array $call)
{
$params = $call['params'];
if (is_array($params)) {
$function = array_shift($params);
} else {
$function = $params;
$params = [];
}
$list = preg_split('/::/', $function, 2);
$f = array_pop($list);
$o = array_pop($list);
if (!$o) {
if (function_exists($f)) {
$data = call_user_func_array($f, $params);
}
} else {
if (method_exists($o, $f)) {
$data = call_user_func_array(array($o, $f), $params);
}
}
// If function returns a value,
if (isset($data)) {
if (isset($field[$property]) && is_array($field[$property]) && is_array($data)) {
// Combine field and @data-field together.
$field[$property] += $data;
} else {
// Or create/replace field with @data-field.
$field[$property] = $data;
}
}
}
}

View File

@ -0,0 +1,12 @@
<?php
namespace RocketTheme\Toolbox\Blueprints;
/**
* Deprecated class, use BlueprintSchema instead.
*
* @package RocketTheme\Toolbox\Blueprints
* @author RocketTheme
* @license MIT
* @deprecated
*/
class Blueprints extends BlueprintSchema {}

View File

@ -0,0 +1,96 @@
<?php
use RocketTheme\Toolbox\Blueprints\BlueprintForm;
use RocketTheme\Toolbox\File\YamlFile;
require_once 'helper.php';
class BlueprintsBlueprintFormTest extends PHPUnit_Framework_TestCase
{
/**
* @dataProvider dataProvider
*/
public function testLoad($test)
{
$blueprint = new Blueprint($test);
$blueprint->setOverrides(
['extends' => ['extends', 'basic']]
);
$blueprint->load();
// Save test results if they do not exist (data needs to be verified by human!)
/*
$resultFile = YamlFile::instance(__DIR__ . '/data/form/items/' . $test . '.yaml');
if (!$resultFile->exists()) {
$resultFile->content($blueprint->toArray());
$resultFile->save();
}
*/
// Test 1: Loaded form.
$this->assertEquals($this->loadYaml($test, 'form/items'), $blueprint->toArray());
}
public function dataProvider()
{
return [
['empty'],
['basic'],
['import'],
['extends']
];
}
protected function loadYaml($test, $type = 'blueprint')
{
$file = YamlFile::instance(__DIR__ . "/data/{$type}/{$test}.yaml");
$content = $file->content();
$file->free();
return $content;
}
}
class Blueprint extends BlueprintForm
{
/**
* @param string $filename
* @return string
*/
protected function loadFile($filename)
{
$file = YamlFile::instance(__DIR__ . "/data/blueprint/{$filename}.yaml");
$content = $file->content();
$file->free();
return $content;
}
/**
* @param string|array $path
* @param string $context
* @return array
*/
protected function getFiles($path, $context = null)
{
if (is_string($path)) {
// Resolve filename.
if (isset($this->overrides[$path])) {
$path = $this->overrides[$path];
} else {
if ($context === null) {
$context = $this->context;
}
if ($context && $context[strlen($context)-1] !== '/') {
$context .= '/';
}
$path = $context . $path;
}
}
$files = (array) $path;
return $files;
}
}

View File

@ -0,0 +1,90 @@
<?php
use RocketTheme\Toolbox\Blueprints\BlueprintSchema;
use RocketTheme\Toolbox\File\YamlFile;
require_once 'helper.php';
class BlueprintsBlueprintSchemaTest extends PHPUnit_Framework_TestCase
{
public function testCreation()
{
$blueprints = new BlueprintSchema;
$this->assertEquals(
[
'items' => [],
'rules' => [],
'nested' => [],
'dynamic' => [],
'filter' => ['validation' => true]
],
$blueprints->getState());
$this->assertEquals([], $blueprints->getDefaults());
}
/**
* @dataProvider dataProvider
*/
public function testLoad($test)
{
$input = $this->loadYaml($test);
$blueprint = new BlueprintSchema;
$blueprint->embed('', $input);
// Save test results if they do not exist (data needs to be verified by human!)
/*
$resultFile = YamlFile::instance(__DIR__ . '/data/schema/state/' . $test . '.yaml');
if (!$resultFile->exists()) {
$resultFile->content($blueprint->getState());
$resultFile->save();
}
*/
// Test 1: Internal state.
$this->assertEquals($this->loadYaml($test, 'schema/state'), $blueprint->getState());
// Save test results if they do not exist (data needs to be verified by human!)
$resultFile = YamlFile::instance(__DIR__ . '/data/schema/init/' . $test . '.yaml');
if (!$resultFile->exists()) {
$resultFile->content($blueprint->init()->getState());
$resultFile->save();
}
// Test 2: Initialize blueprint.
$this->assertEquals($this->loadYaml($test, 'schema/init'), $blueprint->init()->getState());
// Test 3: Default values.
$this->assertEquals($this->loadYaml($test, 'schema/defaults'), $blueprint->getDefaults());
// Test 4: Extra values.
$this->assertEquals($this->loadYaml($test, 'schema/extra'), $blueprint->extra($this->loadYaml($test, 'input')));
// Test 5: Merge data.
$this->assertEquals(
$this->loadYaml($test, 'schema/merge'),
$blueprint->mergeData($blueprint->getDefaults(), $this->loadYaml($test, 'input'))
);
}
public function dataProvider()
{
return [
['empty'],
['basic'],
];
}
protected function loadYaml($test, $type = 'blueprint')
{
$file = YamlFile::instance(__DIR__ . "/data/{$type}/{$test}.yaml");
$content = $file->content();
$file->free();
return $content;
}
}

View File

@ -0,0 +1,41 @@
rules:
name:
pattern: "[A-Z][a-z]+"
min: 3
max: 15
form:
validation: loose
fields:
text:
type: text
label: Text
placeholder: 'Enter your text'
enabled:
type: select
label: Enabled
default: true
data-options@: blueprint_data_option_test
user.name:
type: name
label: Name
default: John
validate:
type: name
user.country:
type: select
label: Enabled
default: fi
data-options@:
- blueprint_data_option_test
- { us: 'United States', fi: 'Finland', po: 'Poland' }
- true
undefined:
type: select
label: Undefined
data-options@: undefined_function

View File

@ -0,0 +1,17 @@
extends@: parent@
form:
fields:
enabled:
default: false
text:
default: My text
ordering@: enabled
user.name:
default: Joe
unset-validate@: true
undefined:
unset@: true

View File

@ -0,0 +1,4 @@
form:
fields:
address:
import@: partials/address

View File

@ -0,0 +1,17 @@
form:
fields:
user.address:
type: text
label: Address
user.zip:
type: text
label: ZIP code
user.city:
type: text
label: City
user.country:
type: text
label: Country

View File

@ -0,0 +1,40 @@
rules:
name:
pattern: '[A-Z][a-z]+'
min: 3
max: 15
form:
validation: loose
fields:
text:
type: text
label: Text
placeholder: 'Enter your text'
name: text
enabled:
type: select
label: Enabled
default: true
data-options@: blueprint_data_option_test
name: enabled
user.name:
type: name
label: Name
default: John
validate:
type: name
name: user.name
user.country:
type: select
label: Enabled
default: fi
data-options@:
- blueprint_data_option_test
- { us: 'United States', fi: Finland, po: Poland }
- true
name: user.country
undefined:
type: select
label: Undefined
data-options@: undefined_function
name: undefined

View File

@ -0,0 +1,34 @@
rules:
name:
pattern: '[A-Z][a-z]+'
min: 3
max: 15
form:
validation: loose
fields:
enabled:
type: select
label: Enabled
default: false
data-options@: blueprint_data_option_test
name: enabled
text:
type: text
label: Text
placeholder: 'Enter your text'
default: 'My text'
name: text
user.name:
type: name
label: Name
default: Joe
name: user.name
user.country:
type: select
label: Enabled
default: fi
data-options@:
- blueprint_data_option_test
- { us: 'United States', fi: Finland, po: Poland }
- true
name: user.country

View File

@ -0,0 +1,8 @@
form:
fields:
address:
fields:
user.address: { type: text, label: Address, name: user.address }
user.zip: { type: text, label: 'ZIP code', name: user.zip }
user.city: { type: text, label: City, name: user.city }
user.country: { type: text, label: Country, name: user.country }

View File

@ -0,0 +1,12 @@
text: Testing...
user:
name: Igor
extra: data...
some:
random: false
variables:
- true
- false
just: for
fun:

View File

@ -0,0 +1,7 @@
some:
random: false
variables:
- true
- false
just: for
fun:

View File

@ -0,0 +1,4 @@
enabled: true
user:
name: John
country: fi

View File

@ -0,0 +1,8 @@
some:
random: false
variables:
- true
- false
just: for
fun:
user.extra: data...

View File

@ -0,0 +1,7 @@
some:
random: false
variables:
- true
- false
just: for
fun:

View File

@ -0,0 +1,88 @@
items:
'':
form:
validation: loose
type: _root
form_field: false
text:
type: text
label: Text
placeholder: 'Enter your text'
validation: loose
name: text
enabled:
type: select
label: Enabled
default: true
data-options@: blueprint_data_option_test
validation: loose
name: enabled
options:
'yes': 'Yes'
'no': 'No'
user:
type: _parent
name: user
form_field: false
user.name:
type: name
label: Name
default: John
validate:
type: name
validation: loose
name: user.name
user.country:
type: select
label: Enabled
default: fi
data-options@:
- blueprint_data_option_test
-
us: 'United States'
fi: Finland
po: Poland
- true
validation: loose
name: user.country
options:
fi: Finland
po: Poland
us: 'United States'
undefined:
type: select
label: Undefined
data-options@: undefined_function
validation: loose
name: undefined
rules:
name:
pattern: '[A-Z][a-z]+'
min: 3
max: 15
nested:
'': ''
text: text
enabled: enabled
user:
name: user.name
country: user.country
undefined: undefined
dynamic:
enabled:
options:
action: data
params: blueprint_data_option_test
user.country:
options:
action: data
params:
- blueprint_data_option_test
- { us: 'United States', fi: Finland, po: Poland }
- true
undefined:
options:
action: data
params: undefined_function
filter:
validation: true

View File

@ -0,0 +1,11 @@
items:
'':
form: { }
type: _root
form_field: false
rules: { }
nested:
'': ''
dynamic: { }
filter:
validation: true

View File

@ -0,0 +1,14 @@
text: Testing...
user:
name: Igor
country: fi
extra: data...
some:
random: false
variables:
- true
- false
just: for
fun:
enabled: true

View File

@ -0,0 +1,7 @@
some:
random: false
variables:
- true
- false
just: for
fun:

View File

@ -0,0 +1,81 @@
items:
'':
form:
validation: loose
type: _root
form_field: false
text:
type: text
label: Text
placeholder: 'Enter your text'
validation: loose
name: text
enabled:
type: select
label: Enabled
default: true
data-options@: blueprint_data_option_test
validation: loose
name: enabled
user:
type: _parent
name: user
form_field: false
user.name:
type: name
label: Name
default: John
validate:
type: name
validation: loose
name: user.name
user.country:
type: select
label: Enabled
default: fi
data-options@:
- blueprint_data_option_test
-
us: 'United States'
fi: Finland
po: Poland
- true
validation: loose
name: user.country
undefined:
type: select
label: Undefined
data-options@: undefined_function
validation: loose
name: undefined
rules:
name:
pattern: '[A-Z][a-z]+'
min: 3
max: 15
nested:
'': ''
text: text
enabled: enabled
user:
name: user.name
country: user.country
undefined: undefined
dynamic:
enabled:
options:
action: data
params: blueprint_data_option_test
user.country:
options:
action: data
params:
- blueprint_data_option_test
- { us: 'United States', fi: Finland, po: Poland }
- true
undefined:
options:
action: data
params: undefined_function
filter:
validation: true

View File

@ -0,0 +1,11 @@
items:
'':
form: { }
type: _root
form_field: false
rules: { }
nested:
'': ''
dynamic: { }
filter:
validation: true

View File

@ -0,0 +1,9 @@
<?php
function blueprint_data_option_test(array $param = null, $sort = false)
{
if ($sort) {
asort($param);
}
return $param ?: ['yes' => 'Yes', 'no' => 'No'];
}

View File

@ -0,0 +1,77 @@
# v1.3.3
## XX/XX/2016
2. [](#improved)
* Allow calls without parameter in UniformResourceLocator::getPaths()
* Add support for BlueprintSchema::getPropertyName() and getProperty()
* Add domain parameter to Session constructor
* Add support for `FilesystemIterator::FOLLOW_SYMLINKS` in RecursiveUniformResourceIterator class
# v1.3.2
## 05/24/2016
1. [](#new)
* Added a new function BlueprintForm::getFilename()
3. [](#bugfix)
* BlueprintsForm: Detect if user really meant to extend parent blueprint, not another one
# v1.3.1
## 04/25/2016
1. [](#new)
* Add new function File::rename()
* Add new function UniformResourceLocator::fillCache()
3. [](#bugfix)
* Fix collections support in BluprintSchema::extra()
* Fix exception in stream wrapper when scheme is not defined in locator
* Prevent UniformResourceLocator from resolving paths outside of defined scheme paths (#8)
* Fix breaking YAML files which start with three dashes (#5)
# v1.3.0
## 03/07/2016
1. [](#new)
* Add new function UniformResourceLocator::isStream()
* Add new class BlueprintForm
* Renamed Blueprints class into BlueprintSchema
* Add new function BlueprintSchema::extra() to return data fields which haven't been defined in blueprints
* Add support to unset and replace blueprint fields and properties
* Allow arbitrary dynamic fields in Blueprints (property@)
* Add default properties support for form field types
* Remove dependency on ircmaxell/password-compat
* Add support for Symfony 3
* Add a few unit tests
2. [](#improved)
* UniformResourceLocator::addPath(): Add option to add path after existing one (falls back to be last if path is not found)
3. [](#bugfix)
* Fix blueprint without a form
* Fix merging data with empty blueprint
# v1.2.0
## 10/24/2015
1. [](#new)
* **Backwards compatibility break**: Blueprints class needs to be initialized with `init()` if blueprints contain `@data-*` fields
* Renamed NestedArrayAccess::remove() into NestedArrayAccess::undef() to avoid name clashes
# v1.1.4
## 10/15/2015
1. [](#new)
* Add support for native YAML parsing option to Markdown and YAML file classes
# v1.1.3
## 09/14/2015
3. [](#bugfix)
* Fix regression: Default values for collections were broken
* Fix Argument 1 passed to `RocketTheme\Toolbox\Blueprints\Blueprints::mergeArrays()` must be of the type array
* Add exception on Blueprint collection merging; only overridden value should be used
* File locking truncates contents of the file
* Stop duplicate Messages getting added to the queue
# v1.1.2
## 08/27/2015
1. [](#new)
* Creation of Changelog

View File

@ -0,0 +1,15 @@
<?php
namespace RocketTheme\Toolbox\DI;
use Pimple\Container as BaseContainer;
/**
* Implements Dependency Injection Container.
*
* @package RocketTheme\Toolbox\DI
* @author RocketTheme
* @license MIT
*/
class Container extends BaseContainer
{
}

View File

@ -0,0 +1,15 @@
<?php
namespace RocketTheme\Toolbox\DI;
use \Pimple\ServiceProviderInterface as BaseServiceProviderInterface;
/**
* Defines ServiceProviderInterface.
*
* @package RocketTheme\Toolbox\DI
* @author RocketTheme
* @license MIT
*/
interface ServiceProviderInterface extends BaseServiceProviderInterface
{
}

View File

@ -0,0 +1,23 @@
<?php
namespace RocketTheme\Toolbox\Event;
use RocketTheme\Toolbox\ArrayTraits\ArrayAccess;
use RocketTheme\Toolbox\ArrayTraits\Constructor;
use Symfony\Component\EventDispatcher\Event as BaseEvent;
/**
* Implements Symfony Event interface.
*
* @package RocketTheme\Toolbox\Event
* @author RocketTheme
* @license MIT
*/
class Event extends BaseEvent implements \ArrayAccess
{
use ArrayAccess, Constructor;
/**
* @var array
*/
protected $items = array();
}

View File

@ -0,0 +1,25 @@
<?php
namespace RocketTheme\Toolbox\Event;
use Symfony\Component\EventDispatcher\Event as BaseEvent;
use Symfony\Component\EventDispatcher\EventDispatcher as BaseEventDispatcher;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
/**
* Implements Symfony EventDispatcher interface.
*
* @package RocketTheme\Toolbox\Event
* @author RocketTheme
* @license MIT
*/
class EventDispatcher extends BaseEventDispatcher implements EventDispatcherInterface
{
public function dispatch($eventName, BaseEvent $event = null)
{
if (null === $event) {
$event = new Event();
}
return parent::dispatch($eventName, $event);
}
}

View File

@ -0,0 +1,15 @@
<?php
namespace RocketTheme\Toolbox\Event;
use Symfony\Component\EventDispatcher\EventSubscriberInterface as BaseEventSubscriberInterface;
/**
* Defines EventSubscriberInterface.
*
* @package RocketTheme\Toolbox\Event
* @author RocketTheme
* @license MIT
*/
interface EventSubscriberInterface extends BaseEventSubscriberInterface
{
}

View File

@ -0,0 +1,440 @@
<?php
namespace RocketTheme\Toolbox\File;
/**
* Implements Universal File Reader.
*
* @package RocketTheme\Toolbox\File
* @author RocketTheme
* @license MIT
*/
class File implements FileInterface
{
/**
* @var string
*/
protected $filename;
/**
* @var resource
*/
protected $handle;
/**
* @var bool|null
*/
protected $locked;
/**
* @var string
*/
protected $extension;
/**
* @var string Raw file contents.
*/
protected $raw;
/**
* @var array Parsed file contents.
*/
protected $content;
/**
* @var array
*/
protected $settings = [];
/**
* @var array|File[]
*/
static protected $instances = array();
/**
* Get file instance.
*
* @param string $filename
* @return FileInterface
*/
public static function instance($filename)
{
if (!is_string($filename) && $filename) {
throw new \InvalidArgumentException('Filename should be non-empty string');
}
if (!isset(static::$instances[$filename])) {
static::$instances[$filename] = new static;
static::$instances[$filename]->init($filename);
}
return static::$instances[$filename];
}
/**
* Set/get settings.
*
* @param array $settings
* @return array
*/
public function settings(array $settings = null)
{
if ($settings !== null) {
$this->settings = $settings;
}
return $this->settings;
}
/**
* Get setting.
*
* @param string $setting
* @param mixed $default
* @return mixed
*/
public function setting($setting, $default = null)
{
return isset($this->settings[$setting]) ? $this->settings[$setting] : $default;
}
/**
* Prevent constructor from being used.
*
* @internal
*/
protected function __construct()
{
}
/**
* Prevent cloning.
*
* @internal
*/
protected function __clone()
{
//Me not like clones! Me smash clones!
}
/**
* Set filename.
*
* @param $filename
*/
protected function init($filename)
{
$this->filename = $filename;
}
/**
* Free the file instance.
*/
public function free()
{
if ($this->locked) {
$this->unlock();
}
$this->content = null;
$this->raw = null;
unset(static::$instances[$this->filename]);
}
/**
* Get/set the file location.
*
* @param string $var
* @return string
*/
public function filename($var = null)
{
if ($var !== null) {
$this->filename = $var;
}
return $this->filename;
}
/**
* Return basename of the file.
*
* @return string
*/
public function basename()
{
return basename($this->filename, $this->extension);
}
/**
* Check if file exits.
*
* @return bool
*/
public function exists()
{
return is_file($this->filename);
}
/**
* Return file modification time.
*
* @return int|bool Timestamp or false if file doesn't exist.
*/
public function modified()
{
return is_file($this->filename) ? filemtime($this->filename) : false;
}
/**
* Lock file for writing. You need to manually unlock().
*
* @param bool $block For non-blocking lock, set the parameter to false.
* @return bool
* @throws \RuntimeException
*/
public function lock($block = true)
{
if (!$this->handle) {
if (!$this->mkdir(dirname($this->filename))) {
throw new \RuntimeException('Creating directory failed for ' . $this->filename);
}
$this->handle = @fopen($this->filename, 'cb+');
if (!$this->handle) {
$error = error_get_last();
throw new \RuntimeException("Opening file for writing failed on error {$error['message']}");
}
}
$lock = $block ? LOCK_EX : LOCK_EX | LOCK_NB;
return $this->locked = $this->handle ? flock($this->handle, $lock) : false;
}
/**
* Returns true if file has been locked for writing.
*
* @return bool|null True = locked, false = failed, null = not locked.
*/
public function locked()
{
return $this->locked;
}
/**
* Unlock file.
*
* @return bool
*/
public function unlock()
{
if (!$this->handle) {
return;
}
if ($this->locked) {
flock($this->handle, LOCK_UN);
$this->locked = null;
}
fclose($this->handle);
$this->handle = null;
}
/**
* Check if file can be written.
*
* @return bool
*/
public function writable()
{
return is_writable($this->filename) || $this->writableDir(dirname($this->filename));
}
/**
* (Re)Load a file and return RAW file contents.
*
* @return string
*/
public function load()
{
$this->raw = $this->exists() ? (string) file_get_contents($this->filename) : '';
$this->content = null;
return $this->raw;
}
/**
* Get/set raw file contents.
*
* @param string $var
* @return string
*/
public function raw($var = null)
{
if ($var !== null) {
$this->raw = (string) $var;
$this->content = null;
}
if (!is_string($this->raw)) {
$this->raw = $this->load();
}
return $this->raw;
}
/**
* Get/set parsed file contents.
*
* @param mixed $var
* @return string|array
*/
public function content($var = null)
{
if ($var !== null) {
$this->content = $this->check($var);
// Update RAW, too.
$this->raw = $this->encode($this->content);
} elseif ($this->content === null) {
// Decode RAW file.
$this->content = $this->decode($this->raw());
}
return $this->content;
}
/**
* Save file.
*
* @param mixed $data Optional data to be saved, usually array.
* @throws \RuntimeException
*/
public function save($data = null)
{
if ($data !== null) {
$this->content($data);
}
if (!$this->locked) {
// Obtain blocking lock or fail.
if (!$this->lock()) {
throw new \RuntimeException('Obtaining write lock failed on file: ' . $this->filename);
}
$lock = true;
}
// As we are using non-truncating locking, make sure that the file is empty before writing.
if (@ftruncate($this->handle, 0) === false || @fwrite($this->handle, $this->raw()) === false) {
$this->unlock();
throw new \RuntimeException('Saving file failed: ' . $this->filename);
}
if (isset($lock)) {
$this->unlock();
}
// Touch the directory as well, thus marking it modified.
@touch(dirname($this->filename));
}
/**
* Rename file in the filesystem if it exists.
*
* @param $filename
* @return bool
*/
public function rename($filename)
{
if ($this->exists() && !@rename($this->filename, $filename)) {
return false;
}
unset(static::$instances[$this->filename]);
static::$instances[$filename] = $this;
$this->filename = $filename;
return true;
}
/**
* Delete file from filesystem.
*
* @return bool
*/
public function delete()
{
return unlink($this->filename);
}
/**
* Check contents and make sure it is in correct format.
*
* Override in derived class.
*
* @param string $var
* @return string
*/
protected function check($var)
{
return (string) $var;
}
/**
* Encode contents into RAW string.
*
* Override in derived class.
*
* @param string $var
* @return string
*/
protected function encode($var)
{
return (string) $var;
}
/**
* Decode RAW string into contents.
*
* Override in derived class.
*
* @param string $var
* @return string mixed
*/
protected function decode($var)
{
return (string) $var;
}
/**
* @param string $dir
* @return bool
* @throws \RuntimeException
* @internal
*/
protected function mkdir($dir)
{
// Silence error for open_basedir; should fail in mkdir instead.
if (!@is_dir($dir)) {
$success = @mkdir($dir, 0777, true);
if (!$success) {
$error = error_get_last();
throw new \RuntimeException("Creating directory '{$dir}' failed on error {$error['message']}");
}
}
return true;
}
/**
* @param string $dir
* @return bool
* @internal
*/
protected function writableDir($dir)
{
if ($dir && !file_exists($dir)) {
return $this->writableDir(dirname($dir));
}
return $dir && is_dir($dir) && is_writable($dir);
}
}

View File

@ -0,0 +1,106 @@
<?php
namespace RocketTheme\Toolbox\File;
/**
* Defines FileInterface.
*
* @package RocketTheme\Toolbox\File
* @author RocketTheme
* @license MIT
*/
interface FileInterface
{
/**
* Get file instance.
*
* @param string $filename
* @return mixed
*/
public static function instance($filename);
/**
* Free the file instance.
*/
public function free();
/**
* Check if file exits.
*
* @return bool
*/
public function exists();
/**
* Return file modification time.
*
* @return int Timestamp
*/
public function modified();
/**
* Lock file for writing. Lock gets automatically released during the save().
*
* @param bool $block For non-blocking lock, set the parameter to false.
* @return bool
*/
public function lock($block = true);
/**
* Returns true if file has been locked for writing.
*
* @return bool|null True = locked, false = failed, null = not locked.
*/
public function locked();
/**
* Unlock file.
*
* @return bool
*/
public function unlock();
/**
* Check if file can be written.
*
* @return bool
*/
public function writable();
/**
* (Re)Load a file and return its contents.
*
* @return string
*/
public function load();
/**
* Get/set raw file contents.
*
* @param string $var
* @return string
*/
public function raw($var = null);
/**
* Get/set parsed file contents.
*
* @param string $var
* @return string
*/
public function content($var = null);
/**
* Save file.
*
* @param string $data Optional data to be saved.
* @throws \RuntimeException
*/
public function save($data = null);
/**
* Delete file from filesystem.
*
* @return bool
*/
public function delete();
}

View File

@ -0,0 +1,76 @@
<?php
namespace RocketTheme\Toolbox\File;
/**
* Implements INI File reader.
*
* @package RocketTheme\Toolbox\File
* @author RocketTheme
* @license MIT
*/
class IniFile extends File
{
/**
* @var string
*/
protected $extension = '.ini';
/**
* @var array|File[]
*/
static protected $instances = array();
/**
* Check contents and make sure it is in correct format.
*
* @param array $var
* @return array
* @throws \RuntimeException
*/
protected function check($var)
{
if (!is_array($var)) {
throw new \RuntimeException('Provided data is not an array');
}
return $var;
}
/**
* Encode configuration object into RAW string (INI).
*
* @param array $var
* @return string
* @throws \RuntimeException
*/
protected function encode($var)
{
$string = '';
foreach ($var as $key => $value) {
$string .= $key . '="' . preg_replace(
['/"/', '/\\\/', "/\t/", "/\n/", "/\r/"],
['\"', '\\\\', '\t', '\n', '\r'],
$value
) . "\"\n";
}
return $string;
}
/**
* Decode INI file into contents.
*
* @param string $var
* @return array
* @throws \RuntimeException
*/
protected function decode($var)
{
$var = @parse_ini_file($this->filename);
if ($var === false) {
throw new \RuntimeException("Decoding file '{$this->filename}' failed'");
}
return $var;
}
}

View File

@ -0,0 +1,57 @@
<?php
namespace RocketTheme\Toolbox\File;
/**
* Implements Json File reader.
*
* @package RocketTheme\Toolbox\File
* @author RocketTheme
* @license MIT
*/
class JsonFile extends File
{
/**
* @var string
*/
protected $extension = '.json';
/**
* @var array|File[]
*/
static protected $instances = array();
/**
* Check contents and make sure it is in correct format.
*
* @param array $var
* @return array
*/
protected function check($var)
{
return (array) $var;
}
/**
* Encode contents into RAW string.
*
* @param string $var
* @params bitmask $options
* @return string
*/
protected function encode($var, $options = 0)
{
return (string) json_encode($var, $options);
}
/**
* Decode RAW string into contents.
*
* @param string $var
* @param bool $assoc
* @return array mixed
*/
protected function decode($var, $assoc = false)
{
return (array) json_decode($var, $assoc);
}
}

View File

@ -0,0 +1,71 @@
<?php
namespace RocketTheme\Toolbox\File;
/**
* Implements Log File reader.
*
* @package RocketTheme\Toolbox\File
* @author RocketTheme
* @license MIT
*/
class LogFile extends File
{
/**
* @var array|File[]
*/
static protected $instances = array();
/**
* Constructor.
*/
protected function __construct()
{
parent::__construct();
$this->extension = '.log';
}
/**
* Check contents and make sure it is in correct format.
*
* @param array $var
* @return array
*/
protected function check($var)
{
return (array) $var;
}
/**
* Encode contents into RAW string (unsupported).
*
* @param string $var
* @return string|void
* @throws \Exception
*/
protected function encode($var)
{
throw new \Exception('Saving log file is forbidden.');
}
/**
* Decode RAW string into contents.
*
* @param string $var
* @return array mixed
*/
protected function decode($var)
{
$lines = (array) preg_split('#(\r\n|\n|\r)#', $var);
$results = array();
foreach ($lines as $line) {
preg_match('#^\[(.*)\] (.*) @ (.*) @@ (.*)$#', $line, $matches);
if ($matches) {
$results[] = ['date' => $matches[1], 'message' => $matches[2], 'url' => $matches[3], 'file' => $matches[4]];
}
}
return $results;
}
}

View File

@ -0,0 +1,168 @@
<?php
namespace RocketTheme\Toolbox\File;
use \Symfony\Component\Yaml\Yaml as YamlParser;
/**
* Implements Markdown File reader.
*
* @package RocketTheme\Toolbox\File
* @author RocketTheme
* @license MIT
*/
class MarkdownFile extends File
{
/**
* @var string
*/
protected $extension = '.md';
/**
* @var array|File[]
*/
static protected $instances = array();
/**
* Get/set file header.
*
* @param array $var
*
* @return array
*/
public function header(array $var = null)
{
$content = $this->content();
if ($var !== null) {
$content['header'] = $var;
$this->content($content);
}
return $content['header'];
}
/**
* Get/set markdown content.
*
* @param string $var
*
* @return string
*/
public function markdown($var = null)
{
$content = $this->content();
if ($var !== null) {
$content['markdown'] = (string) $var;
$this->content($content);
}
return $content['markdown'];
}
/**
* Get/set frontmatter content.
*
* @param string $var
*
* @return string
*/
public function frontmatter($var = null)
{
$content = $this->content();
if ($var !== null) {
$content['frontmatter'] = (string) $var;
$this->content($content);
}
return $content['frontmatter'];
}
/**
* Check contents and make sure it is in correct format.
*
* @param array $var
* @return array
*/
protected function check($var)
{
$var = (array) $var;
if (!isset($var['header']) || !is_array($var['header'])) {
$var['header'] = array();
}
if (!isset($var['markdown']) || !is_string($var['markdown'])) {
$var['markdown'] = '';
}
return $var;
}
/**
* Encode contents into RAW string.
*
* @param string $var
* @return string
*/
protected function encode($var)
{
// Create Markdown file with YAML header.
$o = (!empty($var['header']) ? "---\n" . trim(YamlParser::dump($var['header'], 5)) . "\n---\n\n" : '') . $var['markdown'];
// Normalize line endings to Unix style.
$o = preg_replace("/(\r\n|\r)/", "\n", $o);
return $o;
}
/**
* Decode RAW string into contents.
*
* @param string $var
* @return array mixed
*/
protected function decode($var)
{
$content = [
'header' => false,
'frontmatter' => ''
];
$frontmatter_regex = "/^---\n(.+?)\n---\n{0,}(.*)$/uis";
// Normalize line endings to Unix style.
$var = preg_replace("/(\r\n|\r)/", "\n", $var);
// Parse header.
preg_match($frontmatter_regex, ltrim($var), $m);
if(!empty($m)) {
$content['frontmatter'] = preg_replace("/\n\t/", "\n ", $m[1]);
// Try native PECL YAML PHP extension first if available.
if ($this->setting('native') && function_exists('yaml_parse')) {
$data = $content['frontmatter'];
if ($this->setting('compat', true)) {
// Fix illegal @ start character.
$data = preg_replace('/ (@[\w\.\-]*)/', " '\${1}'", $data);
}
// Safely decode YAML.
$saved = @ini_get('yaml.decode_php');
@ini_set('yaml.decode_php', 0);
$content['header'] = @yaml_parse("---\n" . $data . "\n...");
@ini_set('yaml.decode_php', $saved);
}
if ($content['header'] === false) {
// YAML hasn't been parsed yet (error or extension isn't available). Fall back to Symfony parser.
$content['header'] = (array) YamlParser::parse($content['frontmatter']);
}
$content['markdown'] = $m[2];
} else {
$content['header'] = [];
$content['markdown'] = $var;
}
return $content;
}
}

View File

@ -0,0 +1,160 @@
<?php
namespace RocketTheme\Toolbox\File;
/**
* Implements Gettext Mo File reader (readonly).
*
* @package RocketTheme\Toolbox\File
* @author RocketTheme
* @license MIT
*/
class MoFile extends File
{
/**
* @var string
*/
protected $extension = '.mo';
protected $pos = 0;
protected $str;
protected $len;
protected $endian;
/**
* @var array|File[]
*/
static protected $instances = array();
/**
* File can never be written.
*
* @return bool
*/
public function writable()
{
return false;
}
/**
* Prevent saving file.
*
* @throws \BadMethodCallException
*/
public function save($data = null)
{
throw new \BadMethodCallException('save() not supported for .mo files.');
}
/**
* Prevent deleting file from filesystem.
*
* @return bool
*/
public function delete()
{
return false;
}
/**
* @param $var
* @return array
* @throws \RuntimeException
*/
public function decode($var)
{
$this->endian = 'V';
$this->str = $var;
$this->len = strlen($var);
$magic = $this->readInt() & 0xffffffff;
if ($magic === 0x950412de) {
// Low endian.
$this->endian = 'V';
} elseif ($magic === 0xde120495) {
// Big endian.
$this->endian = 'N';
} else {
throw new \RuntimeException('Not a Gettext file (.mo).');
}
// Skip revision number.
$rev = $this->readInt();
// Total count.
$total = $this->readInt();
// Offset of original table.
$originals = $this->readInt();
// Offset of translation table.
$translations = $this->readInt();
// Each table consists of string length and offset of the string.
$this->seek($originals);
$table_originals = $this->readIntArray($total * 2);
$this->seek($translations);
$table_translations = $this->readIntArray($total * 2);
$items = [];
for ($i = 0; $i < $total; $i++) {
$this->seek($table_originals[$i * 2 + 2]);
// TODO: Original string can have context concatenated on it. We do not yet support that.
$original = $this->read($table_originals[$i * 2 + 1]);
if ($original) {
$this->seek($table_translations[$i * 2 + 2]);
// TODO: Plural forms are stored by letting the plural of the original string follow the singular of the original string, separated through a NUL byte.
$translated = $this->read($table_translations[$i * 2 + 1]);
$items[$original] = $translated;
}
}
return $items;
}
/**
* @return int
*/
protected function readInt()
{
$read = $this->read(4);
if ($read === false) {
return false;
}
$read = unpack($this->endian, $read);
return array_shift($read);
}
/**
* @param $count
* @return array
*/
protected function readIntArray($count)
{
return unpack($this->endian . $count, $this->read(4 * $count));
}
/**
* @param $bytes
* @return string
*/
private function read($bytes)
{
$data = substr($this->str, $this->pos, $bytes);
$this->seek($this->pos + $bytes);
return $data;
}
/**
* @param $pos
* @return mixed
*/
private function seek($pos)
{
$this->pos = $pos < $this->len ? $pos : $this->len;
return $this->pos;
}
}

View File

@ -0,0 +1,106 @@
<?php
namespace RocketTheme\Toolbox\File;
/**
* Implements PHP File reader.
*
* @package RocketTheme\Toolbox\File
* @author RocketTheme
* @license MIT
*/
class PhpFile extends File
{
/**
* @var string
*/
protected $extension = '.php';
/**
* @var array|File[]
*/
static protected $instances = array();
/**
* Saves PHP file and invalidates opcache.
*
* @param mixed $data Optional data to be saved, usually array.
* @throws \RuntimeException
*/
public function save($data = null)
{
parent::save($data);
// Invalidate configuration file from the opcache.
if (function_exists('opcache_invalidate')) {
// PHP 5.5.5+
@opcache_invalidate($this->filename, true);
} elseif (function_exists('apc_invalidate')) {
// APC
@apc_invalidate($this->filename);
}
}
/**
* Check contents and make sure it is in correct format.
*
* @param array $var
* @return array
* @throws \RuntimeException
*/
protected function check($var)
{
if (!(is_array($var) || is_object($var))) {
throw new \RuntimeException('Provided data is not an array');
}
return $var;
}
/**
* Encode configuration object into RAW string (PHP class).
*
* @param array $var
* @return string
* @throws \RuntimeException
*/
protected function encode($var)
{
// Build the object variables string
return "<?php\nreturn {$this->encodeArray((array) $var)};\n";
}
/**
* Method to get an array as an exported string.
*
* @param array $a The array to get as a string.
* @param int $level Used internally to indent rows.
*
* @return array
*/
protected function encodeArray(array $a, $level = 0)
{
$r = [];
foreach ($a as $k => $v) {
if (is_array($v) || is_object($v)) {
$r[] = var_export($k, true) . " => " . $this->encodeArray((array) $v, $level + 1);
} else {
$r[] = var_export($k, true) . " => " . var_export($v, true);
}
}
$space = str_repeat(" ", $level);
return "[\n {$space}" . implode(",\n {$space}", $r) . "\n{$space}]";
}
/**
* Decode PHP file into contents.
*
* @param string $var
* @return array
*/
protected function decode($var)
{
$var = (array) include $this->filename;
return $var;
}
}

View File

@ -0,0 +1,84 @@
<?php
namespace RocketTheme\Toolbox\File;
use Symfony\Component\Yaml\Exception\DumpException;
use Symfony\Component\Yaml\Exception\ParseException;
use \Symfony\Component\Yaml\Yaml as YamlParser;
/**
* Implements YAML File reader.
*
* @package RocketTheme\Toolbox\File
* @author RocketTheme
* @license MIT
*/
class YamlFile extends File
{
/**
* @var array|File[]
*/
static protected $instances = array();
/**
* Constructor.
*/
protected function __construct()
{
parent::__construct();
$this->extension = '.yaml';
}
/**
* Check contents and make sure it is in correct format.
*
* @param array $var
* @return array
*/
protected function check($var)
{
return (array) $var;
}
/**
* Encode contents into RAW string.
*
* @param string $var
* @return string
* @throws DumpException
*/
protected function encode($var)
{
return (string) YamlParser::dump($var, $this->setting('inline', 5), $this->setting('indent', 2), true, false);
}
/**
* Decode RAW string into contents.
*
* @param string $var
* @return array mixed
* @throws ParseException
*/
protected function decode($var)
{
$data = false;
// Try native PECL YAML PHP extension first if available.
if ($this->setting('native') && function_exists('yaml_parse')) {
if ($this->setting('compat', true)) {
// Fix illegal @ start character.
$data = preg_replace('/ (@[\w\.\-]*)/', " '\${1}'", $var);
} else {
$data = $var;
}
// Safely decode YAML.
$saved = @ini_get('yaml.decode_php');
@ini_set('yaml.decode_php', 0);
$data = @yaml_parse($data);
@ini_set('yaml.decode_php', $saved);
}
return $data !== false ? $data : (array) YamlParser::parse($var);
}
}

View File

@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2014 RocketTheme
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -0,0 +1,56 @@
# ![](https://avatars1.githubusercontent.com/u/1310198?v=2&s=50) RocketTheme Toolbox
[![Latest Version](http://img.shields.io/packagist/v/rockettheme/toolbox.svg?style=flat)](https://packagist.org/packages/rockettheme/toolbox)
[![Software License](https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat)](LICENSE)
[![Build Status](https://img.shields.io/travis/rockettheme/toolbox/master.svg?style=flat)](https://travis-ci.org/rockettheme/toolbox)
[![Coverage Status](https://img.shields.io/scrutinizer/coverage/g/rockettheme/toolbox.svg?style=flat)](https://scrutinizer-ci.com/g/rockettheme/toolbox/code-structure)
[![Quality Score](https://img.shields.io/scrutinizer/g/rockettheme/toolbox.svg?style=flat)](https://scrutinizer-ci.com/g/rockettheme/toolbox)
[![Total Downloads](https://img.shields.io/packagist/dt/rockettheme/toolbox.svg?style=flat)](https://packagist.org/packages/rockettheme/toolbox)
RocketTheme\Toolbox package contains a set of reusable PHP interfaces, classes and traits.
* ArrayTraits
* DI
* Event
* File
* ResourceLocator
* Session
* StreamWrapper
## Installation
You can use [Composer](http://getcomposer.org/) to download and install this package as well as its dependencies.
### Composer
To add this package as a local, per-project dependency to your project, simply add a dependency on `rockettheme/toolbox` to your project's `composer.json` file. Here is a minimal example of a `composer.json` file that just defines a dependency on Diff:
{
"require": {
"rockettheme/toolbox": "dev-master"
}
}
# Contributing
We appreciate any contribution to ToolBox, whether it is related to bugs or simply a suggestion or improvement.
However, we ask that any contribution follow our simple guidelines in order to be properly received.
All our projects follow the [GitFlow branching model][gitflow-model], from development to release. If you are not familiar with it, there are several guides and tutorials to make you understand what it is about.
You will probably want to get started by installing [this very good collection of git extensions][gitflow-extensions].
What you mainly want to know is that:
- All the main activity happens in the `develop` branch. Any pull request should be addressed only to that branch. We will not consider pull requests made to the `master`.
- It's very well appreciated, and highly suggested, to start a new feature whenever you want to make changes or add functionalities. It will make it much easier for us to just checkout your feature branch and test it, before merging it into `develop`
# Getting Started
* Have fun!!!
[gitflow-model]: http://nvie.com/posts/a-successful-git-branching-model/
[gitflow-extensions]: https://github.com/nvie/gitflow

View File

@ -0,0 +1,50 @@
<?php
namespace RocketTheme\Toolbox\ResourceLocator;
/**
* Implements recursive iterator over filesystem.
*
* @package RocketTheme\Toolbox\ResourceLocator
* @author RocketTheme
* @license MIT
*/
class RecursiveUniformResourceIterator extends UniformResourceIterator implements \SeekableIterator, \RecursiveIterator
{
protected $subPath;
public function getChildren()
{
$subPath = $this->getSubPathName();
return (new RecursiveUniformResourceIterator($this->getUrl(), $this->flags, $this->locator))->setSubPath($subPath);
}
public function hasChildren($allow_links = null)
{
$allow_links = (bool) ($allow_links !== null ? $allow_links : $this->flags & \FilesystemIterator::FOLLOW_SYMLINKS);
return $this->iterator && $this->isDir() && !$this->isDot() && ($allow_links || !$this->isLink());
}
public function getSubPath()
{
return $this->subPath;
}
public function getSubPathName()
{
return ($this->subPath ? $this->subPath . '/' : '') . $this->getFilename();
}
/**
* @param $path
* @return $this
* @internal
*/
public function setSubPath($path)
{
$this->subPath = $path;
return $this;
}
}

View File

@ -0,0 +1,45 @@
<?php
namespace RocketTheme\Toolbox\ResourceLocator;
/**
* Defines ResourceLocatorInterface.
*
* @package RocketTheme\Toolbox\ResourceLocator
* @author RocketTheme
* @license MIT
*/
interface ResourceLocatorInterface
{
/**
* Alias for findResource()
*
* @param $uri
* @return string|bool
*/
public function __invoke($uri);
/**
* Returns true if uri is resolvable by using locator.
*
* @param string $uri
* @return bool
*/
public function isStream($uri);
/**
* @param string $uri
* @param bool $absolute
* @param bool $first
* @return string|bool
*/
public function findResource($uri, $absolute = true, $first = false);
/**
* @param string $uri
* @param bool $absolute
* @param bool $all
* @return array
*/
public function findResources($uri, $absolute = true, $all = false);
}

View File

@ -0,0 +1,250 @@
<?php
namespace RocketTheme\Toolbox\ResourceLocator;
use FilesystemIterator;
/**
* Implements FilesystemIterator for uniform resource locator.
*
* @package RocketTheme\Toolbox\ResourceLocator
* @author RocketTheme
* @license MIT
*/
class UniformResourceIterator extends FilesystemIterator
{
/**
* @var FilesystemIterator
*/
protected $iterator;
/**
* @var array
*/
protected $found;
/**
* @var array
*/
protected $stack;
/**
* @var string
*/
protected $path;
/**
* @var int
*/
protected $flags;
/**
* @var UniformResourceLocator
*/
protected $locator;
public function __construct($path, $flags = null, UniformResourceLocator $locator = null)
{
if (!$locator) {
throw new \BadMethodCallException('Use $locator->getIterator() instead');
}
$this->path = $path;
$this->setFlags($flags);
$this->locator = $locator;
$this->rewind();
}
public function current()
{
if ($this->flags & static::CURRENT_AS_SELF) {
return $this;
}
return $this->iterator->current();
}
public function key()
{
return $this->iterator->key();
}
public function next()
{
do {
$found = $this->findNext();
} while ($found && !empty($this->found[$found]));
if ($found) {
// Mark the file as found.
$this->found[$found] = true;
}
}
public function valid()
{
return $this->iterator && $this->iterator->valid();
}
public function rewind()
{
$this->found = [];
$this->stack = $this->locator->findResources($this->path);
$this->next();
}
public function getUrl()
{
$path = $this->path . (substr($this->path, -1, 1) === '/' ? '' : '/');
return $path . $this->iterator->getFilename();
}
public function seek($position)
{
throw new \RuntimeException('Seek not implemented');
}
public function getATime()
{
return $this->iterator->getATime();
}
public function getBasename($suffix = null)
{
return $this->iterator->getBasename($suffix);
}
public function getCTime()
{
return $this->iterator->getCTime();
}
public function getExtension()
{
return $this->iterator->getExtension();
}
public function getFilename()
{
return $this->iterator->getFilename();
}
public function getGroup()
{
return $this->iterator->getGroup();
}
public function getInode()
{
return $this->iterator->getInode();
}
public function getMTime()
{
return $this->iterator->getMTime();
}
public function getOwner()
{
return $this->iterator->getOwner();
}
public function getPath()
{
return $this->iterator->getPath();
}
public function getPathname()
{
return $this->iterator->getPathname();
}
public function getPerms()
{
return $this->iterator->getPerms();
}
public function getSize()
{
return $this->iterator->getSize();
}
public function getType()
{
return $this->iterator->getType();
}
public function isDir()
{
return $this->iterator->isDir();
}
public function isDot()
{
return $this->iterator->isDot();
}
public function isExecutable()
{
return $this->iterator->isExecutable();
}
public function isFile()
{
return $this->iterator->isFile();
}
public function isLink()
{
return $this->iterator->isLink();
}
public function isReadable()
{
return $this->iterator->isReadable();
}
public function isWritable()
{
return $this->iterator->isWritable();
}
public function __toString()
{
return $this->iterator->__toString();
}
public function getFlags()
{
return $this->flags;
}
public function setFlags($flags = null)
{
$this->flags = $flags === null ? static::KEY_AS_PATHNAME | static::CURRENT_AS_SELF | static::SKIP_DOTS : $flags;
if ($this->iterator) {
$this->iterator->setFlags($this->flags);
}
}
protected function findNext()
{
if ($this->iterator) {
$this->iterator->next();
}
if (!$this->valid()) {
do {
// Move to the next iterator if it exists.
$path = array_shift($this->stack);
if (!isset($path)) {
return null;
}
$this->iterator = new \FilesystemIterator($path, $this->getFlags());
} while (!$this->iterator->valid());
}
return $this->getFilename();
}
}

View File

@ -0,0 +1,447 @@
<?php
namespace RocketTheme\Toolbox\ResourceLocator;
/**
* Implements Uniform Resource Location.
*
* @package RocketTheme\Toolbox\ResourceLocator
* @author RocketTheme
* @license MIT
*
* @link http://webmozarts.com/2013/06/19/the-power-of-uniform-resource-location-in-php/
*/
class UniformResourceLocator implements ResourceLocatorInterface
{
/**
* @var string Base URL for all the streams.
*/
public $base;
/**
* @var array
*/
protected $schemes = [];
/**
* @var array
*/
protected $cache = [];
public function __construct($base = null)
{
// Normalize base path.
$this->base = rtrim(str_replace('\\', '/', $base ?: getcwd()), '/');
}
/**
* Return iterator for the resource URI.
*
* @param string $uri
* @param int $flags See constants from FilesystemIterator class.
* @return UniformResourceIterator
*/
public function getIterator($uri, $flags = null)
{
return new UniformResourceIterator($uri, $flags, $this);
}
/**
* Return recursive iterator for the resource URI.
*
* @param string $uri
* @param int $flags See constants from FilesystemIterator class.
* @return RecursiveUniformResourceIterator
*/
public function getRecursiveIterator($uri, $flags = null)
{
return new RecursiveUniformResourceIterator($uri, $flags, $this);
}
/**
* Reset locator by removing all the schemes.
*
* @return $this
*/
public function reset()
{
$this->schemes = [];
$this->cache = [];
return $this;
}
/**
* Reset a locator scheme
*
* @param string $scheme The scheme to reset
*
* @return $this
*/
public function resetScheme($scheme)
{
$this->schemes[$scheme] = [];
$this->cache = [];
return $this;
}
/**
* Add new paths to the scheme.
*
* @param string $scheme
* @param string $prefix
* @param string|array $paths
* @param bool|string $override True to add path as override, string
* @param bool $force True to add paths even if them do not exist.
* @throws \BadMethodCallException
*/
public function addPath($scheme, $prefix, $paths, $override = false, $force = false)
{
$list = [];
foreach((array) $paths as $path) {
if (is_array($path)) {
// Support stream lookup in ['theme', 'path/to'] format.
if (count($path) != 2) {
throw new \BadMethodCallException('Invalid stream path given.');
}
$list[] = $path;
} elseif (strstr($path, '://')) {
// Support stream lookup in 'theme://path/to' format.
$stream = explode('://', $path, 2);
$stream[1] = trim($stream[1], '/');
$list[] = $stream;
} else {
// Normalize path.
$path = rtrim(str_replace('\\', '/', $path), '/');
if ($force || @file_exists("{$this->base}/{$path}") || @file_exists($path)) {
// Support for absolute and relative paths.
$list[] = $path;
}
}
}
if (isset($this->schemes[$scheme][$prefix])) {
$paths = $this->schemes[$scheme][$prefix];
if (!$override || $override == 1) {
$list = $override ? array_merge($paths, $list) : array_merge($list, $paths);
} else {
$location = array_search($override, $paths) ?: count($paths);
array_splice($paths, $location, 0, $list);
$list = $paths;
}
}
$this->schemes[$scheme][$prefix] = $list;
// Sort in reverse order to get longer prefixes to be matched first.
krsort($this->schemes[$scheme]);
$this->cache = [];
}
/**
* Return base directory.
*
* @return string
*/
public function getBase()
{
return $this->base;
}
/**
* Return true if scheme has been defined.
*
* @param string $name
* @return bool
*/
public function schemeExists($name)
{
return isset($this->schemes[$name]);
}
/**
* Return defined schemes.
*
* @return array
*/
public function getSchemes()
{
return array_keys($this->schemes);
}
/**
* Return all scheme lookup paths.
*
* @param string $scheme
* @return array
*/
public function getPaths($scheme = null)
{
return !$scheme ? $this->schemes : (isset($this->schemes[$scheme]) ? $this->schemes[$scheme] : []);
}
/**
* @param string $uri
* @return string|bool
* @throws \BadMethodCallException
*/
public function __invoke($uri)
{
if (!is_string($uri)) {
throw new \BadMethodCallException('Invalid parameter $uri.');
}
return $this->findCached($uri, false, true, false);
}
/**
* Returns true if uri is resolvable by using locator.
*
* @param string $uri
* @return bool
*/
public function isStream($uri)
{
try {
list ($scheme,) = $this->normalize($uri, true, true);
} catch (\Exception $e) {
return false;
}
return $this->schemeExists($scheme);
}
/**
* Returns the canonicalized URI on success. The resulting path will have no '/./' or '/../' components.
* Trailing delimiter `/` is kept.
*
* By default (if $throwException parameter is not set to true) returns false on failure.
*
* @param string $uri
* @param bool $throwException
* @param bool $splitStream
* @return string|array|bool
* @throws \BadMethodCallException
*/
public function normalize($uri, $throwException = false, $splitStream = false)
{
if (!is_string($uri)) {
if ($throwException) {
throw new \BadMethodCallException('Invalid parameter $uri.');
} else {
return false;
}
}
$uri = preg_replace('|\\\|u', '/', $uri);
$segments = explode('://', $uri, 2);
$path = array_pop($segments);
$scheme = array_pop($segments) ?: 'file';
if ($path) {
$path = preg_replace('|\\\|u', '/', $path);
$parts = explode('/', $path);
$list = [];
foreach ($parts as $i => $part) {
if ($part === '..') {
$part = array_pop($list);
if ($part === null || $part === '' || (!$list && strpos($part, ':'))) {
if ($throwException) {
throw new \BadMethodCallException('Invalid parameter $uri.');
} else {
return false;
}
}
} elseif (($i && $part === '') || $part === '.') {
continue;
} else {
$list[] = $part;
}
}
if (($l = end($parts)) === '' || $l === '.' || $l === '..') {
$list[] = '';
}
$path = implode('/', $list);
}
return $splitStream ? [$scheme, $path] : ($scheme !== 'file' ? "{$scheme}://{$path}" : $path);
}
/**
* Find highest priority instance from a resource.
*
* @param string $uri Input URI to be searched.
* @param bool $absolute Whether to return absolute path.
* @param bool $first Whether to return first path even if it doesn't exist.
* @throws \BadMethodCallException
* @return string|bool
*/
public function findResource($uri, $absolute = true, $first = false)
{
if (!is_string($uri)) {
throw new \BadMethodCallException('Invalid parameter $uri.');
}
return $this->findCached($uri, false, $absolute, $first);
}
/**
* Find all instances from a resource.
*
* @param string $uri Input URI to be searched.
* @param bool $absolute Whether to return absolute path.
* @param bool $all Whether to return all paths even if they don't exist.
* @throws \BadMethodCallException
* @return array
*/
public function findResources($uri, $absolute = true, $all = false)
{
if (!is_string($uri)) {
throw new \BadMethodCallException('Invalid parameter $uri.');
}
return $this->findCached($uri, true, $absolute, $all);
}
/**
* Find all instances from a list of resources.
*
* @param array $uris Input URIs to be searched.
* @param bool $absolute Whether to return absolute path.
* @param bool $all Whether to return all paths even if they don't exist.
* @throws \BadMethodCallException
* @return array
*/
public function mergeResources(array $uris, $absolute = true, $all = false)
{
$uris = array_unique($uris);
$list = [];
foreach ($uris as $uri) {
$list = array_merge($list, $this->findResources($uri, $absolute, $all));
}
return $list;
}
/**
* Pre-fill cache by a stream.
*
* @param string $uri
* @return $this
*/
public function fillCache($uri)
{
$cacheKey = $uri . '@cache';
if (!isset($this->cache[$cacheKey])) {
$this->cache[$cacheKey] = true;
$iterator = new \RecursiveIteratorIterator($this->getRecursiveIterator($uri), \RecursiveIteratorIterator::SELF_FIRST);
/** @var UniformResourceIterator $uri */
foreach ($iterator as $uri) {
$key = $uri->getUrl() . '@010';
$this->cache[$key] = $uri->getPathname();
}
}
return $this;
}
protected function findCached($uri, $array, $absolute, $all)
{
// Local caching: make sure that the function gets only called at once for each file.
$key = $uri .'@'. (int) $array . (int) $absolute . (int) $all;
if (!isset($this->cache[$key])) {
try {
list ($scheme, $file) = $this->normalize($uri, true, true);
if (!$file && $scheme === 'file') {
$file = $this->base;
}
$this->cache[$key] = $this->find($scheme, $file, $array, $absolute, $all);
} catch (\BadMethodCallException $e) {
$this->cache[$key] = $array ? [] : false;
}
}
return $this->cache[$key];
}
/**
* @param string $scheme
* @param string $file
* @param bool $array
* @param bool $absolute
* @param bool $all
*
* @throws \InvalidArgumentException
* @return array|string|bool
* @internal
*/
protected function find($scheme, $file, $array, $absolute, $all)
{
if (!isset($this->schemes[$scheme])) {
throw new \InvalidArgumentException("Invalid resource {$scheme}://");
}
$results = $array ? [] : false;
foreach ($this->schemes[$scheme] as $prefix => $paths) {
if ($prefix && strpos($file, $prefix) !== 0) {
continue;
}
// Remove prefix from filename.
$filename = '/' . trim(substr($file, strlen($prefix)), '\/');
foreach ($paths as $path) {
if (is_array($path)) {
// Handle scheme lookup.
$relPath = trim($path[1] . $filename, '/');
$found = $this->find($path[0], $relPath, $array, $absolute, $all);
if ($found) {
if (!$array) {
return $found;
}
$results = array_merge($results, $found);
}
} else {
// TODO: We could provide some extra information about the path to remove preg_match().
// Check absolute paths for both unix and windows
if (!$path || !preg_match('`^/|\w+:`', $path)) {
// Handle relative path lookup.
$relPath = trim($path . $filename, '/');
$fullPath = $this->base . '/' . $relPath;
} else {
// Handle absolute path lookup.
$fullPath = rtrim($path . $filename, '/');
if (!$absolute) {
throw new \RuntimeException("UniformResourceLocator: Absolute stream path with relative lookup not allowed ({$prefix})", 500);
}
}
if ($all || file_exists($fullPath)) {
$current = $absolute ? $fullPath : $relPath;
if (!$array) {
return $current;
}
$results[] = $current;
}
}
}
}
return $results;
}
}

View File

@ -0,0 +1,258 @@
<?php
use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
class UniformResourceLocatorTest extends PHPUnit_Framework_TestCase
{
/**
* @var UniformResourceLocator
*/
static protected $locator;
public static function setUpBeforeClass()
{
// Share locator in all tests.
self::$locator = new UniformResourceLocator(__DIR__ . '/data');
}
public function testGetBase()
{
$this->assertEquals(__DIR__ . '/data', self::$locator->getBase());
}
/**
* @param $scheme
* @param $path
* @param $lookup
*
* @dataProvider addPathProvider
*/
public function testAddPath($scheme, $path, $lookup)
{
$locator = self::$locator;
$this->assertFalse($locator->schemeExists($scheme));
$locator->addPath($scheme, $path, $lookup);
$this->assertTrue($locator->schemeExists($scheme));
}
public function addPathProvider() {
return [
['base', '', 'base'],
['local', '', 'local'],
['override', '', 'override'],
['all', '', ['override://all', 'local://all', 'base://all']],
];
}
/**
* @depends testAddPath
*/
public function testGetSchemes()
{
$this->assertEquals(
['base', 'local', 'override', 'all'],
self::$locator->getSchemes()
);
}
/**
* @depends testAddPath
* @dataProvider getPathsProvider
*/
public function testGetPaths($scheme, $expected)
{
$locator = self::$locator;
$this->assertEquals($expected, $locator->getPaths($scheme));
}
public function getPathsProvider() {
return [
['base', ['' => ['base']]],
['local', ['' => ['local']]],
['override', ['' => ['override']]],
['all', ['' => [['override', 'all'], ['local', 'all'], ['base', 'all']]]],
['fail', []]
];
}
/**
* @depends testAddPath
*/
public function testSchemeExists()
{
$locator = self::$locator;
// Partially tested in addPath() tests.
$this->assertFalse($locator->schemeExists('foo'));
$this->assertFalse($locator->schemeExists('file'));
}
/**
* @depends testAddPath
*/
public function testGetIterator()
{
$locator = self::$locator;
$this->assertInstanceOf(
'RocketTheme\Toolbox\ResourceLocator\UniformResourceIterator',
$locator->getIterator('all://')
);
$this->setExpectedException('InvalidArgumentException', 'Invalid resource fail://');
$locator->getIterator('fail://');
}
/**
* @depends testAddPath
*/
public function testGetRecursiveIterator()
{
$locator = self::$locator;
$this->assertInstanceOf(
'RocketTheme\Toolbox\ResourceLocator\RecursiveUniformResourceIterator',
$locator->getRecursiveIterator('all://')
);
$this->setExpectedException('InvalidArgumentException', 'Invalid resource fail://');
$locator->getRecursiveIterator('fail://');
}
/**
* @depends testAddPath
*/
public function testIsStream($uri)
{
$locator = self::$locator;
// Existing file.
$this->assertEquals(true, $locator->isStream('all://base.txt'));
// Non-existing file.
$this->assertEquals(true, $locator->isStream('all://bar.txt'));
// Unknown uri type.
$this->assertEquals(false, $locator->isStream('fail://base.txt'));
// Bad uri.
$this->assertEquals(false, $locator->isStream('fail://../base.txt'));
}
/**
* @dataProvider normalizeProvider
*/
public function testNormalize($uri, $path)
{
$locator = self::$locator;
$this->assertEquals($path, $locator->normalize($uri));
}
/**
* @depends testAddPath
* @dataProvider findResourcesProvider
*/
public function testFindResource($uri, $paths)
{
$locator = self::$locator;
$path = $paths ? reset($paths) : false;
$fullPath = !$path ? false : __DIR__ . "/data/{$path}";
$this->assertEquals($fullPath, $locator->findResource($uri));
$this->assertEquals($path, $locator->findResource($uri, false));
}
/**
* @depends testAddPath
* @dataProvider findResourcesProvider
*/
public function testFindResources($uri, $paths)
{
$locator = self::$locator;
$this->assertEquals($paths, $locator->findResources($uri, false));
}
/**
* @depends testFindResource
* @dataProvider findResourcesProvider
*/
public function testInvoke($uri, $paths)
{
$locator = self::$locator;
$path = $paths ? reset($paths) : false;
$fullPath = !$path ? false : __DIR__ . "/data/{$path}";
$this->assertEquals($fullPath, $locator($uri));
}
public function normalizeProvider() {
return [
['', ''],
['./', ''],
['././/./', ''],
['././/../', false],
['/', '/'],
['//', '/'],
['///', '/'],
['/././', '/'],
['foo', 'foo'],
['/foo', '/foo'],
['//foo', '/foo'],
['/foo/', '/foo/'],
['//foo//', '/foo/'],
['path/to/file.txt', 'path/to/file.txt'],
['path/to/../file.txt', 'path/file.txt'],
['path/to/../../file.txt', 'file.txt'],
['path/to/../../../file.txt', false],
['/path/to/file.txt', '/path/to/file.txt'],
['/path/to/../file.txt', '/path/file.txt'],
['/path/to/../../file.txt', '/file.txt'],
['/path/to/../../../file.txt', false],
['c:\\', 'c:/'],
['c:\\path\\to\file.txt', 'c:/path/to/file.txt'],
['c:\\path\\to\../file.txt', 'c:/path/file.txt'],
['c:\\path\\to\../../file.txt', 'c:/file.txt'],
['c:\\path\\to\../../../file.txt', false],
['stream://path/to/file.txt', 'stream://path/to/file.txt'],
['stream://path/to/../file.txt', 'stream://path/file.txt'],
['stream://path/to/../../file.txt', 'stream://file.txt'],
['stream://path/to/../../../file.txt', false],
];
}
public function findResourcesProvider() {
return [
['all://base.txt', ['base/all/base.txt']],
['all://base_all.txt', ['override/all/base_all.txt', 'local/all/base_all.txt', 'base/all/base_all.txt']],
['all://base_local.txt', ['local/all/base_local.txt', 'base/all/base_local.txt']],
['all://base_override.txt', ['override/all/base_override.txt', 'base/all/base_override.txt']],
['all://local.txt', ['local/all/local.txt']],
['all://local_override.txt', ['override/all/local_override.txt', 'local/all/local_override.txt']],
['all://override.txt', ['override/all/override.txt']],
['all://missing.txt', []],
['all://asdf/../base.txt', ['base/all/base.txt']],
];
}
/**
* @depends testAddPath
*/
public function testMergeResources()
{
$locator = self::$locator;
}
public function testReset()
{
$locator = self::$locator;
}
public function testResetScheme()
{
$locator = self::$locator;
}
}

View File

@ -0,0 +1,94 @@
<?php
namespace RocketTheme\Toolbox\Session;
/**
* Implements session messages.
*
* @package RocketTheme\Toolbox\Session
* @author RocketTheme
* @license MIT
*/
class Message
{
/**
* @var array|string[]
*/
protected $messages = array();
/**
* Add message to the queue.
*
* @param string $message
* @param string $scope
* @return $this
*/
public function add($message, $scope = 'default')
{
$key = md5($scope.'~'.$message);
$message = array('message' => $message, 'scope' => $scope);
// don't add duplicates
if (!array_key_exists($key, $this->messages)) {
$this->messages[$key] = $message;
}
return $this;
}
/**
* Clear message queue.
*
* @param string $scope
* @return $this
*/
public function clear($scope = null)
{
if ($scope === null) {
$this->messages = array();
} else {
foreach ($this->messages as $key => $message) {
if ($message['scope'] == $scope) {
unset($this->messages[$key]);
}
}
}
return $this;
}
/**
* Fetch all messages.
*
* @param string $scope
* @return array
*/
public function all($scope = null)
{
if ($scope === null) {
return array_values($this->messages);
}
$messages = array();
foreach ($this->messages as $message) {
if ($message['scope'] == $scope) {
$messages[] = $message;
}
}
return $messages;
}
/**
* Fetch and clear message queue.
*
* @param string $scope
* @return array
*/
public function fetch($scope = null)
{
$messages = $this->all($scope);
$this->clear($scope);
return $messages;
}
}

View File

@ -0,0 +1,258 @@
<?php
namespace RocketTheme\Toolbox\Session;
/**
* Implements Session handling.
*
* @package RocketTheme\Toolbox\Session
* @author RocketTheme
* @license MIT
*/
class Session implements \IteratorAggregate
{
/**
* @var bool
*/
protected $started = false;
/**
* @var Session
*/
static $instance;
/**
* @param int $lifetime Defaults to 1800 seconds.
* @param string $path Cookie path.
* @param string $domain Optional, domain for the session
* @throws \RuntimeException
*/
public function __construct($lifetime, $path, $domain = null)
{
// Session is a singleton.
if (isset(self::$instance)) {
throw new \RuntimeException("Session has already been initialized.", 500);
}
// Destroy any existing sessions started with session.auto_start
if (session_id())
{
session_unset();
session_destroy();
}
// Disable transparent sid support
ini_set('session.use_trans_sid', 0);
// Only allow cookies
ini_set('session.use_cookies', 1);
session_name('msF9kJcW');
session_set_cookie_params($lifetime, $path, $domain);
register_shutdown_function([$this, 'close']);
session_cache_limiter('nocache');
if (isset($this->count)) {
$this->count++;
} else {
$this->count = 1;
}
self::$instance = $this;
}
/**
* Get current session instance.
*
* @return Session
* @throws \RuntimeException
*/
public function instance()
{
if (!isset(self::$instance)) {
throw new \RuntimeException("Session hasn't been initialized.", 500);
}
return self::$instance;
}
/**
* Starts the session storage
*
* @return $this
* @throws \RuntimeException
*/
public function start()
{
if (!session_start()) {
throw new \RuntimeException('Failed to start session.', 500);
}
$this->started = true;
return $this;
}
/**
* Get session ID
*
* @return string Session ID
*/
public function getId()
{
return session_id();
}
/**
* Set session Id
*
* @param string $id Session ID
*
* @return $this
*/
public function setId($id)
{
session_id($id);
return $this;
}
/**
* Get session name
*
* @return string
*/
public function getName()
{
return session_name();
}
/**
* Set session name
*
* @param string $name
*
* @return $this
*/
public function setName($name)
{
session_name($name);
return $this;
}
/**
* Invalidates the current session.
*
* @return $this
*/
public function invalidate()
{
$params = session_get_cookie_params();
setcookie(session_name(), '', time() - 42000,
$params['path'], $params['domain'],
$params['secure'], $params['httponly']
);
session_unset();
session_destroy();
$this->started = false;
return $this;
}
/**
* Force the session to be saved and closed
*
* @return $this
*/
public function close()
{
if ($this->started) {
session_write_close();
}
$this->started = false;
return $this;
}
/**
* Checks if an attribute is defined.
*
* @param string $name The attribute name
*
* @return bool True if the attribute is defined, false otherwise
*/
public function __isset($name)
{
return isset($_SESSION[$name]);
}
/**
* Returns an attribute.
*
* @param string $name The attribute name
*
* @return mixed
*/
public function __get($name)
{
return isset($_SESSION[$name]) ? $_SESSION[$name] : null;
}
/**
* Sets an attribute.
*
* @param string $name
* @param mixed $value
*/
public function __set($name, $value)
{
$_SESSION[$name] = $value;
}
/**
* Removes an attribute.
*
* @param string $name
*
* @return mixed The removed value or null when it does not exist
*/
public function __unset($name)
{
unset($_SESSION[$name]);
}
/**
* Returns attributes.
*
* @return array Attributes
*/
public function all()
{
return $_SESSION;
}
/**
* Retrieve an external iterator
*
* @return \ArrayIterator Return an ArrayIterator of $_SESSION
*/
public function getIterator()
{
return new \ArrayIterator($_SESSION);
}
/**
* Checks if the session was started.
*
* @return Boolean
*/
public function started()
{
return $this->started;
}
}

View File

@ -0,0 +1,13 @@
<?php
use RocketTheme\Toolbox\Session\Message;
class SessionMessageTest extends PHPUnit_Framework_TestCase
{
public function testCreation()
{
$message = new Message;
$this->assertTrue(true);
}
}

View File

@ -0,0 +1,78 @@
<?php
namespace RocketTheme\Toolbox\StreamWrapper;
use RocketTheme\Toolbox\ResourceLocator\ResourceLocatorInterface;
/**
* Implements Read Only Streams.
*
* @package RocketTheme\Toolbox\StreamWrapper
* @author RocketTheme
* @license MIT
*/
class ReadOnlyStream extends Stream implements StreamInterface
{
/**
* @var ResourceLocatorInterface
*/
protected static $locator;
public function stream_open($uri, $mode, $options, &$opened_url)
{
if (!in_array($mode, ['r', 'rb', 'rt'])) {
if ($options & STREAM_REPORT_ERRORS) {
trigger_error('stream_open() write modes not supported for read-only stream wrappers', E_USER_WARNING);
}
return false;
}
$path = $this->getPath($uri);
if (!$path) {
return false;
}
$this->handle = ($options & STREAM_REPORT_ERRORS) ? fopen($path, $mode) : @fopen($path, $mode);
return (bool) $this->handle;
}
public function stream_lock($operation)
{
// Disallow exclusive lock or non-blocking lock requests
if (!in_array($operation, [LOCK_SH, LOCK_UN, LOCK_SH | LOCK_NB])) {
trigger_error(
'stream_lock() exclusive lock operations not supported for read-only stream wrappers',
E_USER_WARNING
);
return false;
}
return flock($this->handle, $operation);
}
public function stream_write($data)
{
throw new \BadMethodCallException('stream_write() not supported for read-only stream wrappers');
}
public function unlink($uri)
{
throw new \BadMethodCallException('unlink() not supported for read-only stream wrappers');
}
public function rename($from_uri, $to_uri)
{
throw new \BadMethodCallException('rename() not supported for read-only stream wrappers');
}
public function mkdir($uri, $mode, $options)
{
throw new \BadMethodCallException('mkdir() not supported for read-only stream wrappers');
}
public function rmdir($uri, $options)
{
throw new \BadMethodCallException('rmdir() not supported for read-only stream wrappers');
}
}

View File

@ -0,0 +1,239 @@
<?php
namespace RocketTheme\Toolbox\StreamWrapper;
use RocketTheme\Toolbox\ResourceLocator\ResourceLocatorInterface;
/**
* Implements Read/Write Streams.
*
* @package RocketTheme\Toolbox\StreamWrapper
* @author RocketTheme
* @license MIT
*/
class Stream implements StreamInterface
{
/**
* A generic resource handle.
*
* @var Resource
*/
protected $handle = null;
/**
* @var ResourceLocatorInterface
*/
protected static $locator;
/**
* @param ResourceLocatorInterface $locator
*/
public static function setLocator(ResourceLocatorInterface $locator)
{
static::$locator = $locator;
}
public function stream_open($uri, $mode, $options, &$opened_url)
{
$path = $this->getPath($uri, $mode);
if (!$path) {
return false;
}
$this->handle = ($options & STREAM_REPORT_ERRORS) ? fopen($path, $mode) : @fopen($path, $mode);
return (bool) $this->handle;
}
public function stream_close()
{
return fclose($this->handle);
}
public function stream_lock($operation)
{
if (in_array($operation, [LOCK_SH, LOCK_EX, LOCK_UN, LOCK_NB])) {
return flock($this->handle, $operation);
}
return false;
}
public function stream_metadata($uri, $option, $value)
{
switch ($option) {
case STREAM_META_TOUCH:
list ($time, $atime) = $value;
return touch($uri, $time, $atime);
case STREAM_META_OWNER_NAME:
case STREAM_META_OWNER:
return chown($uri, $value);
case STREAM_META_GROUP_NAME:
case STREAM_META_GROUP:
return chgrp($uri, $value);
case STREAM_META_ACCESS:
return chmod($uri, $value);
}
return false;
}
public function stream_read($count)
{
return fread($this->handle, $count);
}
public function stream_write($data)
{
return fwrite($this->handle, $data);
}
public function stream_eof()
{
return feof($this->handle);
}
public function stream_seek($offset, $whence)
{
// fseek returns 0 on success and -1 on a failure.
return !fseek($this->handle, $offset, $whence);
}
public function stream_flush()
{
return fflush($this->handle);
}
public function stream_tell()
{
return ftell($this->handle);
}
public function stream_stat()
{
return fstat($this->handle);
}
public function unlink($uri)
{
$path = $this->getPath($uri);
if (!$path) {
return false;
}
return unlink($path);
}
public function rename($fromUri, $toUri)
{
$fromPath = $this->getPath($fromUri);
$toPath = $this->getPath($toUri);
if (!($fromPath && $toPath)) {
return false;
}
return rename($fromPath, $toPath);
}
public function mkdir($uri, $mode, $options)
{
$recursive = (bool) ($options & STREAM_MKDIR_RECURSIVE);
$path = $this->getPath($uri, $recursive ? $mode : null);
if (!$path) {
return false;
}
return ($options & STREAM_REPORT_ERRORS) ? mkdir($path, $mode, $recursive) : @mkdir($path, $mode, $recursive);
}
public function rmdir($uri, $options)
{
$path = $this->getPath($uri);
if (!$path) {
return false;
}
return ($options & STREAM_REPORT_ERRORS) ? rmdir($path) : @rmdir($path);
}
public function url_stat($uri, $flags)
{
$path = $this->getPath($uri);
if (!$path) {
return false;
}
// Suppress warnings if requested or if the file or directory does not
// exist. This is consistent with PHP's plain filesystem stream wrapper.
return ($flags & STREAM_URL_STAT_QUIET || !file_exists($path)) ? @stat($path) : stat($path);
}
public function dir_opendir($uri, $options)
{
$path = $this->getPath($uri);
if (!$path) {
return false;
}
$this->handle = opendir($path);
return (bool) $this->handle;
}
public function dir_readdir()
{
return readdir($this->handle);
}
public function dir_rewinddir()
{
rewinddir($this->handle);
return true;
}
public function dir_closedir()
{
closedir($this->handle);
return true;
}
protected function getPath($uri, $mode = null)
{
$path = $this->findPath($uri);
if ($mode == null || !$path || file_exists($path)) {
return $path;
}
if ($mode[0] == 'r') {
return false;
}
// We are either opening a file or creating directory.
list($scheme, $target) = explode('://', $uri, 2);
$path = $this->findPath($scheme . '://' . dirname($target));
if (!$path) {
return false;
}
return $path . '/' . basename($uri);
}
protected function findPath($uri)
{
return static::$locator && static::$locator->isStream($uri) ? static::$locator->findResource($uri) : false;
}
}

View File

@ -0,0 +1,89 @@
<?php
namespace RocketTheme\Toolbox\StreamWrapper;
/**
* Class StreamBuilder
* @package RocketTheme\Toolbox\StreamWrapper
*/
class StreamBuilder
{
/**
* @var array
*/
protected $items = [];
public function __construct(array $items = [])
{
foreach ($items as $scheme => $handler) {
$this->add($scheme, $handler);
}
}
/**
* @param $scheme
* @param $handler
* @return $this
* @throws \InvalidArgumentException
*/
public function add($scheme, $handler)
{
if (isset($this->items[$scheme])) {
if ($handler == $this->items[$scheme]) {
return $this;
}
throw new \InvalidArgumentException("Stream '{$scheme}' has already been initialized.");
}
if (!is_subclass_of($handler, 'RocketTheme\Toolbox\StreamWrapper\StreamInterface')) {
throw new \InvalidArgumentException("Stream '{$scheme}' has unknown or invalid type.");
}
if (!@stream_wrapper_register($scheme, $handler)) {
throw new \InvalidArgumentException("Stream '{$scheme}' could not be initialized.");
}
$this->items[$scheme] = $handler;
return $this;
}
/**
* @param $scheme
* @return $this
*/
public function remove($scheme)
{
if (isset($this->items[$scheme])) {
stream_wrapper_unregister($scheme);
unset($this->items[$scheme]);
}
return $this;
}
/**
* @return array
*/
public function getStreams()
{
return $this->items;
}
/**
* @param $scheme
* @return bool
*/
public function isStream($scheme)
{
return isset($this->items[$scheme]);
}
/**
* @param $scheme
* @return null
*/
public function getStreamType($scheme)
{
return isset($this->items[$scheme]) ? $this->items[$scheme] : null;
}
}

View File

@ -0,0 +1,255 @@
<?php
namespace RocketTheme\Toolbox\StreamWrapper;
/**
* Defines Generic PHP stream wrapper interface.
*
* @package RocketTheme\Toolbox\StreamWrapper
* @author RocketTheme
* @license MIT
*
* @see http://www.php.net/manual/class.streamwrapper.php
*/
interface StreamInterface
{
/**
* Support for fopen(), file_get_contents(), file_put_contents() etc.
*
* @param string $uri A string containing the URI to the file to open.
* @param string $mode The file mode ("r", "wb" etc.).
* @param int $options A bit mask of STREAM_USE_PATH and STREAM_REPORT_ERRORS.
* @param string $opened_url A string containing the path actually opened.
*
* @return bool Returns TRUE if file was opened successfully.
* @see http://php.net/manual/streamwrapper.stream-open.php
*/
public function stream_open($uri, $mode, $options, &$opened_url);
/**
* Support for fclose().
*
* @return bool TRUE if stream was successfully closed.
* @see http://php.net/manual/streamwrapper.stream-close.php
*/
public function stream_close();
/**
* Support for flock().
*
* @param $operation
* One of the following:
* - LOCK_SH to acquire a shared lock (reader).
* - LOCK_EX to acquire an exclusive lock (writer).
* - LOCK_UN to release a lock (shared or exclusive).
* - LOCK_NB if you don't want flock() to block while locking (not
* supported on Windows).
*
* @return bool Always returns TRUE at the present time.
* @see http://php.net/manual/streamwrapper.stream-lock.php
*/
public function stream_lock($operation);
/**
* Support for touch(), chmod(), chown(), chgrp().
*
* @param $path
* The file path or URL to set metadata. Note that in the case of a URL, it must be a :// delimited URL.
* Other URL forms are not supported.
*
* @param $option
* One of:
* - STREAM_META_TOUCH The method was called in response to touch()
* - STREAM_META_OWNER_NAME The method was called in response to chown() with string parameter
* - STREAM_META_OWNER The method was called in response to chown()
* - STREAM_META_GROUP_NAME The method was called in response to chgrp()
* - STREAM_META_GROUP The method was called in response to chgrp()
* - STREAM_META_ACCESS The method was called in response to chmod()
*
* @param $value
* If option is
* - STREAM_META_TOUCH: Array consisting of two arguments of the touch() function.
* - STREAM_META_OWNER_NAME or
* STREAM_META_GROUP_NAME: The name of the owner user/group as string.
* - STREAM_META_OWNER or
* STREAM_META_GROUP: The value owner user/group argument as integer.
* - STREAM_META_ACCESS: The argument of the chmod() as integer.
*
* @return bool
* @see http://php.net/manual/en/streamwrapper.stream-metadata.php
*/
public function stream_metadata($path, $option, $value);
/**
* Support for fread(), file_get_contents() etc.
*
* @param $count
* Maximum number of bytes to be read.
*
* @return string|bool The string that was read, or FALSE in case of an error.
* @see http://php.net/manual/streamwrapper.stream-read.php
*/
public function stream_read($count);
/**
* Support for fwrite(), file_put_contents() etc.
*
* @param $data
* The string to be written.
*
* @return int The number of bytes written (integer).
* @see http://php.net/manual/streamwrapper.stream-write.php
*/
public function stream_write($data);
/**
* Support for feof().
*
* @return bool TRUE if end-of-file has been reached.
* @see http://php.net/manual/streamwrapper.stream-eof.php
*/
public function stream_eof();
/**
* Support for fseek().
*
* @param $offset
* The byte offset to got to.
* @param $whence
* SEEK_SET, SEEK_CUR, or SEEK_END.
*
* @return bool TRUE on success.
* @see http://php.net/manual/streamwrapper.stream-seek.php
*/
public function stream_seek($offset, $whence);
/**
* Support for fflush().
*
* @return bool TRUE if data was successfully stored (or there was no data to store).
* @see http://php.net/manual/streamwrapper.stream-flush.php
*/
public function stream_flush();
/**
* Support for ftell().
*
* @return int The current offset in bytes from the beginning of file.
* @see http://php.net/manual/streamwrapper.stream-tell.php
*/
public function stream_tell();
/**
* Support for fstat().
*
* @return array An array with file status, or FALSE in case of an error - see fstat()
* @see http://php.net/manual/streamwrapper.stream-stat.php
*/
public function stream_stat();
/**
* Support for unlink().
*
* @param $uri
* A string containing the URI to the resource to delete.
*
* @return
* TRUE if resource was successfully deleted.
* @see http://php.net/manual/streamwrapper.unlink.php
*/
public function unlink($uri);
/**
* Support for rename().
*
* @param $from_uri ,
* The URI to the file to rename.
* @param $to_uri
* The new URI for file.
*
* @return bool TRUE if file was successfully renamed.
* @see http://php.net/manual/streamwrapper.rename.php
*/
public function rename($from_uri, $to_uri);
/**
* Support for mkdir().
*
* @param $uri
* A string containing the URI to the directory to create.
* @param $mode
* Permission flags - see mkdir().
* @param $options
* A bit mask of STREAM_REPORT_ERRORS and STREAM_MKDIR_RECURSIVE.
*
* @return bool TRUE if directory was successfully created.
* @see http://php.net/manual/streamwrapper.mkdir.php
*/
public function mkdir($uri, $mode, $options);
/**
* Support for rmdir().
*
* @param $uri
* A string containing the URI to the directory to delete.
* @param $options
* A bit mask of STREAM_REPORT_ERRORS.
*
* @return
* TRUE if directory was successfully removed.
*
* @see http://php.net/manual/streamwrapper.rmdir.php
*/
public function rmdir($uri, $options);
/**
* Support for stat().
*
* @param $uri
* A string containing the URI to get information about.
* @param $flags
* A bit mask of STREAM_URL_STAT_LINK and STREAM_URL_STAT_QUIET.
*
* @return array An array with file status, or FALSE in case of an error - see fstat()
* @see http://php.net/manual/streamwrapper.url-stat.php
*/
public function url_stat($uri, $flags);
/**
* Support for opendir().
*
* @param $uri
* A string containing the URI to the directory to open.
* @param $options
* Unknown (parameter is not documented in PHP Manual).
*
* @return bool TRUE on success.
* @see http://php.net/manual/streamwrapper.dir-opendir.php
*/
public function dir_opendir($uri, $options);
/**
* Support for readdir().
*
* @return string The next filename, or FALSE if there are no more files in the directory.
* @see http://php.net/manual/streamwrapper.dir-readdir.php
*/
public function dir_readdir();
/**
* Support for rewinddir().
*
* @return bool TRUE on success.
* @see http://php.net/manual/streamwrapper.dir-rewinddir.php
*/
public function dir_rewinddir();
/**
* Support for closedir().
*
* @return bool TRUE on success.
* @see http://php.net/manual/streamwrapper.dir-closedir.php
*/
public function dir_closedir();
}