├── .gitignore ├── docs ├── _config.yml ├── index.md ├── ru │ ├── index.md │ └── release.md └── release.md ├── tests ├── autoload.php ├── Core │ ├── ExceptionTest.php │ ├── ConfigAwareTraitTest.php │ ├── DateTimeTest.php │ ├── InstanceableTraitTest.php │ ├── CollectionTest.php │ ├── TypedCollectionTest.php │ ├── SingletonTraitTest.php │ ├── HasSchemaTraitTest.php │ ├── StdGetSetTraitTest.php │ ├── ConfigSaveTest.php │ ├── ConfigTest.php │ ├── StdTest.php │ ├── ExceptionsTest.php │ ├── TypedCollectionTraitTest.php │ ├── StdGetSetValidateSanitizeTraitTest.php │ ├── ObjectAsArrayTraitTest.php │ └── CollectionTraitTest.php ├── phpunit.xml ├── Di │ ├── ContainerExceptionTest.php │ ├── ContainerEntryNotFoundExceptionTest.php │ ├── ContainerTest.php │ └── ContainerResolveTest.php └── Reflection │ └── ReflectionHelpersTest.php ├── src ├── Reflection │ ├── Exception.php │ └── ReflectionHelpers.php ├── Storages │ ├── StorageInterface.php │ ├── StorageAwareInterface.php │ ├── KeyValueStorageAwareInterface.php │ ├── SingleValueStorageAwareInterface.php │ ├── SingleValueStorageInterface.php │ └── KeyValueStorageInterface.php ├── Di │ ├── Container.php │ ├── ContainerException.php │ ├── ContainerEntryNotFoundException.php │ ├── ContainerInterface.php │ └── ContainerTrait.php └── Core │ ├── HasInnerCastingInterface.php │ ├── HasInnerValidationInterface.php │ ├── HasInnerSanitizationInterface.php │ ├── SingletonInterface.php │ ├── HasRequiredInterface.php │ ├── TypedCollectionInterface.php │ ├── Exception.php │ ├── InstanceableInterface.php │ ├── StdGetSetInterface.php │ ├── ConfigAwareInterface.php │ ├── Collection.php │ ├── InstanceableTrait.php │ ├── TypedCollection.php │ ├── InstanceableByConfigInterface.php │ ├── HasSchemaInterface.php │ ├── DateTime.php │ ├── ConfigAwareTrait.php │ ├── ArrayCastingInterface.php │ ├── SingletonTrait.php │ ├── Exceptions.php │ ├── ObjectAsArrayInterface.php │ ├── StdGetSetTrait.php │ ├── CollectionInterface.php │ ├── Std.php │ ├── HasSchemaTrait.php │ ├── TypedCollectionTrait.php │ ├── Config.php │ ├── StdGetSetValidateSanitizeTrait.php │ ├── ObjectAsArrayTrait.php │ └── CollectionTrait.php ├── .travis.yml ├── README.md ├── composer.json └── LICENSE /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor/ -------------------------------------------------------------------------------- /docs/_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-slate -------------------------------------------------------------------------------- /tests/autoload.php: -------------------------------------------------------------------------------- 1 | $this->getCode(), 'message' => $this->getMessage()]; 20 | } 21 | 22 | } -------------------------------------------------------------------------------- /src/Core/InstanceableInterface.php: -------------------------------------------------------------------------------- 1 | fromArray($data); 24 | } 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /src/Core/InstanceableTrait.php: -------------------------------------------------------------------------------- 1 | fromArray($data); 23 | } 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /src/Storages/StorageAwareInterface.php: -------------------------------------------------------------------------------- 1 | assertSame('{"code":0,"message":""}', json_encode($e)); 15 | 16 | $e = new Exception('Some error'); 17 | $this->assertSame('{"code":0,"message":"Some error"}', json_encode($e)); 18 | 19 | $e = new Exception('Some error', 42); 20 | $this->assertSame('{"code":42,"message":"Some error"}', json_encode($e)); 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /src/Core/DateTime.php: -------------------------------------------------------------------------------- 1 | json_encode, 18 | * which is a value of any type other than a resource. 19 | * @since 5.4.0 20 | */ 21 | public function jsonSerialize() 22 | { 23 | return $this->format('c'); 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /src/Core/ConfigAwareTrait.php: -------------------------------------------------------------------------------- 1 | config = $config; 26 | return $this; 27 | } 28 | 29 | /** 30 | * @return \Runn\Core\Config|null 31 | */ 32 | public function getConfig(): ?Config 33 | { 34 | return $this->config; 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /src/Core/ArrayCastingInterface.php: -------------------------------------------------------------------------------- 1 | assertNull($obj->getConfig()); 18 | 19 | $config = new Config(['foo' => 'bar']); 20 | 21 | $ret = $obj->setConfig($config); 22 | $this->assertSame($config, $obj->getConfig()); 23 | $this->assertSame($obj, $ret); 24 | 25 | $ret = $obj->setConfig(null); 26 | $this->assertNull($obj->getConfig()); 27 | $this->assertSame($obj, $ret); 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /tests/Core/DateTimeTest.php: -------------------------------------------------------------------------------- 1 | assertInstanceOf(DateTime::class, $datetime); 17 | $this->assertInstanceOf(\DateTime::class, $datetime); 18 | $this->assertInstanceOf(\JsonSerializable::class, $datetime); 19 | 20 | $this->assertEquals(new \DateTime($time), $datetime); 21 | } 22 | 23 | public function testJson() 24 | { 25 | $time = date('Y-m-d H:i:s', time()); 26 | $datetime = new DateTime($time); 27 | 28 | $this->assertSame('"' . date('c', strtotime($time)) . '"', json_encode($datetime)); 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /src/Core/SingletonTrait.php: -------------------------------------------------------------------------------- 1 | =7.4", 29 | "psr/container": "*" 30 | }, 31 | "require-dev": { 32 | "phpunit/phpunit": "8.*" 33 | }, 34 | "autoload": { 35 | "psr-4": { 36 | "Runn\\": "src/" 37 | } 38 | } 39 | } -------------------------------------------------------------------------------- /src/Storages/SingleValueStorageInterface.php: -------------------------------------------------------------------------------- 1 | id = $id; 26 | parent::__construct($previous); 27 | } 28 | 29 | /** 30 | * Container entry identifier 31 | * 32 | * @return string 33 | */ 34 | public function getId(): string 35 | { 36 | return $this->id; 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /tests/phpunit.xml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 9 | Core 10 | 11 | 12 | Di 13 | 14 | 15 | Reflection 16 | 17 | 18 | 19 | 20 | 21 | ../src 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /tests/Core/InstanceableTraitTest.php: -------------------------------------------------------------------------------- 1 | x = $x; 19 | $this->y = $y; 20 | } 21 | } 22 | 23 | class InstanceableTraitTest extends TestCase 24 | { 25 | 26 | public function testWoArguments() 27 | { 28 | $obj = testClass1::instance(); 29 | 30 | $this->assertInstanceOf(testClass1::class, $obj); 31 | $this->assertNull($obj->x); 32 | $this->assertNull($obj->y); 33 | } 34 | 35 | 36 | public function testWArguments() 37 | { 38 | $obj = testClass1::instance(1, -1); 39 | 40 | $this->assertInstanceOf(testClass1::class, $obj); 41 | $this->assertEquals(1, $obj->x); 42 | $this->assertEquals(-1, $obj->y); 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /src/Core/Exceptions.php: -------------------------------------------------------------------------------- 1 | fromArray($data); 32 | } 33 | } 34 | 35 | /** 36 | * @param $value 37 | * @return $this 38 | */ 39 | public function add($value) 40 | { 41 | if ($value instanceof self) { 42 | foreach ($value as $v) { 43 | $this->collectionAdd($v); 44 | } 45 | } else { 46 | $this->collectionAdd($value); 47 | } 48 | return $this; 49 | } 50 | 51 | } 52 | -------------------------------------------------------------------------------- /tests/Di/ContainerExceptionTest.php: -------------------------------------------------------------------------------- 1 | assertInstanceOf(ContainerException::class, $exception); 16 | $this->assertInstanceOf(ContainerExceptionInterface::class, $exception); 17 | $this->assertEmpty($exception->getMessage()); 18 | $this->assertEmpty($exception->getCode()); 19 | $this->assertEmpty($exception->getPrevious()); 20 | 21 | $prev = new \Exception; 22 | $exception = new ContainerException($prev); 23 | $this->assertInstanceOf(ContainerException::class, $exception); 24 | $this->assertInstanceOf(ContainerExceptionInterface::class, $exception); 25 | $this->assertEmpty($exception->getMessage()); 26 | $this->assertEmpty($exception->getCode()); 27 | $this->assertSame($prev, $exception->getPrevious()); 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /src/Storages/KeyValueStorageInterface.php: -------------------------------------------------------------------------------- 1 | assertInstanceOf(Collection::class, $collection); 17 | $this->assertInstanceOf(CollectionInterface::class, $collection); 18 | $this->assertCount(0, $collection); 19 | 20 | $collection = new Collection([100, 200, 300, [400, 500]]); 21 | 22 | $this->assertInstanceOf(Collection::class, $collection); 23 | $this->assertInstanceOf(CollectionInterface::class, $collection); 24 | $this->assertCount(4, $collection); 25 | 26 | $this->assertEquals( 27 | [100, 200, 300, new Collection([400, 500])], 28 | $collection->values() 29 | ); 30 | $this->assertEquals( 31 | [100, 200, 300, [400, 500]], 32 | $collection->toArray() 33 | ); 34 | $this->assertEquals(100, $collection[0]); 35 | $this->assertEquals(200, $collection[1]); 36 | $this->assertEquals(300, $collection[2]); 37 | $this->assertEquals(new Collection([400, 500]), $collection[3]); 38 | } 39 | 40 | public function testConstructWithZeroKey() 41 | { 42 | $collection = new Collection([1 => '1', 0 => '0', 2 => '2']); 43 | $this->assertCount(3, $collection); 44 | $this->assertSame([1 => '1', 0 => '0', 2 => '2'], $collection->values()); 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /src/Core/StdGetSetTrait.php: -------------------------------------------------------------------------------- 1 | innerIsSet($key); 29 | } 30 | 31 | public function __unset($key) 32 | { 33 | $this->innerUnSet($key); 34 | } 35 | 36 | public function __get($key) 37 | { 38 | if (!$this->innerIsSet($key)) { 39 | $debug = debug_backtrace(\DEBUG_BACKTRACE_PROVIDE_OBJECT, 1)[0]; 40 | if ($debug['function'] == '__get' && $debug['object'] === $this && $debug['type'] == '->') { 41 | $property = $debug['args']['0']; 42 | $line = (file($debug['file'])[$debug['line'] - 1]); 43 | if (preg_match('~\-\>' . $property . '\-\>.+\=~', $line, $m)) { 44 | $this->__data[$property] = new static; 45 | return $this->__data[$property]; 46 | } 47 | } 48 | } 49 | return $this->innerGet($key); 50 | } 51 | 52 | public function __set($key, $val) 53 | { 54 | $this->innerSet($key, $val); 55 | } 56 | 57 | } -------------------------------------------------------------------------------- /tests/Di/ContainerEntryNotFoundExceptionTest.php: -------------------------------------------------------------------------------- 1 | expectException(\ArgumentCountError::class); 16 | new ContainerEntryNotFoundException; 17 | } 18 | 19 | public function testConstruct() 20 | { 21 | $exception = new ContainerEntryNotFoundException('foo'); 22 | $this->assertInstanceOf(ContainerEntryNotFoundException::class, $exception); 23 | $this->assertInstanceOf(ContainerException::class, $exception); 24 | $this->assertInstanceOf(ContainerExceptionInterface::class, $exception); 25 | $this->assertSame('foo', $exception->getId()); 26 | $this->assertEmpty($exception->getMessage()); 27 | $this->assertEmpty($exception->getCode()); 28 | $this->assertEmpty($exception->getPrevious()); 29 | 30 | $prev = new \Exception; 31 | $exception = new ContainerEntryNotFoundException('foo', $prev); 32 | $this->assertInstanceOf(ContainerException::class, $exception); 33 | $this->assertInstanceOf(ContainerExceptionInterface::class, $exception); 34 | $this->assertSame('foo', $exception->getId()); 35 | $this->assertEmpty($exception->getMessage()); 36 | $this->assertEmpty($exception->getCode()); 37 | $this->assertSame($prev, $exception->getPrevious()); 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /tests/Core/TypedCollectionTest.php: -------------------------------------------------------------------------------- 1 | 1], (object)['bar' => 2]]) 15 | extends TypedCollection { 16 | public static function getType() { return \stdClass::class;} 17 | }; 18 | 19 | $this->assertInstanceOf(TypedCollection::class, $collection); 20 | $this->assertSame(\stdClass::class, $collection->getType()); 21 | $this->assertCount(2, $collection); 22 | 23 | $this->assertEquals((object)['foo' => 1], $collection[0]); 24 | $this->assertEquals((object)['bar' => 2], $collection[1]); 25 | } 26 | 27 | public function testGetType() 28 | { 29 | $collection = new class(['type' => (object)['value' => 42]]) 30 | extends TypedCollection { 31 | public static function getType() { return \stdClass::class;} 32 | }; 33 | 34 | $this->assertSame(\stdClass::class, $collection->getType()); 35 | $this->assertSame(42, $collection['type']->value); 36 | } 37 | 38 | public function testConstructTypeMismatch() 39 | { 40 | $this->expectException(Exception::class); 41 | $this->expectExceptionMessage('Typed collection type mismatch'); 42 | $collection = new class(['foo' => 1, 'bar' => 2]) 43 | extends TypedCollection { 44 | public static function getType() { return \stdClass::class;} 45 | }; 46 | } 47 | 48 | } 49 | -------------------------------------------------------------------------------- /src/Core/CollectionInterface.php: -------------------------------------------------------------------------------- 1 | x = $x; 22 | $this->y = $y; 23 | } 24 | } 25 | 26 | class SingletonTraitTest extends TestCase 27 | { 28 | 29 | public function testWoArguments() 30 | { 31 | $obj1 = testClass1::instance(); 32 | $obj2 = testClass1::instance(); 33 | 34 | $this->assertSame($obj2, $obj1); 35 | } 36 | 37 | public function testDifferentInstances() 38 | { 39 | $obj1 = testClass1::instance(); 40 | $obj2 = testClass2::instance(1, -1); 41 | 42 | $this->assertNotSame($obj1, $obj2); 43 | $this->assertInstanceOf(testClass1::class, $obj1); 44 | $this->assertInstanceOf(testClass2::class, $obj2); 45 | } 46 | 47 | public function testWArguments() 48 | { 49 | $obj1 = testClass2::instance(1, -1); 50 | $obj2 = testClass2::instance(1, -1); 51 | 52 | $this->assertSame($obj2, $obj1); 53 | $this->assertEquals(1, $obj1->x); 54 | $this->assertEquals(-1, $obj1->y); 55 | } 56 | 57 | public function testConstruct() 58 | { 59 | $reflector = new \ReflectionClass(testClass1::class); 60 | $this->assertFalse( $reflector->getMethod('__construct')->isPublic() ); 61 | } 62 | 63 | public function testClone() 64 | { 65 | $reflector = new \ReflectionClass(testClass1::class); 66 | $this->assertFalse( $reflector->getMethod('__clone')->isPublic() ); 67 | } 68 | 69 | } 70 | -------------------------------------------------------------------------------- /docs/release.md: -------------------------------------------------------------------------------- 1 | 7.4.11, 8.0.11 2 | ============== 3 | * PHP 7.2 support is dropped 4 | * PHP 7.3 support is dropped 5 | * PHP 8.0 support is added 6 | * PSR Container compatibility fix 7 | 8 | 7.2.10, 7.3.10, 7.4.10 9 | ====================== 10 | * Container implementation refactoring 11 | * `Container::resolve()` method is added 12 | 13 | 7.2.9, 7.3.9, 7.4.9 14 | =================== 15 | * Minor fix with `ReflectionType` 16 | 17 | 7.2.8, 7.3.8, 7.4.8 18 | =================== 19 | * Tests are using PHPUnit 8 framework now 20 | 21 | 7.2.7, 7.3.7, 7.4.7 22 | =================== 23 | * Runn\Core\DateTime class is added 24 | 25 | 7.2.6, 7.3.6, 7.4.6 26 | =================== 27 | * PHP 7.0 support is dropped 28 | * PHP 7.1 support is dropped 29 | * PHP 7.4 support is added 30 | 31 | 7.0.5, 7.1.5, 7.2.5 32 | =================== 33 | * PSR DI Container is added 34 | 35 | 7.0.4, 7.1.4, 7.2.4 36 | =================== 37 | * More PHP 7.1 compatibility 38 | * License identifier update 39 | 40 | 7.0.3, 7.1.3, 7.2.3 41 | =================== 42 | * Strict type check in TypedCollection is added 43 | * notgetters() and notsetters() protected methods are added instead of $__notgetters and $__notsetters 44 | 45 | 7.0.2, 7.1.2, 7.2.2 46 | =================== 47 | * InstanceableInterface and its implementation by InstanceableTrait are added 48 | * \#3 $__notgetters and $__notsetters properties are added in ObjectAsArray for getters and setters ignore 49 | * ObjectAsArrayInterface::isEmpty() method is renamed to empty() 50 | * ConfigAwareInterface interface and its implementation by ConfigAwareTrait are added 51 | * HasSchemaInterface and HasSchemaTrait are added for classes with data schema 52 | * ReflectionHelpers class is added 53 | * ObjectAsArrayTrait::needCasting() fix 54 | 55 | 7.0.1, 7.1.1, 7.2.1 56 | =================== 57 | * Zero keys in Objects-as-Arrays fix 58 | 59 | 7.0.0, 7.1.0, 7.2.0 60 | =================== 61 | * First released version. Code is transfered from Running.FM project -------------------------------------------------------------------------------- /src/Core/Std.php: -------------------------------------------------------------------------------- 1 | getRequiredKeys() as $required) { 47 | if (!isset($this->$required)) { 48 | $errors->add(new Exception('Required property "' . $required . '" is missing')); 49 | } 50 | } 51 | if (!$errors->empty()) { 52 | throw $errors; 53 | } 54 | return true; 55 | } 56 | 57 | /** 58 | * Std constructor. 59 | * @param iterable|null $data 60 | * @throws \Runn\Core\Exceptions 61 | */ 62 | public function __construct(iterable $data = null) 63 | { 64 | $errors = new Exceptions(); 65 | 66 | if (null !== $data) { 67 | try { 68 | $this->fromArray($data); 69 | } catch (Exceptions $errors) {} 70 | } 71 | 72 | try { 73 | $this->checkRequired(); 74 | } catch (\Throwable $e) { 75 | $errors->add($e); 76 | } 77 | 78 | if (!$errors->empty()) { 79 | throw $errors; 80 | } 81 | } 82 | 83 | } 84 | -------------------------------------------------------------------------------- /docs/ru/release.md: -------------------------------------------------------------------------------- 1 | 7.4.11, 8.0.11 2 | ============== 3 | * Прекращена поддержка PHP 7.2 4 | * Прекращена поддержка PHP 7.3 5 | * Добавлена поддержка PHP 8.0 6 | * Исправлена ошибка совместимости с новой версией PSR Container 7 | 8 | 7.2.10, 7.3.10, 7.4.10 9 | ====================== 10 | * Рефакторинг реализации контейнера 11 | * Добавлен метод `Container::resolve()` 12 | 13 | 7.2.9, 7.3.9, 7.4.9 14 | =================== 15 | * Исправление ошибки с `ReflectionType` 16 | 17 | 7.2.8, 7.3.8, 7.4.8 18 | =================== 19 | * Тесты теперь используют фреймворк PHPUnit 8 20 | 21 | 7.2.7, 7.3.7, 7.4.7 22 | =================== 23 | * Добавлен класс Runn\Core\DateTime 24 | 25 | 7.2.6, 7.3.6, 7.4.6 26 | =================== 27 | * Прекращена поддержка PHP 7.0 28 | * Прекращена поддержка PHP 7.1 29 | * Добавлена поддержка PHP 7.4 30 | 31 | 7.0.5, 7.1.5, 7.2.5 32 | =================== 33 | * Добавлена реализация PSR DI Container 34 | 35 | 7.0.4, 7.1.4, 7.2.4 36 | =================== 37 | * Улучшена совместимость с PHP 7.1 38 | * Обновлен идентификатор лицензии библиотеки 39 | 40 | 7.0.3, 7.1.3, 7.2.3 41 | =================== 42 | * Добавлен строгий (без наследников) режим проверки типов в TypedCollection 43 | * Вместо свойств $__notgetters и $__notsetters добавлены защищенные методы notgetters() и notsetters() 44 | 45 | 7.0.2, 7.1.2, 7.2.2 46 | =================== 47 | * Добавлен интерфейс InstanceableInterface и его эталонная реализация InstanceableTrait 48 | * \#3 В ObjectAsArray добавлены свойства $__notgetters и $__notsetters и их использования для игнорирования геттеров и сеттеров 49 | * Метод ObjectAsArrayInterface::isEmpty() переименован в empty() 50 | * Добавлен интерфейс ConfigAwareInterface и его эталонная реализация ConfigAwareTrait 51 | * Для классов, имеющих схему данных, добавлены HasSchemaInterface и HasSchemaTrait 52 | * Добавлен класс ReflectionHelpers 53 | * Исправление логики ObjectAsArrayTrait::needCasting() 54 | 55 | 7.0.1, 7.1.1, 7.2.1 56 | =================== 57 | * Исправление ошибки с нулевыми ключами в Objects-as-Arrays 58 | 59 | 7.0.0, 7.1.0, 7.2.0 60 | =================== 61 | * Первая релизная версия. Перенос кода из проекта Running.FM -------------------------------------------------------------------------------- /src/Core/HasSchemaTrait.php: -------------------------------------------------------------------------------- 1 | $def) { 38 | $value = $this->prepareValueBySchemaDef($key, $def); 39 | $data[$key] = $value; 40 | } 41 | } 42 | return $data; 43 | } 44 | 45 | /** 46 | * @param string $key 47 | * @param mixed $def 48 | * @return mixed 49 | */ 50 | protected function prepareValueBySchemaDef($key, $def) 51 | { 52 | if (!empty($def['class'])) { 53 | 54 | $class = $def['class']; 55 | unset($def['class']); 56 | 57 | // check if $def has only digital keys 58 | if (ctype_digit(implode('', array_keys($def)))) { 59 | return new $class(...array_values($def)); 60 | 61 | // or not - it has string keys? 62 | } else { 63 | $ctor = ReflectionHelpers::getClassMethodArgs($class, '__construct'); 64 | $args = ReflectionHelpers::prepareArgs($ctor, $def); 65 | return new $class(...array_values($args)); 66 | } 67 | } else { 68 | return $def; 69 | } 70 | } 71 | 72 | /** 73 | * @param iterable $schema 74 | * @return $this 75 | */ 76 | public function fromSchema(iterable $schema = null) 77 | { 78 | $data = $this->prepareDataBySchema($schema); 79 | $this->fromArray($data); 80 | return $this; 81 | } 82 | 83 | } 84 | -------------------------------------------------------------------------------- /src/Core/TypedCollectionTrait.php: -------------------------------------------------------------------------------- 1 | isValueTypeValid($value)) { 59 | throw new Exception('Typed collection type mismatch'); 60 | } 61 | } 62 | 63 | public function append($value) 64 | { 65 | $this->checkValueType($value); 66 | return $this->collectionAppend($value); 67 | } 68 | 69 | public function prepend($value) 70 | { 71 | $this->checkValueType($value); 72 | return $this->collectionPrepend($value); 73 | } 74 | 75 | public function innerSet($key, $value) 76 | { 77 | $this->checkValueType($value); 78 | $this->collectionInnerSet($key, $value); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /tests/Core/HasSchemaTraitTest.php: -------------------------------------------------------------------------------- 1 | foo = $foo; 18 | $this->bar = $bar; 19 | } 20 | } 21 | 22 | class testClass implements ObjectAsArrayInterface, HasSchemaInterface 23 | { 24 | protected static $schema = [ 25 | 'x' => ['class' => Std::class, ['foo' => 'bar', 'baz' => 42]], 26 | 'y' => ['class' => testConstructClass::class, 'bar' => 'test2', 'foo' => 'test1'], 27 | 'z' => ['class' => testConstructClass::class, 'foo' => 'test1'], 28 | 42, 29 | ]; 30 | 31 | use ObjectAsArrayTrait; 32 | use HasSchemaTrait; 33 | } 34 | 35 | class HasSchemaTraitTest extends TestCase 36 | { 37 | 38 | public function testInterfaces() 39 | { 40 | $obj = new testClass(); 41 | 42 | $this->assertInstanceOf(ObjectAsArrayInterface::class, $obj); 43 | $this->assertInstanceOf(HasSchemaInterface::class, $obj); 44 | } 45 | 46 | public function testGetSchema() 47 | { 48 | $obj = new testClass(); 49 | $reflector = new \ReflectionProperty(testClass::class, 'schema'); 50 | $reflector->setAccessible(true); 51 | $this->assertSame($reflector->getValue($obj), $obj->getSchema()); 52 | } 53 | 54 | public function testFromSchema() 55 | { 56 | $obj = new testClass(); 57 | $schema = $obj->getSchema(); 58 | 59 | $obj->fromSchema($schema); 60 | 61 | $this->assertInstanceOf(Std::class, $obj['x']); 62 | $this->assertSame('bar', $obj['x']->foo); 63 | $this->assertSame(42, $obj['x']->baz); 64 | 65 | $this->assertInstanceOf(testConstructClass::class, $obj['y']); 66 | $this->assertSame('test1', $obj['y']->foo); 67 | $this->assertSame('test2', $obj['y']->bar); 68 | 69 | $this->assertInstanceOf(testConstructClass::class, $obj['z']); 70 | $this->assertSame('test1', $obj['z']->foo); 71 | $this->assertSame('bar', $obj['z']->bar); 72 | 73 | $this->assertSame(42, $obj[0]); 74 | 75 | } 76 | 77 | } 78 | -------------------------------------------------------------------------------- /tests/Core/StdGetSetTraitTest.php: -------------------------------------------------------------------------------- 1 | foo = 42; 22 | $obj->bar = 'bla-bla'; 23 | $obj->baz = [1, 2, 3]; 24 | 25 | $this->assertInstanceOf(StdGetSetInterface::class, $obj); 26 | 27 | $this->assertCount(3, $obj); 28 | 29 | $this->assertTrue(isset($obj->foo)); 30 | $this->assertTrue(isset($obj['foo'])); 31 | $this->assertTrue(isset($obj->bar)); 32 | $this->assertTrue(isset($obj['bar'])); 33 | $this->assertTrue(isset($obj->baz)); 34 | $this->assertTrue(isset($obj['baz'])); 35 | 36 | $this->assertEquals(42, $obj->foo); 37 | $this->assertEquals(42, $obj['foo']); 38 | $this->assertEquals($obj->foo, $obj['foo']); 39 | $this->assertEquals('bla-bla', $obj->bar); 40 | $this->assertEquals('bla-bla', $obj['bar']); 41 | $this->assertEquals($obj->bar, $obj['bar']); 42 | $this->assertEquals([1, 2, 3], $obj->baz); 43 | $this->assertEquals([1, 2, 3], $obj['baz']); 44 | $this->assertEquals($obj->baz, $obj['baz']); 45 | 46 | unset($obj->baz); 47 | 48 | $this->assertCount(2, $obj); 49 | $this->assertFalse(isset($obj->baz)); 50 | $this->assertFalse(isset($obj['baz'])); 51 | } 52 | 53 | public function testChain() 54 | { 55 | $obj = new testClass(); 56 | $this->assertFalse(isset($obj->foo)); 57 | $this->assertFalse(isset($obj->foo->bar)); 58 | $this->assertTrue(empty($obj->foo)); 59 | $this->assertTrue(empty($obj->foo->bar)); 60 | 61 | $obj->foo->bar = 'baz'; 62 | $this->assertTrue(isset($obj->foo)); 63 | $this->assertTrue(isset($obj->foo->bar)); 64 | $this->assertFalse(empty($obj->foo)); 65 | $this->assertFalse(empty($obj->foo->bar)); 66 | 67 | $this->assertTrue($obj->foo instanceof testClass); 68 | $this->assertEquals((new testClass)->fromArray(['bar' => 'baz']), $obj->foo); 69 | $this->assertEquals('baz', $obj->foo->bar); 70 | 71 | } 72 | 73 | } 74 | -------------------------------------------------------------------------------- /tests/Core/ConfigSaveTest.php: -------------------------------------------------------------------------------- 1 | file = $file; 18 | } 19 | 20 | public function load() { 21 | $this->data = unserialize(file_get_contents($this->file)); 22 | } 23 | 24 | public function save() { 25 | file_put_contents($this->file, serialize($this->data)); 26 | } 27 | 28 | public function get() 29 | { 30 | return $this->data; 31 | } 32 | 33 | public function set($value) 34 | { 35 | $this->data = $value; 36 | } 37 | } 38 | 39 | class ConfigSaveTest extends TestCase 40 | { 41 | 42 | const TMP_PATH = __DIR__ . '/tmp'; 43 | 44 | protected function setUp(): void 45 | { 46 | mkdir(self::TMP_PATH); 47 | file_put_contents(self::TMP_PATH . '/savetest.config', serialize(['application' => ['name' => 'Test Application']])); 48 | } 49 | 50 | public function testEmptyStorageSave() 51 | { 52 | $config = new Config(); 53 | $config->foo = 'bar'; 54 | 55 | $this->expectException(Exception::class); 56 | $this->expectExceptionMessage('Empty config storage'); 57 | $config->save(); 58 | } 59 | 60 | public function testSave() 61 | { 62 | $config = new Config(new SimpleFileStorage(self::TMP_PATH . '/savetest.config')); 63 | $this->assertEquals('Test Application', $config->application->name); 64 | 65 | $config->foo = 'bar'; 66 | $config->baz = [1, 2, 3]; 67 | $config->songs = ['Hey' => 'Jude', 'I just' => ['call' => ['to' => 'say']]]; 68 | 69 | $config->save(); 70 | 71 | $expectedText = serialize(['application' => ['name' => 'Test Application'], 'foo' => 'bar', 'baz' => [1, 2, 3], 'songs' => ['Hey' => 'Jude', 'I just' => ['call' => ['to' => 'say']]]]); 72 | $this->assertEquals( 73 | str_replace("\r\n", "\n", $expectedText), 74 | str_replace("\r\n", "\n", file_get_contents(self::TMP_PATH . '/savetest.config')) 75 | ); 76 | } 77 | 78 | protected function tearDown(): void 79 | { 80 | unlink(self::TMP_PATH . '/savetest.config'); 81 | rmdir(self::TMP_PATH); 82 | } 83 | 84 | } 85 | -------------------------------------------------------------------------------- /src/Core/Config.php: -------------------------------------------------------------------------------- 1 | setStorage($arg)->load(); 49 | } else { 50 | parent::__construct($arg); 51 | } 52 | } 53 | 54 | /** 55 | * @param \Runn\Storages\SingleValueStorageInterface|null $storage 56 | * @return $this 57 | */ 58 | public function setStorage(?SingleValueStorageInterface $storage) 59 | { 60 | $this->storage = $storage; 61 | return $this; 62 | } 63 | 64 | /** 65 | * @return \Runn\Storages\SingleValueStorageInterface|null 66 | */ 67 | public function getStorage(): ?SingleValueStorageInterface 68 | { 69 | return $this->storage; 70 | } 71 | 72 | /** 73 | * Loads config from storage 74 | * 75 | * @return $this 76 | * @throws \Runn\Core\Exception 77 | */ 78 | public function load() 79 | { 80 | if (empty($this->storage)) { 81 | throw new Exception('Empty config storage'); 82 | } 83 | $storage = $this->getStorage(); 84 | $storage->load(); 85 | return $this->fromArray($storage->get()); 86 | } 87 | 88 | /** 89 | * @return $this 90 | * @throws \Runn\Core\Exception 91 | */ 92 | public function save() 93 | { 94 | if (empty($this->storage)) { 95 | throw new Exception('Empty config storage'); 96 | } 97 | $storage = $this->getStorage(); 98 | $storage->set($this->toArray()); 99 | $storage->save(); 100 | return $this; 101 | } 102 | 103 | public function get() 104 | { 105 | throw new \BadMethodCallException(); 106 | } 107 | 108 | public function set($value) 109 | { 110 | throw new \BadMethodCallException(); 111 | } 112 | 113 | } 114 | -------------------------------------------------------------------------------- /src/Reflection/ReflectionHelpers.php: -------------------------------------------------------------------------------- 1 | getParameters(); 33 | if (empty($params)) { 34 | $cache[$class][$method] = []; 35 | return []; 36 | } 37 | $args = []; 38 | foreach ($params as $param) { 39 | $args[$param->name] = [ 40 | 'optional' => $param->isOptional(), 41 | 'variadic' => $param->isVariadic(), 42 | ]; 43 | if ($param->isOptional() && $param->isDefaultValueAvailable()) { 44 | $args[$param->name]['default'] = $param->getDefaultValue(); 45 | } 46 | if ($param->hasType() && $param->getType() instanceof \ReflectionNamedType) { 47 | $args[$param->name]['type'] = $param->getType()->getName(); 48 | } 49 | } 50 | $cache[$class][$method] = $args; 51 | return $args; 52 | } 53 | 54 | /** 55 | * Returns the argument list by object and its method name 56 | * 57 | * @param object $obj 58 | * @param string $method 59 | * @return array 60 | * @throws \Runn\Reflection\Exception 61 | */ 62 | public static function getObjectMethodArgs($obj, string $method): array 63 | { 64 | if (!is_object($obj)) { 65 | throw new Exception('$obj is not an object'); 66 | } 67 | return static::getClassMethodArgs(get_class($obj), $method); 68 | } 69 | 70 | /** 71 | * @param array $args 72 | * @param array|\Runn\Core\ObjectAsArrayInterface $data 73 | * @return array 74 | * @throws \Runn\Core\Exceptions 75 | */ 76 | public static function prepareArgs(array $args, $data): array 77 | { 78 | $ret = []; 79 | $errors = new Exceptions; 80 | 81 | foreach ($args as $name => $def) { 82 | if ((is_array($data) && array_key_exists($name, $data)) || isset($data[$name])) { 83 | $ret[$name] = $data[$name]; 84 | continue; 85 | } 86 | if (array_key_exists('optional', $def) && true === $def['optional'] && array_key_exists('default', $def)) { 87 | $ret[$name] = $def['default']; 88 | continue; 89 | } 90 | $errors[] = new Exception('Argument "' . $name . '" has not set or default value'); 91 | } 92 | if (!$errors->empty()) { 93 | throw $errors; 94 | } 95 | return $ret; 96 | } 97 | 98 | } 99 | -------------------------------------------------------------------------------- /src/Core/StdGetSetValidateSanitizeTrait.php: -------------------------------------------------------------------------------- 1 | notsetters()) && method_exists($this, $setMethod) ) { 38 | $this->$setMethod($val); 39 | } else { 40 | 41 | $validateMethod = 'validate' . ucfirst($key); 42 | 43 | if (method_exists($this, $validateMethod)) { 44 | 45 | $errors = new Exceptions(); 46 | 47 | try { 48 | $validateResult = $this->$validateMethod($val); 49 | if (false === $validateResult) { 50 | return; 51 | } 52 | if ($validateResult instanceof \Generator) { 53 | foreach ($validateResult as $error) { 54 | if ($error instanceof \Throwable) { 55 | $errors->add($error); 56 | } 57 | } 58 | } 59 | } catch (\Throwable $e) { 60 | $errors->add($e); 61 | } 62 | 63 | if (!$errors->empty()) { 64 | throw $errors; 65 | } 66 | 67 | } 68 | 69 | $sanitizeMethod = 'sanitize' . ucfirst($key); 70 | if (method_exists($this, $sanitizeMethod)) { 71 | $val = $this->$sanitizeMethod($val); 72 | } 73 | 74 | if (null === $key) { 75 | $this->__data[] = $val; 76 | } else { 77 | $this->__data[$key] = $val; 78 | } 79 | } 80 | } 81 | 82 | /** 83 | * @param iterable $data 84 | * @return $this 85 | * @throws \Runn\Core\Exceptions 86 | */ 87 | public function merge(iterable $data) 88 | { 89 | if ($data instanceof ArrayCastingInterface) { 90 | $data = $data->toArray(); 91 | } 92 | 93 | $errors = new Exceptions(); 94 | 95 | foreach ($data as $key => $value) { 96 | try { 97 | if ($this->needCasting($key, $value)) { 98 | $value = $this->innerCast($key, $value); 99 | } 100 | $this->innerSet($key, $value); 101 | } catch (\Throwable $e) { 102 | $errors->add($e); 103 | } 104 | } 105 | 106 | if (!$errors->empty()) { 107 | throw $errors; 108 | } 109 | 110 | return $this; 111 | } 112 | 113 | } 114 | -------------------------------------------------------------------------------- /tests/Core/ConfigTest.php: -------------------------------------------------------------------------------- 1 | data = ['foo' => 42, 'bar' => 'bla-bla', 'baz' => [1, 2, 3]]; 22 | } 23 | 24 | public function save() {} 25 | 26 | public function get() 27 | { 28 | return $this->data; 29 | } 30 | 31 | public function set($value) 32 | { 33 | $this->data = $value; 34 | } 35 | } 36 | 37 | class ConfigTest extends TestCase 38 | { 39 | 40 | public function testConstructWData() 41 | { 42 | $obj = new Config(['foo' => 42, 'bar' => 'bla-bla', 'baz' => [1, 2, 3]]); 43 | 44 | $this->assertInstanceOf(ObjectAsArrayInterface::class, $obj); 45 | $this->assertInstanceOf(StdGetSetInterface::class, $obj); 46 | $this->assertInstanceOf(HasInnerValidationInterface::class, $obj); 47 | $this->assertInstanceOf(HasInnerSanitizationInterface::class, $obj); 48 | $this->assertInstanceOf(Std::class, $obj); 49 | $this->assertInstanceOf(SingleValueStorageInterface::class, $obj); 50 | $this->assertInstanceOf(Config::class, $obj); 51 | 52 | $this->assertCount(3, $obj); 53 | $this->assertEquals(42, $obj->foo); 54 | $this->assertEquals('bla-bla', $obj->bar); 55 | $this->assertEquals(new Config([1, 2, 3]), $obj->baz); 56 | } 57 | 58 | public function testConstructWFile() 59 | { 60 | $obj = new Config(new FakeStorage()); 61 | 62 | $this->assertInstanceOf(Config::class, $obj); 63 | $this->assertCount(3, $obj); 64 | $this->assertEquals(42, $obj->foo); 65 | $this->assertEquals('bla-bla', $obj->bar); 66 | $this->assertEquals(new Config([1, 2, 3]), $obj->baz); 67 | } 68 | 69 | public function testSetGetStorage() 70 | { 71 | $obj = new Config(); 72 | 73 | $this->assertNull($obj->getStorage()); 74 | 75 | $file = new FakeStorage(); 76 | $ret = $obj->setStorage($file); 77 | 78 | $this->assertSame($obj, $ret); 79 | $this->assertEquals($file, $obj->getStorage()); 80 | 81 | $ret = $obj->setStorage(null); 82 | 83 | $this->assertSame($obj, $ret); 84 | $this->assertNull($obj->getStorage()); 85 | } 86 | 87 | public function testMagicStorage() 88 | { 89 | $obj = new Config(); 90 | 91 | $this->assertNull($obj->storage); 92 | $this->assertNull($obj->getStorage()); 93 | 94 | $obj->storage = 'test.txt'; 95 | 96 | $this->assertEquals('test.txt', $obj->storage); 97 | $this->assertNull($obj->getStorage()); 98 | 99 | $obj->setStorage(new FakeStorage()); 100 | 101 | $this->assertEquals('test.txt', $obj->storage); 102 | $this->assertEquals(new FakeStorage(), $obj->getStorage()); 103 | } 104 | 105 | public function testLoadEmptyFile() 106 | { 107 | $obj = new Config(); 108 | 109 | $this->expectException(Exception::class); 110 | $this->expectExceptionMessage('Empty config storage'); 111 | $obj->load(); 112 | } 113 | 114 | public function testLoad() 115 | { 116 | $file = new FakeStorage(); 117 | $file->load(); 118 | 119 | $obj = new Config(); 120 | $obj->setStorage(new FakeStorage())->load(); 121 | 122 | $this->assertEquals($file, $obj->getStorage()); 123 | 124 | $this->assertCount(3, $obj); 125 | $this->assertEquals(42, $obj->foo); 126 | $this->assertEquals('bla-bla', $obj->bar); 127 | $this->assertEquals(new Config([1, 2, 3]), $obj->baz); 128 | 129 | $obj->foo = null; 130 | unset($obj->bar); 131 | $obj->baz = 'nothing'; 132 | 133 | $obj->load(); 134 | 135 | $this->assertCount(3, $obj); 136 | $this->assertEquals(42, $obj->foo); 137 | $this->assertEquals('bla-bla', $obj->bar); 138 | $this->assertEquals(new Config([1, 2, 3]), $obj->baz); 139 | } 140 | 141 | public function testSet() 142 | { 143 | $config = new Config(); 144 | 145 | $this->expectException(\BadMethodCallException::class); 146 | $config->set(42); 147 | } 148 | 149 | public function testGet() 150 | { 151 | $config = new Config(new FakeStorage()); 152 | 153 | $this->expectException(\BadMethodCallException::class); 154 | $data = $config->get(); 155 | } 156 | 157 | } 158 | -------------------------------------------------------------------------------- /tests/Di/ContainerTest.php: -------------------------------------------------------------------------------- 1 | expectException(\TypeError::class); 19 | $container->set('id', 'foo'); 20 | } 21 | 22 | public function testSetInvalidArgument2() 23 | { 24 | $container = new Container(); 25 | 26 | $this->expectException(\TypeError::class); 27 | $container->id = 'foo'; 28 | } 29 | 30 | public function testSetInvalidArgument3() 31 | { 32 | $container = new Container(); 33 | 34 | $this->expectException(\TypeError::class); 35 | $container['id'] = 'foo'; 36 | } 37 | 38 | public function testGetInvalidId1() 39 | { 40 | try { 41 | $container = new Container(); 42 | $res = $container->get('test'); 43 | } catch (ContainerEntryNotFoundException $e) { 44 | $this->assertSame('test', $e->getId()); 45 | return; 46 | } 47 | $this->fail(); 48 | } 49 | 50 | public function testGetInvalidId2() 51 | { 52 | try { 53 | $container = new Container(); 54 | $res = $container->test; 55 | } catch (ContainerEntryNotFoundException $e) { 56 | $this->assertSame('test', $e->getId()); 57 | return; 58 | } 59 | $this->fail(); 60 | } 61 | 62 | public function testGetInvalidId3() 63 | { 64 | try { 65 | $container = new Container(); 66 | $res = $container['test']; 67 | } catch (ContainerEntryNotFoundException $e) { 68 | $this->assertSame('test', $e->getId()); 69 | return; 70 | } 71 | $this->fail(); 72 | } 73 | 74 | public function testGetWithException1() 75 | { 76 | $exception = new \Exception; 77 | try { 78 | $container = new Container(); 79 | $container->set('test', function () use ($exception) { 80 | throw $exception; 81 | }); 82 | $res = $container->get('test'); 83 | } catch (ContainerException $e) { 84 | $this->assertSame($exception, $e->getPrevious()); 85 | return; 86 | } 87 | $this->fail(); 88 | } 89 | 90 | public function testGetWithException2() 91 | { 92 | $exception = new \Exception; 93 | try { 94 | $container = new Container(); 95 | $container->set('test', function () use ($exception) { 96 | throw $exception; 97 | }); 98 | $res = $container->test; 99 | } catch (ContainerException $e) { 100 | $this->assertSame($exception, $e->getPrevious()); 101 | return; 102 | } 103 | $this->fail(); 104 | } 105 | 106 | public function testGetWithException3() 107 | { 108 | $exception = new \Exception; 109 | try { 110 | $container = new Container(); 111 | $container->set('test', function () use ($exception) { 112 | throw $exception; 113 | }); 114 | $res = $container['test']; 115 | } catch (ContainerException $e) { 116 | $this->assertSame($exception, $e->getPrevious()); 117 | return; 118 | } 119 | $this->fail(); 120 | } 121 | 122 | public function testSetGet() 123 | { 124 | $container = new Container(); 125 | 126 | $this->assertFalse($container->has('test')); 127 | 128 | $container->set('test', function () {return 42;}); 129 | 130 | $this->assertTrue($container->has('test')); 131 | $this->assertTrue(isset($container->test)); 132 | $this->assertTrue(isset($container['test'])); 133 | 134 | $this->assertSame(42, $container->get('test')); 135 | $this->assertSame(42, $container->test); 136 | $this->assertSame(42, $container['test']); 137 | 138 | $container->test = function () {return 24;}; 139 | 140 | $this->assertSame(24, $container->get('test')); 141 | $this->assertSame(24, $container->test); 142 | $this->assertSame(24, $container['test']); 143 | 144 | $container['test'] = function () {return 12;}; 145 | 146 | $this->assertSame(12, $container->get('test')); 147 | $this->assertSame(12, $container->test); 148 | $this->assertSame(12, $container['test']); 149 | } 150 | 151 | public function testSingleton() 152 | { 153 | $container = new Container(); 154 | 155 | $this->assertFalse($container->has('test')); 156 | 157 | $container->singleton('test', function () {return new Std(['foo' => 'bar']);}); 158 | 159 | $this->assertTrue($container->has('test')); 160 | 161 | $obj1 = $container->get('test'); 162 | $this->assertEquals(new Std(['foo' => 'bar']), $obj1); 163 | 164 | $obj2 = $container->get('test'); 165 | $this->assertEquals(new Std(['foo' => 'bar']), $obj2); 166 | 167 | $this->assertSame($obj1, $obj2); 168 | } 169 | 170 | } 171 | -------------------------------------------------------------------------------- /tests/Di/ContainerResolveTest.php: -------------------------------------------------------------------------------- 1 | foo = $foo; 25 | $this->bar = $bar; 26 | $this->baz = $baz; 27 | } 28 | } 29 | 30 | class testWithUndefinedDependency { 31 | public $foo; 32 | public $bar; 33 | public function __construct(Baz $baz) { 34 | $this->baz = $baz; 35 | } 36 | } 37 | 38 | class Foo111 {} 39 | class Foo11 { 40 | public $foo111; 41 | public function __construct(Foo111 $foo111) 42 | { 43 | $this->foo111 = $foo111; 44 | } 45 | } 46 | class Foo1 { 47 | public $foo11; 48 | public function __construct(Foo11 $foo11) 49 | { 50 | $this->foo11 = $foo11; 51 | } 52 | } 53 | 54 | class ContainerResolveTest extends TestCase 55 | { 56 | 57 | 58 | public function testResolveInvalidId() 59 | { 60 | try { 61 | $container = new Container(); 62 | $res = $container->resolve('test'); 63 | } catch (ContainerEntryNotFoundException $e) { 64 | $this->assertSame('test', $e->getId()); 65 | return; 66 | } 67 | $this->fail(); 68 | } 69 | 70 | 71 | public function testResolveWithException() 72 | { 73 | $exception = new \Exception; 74 | try { 75 | $container = new Container(); 76 | $container->set('test', function () use ($exception) { 77 | throw $exception; 78 | }); 79 | $res = $container->resolve('test'); 80 | } catch (ContainerException $e) { 81 | $this->assertSame($exception, $e->getPrevious()); 82 | return; 83 | } 84 | $this->fail(); 85 | } 86 | 87 | 88 | public function testSetGet() 89 | { 90 | $container = new Container(); 91 | $container->set('test', function () {return 42;}); 92 | $this->assertSame(42, $container->resolve('test')); 93 | } 94 | 95 | public function testSingleton() 96 | { 97 | $container = new Container(); 98 | $container->singleton('test', function () {return new Std(['foo' => 'bar']);}); 99 | 100 | $obj1 = $container->resolve('test'); 101 | $this->assertEquals(new Std(['foo' => 'bar']), $obj1); 102 | 103 | $obj2 = $container->resolve('test'); 104 | $this->assertEquals(new Std(['foo' => 'bar']), $obj2); 105 | 106 | $this->assertSame($obj1, $obj2); 107 | } 108 | 109 | public function testResolveExistingClassWithoutConstructor() 110 | { 111 | $container = new Container(); 112 | 113 | $res1 = $container->resolve(testWithoutConstructor::class); 114 | $this->assertEquals(new testWithoutConstructor(), $res1); 115 | 116 | $res2 = $container->resolve(testWithoutConstructor::class); 117 | $this->assertEquals(new testWithoutConstructor(), $res2); 118 | 119 | $this->assertSame($res1, $res2); 120 | } 121 | 122 | public function testResolveExistingClassWithConstructorWithoutArgs() 123 | { 124 | $container = new Container(); 125 | 126 | $res1 = $container->resolve(testWithConstructorWithoutArgs::class); 127 | $this->assertEquals(new testWithConstructorWithoutArgs(), $res1); 128 | 129 | $res2 = $container->resolve(testWithConstructorWithoutArgs::class); 130 | $this->assertEquals(new testWithConstructorWithoutArgs(), $res2); 131 | 132 | $this->assertSame($res1, $res2); 133 | } 134 | 135 | public function testResolveWithDependecies() 136 | { 137 | $container = new Container(); 138 | 139 | $res1 = $container->resolve(testWithDependencies::class); 140 | $this->assertInstanceOf(testWithDependencies::class, $res1); 141 | $this->assertEquals(new Foo(), $res1->foo); 142 | $this->assertEquals(new Bar(), $res1->bar); 143 | $this->assertNull($res1->baz); 144 | 145 | $res2 = $container->resolve(testWithDependencies::class); 146 | $this->assertSame($res1, $res2); 147 | } 148 | 149 | public function testResolveUndefinedDependecy() 150 | { 151 | try { 152 | $container = new Container(); 153 | $res = $container->resolve(testWithUndefinedDependency::class); 154 | } catch (ContainerEntryNotFoundException $e) { 155 | $this->assertSame(__NAMESPACE__ . '\\Baz', $e->getId()); 156 | return; 157 | } 158 | $this->fail(); 159 | } 160 | 161 | public function testResolveNestedDependecies() 162 | { 163 | $container = new Container(); 164 | 165 | $res1 = $container->resolve(Foo1::class); 166 | $this->assertInstanceOf(Foo1::class, $res1); 167 | $this->assertEquals(new Foo11(new Foo111()), $res1->foo11); 168 | $this->assertEquals(new Foo111, $res1->foo11->foo111); 169 | 170 | $res2 = $container->resolve(Foo1::class); 171 | $this->assertSame($res1, $res2); 172 | } 173 | 174 | } 175 | -------------------------------------------------------------------------------- /src/Di/ContainerTrait.php: -------------------------------------------------------------------------------- 1 | innerSet($id, $resolver); 35 | unset($this->resolved[$id]); 36 | unset($this->singletons[$id]); 37 | return $this; 38 | } 39 | 40 | /** 41 | * Sets a resolver for entry of the container by its identifier as singleton. 42 | * 43 | * @param string $id Identifier of the entry to look for. 44 | * @param callable $resolver Function that resolves the entry and returns it. 45 | * 46 | * @return $this. 47 | */ 48 | public function singleton(string $id, callable $resolver) 49 | { 50 | $this->innerSet($id, $resolver); 51 | unset($this->resolved[$id]); 52 | $this->singletons[$id] = true; 53 | return $this; 54 | } 55 | 56 | /** 57 | * Finds an entry of the container by its identifier and returns it. 58 | * 59 | * @param string $id Identifier of the entry to look for. 60 | * 61 | * @throws NotFoundExceptionInterface No entry was found for **this** identifier. 62 | * @throws ContainerExceptionInterface Error while retrieving the entry. 63 | * 64 | * @return mixed Entry. 65 | */ 66 | public function get(string $id) 67 | { 68 | if (!$this->has($id)) { 69 | throw new ContainerEntryNotFoundException($id); 70 | } 71 | try { 72 | if (isset($this->singletons[$id])) { 73 | if (!isset($this->resolved[$id])) { 74 | $this->resolved[$id] = $this->innerGet($id)(); 75 | } 76 | return $this->resolved[$id]; 77 | } 78 | return $this->innerGet($id)(); 79 | } catch (\Throwable $e) { 80 | throw new ContainerException($e); 81 | } 82 | } 83 | 84 | /** 85 | * Returns true if the container can return an entry for the given identifier. 86 | * Returns false otherwise. 87 | * 88 | * `has($id)` returning true does not mean that `get($id)` will not throw an exception. 89 | * It does however mean that `get($id)` will not throw a `NotFoundExceptionInterface`. 90 | * 91 | * @param string $id Identifier of the entry to look for. 92 | * 93 | * @return bool 94 | */ 95 | public function has(string $id): bool 96 | { 97 | return isset($this[$id]); 98 | } 99 | 100 | /** 101 | * Resolves and returns an entry with all ones dependencies 102 | * 103 | * @param string $id 104 | * @return mixed 105 | * @throws ContainerEntryNotFoundException 106 | * @throws ContainerException 107 | */ 108 | public function resolve(string $id) 109 | { 110 | if ($this->has($id)) { 111 | return $this->get($id); 112 | } 113 | // Check if $id is available class name 114 | if (class_exists($id, true)) { 115 | 116 | // Check if class has not constructor 117 | if (!method_exists($id, '__construct')) { 118 | $resolved = new $id; 119 | $this->set($id, static function () use ($resolved) { return $resolved; } ); 120 | return $this->get($id); 121 | } 122 | 123 | // Check if constructor has not arguments 124 | $constructArgs = ReflectionHelpers::getClassMethodArgs($id, '__construct'); 125 | if (empty($constructArgs)) { 126 | $resolved = new $id; 127 | $this->set($id, static function () use ($resolved) { return $resolved; } ); 128 | return $this->get($id); 129 | } 130 | 131 | $args = []; 132 | foreach ($constructArgs as $neededArg) { 133 | try { 134 | $args[] = $this->resolve($neededArg['type']); 135 | } catch (ContainerEntryNotFoundException $e) { 136 | if ($neededArg['optional']) { 137 | $args[] = $neededArg['default']; 138 | } else { 139 | throw $e; 140 | } 141 | } 142 | } 143 | 144 | $resolved = new $id(...$args); 145 | $this->set($id, static function () use ($resolved) { return $resolved; } ); 146 | return $this->get($id); 147 | 148 | } 149 | throw new ContainerEntryNotFoundException($id); 150 | } 151 | 152 | /** 153 | * @param $key 154 | * @param $val 155 | */ 156 | public function __set($key, $val) 157 | { 158 | $this->set($key, $val); 159 | } 160 | 161 | /** 162 | * @param $key 163 | * @return mixed 164 | * @throws ContainerEntryNotFoundException 165 | * @throws ContainerException 166 | */ 167 | public function __get($key) 168 | { 169 | return $this->get($key); 170 | } 171 | 172 | /** 173 | * @param mixed $offset 174 | * @param mixed $value 175 | */ 176 | public function offsetSet($offset, $value) 177 | { 178 | $this->set($offset, $value); 179 | } 180 | 181 | /** 182 | * @param mixed $offset 183 | * @return mixed 184 | * @throws ContainerEntryNotFoundException 185 | * @throws ContainerException 186 | */ 187 | public function offsetGet($offset) 188 | { 189 | return $this->get($offset); 190 | } 191 | 192 | } 193 | -------------------------------------------------------------------------------- /tests/Core/StdTest.php: -------------------------------------------------------------------------------- 1 | 0; 19 | } 20 | protected function sanitizeBar($val) { 21 | return trim($val); 22 | } 23 | protected function setBaz($val) { 24 | $this->__data['baz'] = $val*2; 25 | } 26 | } 27 | 28 | class testClassWExceptions extends Std 29 | { 30 | protected function validateFoo($val) { 31 | if ($val < 0) { 32 | throw new Exception('Minus'); 33 | } 34 | return true; 35 | } 36 | protected function validateBar($val) { 37 | if (strlen($val) < 3) { 38 | yield new Exception('Small'); 39 | } 40 | if (false !== strpos($val, '0')) { 41 | yield new Exception('Zero'); 42 | } 43 | return true; 44 | } 45 | protected function validateBaz($val) { 46 | if ($val > 100) { 47 | throw new Exception('Large'); 48 | } 49 | return true; 50 | } 51 | } 52 | 53 | class testClassWithRequired extends Std 54 | { 55 | protected static $required = ['foo', 'bar']; 56 | protected function validateBaz() { 57 | throw new Exception('Invalid baz'); 58 | } 59 | } 60 | 61 | class StdTest extends TestCase 62 | { 63 | 64 | public function testEmptyConstruct() 65 | { 66 | $obj = new Std; 67 | 68 | $this->assertInstanceOf(ObjectAsArrayInterface::class, $obj); 69 | $this->assertInstanceOf(StdGetSetInterface::class, $obj); 70 | $this->assertInstanceOf(HasInnerValidationInterface::class, $obj); 71 | $this->assertInstanceOf(HasInnerSanitizationInterface::class, $obj); 72 | $this->assertInstanceOf(HasRequiredInterface::class, $obj); 73 | 74 | $this->assertCount(0, $obj); 75 | } 76 | 77 | public function testValidConstruct() 78 | { 79 | $obj = new Std(['foo' => 42, 'bar' => 'bla-bla', 'baz' => [1, 2, 3]]); 80 | 81 | $this->assertInstanceOf(ObjectAsArrayInterface::class, $obj); 82 | $this->assertInstanceOf(StdGetSetInterface::class, $obj); 83 | $this->assertInstanceOf(HasInnerValidationInterface::class, $obj); 84 | $this->assertInstanceOf(HasInnerSanitizationInterface::class, $obj); 85 | $this->assertInstanceOf(HasRequiredInterface::class, $obj); 86 | 87 | $this->assertCount(3, $obj); 88 | 89 | $this->assertEquals(42, $obj->foo); 90 | $this->assertEquals('bla-bla', $obj->bar); 91 | $this->assertEquals(new Std([1, 2, 3]), $obj->baz); 92 | } 93 | 94 | public function testGetRequiredKeys() 95 | { 96 | $obj = new testClassWithRequired(['foo' => 1, 'bar' => 2]); 97 | $obj->requiredKeys = 42; 98 | 99 | $this->assertSame(['foo', 'bar'], $obj->getRequiredKeys()); 100 | $this->assertSame(42, $obj->requiredKeys); 101 | } 102 | 103 | public function testMerge() 104 | { 105 | $obj1 = new Std(['foo' => 1]); 106 | $obj1->merge(['bar' => 2]); 107 | $this->assertEquals(1, $obj1->foo); 108 | $this->assertEquals(2, $obj1->bar); 109 | $this->assertEquals(new Std(['foo' => 1, 'bar' => 2]), $obj1); 110 | 111 | $obj2 = new Std(['foo' => 11]); 112 | $obj2->merge(new Std(['bar' => 21])); 113 | $this->assertEquals(11, $obj2->foo); 114 | $this->assertEquals(21, $obj2->bar); 115 | $this->assertEquals(new Std(['foo' => 11, 'bar' => 21]), $obj2); 116 | 117 | $obj2 = new Std(['foo' => 11, 'bar' => 12]); 118 | $obj2->merge(new Std(['bar' => 21])); 119 | $this->assertEquals(11, $obj2->foo); 120 | $this->assertEquals(21, $obj2->bar); 121 | $this->assertEquals(new Std(['foo' => 11, 'bar' => 21]), $obj2); 122 | } 123 | 124 | public function testSetter() 125 | { 126 | $obj = new testClass(); 127 | $obj->baz = 42; 128 | 129 | $this->assertTrue(isset($obj->baz)); 130 | $this->assertEquals(84, $obj->baz); 131 | } 132 | 133 | public function testValidate() 134 | { 135 | $obj = new testClass(); 136 | $obj->foo = 42; 137 | 138 | $this->assertTrue(isset($obj->foo)); 139 | $this->assertEquals(42, $obj->foo); 140 | 141 | $obj = new testClass(); 142 | $obj->foo = -42; 143 | 144 | $this->assertFalse(isset($obj->foo)); 145 | } 146 | 147 | public function testSanitize() 148 | { 149 | $obj = new testClass(); 150 | $obj->bar = ' test '; 151 | 152 | $this->assertTrue(isset($obj->bar)); 153 | $this->assertEquals('test', $obj->bar); 154 | } 155 | 156 | public function testRequiredMissing() 157 | { 158 | try { 159 | $obj = new testClassWithRequired(); 160 | $this->fail(); 161 | } catch (Exceptions $errors) { 162 | $this->assertCount(2, $errors); 163 | $this->assertInstanceOf(Exception::class, $errors[0]); 164 | $this->assertInstanceOf(Exception::class, $errors[1]); 165 | $this->assertEquals('Required property "foo" is missing', $errors[0]->getMessage()); 166 | $this->assertEquals('Required property "bar" is missing', $errors[1]->getMessage()); 167 | return; 168 | } 169 | } 170 | 171 | public function testRequiredMissingAndInvalid() 172 | { 173 | try { 174 | $obj = new testClassWithRequired(['baz' => 1]); 175 | $this->fail(); 176 | } catch (Exceptions $errors) { 177 | $this->assertCount(3, $errors); 178 | $this->assertInstanceOf(Exception::class, $errors[0]); 179 | $this->assertInstanceOf(Exception::class, $errors[1]); 180 | $this->assertInstanceOf(Exception::class, $errors[2]); 181 | $this->assertEquals('Invalid baz', $errors[0]->getMessage()); 182 | $this->assertEquals('Required property "foo" is missing', $errors[1]->getMessage()); 183 | $this->assertEquals('Required property "bar" is missing', $errors[2]->getMessage()); 184 | return; 185 | } 186 | } 187 | 188 | public function testRequiredValid() 189 | { 190 | try { 191 | $obj = new testClassWithRequired(['foo' => 1, 'bar' => 2]); 192 | $this->assertCount(2, $obj); 193 | } catch (Exceptions $errors) { 194 | $this->fail(); 195 | } 196 | } 197 | 198 | } 199 | -------------------------------------------------------------------------------- /tests/Core/ExceptionsTest.php: -------------------------------------------------------------------------------- 1 | assertInstanceOf( 21 | Exceptions::class, 22 | $errors 23 | ); 24 | $this->assertInstanceOf( 25 | CollectionInterface::class, 26 | $errors 27 | ); 28 | $this->assertTrue($errors->empty()); 29 | } 30 | 31 | public function testConstructValid() 32 | { 33 | $errors = new Exceptions([new Exception('First'), new Exception('Second')]); 34 | 35 | $this->assertEquals(2, $errors->count()); 36 | $this->assertEquals( 37 | [new Exception('First'), new Exception('Second')], 38 | $errors->toArray() 39 | ); 40 | } 41 | 42 | public function testConstructInvalid() 43 | { 44 | $this->expectException(Exception::class); 45 | $this->expectExceptionMessage('Typed collection type mismatch'); 46 | $errors = new Exceptions([new \stdClass('First'), new \stdClass('Second')]); 47 | } 48 | 49 | public function testAppend() 50 | { 51 | $errors = new Exceptions; 52 | $this->assertTrue($errors->empty()); 53 | 54 | $errors->append(new Exception('First')); 55 | $this->assertFalse($errors->empty()); 56 | $this->assertEquals(1, $errors->count()); 57 | 58 | $errors->append(new Exception('Second')); 59 | $this->assertFalse($errors->empty()); 60 | $this->assertEquals(2, $errors->count()); 61 | 62 | $this->assertEquals( 63 | [new Exception('First'), new Exception('Second')], 64 | $errors->toArray() 65 | ); 66 | } 67 | 68 | public function testPrepend() 69 | { 70 | $errors = new Exceptions; 71 | $this->assertTrue($errors->empty()); 72 | 73 | $errors->prepend(new Exception('First')); 74 | $this->assertFalse($errors->empty()); 75 | $this->assertEquals(1, $errors->count()); 76 | 77 | $errors->prepend(new Exception('Second')); 78 | $this->assertFalse($errors->empty()); 79 | $this->assertEquals(2, $errors->count()); 80 | 81 | $this->assertEquals( 82 | [new Exception('Second'), new Exception('First')], 83 | $errors->toArray() 84 | ); 85 | } 86 | 87 | public function testAdd() 88 | { 89 | $errors = new Exceptions; 90 | $this->assertTrue($errors->empty()); 91 | 92 | $errors->add(new Exception('First')); 93 | $this->assertFalse($errors->empty()); 94 | $this->assertEquals(1, $errors->count()); 95 | 96 | $errors->add(new Exception('Second')); 97 | $this->assertFalse($errors->empty()); 98 | $this->assertEquals(2, $errors->count()); 99 | 100 | $this->assertInstanceOf( 101 | \Runn\Core\Exception::class, 102 | $errors[0] 103 | ); 104 | $this->assertInstanceOf( 105 | \Runn\Core\Exception::class, 106 | $errors[1] 107 | ); 108 | $this->assertEquals(new Exception('First'), $errors[0]); 109 | $this->assertEquals(new Exception('Second'), $errors[1]); 110 | } 111 | 112 | public function testAddSelf() 113 | { 114 | $errors = new Exceptions; 115 | $this->assertTrue($errors->empty()); 116 | 117 | $errors->add(new Exception('First')); 118 | $this->assertFalse($errors->empty()); 119 | $this->assertEquals(1, $errors->count()); 120 | 121 | $merged = new Exceptions; 122 | $merged[] = new Exception('Second'); 123 | $merged[] = new Exception('Third'); 124 | $this->assertEquals(2, $merged->count()); 125 | 126 | $errors->add($merged); 127 | $this->assertEquals(3, $errors->count()); 128 | $this->assertEquals(new Exception('First'), $errors[0]); 129 | $this->assertEquals(new Exception('Second'), $errors[1]); 130 | $this->assertEquals(new Exception('Third'), $errors[2]); 131 | } 132 | 133 | public function testInvalidClassPrepend() 134 | { 135 | $errors = new class extends Exceptions 136 | { 137 | public static function getType() 138 | { 139 | return SomeException::class; 140 | } 141 | }; 142 | 143 | $this->expectException(Exception::class); 144 | $this->expectExceptionMessage('Typed collection type mismatch'); 145 | $errors->prepend(new Exception); 146 | } 147 | 148 | public function testInvalidClassAppend() 149 | { 150 | $errors = new class extends Exceptions 151 | { 152 | public static function getType() 153 | { 154 | return SomeException::class; 155 | } 156 | }; 157 | 158 | $this->expectException(Exception::class); 159 | $this->expectExceptionMessage('Typed collection type mismatch'); 160 | $errors->append(new Exception); 161 | } 162 | 163 | public function testInvalidInnserSet() 164 | { 165 | $errors = new class extends Exceptions 166 | { 167 | public static function getType() 168 | { 169 | return SomeException::class; 170 | } 171 | }; 172 | 173 | $this->expectException(Exception::class); 174 | $this->expectExceptionMessage('Typed collection type mismatch'); 175 | $errors[] = new Exception; 176 | } 177 | 178 | public function testThrow() 179 | { 180 | try { 181 | 182 | $errors = new Exceptions(); 183 | $errors->add(new Exception('Foo')); 184 | $errors->add(new Exception('Bar')); 185 | $errors->add(new Exception('Baz')); 186 | 187 | if (!$errors->empty()) { 188 | throw $errors; 189 | } 190 | 191 | $this->fail(); 192 | 193 | } catch (Exceptions $ex) { 194 | 195 | $this->assertEquals(3, $ex->count()); 196 | 197 | $this->assertInstanceOf( 198 | \Runn\Core\Exception::class, 199 | $ex[0] 200 | ); 201 | $this->assertInstanceOf( 202 | \Runn\Core\Exception::class, 203 | $ex[1] 204 | ); 205 | $this->assertInstanceOf( 206 | \Runn\Core\Exception::class, 207 | $ex[2] 208 | ); 209 | 210 | $this->assertEquals('Foo', $ex[0]->getMessage()); 211 | $this->assertEquals('Bar', $ex[1]->getMessage()); 212 | $this->assertEquals('Baz', $ex[2]->getMessage()); 213 | 214 | } 215 | } 216 | 217 | } 218 | -------------------------------------------------------------------------------- /tests/Reflection/ReflectionHelpersTest.php: -------------------------------------------------------------------------------- 1 | assertTrue(is_array($args)); 22 | $this->assertCount(0, $args); 23 | 24 | $object = new class { 25 | public function foo($arg1) {} 26 | }; 27 | 28 | $args = ReflectionHelpers::getClassMethodArgs(get_class($object), 'foo'); 29 | $this->assertTrue(is_array($args)); 30 | $this->assertCount(1, $args); 31 | $this->assertFalse($args['arg1']['optional']); 32 | $this->assertFalse($args['arg1']['variadic']); 33 | $this->assertFalse(isset($args['arg1']['default'])); 34 | 35 | $object = new class { 36 | public function foo($arg1 = 'test') {} 37 | }; 38 | 39 | $args = ReflectionHelpers::getClassMethodArgs(get_class($object), 'foo'); 40 | $this->assertTrue(is_array($args)); 41 | $this->assertCount(1, $args); 42 | $this->assertTrue($args['arg1']['optional']); 43 | $this->assertFalse($args['arg1']['variadic']); 44 | $this->assertTrue(isset($args['arg1']['default'])); 45 | $this->assertSame('test', $args['arg1']['default']); 46 | 47 | $object = new class { 48 | public function foo(string $arg1, int $arg2, array $arg3, $arg4) {} 49 | }; 50 | 51 | $args = ReflectionHelpers::getClassMethodArgs(get_class($object), 'foo'); 52 | $this->assertIsArray($args); 53 | $this->assertCount(4, $args); 54 | $this->assertSame('string', $args['arg1']['type']); 55 | $this->assertSame('int', $args['arg2']['type']); 56 | $this->assertSame('array', $args['arg3']['type']); 57 | $this->assertFalse(isset($args['arg4']['type'])); 58 | 59 | $object = new class { 60 | public function foo(...$args) {} 61 | }; 62 | 63 | $args = ReflectionHelpers::getClassMethodArgs(get_class($object), 'foo'); 64 | $this->assertTrue(is_array($args)); 65 | $this->assertCount(1, $args); 66 | $this->assertTrue($args['args']['optional']); 67 | $this->assertTrue($args['args']['variadic']); 68 | $this->assertFalse(isset($args['args']['default'])); 69 | $this->assertFalse(isset($args['args']['type'])); 70 | 71 | $reflector = new \ReflectionMethod(ReflectionHelpers::class, 'getClassMethodArgs'); 72 | $staticVariables = $reflector->getStaticVariables(); 73 | $this->assertIsArray($staticVariables); 74 | $this->assertCount(1, $staticVariables); 75 | $this->assertNotNull($staticVariables['cache']); 76 | 77 | $object2 = new ReflectionHelpers(); 78 | ReflectionHelpers::getClassMethodArgs(get_class($object2), 'getClassMethodArgs'); 79 | $reflector = new \ReflectionMethod(ReflectionHelpers::class, 'getClassMethodArgs'); 80 | $staticVariables = $reflector->getStaticVariables(); 81 | $this->assertIsArray($staticVariables['cache']); 82 | $this->assertNotNull($staticVariables['cache'][get_class($object)]['foo']); 83 | $this->assertNotNull($staticVariables['cache'][get_class($object2)]['getClassMethodArgs']); 84 | } 85 | 86 | public function testGetObjectMethodInvalidObject() 87 | { 88 | $this->expectException(Exception::class); 89 | $args = ReflectionHelpers::getObjectMethodArgs('', 'foo'); 90 | } 91 | 92 | public function testGetObjectMethodArgs() 93 | { 94 | $object = new class { 95 | public function foo() {} 96 | }; 97 | 98 | $args = ReflectionHelpers::getObjectMethodArgs($object, 'foo'); 99 | $this->assertTrue(is_array($args)); 100 | $this->assertCount(0, $args); 101 | 102 | $object = new class { 103 | public function foo($arg1) {} 104 | }; 105 | 106 | $args = ReflectionHelpers::getObjectMethodArgs($object, 'foo'); 107 | $this->assertTrue(is_array($args)); 108 | $this->assertCount(1, $args); 109 | $this->assertFalse($args['arg1']['optional']); 110 | $this->assertFalse($args['arg1']['variadic']); 111 | $this->assertFalse(isset($args['arg1']['default'])); 112 | 113 | $object = new class { 114 | public function foo($arg1 = 'test') {} 115 | }; 116 | 117 | $args = ReflectionHelpers::getObjectMethodArgs($object, 'foo'); 118 | $this->assertTrue(is_array($args)); 119 | $this->assertCount(1, $args); 120 | $this->assertTrue($args['arg1']['optional']); 121 | $this->assertFalse($args['arg1']['variadic']); 122 | $this->assertTrue(isset($args['arg1']['default'])); 123 | $this->assertSame('test', $args['arg1']['default']); 124 | 125 | $object = new class { 126 | public function foo(...$args) {} 127 | }; 128 | 129 | $args = ReflectionHelpers::getObjectMethodArgs($object, 'foo'); 130 | $this->assertTrue(is_array($args)); 131 | $this->assertCount(1, $args); 132 | $this->assertTrue($args['args']['optional']); 133 | $this->assertTrue($args['args']['variadic']); 134 | $this->assertFalse(isset($args['args']['default'])); 135 | } 136 | 137 | public function testPrepareArgs() 138 | { 139 | $args = []; 140 | $data = [1, 2, 3]; 141 | $this->assertSame([], ReflectionHelpers::prepareArgs($args, $data)); 142 | 143 | $args = ['foo' => [], 'bar' => []]; 144 | $data = ['foo' => 42, 'bar' => null, 'baz' => 'some']; 145 | $this->assertSame(['foo' => 42, 'bar' => null], ReflectionHelpers::prepareArgs($args, $data)); 146 | 147 | $args = ['foo' => [], 'bar' => []]; 148 | $data = new Std(['foo' => 42, 'bar' => null, 'baz' => 'some']); 149 | $this->assertSame(['foo' => 42, 'bar' => null], ReflectionHelpers::prepareArgs($args, $data)); 150 | 151 | $args = ['foo' => [], 'bar' => ['optional' => true, 'default' => 'test']]; 152 | $data = ['foo' => 42, 'baz' => 'some']; 153 | $this->assertSame(['foo' => 42, 'bar' => 'test'], ReflectionHelpers::prepareArgs($args, $data)); 154 | 155 | try { 156 | $args = ['foo' => [], 'bar' => []]; 157 | $data = ['foo' => 42, 'baz' => 'some']; 158 | ReflectionHelpers::prepareArgs($args, $data); 159 | } catch (Exceptions $errors) { 160 | $this->assertCount(1, $errors); 161 | $this->assertInstanceOf(Exception::class, $errors[0]); 162 | $this->assertSame('Argument "bar" has not set or default value', $errors[0]->getMessage()); 163 | return; 164 | } 165 | $this->fail(); 166 | } 167 | 168 | } 169 | -------------------------------------------------------------------------------- /tests/Core/TypedCollectionTraitTest.php: -------------------------------------------------------------------------------- 1 | isValueTypeValid($value, true)) { 29 | throw new Exception('Typed collection type mismatch'); 30 | } 31 | } 32 | } 33 | 34 | class testFloatClass implements TypedCollectionInterface 35 | { 36 | use TypedCollectionTrait; 37 | public static function getType() 38 | { 39 | return 'float'; 40 | } 41 | } 42 | 43 | class testIntClass implements TypedCollectionInterface 44 | { 45 | use TypedCollectionTrait; 46 | public static function getType() 47 | { 48 | return 'int'; 49 | } 50 | } 51 | 52 | class testBoolClass implements TypedCollectionInterface 53 | { 54 | use TypedCollectionTrait; 55 | public static function getType() 56 | { 57 | return 'bool'; 58 | } 59 | } 60 | 61 | class testIncorrectTypeClass implements TypedCollectionInterface 62 | { 63 | use TypedCollectionTrait; 64 | public static function getType() 65 | { 66 | return 'foo'; 67 | } 68 | } 69 | 70 | class testValueClass 71 | { 72 | protected $data; 73 | public function __construct($x) 74 | { 75 | $this->data = $x; 76 | } 77 | public function getValue() 78 | { 79 | return $this->data; 80 | } 81 | } 82 | 83 | 84 | class TypedCollectionTraitTest extends TestCase 85 | { 86 | 87 | public function testInvalidFromArray() 88 | { 89 | $this->expectException(Exception::class); 90 | $this->expectExceptionMessage('Typed collection type mismatch'); 91 | (new testClass)->fromArray([1, 2, 3]); 92 | } 93 | 94 | public function testInvalidAppend() 95 | { 96 | $collection = new testClass(); 97 | 98 | $this->expectException(Exception::class); 99 | $this->expectExceptionMessage('Typed collection type mismatch'); 100 | $collection->append(42); 101 | } 102 | 103 | public function testInvalidPrepend() 104 | { 105 | $collection = new testClass(); 106 | 107 | $this->expectException(Exception::class); 108 | $this->expectExceptionMessage('Typed collection type mismatch'); 109 | $collection->prepend(new class {}); 110 | } 111 | 112 | public function testInvalidInnerSet() 113 | { 114 | $collection = new testClass(); 115 | 116 | $this->expectException(Exception::class); 117 | $this->expectExceptionMessage('Typed collection type mismatch'); 118 | $collection[] = new class {}; 119 | } 120 | 121 | public function testValid() 122 | { 123 | $this->assertSame(testValueClass::class, testClass::getType()); 124 | 125 | $collection = (new testClass)->fromArray([new testValueClass(1), new testValueClass(2)]); 126 | 127 | $this->assertInstanceOf(TypedCollectionInterface::class, $collection); 128 | $this->assertCount(2, $collection); 129 | 130 | $collection->append(new testValueClass(3)); 131 | $this->assertCount(3, $collection); 132 | 133 | $collection->prepend(new testValueClass(4)); 134 | $this->assertCount(4, $collection); 135 | 136 | $collection[] = new testValueClass(5); 137 | $this->assertCount(5, $collection); 138 | } 139 | 140 | public function testValidStrictType() 141 | { 142 | $this->assertSame(testValueClass::class, testStrictClass::getType()); 143 | $collection = (new testStrictClass)->fromArray([new testValueClass(1), new testValueClass(2)]); 144 | 145 | $this->assertInstanceOf(TypedCollectionInterface::class, $collection); 146 | $this->assertCount(2, $collection); 147 | } 148 | 149 | public function testInvalidStrictType() 150 | { 151 | $this->assertSame(testValueClass::class, testStrictClass::getType()); 152 | 153 | $this->expectException(Exception::class); 154 | $this->expectExceptionMessage('Typed collection type mismatch'); 155 | (new testStrictClass)->fromArray([new class (1) extends testValueClass {}]); 156 | } 157 | 158 | public function testInvalidIntegerClass() 159 | { 160 | $collection = new testClass(); 161 | 162 | $this->expectException(Exception::class); 163 | $this->expectExceptionMessage('Typed collection type mismatch'); 164 | $collection->append('42'); 165 | } 166 | 167 | public function testValidIntegerClass() 168 | { 169 | $collection = (new testIntClass)->fromArray([1, 2]); 170 | 171 | $this->assertInstanceOf(TypedCollectionInterface::class, $collection); 172 | $this->assertCount(2, $collection); 173 | 174 | $collection->append(3); 175 | $this->assertCount(3, $collection); 176 | 177 | $collection->prepend(4); 178 | $this->assertCount(4, $collection); 179 | 180 | $collection[] = 5; 181 | $this->assertCount(5, $collection); 182 | } 183 | 184 | public function testInvalidFloatClass() 185 | { 186 | $collection = new testClass(); 187 | 188 | $this->expectException(Exception::class); 189 | $this->expectExceptionMessage('Typed collection type mismatch'); 190 | $collection->append(true); 191 | } 192 | 193 | public function testValidFloatClass() 194 | { 195 | $collection = (new testFloatClass)->fromArray([1.0, 2.1]); 196 | 197 | $this->assertInstanceOf(TypedCollectionInterface::class, $collection); 198 | $this->assertCount(2, $collection); 199 | 200 | $collection->append(3.2); 201 | $this->assertCount(3, $collection); 202 | 203 | $collection->prepend(4.3); 204 | $this->assertCount(4, $collection); 205 | 206 | $collection[] = 5.4; 207 | $this->assertCount(5, $collection); 208 | } 209 | 210 | public function testInvalidBooleanClass() 211 | { 212 | $collection = new testClass(); 213 | 214 | $this->expectException(Exception::class); 215 | $this->expectExceptionMessage('Typed collection type mismatch'); 216 | $collection->append('42'); 217 | } 218 | 219 | public function testValidBooleanClass() 220 | { 221 | $collection = (new testBoolClass)->fromArray([true, true]); 222 | 223 | $this->assertInstanceOf(TypedCollectionInterface::class, $collection); 224 | $this->assertCount(2, $collection); 225 | 226 | $collection->append(false); 227 | $this->assertCount(3, $collection); 228 | 229 | $collection->prepend(true); 230 | $this->assertCount(4, $collection); 231 | 232 | $collection[] = false; 233 | $this->assertCount(5, $collection); 234 | } 235 | 236 | public function testIncorrectTypeClass() 237 | { 238 | $collection = new testIncorrectTypeClass(); 239 | 240 | $this->expectException(Exception::class); 241 | $this->expectExceptionMessage('Typed collection type mismatch'); 242 | $collection->append(42); 243 | } 244 | 245 | } 246 | -------------------------------------------------------------------------------- /tests/Core/StdGetSetValidateSanitizeTraitTest.php: -------------------------------------------------------------------------------- 1 | __data['bla1'] = '!!!'; 39 | } 40 | public function setBla2($val) { 41 | $this->__data['bla2'] = '!!!'; 42 | } 43 | } 44 | 45 | class StdGetSetValidateSanitizeTraitTest extends TestCase 46 | { 47 | 48 | public function testNullKey() 49 | { 50 | $obj = new testClass(); 51 | 52 | $obj[] = '1'; 53 | $this->assertCount(1, $obj); 54 | $this->assertSame('1', $obj[0]); 55 | 56 | $obj[2] = '2'; 57 | $this->assertCount(2, $obj); 58 | $this->assertSame('1', $obj[0]); 59 | $this->assertSame('2', $obj[2]); 60 | } 61 | 62 | public function testNotSetters() 63 | { 64 | $obj = new testClass(); 65 | 66 | $obj->bla1 = 'test1'; 67 | $this->assertSame('test1', $obj->bla1); 68 | 69 | $obj->bla2 = 'test2'; 70 | $this->assertSame('!!!', $obj->bla2); 71 | } 72 | 73 | public function testSimpleExceptionFromArray() 74 | { 75 | try { 76 | $obj = new testClass(); 77 | $obj->fromArray(['foo' => 1]); 78 | $this->fail(); 79 | } catch (Exceptions $errors) { 80 | 81 | $this->assertCount(0, $obj); 82 | $this->assertCount(1, $errors); 83 | $this->assertSame('Invalid foo', $errors[0]->getMessage()); 84 | 85 | return; 86 | } 87 | $this->fail(); 88 | } 89 | 90 | public function testSimpleExceptionMerge() 91 | { 92 | try { 93 | $obj = (new testClass())->fromArray(['sample' => 'test']); 94 | $obj->merge(['foo' => 1]); 95 | $this->fail(); 96 | } catch (Exceptions $errors) { 97 | 98 | $this->assertCount(1, $obj); 99 | $this->assertSame('test', $obj->sample); 100 | 101 | $this->assertCount(1, $errors); 102 | $this->assertSame('Invalid foo', $errors[0]->getMessage()); 103 | 104 | return; 105 | } 106 | $this->fail(); 107 | } 108 | 109 | public function testMultiExceptionFromArray() 110 | { 111 | try { 112 | $obj = new testClass(); 113 | $obj->fromArray(['bar' => 1]); 114 | $this->fail(); 115 | } catch (Exceptions $errors) { 116 | 117 | $this->assertCount(0, $obj); 118 | $this->assertCount(2, $errors); 119 | $this->assertSame('Invalid bar 1', $errors[0]->getMessage()); 120 | $this->assertSame('Invalid bar 2', $errors[1]->getMessage()); 121 | 122 | return; 123 | } 124 | $this->fail(); 125 | } 126 | 127 | public function testMultiExceptionMerge() 128 | { 129 | try { 130 | $obj = (new testClass())->fromArray(['sample' => 'test']); 131 | $obj->merge(['bar' => 1]); 132 | $this->fail(); 133 | } catch (Exceptions $errors) { 134 | 135 | $this->assertCount(1, $obj); 136 | $this->assertSame('test', $obj->sample); 137 | 138 | $this->assertCount(2, $errors); 139 | $this->assertSame('Invalid bar 1', $errors[0]->getMessage()); 140 | $this->assertSame('Invalid bar 2', $errors[1]->getMessage()); 141 | 142 | return; 143 | } 144 | $this->fail(); 145 | } 146 | 147 | public function testGenerateExceptionFromArray() 148 | { 149 | try { 150 | $obj = new testClass(); 151 | $obj->fromArray(['baz' => 1]); 152 | $this->fail(); 153 | } catch (Exceptions $errors) { 154 | 155 | $this->assertCount(0, $obj); 156 | $this->assertCount(2, $errors); 157 | $this->assertSame('Invalid baz 1', $errors[0]->getMessage()); 158 | $this->assertSame('Invalid baz 2', $errors[1]->getMessage()); 159 | 160 | return; 161 | } 162 | $this->fail(); 163 | } 164 | 165 | public function testGenerateExceptionMerge() 166 | { 167 | try { 168 | $obj = (new testClass())->fromArray(['sample' => 'test']); 169 | $obj->merge(['baz' => 1]); 170 | $this->fail(); 171 | } catch (Exceptions $errors) { 172 | 173 | $this->assertCount(1, $obj); 174 | $this->assertSame('test', $obj->sample); 175 | 176 | $this->assertCount(2, $errors); 177 | $this->assertSame('Invalid baz 1', $errors[0]->getMessage()); 178 | $this->assertSame('Invalid baz 2', $errors[1]->getMessage()); 179 | 180 | return; 181 | } 182 | $this->fail(); 183 | } 184 | 185 | public function testComplexFromArray() 186 | { 187 | try { 188 | $obj = new testClass(); 189 | $obj->fromArray(['foo' => 1, 'bar' =>2, 'baz' => 3]); 190 | $this->fail(); 191 | } catch (Exceptions $errors) { 192 | 193 | $this->assertCount(0, $obj); 194 | $this->assertCount(5, $errors); 195 | $this->assertSame('Invalid foo', $errors[0]->getMessage()); 196 | $this->assertSame('Invalid bar 1', $errors[1]->getMessage()); 197 | $this->assertSame('Invalid bar 2', $errors[2]->getMessage()); 198 | $this->assertSame('Invalid baz 1', $errors[3]->getMessage()); 199 | $this->assertSame('Invalid baz 2', $errors[4]->getMessage()); 200 | 201 | return; 202 | } 203 | $this->fail(); 204 | } 205 | 206 | public function testComplexMerge() 207 | { 208 | try { 209 | $obj = (new testClass())->fromArray(['sample' => 'test']); 210 | $obj->merge(['foo' => 1, 'bar' =>2, 'baz' => 3]); 211 | $this->fail(); 212 | } catch (Exceptions $errors) { 213 | 214 | $this->assertCount(1, $obj); 215 | $this->assertSame('test', $obj->sample); 216 | 217 | $this->assertCount(5, $errors); 218 | $this->assertSame('Invalid foo', $errors[0]->getMessage()); 219 | $this->assertSame('Invalid bar 1', $errors[1]->getMessage()); 220 | $this->assertSame('Invalid bar 2', $errors[2]->getMessage()); 221 | $this->assertSame('Invalid baz 1', $errors[3]->getMessage()); 222 | $this->assertSame('Invalid baz 2', $errors[4]->getMessage()); 223 | 224 | return; 225 | } 226 | $this->fail(); 227 | } 228 | 229 | } 230 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU LESSER GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | 9 | This version of the GNU Lesser General Public License incorporates 10 | the terms and conditions of version 3 of the GNU General Public 11 | License, supplemented by the additional permissions listed below. 12 | 13 | 0. Additional Definitions. 14 | 15 | As used herein, "this License" refers to version 3 of the GNU Lesser 16 | General Public License, and the "GNU GPL" refers to version 3 of the GNU 17 | General Public License. 18 | 19 | "The Library" refers to a covered work governed by this License, 20 | other than an Application or a Combined Work as defined below. 21 | 22 | An "Application" is any work that makes use of an interface provided 23 | by the Library, but which is not otherwise based on the Library. 24 | Defining a subclass of a class defined by the Library is deemed a mode 25 | of using an interface provided by the Library. 26 | 27 | A "Combined Work" is a work produced by combining or linking an 28 | Application with the Library. The particular version of the Library 29 | with which the Combined Work was made is also called the "Linked 30 | Version". 31 | 32 | The "Minimal Corresponding Source" for a Combined Work means the 33 | Corresponding Source for the Combined Work, excluding any source code 34 | for portions of the Combined Work that, considered in isolation, are 35 | based on the Application, and not on the Linked Version. 36 | 37 | The "Corresponding Application Code" for a Combined Work means the 38 | object code and/or source code for the Application, including any data 39 | and utility programs needed for reproducing the Combined Work from the 40 | Application, but excluding the System Libraries of the Combined Work. 41 | 42 | 1. Exception to Section 3 of the GNU GPL. 43 | 44 | You may convey a covered work under sections 3 and 4 of this License 45 | without being bound by section 3 of the GNU GPL. 46 | 47 | 2. Conveying Modified Versions. 48 | 49 | If you modify a copy of the Library, and, in your modifications, a 50 | facility refers to a function or data to be supplied by an Application 51 | that uses the facility (other than as an argument passed when the 52 | facility is invoked), then you may convey a copy of the modified 53 | version: 54 | 55 | a) under this License, provided that you make a good faith effort to 56 | ensure that, in the event an Application does not supply the 57 | function or data, the facility still operates, and performs 58 | whatever part of its purpose remains meaningful, or 59 | 60 | b) under the GNU GPL, with none of the additional permissions of 61 | this License applicable to that copy. 62 | 63 | 3. Object Code Incorporating Material from Library Header Files. 64 | 65 | The object code form of an Application may incorporate material from 66 | a header file that is part of the Library. You may convey such object 67 | code under terms of your choice, provided that, if the incorporated 68 | material is not limited to numerical parameters, data structure 69 | layouts and accessors, or small macros, inline functions and templates 70 | (ten or fewer lines in length), you do both of the following: 71 | 72 | a) Give prominent notice with each copy of the object code that the 73 | Library is used in it and that the Library and its use are 74 | covered by this License. 75 | 76 | b) Accompany the object code with a copy of the GNU GPL and this license 77 | document. 78 | 79 | 4. Combined Works. 80 | 81 | You may convey a Combined Work under terms of your choice that, 82 | taken together, effectively do not restrict modification of the 83 | portions of the Library contained in the Combined Work and reverse 84 | engineering for debugging such modifications, if you also do each of 85 | the following: 86 | 87 | a) Give prominent notice with each copy of the Combined Work that 88 | the Library is used in it and that the Library and its use are 89 | covered by this License. 90 | 91 | b) Accompany the Combined Work with a copy of the GNU GPL and this license 92 | document. 93 | 94 | c) For a Combined Work that displays copyright notices during 95 | execution, include the copyright notice for the Library among 96 | these notices, as well as a reference directing the user to the 97 | copies of the GNU GPL and this license document. 98 | 99 | d) Do one of the following: 100 | 101 | 0) Convey the Minimal Corresponding Source under the terms of this 102 | License, and the Corresponding Application Code in a form 103 | suitable for, and under terms that permit, the user to 104 | recombine or relink the Application with a modified version of 105 | the Linked Version to produce a modified Combined Work, in the 106 | manner specified by section 6 of the GNU GPL for conveying 107 | Corresponding Source. 108 | 109 | 1) Use a suitable shared library mechanism for linking with the 110 | Library. A suitable mechanism is one that (a) uses at run time 111 | a copy of the Library already present on the user's computer 112 | system, and (b) will operate properly with a modified version 113 | of the Library that is interface-compatible with the Linked 114 | Version. 115 | 116 | e) Provide Installation Information, but only if you would otherwise 117 | be required to provide such information under section 6 of the 118 | GNU GPL, and only to the extent that such information is 119 | necessary to install and execute a modified version of the 120 | Combined Work produced by recombining or relinking the 121 | Application with a modified version of the Linked Version. (If 122 | you use option 4d0, the Installation Information must accompany 123 | the Minimal Corresponding Source and Corresponding Application 124 | Code. If you use option 4d1, you must provide the Installation 125 | Information in the manner specified by section 6 of the GNU GPL 126 | for conveying Corresponding Source.) 127 | 128 | 5. Combined Libraries. 129 | 130 | You may place library facilities that are a work based on the 131 | Library side by side in a single library together with other library 132 | facilities that are not Applications and are not covered by this 133 | License, and convey such a combined library under terms of your 134 | choice, if you do both of the following: 135 | 136 | a) Accompany the combined library with a copy of the same work based 137 | on the Library, uncombined with any other library facilities, 138 | conveyed under the terms of this License. 139 | 140 | b) Give prominent notice with the combined library that part of it 141 | is a work based on the Library, and explaining where to find the 142 | accompanying uncombined form of the same work. 143 | 144 | 6. Revised Versions of the GNU Lesser General Public License. 145 | 146 | The Free Software Foundation may publish revised and/or new versions 147 | of the GNU Lesser General Public License from time to time. Such new 148 | versions will be similar in spirit to the present version, but may 149 | differ in detail to address new problems or concerns. 150 | 151 | Each version is given a distinguishing version number. If the 152 | Library as you received it specifies that a certain numbered version 153 | of the GNU Lesser General Public License "or any later version" 154 | applies to it, you have the option of following the terms and 155 | conditions either of that published version or of any later version 156 | published by the Free Software Foundation. If the Library as you 157 | received it does not specify a version number of the GNU Lesser 158 | General Public License, you may choose any version of the GNU Lesser 159 | General Public License ever published by the Free Software Foundation. 160 | 161 | If the Library as you received it specifies that a proxy can decide 162 | whether future versions of the GNU Lesser General Public License shall 163 | apply, that proxy's public statement of acceptance of any version is 164 | permanent authorization for you to choose that version for the 165 | Library. 166 | -------------------------------------------------------------------------------- /src/Core/ObjectAsArrayTrait.php: -------------------------------------------------------------------------------- 1 | __data) 49 | || 50 | ( !in_array($key, $this->notgetters()) && method_exists($this, 'get' . ucfirst($key)) ) ; 51 | } 52 | 53 | protected function innerUnSet($key) 54 | { 55 | unset($this->__data[$key]); 56 | } 57 | 58 | protected function innerGet($key) 59 | { 60 | $method = 'get' . ucfirst($key); 61 | if ( !in_array($key, $this->notgetters()) && method_exists($this, $method) ) { 62 | return $this->$method(); 63 | } 64 | return isset($this->__data[$key]) ? $this->__data[$key] : null; 65 | } 66 | 67 | protected function innerSet($key, $val) 68 | { 69 | $method = 'set' . ucfirst($key); 70 | if ( !in_array($key, $this->notsetters()) && method_exists($this, $method) ) { 71 | $this->$method($val); 72 | } else { 73 | if (null === $key) { 74 | $this->__data[] = $val; 75 | } else { 76 | $this->__data[$key] = $val; 77 | } 78 | } 79 | } 80 | 81 | /* 82 | * ObjectAsArrayInterface implementation 83 | */ 84 | 85 | /** 86 | * Returns array of all object's keys 87 | * @return array 88 | */ 89 | public function keys(): array 90 | { 91 | return array_keys($this->__data); 92 | } 93 | 94 | /** 95 | * Returns array of all object's stored values 96 | * @return array 97 | */ 98 | public function values(): array 99 | { 100 | $ret = []; 101 | foreach (array_keys($this->__data) as $key) { 102 | $ret[$key] = $this->innerGet($key); 103 | } 104 | return $ret; 105 | } 106 | 107 | /** 108 | * Returns true if this object-as-array ie empty (contains zero elements) 109 | * Otherwise returns false 110 | * @return bool 111 | */ 112 | public function empty(): bool 113 | { 114 | return empty($this->__data); 115 | } 116 | 117 | /** 118 | * Returns true if the same (===) element exists in this object-as-array 119 | * Otherwise returns false 120 | * @param $element 121 | * @return bool 122 | */ 123 | public function existsSame($element): bool 124 | { 125 | return false !== array_search($element, $this->values(), true); 126 | } 127 | 128 | /** 129 | * Returns index of first found same (===) element 130 | * MUST returns null if the same element is not found 131 | * @param mixed $element 132 | * @return int|string|bool|null 133 | */ 134 | public function searchSame($element) 135 | { 136 | $key = array_search($element, $this->values(), true); 137 | return false === $key ? null : $key; 138 | } 139 | 140 | /** 141 | * \ArrayAccess implementation 142 | */ 143 | public function offsetExists($offset) 144 | { 145 | return $this->innerIsSet($offset); 146 | } 147 | 148 | public function offsetUnset($offset) 149 | { 150 | $this->innerUnSet($offset); 151 | } 152 | 153 | public function offsetGet($offset) 154 | { 155 | return $this->innerGet($offset); 156 | } 157 | 158 | public function offsetSet($offset, $value) 159 | { 160 | $this->innerSet($offset, $value); 161 | } 162 | 163 | /** 164 | * \Countable implementation 165 | */ 166 | public function count() 167 | { 168 | return count($this->__data); 169 | } 170 | 171 | /** 172 | * \Iterator implementation 173 | */ 174 | public function current() 175 | { 176 | return $this->innerGet(key($this->__data)); 177 | } 178 | 179 | public function next() 180 | { 181 | next($this->__data); 182 | } 183 | 184 | public function key() 185 | { 186 | return key($this->__data); 187 | } 188 | 189 | public function valid() 190 | { 191 | return null !== key($this->__data); 192 | } 193 | 194 | public function rewind() 195 | { 196 | reset($this->__data); 197 | } 198 | 199 | /** 200 | * \Runn\Core\HasInnerCastingInterface and \Runn\Core\ArrayCastingInterface implementation 201 | */ 202 | 203 | /** 204 | * Does value need cast to this (or another) class? 205 | * @param mixed $key 206 | * @param mixed $value 207 | * @return bool 208 | */ 209 | protected function needCasting($key, $value): bool 210 | { 211 | if (is_null($value) || is_scalar($value) || is_object($value)) { 212 | return false; 213 | } 214 | return true; 215 | } 216 | 217 | /** 218 | * @param $key 219 | * @param $value 220 | * @return mixed 221 | */ 222 | protected function innerCast($key, $value) 223 | { 224 | return (new static)->fromArray($value); 225 | } 226 | 227 | /** 228 | * @param iterable $data 229 | * @return $this 230 | */ 231 | public function fromArray(iterable $data) 232 | { 233 | $this->__data = []; 234 | $this->merge($data); 235 | return $this; 236 | } 237 | 238 | /** 239 | * @param iterable $data 240 | * @return $this 241 | */ 242 | public function merge(iterable $data) 243 | { 244 | if ($data instanceof ArrayCastingInterface) { 245 | $data = $data->toArray(); 246 | } 247 | 248 | foreach ($data as $key => $value) { 249 | if ($this->needCasting($key, $value)) { 250 | $value = $this->innerCast($key, $value); 251 | } 252 | $this->innerSet($key, $value); 253 | } 254 | 255 | return $this; 256 | } 257 | 258 | /** 259 | * @return array 260 | */ 261 | public function toArray() : array 262 | { 263 | $data = []; 264 | foreach (array_keys($this->__data) as $key) { 265 | $value = $this->innerGet($key); 266 | if ($value instanceof self) { 267 | $data[$key] = $value->toArray(); 268 | } else { 269 | $data[$key] = $value; 270 | } 271 | } 272 | return $data; 273 | } 274 | /** 275 | * @return array 276 | */ 277 | public function toArrayRecursive() : array 278 | { 279 | $data = []; 280 | foreach (array_keys($this->__data) as $key) { 281 | $value = $this->innerGet($key); 282 | if ($value instanceof ArrayCastingInterface) { 283 | $data[$key] = $value->toArrayRecursive(); 284 | } else { 285 | $data[$key] = $value; 286 | } 287 | } 288 | return $data; 289 | } 290 | 291 | /** 292 | * \Serializable implementation 293 | */ 294 | 295 | /** 296 | * @return string 297 | */ 298 | public function serialize() 299 | { 300 | return serialize($this->__data); 301 | } 302 | 303 | /** 304 | * @param string $serialized 305 | */ 306 | public function unserialize($serialized) 307 | { 308 | $this->__data = unserialize($serialized); 309 | } 310 | 311 | /** 312 | * \JsonSerializable implementation 313 | */ 314 | 315 | /** 316 | * @return array 317 | */ 318 | public function jsonSerialize() 319 | { 320 | return $this->values(); 321 | } 322 | 323 | } 324 | -------------------------------------------------------------------------------- /src/Core/CollectionTrait.php: -------------------------------------------------------------------------------- 1 | fromArray($value); 40 | } 41 | 42 | /** 43 | * @param iterable $data 44 | * @return $this 45 | */ 46 | public function merge(iterable $data) 47 | { 48 | if ($data instanceof ArrayCastingInterface) { 49 | $data = $data->toArray(); 50 | } 51 | 52 | foreach ($data as $key => $value) { 53 | if ($this->needCasting($key, $value)) { 54 | $value = $this->innerCast($key, $value); 55 | } 56 | if (is_int($key) && $this->innerIsSet($key)) { 57 | $this->innerSet(null, $value); 58 | } else { 59 | $this->innerSet($key, $value); 60 | } 61 | } 62 | 63 | return $this; 64 | } 65 | 66 | /** 67 | * @param mixed $value 68 | * @return $this 69 | */ 70 | public function add($value) 71 | { 72 | return $this->append($value); 73 | } 74 | 75 | /** 76 | * @param mixed $value 77 | * @return $this 78 | */ 79 | public function append($value) 80 | { 81 | if ($this->needCasting(null, $value)) { 82 | $value = $this->innerCast(null, $value); 83 | } 84 | $this->__data = array_merge($this->__data, [$value]); 85 | return $this; 86 | } 87 | 88 | /** 89 | * @param mixed $value 90 | * @return $this 91 | */ 92 | public function prepend($value) 93 | { 94 | if ($this->needCasting(null, $value)) { 95 | $value = $this->innerCast(null, $value); 96 | } 97 | $this->__data = array_merge([$value], $this->__data); 98 | return $this; 99 | } 100 | 101 | /** 102 | * @param int $offset 103 | * @param int|null $length 104 | * @return mixed 105 | */ 106 | public function slice(int $offset, int $length = null) 107 | { 108 | return (new static)->fromArray(array_slice($this->__data, $offset, $length)); 109 | } 110 | 111 | /** 112 | * @return mixed 113 | */ 114 | public function first() 115 | { 116 | return $this->slice(0, 1)[0]; 117 | } 118 | 119 | /** 120 | * @return mixed 121 | */ 122 | public function last() 123 | { 124 | return $this->slice(-1, 1)[0]; 125 | } 126 | 127 | /** 128 | * @param iterable $attributes 129 | * @return bool 130 | */ 131 | public function existsElementByAttributes(iterable $attributes): bool 132 | { 133 | if (empty($attributes)) { 134 | return false; 135 | } 136 | foreach ($this as $element) { 137 | $elementAttributes = []; 138 | if (!is_array($element) && !(is_object($element) && $element instanceof \Traversable)) { 139 | continue; 140 | } 141 | foreach ($element as $key => $val) { 142 | foreach ($attributes as $attrkey => $attribute) { 143 | if ($attrkey == $key) { 144 | $elementAttributes[$key] = $val; 145 | } 146 | } 147 | } 148 | if ($attributes == $elementAttributes) 149 | return true; 150 | } 151 | return false; 152 | } 153 | 154 | /** 155 | * @param iterable $attributes 156 | * @return static 157 | */ 158 | public function findAllByAttributes(iterable $attributes) 159 | { 160 | return $this->filter(function ($x) use ($attributes) { 161 | if (!is_array($x) && !(is_object($x) && $x instanceof \Traversable)) { 162 | return false; 163 | } 164 | $elementAttributes = []; 165 | foreach ($x as $key => $value) { 166 | foreach ($attributes as $attrkey => $attribute) { 167 | if ($attrkey == $key) { 168 | $elementAttributes[$key] = $value; 169 | } 170 | } 171 | } 172 | return $elementAttributes == $attributes; 173 | }); 174 | } 175 | 176 | /** 177 | * @param iterable $attributes 178 | * @return mixed|null 179 | */ 180 | public function findByAttributes(iterable $attributes) 181 | { 182 | $all = $this->findAllByAttributes($attributes); 183 | return $all->empty() ? null : $all[0]; 184 | } 185 | 186 | /** 187 | * @return static 188 | */ 189 | public function asort() 190 | { 191 | $copy = $this->toArray(); 192 | asort($copy); 193 | return (new static)->fromArray($copy); 194 | } 195 | 196 | /** 197 | * @return static 198 | */ 199 | public function ksort() 200 | { 201 | $copy = $this->toArray(); 202 | ksort($copy); 203 | return (new static)->fromArray($copy); 204 | } 205 | 206 | /** 207 | * @param callable $callback 208 | * @return static 209 | */ 210 | public function uasort(callable $callback) { 211 | $copy = $this->toArray(); 212 | uasort($copy, $callback); 213 | return (new static)->fromArray($copy); 214 | } 215 | 216 | /** 217 | * @param callable $callback 218 | * @return static 219 | */ 220 | public function uksort(callable $callback) { 221 | $copy = $this->toArray(); 222 | uksort($copy, $callback); 223 | return (new static)->fromArray($copy); 224 | } 225 | 226 | /** 227 | * @return static 228 | */ 229 | public function natsort() { 230 | $copy = $this->toArray(); 231 | natsort($copy); 232 | return (new static)->fromArray($copy); 233 | } 234 | 235 | /** 236 | * @return static 237 | */ 238 | public function natcasesort() { 239 | $copy = $this->toArray(); 240 | natcasesort($copy); 241 | return (new static)->fromArray($copy); 242 | } 243 | 244 | /** 245 | * @param callable $callback 246 | * @return static 247 | */ 248 | public function sort(callable $callback) 249 | { 250 | return $this->uasort($callback); 251 | } 252 | 253 | /** 254 | * @return static 255 | */ 256 | public function reverse() { 257 | $clone = clone $this; 258 | $clone->__data = array_reverse($clone->__data, true); 259 | return $clone; 260 | } 261 | 262 | 263 | /** 264 | * @param callable $callback 265 | * @return static 266 | */ 267 | public function map(callable $callback) 268 | { 269 | return (new static)->fromArray(array_values(array_map($callback, $this->toArray()))); 270 | } 271 | 272 | /** 273 | * @param callable $callback 274 | * @return static 275 | */ 276 | public function filter(callable $callback) 277 | { 278 | return (new static)->fromArray(array_values(array_filter($this->toArray(), $callback))); 279 | } 280 | 281 | /** 282 | * @param mixed $start 283 | * @param callable $callback 284 | * @return mixed 285 | */ 286 | public function reduce($start, callable $callback) 287 | { 288 | return array_reduce($this->toArray(), $callback, $start); 289 | } 290 | 291 | /** 292 | * @param mixed $what 293 | * @return array 294 | */ 295 | public function collect($what) 296 | { 297 | $ret = []; 298 | foreach ($this as $element) { 299 | if ($what instanceof \Closure) { 300 | $ret[] = $what($element); 301 | } elseif (is_array($element) || ($element instanceof ObjectAsArrayInterface)) { 302 | $ret[] = $element[$what]; 303 | } elseif (is_object($element)) { 304 | $ret[] = $element->$what; 305 | } 306 | } 307 | return $ret; 308 | } 309 | 310 | /** 311 | * @param $by 312 | * @return array|static[] 313 | */ 314 | public function group($by) 315 | { 316 | $ret = []; 317 | foreach ($this as $element) { 318 | if ($by instanceof \Closure) { 319 | $key = $by($element); 320 | } elseif (is_array($element) || ($element instanceof ObjectAsArrayInterface)) { 321 | $key = $element[$by]; 322 | } elseif (is_object($element)) { 323 | $key = $element->$by; 324 | } 325 | if (!isset($ret[$key])) { 326 | $ret[$key] = new static; 327 | } 328 | $ret[$key]->add($element); 329 | } 330 | return $ret; 331 | } 332 | 333 | public function __call(string $method, array $params = []) 334 | { 335 | foreach ($this as $element) { 336 | call_user_func_array([$element, $method], $params); 337 | } 338 | } 339 | 340 | } 341 | -------------------------------------------------------------------------------- /tests/Core/ObjectAsArrayTraitTest.php: -------------------------------------------------------------------------------- 1 | __data['foo'] = $val*2; 50 | } 51 | } 52 | 53 | class testWithSetterNotsetterClass implements ObjectAsArrayInterface 54 | { 55 | use ObjectAsArrayTrait; 56 | protected function notsetters(): array { return ['bar']; } 57 | protected function setFoo($val) 58 | { 59 | $this->__data['foo'] = $val*2; 60 | } 61 | protected function setBar($val) 62 | { 63 | $this->__data['bar'] = $val/2; 64 | } 65 | } 66 | 67 | class ObjectAsArrayTraitTest extends TestCase 68 | { 69 | 70 | public function testInterfaces() 71 | { 72 | $obj = new testClass(); 73 | 74 | $this->assertInstanceOf(ObjectAsArrayInterface::class, $obj); 75 | $this->assertInstanceOf(\ArrayAccess::class, $obj); 76 | $this->assertInstanceOf(\Countable::class, $obj); 77 | $this->assertInstanceOf(\Iterator::class, $obj); 78 | $this->assertInstanceOf(ArrayCastingInterface::class, $obj); 79 | $this->assertInstanceOf(HasInnerCastingInterface::class, $obj); 80 | $this->assertInstanceOf(\Serializable::class, $obj); 81 | $this->assertInstanceOf(\JsonSerializable::class, $obj); 82 | } 83 | 84 | public function testKeysValues() 85 | { 86 | $obj = new testClass(); 87 | $obj[1] = 100; 88 | $obj[2] = '200'; 89 | $obj['foo'] = 'bar'; 90 | $obj[] = 'baz'; 91 | 92 | $this->assertSame([1, 2, 'foo', 3], $obj->keys()); 93 | $this->assertSame([1=>100, 2=>'200', 'foo'=>'bar', 3=>'baz'], $obj->values()); 94 | } 95 | 96 | public function testIsEmpty() 97 | { 98 | $obj = new testClass(); 99 | $this->assertTrue($obj->empty()); 100 | 101 | $obj[0] = 1; 102 | $this->assertFalse($obj->empty()); 103 | 104 | unset($obj[0]); 105 | $this->assertTrue($obj->empty()); 106 | } 107 | 108 | public function testExistsSame() 109 | { 110 | $obj = new testClass(); 111 | $obj[1] = 100; 112 | $obj[2] = '200'; 113 | 114 | $this->assertTrue($obj->existsSame(100)); 115 | $this->assertFalse($obj->existsSame('100')); 116 | $this->assertTrue($obj->existsSame('200')); 117 | $this->assertFalse($obj->existsSame(200)); 118 | } 119 | 120 | public function testSearchSame() 121 | { 122 | $obj = new testClass(); 123 | $obj[1] = 100; 124 | $obj[2] = '200'; 125 | 126 | $this->assertSame(1, $obj->searchSame(100)); 127 | $this->assertNull($obj->searchSame('100')); 128 | $this->assertSame(2, $obj->searchSame('200')); 129 | $this->assertNull($obj->searchSame(200)); 130 | $this->assertNull($obj->searchSame(300)); 131 | } 132 | 133 | public function testArrayAccess() 134 | { 135 | $obj = new testClass(); 136 | $obj[1] = 100; 137 | $obj[2] = '200'; 138 | $obj[] = 300; 139 | 140 | $this->assertTrue(isset($obj[1])); 141 | $this->assertTrue(isset($obj[2])); 142 | $this->assertTrue(isset($obj[3])); 143 | 144 | $this->assertEquals(100, $obj[1]); 145 | $this->assertEquals('200', $obj[2]); 146 | $this->assertEquals(300, $obj[3]); 147 | 148 | unset($obj[2]); 149 | 150 | $this->assertTrue(isset($obj[1])); 151 | $this->assertFalse(isset($obj[2])); 152 | $this->assertTrue(isset($obj[3])); 153 | } 154 | 155 | public function testCountable() 156 | { 157 | $obj = new testClass(); 158 | 159 | $this->assertCount(0, $obj); 160 | 161 | $obj[] = 'foo'; 162 | $obj[] = 'bar'; 163 | 164 | $this->assertCount(2, $obj); 165 | 166 | unset($obj[0]); 167 | 168 | $this->assertCount(1, $obj); 169 | } 170 | 171 | public function testIterator() 172 | { 173 | $obj = new testClass(); 174 | $obj['foo'] = 100; 175 | $obj['bar'] = 200; 176 | $obj[300] = 'baz'; 177 | 178 | $res = ''; 179 | foreach ($obj as $key => $val) { 180 | $res .= $key . '=' . $val . ';'; 181 | } 182 | 183 | $this->assertEquals('foo=100;bar=200;300=baz;', $res); 184 | } 185 | 186 | public function testNeedCasting() 187 | { 188 | $method = new \ReflectionMethod(testClass::class, 'needCasting'); 189 | $closure = $method->getClosure(new testClass()); 190 | 191 | $this->assertFalse($closure(null, null)); 192 | $this->assertFalse($closure(null, 42)); 193 | $this->assertFalse($closure(null, 3.14159)); 194 | $this->assertFalse($closure(null, 'foo')); 195 | $this->assertFalse($closure(null, function () {return 0;})); 196 | $this->assertFalse($closure(null, new testClass(['foo' => 'bar']))); 197 | $this->assertFalse($closure(null, new \stdClass())); 198 | 199 | $this->assertTrue($closure(null, [1, 2, 3])); 200 | } 201 | 202 | public function testFromArray() 203 | { 204 | // First: create data 205 | $obj = new testClass(); 206 | $obj->fromArray(['foo' => 100, 'bar' => 200, 'baz' => ['one' => 1, 'two' => 2]]); 207 | 208 | $this->assertInstanceOf(testClass::class, $obj); 209 | $this->assertInstanceOf(testClass::class, $obj['baz']); 210 | 211 | $this->assertCount(3, $obj); 212 | 213 | $this->assertEquals(100, $obj['foo']); 214 | $this->assertEquals(200, $obj['bar']); 215 | 216 | $this->assertEquals((new testClass())->fromArray(['one' => 1, 'two' => 2]), $obj['baz']); 217 | $this->assertEquals(1, $obj['baz']['one']); 218 | $this->assertEquals(2, $obj['baz']['two']); 219 | 220 | // Next: rewrite data 221 | $obj->fromArray(['sample' => 1, 'test' => 2]); 222 | 223 | $this->assertCount(2, $obj); 224 | 225 | $this->assertEquals(1, $obj['sample']); 226 | $this->assertEquals(2, $obj['test']); 227 | 228 | $this->assertFalse(isset($obj['foo'])); 229 | $this->assertFalse(isset($obj['bar'])); 230 | $this->assertFalse(isset($obj['baz'])); 231 | } 232 | 233 | public function testMerge() 234 | { 235 | // Create data 236 | $obj = new testClass(); 237 | $obj->fromArray(['foo' => 100, 'bar' => 200, 'baz' => ['one' => 1, 'two' => 2]]); 238 | 239 | // Merge new data now! 240 | $obj->merge(['sample' => 1, 'test' => 2]); 241 | 242 | $this->assertCount(5, $obj); 243 | 244 | $this->assertEquals(100, $obj['foo']); 245 | $this->assertEquals(200, $obj['bar']); 246 | $this->assertEquals((new testClass())->fromArray(['one' => 1, 'two' => 2]), $obj['baz']); 247 | $this->assertEquals(1, $obj['baz']['one']); 248 | $this->assertEquals(2, $obj['baz']['two']); 249 | $this->assertEquals(1, $obj['sample']); 250 | $this->assertEquals(2, $obj['test']); 251 | } 252 | 253 | public function testMergeWithArrayable() 254 | { 255 | // Create data 256 | $obj = new testClass(); 257 | $obj->fromArray(['sample' => 100, 'test' => 200]); 258 | 259 | $this->assertCount(2, $obj); 260 | $this->assertEquals(100, $obj['sample']); 261 | $this->assertEquals(200, $obj['test']); 262 | 263 | // Create arrayable 264 | $merged = new testWithGetterClass(); 265 | $merged['foo'] = 300; 266 | 267 | // Merge now 268 | $obj->merge($merged); 269 | 270 | $this->assertCount(3, $obj); 271 | $this->assertEquals(100, $obj['sample']); 272 | $this->assertEquals(200, $obj['test']); 273 | $this->assertEquals(42, $obj['foo']); 274 | } 275 | 276 | public function testToArray() 277 | { 278 | $obj = new testClass(); 279 | $obj->fromArray(['foo' => 100, 'bar' => 200, 'baz' => ['one' => 1, 'two' => 2]]); 280 | $arr = $obj->toArray(); 281 | 282 | $this->assertTrue(is_array($arr)); 283 | $this->assertEquals( 284 | ['foo' => 100, 'bar' => 200, 'baz' => ['one' => 1, 'two' => 2]], 285 | $arr 286 | ); 287 | 288 | $obj = new testClass(); 289 | $obj['foo'] = 100; 290 | $obj['bar'] = 200; 291 | $obj['baz'] = (new testAnotherClass())->fromArray(['one' => 1, 'two' => 2]); 292 | $arr = $obj->toArray(); 293 | 294 | $this->assertTrue(is_array($arr)); 295 | $this->assertEquals( 296 | ['foo' => 100, 'bar' => 200, 'baz' => (new testAnotherClass())->fromArray(['one' => 1, 'two' => 2])], 297 | $arr 298 | ); 299 | } 300 | 301 | public function testToArrayRecursive() 302 | { 303 | $obj = new testClass(); 304 | $obj['foo'] = 100; 305 | $obj['bar'] = 200; 306 | $obj['baz'] = (new testAnotherClass())->fromArray(['one' => 1, 'two' => 2]); 307 | $arr = $obj->toArrayRecursive(); 308 | 309 | $this->assertTrue(is_array($arr)); 310 | $this->assertEquals( 311 | ['foo' => 100, 'bar' => 200, 'baz' => ['one' => 1, 'two' => 2]], 312 | $arr 313 | ); 314 | } 315 | 316 | public function testGetter() 317 | { 318 | $obj = new testWithGetterClass(); 319 | 320 | $this->assertTrue(isset($obj['foo'])); 321 | 322 | $obj[1] = 100; 323 | $obj['foo'] = 200; 324 | 325 | $this->assertEquals(100, $obj[1]); 326 | $this->assertEquals(42, $obj['foo']); 327 | } 328 | 329 | public function testGetterNotGetter() 330 | { 331 | $obj = new testWithGetterNotgetterClass(); 332 | 333 | $this->assertTrue(isset($obj['foo'])); 334 | $this->assertFalse(isset($obj['bar'])); 335 | 336 | $this->assertSame(42, $obj['foo']); 337 | $this->assertSame(null, $obj['bar']); 338 | } 339 | 340 | public function testSetter() 341 | { 342 | $obj = new testWithSetterClass(); 343 | $obj[1] = 100; 344 | $obj['foo'] = 200; 345 | 346 | $this->assertEquals(100, $obj[1]); 347 | $this->assertEquals(400, $obj['foo']); 348 | } 349 | 350 | public function testSetterNotsetter() 351 | { 352 | $obj = new testWithSetterNotsetterClass(); 353 | $obj['foo'] = 200; 354 | $obj['bar'] = 400; 355 | 356 | $this->assertEquals(400, $obj['foo']); 357 | $this->assertEquals(400, $obj['bar']); 358 | } 359 | 360 | public function testSerialize() 361 | { 362 | $obj = new testClass(); 363 | $obj->fromArray([1=>100, 2=>200, 'foo'=>'bar']); 364 | 365 | $this->assertStringContainsString('{a:3:{i:1;i:100;i:2;i:200;s:3:"foo";s:3:"bar";}', serialize($obj)); 366 | $this->assertEquals($obj, unserialize(serialize($obj))); 367 | } 368 | 369 | public function testJsonSerialize() 370 | { 371 | $obj = new testClass; 372 | $obj->fromArray([1=>100, 2=>200, 'foo'=>'bar']); 373 | 374 | $this->assertEquals('{"1":100,"2":200,"foo":"bar"}', json_encode($obj)); 375 | 376 | $obj = new testWithGetterClass; 377 | $obj->fromArray(['foo' => 100]); 378 | 379 | $this->assertEquals('{"foo":42}', json_encode($obj)); 380 | } 381 | 382 | public function testSiblingClasses() 383 | { 384 | $obj = new testClass(); 385 | $obj->fromArray(['foo' => new testAnotherClass()]); 386 | 387 | $this->assertInstanceOf(testClass::class, $obj); 388 | $this->assertInstanceOf(testAnotherClass::class, $obj['foo']); 389 | } 390 | 391 | } 392 | -------------------------------------------------------------------------------- /tests/Core/CollectionTraitTest.php: -------------------------------------------------------------------------------- 1 | data = $x; 21 | } 22 | public function increment() 23 | { 24 | $this->data++; 25 | } 26 | } 27 | 28 | class CollectionTraitTest extends TestCase 29 | { 30 | 31 | public function testFromArray() 32 | { 33 | $collection = (new testClass)->fromArray([100, 200, 300, [400, 500]]); 34 | 35 | $this->assertInstanceOf(CollectionInterface::class, $collection); 36 | $this->assertCount(4, $collection); 37 | $this->assertEquals( 38 | [100, 200, 300, (new testClass)->fromArray([400, 500])], 39 | $collection->values() 40 | ); 41 | $this->assertEquals( 42 | [100, 200, 300, [400, 500]], 43 | $collection->toArray() 44 | ); 45 | $this->assertEquals(100, $collection[0]); 46 | $this->assertEquals(200, $collection[1]); 47 | $this->assertEquals(300, $collection[2]); 48 | $this->assertEquals((new testClass)->fromArray([400, 500]), $collection[3]); 49 | } 50 | 51 | public function testAppendPrependAdd() 52 | { 53 | $collection = new testClass(); 54 | 55 | $collection->append(100); 56 | $collection->append(200); 57 | 58 | $this->assertCount(2, $collection); 59 | $this->assertEquals(100, $collection[0]); 60 | $this->assertEquals(200, $collection[1]); 61 | $this->assertEquals( 62 | [100, 200], 63 | $collection->toArray() 64 | ); 65 | 66 | $collection->prepend(300); 67 | 68 | $this->assertCount(3, $collection); 69 | $this->assertEquals(300, $collection[0]); 70 | $this->assertEquals(100, $collection[1]); 71 | $this->assertEquals(200, $collection[2]); 72 | $this->assertEquals( 73 | [300, 100, 200], 74 | $collection->toArray() 75 | ); 76 | 77 | $collection->add(400); 78 | 79 | $this->assertCount(4, $collection); 80 | $this->assertEquals(300, $collection[0]); 81 | $this->assertEquals(100, $collection[1]); 82 | $this->assertEquals(200, $collection[2]); 83 | $this->assertEquals(400, $collection[3]); 84 | $this->assertEquals( 85 | [300, 100, 200, 400], 86 | $collection->toArray() 87 | ); 88 | 89 | $collection->append([1, 2]); 90 | $this->assertCount(5, $collection); 91 | $this->assertEquals( 92 | [300, 100, 200, 400, (new testClass)->fromArray([1, 2])], 93 | $collection->values() 94 | ); 95 | 96 | $collection->prepend([3, 4]); 97 | $this->assertCount(6, $collection); 98 | $this->assertEquals( 99 | [(new testClass)->fromArray([3, 4]), 300, 100, 200, 400, (new testClass)->fromArray([1, 2])], 100 | $collection->values() 101 | ); 102 | 103 | } 104 | 105 | public function testMerge() 106 | { 107 | $collection = (new testClass)->fromArray([1, 2]); 108 | 109 | $collection->merge([3, 4]); 110 | $this->assertCount(4, $collection); 111 | $expected = (new testClass)->fromArray([1, 2, 3, 4]); 112 | $this->assertEquals($expected->toArray(), $collection->toArray()); 113 | 114 | $collection->merge((new testClass)->fromArray([5, 6])); 115 | $this->assertCount(6, $collection); 116 | $expected = (new testClass)->fromArray([1, 2, 3, 4, 5, 6]); 117 | $this->assertEquals($expected->toArray(), $collection->toArray()); 118 | } 119 | 120 | public function testSlice() 121 | { 122 | $collection = (new testClass)->fromArray([10, 20, 30, 40, 50]); 123 | $this->assertEquals( 124 | (new testClass)->fromArray([30, 40, 50]), 125 | $collection->slice(2) 126 | ); 127 | $this->assertEquals( 128 | (new testClass)->fromArray([40, 50]), 129 | $collection->slice(-2) 130 | ); 131 | $this->assertEquals( 132 | (new testClass)->fromArray([30, 40]), 133 | $collection->slice(2, 2) 134 | ); 135 | $this->assertEquals( 136 | (new testClass)->fromArray([40]), 137 | $collection->slice(-2, 1) 138 | ); 139 | } 140 | 141 | public function testFirstLast() 142 | { 143 | $collection = (new testClass)->fromArray([10, 20, 30, 40, 50]); 144 | $this->assertEquals( 145 | 10, 146 | $collection->first() 147 | ); 148 | $this->assertEquals( 149 | 50, 150 | $collection->last() 151 | ); 152 | } 153 | 154 | public function testExistsElementByAttributes() 155 | { 156 | $collection = new testClass(); 157 | $el1 = new \Runn\Core\Std(['id' => 1, 'title' => 'foo', 'text' => 'FooFooFoo']); 158 | $collection->append($el1); 159 | $el2 = new \Runn\Core\Std(['id' => 2, 'title' => 'bar', 'text' => 'BarBarBar']); 160 | $collection->append($el2); 161 | $collection->append(42); 162 | 163 | $this->assertFalse($collection->existsElementByAttributes([])); 164 | $this->assertTrue($collection->existsElementByAttributes(['id' => 1])); 165 | $this->assertFalse($collection->existsElementByAttributes(['id' => 3])); 166 | $this->assertTrue($collection->existsElementByAttributes(['title' => 'foo'])); 167 | $this->assertTrue($collection->existsElementByAttributes(['title' => 'foo', 'text' => 'FooFooFoo'])); 168 | $this->assertFalse($collection->existsElementByAttributes(['title' => 'foo', 'text' => 'BarBarBar'])); 169 | } 170 | 171 | public function testFindAllByAttibutes() 172 | { 173 | $collection = new testClass(); 174 | $el1 = new \Runn\Core\Std(['id' => 1, 'title' => 'foo', 'text' => 'FooFooFoo']); 175 | $collection->append($el1); 176 | $el2 = new \Runn\Core\Std(['id' => 2, 'title' => 'foo', 'text' => 'AnotherFoo']); 177 | $collection->append($el2); 178 | $collection->append(42); 179 | 180 | $this->assertEquals( 181 | (new testClass)->fromArray([ 182 | new \Runn\Core\Std(['id' => 1, 'title' => 'foo', 'text' => 'FooFooFoo']), 183 | new \Runn\Core\Std(['id' => 2, 'title' => 'foo', 'text' => 'AnotherFoo']) 184 | ]), 185 | $collection->findAllByAttributes(['title' => 'foo']) 186 | ); 187 | } 188 | 189 | public function testFindByAttibutes() 190 | { 191 | $collection = new testClass(); 192 | $el1 = new \Runn\Core\Std(['id' => 1, 'title' => 'foo', 'text' => 'FooFooFoo']); 193 | $collection->append($el1); 194 | $el2 = new \Runn\Core\Std(['id' => 2, 'title' => 'foo', 'text' => 'AnotherFoo']); 195 | $collection->append($el2); 196 | $collection->append(42); 197 | 198 | $this->assertEquals( 199 | new \Runn\Core\Std(['id' => 1, 'title' => 'foo', 'text' => 'FooFooFoo']), 200 | $collection->findByAttributes(['title' => 'foo']) 201 | ); 202 | } 203 | 204 | public function testSort() 205 | { 206 | $collection = (new testClass)->fromArray([10 => 1, 30 => 3, 20 => 2, 'a' => -1, 'b' => 0, 'c' => 42, 1 => '1', '111', '11']); 207 | 208 | $result = $collection->asort(); 209 | 210 | $expected = (new testClass)->fromArray(['a' => -1, 'b' => 0, 1 => '1', 10 => 1, 20 => 2, 30 => 3, 32 => '11', 'c' => 42, 31 => '111']); 211 | $this->assertEquals($expected->toArray(), $result->toArray()); 212 | 213 | $result = $collection->ksort(); 214 | 215 | $expected = (new testClass)->fromArray(['a' => -1, 'b' => 0, 'c' => 42, 1 => '1', 10 => 1, 20 => 2, 30 => 3, 31 => '111', 32 => '11']); 216 | $this->assertEquals($expected->toArray(), $result->toArray()); 217 | 218 | $result = $collection->uasort(function ($a, $b) { return $a < $b ? 1 : ($a > $b ? -1 : 0);}); 219 | 220 | $expected = (new testClass)->fromArray([31 => '111', 'c' => 42, 32 => '11', 30 => 3, 20 => 2, 10 => 1, 1 => '1', 'b' => 0, 'a' => -1]); 221 | $this->assertEquals($expected->toArray(), $result->toArray()); 222 | 223 | $result = $collection->sort(function ($a, $b) { return -($a <=> $b);}); 224 | 225 | $expected = (new testClass)->fromArray([31 => '111', 'c' => 42, 32 => '11', 30 => 3, 20 => 2, 10 => 1, 1 => '1', 'b' => 0, 'a' => -1]); 226 | $this->assertEquals($expected->toArray(), $result->toArray()); 227 | 228 | $result = $collection->uksort(function ($a, $b) { return $a < $b ? 1 : ($a > $b ? -1 : 0);}); 229 | 230 | $expected = (new testClass)->fromArray([32 => '11', 31 => '111', 30 => 3, 20 => 2, 10 => 1, 1 => '1', 'c' => 42, 'b' => 0, 'a' => -1]); 231 | $this->assertEquals($expected->toArray(), $result->toArray()); 232 | 233 | $collection = (new testClass)->fromArray([0 => '12', 1 => '10', 2 => '2', 3 => '1']); 234 | $result = $collection->natsort(); 235 | $expected = (new testClass)->fromArray([3 => '1', 2 => '2', 1 => '10', 0 => '12']); 236 | $this->assertEquals($expected->toArray(), $result->toArray()); 237 | 238 | $collection = (new testClass)->fromArray([0 => 'IMG0.png', 1 => 'img12.png', 2 => 'img10.png', 3 => 'img2.png', 4 => 'img1.png', 5 => 'IMG3.png']); 239 | $result = $collection->natcasesort(); 240 | $expected = (new testClass)->fromArray([0 => 'IMG0.png', 4 => 'img1.png', 3 => 'img2.png', 5 => 'IMG3.png', 2 => 'img10.png', 1 => 'img12.png']); 241 | $this->assertEquals($expected->toArray(), $result->toArray()); 242 | } 243 | 244 | public function testReverse() 245 | { 246 | $collection = (new testClass)->fromArray([10 => 1, 30 => 3, 20 => 2, 'a' => -1, 'b' => 0, 'c' => 42, '111', '11']); 247 | $result = $collection->reverse(); 248 | 249 | $expected = (new testClass)->fromArray([32 => '11', 31 => '111', 'c' => 42, 'b' => 0, 'a' => -1, 20 => 2, 30 => 3, 10 => 1]); 250 | $this->assertEquals($expected->toArray(), $result->toArray()); 251 | } 252 | 253 | public function testMap() 254 | { 255 | $collection = (new testClass)->fromArray([1, 2, 3]); 256 | $result = $collection->map(function ($x) {return $x*2;}); 257 | 258 | $expected = (new testClass)->fromArray([2, 4, 6]); 259 | $this->assertEquals(array_values($expected->toArray()), array_values($result->toArray())); 260 | } 261 | 262 | public function testFilter() 263 | { 264 | $collection = (new testClass)->fromArray([1, -1, 0, 2, 3, -5]); 265 | $result = $collection->filter(function ($x) {return $x>0;}); 266 | 267 | $expected = (new testClass)->fromArray([1, 2, 3]); 268 | $this->assertEquals($expected->toArray(), $result->toArray()); 269 | } 270 | 271 | public function testReduce() 272 | { 273 | $collection = (new testClass)->fromArray([1, 2, 3, 4]); 274 | $reduced = $collection->reduce(0, function($carry, $item) { 275 | return $carry + $item; 276 | }); 277 | $this->assertEquals(10, $reduced); 278 | } 279 | 280 | public function testCollect() 281 | { 282 | $i1 = new \Runn\Core\Std(['id' => 1, 'title' => 'foo']); 283 | $i2 = new \Runn\Core\Std(['id' => 2, 'title' => 'bar']); 284 | $i3 = (object)['id' => 3, 'title' => 'baz']; 285 | 286 | $collection = new testClass(); 287 | $collection->append($i1); 288 | $collection->append($i2); 289 | $collection->append($i3); 290 | 291 | $this->assertEquals( 292 | [ 293 | new \Runn\Core\Std(['id' => 1, 'title' => 'foo']), 294 | new \Runn\Core\Std(['id' => 2, 'title' => 'bar']), 295 | (object)['id' => 3, 'title' => 'baz'] 296 | ], 297 | $collection->toArray() 298 | ); 299 | 300 | $ids = $collection->collect('id'); 301 | $this->assertEquals([1, 2, 3], $ids); 302 | 303 | $titles = $collection->collect(function ($x) { 304 | return $x->title; 305 | }); 306 | $this->assertEquals(['foo', 'bar', 'baz'], $titles); 307 | 308 | $collection = (new testClass)->fromArray([ 309 | ['id' => 1, 'title' => 'foo'], 310 | ['id' => 2, 'title' => 'bar'], 311 | ['id' => 3, 'title' => 'baz'], 312 | ]); 313 | 314 | $ids = $collection->collect('id'); 315 | $this->assertEquals([1, 2, 3], $ids); 316 | 317 | $titles = $collection->collect(function ($x) { 318 | return $x['title']; 319 | }); 320 | $this->assertEquals(['foo', 'bar', 'baz'], $titles); 321 | } 322 | 323 | public function testGroup() 324 | { 325 | $collection = (new testClass)->fromArray([ 326 | ['date' => '2000-01-01', 'title' => 'First'], 327 | ['date' => '2000-01-01', 'title' => 'Second'], 328 | ['date' => '2000-01-02', 'title' => 'Third'], 329 | (object)['date' => '2000-01-04', 'title' => 'Fourth'], 330 | ]); 331 | 332 | $grouped = $collection->group('date'); 333 | $this->assertEquals([ 334 | '2000-01-01' => (new testClass)->fromArray([['date' => '2000-01-01', 'title' => 'First'], ['date' => '2000-01-01', 'title' => 'Second']]), 335 | '2000-01-02' => (new testClass)->fromArray([['date' => '2000-01-02', 'title' => 'Third']]), 336 | '2000-01-04' => (new testClass)->fromArray([(object)['date' => '2000-01-04', 'title' => 'Fourth']]), 337 | ], $grouped); 338 | 339 | $grouped = $collection->group(function ($x) {return date('m-d', strtotime($x instanceof ObjectAsArrayInterface ? $x['date'] : $x->date));}); 340 | $this->assertEquals([ 341 | '01-01' => (new testClass)->fromArray([['date' => '2000-01-01', 'title' => 'First'], ['date' => '2000-01-01', 'title' => 'Second']]), 342 | '01-02' => (new testClass)->fromArray([['date' => '2000-01-02', 'title' => 'Third']]), 343 | '01-04' => (new testClass)->fromArray([(object)['date' => '2000-01-04', 'title' => 'Fourth']]), 344 | ], $grouped); 345 | } 346 | 347 | public function testCall() 348 | { 349 | $collection = new testClass(); 350 | $collection->append(new Number(1)); 351 | $collection->append(new Number(2)); 352 | $collection->append(new Number(3)); 353 | 354 | $collectionExpected = new testClass(); 355 | $collectionExpected->append(new Number(2)); 356 | $collectionExpected->append(new Number(3)); 357 | $collectionExpected->append(new Number(4)); 358 | 359 | $collection->increment(); 360 | $this->assertEquals($collectionExpected, $collection); 361 | } 362 | 363 | } 364 | --------------------------------------------------------------------------------