diff --git a/.travis.yml b/.travis.yml index 1d13957..5e1df7c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -18,10 +18,10 @@ before_script: script: - composer phpunit_clover - - composer phpstan - - composer psalm + - if [ "${TRAVIS_PHP_VERSION:0:1}" == "8" ]; then composer phpstan; fi + - if [ "${TRAVIS_PHP_VERSION:0:1}" == "8" ]; then composer psalm; fi - composer phpcs - - if [ "$TRAVIS_PULL_REQUEST" == "false" ]; then ./cc-test-reporter after-build -t clover --exit-code $TRAVIS_TEST_RESULT; fi + - if [ "$TRAVIS_PULL_REQUEST" == "false" -a "${TRAVIS_PHP_VERSION:0:1}" == "8" ]; then ./cc-test-reporter after-build -t clover --exit-code $TRAVIS_TEST_RESULT; fi notifications: email: false diff --git a/README.md b/README.md index a105c6e..b77dac6 100644 --- a/README.md +++ b/README.md @@ -84,6 +84,43 @@ class User } ``` +Or if you are using PHP8+, use the new attributes: + +```php +namespace Application\Entity; + +use Squirrel\Entities\Annotation\Entity; +use Squirrel\Entities\Annotation\Field; + +#[Entity("users")] +class User +{ + #[Field("user_id", autoincrement: true)] + private int $userId; + + #[Field("active")] + private bool $active; + + #[Field("street_name")] + private ?string $streetName; + + #[Field("street_number")] + private ?string $streetNumber; + + #[Field("city")] + private string $city; + + #[Field("balance")] + private float $balance; + + #[Field("picture_file", blob: true)] + private ?string $picture; + + #[Field("visits")] + private int $visitsNumber; +} +``` + The class is defined as an entity with the table name, and each class property is defined as a table field with the column name in the database, where the type is taken from the PHP property type (string, int, float, bool). If the property type is nullable, the column type is assumed to be nullable too. You can also define if it is an autoincrement column (called SERIAL in Postgres) and if it is a blob column (binary large object, called "blob" in most databases or "bytea" in Postgres). Whether the class properties are private, protected or public does not matter, you can choose whatever names you want, and you can design the rest of the class however you want. You can even make the classes read-only, by having private properties and only defining getters - see [Read-only entity objects](#read-only-entity-objects) for more details on why you would want to do that. @@ -464,24 +501,16 @@ namespace Application\Entity; use Squirrel\Entities\Annotation\Entity; use Squirrel\Entities\Annotation\Field; -/** - * @Entity("users_visits") - */ +#[Entity("users_visits")] class Visit { - /** - * @Field("visit_id", autoincrement=true) - */ + #[Field("visit_id", autoincrement: true)] private int $visitId = 0; - /** - * @Field("user_id") - */ + #[Field("user_id")] private int $userId = 0; - /** - * @Field("created_timestamp") - */ + #[Field("created_timestamp")] private int $timestamp = 0; } ``` @@ -750,33 +779,25 @@ namespace Application\Entity; use Squirrel\Entities\Annotation\Entity; use Squirrel\Entities\Annotation\Field; -/** - * @Entity("users") - */ +#[Entity("users")] class User { - /** - * @Field("user_id", autoincrement=true) - */ + #[Field("user_id", autoincrement=true)] private int $userId = 0; - /** - * @Field("active") - */ + #[Field("active")] private bool $active = false; /** - * @Field("note_data") - * * @var string JSON data in the database */ + #[Field("note_data")] private string $notes = ''; /** - * @Field("created") - * * @var string datetime in the database */ + #[Field("created")] private string $createDate = ''; public function getUserId(): int @@ -815,8 +836,8 @@ class GeoPoint public function __construct(float $lat, float $lng) { - $this->lat = lat; - $this->lng = lng; + $this->lat = $lat; + $this->lng = $lng; } public function getLatitude(): float @@ -838,21 +859,16 @@ use Application\Value\GeoPoint; use Squirrel\Entities\Annotation\Entity; use Squirrel\Entities\Annotation\Field; -/** - * @Entity("users_locations") - */ +#[Entity("users_locations")] class UserLocation { - /** - * @Field("user_id") - */ + #[Field("user_id")] private int $userId = 0; /** - * @Field("location") - * * @var string "point" in Postgres */ + #[Field("location")] private string $locationPoint = ''; public function getUserId(): int diff --git a/composer.json b/composer.json index 59c36f3..bf115ba 100644 --- a/composer.json +++ b/composer.json @@ -61,9 +61,9 @@ "psalm_base": "vendor/bin/psalm --set-baseline=psalm-baseline.xml", "phpunit": "vendor/bin/phpunit --colors=always", "phpunit_clover": "vendor/bin/phpunit --coverage-text --coverage-clover build/logs/clover.xml", - "coverage": "vendor/bin/phpunit --coverage-html tests/_reports", - "phpcs": "vendor/bin/phpcs --standard=ruleset.xml --extensions=php --cache=.phpcs-cache --colors src", - "phpcsfix": "vendor/bin/phpcbf --standard=ruleset.xml --extensions=php --cache=.phpcs-cache src", + "coverage": "XDEBUG_MODE=coverage vendor/bin/phpunit --coverage-html tests/_reports", + "phpcs": "vendor/bin/phpcs --standard=ruleset.xml --extensions=php --cache=.phpcs-cache --ignore=src/Annotation/Entity.php,src/Annotation/Field.php --colors src", + "phpcsfix": "vendor/bin/phpcbf --standard=ruleset.xml --extensions=php --cache=.phpcs-cache --ignore=src/Annotation/Entity.php,src/Annotation/Field.php src", "binupdate": "@composer bin all update --ansi", "bininstall": "@composer bin all install --ansi" } diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index bab7363..684ce34 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -5,6 +5,26 @@ parameters: count: 1 path: src/Annotation/EntityProcessor.php + - + message: "#^Method Squirrel\\\\Entities\\\\Annotation\\\\EntityProcessor\\:\\:getEntityFromAttribute\\(\\) has parameter \\$class with generic class ReflectionClass but does not specify its types\\: T$#" + count: 1 + path: src/Annotation/EntityProcessor.php + + - + message: "#^Method Squirrel\\\\Entities\\\\Annotation\\\\EntityProcessor\\:\\:getEntityFromAttribute\\(\\) should return Squirrel\\\\Entities\\\\Annotation\\\\Entity\\|null but returns object\\.$#" + count: 1 + path: src/Annotation/EntityProcessor.php + + - + message: "#^Method Squirrel\\\\Entities\\\\Annotation\\\\EntityProcessor\\:\\:getEntityFromAnnotation\\(\\) has parameter \\$class with generic class ReflectionClass but does not specify its types\\: T$#" + count: 1 + path: src/Annotation/EntityProcessor.php + + - + message: "#^Method Squirrel\\\\Entities\\\\Annotation\\\\EntityProcessor\\:\\:getFieldFromAttribute\\(\\) should return Squirrel\\\\Entities\\\\Annotation\\\\Field\\|null but returns object\\.$#" + count: 1 + path: src/Annotation/EntityProcessor.php + - message: "#^Result of && is always false\\.$#" count: 1 diff --git a/src/Annotation/Entity.php b/src/Annotation/Entity.php index 01f9d16..5849278 100644 --- a/src/Annotation/Entity.php +++ b/src/Annotation/Entity.php @@ -8,15 +8,55 @@ * @Annotation * @Target({"CLASS"}) */ +#[\Attribute(\Attribute::TARGET_CLASS)] class Entity { /** * @var string Name of the SQL table */ - public string $name = ''; + private string $name = ''; /** * @var string Database connection - if empty the default connection is used */ - public string $connection = ''; + private string $connection = ''; + + /** + * @param string|array{value?: string, name?: string, connection?: string} $name + */ + public function __construct($name, string $connection = '') + { + // Doctrine annotations always provides an array as a first argument - this is for backwards compatibility + if (\is_array($name)) { + // First value is provided directly for the name + if (isset($name['value'])) { + $this->name = $name['value']; + } + + // All values as "named parameters" from annotations + if (isset($name['name'])) { + $this->name = $name['name']; + } + if (isset($name['connection'])) { + $this->connection = $name['connection']; + } + } else { + $this->name = $name; + $this->connection = $connection; + } + + if (\strlen($this->name) === 0) { + throw new \InvalidArgumentException('No name provided for entity'); + } + } + + public function getName(): string + { + return $this->name; + } + + public function getConnection(): string + { + return $this->connection; + } } diff --git a/src/Annotation/EntityProcessor.php b/src/Annotation/EntityProcessor.php index 13829a9..7c6c809 100644 --- a/src/Annotation/EntityProcessor.php +++ b/src/Annotation/EntityProcessor.php @@ -25,15 +25,19 @@ public function __construct(Reader $annotationReader) * * @param object|string $class * @psalm-param object|class-string $class - * @return null|RepositoryConfig */ - public function process($class) + public function process($class): ?RepositoryConfig { // Get class reflection data $annotationClass = new \ReflectionClass($class); - // Get entity annotation for class - $entity = $this->annotationReader->getClassAnnotation($annotationClass, Entity::class); + // @codeCoverageIgnoreStart + if (PHP_VERSION_ID >= 80000) { + $entity = $this->getEntityFromAttribute($annotationClass); + } else { + $entity = $this->getEntityFromAnnotation($annotationClass); + } + // @codeCoverageIgnoreEnd // This class was annotated as Entity if ($entity instanceof Entity) { @@ -46,47 +50,50 @@ public function process($class) // Go through all public values of the class foreach ($annotationClass->getProperties() as $property) { - // Get annotations for a propery - $annotationProperty = new \ReflectionProperty($class, $property->getName()); - - // Find Field annotation on the property - $field = $this->annotationReader->getPropertyAnnotation( - $annotationProperty, - Field::class, - ); + // @codeCoverageIgnoreStart + if (PHP_VERSION_ID >= 80000) { + $field = $this->getFieldFromAttribute($property); + } else { + $field = $this->getFieldFromAnnotation($property); + } + // @codeCoverageIgnoreEnd // A Field annotation was found if ($field instanceof Field) { - $fieldType = $annotationProperty->getType(); + $fieldType = $property->getType(); // We need property types to know what to cast fields to - if (!isset($fieldType)) { + if ($fieldType === null) { throw new \InvalidArgumentException('No property type for property field ' . $property->getName() . ' in ' . $annotationClass->getName()); } + if ($fieldType instanceof \ReflectionUnionType) { + throw new \InvalidArgumentException('Union property types are not supported, encountered with property field ' . $property->getName() . ' in ' . $annotationClass->getName()); + } + $fieldTypeName = $fieldType->getName(); - if ($field->blob === true && $fieldTypeName === 'string') { + if ($field->isBlob() === true && $fieldTypeName === 'string') { $fieldTypeName = 'blob'; - } elseif ($field->blob === true) { + } elseif ($field->isBlob() === true) { throw new \InvalidArgumentException('Blob property type set for a non-string property field: ' . $property->getName() . ' in ' . $annotationClass->getName()); } - $tableToObjectFields[$field->name] = $property->getName(); - $objectToTableFields[$property->getName()] = $field->name; + $tableToObjectFields[$field->getName()] = $property->getName(); + $objectToTableFields[$property->getName()] = $field->getName(); $objectTypes[$property->getName()] = $fieldTypeName; $objectTypesNullable[$property->getName()] = $fieldType->allowsNull(); - if ($field->autoincrement === true) { - $autoincrement = $field->name; + if ($field->isAutoincrement() === true) { + $autoincrement = $field->getName(); } } } // Create new config for a repository return new RepositoryConfig( - $entity->connection, - $entity->name, + $entity->getConnection(), + $entity->getName(), $tableToObjectFields, $objectToTableFields, $annotationClass->getName(), @@ -99,4 +106,54 @@ public function process($class) // No entity found, so no configuration could be generated return null; } + + private function getEntityFromAttribute(\ReflectionClass $class): ?Entity + { + $attributes = $class->getAttributes(Entity::class); + + if (\count($attributes) === 0) { + return $this->getEntityFromAnnotation($class); + } + + return $attributes[0]->newInstance(); + } + + private function getEntityFromAnnotation(\ReflectionClass $class): ?Entity + { + $entity = $this->annotationReader->getClassAnnotation($class, Entity::class); + + // A StringFilters annotation was not found + if (!($entity instanceof Entity)) { + return null; + } + + return $entity; + } + + private function getFieldFromAttribute(\ReflectionProperty $property): ?Field + { + $attributes = $property->getAttributes(Field::class); + + if (\count($attributes) === 0) { + return $this->getFieldFromAnnotation($property); + } + + return $attributes[0]->newInstance(); + } + + private function getFieldFromAnnotation(\ReflectionProperty $property): ?Field + { + // Find StringFilter annotation on the property + $stringFilters = $this->annotationReader->getPropertyAnnotation( + $property, + Field::class, + ); + + // A StringFilters annotation was not found + if (!($stringFilters instanceof Field)) { + return null; + } + + return $stringFilters; + } } diff --git a/src/Annotation/Field.php b/src/Annotation/Field.php index d1971a0..171fb0b 100644 --- a/src/Annotation/Field.php +++ b/src/Annotation/Field.php @@ -8,20 +8,69 @@ * @Annotation * @Target({"PROPERTY"}) */ +#[\Attribute(\Attribute::TARGET_PROPERTY)] class Field { /** * @var string Name of the field in the SQL table */ - public string $name = ''; + private string $name = ''; /** * @var bool Whether this is the autoincrement field for the table - only one per table is legal! */ - public bool $autoincrement = false; + private bool $autoincrement = false; /** * @var bool Whether this is a blob field (binary large object) - needed for Postgres compatibility */ - public bool $blob = false; + private bool $blob = false; + + /** + * @param string|array{value?: string, name?: string, autoincrement?: bool, blob?: bool} $name + */ + public function __construct($name, bool $autoincrement = false, bool $blob = false) + { + // Doctrine annotations always provides an array as a first argument - this is for backwards compatibility + if (\is_array($name)) { + // First value is provided directly for the name + if (isset($name['value'])) { + $this->name = $name['value']; + } + + // All values as "named parameters" from annotations + if (isset($name['name'])) { + $this->name = $name['name']; + } + if (isset($name['autoincrement'])) { + $this->autoincrement = $name['autoincrement']; + } + if (isset($name['blob'])) { + $this->blob = $name['blob']; + } + } else { + $this->name = $name; + $this->autoincrement = $autoincrement; + $this->blob = $blob; + } + + if (\strlen($this->name) === 0) { + throw new \InvalidArgumentException('No name provided for field'); + } + } + + public function getName(): string + { + return $this->name; + } + + public function isAutoincrement(): bool + { + return $this->autoincrement; + } + + public function isBlob(): bool + { + return $this->blob; + } } diff --git a/tests/RepositoriesGenerateCommandTest.php b/tests/RepositoriesGenerateCommandTest.php index cb9351e..ec5e476 100644 --- a/tests/RepositoriesGenerateCommandTest.php +++ b/tests/RepositoriesGenerateCommandTest.php @@ -22,6 +22,7 @@ public function testGenerationAndValidRepositories() 'NonRepositoryWithAnnotationInUse.php', 'User.php', 'UserAddress.php', + 'UserWithAttributes.php', ]; $sourceFinder = new Finder(); @@ -54,8 +55,14 @@ public function testGenerationAndValidRepositories() 'UserAddress.php', 'UserAddressRepositoryReadOnly.php', 'UserAddressRepositoryWriteable.php', + 'UserWithAttributes.php', ]; + if (PHP_VERSION_ID >= 80000) { + $validFiles[] = 'UserWithAttributesRepositoryReadOnly.php'; + $validFiles[] = 'UserWithAttributesRepositoryWriteable.php'; + } + $sourceFinder = new Finder(); $sourceFinder->in(__DIR__ . '/' . 'TestEntities')->files()->sortByName()->ignoreDotFiles(false); @@ -68,11 +75,38 @@ public function testGenerationAndValidRepositories() // Make sure we have the same array contents / the same files $this->assertEqualsCanonicalizing($validFiles, $foundFiles); + $gitignoreContents = '.gitignore' . "\n" . + 'UserRepositoryReadOnly.php' . "\n" . 'UserRepositoryWriteable.php' . "\n" . + 'UserAddressRepositoryReadOnly.php' . "\n" . 'UserAddressRepositoryWriteable.php'; + + if (PHP_VERSION_ID >= 80000) { + $gitignoreContents .= "\n" . 'UserWithAttributesRepositoryReadOnly.php' . "\n" . 'UserWithAttributesRepositoryWriteable.php'; + } + // Check the .gitignore file contents, that we exclude the right files in the right order $this->assertEquals( - '.gitignore' . "\n" . - 'UserRepositoryReadOnly.php' . "\n" . 'UserRepositoryWriteable.php' . "\n" . - 'UserAddressRepositoryReadOnly.php' . "\n" . 'UserAddressRepositoryWriteable.php', + $gitignoreContents, + \file_get_contents(__DIR__ . '/' . 'TestEntities/' . '.gitignore') + ); + + // Run repositories generator again to check that the output is identical + $repositoriesGenerator(); + + $sourceFinder = new Finder(); + $sourceFinder->in(__DIR__ . '/' . 'TestEntities')->files()->sortByName()->ignoreDotFiles(false); + + $foundFiles = []; + + foreach ($sourceFinder as $file) { + $foundFiles[] = $file->getFilename(); + } + + // Make sure we have the same array contents / the same files + $this->assertEqualsCanonicalizing($validFiles, $foundFiles); + + // Check the .gitignore file contents, that we exclude the right files in the right order + $this->assertEquals( + $gitignoreContents, \file_get_contents(__DIR__ . '/' . 'TestEntities/' . '.gitignore') ); @@ -82,6 +116,18 @@ public function testGenerationAndValidRepositories() require(__DIR__ . '/' . 'TestEntities/' . 'UserAddressRepositoryReadOnly.php'); require(__DIR__ . '/' . 'TestEntities/' . 'UserAddressRepositoryWriteable.php'); + if (PHP_VERSION_ID >= 80000) { + require(__DIR__ . '/' . 'TestEntities/' . 'UserWithAttributesRepositoryReadOnly.php'); + require(__DIR__ . '/' . 'TestEntities/' . 'UserWithAttributesRepositoryWriteable.php'); + + if (!\class_exists('Squirrel\Entities\Tests\TestEntities\UserWithAttributesRepositoryReadOnly', false)) { + $this->assertEquals('', 'Squirrel\Entities\Tests\TestEntities\UserWithAttributesRepositoryReadOnly'); + } + if (!\class_exists('Squirrel\Entities\Tests\TestEntities\UserWithAttributesRepositoryWriteable', false)) { + $this->assertEquals('', 'Squirrel\Entities\Tests\TestEntities\UserWithAttributesRepositoryWriteable'); + } + } + // Make sure all repository classes exist if (!\class_exists('Squirrel\Entities\Tests\TestEntities\UserRepositoryReadOnly', false)) { $this->assertEquals('', 'Squirrel\Entities\Tests\TestEntities\UserRepositoryReadOnly'); @@ -160,6 +206,11 @@ public function testGenerationAndValidRepositories() @\unlink(__DIR__ . '/' . 'TestEntities/' . 'UserRepositoryWriteable.php'); @\unlink(__DIR__ . '/' . 'TestEntities/' . 'UserAddressRepositoryReadOnly.php'); @\unlink(__DIR__ . '/' . 'TestEntities/' . 'UserAddressRepositoryWriteable.php'); + + if (PHP_VERSION_ID >= 80000) { + @\unlink(__DIR__ . '/' . 'TestEntities/' . 'UserWithAttributesRepositoryReadOnly.php'); + @\unlink(__DIR__ . '/' . 'TestEntities/' . 'UserWithAttributesRepositoryWriteable.php'); + } } public function testGenerationForInvalidBlobRepositories() @@ -219,4 +270,96 @@ public function testGenerationForNoTypeRepositories() // Execute the generator $repositoriesGenerator(); } + + public function testGenerationForInvalidNoEntityNameRepositories() + { + $this->expectException(\InvalidArgumentException::class); + + $validFiles = [ + 'UserAddressInvalid.php', + ]; + + $sourceFinder = new Finder(); + $sourceFinder->in(__DIR__ . '/' . 'TestEntitiesInvalidNoEntityName')->files()->sortByName()->ignoreDotFiles(false); + + $foundFiles = []; + + foreach ($sourceFinder as $file) { + $foundFiles[] = $file->getFilename(); + } + + // Make sure we have the same array contents / the same files + $this->assertEqualsCanonicalizing($validFiles, $foundFiles); + + $repositoriesGenerator = new RepositoriesGenerateCommand( + [__DIR__ . '/' . 'TestEntitiesInvalidNoEntityName'], + new PHPFilesInDirectoryGetContents() + ); + + // Execute the generator + $repositoriesGenerator(); + } + + public function testGenerationForInvalidNoFieldNameRepositories() + { + $this->expectException(\InvalidArgumentException::class); + + $validFiles = [ + 'UserAddressInvalid.php', + ]; + + $sourceFinder = new Finder(); + $sourceFinder->in(__DIR__ . '/' . 'TestEntitiesInvalidNoFieldName')->files()->sortByName()->ignoreDotFiles(false); + + $foundFiles = []; + + foreach ($sourceFinder as $file) { + $foundFiles[] = $file->getFilename(); + } + + // Make sure we have the same array contents / the same files + $this->assertEqualsCanonicalizing($validFiles, $foundFiles); + + $repositoriesGenerator = new RepositoriesGenerateCommand( + [__DIR__ . '/' . 'TestEntitiesInvalidNoFieldName'], + new PHPFilesInDirectoryGetContents() + ); + + // Execute the generator + $repositoriesGenerator(); + } + + public function testGenerationForInvalidFieldUnionTypeRepositories() + { + if (PHP_VERSION_ID < 80000) { + $this->assertTrue(true); + return; + } + + $this->expectException(\InvalidArgumentException::class); + + $validFiles = [ + 'UserAddressInvalid.php', + ]; + + $sourceFinder = new Finder(); + $sourceFinder->in(__DIR__ . '/' . 'TestEntitiesInvalidFieldUnionType')->files()->sortByName()->ignoreDotFiles(false); + + $foundFiles = []; + + foreach ($sourceFinder as $file) { + $foundFiles[] = $file->getFilename(); + } + + // Make sure we have the same array contents / the same files + $this->assertEqualsCanonicalizing($validFiles, $foundFiles); + + $repositoriesGenerator = new RepositoriesGenerateCommand( + [__DIR__ . '/' . 'TestEntitiesInvalidFieldUnionType'], + new PHPFilesInDirectoryGetContents() + ); + + // Execute the generator + $repositoriesGenerator(); + } } diff --git a/tests/TestEntities/User.php b/tests/TestEntities/User.php index 383b851..bc779c2 100644 --- a/tests/TestEntities/User.php +++ b/tests/TestEntities/User.php @@ -6,7 +6,7 @@ use Squirrel\Entities\PopulatePropertiesWithIterableTrait; /** - * @SQL\Entity("users") + * @SQL\Entity("users", connection="dada") */ class User { @@ -33,7 +33,7 @@ class User private string $loginNameMD5 = ''; /** - * @SQL\Field("login_password") + * @SQL\Field(name="login_password") */ private string $loginPassword = ''; @@ -57,6 +57,8 @@ class User */ private int $createDate = 0; + private string $fieldWithoutAnnotation = ''; + public function getUserId(): int { return $this->userId; @@ -101,4 +103,9 @@ public function getCreateDate(): int { return $this->createDate; } + + public function getFieldWithoutAnnotation(): string + { + return $this->fieldWithoutAnnotation; + } } diff --git a/tests/TestEntities/UserAddress.php b/tests/TestEntities/UserAddress.php index 8aaf3d1..d414b1a 100644 --- a/tests/TestEntities/UserAddress.php +++ b/tests/TestEntities/UserAddress.php @@ -7,7 +7,7 @@ use Squirrel\Entities\PopulatePropertiesWithIterableTrait; /** - * @Entity("users_address") + * @Entity(name="users_address") */ class UserAddress { diff --git a/tests/TestEntities/UserWithAttributes.php b/tests/TestEntities/UserWithAttributes.php new file mode 100644 index 0000000..728c331 --- /dev/null +++ b/tests/TestEntities/UserWithAttributes.php @@ -0,0 +1,75 @@ +userId; + } + + public function isActive(): bool + { + return $this->active; + } + + public function getUserName(): string + { + return $this->userName; + } + + public function getDescription(): string + { + return $this->description; + } + + public function getBalance(): float + { + return $this->balance; + } + + public function getLocationId(): ?int + { + return $this->locationId; + } + + public function getCreateDate(): int + { + return $this->createDate; + } + + public function getFieldWithoutAttribute(): string + { + return $this->fieldWithoutAttribute; + } +} diff --git a/tests/TestEntitiesInvalidFieldUnionType/UserAddressInvalid.php b/tests/TestEntitiesInvalidFieldUnionType/UserAddressInvalid.php new file mode 100644 index 0000000..f60349f --- /dev/null +++ b/tests/TestEntitiesInvalidFieldUnionType/UserAddressInvalid.php @@ -0,0 +1,27 @@ +userId; + } +} diff --git a/tests/TestEntitiesInvalidNoEntityName/UserAddressInvalid.php b/tests/TestEntitiesInvalidNoEntityName/UserAddressInvalid.php new file mode 100644 index 0000000..f61d652 --- /dev/null +++ b/tests/TestEntitiesInvalidNoEntityName/UserAddressInvalid.php @@ -0,0 +1,27 @@ +userId; + } +} diff --git a/tests/TestEntitiesInvalidNoFieldName/UserAddressInvalid.php b/tests/TestEntitiesInvalidNoFieldName/UserAddressInvalid.php new file mode 100644 index 0000000..0da2bf2 --- /dev/null +++ b/tests/TestEntitiesInvalidNoFieldName/UserAddressInvalid.php @@ -0,0 +1,27 @@ +userId; + } +}