Skip to content

Instantly share code, notes, and snippets.

@m4rw3r
Created November 15, 2013 09:29
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save m4rw3r/7481616 to your computer and use it in GitHub Desktop.
Save m4rw3r/7481616 to your computer and use it in GitHub Desktop.
Dirty start implementation of a data-structure parser for a modular Dependency Injection container
<?php
namespace ModContainer {
use RuntimeException;
class ParseException extends RuntimeException {}
class PathException extends RuntimeException {}
class Module
{
const NAME_REGEX = '/^[a-z0-9_-]+$/i';
protected $name;
protected $source_pathname;
protected $parent = null;
protected $parameters = array();
protected $services = array();
protected $exports = array();
protected $uses = array();
public function __construct($name, $source_pathname, $parent = null)
{
$this->name = $name;
$this->source_pathname = $source_pathname;
$this->parent = $parent;
}
public function getName()
{
return $this->name;
}
public function getSourcePath()
{
return $this->source_pathname;
}
public function getParent()
{
return $this->parent;
}
public function getParameters()
{
return $this->parameters;
}
public function getParameter($key)
{
if( ! array_key_exists($key, $this->parameters)) {
throw new MissingParameterException(sprintf('Missing parameter "%s"', $key));
}
return $this->parameters[$key];
}
public function hasParameter($key)
{
return array_key_exists($key, $this->parameters);
}
public function setParameter($key, $value)
{
if(array_key_exists($key, $this->parameters)) {
throw new RuntimeException(sprintf('Attempting to overwrite parameter "%s"', $key));
}
if( ! preg_match(self::NAME_REGEX, $key)) {
throw new ParseException(sprintf('Invalid parameter name "%s"', $key));
}
if( ! is_scalar($value)) {
/* TODO: Line number and column */
throw new ParseException(sprintf('Expected scalar parameter at key "%s", got %s', $key, gettype($value)));
}
$this->parameters[$key] = $value;
}
public function getServices()
{
return $this->services;
}
public function getService($key)
{
if( ! array_key_exists($key, $this->services)) {
throw new MssingServiceException(sprintf('No service with name "%s" exists', $key));
}
return $this->services[$key];
}
public function hasService($key)
{
return array_key_exists($key, $this->services);
}
public function setService($key, Service $service)
{
if(array_key_exists($key, $this->services)) {
throw new RuntimeException(sprintf('Attempting to overwrite service "%s"', $key));
}
if( ! is_string($key)) {
/* TODO: line number and column */
throw new ParseException(sprintf('Invalid key type "%s"', gettype($key)));
}
if( ! preg_match(self::NAME_REGEX, $key)) {
throw new ParseException(sprintf('Invalid service name "%s"', $key));
}
$this->services[$key] = $service;
}
public function getExports()
{
return $this->exports;
}
public function getExport($key)
{
if( ! array_key_exists($key, $this->exports)) {
throw new Exception(sprintf('No export with name "%s" exists', $key));
}
return $this->exports[$key];
}
public function hasExport($key)
{
return array_key_exists($key, $this->exports);
}
public function setExport($key, Reference $reference)
{
if(array_key_exists($key, $this->services)) {
throw new RuntimeException(sprintf('Attempting to overwrite export "%s"', $key));
}
if( ! is_string($key)) {
/* TODO: line number and column */
throw new ParseException(sprintf('Invalid key type "%s"', gettype($key)));
}
if( ! preg_match(self::NAME_REGEX, $key)) {
throw new ParseException(sprintf('Invalid export name "%s"', $key));
}
$this->exports[$key] = $reference;
}
public function getImports()
{
return $this->imports;
}
public function getImport($key)
{
}
public function getImportParameters($key)
{
}
public function setImport($key, Module $import, $parameters)
{
$this->imports[$key] = [
'module' => $import,
'parameters' => $parameters
];
}
public function addUses(Reference $uses)
{
if($uses instanceof ModuleReference) {
throw new ParseException(sprintf('Service references cannot be used'));
}
/* TODO: Check for duplicate */
$this->uses[] = $uses;
}
}
abstract class Reference {
/**
* 1: Type: "$" = parameter, nothing = module or service
* 2: Parameter/service/module name
* 3: If accessing an exported module parameter, "$" = true, nothing = false
* 4: Nested parameter/service/module name
*
* 3 can't exist without 4, and 2 is required.
*/
const REFERENCE_REGEX = '/^(\$)?([a-z0-9_-]+)(?:\.(\$)?([a-z0-9_-]+))?$/i';
public static function parseReference($reference_str)
{
if( ! preg_match(self::REFERENCE_REGEX, $reference_str, $matches)) {
throw new ParseException(sprintf('Invalid reference name "%s"', $reference_str));
}
$parameter = $matches[1] === '$' ? true : false;
$module = ! empty($matches[4]);
if($parameter && $module) {
throw new ParseException(sprintf('Invalid reference "%s", attempting to read exports from parameter', $reference_str));
}
if($parameter) {
return new ParameterReference($matches[1]);
}
elseif($module) {
/* We can access parameters exported from a module */
$parameter = $matches[3] === '$' ? true : false;
if($parameter) {
$nested = new ParameterReference($matches[4]);
}
else {
$nested = new ServiceReference($matches[4]);
}
return new ModuleReference($matches[2], $nested);
}
else {
return new ServiceReference($matches[2]);
}
}
}
class ModuleReference extends Reference
{
protected $name;
protected $nested = null;
public function __construct($name, Reference $nested = null)
{
$this->name = $name;
$this->nested = $nested;
}
}
class ParameterReference extends Reference
{
protected $name;
public function __construct($name)
{
$this->name = $name;
}
}
class ServiceReference extends Reference
{
protected $name;
public function __construct($name)
{
$this->name = $name;
}
}
class Loader
{
const FILE_EXT = '.json';
const NAME_REGEX = '/^[a-z0-9_-]+$/i';
protected $include_path = array();
public function __construct(array $paths = array())
{
$paths = empty($paths) ? explode(PATH_SEPARATOR, get_include_path()) : $paths;
$this->include_path = array_map(function($path) {
$p = realpath($path);
if( ! $p) {
throw new PathException(sprintf('The path "%s" could not be resolved', $path));
}
return $p;
}, $paths);
}
public function loadModule($module, Module $parent = null)
{
$pathname = false;
if(strpos($module, './') === 0) {
/* Path is relative the parent module */
if( ! $parent) {
throw new PathException(sprintf('The module-path "%s" is missing a parent module', $module));
}
$module = substr($module, 2);
$pathname = basename($parent->getSourcePathname()).DIRECTORY_SEPARATOR.$module.self::FILE_EXT;
$module = $parent->getName().'/'.$module;
}
else {
/* TODO: Implement a module-map to be able to specify specific paths to some modules */
foreach($this->include_path as $path) {
if(file_exists($path.DIRECTORY_SEPARATOR.$module.self::FILE_EXT)) {
$pathname = $path.DIRECTORY_SEPARATOR.$module.self::FILE_EXT;
break;
}
}
}
if( ! $pathname || ! file_exists($pathname)) {
throw new PathException(sprintf('The module "%s" was not found', $module));
}
if($parent) {
$this->ensureNoCycle($parent, $module);
}
$data = json_decode(file_get_contents($pathname));
if(JSON_ERROR_NONE !== json_last_error()) {
/* TODO: Get a *real* parser with error messages for lines and everything. */
throw new ParseException(sprintf('%s in file %s', json_last_error_msg(), $pathname));
}
if( ! is_object($data)) {
throw new ParseException(sprintf('Expected file "%s" to contain a JSON-object', $pathname));
}
try {
$mod = new Module($module, $pathname, $parent);
foreach(get_object_vars($data) as $key => $value) {
try {
switch($key) {
case 'imports':
$this->parseImports($mod, $value);
break;
case 'uses':
$this->parseUses($mod, $value);
break;
case 'parameters':
$this->parseParameters($mod, $value);
break;
case 'services':
$this->parseServices($mod, $value);
break;
case 'exports':
$this->parseExports($mod, $value);
break;
default:
/* TODO: Line-number and column */
throw new RuntimeException(sprintf('Unexpected key "%s"', $key));
}
}
catch(ParseException $e) {
throw new ParseException($e->getMessage().' in '.$key, $e->getCode());
}
}
}
catch(ParseException $e) {
$message = $e->getMessage().sprintf(' in module %s ("%s")', $module, $pathname);
throw new ParseException($message, $e->getCode());
}
return $mod;
}
protected function parseImports(Module $mod, $imports)
{
if( ! is_object($imports)) {
/* TODO: Line number and column */
throw new ParseException(sprintf('Expected hash, got %s', gettype($parameter)));
}
foreach(get_object_vars($imports) as $key => $value) {
if(is_scalar($value)) {
$value = [$value];
}
if( ! is_array($value) || ! in_array(count($value), [1, 2])) {
throw new ParseException(sprintf('Expected string or array of one or two elements in import "%s"', $key));
}
if( ! is_string($value[0])) {
throw new ParseException(sprintf('Import name must be string, %s given', gettype($value[0])));
}
$mod->setImport($key, $this->loadModule($value[0], $mod), array_key_exists(1, $value) ? $value[1] : array());
}
}
protected function parseUses(Module $mod, $uses)
{
if( ! is_array($uses)) {
throw new ParseException(sprintf('Expected array, got %s', gettype($uses)));
}
foreach($uses as $u) {
$r = Reference::parseReference($u);
$mod->addUses($r);
}
}
protected function parseParameters(Module $mod, $parameters)
{
if( ! is_object($parameters)) {
/* TODO: Line number and column */
throw new ParseException(sprintf('Expected hash, got %s', gettype($parameters)));
}
foreach(get_object_vars($parameters) as $key => $value) {
$mod->setParameter($key, $value);
}
}
protected function parseServices(Module $mod, $services)
{
if( ! is_object($services)) {
/* TODO: Line number and column */
throw new ParseException(sprintf('Expected hash, got %s', gettype($services)));
}
foreach(get_object_vars($services) as $key => $value) {
}
}
protected function parseExports(Module $mod, $exports)
{
if( ! is_object($exports)) {
/* TODO: line number and column */
throw new ParseException(sprintf('Expected hash, got %s', gettype($exports)));
}
foreach(get_object_vars($exports) as $key => $value) {
$mod->setExport($key, Reference::parseReference($value));
}
}
protected function ensureNoCycle(Module $mod, $module_name)
{
$path = array();
$first = array();
do {
$path[] = $module_name;
if($mod->getName() === $module_name) {
do {
$first[] = $mod->getName();
$mod = $mod->getParent();
}
while($mod);
throw new ParseException(sprintf('Cycle detected in "%s", having path "%s"', implode('.', $first), implode('.', $path)));
}
$mod = $mod->getParent();
}
while($mod);
}
}
}
namespace {
use ModContainer\Loader;
$loader = new Loader();
$module = $loader->loadModule('module');
var_dump($module);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment