diff --git a/src/IsMigrationDataTransferObject.php b/src/IsMigrationDataTransferObject.php new file mode 100644 index 0000000..da674a7 --- /dev/null +++ b/src/IsMigrationDataTransferObject.php @@ -0,0 +1,191 @@ +newInstanceWithoutConstructor(); + } + + /** + * @param array|DataTransferObject $data + * @param array $map Key-value map, where key is key from $data and value is mapped field from ->fields + * + * @return static + * + * @deprecated + * + * Create instance of self and fill it with given data + */ + public static function from($data, array $map = []) + { + if ($data instanceof static) { + return clone $data; + } + + // Replace old keys with new ones. + foreach ($map as $oldKey => $newKey) { + if (Arr::has($data, $oldKey)) { + Arr::set($data, $newKey, Arr::pull($data, $oldKey)); + } + } + + return static::create()->fill($data); + } + + /** + * @return static + * + * @deprecated + * + * Fill with given data + */ + public function fill(array $data) + { + foreach ($data as $key => $value) { + if (!$this->doesFieldExist($key)) { + continue; + } + + $this->{'set' . $this->keyToMethod($key)}($value); + } + + return $this; + } + + /** + * @deprecated + */ + public function all(): array + { + return collect(get_object_vars($this)) + ->mapWithKeys(fn (mixed $value, string $key) => [$this->propertyToKey($key) => $value]) + ->all(); + } + + /** + * @deprecated + */ + public function toArray(): array + { + return array_map(static function ($value) { + if (is_array($value)) { + return array_map(static fn ($value) => $value instanceof Arrayable ? $value->toArray() : $value, $value); + } + + return $value instanceof Arrayable ? $value->toArray() : $value; + }, $this->all()); + } + + /** + * @deprecated + */ + public function jsonSerialize(): array + { + return $this->all(); + } + + /** + * Transform data key from property name to stored key name. (FieldName (from setFieldName, getFieldName) -> field_name). + */ + abstract protected function propertyToKey(string $key): string; + + /** + * Transform data key from stored key name to property name. (field_name -> FieldName (from setFieldName, getFieldName)). + */ + abstract protected function keyToProperty(string $key): string; + + /** + * Internal method for getters. + */ + protected function get(string $key) + { + $this->assertFieldExists($key); + + return $this->{$this->keyToProperty($key)} ?? null; + } + + /** + * Internal method for has* methods. + */ + protected function has(string $key): bool + { + $this->assertFieldExists($key); + + return (new ReflectionProperty(static::class, $this->keyToProperty($key)))->isInitialized($this); + } + + /** + * Internal method for setters. + * + * @return static + */ + protected function set(string $key, $data): self + { + $this->assertFieldExists($key); + (fn () => $this->{$this->keyToProperty($key)} = $data)->call($this); + + return $this; + } + + /** + * Asserts that a given field key is allowed to exist. + */ + protected function assertFieldExists(string $key): void + { + Assert::true($this->doesFieldExist($key), "Key {$key} doesn't exist"); + } + + /** + * Whether given field can be filled. + */ + protected function doesFieldExist(string $key): bool + { + return property_exists($this, $this->keyToProperty($key)); + } + + protected function methodToKey(string $key): string + { + return $this->propertyToKey(lcfirst($key)); + } + + protected function keyToMethod(string $key): string + { + return ucfirst($this->keyToProperty($key)); + } + + /** + * Forwards ->getFieldName(), ->setFieldName($value) and ->hasFieldName() to reduce boilerplate. + */ + public function __call($method, $arguments) + { + if (Str::startsWith($method, 'set')) { + return $this->set($this->methodToKey(mb_substr($method, 3)), ...$arguments); + } + + if (Str::startsWith($method, 'get')) { + return $this->get($this->methodToKey(mb_substr($method, 3))); + } + + if (Str::startsWith($method, 'has')) { + return $this->has($this->methodToKey(mb_substr($method, 3))); + } + + static::throwBadMethodCallException($method); + } +} diff --git a/src/MigrationDataTransferObject.php b/src/MigrationDataTransferObject.php new file mode 100644 index 0000000..db9144f --- /dev/null +++ b/src/MigrationDataTransferObject.php @@ -0,0 +1,22 @@ +setName('nested'), TestEnum::$ONE); + + self::assertSame([ + 'name' => 'test', + 'dto' => $stub, + 'enum' => TestEnum::$ONE, + ], $dto->all()); + self::assertSame([ + 'name' => 'test', + 'dto' => $stub, + 'enum' => TestEnum::$ONE, + ], $dto->jsonSerialize()); + self::assertSame([ + 'name' => 'test', + 'dto' => [ + 'name' => 'nested', + ], + 'enum' => TestEnum::$ONE, + ], $dto->toArray()); + self::assertSame('test', $dto->name); + self::assertSame('test', $dto->getName()); + self::assertTrue($dto->hasName()); + $dto->setName('test2'); + self::assertSame('test2', $dto->getName()); + self::assertSame($stub, $dto->getDto()); + self::assertTrue($dto->hasDto()); + self::assertSame(TestEnum::$ONE, $dto->getEnum()); + self::assertTrue($dto->hasEnum()); + + $this->expectExceptionMessage('Cannot modify readonly property Tests\\Stubs\\MigrationStubDTO::$enum'); + + $dto->setEnum(TestEnum::$ONE); + } + + public function testLegacyConstruction(): void + { + $dto = MigrationStubDTO::create(); + + self::assertSame([], $dto->all()); + self::assertSame([], $dto->jsonSerialize()); + self::assertSame([], $dto->toArray()); + + self::assertNull($dto->getName()); + self::assertFalse($dto->hasName()); + self::assertNull($dto->getDto()); + self::assertFalse($dto->hasDto()); + self::assertNull($dto->getEnum()); + self::assertFalse($dto->hasEnum()); + + $dto->setName('test') + ->setDto($stub = StubDTO::create()->setName('nested')) + ->setEnum(TestEnum::$ONE); + + self::assertSame([ + 'name' => 'test', + 'dto' => $stub, + 'enum' => TestEnum::$ONE, + ], $dto->all()); + self::assertSame([ + 'name' => 'test', + 'dto' => $stub, + 'enum' => TestEnum::$ONE, + ], $dto->jsonSerialize()); + self::assertSame([ + 'name' => 'test', + 'dto' => [ + 'name' => 'nested', + ], + 'enum' => TestEnum::$ONE, + ], $dto->toArray()); + self::assertSame('test', $dto->name); + self::assertSame('test', $dto->getName()); + self::assertTrue($dto->hasName()); + $dto->setName('test2'); + self::assertSame('test2', $dto->getName()); + self::assertSame($stub, $dto->getDto()); + self::assertTrue($dto->hasDto()); + self::assertSame(TestEnum::$ONE, $dto->getEnum()); + self::assertTrue($dto->hasEnum()); + + $this->expectExceptionMessage('Cannot modify readonly property Tests\\Stubs\\MigrationStubDTO::$enum'); + + $dto->setEnum(TestEnum::$ONE); + } + + public function testLegacyConstructionFromArray(): void + { + $dto = MigrationStubDTO::from([ + 'name' => 'test', + 'dto' => $stub = StubDTO::create()->setName('nested'), + 'enum' => TestEnum::$ONE, + ]); + + self::assertSame([ + 'name' => 'test', + 'dto' => $stub, + 'enum' => TestEnum::$ONE, + ], $dto->all()); + self::assertSame([ + 'name' => 'test', + 'dto' => $stub, + 'enum' => TestEnum::$ONE, + ], $dto->jsonSerialize()); + self::assertSame([ + 'name' => 'test', + 'dto' => [ + 'name' => 'nested', + ], + 'enum' => TestEnum::$ONE, + ], $dto->toArray()); + self::assertSame('test', $dto->name); + self::assertSame('test', $dto->getName()); + self::assertTrue($dto->hasName()); + $dto->setName('test2'); + self::assertSame('test2', $dto->getName()); + self::assertSame($stub, $dto->getDto()); + self::assertTrue($dto->hasDto()); + self::assertSame(TestEnum::$ONE, $dto->getEnum()); + self::assertTrue($dto->hasEnum()); + + $this->expectExceptionMessage('Cannot modify readonly property Tests\\Stubs\\MigrationStubDTO::$enum'); + + $dto->setEnum(TestEnum::$ONE); + } +} diff --git a/tests/Stubs/MigrationStubDTO.php b/tests/Stubs/MigrationStubDTO.php new file mode 100644 index 0000000..546294f --- /dev/null +++ b/tests/Stubs/MigrationStubDTO.php @@ -0,0 +1,14 @@ +