Skip to content

Instantly share code, notes, and snippets.

@jeremeamia
Last active March 26, 2016 11:36
Show Gist options
  • Star 4 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jeremeamia/cc602630a712672baa9e to your computer and use it in GitHub Desktop.
Save jeremeamia/cc602630a712672baa9e to your computer and use it in GitHub Desktop.
Mini-PHP config system for fun. (MIT Licensed)
<?php
namespace config;
const DELIM = '.';
/**
* Loads a configuration array from a file, based on it's type.
*
* @param string $path
* @param string|null $type
*
* @return array
* @throws \RuntimeException
* @throws \InvalidArgumentException
*/
function load($path, $type = null)
{
if (!is_readable($path)) {
throw new \InvalidArgumentException('Unable to load configuration file.');
}
$type = $type ?: strtolower(pathinfo($path, PATHINFO_EXTENSION));
switch ($type) {
case 'php':
$data = include $path;
break;
case 'json':
$data = json_decode(file_get_contents($path), true);
if (json_last_error() !== JSON_ERROR_NONE) {
throw new \InvalidArgumentException('Unable to parse JSON data.');
}
break;
case 'ini':
$data = parse_ini_file($path);
break;
case 'yaml':
if (!class_exists('Symfony\Component\Yaml\Yaml')) {
throw new \RuntimeException('Symfony YAML library not available.');
}
$data = \Symfony\Component\Yaml\Yaml::parse($path);
break;
default:
throw new \InvalidArgumentException('Invalid config file format.');
}
if (!is_array($data)) {
throw new \InvalidArgumentException('Loading the config file must return an array.');
}
return $data;
}
/**
* Validates a config array based on the provided schema, the schema can specify
* for each key: the default value, whether it is required, the schema of any
* nested values, how to validate it, and how to transform it prior to validation.
*
* @param array $config
* @param array $schema
* @param string $delim Defaults to ".".
*
* @return array
* @throws \InvalidArgumentException
*/
function validate(array $config = [], array $schema = [], $delim = DELIM, $namespace = null)
{
foreach ($schema as $key => $s) {
$path = trim("{$namespace}{$delim}{$key}", $delim);
// Get the value from the config or use the default value.
$value = isset($config[$key])
? $config[$key]
: (isset($s['default']) ? $s['default'] : null);
// Transform the value, if needed.
if (isset($s['transform']) && is_callable($s['transform'])) {
$value = $s['transform']($value);
}
// Ensure the value exists, if required.
if (isset($s['required']) && $s['required'] && $value === null) {
throw new \InvalidArgumentException("Missing a value for \"{$path}\" in the config.");
}
// Recurse into nested structures and validate, if needed.
if (is_array($value) && isset($s['schema'])) {
if (is_array($s['schema'])) {
validate($value, $s['schema'], $path);
} elseif (is_callable($s['schema'])) {
array_map(function ($value) use ($s, $path) {
if (!$s['schema']($value)) {
throw new \InvalidArgumentException("Invalid type for \"{$path}\".");
}
}, $value);
} else {
throw new \InvalidArgumentException("A sub-schema must be an array or callable.");
}
}
// Validate the value using the specified callback.
if (isset($s['validate']) && is_callable($s['validate'])
&& !is_null($value) && !$s['validate']($value)
) {
throw new \InvalidArgumentException("Invalid type for \"{$path}\".");
}
$config[$key] = $value;
}
return $config;
}
/**
* Fetches a value, or a nested value, from a config array using a path. Returns
* null if the value is not found.
*
* @param array $config
* @param string|array $path
* @param string $delim
*
* @return mixed
*/
function get(array $config, $path, $delim = DELIM)
{
$path = is_array($path) ? $path : explode($delim, $path);
$key = array_shift($path);
$value = isset($config[$key]) ? $config[$key] : null;
if (is_array($value) && $path) {
// Call this function recursively on the nested level.
$value = get($value, $path);
}
// If a lazy value is encountered, invoke it to get the real value.
if ($value instanceof _lazy_value) {
$value = $value();
}
return $value;
}
/**
* Wraps a callable that is meant to be a factory for a value, so that the
* factory logic is deferred, or lazy.
*
* @param callable $createValue
*
* @return callable
*/
function lazy(callable $createValue) {
return new _lazy_value($createValue);
}
/**
* @internal
*/
final class _lazy_value
{
private $createValue;
public function __construct(callable $createValue)
{
$this->createValue = $createValue;
}
public function __invoke()
{
return call_user_func($this->createValue);
}
}
<?php
use config;
$data = config\load('config.json');
// OR
$data = [
'a' => 'foo',
'c' => ['a', 'b', 'c'],
'd' => ['foo' => '5'],
'e' => config\lazy(function() {return time();})
];
// Allowed rules: default <mixed>, required <bool>, schema <callable|array>, transform <callable>, validate <callable>
$schema = [
'a' => ['validate' => 'is_string', 'required' => true],
'b' => ['validate' => 'is_string', 'default' => 'cheese'],
'c' => ['validate' => 'is_array', 'schema' => 'is_string'],
'd' => ['validate' => 'is_array', 'schema' => [
'foo' => [
'required' => true,
'validate' => 'is_integer',
'transform' => 'intval'
],
]],
]
$data = config\validate($data, $schema);
print_r($data);
var_dump(config\get($data, 'd.foo'));
var_dump(config\get($data, 'e'));
@nubs
Copy link

nubs commented Aug 27, 2014

Heh, we've created something very similar for validating input: https://github.com/dominionenterprises/filter-php/.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment