Skip to content

Commit b03e864

Browse files
feat: improve nullable detection and code structure
- Use PHP typehints instead of Column attribute for nullable detection - Support nullable embeddables and other Doctrine mapping types - Refactor AttributeHelper to eliminate duplicate code - Add doctrine/doctrine-bundle ^3.0 support in composer.json
1 parent e18db17 commit b03e864

5 files changed

Lines changed: 41 additions & 33 deletions

File tree

composer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
],
2424
"require": {
2525
"php": ">=8.4",
26-
"doctrine/doctrine-bundle": "^2.18.1",
26+
"doctrine/doctrine-bundle": "^2.18 || ^3.0",
2727
"doctrine/orm": "^3.5.7",
2828
"symfony/framework-bundle": "^7.3 || ^8.0",
2929
"symfony/security-bundle": "^7.3 || ^8.0",

src/Utils/AttributeHelper.php

Lines changed: 34 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -4,96 +4,104 @@
44

55
namespace Tmi\TranslationBundle\Utils;
66

7-
use Doctrine\ORM\Mapping\Column;
8-
use Doctrine\ORM\Mapping\Embedded;
9-
use Doctrine\ORM\Mapping\Id;
10-
use Doctrine\ORM\Mapping\ManyToMany;
11-
use Doctrine\ORM\Mapping\ManyToOne;
12-
use Doctrine\ORM\Mapping\OneToMany;
13-
use Doctrine\ORM\Mapping\OneToOne;
14-
use Tmi\TranslationBundle\Doctrine\Attribute\EmptyOnTranslate;
15-
use Tmi\TranslationBundle\Doctrine\Attribute\SharedAmongstTranslations;
7+
use Doctrine\ORM\Mapping as ORM;
8+
use Tmi\TranslationBundle\Doctrine\Attribute as TranslationAttribute;
169

1710
class AttributeHelper
1811
{
12+
private const array DOCTRINE_ATTRIBUTES = [
13+
'isEmbedded' => ORM\Embedded::class,
14+
'isOneToOne' => ORM\OneToOne::class,
15+
'isId' => ORM\Id::class,
16+
'isManyToOne' => ORM\ManyToOne::class,
17+
'isOneToMany' => ORM\OneToMany::class,
18+
'isManyToMany' => ORM\ManyToMany::class,
19+
];
20+
21+
private const array TRANSLATION_ATTRIBUTES = [
22+
'isSharedAmongstTranslations' => TranslationAttribute\SharedAmongstTranslations::class,
23+
'isEmptyOnTranslate' => TranslationAttribute\EmptyOnTranslate::class,
24+
];
25+
1926
/**
2027
* Defines if the property is embedded.
2128
*/
2229
public function isEmbedded(\ReflectionProperty $property): bool
2330
{
24-
return [] !== $property->getAttributes(Embedded::class, \ReflectionAttribute::IS_INSTANCEOF);
31+
return $this->hasAttribute($property, self::DOCTRINE_ATTRIBUTES[__FUNCTION__]);
2532
}
2633

2734
/**
2835
* Defines if the property is to be shared amongst parents' translations.
2936
*/
3037
public function isSharedAmongstTranslations(\ReflectionProperty $property): bool
3138
{
32-
return [] !== $property->getAttributes(SharedAmongstTranslations::class, \ReflectionAttribute::IS_INSTANCEOF);
39+
return $this->hasAttribute($property, self::TRANSLATION_ATTRIBUTES[__FUNCTION__]);
3340
}
3441

3542
/**
3643
* Defines if the property should be emptied on translate.
3744
*/
3845
public function isEmptyOnTranslate(\ReflectionProperty $property): bool
3946
{
40-
return [] !== $property->getAttributes(EmptyOnTranslate::class, \ReflectionAttribute::IS_INSTANCEOF);
47+
return $this->hasAttribute($property, self::TRANSLATION_ATTRIBUTES[__FUNCTION__]);
4148
}
4249

4350
/**
4451
* Defines if the property is a OneToOne relation.
4552
*/
4653
public function isOneToOne(\ReflectionProperty $property): bool
4754
{
48-
return [] !== $property->getAttributes(OneToOne::class, \ReflectionAttribute::IS_INSTANCEOF);
55+
return $this->hasAttribute($property, self::DOCTRINE_ATTRIBUTES[__FUNCTION__]);
4956
}
5057

5158
/**
5259
* Defines if the property is an ID.
5360
*/
5461
public function isId(\ReflectionProperty $property): bool
5562
{
56-
return [] !== $property->getAttributes(Id::class, \ReflectionAttribute::IS_INSTANCEOF);
63+
return $this->hasAttribute($property, self::DOCTRINE_ATTRIBUTES[__FUNCTION__]);
5764
}
5865

5966
/**
6067
* Defines if the property is a ManyToOne relation.
6168
*/
6269
public function isManyToOne(\ReflectionProperty $property): bool
6370
{
64-
return [] !== $property->getAttributes(ManyToOne::class, \ReflectionAttribute::IS_INSTANCEOF);
71+
return $this->hasAttribute($property, self::DOCTRINE_ATTRIBUTES[__FUNCTION__]);
6572
}
6673

6774
/**
68-
* Defines if the property is a ManyToOne relation.
75+
* Defines if the property is a OneToMany relation.
6976
*/
7077
public function isOneToMany(\ReflectionProperty $property): bool
7178
{
72-
return [] !== $property->getAttributes(OneToMany::class, \ReflectionAttribute::IS_INSTANCEOF);
79+
return $this->hasAttribute($property, self::DOCTRINE_ATTRIBUTES[__FUNCTION__]);
7380
}
7481

7582
/**
7683
* Defines if the property is a ManyToMany relation.
7784
*/
7885
public function isManyToMany(\ReflectionProperty $property): bool
7986
{
80-
return [] !== $property->getAttributes(ManyToMany::class, \ReflectionAttribute::IS_INSTANCEOF);
87+
return $this->hasAttribute($property, self::DOCTRINE_ATTRIBUTES[__FUNCTION__]);
8188
}
8289

8390
/**
8491
* Defines if the property can be null.
8592
*/
8693
public function isNullable(\ReflectionProperty $property): bool
8794
{
88-
$ra = $property->getAttributes(Column::class, \ReflectionAttribute::IS_INSTANCEOF);
89-
90-
if (0 < count($ra)) {
91-
$ra = reset($ra);
92-
$args = $ra->getArguments();
95+
$type = $property->getType();
9396

94-
return array_key_exists('nullable', $args) && true === $args['nullable'];
95-
}
97+
return $type && $type->allowsNull();
98+
}
9699

97-
return true;
100+
/**
101+
* Generic attribute check with consistent configuration.
102+
*/
103+
private function hasAttribute(\ReflectionProperty $property, string $attributeClass): bool
104+
{
105+
return [] !== $property->getAttributes($attributeClass, \ReflectionAttribute::IS_INSTANCEOF);
98106
}
99107
}

tests/Fixtures/Entity/CanNotBeNull.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ final class CanNotBeNull implements TranslatableInterface
2222

2323
#[EmptyOnTranslate]
2424
#[ORM\Column(type: Types::STRING, nullable: false)]
25-
private string|null $emptyNotNullable = null;
25+
private string $emptyNotNullable;
2626

2727
public function getId(): int|null
2828
{

tests/TestKernel.php

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -47,10 +47,9 @@ public function configureContainer(ContainerConfigurator $container): void
4747
'charset' => 'utf8',
4848
],
4949
'orm' => [
50-
'auto_generate_proxy_classes' => true,
51-
'naming_strategy' => 'doctrine.orm.naming_strategy.underscore_number_aware',
52-
'auto_mapping' => true,
53-
'mappings' => [
50+
'naming_strategy' => 'doctrine.orm.naming_strategy.underscore_number_aware',
51+
'auto_mapping' => true,
52+
'mappings' => [
5453
'TestBundle' => [
5554
'type' => 'attribute',
5655
'dir' => '%kernel.project_dir%/tests/Fixtures/Entity',

tests/Translation/Handlers/DoctrineObjectHandlerTest.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,14 @@
77
use Doctrine\Common\Collections\ArrayCollection;
88
use Doctrine\Common\Collections\Collection;
99
use Doctrine\ORM\Mapping\ClassMetadataFactory;
10+
use PHPUnit\Framework\Attributes\CoversClass;
1011
use Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException;
1112
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
1213
use Tmi\TranslationBundle\Test\Translation\UnitTestCase;
1314
use Tmi\TranslationBundle\Translation\Args\TranslationArgs;
1415
use Tmi\TranslationBundle\Translation\Handlers\DoctrineObjectHandler;
1516

16-
#[\PHPUnit\Framework\Attributes\CoversClass(DoctrineObjectHandler::class)]
17+
#[CoversClass(DoctrineObjectHandler::class)]
1718
final class DoctrineObjectHandlerTest extends UnitTestCase
1819
{
1920
private DoctrineObjectHandler $handler;

0 commit comments

Comments
 (0)