diff --git a/composer.json b/composer.json index 659ba64..83af31e 100644 --- a/composer.json +++ b/composer.json @@ -12,19 +12,22 @@ } ], "require": { - "php": ">=5.4.0", + "php": "~8", "ext-json":"*", - "regex-guard/regex-guard": "~1.0", - "nikic/php-parser" : ">=2.0 <5.0.0" + "regex-guard/regex-guard": "~1.1", + "nikic/php-parser" : ">=4.0 <5.0.0" }, "require-dev": { - "mockery/mockery": "^0.9", - "phpunit/phpunit": ">=4.8 <6.0.0" + "mockery/mockery": "~1.6", + "phpunit/phpunit": "~10" }, "autoload": { "psr-4": { "Minime\\Annotations\\": "src/" } }, "autoload-dev": { "psr-4": { "Minime\\Annotations\\Fixtures\\": "test/fixtures/" } + }, + "scripts": { + "test": "phpunit" } } diff --git a/phpunit.xml.dist b/phpunit.xml.dist index a1772a1..621e1fc 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -2,50 +2,18 @@ - + - + test/suite/ - - - src/ - - - - - - - - + diff --git a/src/AnnotationsBag.php b/src/AnnotationsBag.php index b27043d..7c7ba26 100644 --- a/src/AnnotationsBag.php +++ b/src/AnnotationsBag.php @@ -153,7 +153,7 @@ public function union(AnnotationsBagInterface $bag) /** * Countable */ - public function count() + public function count(): int { return count($this->attributes); } @@ -161,7 +161,7 @@ public function count() /** * JsonSerializable */ - public function jsonSerialize() + public function jsonSerialize(): array { return $this->toArray(); } @@ -169,7 +169,7 @@ public function jsonSerialize() /** * IteratorAggregate */ - public function getIterator() + public function getIterator(): \Traversable { return new ArrayIterator($this->attributes); } @@ -177,7 +177,7 @@ public function getIterator() /** * ArrayAccess - Whether or not an offset exists. */ - public function offsetExists($key) + public function offsetExists($key): bool { return $this->has($key); } @@ -185,7 +185,7 @@ public function offsetExists($key) /** * ArrayAccess - Returns the value at specified offset. */ - public function offsetGet($key) + public function offsetGet($key): mixed { return $this->get($key); } @@ -193,17 +193,15 @@ public function offsetGet($key) /** * ArrayAccess - Assigns a value to the specified offset. */ - public function offsetSet($key, $value) + public function offsetSet($key, $value): void { $this->set($key, $value); - - return true; } /** * ArrayAccess - Unsets an offset. */ - public function offsetUnset($key) + public function offsetUnset($key): void { unset($this->attributes[$key]); } diff --git a/src/DynamicParser.php b/src/DynamicParser.php index 4e3c967..59c79e1 100644 --- a/src/DynamicParser.php +++ b/src/DynamicParser.php @@ -121,4 +121,9 @@ protected function sanitizeKey($key) { return $key; } + + public function registerConcreteNamespaceLookup(array $namespaces) + { + } + } diff --git a/src/Interfaces/ParserInterface.php b/src/Interfaces/ParserInterface.php index 89da9af..a93b6b7 100644 --- a/src/Interfaces/ParserInterface.php +++ b/src/Interfaces/ParserInterface.php @@ -17,4 +17,11 @@ interface ParserInterface * @return array */ public function parse($docBlock); + + /** + * Register set of namespaces for ConcreteType to autolookup. + * + * @param array $namespaces + */ + public function registerConcreteNamespaceLookup(array $namespaces); } diff --git a/src/Interfaces/TypeInterface.php b/src/Interfaces/TypeInterface.php index 538bff3..3f8a78f 100644 --- a/src/Interfaces/TypeInterface.php +++ b/src/Interfaces/TypeInterface.php @@ -9,6 +9,11 @@ */ interface TypeInterface { + /** + * @return TypeInterface + */ + public static function getType(); + /** * Parses a type * @param string $value value to be processed diff --git a/src/Parser.php b/src/Parser.php index 62314bf..a806fc9 100644 --- a/src/Parser.php +++ b/src/Parser.php @@ -19,13 +19,20 @@ class Parser extends DynamicParser * @var array */ protected $types = [ - '\Minime\Annotations\Types\IntegerType' => 'integer', - '\Minime\Annotations\Types\StringType' => 'string', - '\Minime\Annotations\Types\FloatType' => 'float', - '\Minime\Annotations\Types\JsonType' => 'json', - '\Minime\Annotations\Types\ConcreteType' => '->' + 'integerType' => 'integer', + 'stringType' => 'string', + 'floatType' => 'float', + 'jsonType' => 'json', + 'concreteType' => '->' ]; + /** + * A fallback type if no strong type declaration found. + * + * @var string + */ + protected $typeFallback = 'dynamicType'; + /** * The regex equivalent of $types * @@ -33,12 +40,24 @@ class Parser extends DynamicParser */ protected $typesPattern; + /** + * @var TypeContainer + */ + private $typeContainer; + /** * Parser constructor * */ public function __construct() { + $this->typeContainer = new TypeContainer(); + $this->typeContainer->add($this->typeFallback); + + foreach ($this->types as $key => $value) { + $this->typeContainer->add($key); + } + $this->buildTypesPattern(); parent::__construct(); } @@ -46,12 +65,14 @@ public function __construct() public function registerType($class, $token) { $this->types[$class] = $token; + $this->typeContainer->add($class); $this->buildTypesPattern(); } public function unregisterType($class) { unset($this->types[$class]); + $this->typeContainer->remove($class); $this->buildTypesPattern(); } @@ -65,16 +86,18 @@ public function unregisterType($class) protected function parseValue($value, $key = null) { $value = trim($value); - $type = '\Minime\\Annotations\\Types\\DynamicType'; + $type = $this->typeFallback; + if (preg_match($this->typesPattern, $value, $found)) { // strong typed $type = $found[1]; $value = trim(substr($value, strlen($type))); + if (in_array($type, $this->types)) { $type = array_search($type, $this->types); } } - return (new $type)->parse($value, $key); + return $this->typeContainer->{$type}->parse($value, $key); } /** @@ -96,4 +119,8 @@ protected function buildTypesPattern() { $this->typesPattern = '/^('.implode('|', $this->types).')(\s+)/'; } + + public function registerConcreteNamespaceLookup(array $namespaces) { + $this->typeContainer->concreteType->setNamespaces($namespaces); + } } diff --git a/src/TypeContainer.php b/src/TypeContainer.php new file mode 100644 index 0000000..a3b794d --- /dev/null +++ b/src/TypeContainer.php @@ -0,0 +1,77 @@ +builders[$name] = function() use($name) { + $type = 'Minime\\Annotations\\Types\\' . ucfirst($name); + + // do we have in default configuration setup? + if (!class_exists($type)) { + $type = $name; + } + + if (is_callable($type . '::getType')) { + $typeClass = call_user_func($type . '::getType'); + } else { + $typeClass = new $type; + } + + return new $typeClass; + }; + } + + /** + * Remove a lambda function. + * + * @param string $name + */ + public function remove($name) + { + unset($this->builders[$name]); + unset($this->types[$name]); + } + + /** + * @param string $name + * + * @return TypeInterface + */ + public function __get($name) + { + if (!isset($this->types[$name]) && isset($this->builders[$name])) { + $this->types[$name] = $this->builders[$name](); + } + + return $this->types[$name]; + } +} diff --git a/src/Types/ConcreteType.php b/src/Types/ConcreteType.php index af570c3..41f050a 100644 --- a/src/Types/ConcreteType.php +++ b/src/Types/ConcreteType.php @@ -9,36 +9,90 @@ class ConcreteType implements TypeInterface { + /** + * @var TypeInterface + */ + private static $instance; + + public static function getType() + { + if (!isset(self::$instance)) { + self::$instance = new ConcreteType(); + } + + return self::$instance; + } + + /** + * @var array + */ + private $namespaceLookup = []; + + /** + * Set of user defined namespaces to lookup for class autoloading. + * + * @param array $namespaces + */ + public function setNamespaces(array $namespaces) + { + $this->namespaceLookup = $namespaces; + } + + /** + * @param string $class + * + * @return string + * + * @throws ParserException + */ + protected function checkClassExistence($class) + { + $found = class_exists($class); + $classname = $class; + $i = 0; + + while (!$found && $i < count($this->namespaceLookup)) { + $classname = $this->namespaceLookup[$i] . $class; + $found = class_exists($classname); + $i++; + } + + if (!$found) { + throw new ParserException("Concrete annotation expects '{$class}' to exist."); + } + + return $classname; + } /** * Process a value to be a concrete annotation * - * @param string $value json string - * @param string $class name of concrete annotation type (class) - * @throws \Minime\Annotations\ParserException + * @param string $value json string + * @param string $class name of concrete annotation type (class) + * + * @throws ParserException + * * @return object */ public function parse($value, $class = null) { - if (! class_exists($class)) { - throw new ParserException("Concrete annotation expects {$class} to exist."); - } + $classname = $this->checkClassExistence($class); $prototype = (new JsonType)->parse($value); if ($prototype instanceof stdClass) { - if (! $this->isPrototypeSchemaValid($prototype)) { + if (!$this->isPrototypeSchemaValid($prototype)) { throw new ParserException("Only arrays should be used to configure concrete annotation method calls."); } - return $this->makeInstance($class, $prototype); + return $this->makeInstance($classname, $prototype); } if (is_array($prototype)) { - return $this->makeConstructSugarInjectionInstance($class, $prototype); + return $this->makeConstructSugarInjectionInstance($classname, $prototype); } - throw new ParserException("Json value for annotation({$class}) must be of type object or array."); + throw new ParserException("Json value for annotation({$classname}) must be of type object or array."); } protected function makeConstructSugarInjectionInstance($class, array $prototype) { @@ -79,7 +133,7 @@ protected function doMethodConfiguration($instance, stdClass $prototype) { foreach ($prototype as $method => $args) { call_user_func_array([$instance, $method], $args); - } + } return $instance; } @@ -92,7 +146,7 @@ protected function doMethodConfiguration($instance, stdClass $prototype) */ protected function isPrototypeSchemaValid(stdclass $prototype) { - foreach ($prototype as $method => $args) { + foreach ($prototype as $args) { if (! is_array($args)) { return false; } diff --git a/src/Types/DynamicType.php b/src/Types/DynamicType.php index 2f5bd49..c43ffb3 100644 --- a/src/Types/DynamicType.php +++ b/src/Types/DynamicType.php @@ -6,6 +6,19 @@ class DynamicType implements TypeInterface { + /** + * @var TypeInterface + */ + private static $instance; + + public static function getType() + { + if (!isset(self::$instance)) { + self::$instance = new DynamicType(); + } + + return self::$instance; + } /** * Parse a given undefined type value diff --git a/src/Types/FloatType.php b/src/Types/FloatType.php index 9fafde4..56489dc 100644 --- a/src/Types/FloatType.php +++ b/src/Types/FloatType.php @@ -7,6 +7,19 @@ class FloatType implements TypeInterface { + /** + * @var TypeInterface + */ + private static $instance; + + public static function getType() + { + if (!isset(self::$instance)) { + self::$instance = new FloatType(); + } + + return self::$instance; + } /** * Filter a value to be a Float diff --git a/src/Types/IntegerType.php b/src/Types/IntegerType.php index a18ba03..0df11d6 100644 --- a/src/Types/IntegerType.php +++ b/src/Types/IntegerType.php @@ -7,6 +7,19 @@ class IntegerType implements TypeInterface { + /** + * @var TypeInterface + */ + private static $instance; + + public static function getType() + { + if (!isset(self::$instance)) { + self::$instance = new IntegerType(); + } + + return self::$instance; + } /** * Filter a value to be an Integer diff --git a/src/Types/JsonType.php b/src/Types/JsonType.php index ac9d90e..e1093d3 100644 --- a/src/Types/JsonType.php +++ b/src/Types/JsonType.php @@ -7,6 +7,19 @@ class JsonType implements TypeInterface { + /** + * @var TypeInterface + */ + private static $instance; + + public static function getType() + { + if (!isset(self::$instance)) { + self::$instance = new JsonType(); + } + + return self::$instance; + } /** * Filter a value to be a Json diff --git a/src/Types/StringType.php b/src/Types/StringType.php index b813f90..c27d5bc 100644 --- a/src/Types/StringType.php +++ b/src/Types/StringType.php @@ -6,6 +6,19 @@ class StringType implements TypeInterface { + /** + * @var TypeInterface + */ + private static $instance; + + public static function getType() + { + if (!isset(self::$instance)) { + self::$instance = new StringType(); + } + + return self::$instance; + } /** * Parse a given value as string diff --git a/test/fixtures/AnnotationsFixture.php b/test/fixtures/AnnotationsFixture.php index d48e047..934db61 100644 --- a/test/fixtures/AnnotationsFixture.php +++ b/test/fixtures/AnnotationsFixture.php @@ -82,6 +82,15 @@ class AnnotationsFixture */ private $concrete_fixture; + /** + * @AnnotationConstructInjection -> { "__construct" : ["bar"] } + * + * @AnnotationConstructSugarInjection -> ["foo", "bar"] + * + * @AnnotationSetterInjection -> { "setFoo" : ["bar"] } + */ + private $short_concrete_fixture; + /** * @SomeUndefinedClass -> {} */ diff --git a/test/fixtures/FooType.php b/test/fixtures/FooType.php index 3313d21..1842e2b 100644 --- a/test/fixtures/FooType.php +++ b/test/fixtures/FooType.php @@ -6,6 +6,11 @@ class FooType implements TypeInterface { + public static function getType() + { + return new FooType(); + } + public function parse($value, $annotation = null) { return 'this foo is ' . $value; diff --git a/test/suite/AnnotationsBagTest.php b/test/suite/AnnotationsBagTest.php index 527e29b..f658058 100644 --- a/test/suite/AnnotationsBagTest.php +++ b/test/suite/AnnotationsBagTest.php @@ -7,12 +7,12 @@ /** * @group bag */ -class AnnotationsBagTest extends \PHPUnit_Framework_TestCase +class AnnotationsBagTest extends \PHPUnit\Framework\TestCase { private $Bag; - public function setUp() + protected function setUp(): void { $this->Bag = new AnnotationsBag( [ diff --git a/test/suite/BaseTest.php b/test/suite/BaseTest.php deleted file mode 100644 index 0bee04e..0000000 --- a/test/suite/BaseTest.php +++ /dev/null @@ -1,34 +0,0 @@ -fixture = new AnnotationsFixture; - } - - protected function getDocblock($fixture) - { - $reflection = new ReflectionProperty($this->fixture, $fixture); - - return $reflection->getDocComment(); - } - - protected function getFixture($fixture) - { - return $this->parser->parse($this->getDocblock($fixture)); - } -} diff --git a/test/suite/CacheTest.php b/test/suite/CacheTest.php index 0f294a4..9574ab4 100644 --- a/test/suite/CacheTest.php +++ b/test/suite/CacheTest.php @@ -10,12 +10,12 @@ use Minime\Annotations\Cache\ApcCache; use Minime\Annotations\Fixtures\AnnotationsFixture; -class CacheTest extends \PHPUnit_Framework_TestCase +class CacheTest extends \PHPUnit\Framework\TestCase { protected $fixtureClass = 'Minime\Annotations\Fixtures\AnnotationsFixture'; - public function tearDown() + protected function tearDown(): void { Mockery::close(); } @@ -54,14 +54,13 @@ public function testFileCache(){ } public function testFileCacheWithDefaultStoragePath(){ - new FileCache(); + $this->assertCacheHandlerWorks(new FileCache()); } - /** - * @expectedException \InvalidArgumentException - * @expectedExceptionMessageRegExp #^Cache path is not a writable/readable directory: .+\.# - */ public function testFileCacheWithBadStoragePath(){ + $this->expectException('InvalidArgumentException'); + $this->expectExceptionMessage('Cache path is not a writable/readable directory:'); + new FileCache(__DIR__ . '/invalid/path/'); } diff --git a/test/suite/DynamicParserTest.php b/test/suite/DynamicParserTest.php index 7052d2d..996fdc6 100644 --- a/test/suite/DynamicParserTest.php +++ b/test/suite/DynamicParserTest.php @@ -2,16 +2,39 @@ namespace Minime\Annotations; +use \ReflectionProperty; +use Minime\Annotations\Fixtures\AnnotationsFixture; +use Minime\Annotations\Interfaces\ParserInterface; + /** * DynamicParserTest - * + * * @group parser */ -class DynamicParserTest extends BaseTest +class DynamicParserTest extends \PHPUnit\Framework\TestCase { - public function setUp() + protected $fixture; + + /** + * @var ParserInterface + */ + protected $parser; + + protected function getDocblock($fixture) + { + $reflection = new ReflectionProperty($this->fixture, $fixture); + + return $reflection->getDocComment(); + } + + protected function getFixture($fixture) + { + return $this->parser->parse($this->getDocblock($fixture)); + } + + protected function setUp(): void { - parent::setup(); + $this->fixture = new AnnotationsFixture; $this->parser = new DynamicParser; } diff --git a/test/suite/ParserTest.php b/test/suite/ParserTest.php index 55fdb1d..2e3a7cc 100644 --- a/test/suite/ParserTest.php +++ b/test/suite/ParserTest.php @@ -2,17 +2,14 @@ namespace Minime\Annotations; -use \ReflectionProperty; -use Minime\Annotations\Fixtures\AnnotationsFixture; - /** * ParserTest - * + * * @group parser */ class ParserTest extends DynamicParserTest { - public function setUp() + protected function setUp(): void { parent::setup(); $this->parser = new Parser; @@ -40,6 +37,14 @@ public function parseConcreteFixture() '{"foo":"bar","bar":"baz"}', json_encode($annotations['Minime\Annotations\Fixtures\AnnotationConstructInjection'][1]) ); + $this->assertInstanceOf( + 'Minime\Annotations\Fixtures\AnnotationConstructSugarInjection', + $annotations['Minime\Annotations\Fixtures\AnnotationConstructSugarInjection'][0] + ); + $this->assertInstanceOf( + 'Minime\Annotations\Fixtures\AnnotationConstructSugarInjection', + $annotations['Minime\Annotations\Fixtures\AnnotationConstructSugarInjection'][1] + ); $this->assertSame( '{"foo":"foo","bar":"bar"}', json_encode($annotations['Minime\Annotations\Fixtures\AnnotationConstructSugarInjection'][0]) @@ -68,15 +73,16 @@ public function parseConcreteFixture() /** * @test - * @expectedException \Minime\Annotations\ParserException * @dataProvider invalidConcreteAnnotationFixtureProvider */ public function parseInvalidConcreteFixture($fixture) { + $this->expectException(ParserException::class); + $this->getFixture($fixture); } - public function invalidConcreteAnnotationFixtureProvider() + public static function invalidConcreteAnnotationFixtureProvider() { return [ ['bad_concrete_fixture'], @@ -135,28 +141,31 @@ public function tolerateUnrecognizedTypes() /** * @test - * @expectedException \Minime\Annotations\ParserException */ public function exceptionWithBadJsonValue() { + $this->expectException(ParserException::class); + $this->getFixture('bad_json_fixture'); } /** * @test - * @expectedException \Minime\Annotations\ParserException */ public function exceptionWithBadIntegerValue() { + $this->expectException(ParserException::class); + $this->getFixture('bad_integer_fixture'); } /** * @test - * @expectedException \Minime\Annotations\ParserException */ public function exceptionWithBadFloatValue() { + $this->expectException(ParserException::class); + $this->getFixture('bad_float_fixture'); } @@ -173,4 +182,43 @@ public function testTypeRegister() $this->parser->unregisterType('\Minime\Annotations\Fixtures\FooType'); $this->assertSame(['value' => 'foo bar'], $this->parser->parse($docblock)); } + + /** + * @test + */ + public function testConcreteTypeNamespaceLookup() + { + $this->parser->registerConcreteNamespaceLookup([ + 'Minime\\Annotations\\Types\\Dummy\\', + 'Minime\\Annotations\\Fixtures\\' + ]); + + $annotations = $this->getFixture('short_concrete_fixture'); + + $this->assertInstanceOf( + 'Minime\Annotations\Fixtures\AnnotationConstructInjection', + $annotations['AnnotationConstructInjection'] + ); + $this->assertInstanceOf( + 'Minime\Annotations\Fixtures\AnnotationConstructSugarInjection', + $annotations['AnnotationConstructSugarInjection'] + ); + $this->assertInstanceOf( + 'Minime\Annotations\Fixtures\AnnotationSetterInjection', + $annotations['AnnotationSetterInjection'] + ); + + $this->assertSame( + '{"foo":"bar","bar":"baz"}', + json_encode($annotations['AnnotationConstructInjection']) + ); + $this->assertSame( + '{"foo":"foo","bar":"bar"}', + json_encode($annotations['AnnotationConstructSugarInjection']) + ); + $this->assertSame( + '{"foo":"bar"}', + json_encode($annotations['AnnotationSetterInjection']) + ); + } } diff --git a/test/suite/ReaderTest.php b/test/suite/ReaderTest.php index 34bb0a0..4ac9013 100644 --- a/test/suite/ReaderTest.php +++ b/test/suite/ReaderTest.php @@ -3,11 +3,11 @@ use Minime\Annotations\Fixtures\AnnotationsFixture; -class ReaderTest extends \PHPUnit_Framework_TestCase +class ReaderTest extends \PHPUnit\Framework\TestCase { private $fixture; - public function setUp() + protected function setUp(): void { $this->fixture = new AnnotationsFixture; } @@ -32,15 +32,6 @@ public function testGetAnnotations() $this->assertTrue($annotations->get('get')); } - public function testReadFunctionAnnotations() - { - if(! function_exists($fn = __NAMESPACE__ . '\\fn')){ - /** @bar */ function fn(){} - } - - $this->assertTrue($this->getReader()->getFunctionAnnotations($fn)->get('bar')); - } - public function testReadClosureAnnotations() { /** @foo */