Code Coverage |
||||||||||
Classes and Traits |
Functions and Methods |
Lines |
||||||||
Total | |
0.00% |
0 / 1 |
|
25.00% |
2 / 8 |
CRAP | |
25.56% |
34 / 133 |
ClassConfig\ClassConfig | |
0.00% |
0 / 1 |
|
25.00% |
2 / 8 |
482.14 | |
25.56% |
34 / 133 |
createDirectories | |
0.00% |
0 / 1 |
6 | |
0.00% |
0 / 4 |
|||
getAnnotationReader | |
0.00% |
0 / 1 |
6 | |
0.00% |
0 / 3 |
|||
getCachePath | |
0.00% |
0 / 1 |
6 | |
0.00% |
0 / 3 |
|||
getClassNamespace | |
0.00% |
0 / 1 |
6 | |
0.00% |
0 / 3 |
|||
generate | |
0.00% |
0 / 1 |
240 | |
0.00% |
0 / 78 |
|||
register | |
0.00% |
0 / 1 |
6 | |
0.00% |
0 / 8 |
|||
createClass | |
100.00% |
1 / 1 |
7 | |
100.00% |
32 / 32 |
|||
createInstance | |
100.00% |
1 / 1 |
1 | |
100.00% |
2 / 2 |
<?php | |
namespace ClassConfig; | |
use ClassConfig\Annotation\Config; | |
use ClassConfig\Annotation\ConfigList; | |
use ClassConfig\Annotation\ConfigBoolean; | |
use ClassConfig\Annotation\ConfigEntryInterface; | |
use ClassConfig\Annotation\ConfigFloat; | |
use ClassConfig\Annotation\ConfigInteger; | |
use ClassConfig\Annotation\ConfigMap; | |
use ClassConfig\Annotation\ConfigObject; | |
use ClassConfig\Annotation\ConfigString; | |
use ClassConfig\Exceptions\ClassConfigAlreadyRegisteredException; | |
use ClassConfig\Exceptions\ClassConfigNotRegisteredException; | |
use Doctrine\Common\Annotations\AnnotationReader; | |
/** | |
* Class ClassConfig | |
* @package ClassConfig | |
*/ | |
class ClassConfig | |
{ | |
/** | |
* Config files are always re-generated when requested. | |
*/ | |
const CACHE_NEVER = 0; | |
/** | |
* Config files are re-generated if older than the source (filemtime). | |
*/ | |
const CACHE_VALIDATE = 1; | |
/** | |
* Config files are only generated once (or after being manually deleted). | |
*/ | |
const CACHE_ALWAYS = 2; | |
/** | |
* Flag to determine whether the register() method has been called. | |
* | |
* @var bool | |
*/ | |
protected static $registered = false; | |
/** | |
* In-memory cache for the annotation reader. | |
* | |
* @var AnnotationReader | |
*/ | |
protected static $annotationReader; | |
/** | |
* The registered path to a cache folder. | |
* | |
* @var string | |
*/ | |
protected static $cachePath; | |
/** | |
* The registered caching strategy. | |
* | |
* @var int | |
*/ | |
protected static $cacheStrategy; | |
/** | |
* The registered class namespace for config classes. | |
* This will be used as prefix to source classes. | |
* | |
* @var string | |
*/ | |
protected static $classNamespace; | |
/** | |
* @param string $path | |
*/ | |
protected static function createDirectories(string $path) | |
{ | |
if (!is_dir($path)) { | |
static::createDirectories(dirname($path)); | |
mkdir($path); | |
} | |
} | |
/** | |
* Lazy getter for the annotation reader. | |
* | |
* @return AnnotationReader | |
* @throws \Doctrine\Common\Annotations\AnnotationException | |
*/ | |
protected static function getAnnotationReader(): AnnotationReader | |
{ | |
if (!isset(static::$annotationReader)) { | |
static::$annotationReader = new AnnotationReader(); | |
} | |
return static::$annotationReader; | |
} | |
/** | |
* Getter for the registered cache path. | |
* Throws a ClassConfigNotRegisteredException if register() wasn't called prior. | |
* | |
* @return string | |
* @throws ClassConfigNotRegisteredException | |
*/ | |
protected static function getCachePath(): string | |
{ | |
if (!static::$registered) { | |
throw new ClassConfigNotRegisteredException(); | |
} | |
return static::$cachePath; | |
} | |
/** | |
* Getter for the registered class namespace. | |
* Throws a ClassConfigNotRegisteredException if register() wasn't called prior. | |
* | |
* @return string | |
* @throws ClassConfigNotRegisteredException | |
*/ | |
protected static function getClassNamespace(): string | |
{ | |
if (!static::$registered) { | |
throw new ClassConfigNotRegisteredException(); | |
} | |
return self::$classNamespace; | |
} | |
/** | |
* @param Config $annotation | |
* @param string $className | |
* @param string $classNamespace | |
* @param string $canonicalClassName | |
* @param string $targetClassNamespace | |
* @param string $targetCanonicalClassName | |
* @param int $time | |
* @param int $subClassIteration | |
* @return string | |
*/ | |
protected static function generate( | |
Config $annotation, | |
string $className, | |
string $classNamespace, | |
string $canonicalClassName, | |
string $targetClassNamespace, | |
string $targetCanonicalClassName, | |
int $time, | |
int &$subClassIteration = 0 | |
): string { | |
// a suffix of _0, _1, _2 etc. is added to generated sub-classes | |
$suffix = 0 < $subClassIteration ? '_' . $subClassIteration : ''; | |
$effectiveClassName = $className . $suffix; | |
$effectiveTargetCanonicalClassName = $targetCanonicalClassName . $suffix; | |
$generator = new ClassGenerator($annotation, $effectiveClassName, $targetClassNamespace, $canonicalClassName); | |
/** | |
* @var string $key | |
* @var ConfigEntryInterface $entry | |
*/ | |
foreach ($annotation->value as $key => $entry) { | |
switch (true) { | |
case $entry instanceof ConfigString: | |
case $entry instanceof ConfigInteger: | |
case $entry instanceof ConfigFloat: | |
case $entry instanceof ConfigBoolean: | |
case $entry instanceof ConfigObject: | |
$type = $entry->getType(); | |
$generator | |
->generateProperty($key, $type, isset($entry->default) ? $entry->default : null) | |
->generateGet($key, $type) | |
->generateSet($key, $type) | |
->generateIsset($key) | |
->generateUnset($key); | |
break; | |
case $entry instanceof ConfigList: | |
$type = isset($entry->value) ? $entry->value->getType() : 'mixed'; | |
$generator | |
->generateProperty($key, $type . '[]', isset($entry->default) ? | |
array_values($entry->default) : null) | |
->generateGet($key, $type . '[]') | |
->generateListSet($key, $type . '[]') | |
->generateListGetAt($key, $type) | |
->generateListSetAt($key, $type) | |
->generateListPush($key, $type) | |
->generateListUnshift($key, $type) | |
->generateArrayPop($key, $type) | |
->generateArrayShift($key, $type) | |
->generateArrayClear($key) | |
->generateIsset($key) | |
->generateUnset($key); | |
break; | |
case $entry instanceof ConfigMap: | |
$type = isset($entry->value) ? $entry->value->getType() : 'mixed'; | |
$generator | |
->generateProperty($key, $type . '[]', $entry->default) | |
->generateGet($key, $type . '[]') | |
->generateMapSet($key, $type . '[]') | |
->generateMapGetAt($key, $type) | |
->generateMapSetAt($key, $type) | |
->generateArrayPop($key, $type) | |
->generateArrayShift($key, $type) | |
->generateArrayClear($key) | |
->generateIsset($key) | |
->generateUnset($key); | |
break; | |
case $entry instanceof Config: | |
$subClassIteration++; | |
$entryCanonicalClassName = static::generate( | |
$entry, | |
$className, | |
$classNamespace, | |
$canonicalClassName, | |
$targetClassNamespace, | |
$targetCanonicalClassName, | |
$time, | |
$subClassIteration | |
); | |
$generator | |
->generateProperty($key, $entryCanonicalClassName) | |
->generateConfigGet($key, $entryCanonicalClassName) | |
->generateConfigSet($key) | |
->generateConfigIsset($key) | |
->generateConfigUnset($key); | |
break; | |
default: | |
throw new \RuntimeException(sprintf( | |
'Invalid or unsupported configuration entry type: "%s".', | |
get_class($entry) | |
)); | |
} | |
} | |
$generator | |
->generateMagicGet() | |
->generateMagicSet() | |
->generateMagicIsset() | |
->generateMagicUnset(); | |
$targetDir = static::getCachePath() . '/' . str_replace('\\', '/', $classNamespace); | |
$targetPath = $targetDir . '/' . $effectiveClassName . '.php'; | |
static::createDirectories($targetDir); | |
file_put_contents($targetPath, (string) $generator); | |
touch($targetPath, $time); | |
clearstatcache(); | |
// as optimization measure composer's autoloader remembers that a class does not exist on the first requested | |
// it will refuse to autoload the class even if it subsequently becomes available | |
// for this reason we need to manually load the newly generated class | |
include_once $targetPath; | |
return $effectiveTargetCanonicalClassName; | |
} | |
/** | |
* Register the environment. | |
* This must be called once and only once (on each request) before working with the library. | |
* | |
* @param string $cachePath | |
* @param int $cacheStrategy | |
* @param string $classNamespace | |
*/ | |
public static function register( | |
string $cachePath, | |
int $cacheStrategy = self::CACHE_VALIDATE, | |
string $classNamespace = 'ClassConfig\Cache' | |
) { | |
if (static::$registered) { | |
throw new ClassConfigAlreadyRegisteredException(); | |
} | |
// ensure the cache folder exists | |
static::createDirectories($cachePath); | |
static::$registered = true; | |
static::$cachePath = $cachePath; | |
static::$cacheStrategy = $cacheStrategy; | |
static::$classNamespace = $classNamespace; | |
} | |
/** | |
* @param string $canonicalClassName | |
* @return string | |
* @throws \Doctrine\Common\Annotations\AnnotationException | |
* @throws \ReflectionException | |
* @throws ClassConfigNotRegisteredException | |
*/ | |
public static function createClass(string $canonicalClassName): string | |
{ | |
$parts = explode('\\', $canonicalClassName); | |
$className = $parts[count($parts) - 1]; | |
$classNamespace = implode('\\', array_slice($parts, 0, -1)); | |
$targetClassNamespace = static::getClassNamespace() . '\\' . $classNamespace; | |
$targetCanonicalClassName = $targetClassNamespace . '\\' . $className; | |
switch (static::$cacheStrategy) { | |
case static::CACHE_NEVER: | |
// always regenerate | |
$time = time(); | |
break; | |
case static::CACHE_ALWAYS: | |
// only generate if class does not exist | |
if (class_exists($targetCanonicalClassName)) { | |
return $targetCanonicalClassName; | |
} | |
$time = time(); | |
break; | |
case static::CACHE_VALIDATE: | |
default: | |
// validate by last modified time | |
$time = filemtime((new \ReflectionClass($canonicalClassName))->getFileName()); | |
if ( | |
class_exists($targetCanonicalClassName) && | |
filemtime((new \ReflectionClass($canonicalClassName))->getFileName()) === | |
filemtime((new \ReflectionClass($targetCanonicalClassName))->getFileName()) | |
) { | |
return $targetCanonicalClassName; | |
} | |
break; | |
} | |
/** @var Config $annotation */ | |
$annotation = static::getAnnotationReader()->getClassAnnotation( | |
new \ReflectionClass($canonicalClassName), | |
Config::class | |
); | |
return static::generate( | |
$annotation, | |
$className, | |
$classNamespace, | |
$canonicalClassName, | |
$targetClassNamespace, | |
$targetCanonicalClassName, | |
$time | |
); | |
} | |
/** | |
* @param string $class | |
* @param object $owner | |
* @return AbstractConfig | |
* @throws \Doctrine\Common\Annotations\AnnotationException | |
* @throws \ReflectionException | |
* @throws ClassConfigNotRegisteredException | |
*/ | |
public static function createInstance(string $class, $owner): AbstractConfig | |
{ | |
$canonicalClassName = static::createClass($class); | |
return new $canonicalClassName($owner); | |
} | |
} |