Version 1.0.0
This commit is contained in:
22
.editorconfig
Normal file
22
.editorconfig
Normal file
@@ -0,0 +1,22 @@
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
|
||||
[*.md]
|
||||
trim_trailing_whitespace = false
|
||||
|
||||
[*.php]
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
|
||||
[*.yml]
|
||||
indent_size = 2
|
||||
|
||||
[*.json]
|
||||
indent_size = 2
|
||||
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
vendor/
|
||||
|
||||
.phpunit.cache/
|
||||
19
LICENSE
Normal file
19
LICENSE
Normal file
@@ -0,0 +1,19 @@
|
||||
Copyright 2025 Viktor S. <thinlineseverywhere@gmail.com>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of this software and associated documentation files (the “Software”),
|
||||
to deal in the Software without restriction, including without limitation
|
||||
the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
and/or sell copies of the Software, and to permit persons to whom the
|
||||
Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included
|
||||
in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
||||
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||
DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
|
||||
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
DEALINGS IN THE SOFTWARE.
|
||||
156
README.md
Normal file
156
README.md
Normal file
@@ -0,0 +1,156 @@
|
||||
# PHP DTO Library
|
||||
|
||||
Simple and lightweight PHP library for representing Data Transfer Objects (DTOs) with property value tracking.
|
||||
|
||||
## Features
|
||||
|
||||
- Easy to use abstract `Dto` class for creating data transfer objects
|
||||
- Property value tracking with `Property` wrapper class
|
||||
- Support for creating DTOs from arrays with automatic property name conversion
|
||||
- Track whether properties are initialized or not
|
||||
- Automatic conversion between kebab-case and camelCase
|
||||
- Type-safe with PHP 7.4+ and PHP 8.0+
|
||||
|
||||
## Installation
|
||||
|
||||
Install the library via Composer:
|
||||
|
||||
```bash
|
||||
composer require diffhead/php-dto
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
Run the test suite:
|
||||
|
||||
```bash
|
||||
composer test
|
||||
```
|
||||
|
||||
## Requirements
|
||||
|
||||
- PHP 7.4 or higher
|
||||
- `diffhead/php-interfaces` ^1.0
|
||||
- `jawira/case-converter` ^3.6
|
||||
|
||||
## Quick Start
|
||||
|
||||
Create a DTO class by extending the `Dto` abstract class:
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
use Diffhead\PHP\Dto\Dto;
|
||||
use Diffhead\PHP\Dto\Property;
|
||||
|
||||
/**
|
||||
* Properties inside the Dto class
|
||||
* should be camelCase named and
|
||||
* protected
|
||||
*/
|
||||
class UserCreate extends Dto
|
||||
{
|
||||
protected Property $firstName;
|
||||
protected Property $lastName;
|
||||
protected Property $email;
|
||||
}
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### Creating a DTO from Array
|
||||
|
||||
```php
|
||||
/**
|
||||
* But inside the source array you can use
|
||||
* camelCase, pascal_case, kebab-case, etc
|
||||
*/
|
||||
$data = [
|
||||
'first_name' => 'John',
|
||||
'lastName' => 'Doe',
|
||||
'email' => 'john@example.com'
|
||||
];
|
||||
|
||||
$user = UserCreate::fromArray($data);
|
||||
```
|
||||
|
||||
### Accessing Properties
|
||||
|
||||
Access properties using magic methods.
|
||||
Each property returns a `Property` object that
|
||||
tracks both the value and whether it exists:
|
||||
|
||||
```php
|
||||
/**
|
||||
* Get the property object
|
||||
*/
|
||||
$firstName = $user->firstName;
|
||||
|
||||
/**
|
||||
* Check property exists and set
|
||||
*/
|
||||
if ($firstName->exists()) {
|
||||
echo $firstName->value(); // Output: John
|
||||
}
|
||||
```
|
||||
|
||||
### Working with Property Objects
|
||||
|
||||
The `Property` class wraps values and tracks their existence:
|
||||
|
||||
```php
|
||||
$property = new \Diffhead\PHP\Dto\Property('1', true);
|
||||
|
||||
$property->value(); // Returns: '1'
|
||||
$property->exists(); // Returns: true
|
||||
$property->toInt(); // Returns: 1
|
||||
$property->toFloat(); // Returns: 1.0
|
||||
$proprety->toBool(); // Returns: true
|
||||
$property->toArray(); // Returns: ['1']
|
||||
$property->toString(); // Returns: '1'
|
||||
|
||||
$property = new \Diffhead\PHP\Dto\Property(null, false);
|
||||
|
||||
$property->value(); // Returns: null
|
||||
$property->exists(); // Returns: false
|
||||
```
|
||||
|
||||
### Retrieving Values
|
||||
|
||||
Get multiple property values at once:
|
||||
|
||||
```php
|
||||
/**
|
||||
* Get all properties values
|
||||
*
|
||||
* output: ['first_name' => 'John', 'lastName' => 'Doe', 'email' => 'john@example.com', 'age' => null]
|
||||
*/
|
||||
$values = $user->getValues(['first_name', 'lastName', 'email', 'age']);
|
||||
|
||||
/**
|
||||
* Get only existing properties values
|
||||
*
|
||||
* output: ['first-name' => 'John', 'last.name' => 'Doe', 'email' => 'john@example.com']
|
||||
*/
|
||||
$values = $user->getValues(['first-name', 'last.name', 'email', 'age'], true);
|
||||
```
|
||||
|
||||
## Property Name Conversion
|
||||
|
||||
The library automatically handles
|
||||
class property name conversion:
|
||||
|
||||
- **dot.case** (e.g., `first.name`) → **camelCase** (e.g., `firstName`)
|
||||
- **kebab-case** (e.g., `first-name`) → **camelCase** (e.g., `firstName`)
|
||||
- **snake_case** (e.g., `first_name`) → **camelCase** (e.g., `firstName`)
|
||||
|
||||
This makes it easy to work with API responses and
|
||||
form data that might use different naming conventions.
|
||||
|
||||
But `Diffhead\PHP\Dto\Dto::getValues` method
|
||||
returns raw array keys as they was been passed.
|
||||
|
||||
## License
|
||||
|
||||
This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details.
|
||||
|
||||
35
composer.json
Normal file
35
composer.json
Normal file
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"name": "diffhead/php-dto",
|
||||
"description": "",
|
||||
"type": "library",
|
||||
"license": "MIT",
|
||||
"version": "1.0.0",
|
||||
"keywords": [
|
||||
"php", "data interaction", "data transfer object",
|
||||
"dto", "library", "package", "php8"
|
||||
],
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Diffhead\\PHP\\Dto\\": "src/",
|
||||
"Diffhead\\PHP\\Dto\\Tests\\": "tests/"
|
||||
}
|
||||
},
|
||||
"require": {
|
||||
"php": "^7.4 || ^8.0",
|
||||
"diffhead/php-interfaces": "^1.0",
|
||||
"jawira/case-converter": "^3.6"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "^9.0"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "phpunit"
|
||||
},
|
||||
"minimum-stability": "stable",
|
||||
"authors": [
|
||||
{
|
||||
"name": "Viktor S.",
|
||||
"email": "thinlineseverywhere@gmail.com"
|
||||
}
|
||||
]
|
||||
}
|
||||
1935
composer.lock
generated
Normal file
1935
composer.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
17
phpunit.xml
Normal file
17
phpunit.xml
Normal file
@@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
|
||||
bootstrap="vendor/autoload.php"
|
||||
executionOrder="depends,defects"
|
||||
beStrictAboutOutputDuringTests="true"
|
||||
failOnRisky="true"
|
||||
failOnWarning="true"
|
||||
testdox="true"
|
||||
colors="true"
|
||||
cacheResult="false">
|
||||
<testsuites>
|
||||
<testsuite name="Unit">
|
||||
<directory>tests</directory>
|
||||
</testsuite>
|
||||
</testsuites>
|
||||
</phpunit>
|
||||
101
src/Dto.php
Normal file
101
src/Dto.php
Normal file
@@ -0,0 +1,101 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Diffhead\PHP\Dto;
|
||||
|
||||
use Diffhead\PHP\Interfaces\Object\ArrayInstantiable;
|
||||
use Jawira\CaseConverter\Convert;
|
||||
use TypeError;
|
||||
|
||||
abstract class Dto implements ArrayInstantiable
|
||||
{
|
||||
/**
|
||||
* @var array<int,string>
|
||||
*/
|
||||
private array $initializedProperties = [];
|
||||
|
||||
public static function fromArray(array $data): static
|
||||
{
|
||||
$dto = new static();
|
||||
|
||||
foreach ($data as $prop => $value) {
|
||||
if (str_contains($prop, '-')) {
|
||||
$prop = str_replace('-', '_', $prop);
|
||||
}
|
||||
|
||||
$propConverter = new Convert($prop);
|
||||
$prop = $propConverter->toCamel();
|
||||
|
||||
if (property_exists($dto, $prop)) {
|
||||
$dto->setProperty($prop, $value);
|
||||
}
|
||||
}
|
||||
|
||||
return $dto;
|
||||
}
|
||||
|
||||
public function __get(string $name): Property
|
||||
{
|
||||
return $this->getProperty($name);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int,string> $properties
|
||||
* @param bool $existingOnly
|
||||
*
|
||||
* @return array<string,mixed>
|
||||
*/
|
||||
public function getValues(array $properties, bool $existingOnly = false): array
|
||||
{
|
||||
$values = [];
|
||||
|
||||
foreach ($properties as $property) {
|
||||
if (! is_string($property)) {
|
||||
throw new TypeError(
|
||||
sprintf('Expected string as prop name. Passed "%s"', $property)
|
||||
);
|
||||
}
|
||||
|
||||
$propertyNotExisting = ! $this->getProperty($property)->exists();
|
||||
|
||||
if ($existingOnly && $propertyNotExisting) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$values[$property] = $this->getProperty($property)->value();
|
||||
}
|
||||
|
||||
return $values;
|
||||
}
|
||||
|
||||
final protected function setProperty(string $propName, mixed $value): void
|
||||
{
|
||||
$this->$propName = new Property($value);
|
||||
$this->setPropertyInitialized($propName);
|
||||
}
|
||||
|
||||
final protected function getProperty(string $name): Property
|
||||
{
|
||||
$nameConvert = new Convert($name);
|
||||
|
||||
$propExists = property_exists($this, $name)
|
||||
|| property_exists($this, $name = $nameConvert->toCamel());
|
||||
|
||||
if ($propExists && $this->isPropertyInitialized($name)) {
|
||||
return $this->$name;
|
||||
}
|
||||
|
||||
return new Property(null, false);
|
||||
}
|
||||
|
||||
private function setPropertyInitialized(string $propName): void
|
||||
{
|
||||
$this->initializedProperties[] = $propName;
|
||||
}
|
||||
|
||||
private function isPropertyInitialized(string $propName): bool
|
||||
{
|
||||
return in_array($propName, $this->initializedProperties);
|
||||
}
|
||||
}
|
||||
69
src/Property.php
Normal file
69
src/Property.php
Normal file
@@ -0,0 +1,69 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Diffhead\PHP\Dto;
|
||||
|
||||
/**
|
||||
* @template ValueType
|
||||
*/
|
||||
class Property
|
||||
{
|
||||
/**
|
||||
* @var ValueType
|
||||
*/
|
||||
private $value;
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
private bool $exists;
|
||||
|
||||
/**
|
||||
* @param ValueType $value
|
||||
* @param bool $exists
|
||||
*/
|
||||
public function __construct($value, bool $exists = true)
|
||||
{
|
||||
$this->value = $value;
|
||||
$this->exists = $exists;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return ValueType
|
||||
*/
|
||||
public function value()
|
||||
{
|
||||
return $this->value;
|
||||
}
|
||||
|
||||
public function toBool(): bool
|
||||
{
|
||||
return (bool) $this->value;
|
||||
}
|
||||
|
||||
public function toInt(): int
|
||||
{
|
||||
return (int) $this->value;
|
||||
}
|
||||
|
||||
public function toFloat(): float
|
||||
{
|
||||
return (float) $this->value;
|
||||
}
|
||||
|
||||
public function toString(): string
|
||||
{
|
||||
return (string) $this->value;
|
||||
}
|
||||
|
||||
public function toArray(): array
|
||||
{
|
||||
return (array) $this->value;
|
||||
}
|
||||
|
||||
public function exists(): bool
|
||||
{
|
||||
return $this->exists;
|
||||
}
|
||||
}
|
||||
176
tests/DtoTest.php
Normal file
176
tests/DtoTest.php
Normal file
@@ -0,0 +1,176 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Diffhead\PHP\Dto\Tests;
|
||||
|
||||
use Diffhead\PHP\Dto\Dto;
|
||||
use Diffhead\PHP\Dto\Property;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class DtoTest extends TestCase
|
||||
{
|
||||
public function testProperlyInitializes(): void
|
||||
{
|
||||
$dto = DtoExample::fromArray([
|
||||
'name' => 'John Doe',
|
||||
'age' => 30,
|
||||
]);
|
||||
|
||||
$this->assertSame('John Doe', $dto->name->value());
|
||||
$this->assertTrue($dto->name->exists());
|
||||
$this->assertSame(30, $dto->age->value());
|
||||
$this->assertTrue($dto->age->exists());
|
||||
$this->assertNull($dto->homeAddress->value());
|
||||
$this->assertFalse($dto->homeAddress->exists());
|
||||
}
|
||||
|
||||
public function testInitializationFromKebabCase(): void
|
||||
{
|
||||
$dto = DtoExample::fromArray([
|
||||
'name' => 'Jane Doe',
|
||||
'age' => 25,
|
||||
'home-address' => '39 Vicar Lane, Sandilands, United Kingdom',
|
||||
]);
|
||||
|
||||
$this->assertSame('Jane Doe', $dto->name->value());
|
||||
$this->assertTrue($dto->name->exists());
|
||||
$this->assertSame(25, $dto->age->value());
|
||||
$this->assertTrue($dto->age->exists());
|
||||
$this->assertSame('39 Vicar Lane, Sandilands, United Kingdom', $dto->homeAddress->value());
|
||||
$this->assertTrue($dto->homeAddress->exists());
|
||||
}
|
||||
|
||||
public function testInitializationFromPascalCase(): void
|
||||
{
|
||||
$dto = DtoExample::fromArray([
|
||||
'name' => 'Alice',
|
||||
'age' => 28,
|
||||
'home_address' => '123 Main St, Anytown, USA',
|
||||
]);
|
||||
|
||||
$this->assertSame('Alice', $dto->name->value());
|
||||
$this->assertTrue($dto->name->exists());
|
||||
$this->assertSame(28, $dto->age->value());
|
||||
$this->assertTrue($dto->age->exists());
|
||||
$this->assertSame('123 Main St, Anytown, USA', $dto->homeAddress->value());
|
||||
$this->assertTrue($dto->homeAddress->exists());
|
||||
}
|
||||
|
||||
public function testInitializationFromCamelCase(): void
|
||||
{
|
||||
$dto = DtoExample::fromArray([
|
||||
'name' => 'Bob',
|
||||
'age' => 35,
|
||||
'homeAddress' => '456 Elm St, Othertown, USA',
|
||||
]);
|
||||
|
||||
$this->assertSame('Bob', $dto->name->value());
|
||||
$this->assertTrue($dto->name->exists());
|
||||
$this->assertSame(35, $dto->age->value());
|
||||
$this->assertTrue($dto->age->exists());
|
||||
$this->assertSame('456 Elm St, Othertown, USA', $dto->homeAddress->value());
|
||||
$this->assertTrue($dto->homeAddress->exists());
|
||||
}
|
||||
|
||||
public function testGetStrictExistingPropertiesValues(): void
|
||||
{
|
||||
$dto = DtoExample::fromArray([
|
||||
'name' => 'Charlie',
|
||||
'age' => 40,
|
||||
]);
|
||||
|
||||
$values = $dto->getValues(['name', 'age', 'home_address'], true);
|
||||
|
||||
$this->assertArrayHasKey('name', $values);
|
||||
$this->assertSame('Charlie', $values['name']);
|
||||
$this->assertArrayHasKey('age', $values);
|
||||
$this->assertSame(40, $values['age']);
|
||||
$this->assertArrayNotHasKey('home_address', $values);
|
||||
}
|
||||
|
||||
public function testGetNonStrictExistingPropertiesValues(): void
|
||||
{
|
||||
$dto = DtoExample::fromArray([
|
||||
'name' => 'Diana',
|
||||
]);
|
||||
|
||||
$values = $dto->getValues(['name', 'age', 'home_address'], false);
|
||||
|
||||
$this->assertArrayHasKey('name', $values);
|
||||
$this->assertSame('Diana', $values['name']);
|
||||
$this->assertArrayHasKey('age', $values);
|
||||
$this->assertNull($values['age']);
|
||||
$this->assertArrayHasKey('home_address', $values);
|
||||
$this->assertNull($values['home_address']);
|
||||
}
|
||||
|
||||
public function testGetStrictNonExistingPropertiesValues(): void
|
||||
{
|
||||
$dto = DtoExample::fromArray([]);
|
||||
|
||||
$values = $dto->getValues(['name', 'age'], true);
|
||||
|
||||
$this->assertEmpty($values);
|
||||
}
|
||||
|
||||
public function testGetNonStrictNonExistingPropertiesValues(): void
|
||||
{
|
||||
$dto = DtoExample::fromArray([]);
|
||||
|
||||
$values = $dto->getValues(['name', 'age'], false);
|
||||
|
||||
$this->assertArrayHasKey('name', $values);
|
||||
$this->assertNull($values['name']);
|
||||
$this->assertArrayHasKey('age', $values);
|
||||
$this->assertNull($values['age']);
|
||||
}
|
||||
|
||||
public function testGetKebabCasePropertiesValues(): void
|
||||
{
|
||||
$dto = DtoExample::fromArray([
|
||||
'name' => 'Eve',
|
||||
'age' => 29,
|
||||
'home-address' => '789 Oak St, Sometown, USA',
|
||||
]);
|
||||
|
||||
$values = $dto->getValues(['name', 'age', 'home-address'], false);
|
||||
|
||||
$this->assertArrayHasKey('name', $values);
|
||||
$this->assertSame('Eve', $values['name']);
|
||||
$this->assertArrayHasKey('age', $values);
|
||||
$this->assertSame(29, $values['age']);
|
||||
$this->assertArrayHasKey('home-address', $values);
|
||||
$this->assertSame('789 Oak St, Sometown, USA', $values['home-address']);
|
||||
}
|
||||
|
||||
public function testGetCamelCasePropertiesValues(): void
|
||||
{
|
||||
$dto = DtoExample::fromArray([
|
||||
'name' => 'Frank',
|
||||
'age' => 33,
|
||||
'homeAddress' => '101 Pine St, Newcity, USA',
|
||||
]);
|
||||
|
||||
$values = $dto->getValues(['name', 'age', 'homeAddress'], false);
|
||||
|
||||
$this->assertArrayHasKey('name', $values);
|
||||
$this->assertSame('Frank', $values['name']);
|
||||
$this->assertArrayHasKey('age', $values);
|
||||
$this->assertSame(33, $values['age']);
|
||||
$this->assertArrayHasKey('homeAddress', $values);
|
||||
$this->assertSame('101 Pine St, Newcity, USA', $values['homeAddress']);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @property \Diffhead\PHP\Dto\Property<string> $name
|
||||
* @property \Diffhead\PHP\Dto\Property<int> $age
|
||||
* @property \Diffhead\PHP\Dto\Property<string> $homeAddress
|
||||
*/
|
||||
class DtoExample extends Dto
|
||||
{
|
||||
protected Property $name;
|
||||
protected Property $age;
|
||||
protected Property $homeAddress;
|
||||
}
|
||||
35
tests/PropertyTest.php
Normal file
35
tests/PropertyTest.php
Normal file
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Diffhead\PHP\Dto\Tests;
|
||||
|
||||
use Diffhead\PHP\Dto\Property;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class PropertyTest extends TestCase
|
||||
{
|
||||
public function testProperlyInitializes(): void
|
||||
{
|
||||
$property = new Property(131, true);
|
||||
|
||||
$this->assertSame(131, $property->value());
|
||||
$this->assertTrue($property->exists());
|
||||
|
||||
$property = new Property(null, false);
|
||||
|
||||
$this->assertNull($property->value());
|
||||
$this->assertFalse($property->exists());
|
||||
}
|
||||
|
||||
public function testValueCasting(): void
|
||||
{
|
||||
$property = new Property('1', true);
|
||||
|
||||
$this->assertSame('1', $property->toString());
|
||||
$this->assertSame(1, $property->toInt());
|
||||
$this->assertSame(1.0, $property->toFloat());
|
||||
$this->assertTrue($property->toBool());
|
||||
$this->assertSame(['1'], $property->toArray());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user