Created
November 15, 2013 09:29
-
-
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
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?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