diff --git a/README.md b/README.md index d009172..4bacbf6 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ PHP API library following Deepr specification. -![](https://img.shields.io/badge/phpunit-passed-success) ![](https://img.shields.io/badge/coverage-97%25-green) ![](https://img.shields.io/github/stars/stefanak-michal/deepr-php) ![](https://img.shields.io/packagist/dt/stefanak-michal/deepr-php) ![](https://img.shields.io/github/v/release/stefanak-michal/deepr-php) ![](https://img.shields.io/github/commits-since/stefanak-michal/deepr-php/latest) +![phpunit](https://img.shields.io/badge/phpunit-passed-success) ![coverage](https://img.shields.io/badge/coverage-94%25-green) ![stars](https://img.shields.io/github/stars/stefanak-michal/deepr-php) ![downloads](https://img.shields.io/packagist/dt/stefanak-michal/deepr-php) ![release](https://img.shields.io/github/v/release/stefanak-michal/deepr-php) ![commits](https://img.shields.io/github/commits-since/stefanak-michal/deepr-php/latest) ## Usage @@ -10,7 +10,7 @@ Check [wiki](https://github.com/stefanak-michal/deepr-php/wiki) for more informa ## Requirements -- PHP >= 7.1 +- PHP >= 7.2 ## Installation diff --git a/composer.json b/composer.json index 4f2baff..73f1cd1 100644 --- a/composer.json +++ b/composer.json @@ -1,50 +1,55 @@ { - "name": "stefanak-michal/deepr-php", - "description": "API library following Deepr specification", - "keywords": ["deepr", "php", "api", "rpc"], - "homepage": "https://github.com/stefanak-michal/deepr-php", - "type": "library", - "readme": "README.md", - "license": "MIT", - "minimum-stability": "stable", - "require": { - "php": ">=7.1.0" - }, - "require-dev": { - "phpunit/phpunit": ">=7.5.0", - "ext-json": "*" - }, - "support": { - "issues": "https://github.com/stefanak-michal/deepr-php/issues", - "source": "https://github.com/stefanak-michal/deepr-php" - }, - "funding": [ - { - "type": "paypal", - "url": "https://www.paypal.me/MichalStefanak" - } - ], - "authors": [ - { - "name": "Michal Stefanak", - "role": "Developer", - "homepage": "https://www.linkedin.com/in/michalstefanak/" - } - ], - "autoload": { - "psr-4": { - "Deepr\\": "src/" - } - }, - "autoload-dev": { - "psr-4": { - "Deepr\\tests\\": "tests/" - } - }, - "scripts": { - "test": [ - "Composer\\Config::disableProcessTimeout", - "phpunit" - ] + "name": "stefanak-michal/deepr-php", + "description": "API library following Deepr specification", + "keywords": [ + "deepr", + "php", + "api", + "rpc" + ], + "homepage": "https://github.com/stefanak-michal/deepr-php", + "type": "library", + "readme": "README.md", + "license": "MIT", + "minimum-stability": "stable", + "require": { + "php": ">=7.2.0", + "ext-json": "*" + }, + "require-dev": { + "phpunit/phpunit": ">=7.5.0" + }, + "support": { + "issues": "https://github.com/stefanak-michal/deepr-php/issues", + "source": "https://github.com/stefanak-michal/deepr-php" + }, + "funding": [ + { + "type": "paypal", + "url": "https://www.paypal.me/MichalStefanak" } + ], + "authors": [ + { + "name": "Michal Stefanak", + "role": "Developer", + "homepage": "https://www.linkedin.com/in/michalstefanak/" + } + ], + "autoload": { + "psr-4": { + "Deepr\\": "src/" + } + }, + "autoload-dev": { + "psr-4": { + "Deepr\\tests\\": "tests/" + } + }, + "scripts": { + "test": [ + "Composer\\Config::disableProcessTimeout", + "phpunit" + ] + } } diff --git a/src/Deepr.php b/src/Deepr.php index b918373..9503774 100644 --- a/src/Deepr.php +++ b/src/Deepr.php @@ -2,14 +2,9 @@ namespace Deepr; -use Deepr\components\{ - IComponent, - Collection, - ILoadable, - Value -}; -use \Exception; -use \ReflectionClass; +use Exception; +use Generator; +use ReflectionClass; /** * Class Deepr @@ -17,20 +12,9 @@ * @package Deepr * @author Michal Stefanak * @link https://github.com/stefanak-michal/deepr-php - * @link https://refactoring.guru/design-patterns/composite */ final class Deepr { - /** - * Source Values argument key - * @param string - */ - const OPTION_SV_KEY = 1; - /** - * Source values class names namespace prefix - * @param string - */ - const OPTION_SV_NS = 2; /** * A context that will be passed as the last parameter to all invoked methods. * `null` value is ignored and it's not passed as argument @@ -87,8 +71,6 @@ final class Deepr * @var array */ private static $defaultOptions = [ - self::OPTION_SV_KEY => '_type', - self::OPTION_SV_NS => '', self::OPTION_CONTEXT => null, self::OPTION_IGNORE_KEYS => [], self::OPTION_ACCEPT_KEYS => [], @@ -102,23 +84,23 @@ final class Deepr private $options; /** - * Apply query on specific Collection instance - * @param Collection $root + * Apply query on specific object instance + * @param object $root * @param array $query * @param array $options * @return array * @throws Exception * @see setOptions() */ - public function invokeQuery(Collection $root, array $query, array $options = []): array + public function invokeQuery(object $root, array $query, array $options = []): array { $this->setOptions($options); if (key($query) === '||') throw new Exception('Parallel processing not implemented'); - $this->recursion($root, key($query), $query); - return $root->execute($this->options); + $output = $this->iterate($root, $query); + return iterator_to_array($output); } /** @@ -169,153 +151,117 @@ public function setOptions(array $options = []): Deepr } /** - * Create instance of structure class by parameters - * @link https://github.com/deeprjs/deepr#source-values - * @param array $args - * @return IComponent + * Iterate through query + * @param mixed $root + * @param array $query + * @return Generator * @throws Exception */ - private function getInstance(array $args): IComponent + public function iterate($root, array $query): Generator { - if (!array_key_exists($this->options[self::OPTION_SV_KEY], $args)) - throw new Exception('Source values type key not found in arguments', self::ERROR_INSTANCE); + foreach ($query as $key => $value) { + $key2 = strpos($key, '=>') > 0 + ? substr($key, 0, strpos($key, '=>')) + : $key; - $cls = $this->options[self::OPTION_SV_NS] . $args[$this->options[self::OPTION_SV_KEY]]; - if (!class_exists($cls)) - throw new Exception('Requested class "' . $cls . '" does not exists', self::ERROR_INSTANCE); + if ($key === '<=') { + $this->debug('<='); + if (is_object($value)) { + $root = $value; + } else { + throw new Exception('Key "<=" has to contains class instance. Are you using proper deserializer?', self::ERROR_INSTANCE); + } + } elseif ($key === '=>' && is_array($value)) { + $this->debug('=>'); + yield from $this->iterate($root, $value); + } elseif ($value === true) { + $this->debug('property ' . $key); + if ($this->checkPropertyKey($key2)) { + if (is_object($root) && property_exists($root, $key2)) { + $this->authorize($root, $key2); + yield $key => $root->$key2; + } elseif (is_array($root) && array_key_exists($key2, $root)) { + $this->authorize($root, $key2); + yield $key => $root[$key2]; + } elseif ($key[-1] != '?') { + throw new Exception('Property access is available only for class instance or array.', self::ERROR_STRUCTURE); + } + } + } elseif (is_array($value) && array_key_exists('()', $value)) { + $this->debug('method ' . $key); + if (is_object($root) && method_exists($root, $key2)) { + $this->authorize($root, $key2, 'call'); + $tmp = $value; + unset($tmp['()']); + $result = $this->invokeMethod($root, $key2, $value['()']); + return yield $key => iterator_to_array($this->iterate($result, $tmp)); + } elseif ($key[-1] != '?') { + throw new Exception('You are trying access not existing method.', self::ERROR_MISSING); + } + } elseif (is_array($value) && is_object($root) && property_exists($root, $key2)) { + $this->debug('property cls ' . $key); + if (is_string($root->$key2) && class_exists($root->$key2)) + $root->$key2 = new $root->$key2(); + yield $key => iterator_to_array($this->iterate($root->$key2, $value)); + } elseif ($key === '[]') { + $this->debug('array access'); + if (is_int($value)) { + $offset = $value; + $length = 1; + } elseif (is_array($value)) { + $offset = $value[0] ?? 0; + $length = $value[1] ?? null; + } else { + throw new Exception('Wrong arguments for array access.', self::ERROR_STRUCTURE); + } - $reflection = new ReflectionClass($cls); - $invokeArgs = []; - if ($reflection->getConstructor()) { - foreach ($reflection->getConstructor()->getParameters() as $parameter) { - $invokeArgs[] = array_key_exists($parameter->getName(), $args) - ? $args[$parameter->getName()] - : $parameter->getDefaultValue(); - } - } + $tmp = $query; + unset($tmp['[]']); - $instance = new $cls(...$invokeArgs); - if (!($instance instanceof IComponent)) - throw new Exception($cls . ' has to implement IComponent', self::ERROR_STRUCTURE); + if (is_array($root)) + $items = array_slice($root, $offset, $length); + elseif (is_object($root) && is_callable($root)) + $items = $root($offset, $length); + else + throw new Exception('Array access is available only for array or class implementing Arrayable interface', self::ERROR_STRUCTURE); - return $instance; + foreach ($items as $item) { + if (is_int($value)) + yield from $this->iterate($item, $tmp); + else + yield iterator_to_array($this->iterate($item, $tmp)); + } + return null; + } elseif (is_array($value)) { + $this->debug('array values'); + yield $key => iterator_to_array($this->iterate($root, $value)); + } + } } /** - * @param IComponent $root - * @param string $action - * @param array $values + * @param object $root + * @param string $method + * @param array $args + * @return mixed * @throws Exception */ - private function recursion(IComponent &$root, string $action, array $values) + private function invokeMethod(object $root, string $method, array $args) { - foreach ($values as $k => $v) { - $key = $this->getKey($k, false); - - if ($k === '<=') { - $this->debug('<='); - $tmpValues = $values; - unset($tmpValues['<=']); - $instance = $this->getInstance($v); - $root = $instance; - } elseif (is_int($k)) { - $this->debug('array'); - $clone = clone $root; - $clone->clear(); - $this->recursion($clone, $action, $v); - $root->add($clone); - } elseif ($k === '[]' && !empty($action)) { - $this->debug($action . ' []'); - if (!($root instanceof ILoadable)) - throw new Exception('To access collection of class it has to implement ILoadable interface', self::ERROR_STRUCTURE); - - $tmpValues = $values; - unset($tmpValues['[]']); - - if (is_int($v)) { - foreach ($root->load($v, 1)->getChildren() as $child) { - $this->recursion($child, $action, $tmpValues); - $root = $child; - } - } elseif (is_array($v)) { - foreach ($root->load($v[0] ?? 0, $v[1] ?? null)->getChildren() as $name => $child) { - $this->recursion($child, $action, $tmpValues); - $root->add($child, $name); - } - } - return; - } elseif ($k === '()') { - continue; - } elseif (is_array($v) && array_key_exists('()', $v)) { - $this->debug($key . ' ()'); - $this->authorize($key, 'call'); - - if (!is_null($this->options[self::OPTION_CONTEXT])) - $v['()'][] = $this->options[self::OPTION_CONTEXT]; - - if (method_exists($root, $key)) { - if (count(array_filter(array_keys($v['()']), 'is_int')) == count($v['()'])) - $data = $root->{$key}(...$v['()']); - else { - $reflection = new \ReflectionClass($root); - $method = $reflection->getMethod($key); - $invokeArgs = []; - foreach ($method->getParameters() as $parameter) { - $invokeArgs[] = array_key_exists($parameter->getName(), $v['()']) - ? $v['()'][$parameter->getName()] - : $parameter->getDefaultValue(); - } - $data = $method->invokeArgs($root, $invokeArgs); - } - - if (is_null($data)) { - $this->recursion($root, $key, $v); - $collection = new Collection(); - $collection->add($root, $k); - $root = $collection; - return; - } - - if ($root === $data) - $root = new Collection(); - if (is_subclass_of($data, Collection::class)) - $this->recursion($data, '', $v); - elseif ($data instanceof Collection) { - foreach ($data->getChildren() as $child) - $this->recursion($child, '', $v); - } - if ($data instanceof IComponent) - $root->add($data, $k); - else - throw new Exception('Method has to return instance of IComponent or null', self::ERROR_STRUCTURE); - } elseif (strpos($k, '?') === false) { - throw new Exception('Missing method ' . $key, self::ERROR_MISSING); - } - } elseif ($v === true) { - $this->debug($action . ' ' . $k . ' true'); - if (property_exists($root, $key)) { - if ($this->checkPropertyKey($key)) { - $this->authorize($key); - $root->add(new Value($root->$key), $k); - } - } elseif (strpos($k, '?') === false) { - throw new Exception('Missing property ' . $key, self::ERROR_MISSING); - } - } elseif (property_exists($root, $key)) { - $this->debug('property ' . $key); - $collection = $root->$key; - if (is_string($collection) && class_exists($collection)) - $collection = new $collection(); - if (!($collection instanceof Collection)) - throw new Exception('Property has to be instance of collection class or class name', self::ERROR_STRUCTURE); - $this->recursion($collection, $key, $v); - $root->add($collection, $k); - } elseif (is_array($v)) { - $this->debug($action . ' array nest'); - $clone = clone $root; - $this->recursion($clone, $action, $v); - $root->add($clone, $k); + if (!is_null($this->options[self::OPTION_CONTEXT])) + $args[] = $this->options[self::OPTION_CONTEXT]; + + if (count(array_filter(array_keys($args), 'is_int')) == count($args)) { + return $root->{$method}(...$args); + } else { + $refMethod = (new ReflectionClass($root))->getMethod($method); + $invokeArgs = []; + foreach ($refMethod->getParameters() as $parameter) { + $invokeArgs[] = array_key_exists($parameter->getName(), $args) + ? $args[$parameter->getName()] + : $parameter->getDefaultValue(); } + return $refMethod->invokeArgs($root, $invokeArgs); } } @@ -340,33 +286,18 @@ private function checkPropertyKey(string $key): bool } /** + * @param array|object $root * @param string $key * @param string $operation * @throws Exception */ - private function authorize(string $key, string $operation = 'get') + private function authorize($root, string $key, string $operation = 'get') { - if (is_callable($this->options[self::OPTION_AUTHORIZER]) && $this->options[self::OPTION_AUTHORIZER]($key, $operation) === false) { + if (is_callable($this->options[self::OPTION_AUTHORIZER]) && $this->options[self::OPTION_AUTHORIZER]($root, $key, $operation) === false) { throw new Exception('Operation not allowed by authorizer', self::ERROR_AUTHORIZER); } } - /** - * Get final key - * @param string $key - * @param bool $alias If you want the source key set this to false, otherwise will return target key - * @return string - */ - private function getKey(string $key, bool $alias = true): string - { - $key = str_replace('?', '', $key); - if (strpos($key, '=>') === false) - return $key; - - list($k, $a) = explode('=>', $key, 2); - return $alias ? ($a ?? $k) : $k; - } - /** * @param string $msg */ diff --git a/src/components/Collection.php b/src/components/Collection.php deleted file mode 100644 index 9e76206..0000000 --- a/src/components/Collection.php +++ /dev/null @@ -1,78 +0,0 @@ -children[$name] = $component; - else - $this->children[] = $component; - } - - /** - * Get list of children - * @return IComponent[] - */ - final public function getChildren(): array - { - return $this->children; - } - - /** - * Clear children collection - */ - final public function clear() - { - $this->children = []; - } - - /** - * @inheritDoc - */ - final public function execute(array $options = []) - { - $output = []; - - foreach ($this->getChildren() as $key => $child) { - $result = $child->execute($options); - - if ($key !== '=>' && substr($key, -2) === '=>') { //unnest - $output = $result; - } elseif (is_array($result)) { - if (strpos($key, '=>') === false) { //just a key - $output[$key] = $result; - } else { - list ($k, $a) = explode('=>', $key); - if (empty($k) && empty($a)) { //to return - $output = array_merge($output, $result); - } elseif (!empty($a)) { //nest - $output = array_merge($output, [$a => $result]); - } - } - } else { //value - $output[$key] = $result; - } - } - - return $output; - } -} diff --git a/src/components/IComponent.php b/src/components/IComponent.php deleted file mode 100644 index 66078f2..0000000 --- a/src/components/IComponent.php +++ /dev/null @@ -1,19 +0,0 @@ -value = $value; - } - - /** - * @inheritDoc - */ - final public function execute(array $options = []) - { - return $this->value; - } -} diff --git a/tests/DeeprTest.php b/tests/DeeprTest.php index 091e596..e5d4796 100644 --- a/tests/DeeprTest.php +++ b/tests/DeeprTest.php @@ -4,6 +4,8 @@ use Deepr\Deepr; use Deepr\tests\classes\Root; +use Deepr\tests\classes\Deserializer; +use Deepr\tests\classes\Serializer; use PHPUnit\Framework\TestCase; use Exception; @@ -14,8 +16,6 @@ * @link https://github.com/stefanak-michal/deepr-php * * @covers \Deepr\Deepr - * @covers \Deepr\components\Collection - * @covers \Deepr\components\Value */ class DeeprTest extends TestCase { @@ -29,8 +29,8 @@ public function testDeepr(): ?Deepr try { $deepr::$debug = true; - $result = $deepr->invokeQuery(new Root(), ['date' => ['()' => []]]); - $this->assertEquals(['date' => '2021-07-20'], $result); + $result = $deepr->invokeQuery(new Root(), (new Deserializer())->deserialize(['info=>' => ['()' => [], 'date=>' => true]])); + $this->assertEquals('2021-07-20', (new Serializer())->serialize($result)); $deepr::$debug = false; } catch (Exception $e) { $this->markTestIncomplete($e->getMessage()); @@ -40,7 +40,7 @@ public function testDeepr(): ?Deepr } /** - * @depends testDeepr + * @depends testDeepr * @dataProvider jsonProvider * @param string $input * @param string $output @@ -54,14 +54,8 @@ public function testInvokeQueries(string $input, string $output, Deepr $deepr) try { $root = new Root(); - $input = json_decode($input, true); - if (json_last_error() != JSON_ERROR_NONE) - throw new Exception(json_last_error_msg()); - $result = $deepr->invokeQuery($root, $input, [ - $deepr::OPTION_SV_NS => "\\Deepr\\tests\\classes\\" - ]); - $result = json_encode($result); - $this->assertJsonStringEqualsJsonString($output, $result); + $result = $deepr->invokeQuery($root, (new Deserializer())->deserialize($input)); + $this->assertJsonStringEqualsJsonString($output, (new Serializer())->serialize($result)); } catch (Exception $e) { $this->markTestIncomplete($e->getMessage()); } @@ -101,7 +95,7 @@ public function testParallel(Deepr $deepr) { $root = new Root(); $this->expectException(Exception::class); - $deepr->invokeQuery($root, json_decode('{"||":[]}', true)); + $deepr->invokeQuery($root, (new Deserializer())->deserialize('{"||":[]}')); } /** @@ -113,7 +107,7 @@ public function testException(Deepr $deepr) $root = new Root(); $this->expectException(Exception::class); $this->expectExceptionCode($deepr::ERROR_STRUCTURE); - $deepr->invokeQuery($root, json_decode('{ "[]": [] }', true)); + $deepr->invokeQuery($root, (new Deserializer())->deserialize('{ "[]": [] }')); } /** @@ -124,10 +118,10 @@ public function testOptionIgnoreKeys(Deepr $deepr) { try { $root = new Root(); - $result = $deepr->invokeQuery($root, json_decode('{"movies":{"[]":2,"_id":true,"title":true}}', true), [ + $result = $deepr->invokeQuery($root, (new Deserializer())->deserialize('{"movies":{"[]":2,"_id":true,"title":true}}'), [ $deepr::OPTION_IGNORE_KEYS => ['/^_/'] ]); - $this->assertJsonStringEqualsJsonString(json_encode($result), '{"movies":{"title":"The Matrix Revolutions"}}'); + $this->assertJsonStringEqualsJsonString('{"movies":{"title":"The Matrix Revolutions"}}', (new Serializer())->serialize($result)); } catch (Exception $e) { $this->markTestIncomplete($e->getMessage()); } @@ -141,11 +135,11 @@ public function testOptionAcceptKeys(Deepr $deepr) { try { $root = new Root(); - $result = $deepr->invokeQuery($root, json_decode('{"movies":{"[]":2,"_id":true,"title":true}}', true), [ + $result = $deepr->invokeQuery($root, (new Deserializer())->deserialize('{"movies":{"[]":2,"_id":true,"title":true}}'), [ $deepr::OPTION_IGNORE_KEYS => ['/^_/'], $deepr::OPTION_ACCEPT_KEYS => ['_id'] ]); - $this->assertJsonStringEqualsJsonString(json_encode($result), '{"movies":{"_id":10,"title":"The Matrix Revolutions"}}'); + $this->assertJsonStringEqualsJsonString('{"movies":{"_id":10,"title":"The Matrix Revolutions"}}', (new Serializer())->serialize($result)); } catch (Exception $e) { $this->markTestIncomplete($e->getMessage()); } @@ -159,10 +153,10 @@ public function testOptionContext(Deepr $deepr) { try { $root = new Root(); - $result = $deepr->invokeQuery($root, json_decode('{"sayHello":{"()":["John"]}}', true), [ + $result = $deepr->invokeQuery($root, (new Deserializer())->deserialize('{"sayHello":{"()":["John"], "msg=>": true}}'), [ $deepr::OPTION_CONTEXT => 'Hi', ]); - $this->assertEquals(['sayHello' => 'Hi John!'], $result); + $this->assertJsonStringEqualsJsonString('{"sayHello": "Hi John!"}', (new Serializer())->serialize($result)); } catch (Exception $e) { $this->markTestIncomplete($e->getMessage()); } @@ -176,12 +170,13 @@ public function testOptionAuthorizer(Deepr $deepr) { try { $root = new Root(); - $result = $deepr->invokeQuery($root, json_decode('{"sayHello":{"()":["John"]}}', true), [ - $deepr::OPTION_AUTHORIZER => function (string $key, string $operation) { - return $key == 'sayHello' && $operation == 'call'; + $result = $deepr->invokeQuery($root, (new Deserializer())->deserialize('{"sayHello":{"()":["John"], "msg=>": true}}'), [ + $deepr::OPTION_AUTHORIZER => function ($root, string $key, string $operation) { + return ($root instanceof Root && $key == 'sayHello' && $operation == 'call') + || (is_array($root) && $key == 'msg' && $operation == 'get'); } ]); - $this->assertEquals(['sayHello' => 'Hello John!'], $result); + $this->assertJsonStringEqualsJsonString('{"sayHello": "Hello John!"}', (new Serializer())->serialize($result)); } catch (Exception $e) { $this->markTestIncomplete($e->getMessage()); } @@ -196,8 +191,8 @@ public function testOptionAuthorizerException(Deepr $deepr) $root = new Root(); $this->expectException(Exception::class); $this->expectExceptionCode($deepr::ERROR_AUTHORIZER); - $deepr->invokeQuery($root, json_decode('{"sayHello":{"()":["John"]}}', true), [ - $deepr::OPTION_AUTHORIZER => function (string $key, string $operation) { + $deepr->invokeQuery($root, (new Deserializer())->deserialize('{"sayHello":{"()":["John"]}}'), [ + $deepr::OPTION_AUTHORIZER => function ($root, string $key, string $operation) { return false; } ]); @@ -211,8 +206,11 @@ public function testSetOptions(Deepr $deepr) { try { $deepr->setOptions([ - $deepr::OPTION_SV_KEY => null, - $deepr::OPTION_IGNORE_KEYS => null + $deepr::OPTION_CONTEXT => 'abc', + $deepr::OPTION_IGNORE_KEYS => null, + $deepr::OPTION_ACCEPT_KEYS => ['/.*/'], + $deepr::OPTION_AUTHORIZER => function ($root, string $key, string $operation) { + } ]); $this->assertInstanceOf(Deepr::class, $deepr); } catch (Exception $e) { @@ -229,7 +227,7 @@ public function testSetOptionsStringException(Deepr $deepr) $this->expectException(Exception::class); $this->expectExceptionCode($deepr::ERROR_OPTIONS); $deepr->setOptions([ - $deepr::OPTION_SV_KEY => ['this has to be string and not a array'], + $deepr::OPTION_ACCEPT_KEYS => 'this has to be array and not a string', ]); } @@ -267,7 +265,7 @@ public function testFaultTolerant(Deepr $deepr) { try { $root = new Root(); - $result = $deepr->invokeQuery($root, json_decode('{"abc?": true, "method?": {"()": []}}', true)); + $result = $deepr->invokeQuery($root, (new Deserializer())->deserialize('{"abc?": true, "method?": {"()": []}}')); $this->assertEquals([], $result); } catch (Exception $e) { $this->markTestIncomplete($e->getMessage()); diff --git a/tests/classes/Deserializer.php b/tests/classes/Deserializer.php new file mode 100644 index 0000000..afea750 --- /dev/null +++ b/tests/classes/Deserializer.php @@ -0,0 +1,56 @@ +recursion($query); + } + + /** + * @param array $query + * @return array + * @throws Exception + */ + private function recursion(array $query): array + { + foreach ($query as $key => &$value) { + if ($key === '<=') { // insert class instance by query + if (array_key_exists('_type', $value) && class_exists("\\Deepr\\tests\\classes\\" . $value['_type'])) { + $clsName = "\\Deepr\\tests\\classes\\" . $value['_type']; + unset($value['_type']); + $value = new $clsName(...array_values($value)); + } + } + + if (is_array($value)) { + $value = $this->deserialize($value); + } + } + + return $query; + } +} diff --git a/tests/classes/Movie.php b/tests/classes/Movie.php index bd6aa2c..e6e6037 100644 --- a/tests/classes/Movie.php +++ b/tests/classes/Movie.php @@ -2,16 +2,13 @@ namespace Deepr\tests\classes; -use Deepr\components\Collection; -use Deepr\components\IComponent; - /** * Class Movie * @package Deepr\tests\classes * @author Michal Stefanak * @link https://github.com/stefanak-michal/deepr-php */ -class Movie extends Collection +class Movie { public $_id; /** @@ -24,30 +21,34 @@ class Movie extends Collection * @var int */ public $released; + /** + * {"movies": {"[]":[], "tagline": true}} + * @var string + */ public $tagline; /** - * @var Collection + * @var array */ private $actors; /** * RPC method to get actors of movie * {"movies":{"[]":[],"=>":{"getActors":{"()":[]}}}} - * @return IComponent + * @return array * @see \Deepr\tests\classes\Person */ - public function getActors(): IComponent + public function getActors(): array { if (is_null($this->actors)) { - $this->actors = new Collection(); + $this->actors = []; foreach (Database::getMovieActors($this->_id) as $row) { $person = new Person(); $person->_id = $row['_id']; $person->name = $row['name']; $person->born = $row['born']; $person->_movies = $row['_movies']; - $this->actors->add($person); + $this->actors[] = $person; } } @@ -55,9 +56,10 @@ public function getActors(): IComponent } /** - * RPC method to get movie by title. Use it with source values call. + * RPC method to get movie by title. * {"<=": {"_type": "Movie"}, "byTitle": {"()": ["The Matrix"]}} * @param string $title + * @return Movie * @see \Deepr\tests\classes\Movie */ public function byTitle(string $title) @@ -69,6 +71,7 @@ public function byTitle(string $title) $this->released = $row['released']; $this->tagline = $row['tagline']; } + return $this; } } diff --git a/tests/classes/Movies.php b/tests/classes/Movies.php index 57e88c4..8f80f78 100644 --- a/tests/classes/Movies.php +++ b/tests/classes/Movies.php @@ -2,10 +2,6 @@ namespace Deepr\tests\classes; -use Deepr\components\Collection; -use Deepr\components\IComponent; -use Deepr\components\ILoadable; - /** * Class Movies * Interface ILoadable is required if you want to access collection items with "[]" @@ -13,7 +9,7 @@ * @author Michal Stefanak * @link https://github.com/stefanak-michal/deepr-php */ -class Movies extends Collection implements ILoadable +class Movies { /** * {"movies": {"count": true}} @@ -27,24 +23,40 @@ public function __construct() } /** - * This method is implemented by interface ILoadable and it's called on "[]" - * {"movies": {"[]": []}} + * RPC method to get movie by title + * {"movies": {"getByTitle": {"()": ["The Matrix"]}}} + * @param string $title + * @return Movie + */ + public function getByTitle(string $title): Movie + { + $row = Database::getMovieByTitle($title); + $movie = new Movie(); + $movie->_id = $row['_id']; + $movie->title = $row['title']; + $movie->released = $row['released']; + $movie->tagline = $row['tagline']; + return $movie; + } + + /** + * This magic method is called on "[]" array access * @param int $offset * @param int|null $length - * @return Collection - * @see \Deepr\tests\classes\Movie + * @return array */ - public function load(int $offset, ?int $length): Collection + public function __invoke(int $offset, ?int $length): array { - $items = new Collection(); + $output = []; foreach (array_slice(Database::getMovies(), $offset, $length) as $row) { $movie = new Movie(); $movie->_id = $row['_id']; $movie->title = $row['title']; $movie->released = $row['released']; $movie->tagline = $row['tagline']; - $items->add($movie); + $output[] = $movie; } - return $items; + return $output; } + } diff --git a/tests/classes/Person.php b/tests/classes/Person.php index 7050712..1459ee0 100644 --- a/tests/classes/Person.php +++ b/tests/classes/Person.php @@ -2,15 +2,13 @@ namespace Deepr\tests\classes; -use Deepr\components\Collection; - /** * Class Person * @package Deepr\tests\classes * @author Michal Stefanak * @link https://github.com/stefanak-michal/deepr-php */ -class Person extends Collection +class Person { public $_id; /** @@ -18,8 +16,15 @@ class Person extends Collection * @var string */ public $name; + /** + * @var int + */ public $born; + /** + * IDs of movies + * @var array + */ public $_movies; /** @@ -43,11 +48,11 @@ public function __construct(int $id = -1) /** * RPC method to get movies in which actor acted * {..actor: {"getMovies":{"()": []}} - * @return Collection + * @return array */ - public function getMovies(): Collection + public function getMovies(): array { - $items = new Collection(); + $items = []; if (empty($this->_movies)) return $items; foreach (Database::getMovies() as $row) { @@ -57,7 +62,7 @@ public function getMovies(): Collection $movie->title = $row['title']; $movie->released = $row['released']; $movie->tagline = $row['tagline']; - $items->add($movie); + $items[] = $movie; } } return $items; diff --git a/tests/classes/Root.php b/tests/classes/Root.php index 6df873a..2947899 100644 --- a/tests/classes/Root.php +++ b/tests/classes/Root.php @@ -2,8 +2,6 @@ namespace Deepr\tests\classes; -use Deepr\components\{Collection, Value, IComponent}; - /** * Class Root * This object is provided for Deepr->invokeQuery @@ -11,14 +9,14 @@ * @author Michal Stefanak * @link https://github.com/stefanak-michal/deepr-php */ -class Root extends Collection +class Root { /** * Reference to Movies collection * Only public properties are accessible - * {"movies": ...} + * {"movies": {...}} * @internal It can be instance of class Movies directly - * @var string|Collection + * @var string|Movies * @see \Deepr\tests\classes\Movies */ public $movies = Movies::class; @@ -26,21 +24,24 @@ class Root extends Collection /** * Sample method * RPC methods has to be public and returns IComponent - * {"date": {"()": []}} - * @return IComponent + * {"info": {"()": [], "date": true}} + * @see \Deepr\tests\DeeprTest::testDeepr() + * @return array */ - public function date(): IComponent + public function info(): array { - return new Value('2021-07-20'); + return ['date' => '2021-07-20']; } /** + * For testing OPTION_CONTEXT + * @see \Deepr\tests\DeeprTest::testOptionContext() * @param string $value * @param string $prefix - * @return IComponent + * @return array */ - public function sayHello(string $value, string $prefix = 'Hello'): IComponent + public function sayHello(string $value, string $prefix = 'Hello'): array { - return new Value($prefix . ' ' . $value . '!'); + return ['msg' => $prefix . ' ' . $value . '!']; } } diff --git a/tests/classes/Serializer.php b/tests/classes/Serializer.php new file mode 100644 index 0000000..80079d8 --- /dev/null +++ b/tests/classes/Serializer.php @@ -0,0 +1,55 @@ +recursion($result); + if (!is_string($output)) { + $output = json_encode($output, JSON_PRETTY_PRINT); + if (json_last_error() != JSON_ERROR_NONE) + throw new Exception(json_last_error_msg()); + } + return $output; + } + + /** + * @param array $result + * @return mixed + */ + private function recursion(array $result) + { + $output = []; + foreach ($result as $key => $value) { + if (is_array($value)) + $value = $this->recursion($value); + + if ($key !== '=>' && substr($key, -2) === '=>') { //unnest + return $value; + } else { //value + $a = $key; + if (strpos($key, '=>') !== false) + list ($k, $a) = explode('=>', $key); + $output[$a] = $value; + } + } + return $output; + } +} diff --git a/tests/jsons/07-input.json b/tests/jsons/07-input.json index aa7d43f..c6ca86e 100644 --- a/tests/jsons/07-input.json +++ b/tests/jsons/07-input.json @@ -5,6 +5,7 @@ "title": true, "getActors=>actors": { "()": [], + "[]": [], "name": true } } diff --git a/tests/jsons/09-input.json b/tests/jsons/09-input.json index b47f57c..fb035b2 100644 --- a/tests/jsons/09-input.json +++ b/tests/jsons/09-input.json @@ -1,5 +1,6 @@ { - "date": { - "()": [] + "info": { + "()": [], + "date": true } } \ No newline at end of file diff --git a/tests/jsons/09-output.json b/tests/jsons/09-output.json index 882b8b0..053e475 100644 --- a/tests/jsons/09-output.json +++ b/tests/jsons/09-output.json @@ -1,3 +1,5 @@ { - "date": "2021-07-20" + "info": { + "date": "2021-07-20" + } } \ No newline at end of file diff --git a/tests/jsons/12-input.json b/tests/jsons/12-input.json index dbaedf0..a20f98e 100644 --- a/tests/jsons/12-input.json +++ b/tests/jsons/12-input.json @@ -5,9 +5,11 @@ "title": true, "getActors=>actors": { "()": [], + "[]": [], "name": true, "getMovies=>movies": { "()": [], + "[]": [], "title=>": true } } diff --git a/tests/jsons/13-input.json b/tests/jsons/13-input.json index bf9faa6..8a32944 100644 --- a/tests/jsons/13-input.json +++ b/tests/jsons/13-input.json @@ -7,6 +7,7 @@ "name": true, "getMovies": { "()": [], + "[]": [], "title": true } }