[ ExtensibleObjectExtension::class ], ]; public static function provideGetIterator(): array { return [ 'non-iterable object' => [ 'data' => new ArrayData(['Field1' => 'value1', 'Field2' => 'value2']), 'expected' => BadMethodCallException::class, ], 'non-iterable scalar' => [ 'data' => 'This is some text, aint iterable', 'expected' => BadMethodCallException::class, ], 'empty array' => [ 'data' => [], 'expected' => [], ], 'single item array' => [ 'data' => ['one value'], 'expected' => ['one value'], ], 'multi-item array' => [ 'data' => ['one', 'two', 'three'], 'expected' => ['one', 'two', 'three'], ], 'object implements an Iterable interface' => [ 'data' => new ArrayList(['one', 'two', 'three']), 'expected' => ['one', 'two', 'three'], ], 'built-in PHP iterator' => [ 'data' => new ArrayIterator(['one', 'two', 'three']), 'expected' => ['one', 'two', 'three'], ], 'non-iterable object with getIterator method' => [ 'data' => new NonIterableObject(), 'expected' => ['some value', 'another value', 'isnt this nice'], ], 'extensible object with getIterator extension' => [ 'data' => new ExtensibleObject(), 'expected' => ['1','2','3','4','5','6','7','8','9','a','b','c','d','e'], ], ]; } #[DataProvider('provideGetIterator')] public function testGetIterator(mixed $data, string|array $expected): void { $viewLayerData = new ViewLayerData($data); if ($expected === BadMethodCallException::class) { $this->expectException(BadMethodCallException::class); $this->expectExceptionMessageMatches('/is not iterable.$/'); } $this->assertEquals($expected, iterator_to_array($viewLayerData->getIterator())); // Ensure the iterator is always wrapping values foreach ($viewLayerData as $value) { $this->assertInstanceOf(ViewLayerData::class, $value); } } public static function provideGetIteratorCount(): array { return [ 'uncountable object' => [ 'data' => new ArrayData(['Field1' => 'value1', 'Field2' => 'value2']), 'expected' => 0, ], 'uncountable object - has count field' => [ 'data' => new ArrayData(['count' => 12, 'Field2' => 'value2']), 'expected' => 12, ], 'uncountable object - has count field (non-int)' => [ 'data' => new ArrayData(['count' => 'aahhh', 'Field2' => 'value2']), 'expected' => 0, ], 'empty array' => [ 'data' => [], 'expected' => 0, ], 'array with values' => [ 'data' => [1, 2], 'expected' => 2, ], 'explicitly countable object' => [ 'data' => new CountableObject(), 'expected' => 53, ], 'non-countable object with getCount method' => [ 'data' => new GetCountObject(), 'expected' => 12, ], 'non-countable object with getIterator method' => [ 'data' => new NonIterableObject(), 'expected' => 3, ], 'extensible object with getIterator extension' => [ 'data' => new ExtensibleObject(), 'expected' => 14, ], ]; } #[DataProvider('provideGetIteratorCount')] public function testGetIteratorCount(mixed $data, int $expected): void { $viewLayerData = new ViewLayerData($data); $this->assertSame($expected, $viewLayerData->getIteratorCount()); } public static function provideIsSet(): array { return [ 'list array' => [ 'data' => ['anything'], 'name' => 'anything', 'expected' => false, ], 'associative array has key' => [ 'data' => ['anything' => 'some value'], 'name' => 'anything', 'expected' => true, ], 'ModelData without field' => [ 'data' => new ArrayData(['nothing' => 'some value']), 'name' => 'anything', 'expected' => false, ], 'ModelData with field' => [ 'data' => new ArrayData(['anything' => 'some value']), 'name' => 'anything', 'expected' => true, ], 'extensible class with getter extension' => [ 'data' => new ExtensibleObject(), 'name' => 'anything', 'expected' => true, ], 'extensible class not set' => [ 'data' => new ExtensibleObject(), 'name' => 'anythingelse', 'expected' => false, ], 'class with method' => [ 'data' => new CountableObject(), 'name' => 'count', 'expected' => true, ], ]; } #[DataProvider('provideIsSet')] public function testIsSet(mixed $data, string $name, bool $expected): void { $viewLayerData = new ViewLayerData($data); $this->assertSame($expected, isset($viewLayerData->$name)); } public static function provideGet(): array { return [ 'basic field' => [ 'name' => 'SomeField', 'throwException' => true, 'expected' => [ [ 'type' => 'method', 'name' => 'SomeField', 'args' => [], ], [ 'type' => 'method', 'name' => 'getSomeField', 'args' => [], ], [ 'type' => 'property', 'name' => 'SomeField', ], ], ], 'getter as property' => [ 'name' => 'getSomeField', 'throwException' => true, 'expected' => [ [ 'type' => 'method', 'name' => 'getSomeField', 'args' => [], ], [ 'type' => 'method', 'name' => 'getgetSomeField', 'args' => [], ], [ 'type' => 'property', 'name' => 'getSomeField', ], ], ], 'basic field (lowercase)' => [ 'name' => 'somefield', 'throwException' => true, 'expected' => [ [ 'type' => 'method', 'name' => 'somefield', 'args' => [], ], [ 'type' => 'method', 'name' => 'getsomefield', 'args' => [], ], [ 'type' => 'property', 'name' => 'somefield', ], ], ], 'property not set, dont even try it' => [ 'name' => 'NotSet', 'throwException' => true, 'expected' => [ [ 'type' => 'method', 'name' => 'NotSet', 'args' => [], ], [ 'type' => 'method', 'name' => 'getNotSet', 'args' => [], ], ], ], 'stops after method when not throwing' => [ 'name' => 'SomeField', 'throwException' => false, 'expected' => [ [ 'type' => 'method', 'name' => 'SomeField', 'args' => [], ], ], ], ]; } #[DataProvider('provideGet')] public function testGet(string $name, bool $throwException, array $expected): void { $fixture = new TestFixture(); $fixture->throwException = $throwException; $viewLayerData = new ViewLayerData($fixture); $value = $viewLayerData->$name; $this->assertSame($expected, $fixture->getRequested()); $this->assertNull($value); } public static function provideGetComplex(): array { // Note the actual value checks aren't very comprehensive here because that's done // in more detail in testGetRawDataValue return [ 'exception gets thrown if not __call() method' => [ 'name' => 'badMethodCall', 'expectRequested' => BadMethodCallException::class, 'expected' => null, ], 'returning nothing is like returning null' => [ 'name' => 'voidMethod', 'expectRequested' => [ [ 'type' => 'method', 'name' => 'voidMethod', 'args' => [], ], ], 'expected' => null, ], 'returned value is caught' => [ 'name' => 'justCallMethod', 'expectRequested' => [ [ 'type' => 'method', 'name' => 'justCallMethod', 'args' => [], ], ], 'expected' => 'This is a method value', ], 'getter is used' => [ 'name' => 'ActualValue', 'expectRequested' => [ [ 'type' => 'method', 'name' => 'getActualValue', 'args' => [], ], ], 'expected' => 'this is the value', ], 'if no method exists, only property is fetched' => [ 'name' => 'NoMethod', 'expectRequested' => [ [ 'type' => 'property', 'name' => 'NoMethod', ], ], 'expected' => null, ], 'property value is caught' => [ 'name' => 'ActualValueField', 'expectRequested' => [ [ 'type' => 'property', 'name' => 'ActualValueField', ], ], 'expected' => 'the value is here', ], 'not set and no method' => [ 'name' => 'NotSet', 'expectRequested' => [], 'expected' => null, ], ]; } #[DataProvider('provideGetComplex')] public function testGetComplex(string $name, string|array $expectRequested, ?string $expected): void { $fixture = new TestFixtureComplex(); $viewLayerData = new ViewLayerData($fixture); if ($expectRequested === BadMethodCallException::class) { $this->expectException(BadMethodCallException::class); } $value = $viewLayerData->$name; $this->assertSame($expectRequested, $fixture->getRequested()); $this->assertEquals($expected, $value); // Ensure value is being wrapped when not null if ($value !== null) { $this->assertInstanceOf(ViewLayerData::class, $value); } } public static function provideCall(): array { // Currently there is no distinction between trying to get a property or call a method from ViewLayerData // so the "get" examples should produce the same results when calling a method. $scenarios = static::provideGet(); foreach ($scenarios as &$scenario) { $scenario['args'] = []; } return [ ...$scenarios, 'basic field with args' => [ 'name' => 'SomeField', 'args' => ['abc', 123], 'throwException' => true, 'expected' => [ [ 'type' => 'method', 'name' => 'SomeField', 'args' => ['abc', 123], ], [ 'type' => 'method', 'name' => 'getSomeField', 'args' => ['abc', 123], ], [ 'type' => 'property', 'name' => 'SomeField', ], ], ], ]; } #[DataProvider('provideCall')] public function testCall(string $name, array $args, bool $throwException, array $expected): void { $fixture = new TestFixture(); $fixture->throwException = $throwException; $viewLayerData = new ViewLayerData($fixture); $value = $viewLayerData->$name(...$args); $this->assertSame($expected, $fixture->getRequested()); $this->assertNull($value); } public static function provideCallComplex(): array { // Currently there is no distinction between trying to get a property or call a method from ViewLayerData // so the "get" examples should produce the same results when calling a method. return static::provideGetComplex(); } #[DataProvider('provideCallComplex')] public function testCallComplex(string $name, string|array $expectRequested, ?string $expected): void { $fixture = new TestFixtureComplex(); $viewLayerData = new ViewLayerData($fixture); if ($expectRequested === BadMethodCallException::class) { $this->expectException(BadMethodCallException::class); } $value = $viewLayerData->$name(); $this->assertSame($expectRequested, $fixture->getRequested()); $this->assertEquals($expected, $value); // Ensure value is being wrapped when not null if ($value !== null) { $this->assertInstanceOf(ViewLayerData::class, $value); } } public static function provideToString(): array { return [ // These three all evaluate to ArrayList or ArrayData, which don't have templates to render 'empty array' => [ 'data' => [], 'expected' => MissingTemplateException::class, ], 'array with values' => [ 'data' => ['value1', 'value2'], 'expected' => MissingTemplateException::class, ], 'Class with no template' => [ // Note we won't check classes WITH templates because we're not testing the template engine here 'data' => new ArrayData(['Field1' => 'value1', 'Field2' => 'value2']), 'expected' => MissingTemplateException::class, ], 'string value' => [ 'data' => 'just a string', 'expected' => 'just a string', ], 'html gets escaped by default' => [ 'data' => 'HTML string', 'expected' => '<span>HTML string</span>', ], 'explicit HTML text not escaped' => [ 'data' => (new DBHTMLText())->setValue('HTML string'), 'expected' => 'HTML string', ], 'DBField' => [ 'data' => (new DBDate())->setValue('2024-03-24'), 'expected' => (new DBDate())->setValue('2024-03-24')->forTemplate(), ], '__toString() method' => [ 'data' => new StringableObject(), 'expected' => 'This is the string representation', ], 'forTemplate called from extension' => [ 'data' => new ExtensibleObject(), 'expected' => 'This text comes from the extension class', ], 'cannot convert this class to string' => [ 'data' => new CountableObject(), 'expected' => Error::class, ], ]; } #[DataProvider('provideToString')] public function testToString(mixed $data, string $expected): void { $viewLayerData = new ViewLayerData($data); if (is_a($expected, Throwable::class, true)) { $this->expectException($expected); } $this->assertSame($expected, (string) $viewLayerData); } public static function provideHasDataValue(): array { return [ 'empty array' => [ 'data' => [], 'name' => null, 'expected' => false, ], 'empty ArrayList' => [ 'data' => new ArrayList(), 'name' => null, 'expected' => false, ], 'empty ArrayData' => [ 'data' => new ArrayData(), 'name' => null, 'expected' => false, ], 'empty ArrayIterator' => [ 'data' => new ArrayIterator(), 'name' => null, 'expected' => false, ], 'empty ModelData' => [ 'data' => new ModelData(), 'name' => null, 'expected' => true, ], 'non-countable object' => [ 'data' => new ExtensibleObject(), 'name' => null, 'expected' => true, ], 'array with data' => [ 'data' => [1,2,3], 'name' => null, 'expected' => true, ], 'associative array' => [ 'data' => ['one' => 1, 'two' => 2], 'name' => null, 'expected' => true, ], 'ArrayList with data' => [ 'data' => new ArrayList([1,2,3]), 'name' => null, 'expected' => true, ], 'ArrayData with data' => [ 'data' => new ArrayData(['one' => 1, 'two' => 2]), 'name' => null, 'expected' => true, ], 'ArrayIterator with data' => [ 'data' => new ArrayIterator([1,2,3]), 'name' => null, 'expected' => true, ], 'ArrayData missing value' => [ 'data' => new ArrayData(['one' => 1, 'two' => 2]), 'name' => 'three', 'expected' => false, ], 'ArrayData with truthy value' => [ 'data' => new ArrayData(['one' => 1, 'two' => 2]), 'name' => 'one', 'expected' => true, ], 'ArrayData with null value' => [ 'data' => new ArrayData(['nullVal' => null, 'two' => 2]), 'name' => 'nullVal', 'expected' => false, ], 'ArrayData with falsy value' => [ 'data' => new ArrayData(['zero' => 0, 'two' => 2]), 'name' => 'zero', 'expected' => false, ], 'Empty string' => [ 'data' => '', 'name' => null, 'expected' => false, ], 'Truthy string' => [ 'data' => 'has a value', 'name' => null, 'expected' => true, ], 'Field on a string' => [ 'data' => 'has a value', 'name' => 'SomeField', 'expected' => false, ], ]; } #[DataProvider('provideHasDataValue')] public function testHasDataValue(mixed $data, ?string $name, bool $expected): void { $viewLayerData = new ViewLayerData($data); $this->assertSame($expected, $viewLayerData->hasDataValue($name)); } public static function provideGetRawDataValue(): array { $dbHtml = (new DBHTMLText())->setValue('Some html text'); // Note we're not checking the fetch order or passing args here - see testGet and testCall for that. return [ [ 'data' => ['MyField' => 'some value'], 'name' => 'MissingField', 'expected' => null, ], [ 'data' => ['MyField' => null], 'name' => 'MyField', 'expected' => null, ], [ 'data' => ['MyField' => 'some value'], 'name' => 'MyField', 'expected' => 'some value', ], [ 'data' => ['MyField' => 123], 'name' => 'MyField', 'expected' => 123, ], [ 'data' => ['MyField' => true], 'name' => 'MyField', 'expected' => true, ], [ 'data' => ['MyField' => false], 'name' => 'MyField', 'expected' => false, ], [ 'data' => ['MyField' => $dbHtml], 'name' => 'MyField', 'expected' => $dbHtml, ], [ 'data' => (new ArrayData(['MyField' => 1234]))->customise(new ArrayData(['MyField' => 'overridden value'])), 'name' => 'MyField', 'expected' => 'overridden value', ], [ 'data' => (new ArrayData(['MyField' => 1234]))->customise(new ArrayData(['FieldTwo' => 'checks here'])), 'name' => 'FieldTwo', 'expected' => 'checks here', ], [ 'data' => (new ArrayData(['MyField' => 1234]))->customise(new ArrayData(['FieldTwo' => 'not here'])), 'name' => 'MyField', 'expected' => 1234, ], ]; } #[DataProvider('provideGetRawDataValue')] public function testGetRawDataValue(mixed $data, string $name, mixed $expected): void { $viewLayerData = new ViewLayerData($data); $this->assertSame($expected, $viewLayerData->getRawDataValue($name)); } public static function provideGetRawDataValueType(): array { // The types aren't currently used, but are passed in so we can use them later // if we find the distinction useful. We should test they do what we expect // in the meantime. return [ [ 'type' => 'property', 'shouldThrow' => false, ], [ 'type' => 'method', 'shouldThrow' => false, ], [ 'type' => 'any', 'shouldThrow' => false, ], [ 'type' => 'constant', 'shouldThrow' => true, ], [ 'type' => 'randomtext', 'shouldThrow' => true, ], ]; } #[DataProvider('provideGetRawDataValueType')] public function testGetRawDataValueType(string $type, bool $shouldThrow): void { $viewLayerData = new ViewLayerData([]); if ($shouldThrow) { $this->expectException(InvalidArgumentException::class); } else { $this->expectNotToPerformAssertions(); } $viewLayerData->getRawDataValue('something', type: $type); } public function testCache(): void { $data = new ArrayData(['MyField' => 'some value']); $viewLayerData = new ViewLayerData($data); // No cache because we haven't fetched anything $this->assertNull($data->objCacheGet('MyField')); // Fetching the value caches it $viewLayerData->MyField; $this->assertSame('some value', $data->objCacheGet('MyField')); } public function testSpecialNames(): void { $data = new stdClass; $viewLayerData = new ViewLayerData($data); // Metadata values are available when there's nothing in the actual data $this->assertTrue(isset($viewLayerData->ClassName)); $this->assertTrue(isset($viewLayerData->Me)); $this->assertSame(stdClass::class, $viewLayerData->getRawDataValue('ClassName')->getValue()); $this->assertSame($data, $viewLayerData->getRawDataValue('Me')); // Metadata values are lower priority than real values in the actual data $data->ClassName = 'some other class'; $data->Me = 'something else'; $this->assertTrue(isset($viewLayerData->ClassName)); $this->assertTrue(isset($viewLayerData->Me)); $this->assertSame('some other class', $viewLayerData->getRawDataValue('ClassName')); $this->assertSame('something else', $viewLayerData->getRawDataValue('Me')); } }