├── .github ├── actions │ └── install │ │ └── action.yml └── workflows │ └── tests.yml ├── composer.json ├── phpcs.xml ├── phpstan.neon ├── phpunit.xml └── src ├── CollectionValueObject.php ├── DateTimeValueObject.php ├── Doctrine ├── Types.php └── Types │ ├── CollectionValueObjectType.php │ ├── DateTimeValueObjectType.php │ ├── IntegerValueObjectType.php │ ├── ObjectValueObjectType.php │ ├── StringValueObjectType.php │ └── ValueObjectType.php ├── IntegerValueObject.php ├── ObjectValueObject.php └── StringValueObject.php /.github/actions/install/action.yml: -------------------------------------------------------------------------------- 1 | name: Prepare env & install dependencies 2 | 3 | on: 4 | workflow_call: 5 | inputs: 6 | php-version: 7 | required: true 8 | type: string 9 | dbal-version: 10 | required: true 11 | type: string 12 | orm-version: 13 | required: true 14 | type: string 15 | coverage-mode: 16 | required: false 17 | type: string 18 | default: 'none' 19 | secrets: 20 | token: 21 | required: true 22 | 23 | runs: 24 | using: "composite" 25 | steps: 26 | - name: "Setup PHP" 27 | uses: shivammathur/setup-php@v2 28 | with: 29 | coverage: ${{ inputs.coverage-mode }} 30 | php-version: ${{ inputs.php-version }} 31 | - name: "Install dependencies with composer" 32 | shell: bash 33 | run: | 34 | composer require --quiet --no-update "doctrine/orm:${{ inputs.orm-version }}" 35 | composer require --quiet --no-update "doctrine/dbal:${{ inputs.dbal-version }}" 36 | composer update --no-interaction --no-progress --no-suggest 37 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: "Tests" 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: 7 | - "main" 8 | 9 | jobs: 10 | phpunit: 11 | name: "PHPUnit" 12 | runs-on: ubuntu-latest 13 | strategy: 14 | matrix: 15 | include: 16 | - php-version: '8.0' 17 | dbal-version: '^3.8' 18 | orm-version: '^2.7' 19 | - php-version: '8.2' 20 | dbal-version: '^4.0' 21 | orm-version: '^3.1' 22 | steps: 23 | - name: "Checkout" 24 | uses: actions/checkout@v3 25 | - name: "Setup env & install dependencies" 26 | uses: ./.github/actions/install 27 | with: 28 | php-version: ${{ matrix.php-version }} 29 | dbal-version: ${{ matrix.dbal-version }} 30 | orm-version: ${{ matrix.orm-version }} 31 | - name: "Run tests with phpunit/phpunit" 32 | run: vendor/bin/phpunit 33 | 34 | phpstan: 35 | name: "PhpStan" 36 | runs-on: ubuntu-latest 37 | steps: 38 | - name: "Checkout" 39 | uses: actions/checkout@v3 40 | - name: "Setup env & install dependencies" 41 | uses: ./.github/actions/install 42 | with: 43 | php-version: '8.2' 44 | dbal-version: '^4.0' 45 | orm-version: '^3.1' 46 | - name: "Run static analyzis with phpstan/phpstan" 47 | run: vendor/bin/phpstan analyze 48 | 49 | checkstyke: 50 | name: "Checkstyle" 51 | runs-on: ubuntu-latest 52 | steps: 53 | - name: "Checkout" 54 | uses: actions/checkout@v3 55 | - name: "Setup env & install dependencies" 56 | uses: ./.github/actions/install 57 | with: 58 | php-version: '8.2' 59 | dbal-version: '^4.0' 60 | orm-version: '^3.1' 61 | - name: "Run checkstyle with squizlabs/php_codesniffer" 62 | run: vendor/bin/phpcs 63 | 64 | codecov: 65 | name: "Code coverage" 66 | runs-on: ubuntu-latest 67 | steps: 68 | - name: "Checkout" 69 | uses: actions/checkout@v3 70 | - name: "Setup env & install dependencies" 71 | uses: ./.github/actions/install 72 | with: 73 | php-version: '8.2' 74 | dbal-version: '^4.0' 75 | orm-version: '^3.1' 76 | - name: "Run tests with phpunit/phpunit" 77 | env: 78 | CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} 79 | run: | 80 | vendor/bin/phpunit --coverage-clover coverage.xml 81 | - name: "Upload coverage to Codecov" 82 | uses: codecov/codecov-action@v1 83 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "yokai/doctrine-value-object", 3 | "description": "Value Objects for Doctrine ORM simplified", 4 | "keywords": ["doctrine", "orm", "value object"], 5 | "license": "MIT", 6 | "authors": [ 7 | { 8 | "name": "Yann Eugoné", 9 | "email": "eugone.yann@gmail.com" 10 | } 11 | ], 12 | "require": { 13 | "php": "^8.0", 14 | "doctrine/dbal": "^3.8|^4.0", 15 | "doctrine/orm": "^2.7|^3.1", 16 | "webmozart/assert": "^1.11" 17 | }, 18 | "autoload": { 19 | "psr-4": { 20 | "Yokai\\DoctrineValueObject\\": "src/" 21 | } 22 | }, 23 | "require-dev": { 24 | "phpunit/phpunit": "^9.4", 25 | "phpstan/phpstan": "^1.11", 26 | "squizlabs/php_codesniffer": "^3.9", 27 | "symfony/cache": "^5.4|^6.0|^7.0" 28 | }, 29 | "autoload-dev": { 30 | "psr-4": { 31 | "Yokai\\DoctrineValueObject\\Tests\\": "tests/" 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /phpcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | src/ 14 | tests/ 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /phpstan.neon: -------------------------------------------------------------------------------- 1 | parameters: 2 | level: max 3 | paths: 4 | - src/ 5 | - tests/ 6 | 7 | ignoreErrors: 8 | - identifier: missingType.iterableValue 9 | - identifier: missingType.generics 10 | # callable with array syntax is not inferred 11 | - '#.*function call_user_func expects callable.*#' 12 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 11 | 12 | ./tests 13 | 14 | 15 | 16 | 17 | 18 | ./src 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/CollectionValueObject.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | interface CollectionValueObject extends Countable, IteratorAggregate 16 | { 17 | public static function fromValue(array $value): static; 18 | 19 | public function toValue(): array; 20 | } 21 | -------------------------------------------------------------------------------- /src/DateTimeValueObject.php: -------------------------------------------------------------------------------- 1 | $types 24 | */ 25 | public function __construct(array $types) 26 | { 27 | $this->types = $types; 28 | } 29 | 30 | public function register(TypeRegistry $types): void 31 | { 32 | foreach ($this->types as $doctrineTypeName => $valueObjectType) { 33 | if (!$types->has($doctrineTypeName)) { 34 | $types->register($doctrineTypeName, $this->type($valueObjectType, $doctrineTypeName)); 35 | } 36 | } 37 | } 38 | 39 | private function type(string $valueObjectType, string $doctrineTypeName): Type 40 | { 41 | foreach (self::DOCTRINE_TYPES as $doctrineTypeClass) { 42 | $supportValueObjectType = \call_user_func([$doctrineTypeClass, 'getSupportedValueObjectType']); 43 | if (\is_a($valueObjectType, $supportValueObjectType, true)) { 44 | return \call_user_func([$doctrineTypeClass, 'create'], $valueObjectType, $doctrineTypeName); 45 | } 46 | } 47 | 48 | throw new \InvalidArgumentException( 49 | \sprintf( 50 | 'Value object named "%s" associated to type "%s" has no related doctrine type', 51 | $doctrineTypeName, 52 | $valueObjectType 53 | ) 54 | ); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Doctrine/Types/CollectionValueObjectType.php: -------------------------------------------------------------------------------- 1 | getInheritedType()->getSQLDeclaration($column, $platform); 21 | } 22 | 23 | /** 24 | * @inheritdoc 25 | */ 26 | public static function getSupportedValueObjectType(): string 27 | { 28 | return CollectionValueObject::class; 29 | } 30 | 31 | /** 32 | * @inheritdoc 33 | */ 34 | public function convertToDatabaseValue(mixed $value, AbstractPlatform $platform): ?string 35 | { 36 | if ($value === null) { 37 | return null; 38 | } 39 | 40 | Assert::isInstanceOf($value, $this->class); 41 | /** @var CollectionValueObject $value */ 42 | 43 | return $this->getInheritedType()->convertToDatabaseValue($value->toValue(), $platform); 44 | } 45 | 46 | /** 47 | * @inheritdoc 48 | */ 49 | public function convertToPHPValue(mixed $value, AbstractPlatform $platform): ?CollectionValueObject 50 | { 51 | $value = $this->getInheritedType()->convertToPHPValue($value, $platform); 52 | 53 | if ($value === null) { 54 | return null; 55 | } 56 | 57 | Assert::isArray($value); 58 | 59 | /** @var CollectionValueObject $collection */ 60 | $collection = \call_user_func([$this->class, 'fromValue'], $value); 61 | Assert::isInstanceOf($collection, $this->class); 62 | 63 | return $collection; 64 | } 65 | 66 | private function getInheritedType(): JsonType 67 | { 68 | /** @var JsonType $type */ 69 | $type = $this->getType(Types::JSON); 70 | 71 | return $type; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/Doctrine/Types/DateTimeValueObjectType.php: -------------------------------------------------------------------------------- 1 | getInheritedType()->getSQLDeclaration($column, $platform); 22 | } 23 | 24 | /** 25 | * @inheritdoc 26 | */ 27 | public static function getSupportedValueObjectType(): string 28 | { 29 | return DateTimeValueObject::class; 30 | } 31 | 32 | /** 33 | * @inheritdoc 34 | */ 35 | public function convertToDatabaseValue(mixed $value, AbstractPlatform $platform): ?string 36 | { 37 | if ($value === null) { 38 | return null; 39 | } 40 | 41 | Assert::isInstanceOf($value, $this->class); 42 | /** @var DateTimeValueObject $value */ 43 | 44 | return $this->getInheritedType()->convertToDatabaseValue($value->toValue(), $platform); 45 | } 46 | 47 | /** 48 | * @inheritdoc 49 | */ 50 | public function convertToPHPValue(mixed $value, AbstractPlatform $platform): ?DateTimeValueObject 51 | { 52 | $value = $this->getInheritedType()->convertToPHPValue($value, $platform); 53 | 54 | if ($value === null) { 55 | return null; 56 | } 57 | 58 | Assert::isInstanceOf($value, DateTimeImmutable::class); 59 | 60 | /** @var DateTimeValueObject $object */ 61 | $object = \call_user_func([$this->class, 'fromValue'], $value); 62 | Assert::isInstanceOf($object, $this->class); 63 | 64 | return $object; 65 | } 66 | 67 | private function getInheritedType(): DateTimeTzImmutableType 68 | { 69 | /** @var DateTimeTzImmutableType $type */ 70 | $type = $this->getType(Types::DATETIMETZ_IMMUTABLE); 71 | 72 | return $type; 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/Doctrine/Types/IntegerValueObjectType.php: -------------------------------------------------------------------------------- 1 | getInheritedType()->getSQLDeclaration($column, $platform); 21 | } 22 | 23 | /** 24 | * @inheritdoc 25 | */ 26 | public static function getSupportedValueObjectType(): string 27 | { 28 | return IntegerValueObject::class; 29 | } 30 | 31 | /** 32 | * @inheritdoc 33 | */ 34 | public function convertToDatabaseValue(mixed $value, AbstractPlatform $platform): ?int 35 | { 36 | if ($value === null) { 37 | return null; 38 | } 39 | 40 | Assert::isInstanceOf($value, $this->class); 41 | /** @var IntegerValueObject $value */ 42 | 43 | return $this->getInheritedType()->convertToPHPValue($value->toValue(), $platform); 44 | } 45 | 46 | /** 47 | * @inheritdoc 48 | */ 49 | public function convertToPHPValue(mixed $value, AbstractPlatform $platform): ?IntegerValueObject 50 | { 51 | $value = $this->getInheritedType()->convertToPHPValue($value, $platform); 52 | 53 | if ($value === null) { 54 | return null; 55 | } 56 | 57 | Assert::integer($value); 58 | 59 | /** @var IntegerValueObject $object */ 60 | $object = \call_user_func([$this->class, 'fromValue'], $value); 61 | Assert::isInstanceOf($object, $this->class); 62 | 63 | return $object; 64 | } 65 | 66 | private function getInheritedType(): IntegerType 67 | { 68 | /** @var IntegerType $type */ 69 | $type = $this->getType(Types::INTEGER); 70 | 71 | return $type; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/Doctrine/Types/ObjectValueObjectType.php: -------------------------------------------------------------------------------- 1 | getInheritedType()->getSQLDeclaration($column, $platform); 21 | } 22 | 23 | /** 24 | * @inheritdoc 25 | */ 26 | public static function getSupportedValueObjectType(): string 27 | { 28 | return ObjectValueObject::class; 29 | } 30 | 31 | /** 32 | * @inheritdoc 33 | */ 34 | public function convertToDatabaseValue(mixed $value, AbstractPlatform $platform): ?string 35 | { 36 | if ($value === null) { 37 | return null; 38 | } 39 | 40 | Assert::isInstanceOf($value, $this->class); 41 | /** @var ObjectValueObject $value */ 42 | 43 | return $this->getInheritedType()->convertToDatabaseValue($value->toValue(), $platform); 44 | } 45 | 46 | /** 47 | * @inheritdoc 48 | */ 49 | public function convertToPHPValue(mixed $value, AbstractPlatform $platform): ?ObjectValueObject 50 | { 51 | $value = $this->getInheritedType()->convertToPHPValue($value, $platform); 52 | 53 | if ($value === null) { 54 | return null; 55 | } 56 | 57 | Assert::isArray($value); 58 | 59 | /** @var ObjectValueObject $collection */ 60 | $collection = \call_user_func([$this->class, 'fromValue'], $value); 61 | Assert::isInstanceOf($collection, $this->class); 62 | 63 | return $collection; 64 | } 65 | 66 | private function getInheritedType(): JsonType 67 | { 68 | /** @var JsonType $type */ 69 | $type = $this->getType(Types::JSON); 70 | 71 | return $type; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/Doctrine/Types/StringValueObjectType.php: -------------------------------------------------------------------------------- 1 | getInheritedType()->getSQLDeclaration($column, $platform); 21 | } 22 | 23 | /** 24 | * @inheritdoc 25 | */ 26 | public static function getSupportedValueObjectType(): string 27 | { 28 | return StringValueObject::class; 29 | } 30 | 31 | /** 32 | * @inheritdoc 33 | */ 34 | public function convertToDatabaseValue(mixed $value, AbstractPlatform $platform): mixed 35 | { 36 | if ($value === null) { 37 | return null; 38 | } 39 | 40 | Assert::isInstanceOf($value, $this->class); 41 | /** @var StringValueObject $value */ 42 | 43 | return $this->getInheritedType()->convertToDatabaseValue($value->toValue(), $platform); 44 | } 45 | 46 | /** 47 | * @inheritdoc 48 | */ 49 | public function convertToPHPValue(mixed $value, AbstractPlatform $platform): ?StringValueObject 50 | { 51 | $value = $this->getInheritedType()->convertToPHPValue($value, $platform); 52 | 53 | if ($value === null) { 54 | return null; 55 | } 56 | 57 | Assert::string($value); 58 | 59 | /** @var StringValueObject $object */ 60 | $object = \call_user_func([$this->class, 'fromValue'], $value); 61 | Assert::isInstanceOf($object, $this->class); 62 | 63 | return $object; 64 | } 65 | 66 | private function getInheritedType(): StringType 67 | { 68 | /** @var StringType $type */ 69 | $type = $this->getType(Types::STRING); 70 | 71 | return $type; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/Doctrine/Types/ValueObjectType.php: -------------------------------------------------------------------------------- 1 | class = $class; 37 | $type->name = $name; 38 | 39 | return $type; 40 | } 41 | 42 | /** 43 | * @return class-string 44 | */ 45 | abstract public static function getSupportedValueObjectType(): string; 46 | 47 | /** 48 | * @inheritdoc 49 | */ 50 | public function getName(): string 51 | { 52 | return $this->name; 53 | } 54 | 55 | /** 56 | * {@inheritdoc} 57 | */ 58 | public function requiresSQLCommentHint(AbstractPlatform $platform): bool 59 | { 60 | return true; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/IntegerValueObject.php: -------------------------------------------------------------------------------- 1 |