├── .github
└── workflows
│ └── ci.yml
├── .gitignore
├── bin
└── orm-build
├── composer.json
├── phpunit.xml
├── psalm-baseline.xml
├── psalm.xml
├── src
├── ClassConfiguration.php
├── ClassMetadata.php
├── ClassMetadataBuilder.php
├── Configuration.php
├── EmbeddableConfiguration.php
├── EmbeddableMetadata.php
├── EntityConfiguration.php
├── EntityMetadata.php
├── Exception
│ ├── EntityNotFoundException.php
│ ├── IdentityConflictException.php
│ ├── NoIdentityException.php
│ ├── NonUniqueResultException.php
│ ├── ORMException.php
│ ├── UnknownEntityClassException.php
│ └── UnknownPropertyException.php
├── Gateway.php
├── IdentityMap.php
├── ObjectFactory.php
├── Options.php
├── PropertyMapping.php
├── PropertyMapping
│ ├── BoolMapping.php
│ ├── BuiltinTypeMapping.php
│ ├── EmbeddableMapping.php
│ ├── EntityMapping.php
│ ├── IntMapping.php
│ ├── JsonMapping.php
│ └── StringMapping.php
├── Proxy.php
├── ProxyBuilder.php
├── ProxyTemplate.php
├── Query.php
├── QueryOrderBy.php
├── QueryPredicate.php
├── RepositoryBuilder.php
├── RepositoryTemplate.php
├── SelectQueryBuilder.php
└── TableAliasGenerator.php
├── tests-config.php
└── tests
├── AbstractTestCase.php
├── GatewayNativeQueryTest.php
├── GatewayTest.php
├── Generated
├── ClassMetadata.php
├── Proxy
│ ├── CountryProxy.php
│ ├── Event
│ │ ├── CountryEvent
│ │ │ ├── CreateCountryEventProxy.php
│ │ │ └── EditCountryNameEventProxy.php
│ │ ├── FollowUserEventProxy.php
│ │ └── UserEvent
│ │ │ ├── CreateUserEventProxy.php
│ │ │ ├── EditUserBillingAddressEventProxy.php
│ │ │ ├── EditUserDeliveryAddressEventProxy.php
│ │ │ └── EditUserNameEventProxy.php
│ ├── FollowProxy.php
│ └── UserProxy.php
└── Repository
│ ├── CountryRepository.php
│ ├── EventRepository.php
│ ├── FollowRepository.php
│ └── UserRepository.php
├── InheritanceTest.php
├── ObjectFactoryTest.php
├── ProxyTest.php
├── Resources
├── DTO
│ ├── AddressDTO.php
│ └── UserDTO.php
├── Mappings
│ └── GeometryMapping.php
├── Models
│ ├── Address.php
│ ├── Country.php
│ ├── Event.php
│ ├── Event
│ │ ├── CountryEvent.php
│ │ ├── CountryEvent
│ │ │ ├── CreateCountryEvent.php
│ │ │ └── EditCountryNameEvent.php
│ │ ├── FollowUserEvent.php
│ │ ├── UserEvent.php
│ │ └── UserEvent
│ │ │ ├── CreateUserEvent.php
│ │ │ ├── EditUserBillingAddressEvent.php
│ │ │ ├── EditUserDeliveryAddressEvent.php
│ │ │ └── EditUserNameEvent.php
│ ├── Follow.php
│ ├── GeoAddress.php
│ └── User.php
└── Objects
│ └── Geometry.php
└── TableAliasGeneratorTest.php
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | on:
4 | push:
5 | pull_request:
6 |
7 | env:
8 | PSALM_PHP_VERSION: "8.3"
9 | COVERAGE_PHP_VERSION: "8.3"
10 |
11 | jobs:
12 | psalm:
13 | name: Psalm
14 | runs-on: ubuntu-22.04
15 |
16 | steps:
17 | - name: Checkout
18 | uses: actions/checkout@v4
19 |
20 | - name: Setup PHP
21 | uses: shivammathur/setup-php@v2
22 | with:
23 | php-version: ${{ env.PSALM_PHP_VERSION }}
24 |
25 | - name: Install composer dependencies
26 | uses: ramsey/composer-install@v3
27 |
28 | - name: Run Psalm
29 | run: vendor/bin/psalm --show-info=false --no-progress
30 |
31 | phpunit:
32 | name: PHPUnit
33 | runs-on: ubuntu-22.04
34 |
35 | strategy:
36 | fail-fast: false
37 | matrix:
38 | php-version:
39 | - "8.1"
40 | - "8.2"
41 | - "8.3"
42 | deps:
43 | - "highest"
44 | include:
45 | - php-version: "8.1"
46 | deps: "lowest"
47 |
48 | services:
49 | mysql:
50 | image: "mysql:8.0"
51 | ports:
52 | - "3306:3306"
53 | options: >-
54 | --health-cmd "mysqladmin ping --silent"
55 | env:
56 | MYSQL_ROOT_PASSWORD: password
57 |
58 | steps:
59 | - name: Checkout
60 | uses: actions/checkout@v4
61 |
62 | - name: Setup PHP
63 | uses: shivammathur/setup-php@v2
64 | with:
65 | php-version: ${{ matrix.php-version }}
66 | coverage: pcov
67 |
68 | - name: Install composer dependencies
69 | uses: ramsey/composer-install@v3
70 | with:
71 | dependency-versions: ${{ matrix.deps }}
72 |
73 | - name: Run PHPUnit
74 | run: vendor/bin/phpunit --fail-on-skipped
75 | if: ${{ matrix.php-version != env.COVERAGE_PHP_VERSION }}
76 | env:
77 | DB_HOST: 127.0.0.1
78 | DB_PORT: 3306
79 | DB_USERNAME: root
80 | DB_PASSWORD: password
81 |
82 | - name: Run PHPUnit with coverage
83 | run: |
84 | mkdir -p build/logs
85 | vendor/bin/phpunit --fail-on-skipped --coverage-clover build/logs/clover.xml
86 | if: ${{ matrix.php-version == env.COVERAGE_PHP_VERSION }}
87 | env:
88 | DB_HOST: 127.0.0.1
89 | DB_PORT: 3306
90 | DB_USERNAME: root
91 | DB_PASSWORD: password
92 |
93 | - name: Upload coverage report to Coveralls
94 | run: vendor/bin/php-coveralls --coverage_clover=build/logs/clover.xml -v
95 | env:
96 | COVERALLS_REPO_TOKEN: ${{ secrets.GITHUB_TOKEN }}
97 | if: ${{ matrix.php-version == env.COVERAGE_PHP_VERSION }}
98 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /vendor
2 | /composer.lock
3 | /.phpunit.cache
4 |
--------------------------------------------------------------------------------
/bin/orm-build:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env php
2 | build();
62 |
63 | $php = 'getClassMetadataFile();
66 | echo $path, PHP_EOL;
67 |
68 | file_put_contents($path, $php);
69 |
70 | return $classMetadata;
71 | })();
72 |
73 | // Build repositories for all aggregate roots
74 |
75 | (function() use ($configuration, $classMetadata) {
76 | foreach ($configuration->getEntities() as $className => $entityConfiguration) {
77 | if ($entityConfiguration->getBelongsTo() !== null) {
78 | // Only generate repositories for aggregate roots.
79 | continue;
80 | }
81 |
82 | $builder = new RepositoryBuilder();
83 | $builder->setRepositoryNamespace($configuration->getRepositoryNamespace($className));
84 | $builder->setEntityClassName($className);
85 |
86 | $identityProps = [];
87 |
88 | foreach ($classMetadata[$className]->idProperties as $idProperty) {
89 | $type = $classMetadata[$className]->propertyMappings[$idProperty]->getType();
90 |
91 | if ($type === null) {
92 | throw new \LogicException('An identity property cannot be mapped to a mixed type.');
93 | }
94 |
95 | $identityProps[$idProperty] = $type;
96 | }
97 |
98 | $builder->setIdentityProps($identityProps);
99 |
100 | $path = $configuration->getRepositoryFileName($className);
101 | echo $path, PHP_EOL;
102 |
103 | @mkdir(dirname($path), 0777, true);
104 | file_put_contents($path, $builder->build());
105 | }
106 | })();
107 |
108 | // Build proxies for all non-abstract entity classes
109 |
110 | (function() use ($configuration, $classMetadata) {
111 | foreach ($configuration->getEntities() as $entityConfiguration) {
112 | foreach ($entityConfiguration->getClassHierarchy() as $concreteClassName) {
113 | $reflectionClass = new \ReflectionClass($concreteClassName);
114 |
115 | if ($reflectionClass->isAbstract()) {
116 | continue;
117 | }
118 |
119 | $builder = new ProxyBuilder();
120 | $builder->setProxyNamespace($configuration->getProxyNamespace($concreteClassName));
121 | $builder->setEntityClassName($concreteClassName);
122 | $builder->setNonIdProps($classMetadata[$concreteClassName]->nonIdProperties);
123 |
124 | $path = $configuration->getProxyFileName($concreteClassName);
125 |
126 | echo $path, PHP_EOL;
127 |
128 | @mkdir(dirname($path), 0777, true);
129 | file_put_contents($path, $builder->build());
130 | }
131 | }
132 | })();
133 | })($argv[1]);
134 |
135 | echo 'Success.', PHP_EOL;
136 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "brick/orm",
3 | "description": "Object-Relational Mapper",
4 | "type": "library",
5 | "keywords": [
6 | "ORM",
7 | "Data Mapper"
8 | ],
9 | "license": "MIT",
10 | "require": {
11 | "php": "^8.1",
12 | "brick/db": "dev-interfaces"
13 | },
14 | "require-dev": {
15 | "ext-pdo": "*",
16 | "phpunit/phpunit": "^10.1",
17 | "php-coveralls/php-coveralls": "^2.0",
18 | "vimeo/psalm": "5.24.0"
19 | },
20 | "autoload": {
21 | "psr-4": {
22 | "Brick\\ORM\\": "src/"
23 | }
24 | },
25 | "autoload-dev": {
26 | "psr-4": {
27 | "Brick\\ORM\\Tests\\": "tests/"
28 | }
29 | },
30 | "bin": ["bin/orm-build"]
31 | }
32 |
--------------------------------------------------------------------------------
/phpunit.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | tests
6 |
7 |
8 |
9 |
10 | src
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/psalm-baseline.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | ]]>
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 | entities[$class]]]>
18 | entities[$class]]]>
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
--------------------------------------------------------------------------------
/psalm.xml:
--------------------------------------------------------------------------------
1 |
2 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
--------------------------------------------------------------------------------
/src/ClassConfiguration.php:
--------------------------------------------------------------------------------
1 | configuration = $configuration;
23 |
24 | try {
25 | $this->reflectionClass = new \ReflectionClass($className);
26 | } catch (\ReflectionException $e) {
27 | throw new \InvalidArgumentException(sprintf('%s does not exist.', $className), 0, $e);
28 | }
29 | }
30 |
31 | /**
32 | * @psalm-return class-string
33 | */
34 | public function getClassName() : string
35 | {
36 | return $this->reflectionClass->getName();
37 | }
38 |
39 | /**
40 | * @psalm-param class-string|null $className
41 | *
42 | * @psalm-return list
43 | *
44 | * @param string|null $className The entity class name, or null to use the root entity (this entity)'s class name.
45 | *
46 | * @return string[]
47 | *
48 | * @throws \LogicException
49 | */
50 | public function getPersistentProperties(string|null $className = null) : array
51 | {
52 | if ($className === null) {
53 | $reflectionClass = $this->reflectionClass;
54 | } else {
55 | $reflectionClass = new \ReflectionClass($className);
56 | }
57 |
58 | $className = $reflectionClass->getName();
59 |
60 | $persistableProperties = [];
61 |
62 | foreach ($reflectionClass->getProperties() as $reflectionProperty) {
63 | if ($reflectionProperty->isStatic()) {
64 | continue;
65 | }
66 |
67 | $propertyName = $reflectionProperty->getName();
68 |
69 | if (in_array($propertyName, $this->configuration->getTransientProperties($className))) {
70 | continue;
71 | }
72 |
73 | if ($reflectionProperty->isPrivate()) {
74 | throw new \LogicException(sprintf('%s::$%s is private; private properties are not supported. Make the property protected, or add it to transient properties if it should not be persistent.', $className, $propertyName));
75 | }
76 |
77 | if (! $reflectionProperty->hasType()) {
78 | throw new \LogicException(sprintf('%s::$%s is not typed. Add a type to the property, or add it to transient properties if it should not be persistent.', $className, $propertyName));
79 | }
80 |
81 | $persistableProperties[] = $propertyName;
82 | }
83 |
84 | if (count($persistableProperties) === 0) {
85 | throw new \LogicException(sprintf('%s has not persistable properties.', $className));
86 | }
87 |
88 | return $persistableProperties;
89 | }
90 |
91 | /**
92 | * @psalm-param class-string $className
93 | *
94 | * @param string $className The entity class name.
95 | * @param string $propertyName The property name.
96 | * @param EntityMetadata[] $entityMetadata A map of FQCN to EntityMetadata instances.
97 | * @param EmbeddableMetadata[] $embeddableMetadata A map of FQCN to EmbeddableMetadata instances.
98 | *
99 | * @throws \LogicException
100 | */
101 | public function getPropertyMapping(string $className, string $propertyName, array $entityMetadata, array $embeddableMetadata) : PropertyMapping
102 | {
103 | if (! in_array($propertyName, $this->getPersistentProperties($className))) {
104 | throw new \InvalidArgumentException(sprintf('Cannot return property mapping for unknown or non-persistent property %s::$%s.', $className, $propertyName));
105 | }
106 |
107 | $customPropertyMappings = $this->configuration->getCustomPropertyMappings();
108 |
109 | if (isset($customPropertyMappings[$className][$propertyName])) {
110 | return $customPropertyMappings[$className][$propertyName];
111 | }
112 |
113 | $reflectionProperty = new \ReflectionProperty($className, $propertyName);
114 |
115 | $propertyType = $reflectionProperty->getType();
116 |
117 | if (! $propertyType instanceof ReflectionNamedType) {
118 | throw new \LogicException('Property does not have a single type.');
119 | }
120 |
121 | $typeName = $propertyType->getName();
122 | $allowsNull = $propertyType->allowsNull();
123 |
124 | $fieldNames = $this->configuration->getFieldNames();
125 | $fieldName = $fieldNames[$className][$propertyName] ?? $propertyName;
126 |
127 | if ($propertyType->isBuiltin()) {
128 | return match($typeName) {
129 | 'int' => new PropertyMapping\IntMapping($fieldName, $allowsNull),
130 | 'string' => new PropertyMapping\StringMapping($fieldName, $allowsNull),
131 | 'bool' => new PropertyMapping\BoolMapping($fieldName, $allowsNull),
132 | 'array' => throw new \LogicException(sprintf('Cannot persist type "array" in %s::$%s; you can store an array as JSON if you wish, by configuring a custom JsonMapping instance.', $className, $propertyName)),
133 | default => throw new \LogicException(sprintf('Cannot persist type "%s" in %s::$%s.', $typeName, $className, $propertyName))
134 | };
135 | }
136 |
137 | $customMappings = $this->configuration->getCustomMappings();
138 |
139 | if (isset($customMappings[$typeName])) {
140 | // @todo for now this only works with a single field name/prefix, and fixed constructor
141 | return new $customMappings[$typeName]($fieldName, $allowsNull);
142 | }
143 |
144 | $fieldNamePrefixes = $this->configuration->getFieldNamePrefixes();
145 | $fieldNamePrefix = $fieldNamePrefixes[$className][$propertyName] ?? $propertyName . '_';
146 |
147 | if (isset($entityMetadata[$typeName])) {
148 | return new PropertyMapping\EntityMapping($entityMetadata[$typeName], $fieldNamePrefix, $allowsNull);
149 | }
150 |
151 | if (isset($embeddableMetadata[$typeName])) {
152 | return new PropertyMapping\EmbeddableMapping($embeddableMetadata[$typeName], $fieldNamePrefix, $allowsNull);
153 | }
154 |
155 | throw new \LogicException(sprintf('Type %s of %s::$%s is not an entity or embeddable, and has no custom mapping defined.', $typeName, $className, $propertyName));
156 | }
157 | }
158 |
--------------------------------------------------------------------------------
/src/ClassMetadata.php:
--------------------------------------------------------------------------------
1 |
20 | *
21 | * @var string[]
22 | */
23 | public array $properties;
24 |
25 | /**
26 | * A map of persistent property names to PropertyMapping instances.
27 | *
28 | * The keys of this array must be equal to $properties.
29 | *
30 | * @var array
31 | */
32 | public array $propertyMappings;
33 | }
34 |
--------------------------------------------------------------------------------
/src/ClassMetadataBuilder.php:
--------------------------------------------------------------------------------
1 |
22 | *
23 | * @var EntityMetadata[]
24 | */
25 | private array $entityMetadata;
26 |
27 | /**
28 | * @psalm-var array
29 | *
30 | * @var EmbeddableMetadata[]
31 | */
32 | private array $embeddableMetadata;
33 |
34 | public function __construct(Configuration $configuration)
35 | {
36 | $this->configuration = $configuration;
37 | }
38 |
39 | /**
40 | * @psalm-return array
41 | *
42 | * @return EntityMetadata[] A map of FQCN to EntityMetadata instances for all entities.
43 | */
44 | public function build() : array
45 | {
46 | $this->entityMetadata = [];
47 | $this->embeddableMetadata = [];
48 |
49 | $classConfigurations = $this->configuration->getClasses();
50 |
51 | foreach ($classConfigurations as $classConfiguration) {
52 | if ($classConfiguration instanceof EntityConfiguration) {
53 | foreach ($classConfiguration->getClassHierarchy() as $className) {
54 | $this->entityMetadata[$className] = new EntityMetadata();
55 | }
56 | } elseif ($classConfiguration instanceof EmbeddableConfiguration) {
57 | $this->embeddableMetadata[$classConfiguration->getClassName()] = new EmbeddableMetadata();
58 | }
59 | }
60 |
61 | // This needs to be done in 2 steps, as references to all ClassMetadata instances must be available below.
62 |
63 | foreach ($classConfigurations as $classConfiguration) {
64 | if ($classConfiguration instanceof EntityConfiguration) {
65 | foreach ($classConfiguration->getClassHierarchy() as $className) {
66 | $this->fillEntityMetadata($this->entityMetadata[$className], $className, $classConfiguration);
67 | }
68 | } elseif ($classConfiguration instanceof EmbeddableConfiguration) {
69 | $className = $classConfiguration->getClassName();
70 | $this->fillEmbeddableMetadata($this->embeddableMetadata[$className], $className, $classConfiguration);
71 | }
72 | }
73 |
74 | return $this->entityMetadata;
75 | }
76 |
77 | /**
78 | * @psalm-param class-string $className
79 | *
80 | * @throws \LogicException
81 | */
82 | private function fillEntityMetadata(EntityMetadata $classMetadata, string $className, EntityConfiguration $entityConfiguration) : void
83 | {
84 | $reflectionClass = new \ReflectionClass($className);
85 |
86 | $classMetadata->className = $className;
87 |
88 | $classMetadata->discriminatorColumn = $entityConfiguration->getDiscriminatorColumn();
89 | $classMetadata->discriminatorValue = null;
90 |
91 | foreach ($entityConfiguration->getDiscriminatorMap() as $discriminatorValue => $targetClassName) {
92 | if ($targetClassName === $className) {
93 | $classMetadata->discriminatorValue = $discriminatorValue;
94 | break;
95 | }
96 | }
97 |
98 | $classMetadata->discriminatorMap = $entityConfiguration->getDiscriminatorMap();
99 | $classMetadata->inverseDiscriminatorMap = array_flip($classMetadata->discriminatorMap);
100 |
101 | $classMetadata->childClasses = [];
102 |
103 | foreach ($entityConfiguration->getClassHierarchy() as $hClassName) {
104 | if (is_subclass_of($hClassName, $className)) {
105 | $classMetadata->childClasses[] = $hClassName;
106 | }
107 | }
108 |
109 | $classMetadata->rootClassName = $entityConfiguration->getClassName();
110 |
111 | if ($reflectionClass->isAbstract()) {
112 | $classMetadata->proxyClassName = null;
113 | } else {
114 | $classMetadata->proxyClassName = $this->configuration->getProxyClassName($className);
115 | }
116 |
117 | $classMetadata->tableName = $entityConfiguration->getTableName();
118 | $classMetadata->isAutoIncrement = $entityConfiguration->isAutoIncrement();
119 |
120 | $persistentProperties = $entityConfiguration->getPersistentProperties($className);
121 | $identityProperties = $entityConfiguration->getIdentityProperties();
122 |
123 | $classMetadata->properties = $persistentProperties;
124 | $classMetadata->idProperties = $identityProperties;
125 | $classMetadata->nonIdProperties = array_values(array_diff($persistentProperties, $identityProperties));
126 |
127 | $classMetadata->selfNonIdProperties = [];
128 |
129 | foreach ($classMetadata->nonIdProperties as $nonIdProperty) {
130 | $r = new \ReflectionProperty($className, $nonIdProperty);
131 | if ($r->getDeclaringClass()->getName() === $className) {
132 | $classMetadata->selfNonIdProperties[] = $nonIdProperty;
133 | }
134 | }
135 |
136 | $classMetadata->propertyMappings = [];
137 |
138 | foreach ($persistentProperties as $propertyName) {
139 | $propertyMapping = $entityConfiguration->getPropertyMapping($className, $propertyName, $this->entityMetadata, $this->embeddableMetadata);
140 | $classMetadata->propertyMappings[$propertyName] = $propertyMapping;
141 | }
142 |
143 | // Enforce non-nullable identities, that ultimately map to int or string properties.
144 | // We need this guarantee for our identity map, and other types do not make much sense anyway.
145 |
146 | foreach ($classMetadata->idProperties as $idProperty) {
147 | $propertyMapping = $classMetadata->propertyMappings[$idProperty];
148 |
149 | if ($propertyMapping->isNullable()) {
150 | throw new \LogicException(sprintf(
151 | 'Identity property %s::$%s must not be nullable.',
152 | $classMetadata->className,
153 | $idProperty
154 | ));
155 | }
156 |
157 | if ($propertyMapping instanceof IntMapping) {
158 | continue;
159 | }
160 |
161 | if ($propertyMapping instanceof StringMapping) {
162 | continue;
163 | }
164 |
165 | if ($propertyMapping instanceof EntityMapping) {
166 | continue;
167 | }
168 |
169 | throw new \LogicException(sprintf(
170 | 'Identity property %s::$%s uses an unsupported mapping type %s. ' .
171 | 'Identities must ultimately map to int or string properties.',
172 | $classMetadata->className,
173 | $idProperty,
174 | get_class($propertyMapping)
175 | ));
176 | }
177 | }
178 |
179 | /**
180 | * @psalm-param class-string $className
181 | */
182 | private function fillEmbeddableMetadata(EmbeddableMetadata $classMetadata, string $className, EmbeddableConfiguration $embeddableConfiguration) : void
183 | {
184 | $classMetadata->className = $className;
185 |
186 | $persistentProperties = $embeddableConfiguration->getPersistentProperties($className);
187 |
188 | $classMetadata->properties = $persistentProperties;
189 |
190 | $classMetadata->propertyMappings = [];
191 |
192 | foreach ($persistentProperties as $propertyName) {
193 | $propertyMapping = $embeddableConfiguration->getPropertyMapping($className, $propertyName, $this->entityMetadata, $this->embeddableMetadata);
194 | $classMetadata->propertyMappings[$propertyName] = $propertyMapping;
195 | }
196 | }
197 | }
198 |
--------------------------------------------------------------------------------
/src/Configuration.php:
--------------------------------------------------------------------------------
1 |
23 | *
24 | * @var ClassConfiguration[]
25 | */
26 | private array $classes = [];
27 |
28 | /**
29 | * A map of entity/embeddable class names to lists of transient property names.
30 | *
31 | * @psalm-var array>
32 | *
33 | * @var string[][]
34 | */
35 | private array $transientProperties = [];
36 |
37 | /**
38 | * A map of entity/embeddable class names to property names to PropertyMapping instances.
39 | *
40 | * The mappings are usually inferred from the PHP type, but can be overridden here.
41 | * This is typically used to map a mixed/array type property to a JSON column.
42 | *
43 | * @psalm-var array>
44 | *
45 | * @var PropertyMapping[][]
46 | */
47 | private array $customPropertyMappings = [];
48 |
49 | /**
50 | * A map of entity/embeddable class names to property names to field names.
51 | *
52 | * @psalm-var array>
53 | *
54 | * @var string[][]
55 | */
56 | private array $fieldNames = [];
57 |
58 | /**
59 | * A map of entity/embeddable class names to property names to field name prefixes.
60 | *
61 | * @psalm-var array>
62 | *
63 | * @var string[][]
64 | */
65 | private array $fieldNamePrefixes = [];
66 |
67 | /**
68 | * A map of class names to custom property mapping classes.
69 | *
70 | * @var array>
71 | */
72 | private array $customMappings = [];
73 |
74 | public function setProxyNamespace(string $proxyNamespace) : Configuration
75 | {
76 | $this->proxyNamespace = $proxyNamespace;
77 |
78 | return $this;
79 | }
80 |
81 | /**
82 | * @throws \LogicException
83 | */
84 | public function getProxyNamespace(string|null $entityClass = null) : string
85 | {
86 | if ($this->proxyNamespace === null) {
87 | throw new \LogicException('Proxy namespace is not set.');
88 | }
89 |
90 | if ($entityClass === null) {
91 | return $this->proxyNamespace;
92 | }
93 |
94 | if ($this->baseEntityNamespace !== null) {
95 | $baseNamespace = $this->baseEntityNamespace . '\\';
96 | $length = strlen($baseNamespace);
97 |
98 | if (substr($entityClass, 0, $length) !== $baseNamespace) {
99 | throw new \LogicException(sprintf('%s is not in namespace %s.', $entityClass, $this->baseEntityNamespace));
100 | }
101 |
102 | $entityClass = substr($entityClass, $length);
103 | }
104 |
105 | $pos = strrpos($entityClass, '\\');
106 |
107 | if ($pos === false) {
108 | return $this->proxyNamespace;
109 | }
110 |
111 | return $this->proxyNamespace . '\\' . substr($entityClass, 0, $pos);
112 | }
113 |
114 | /**
115 | * Returns the proxy class name for the given entity class name.
116 | *
117 | * @psalm-param class-string $entityClass
118 | *
119 | * @psalm-return class-string
120 | *
121 | * @psalm-suppress LessSpecificReturnStatement
122 | * @psalm-suppress MoreSpecificReturnType
123 | *
124 | * @param string $entityClass the FQCN of the entity.
125 | *
126 | * @return string The FQCN of the proxy.
127 | *
128 | * @throws \LogicException
129 | */
130 | public function getProxyClassName(string $entityClass) : string
131 | {
132 | if ($this->baseEntityNamespace !== null) {
133 | $baseNamespace = $this->baseEntityNamespace . '\\';
134 | $length = strlen($baseNamespace);
135 |
136 | if (substr($entityClass, 0, $length) !== $baseNamespace) {
137 | throw new \LogicException(sprintf('%s is not in namespace %s.', $entityClass, $this->baseEntityNamespace));
138 | }
139 |
140 | $entityClass = substr($entityClass, $length);
141 | }
142 |
143 | return $this->getProxyNamespace() . '\\' . $entityClass . 'Proxy';
144 | }
145 |
146 | public function getProxyFileName(string $entityClass) : string
147 | {
148 | if ($this->baseEntityNamespace !== null) {
149 | $baseNamespace = $this->baseEntityNamespace . '\\';
150 | $length = strlen($baseNamespace);
151 |
152 | if (substr($entityClass, 0, $length) !== $baseNamespace) {
153 | throw new \LogicException(sprintf('%s is not in namespace %s.', $entityClass, $this->baseEntityNamespace));
154 | }
155 |
156 | $entityClass = substr($entityClass, $length);
157 | }
158 |
159 | return $this->getProxyDir() . DIRECTORY_SEPARATOR . str_replace('\\', DIRECTORY_SEPARATOR, $entityClass) . 'Proxy.php';
160 | }
161 |
162 | public function setProxyDir(string $proxyDir) : Configuration
163 | {
164 | $this->proxyDir = $proxyDir;
165 |
166 | return $this;
167 | }
168 |
169 | /**
170 | * @throws \LogicException
171 | */
172 | public function getProxyDir() : string
173 | {
174 | if ($this->proxyDir === null) {
175 | throw new \LogicException('Proxy dir is not set.');
176 | }
177 |
178 | return $this->proxyDir;
179 | }
180 |
181 | public function setRepositoryNamespace(string $repositoryNamespace) : Configuration
182 | {
183 | $this->repositoryNamespace = $repositoryNamespace;
184 |
185 | return $this;
186 | }
187 |
188 | /**
189 | * @throws \LogicException
190 | */
191 | public function getRepositoryNamespace(string|null $entityClass = null) : string
192 | {
193 | if ($this->repositoryNamespace === null) {
194 | throw new \LogicException('Repository namespace is not set.');
195 | }
196 |
197 | if ($entityClass === null) {
198 | return $this->repositoryNamespace;
199 | }
200 |
201 | if ($this->baseEntityNamespace !== null) {
202 | $baseNamespace = $this->baseEntityNamespace . '\\';
203 | $length = strlen($baseNamespace);
204 |
205 | if (substr($entityClass, 0, $length) !== $baseNamespace) {
206 | throw new \LogicException(sprintf('%s is not in namespace %s.', $entityClass, $this->baseEntityNamespace));
207 | }
208 |
209 | $entityClass = substr($entityClass, $length);
210 | }
211 |
212 | $pos = strrpos($entityClass, '\\');
213 |
214 | if ($pos === false) {
215 | return $this->repositoryNamespace;
216 | }
217 |
218 | return $this->repositoryNamespace . '\\' . substr($entityClass, 0, $pos);
219 | }
220 |
221 | public function getRepositoryFileName(string $entityClass) : string
222 | {
223 | if ($this->baseEntityNamespace !== null) {
224 | $baseNamespace = $this->baseEntityNamespace . '\\';
225 | $length = strlen($baseNamespace);
226 |
227 | if (substr($entityClass, 0, $length) !== $baseNamespace) {
228 | throw new \LogicException(sprintf('%s is not in namespace %s.', $entityClass, $this->baseEntityNamespace));
229 | }
230 |
231 | $entityClass = substr($entityClass, $length);
232 | }
233 |
234 | return $this->getRepositoryDir() . DIRECTORY_SEPARATOR . str_replace('\\', DIRECTORY_SEPARATOR, $entityClass) . 'Repository.php';
235 | }
236 |
237 | public function setRepositoryDir(string $repositoryDir) : Configuration
238 | {
239 | $this->repositoryDir = $repositoryDir;
240 |
241 | return $this;
242 | }
243 |
244 | /**
245 | * @throws \LogicException
246 | */
247 | public function getRepositoryDir() : string
248 | {
249 | if ($this->repositoryDir === null) {
250 | throw new \LogicException('Repository dir is not set.');
251 | }
252 |
253 | return $this->repositoryDir;
254 | }
255 |
256 | /**
257 | * Sets the path to the PHP file where the ClassMetadata will be stored.
258 | */
259 | public function setClassMetadataFile(string $classMetadataFile) : Configuration
260 | {
261 | if (substr($classMetadataFile, -4) !== '.php') {
262 | throw new \InvalidArgumentException('The ClassMetadata file path must have a .php extension.');
263 | }
264 |
265 | $this->classMetadataFile = $classMetadataFile;
266 |
267 | return $this;
268 | }
269 |
270 | /**
271 | * @throws \LogicException
272 | */
273 | public function getClassMetadataFile() : string
274 | {
275 | if ($this->classMetadataFile === null) {
276 | throw new \LogicException('ClassMetadata file path is not set.');
277 | }
278 |
279 | return $this->classMetadataFile;
280 | }
281 |
282 | /**
283 | * Sets the base namespace all entities live in.
284 | *
285 | * This is optional, but restricts the number of sub-namespaces (and subdirs) created for repositories and proxies.
286 | *
287 | * For example, by default App\Model\User's repository would live in RepositoryNamespace\App\Model\UserRepository,
288 | * while with a base entity namespace of App\Model it would live in RepositoryNamespace\UserRepository.
289 | */
290 | public function setBaseEntityNamespace(string $namespace) : Configuration
291 | {
292 | $this->baseEntityNamespace = $namespace;
293 |
294 | return $this;
295 | }
296 |
297 | public function getBaseEntityNamespace() : string|null
298 | {
299 | return $this->baseEntityNamespace;
300 | }
301 |
302 | /**
303 | * @psalm-param class-string $className
304 | */
305 | public function addEntity(string $className) : EntityConfiguration
306 | {
307 | $entityConfiguration = new EntityConfiguration($this, $className);
308 | $this->classes[$className] = $entityConfiguration;
309 |
310 | return $entityConfiguration;
311 | }
312 |
313 | /**
314 | * @psalm-param class-string $className
315 | */
316 | public function addEmbeddable(string $className) : EmbeddableConfiguration
317 | {
318 | $embeddableConfiguration = new EmbeddableConfiguration($this, $className);
319 | $this->classes[$className] = $embeddableConfiguration;
320 |
321 | return $embeddableConfiguration;
322 | }
323 |
324 | /**
325 | * Adds a custom mapping that applies by default to all properties of the given type.
326 | *
327 | * @psalm-param class-string $className
328 | * @psalm-param class-string $propertyMapping
329 | *
330 | * @param string $className The mapped class name.
331 | * @param string $propertyMapping The PropertyMapping implementation class name.
332 | */
333 | public function addCustomMapping(string $className, string $propertyMapping) : Configuration
334 | {
335 | $this->customMappings[$className] = $propertyMapping;
336 |
337 | return $this;
338 | }
339 |
340 | /**
341 | * @psalm-return array>
342 | */
343 | public function getCustomMappings() : array
344 | {
345 | return $this->customMappings;
346 | }
347 |
348 | /**
349 | * Adds a custom property mapping for a specific property of a given entity/embeddable class.
350 | *
351 | * @todo Naming of addCustomMapping() / setCustomPropertyMapping() is a bit confusing
352 | *
353 | * @psalm-param class-string $class
354 | */
355 | public function setCustomPropertyMapping(string $class, string $property, PropertyMapping $mapping) : Configuration
356 | {
357 | $this->customPropertyMappings[$class][$property] = $mapping;
358 |
359 | return $this;
360 | }
361 |
362 | /**
363 | * @psalm-return array>
364 | *
365 | * @return PropertyMapping[][]
366 | */
367 | public function getCustomPropertyMappings() : array
368 | {
369 | return $this->customPropertyMappings;
370 | }
371 |
372 | /**
373 | * @psalm-param class-string $class
374 | */
375 | public function setTransientProperties(string $class, string ...$properties) : Configuration
376 | {
377 | $this->transientProperties[$class] = array_values($properties);
378 |
379 | return $this;
380 | }
381 |
382 | /**
383 | * Returns the list of transient properties for the given class name.
384 | *
385 | * @psalm-param class-string $class
386 | *
387 | * @psalm-return list
388 | *
389 | * @return string[]
390 | */
391 | public function getTransientProperties(string $class) : array
392 | {
393 | return $this->transientProperties[$class] ?? [];
394 | }
395 |
396 | /**
397 | * @psalm-param class-string $class
398 | */
399 | public function setFieldName(string $class, string $property, string $fieldName) : Configuration
400 | {
401 | $this->fieldNames[$class][$property] = $fieldName;
402 |
403 | return $this;
404 | }
405 |
406 | /**
407 | * Sets custom field names for builtin type properties.
408 | *
409 | * If not set, the field name defaults to the property name.
410 | *
411 | * @psalm-return array>
412 | *
413 | * @return string[][]
414 | */
415 | public function getFieldNames() : array
416 | {
417 | return $this->fieldNames;
418 | }
419 |
420 | /**
421 | * Sets field name prefixes for entity/embeddable properties.
422 | *
423 | * If not set, the field name prefix defaults to the property name followed by an underscore character.
424 | *
425 | * @psalm-param class-string $class
426 | */
427 | public function setFieldNamePrefix(string $class, string $property, string $fieldNamePrefix) : Configuration
428 | {
429 | $this->fieldNamePrefixes[$class][$property] = $fieldNamePrefix;
430 |
431 | return $this;
432 | }
433 |
434 | /**
435 | * @psalm-return array>
436 | *
437 | * @return string[][]
438 | */
439 | public function getFieldNamePrefixes() : array
440 | {
441 | return $this->fieldNamePrefixes;
442 | }
443 |
444 | /**
445 | * Returns the class configurations, indexed by FQCN.
446 | *
447 | * @psalm-return array
448 | *
449 | * @return ClassConfiguration[]
450 | */
451 | public function getClasses() : array
452 | {
453 | return $this->classes;
454 | }
455 |
456 | /**
457 | * Returns the entity configurations, indexed by FQCN.
458 | *
459 | * @psalm-return array
460 | *
461 | * @return EntityConfiguration[]
462 | */
463 | public function getEntities() : array
464 | {
465 | $entityConfigurations = [];
466 |
467 | foreach ($this->classes as $className => $classConfiguration) {
468 | if ($classConfiguration instanceof EntityConfiguration) {
469 | $entityConfigurations[$className] = $classConfiguration;
470 | }
471 | }
472 |
473 | return $entityConfigurations;
474 | }
475 | }
476 |
--------------------------------------------------------------------------------
/src/EmbeddableConfiguration.php:
--------------------------------------------------------------------------------
1 | |null
21 | *
22 | * @var string[]|null
23 | */
24 | private array|null $identityProperties = null;
25 |
26 | /**
27 | * The discriminator column name, or null if not set.
28 | */
29 | private string|null $discriminatorColumn = null;
30 |
31 | /**
32 | * A map of discriminator values to entity class names, or an empty array if not set.
33 | *
34 | * @psalm-var array
35 | *
36 | * @var string[]
37 | */
38 | private array $discriminatorMap = [];
39 |
40 | /**
41 | * Sets the root entity of the aggregate this entity belongs to.
42 | *
43 | * @psalm-param class-string $className
44 | */
45 | public function belongsTo(string $className) : EntityConfiguration
46 | {
47 | $this->belongsTo = $className;
48 |
49 | return $this;
50 | }
51 |
52 | public function getBelongsTo() : string|null
53 | {
54 | return $this->belongsTo;
55 | }
56 |
57 | /**
58 | * Sets the table name.
59 | *
60 | * If not set, it will default to the entity short name (i.e. the name without the namespace).
61 | */
62 | public function setTableName(string $tableName) : EntityConfiguration
63 | {
64 | $this->tableName = $tableName;
65 |
66 | return $this;
67 | }
68 |
69 | /**
70 | * Returns the table name.
71 | *
72 | * If not set, it will default to the entity short name (i.e. the name without the namespace).
73 | */
74 | public function getTableName() : string
75 | {
76 | if ($this->tableName !== null) {
77 | return $this->tableName;
78 | }
79 |
80 | return $this->reflectionClass->getShortName();
81 | }
82 |
83 | /**
84 | * Sets whether the database table uses an auto-increment identity field.
85 | */
86 | public function setAutoIncrement() : EntityConfiguration
87 | {
88 | $this->isAutoIncrement = true;
89 |
90 | return $this;
91 | }
92 |
93 | /**
94 | * Returns whether the database table uses an auto-increment identity field.
95 | *
96 | * @throws \LogicException
97 | */
98 | public function isAutoIncrement() : bool
99 | {
100 | if ($this->isAutoIncrement) {
101 | $identityProperties = $this->getIdentityProperties();
102 |
103 | if (count($identityProperties) !== 1) {
104 | throw new \LogicException(sprintf(
105 | 'The entity "%s" has multiple identity properties and cannot be mapped to an auto-increment table.',
106 | $this->getClassName()
107 | ));
108 | }
109 |
110 | $reflectionProperty = $this->reflectionClass->getProperty($identityProperties[0]);
111 |
112 | $propertyType = $reflectionProperty->getType();
113 |
114 | if ($propertyType instanceof ReflectionNamedType) {
115 | $type = $propertyType->getName();
116 |
117 | if ($type !== 'int' && $type !== 'string') {
118 | throw new \LogicException(sprintf(
119 | 'The entity "%s" has an auto-increment identity that maps to an unsupported type "%s", ' .
120 | 'only int and string are allowed.',
121 | $this->getClassName(),
122 | $type
123 | ));
124 | }
125 | } else {
126 | throw new \LogicException(sprintf(
127 | 'The entity "%s" has an auto-increment identity that maps to an untyped or union type property, ' .
128 | 'only int and string are allowed.',
129 | $this->getClassName()
130 | ));
131 | }
132 | }
133 |
134 | return $this->isAutoIncrement;
135 | }
136 |
137 | /**
138 | * @throws \InvalidArgumentException
139 | */
140 | public function setIdentityProperties(string ...$identityProperties) : EntityConfiguration
141 | {
142 | if (count($identityProperties) === 0) {
143 | throw new \InvalidArgumentException('The list of identity properties cannot be empty.');
144 | }
145 |
146 | $identityProperties = array_values($identityProperties);
147 |
148 | $this->checkProperties($identityProperties);
149 |
150 | $this->identityProperties = $identityProperties;
151 |
152 | return $this;
153 | }
154 |
155 | /**
156 | * Returns the list of properties that are part of the entity's identity.
157 | *
158 | * @psalm-return list
159 | *
160 | * @return string[]
161 | *
162 | * @throws \LogicException
163 | */
164 | public function getIdentityProperties() : array
165 | {
166 | if ($this->identityProperties === null) {
167 | throw new \LogicException(sprintf('No identity properties have been set for class %s.', $this->getClassName()));
168 | }
169 |
170 | foreach ($this->identityProperties as $identityProperty) {
171 | if (! in_array($identityProperty, $this->getPersistentProperties())) {
172 | throw new \LogicException(sprintf('Identity property $%s in class %s is not persistent.', $identityProperty, $this->getClassName()));
173 | }
174 | }
175 |
176 | return $this->identityProperties;
177 | }
178 |
179 | /**
180 | * Sets the inheritance mapping for this entity.
181 | *
182 | * Every persistable class in the hierarchy must have an entry in the discriminator map. This excludes abstract
183 | * classes, and root classes that are common to several entities (so-called MappedSuperclass in other ORM).
184 | *
185 | * Note: only single table inheritance is supported for now.
186 | *
187 | * @psalm-param array $discriminatorMap
188 | *
189 | * @param string $discriminatorColumn The discriminator column name.
190 | * @param array $discriminatorMap A map of discriminator value to concrete entity class name.
191 | *
192 | * @throws \InvalidArgumentException If the discriminator map is empty, a class name does not exist, or is not a subclass of the root entity class.
193 | */
194 | public function setInheritanceMapping(string $discriminatorColumn, array $discriminatorMap) : EntityConfiguration
195 | {
196 | if (! $discriminatorMap) {
197 | throw new \InvalidArgumentException('The discriminator map cannot be empty.');
198 | }
199 |
200 | $rootEntityClassName = $this->reflectionClass->getName();
201 |
202 | foreach ($discriminatorMap as $discriminatorValue => $className) {
203 | try {
204 | $reflectionClass = new \ReflectionClass($className);
205 | } catch (\ReflectionException $e) {
206 | throw new \InvalidArgumentException(sprintf('%s does not exist.', $className), 0, $e);
207 | }
208 |
209 | if ($reflectionClass->isAbstract()) {
210 | throw new \InvalidArgumentException(sprintf('Abstract class %s cannot be part of the discriminator map.', $reflectionClass->getName()));
211 | }
212 |
213 | if ($reflectionClass->getName() !== $rootEntityClassName && ! $reflectionClass->isSubclassOf($rootEntityClassName)) {
214 | throw new \InvalidArgumentException(sprintf('%s is not a subclass of %s and cannot be part of its discriminator map.', $reflectionClass->getName(), $rootEntityClassName));
215 | }
216 |
217 | // Override to fix potential wrong case
218 | $discriminatorMap[$discriminatorValue] = $reflectionClass->getName();
219 | }
220 |
221 | // Check that values are unique
222 | if (count(array_unique($discriminatorMap)) !== count($discriminatorMap)) {
223 | throw new \InvalidArgumentException('Duplicate class names in discriminator map.');
224 | }
225 |
226 | $this->discriminatorColumn = $discriminatorColumn;
227 | $this->discriminatorMap = $discriminatorMap;
228 |
229 | return $this;
230 | }
231 |
232 | /**
233 | * Returns the discriminator column name, or null if inheritance is not in use.
234 | */
235 | public function getDiscriminatorColumn() : string|null
236 | {
237 | return $this->discriminatorColumn;
238 | }
239 |
240 | /**
241 | * Returns a map of discriminator values to fully-qualified entity class names.
242 | *
243 | * If no inheritance is mapped, an empty array is returned.
244 | *
245 | * @psalm-return array
246 | */
247 | public function getDiscriminatorMap() : array
248 | {
249 | return $this->discriminatorMap;
250 | }
251 |
252 | /**
253 | * Returns the list of classes part of the hierarchy, starting with the root class (this entity).
254 | *
255 | * If this entity is part of an inheritance hierarchy, the result includes all the classes in the discriminator map,
256 | * plus any abstract class present between the root class and these classes.
257 | *
258 | * If this entity is not part of an inheritance hierarchy, an array with a single ReflectionClass instance, for this
259 | * entity, is returned.
260 | *
261 | * @psalm-return class-string[]
262 | *
263 | * @return string[] The list of all class names in the hierarchy.
264 | */
265 | public function getClassHierarchy() : array
266 | {
267 | $classes = [
268 | $this->getClassName() // root entity
269 | ];
270 |
271 | foreach ($this->discriminatorMap as $className) {
272 | $reflectionClass = new \ReflectionClass($className);
273 |
274 | while ($reflectionClass->getName() !== $this->getClassName()) {
275 | $classes[] = $reflectionClass->getName();
276 | $reflectionClass = $reflectionClass->getParentClass();
277 | }
278 | }
279 |
280 | return array_values(array_unique($classes));
281 | }
282 |
283 | /**
284 | * @psalm-param list $properties
285 | *
286 | * @param string[] $properties The list of property names to check.
287 | *
288 | * @throws \InvalidArgumentException If a property does not exist.
289 | */
290 | private function checkProperties(array $properties) : void
291 | {
292 | foreach ($properties as $property) {
293 | try {
294 | $reflectionProperty = $this->reflectionClass->getProperty($property);
295 | } catch (\ReflectionException $e) {
296 | throw new \InvalidArgumentException(sprintf('Class %s has no property named $%s.', $this->getClassName(), $property), 0, $e);
297 | }
298 |
299 | if ($reflectionProperty->isStatic()) {
300 | throw new \InvalidArgumentException(sprintf('%s::$%s is static; static properties cannot be persisted.', $this->getClassName(), $property));
301 | }
302 |
303 | if ($reflectionProperty->isPrivate()) {
304 | throw new \InvalidArgumentException(sprintf('%s::$%s is private; private properties are not supported.', $this->getClassName(), $property));
305 | }
306 | }
307 | }
308 | }
309 |
--------------------------------------------------------------------------------
/src/EntityMetadata.php:
--------------------------------------------------------------------------------
1 |
34 | */
35 | public array $discriminatorMap;
36 |
37 | /**
38 | * The inverse discriminator map.
39 | *
40 | * The keys are class names, the values are discriminator values (as string or int).
41 | * If the entity is not part of an inheritance hierarchy, this will be an empty array.
42 | *
43 | * This property is set, and is the same, for all classes in the inheritance hiearchy.
44 | *
45 | * @psalm-var array
46 | */
47 | public array $inverseDiscriminatorMap;
48 |
49 | /**
50 | * The root entity class name.
51 | *
52 | * If the entity is not part of an inheritance hierarchy, or is itself the root of the hierarchy, this will be the
53 | * same as the entity class name.
54 | *
55 | * @psalm-var class-string
56 | */
57 | public string $rootClassName;
58 |
59 | /**
60 | * The entity's proxy class name.
61 | *
62 | * This property is only set if the class is a concrete entity.
63 | * For abstract entities, this property will be null.
64 | *
65 | * @psalm-var class-string|null
66 | */
67 | public string|null $proxyClassName;
68 |
69 | /**
70 | * The database table name.
71 | */
72 | public string $tableName;
73 |
74 | /**
75 | * The list of persistent property names that are part of the identity.
76 | *
77 | * This list must not intersect with $nonIdProperties.
78 | * The union of $idProperties and $nonIdProperties must be equal to $properties.
79 | *
80 | * @psalm-var list
81 | *
82 | * @var string[]
83 | */
84 | public array $idProperties;
85 |
86 | /**
87 | * The list of persistent property names that are NOT part of the identity.
88 | *
89 | * This list must not intersect with $idProperties.
90 | * The union of $idProperties and $nonIdProperties must be equal to $properties.
91 | *
92 | * @psalm-var list
93 | *
94 | * @var string[]
95 | */
96 | public array $nonIdProperties;
97 |
98 | /**
99 | * The list of persistent property names that are not part of the identity, and are declared in this class only.
100 | *
101 | * Properties declared in parent classes are not included here.
102 | *
103 | * @psalm-var list
104 | *
105 | * @var string[]
106 | */
107 | public array $selfNonIdProperties;
108 |
109 | /**
110 | * The list of child entity class names, if any.
111 | *
112 | * @psalm-var list
113 | *
114 | * @var string[]
115 | */
116 | public array $childClasses;
117 |
118 | /**
119 | * Whether the table uses an auto-increment primary key.
120 | *
121 | * This is only supported on tables with a single primary key column. If this is true, there must be only one
122 | * property part of the identity, and this property must map to a single database field.
123 | */
124 | public bool $isAutoIncrement;
125 | }
126 |
--------------------------------------------------------------------------------
/src/Exception/EntityNotFoundException.php:
--------------------------------------------------------------------------------
1 | $scalarIdentity
18 | *
19 | * @param string $className The entity class name.
20 | * @param array $scalarIdentity The identity, as a list of int or string values.
21 | */
22 | public static function entityNotFound(string $className, array $scalarIdentity) : self
23 | {
24 | return new self(sprintf(
25 | 'The entity %s with identity %s was not found.',
26 | $className,
27 | self::exportScalarIdentity($scalarIdentity)
28 | ));
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/src/Exception/IdentityConflictException.php:
--------------------------------------------------------------------------------
1 | $scalarIdentity
15 | *
16 | * @param string $className The entity class name.
17 | * @param array $scalarIdentity The identity, as a list of scalar values.
18 | */
19 | public static function identityMapConflict(string $className, array $scalarIdentity) : self
20 | {
21 | return new self(sprintf(
22 | 'The instance of entity type %s with identity %s cannot be added to the identity map, ' .
23 | 'because another instance of this type with the same identity already exists.',
24 | $className,
25 | self::exportScalarIdentity($scalarIdentity)
26 | ));
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/src/Exception/NoIdentityException.php:
--------------------------------------------------------------------------------
1 | $scalarIdentity
38 | *
39 | * @param array $scalarIdentity The identity, as a list of scalar values. Must contain at least one entry.
40 | * Each entry must be an int or a string.
41 | */
42 | protected static function exportScalarIdentity(array $scalarIdentity) : string
43 | {
44 | $result = [];
45 |
46 | foreach ($scalarIdentity as $value) {
47 | if (is_string($value)) {
48 | $result[] = self::exportString($value);
49 | } else {
50 | $result[] = (string) $value;
51 | }
52 | }
53 |
54 | $result = implode(', ', $result);
55 |
56 | if (count($scalarIdentity) === 1) {
57 | return $result;
58 | }
59 |
60 | return '[' . $result . ']';
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/src/Exception/UnknownEntityClassException.php:
--------------------------------------------------------------------------------
1 | $identity
37 | *
38 | * @param string $class The root entity class name.
39 | * @param array $identity The list of scalar values that form the entity's identity.
40 | *
41 | * @return object|null The entity, or null if not found.
42 | */
43 | public function get(string $class, array $identity) : object|null
44 | {
45 | $ref = & $this->entities[$class];
46 |
47 | foreach ($identity as $key) {
48 | $ref = & $ref[$key];
49 | }
50 |
51 | return $ref;
52 | }
53 |
54 | /**
55 | * Adds an entity to the identity map.
56 | *
57 | * If the entity already exists in the identity map, this method does nothing.
58 | * If another entity already exists under this identity, an exception is thrown.
59 | *
60 | * @psalm-suppress MixedArrayAccess
61 | *
62 | * @psalm-param class-string $class
63 | * @psalm-param list $identity
64 | *
65 | * @param string $class The root entity class name.
66 | * @param array $identity The list of scalar values that form the entity's identity.
67 | * @param object $entity The entity to add.
68 | *
69 | * @throws IdentityConflictException If another instance with the same identity already exists.
70 | */
71 | public function set(string $class, array $identity, object $entity) : void
72 | {
73 | $ref = & $this->entities[$class];
74 |
75 | foreach ($identity as $key) {
76 | $ref = & $ref[$key];
77 | }
78 |
79 | if ($ref !== null && $ref !== $entity) {
80 | throw IdentityConflictException::identityMapConflict($class, $identity);
81 | }
82 |
83 | $ref = $entity;
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/src/ObjectFactory.php:
--------------------------------------------------------------------------------
1 |
25 | *
26 | * @var ReflectionClass[]
27 | */
28 | private array $classes = [];
29 |
30 | /**
31 | * A map of full qualified class name to map of property name to Closure.
32 | *
33 | * Each closure converts a property value to the correct type.
34 | *
35 | * @psalm-var array>
36 | *
37 | * @var Closure[][]
38 | */
39 | private array $propertyConverters = [];
40 |
41 | /**
42 | * Instantiates an empty object, without calling the class constructor.
43 | *
44 | * By default, this method returns an object whose *persistent* properties are not initialized.
45 | * Transient properties are still initialized to their default value, if any.
46 | * Properties may be initialized by passing a map of property name to value.
47 | *
48 | * @psalm-param array $values
49 | *
50 | * @param ClassMetadata $classMetadata The class metadata of the entity or embeddable.
51 | * @param array $values An optional map of property name to value to write.
52 | *
53 | * @throws \ReflectionException If the class does not exist.
54 | */
55 | public function instantiate(ClassMetadata $classMetadata, array $values = []) : object
56 | {
57 | $className = $classMetadata->className;
58 |
59 | if (isset($this->classes[$className])) {
60 | $reflectionClass = $this->classes[$className];
61 | } else {
62 | $reflectionClass = $this->classes[$className] = new \ReflectionClass($className);
63 | }
64 |
65 | $object = $reflectionClass->newInstanceWithoutConstructor();
66 |
67 | /** @psalm-suppress PossiblyNullFunctionCall bindTo() should never return null here */
68 | (function() use ($classMetadata, $values, $reflectionClass) {
69 | // Unset persistent properties
70 | // @todo PHP 7.4: for even better performance, only unset typed properties that have a default value, as
71 | // unset() will have no effect on those that have no default value (will require a new metadata prop).
72 | foreach ($classMetadata->properties as $prop) {
73 | unset($this->{$prop});
74 | }
75 |
76 | // Set values
77 | foreach ($values as $key => $value) {
78 | if ($value === null) {
79 | // @todo temporary fix: do not set null values when typed property is not nullable;
80 | // needs investigation to see why these null values are being passed in the first place
81 |
82 | $reflectionType = $reflectionClass->getProperty($key)->getType();
83 |
84 | if ($reflectionType !== null && ! $reflectionType->allowsNull()) {
85 | continue;
86 | }
87 | }
88 |
89 | $this->{$key} = $value;
90 | }
91 | })->bindTo($object, $className)();
92 |
93 | return $object;
94 | }
95 |
96 | /**
97 | * Instantiates a data transfer object with a nested array of scalar values.
98 | *
99 | * The class must have public properties only, and no constructor.
100 | *
101 | * @psalm-suppress MixedMethodCall We know that DTOs have no constructor.
102 | *
103 | * @template T
104 | *
105 | * @psalm-param class-string $className
106 | * @psalm-param array $values
107 | *
108 | * @psalm-return T
109 | *
110 | * @throws \ReflectionException If the class does not exist.
111 | * @throws \InvalidArgumentException If the class is not a valid DTO or an unexpected value is found.
112 | */
113 | public function instantiateDTO(string $className, array $values) : object
114 | {
115 | $propertyConverters = $this->getPropertyConverters($className);
116 |
117 | $object = new $className;
118 |
119 | foreach ($values as $name => $value) {
120 | if (! isset($propertyConverters[$name])) {
121 | throw new \InvalidArgumentException(sprintf('There is no property named $%s in class %s.', $name, $className));
122 | }
123 |
124 | $propertyConverter = $propertyConverters[$name];
125 | $object->{$name} = $propertyConverter($value);
126 | }
127 |
128 | return $object;
129 | }
130 |
131 | /**
132 | * Returns the property converters for the given class, indexed by property name.
133 | *
134 | * @psalm-param class-string $className
135 | *
136 | * @psalm-return array
137 | *
138 | * @return Closure[]
139 | *
140 | * @throws \ReflectionException If the class does not exist.
141 | * @throws \InvalidArgumentException If the class is not a valid DTO or an unexpected value is found.
142 | */
143 | private function getPropertyConverters(string $className) : array
144 | {
145 | if (isset($this->propertyConverters[$className])) {
146 | return $this->propertyConverters[$className];
147 | }
148 |
149 | $reflectionClass = new \ReflectionClass($className);
150 |
151 | if ($reflectionClass->isAbstract()) {
152 | throw new \InvalidArgumentException(sprintf('Cannot instantiate abstract class %s.', $className));
153 | }
154 |
155 | if ($reflectionClass->isInterface()) {
156 | throw new \InvalidArgumentException(sprintf('Cannot instantiate interface %s.', $className));
157 | }
158 |
159 | if ($reflectionClass->isInternal()) {
160 | throw new \InvalidArgumentException(sprintf('Cannot instantiate internal class %s.', $className));
161 | }
162 |
163 | if ($reflectionClass->getConstructor() !== null) {
164 | throw new \InvalidArgumentException(sprintf('Class %s must not have a constructor.', $className));
165 | }
166 |
167 | $properties = $reflectionClass->getProperties();
168 |
169 | $result = [];
170 |
171 | foreach ($properties as $property) {
172 | $name = $property->getName();
173 |
174 | if ($property->isStatic()) {
175 | throw new \InvalidArgumentException(sprintf('Property $%s of class %s must not be static.', $name, $className));
176 | }
177 |
178 | if (! $property->isPublic()) {
179 | throw new \InvalidArgumentException(sprintf('Property $%s of class %s must be public.', $name, $className));
180 | }
181 |
182 | $result[$name] = $this->getPropertyValueConverter($property);
183 | }
184 |
185 | $this->propertyConverters[$className] = $result;
186 |
187 | return $result;
188 | }
189 |
190 | /**
191 | * @psalm-return Closure(mixed): mixed
192 | *
193 | * @psalm-suppress MissingClosureParamType
194 | * @psalm-suppress MissingClosureReturnType
195 | *
196 | * @throws \InvalidArgumentException If an unexpected value is found.
197 | */
198 | private function getPropertyValueConverter(\ReflectionProperty $property) : Closure
199 | {
200 | $type = $property->getType();
201 |
202 | $propertyName = $property->getName();
203 | $className = $property->getDeclaringClass()->getName();
204 |
205 | if ($type instanceof ReflectionNamedType) {
206 | $propertyType = $type->getName();
207 |
208 | if ($type->isBuiltin()) {
209 | return match ($propertyType) {
210 | 'string' => fn ($value) => $value,
211 | 'int' => fn ($value) => (int) $value,
212 | 'float' => fn ($value) => (float) $value,
213 | 'bool' => fn ($value) => (bool) $value,
214 | default => throw new \InvalidArgumentException(sprintf('Unexpected non-scalar type "%s" for property $%s in class %s.', $propertyType, $propertyName, $className))
215 | };
216 | }
217 |
218 | return function($value) use ($propertyName, $className, $type) {
219 | if (! is_array($value)) {
220 | throw new \InvalidArgumentException(sprintf('Expected array for property $%s of class %s, got %s.', $propertyName, $className, gettype($value)));
221 | }
222 |
223 | /** @psalm-var array $value */
224 | return $this->instantiateDTO($type->getName(), $value);
225 | };
226 | }
227 |
228 | return fn ($value) => $value;
229 | }
230 |
231 | /**
232 | * Reads *initialized* object properties.
233 | *
234 | * Properties that are not initialized, or have been unset(), are not included in the array.
235 | * This method assumes that there are no private properties in parent classes.
236 | *
237 | * @psalm-suppress MixedInferredReturnType
238 | * @psalm-suppress MixedReturnStatement
239 | *
240 | * @psalm-return array
241 | *
242 | * @param object $object The object to read.
243 | *
244 | * @return array A map of property names to values.
245 | */
246 | public function read(object $object) : array
247 | {
248 | /** @psalm-suppress PossiblyNullFunctionCall bindTo() should never return null here */
249 | return (function() {
250 | return get_object_vars($this);
251 | })->bindTo($object, $object)();
252 | }
253 |
254 | /**
255 | * Writes an object's properties.
256 | *
257 | * This method does not support writing private properties in parent classes.
258 | *
259 | * @psalm-param array $values
260 | *
261 | * @param object $object The object to write.
262 | * @param array $values A map of property names to values.
263 | */
264 | public function write(object $object, array $values) : void
265 | {
266 | /** @psalm-suppress PossiblyNullFunctionCall bindTo() should never return null here */
267 | (function() use ($values) {
268 | foreach ($values as $key => $value) {
269 | $this->{$key} = $value;
270 | }
271 | })->bindTo($object, $object)();
272 | }
273 | }
274 |
--------------------------------------------------------------------------------
/src/Options.php:
--------------------------------------------------------------------------------
1 |
26 | *
27 | * @return string[]
28 | */
29 | public function getFieldNames() : array;
30 |
31 | /**
32 | * Returns the number of database values required to compute the property value.
33 | *
34 | * This must return the number entries returned by getFieldToInputValuesSQL().
35 | */
36 | public function getInputValuesCount() : int;
37 |
38 | /**
39 | * Returns the SQL expressions to read each database value required to compute the property value.
40 | *
41 | * The input array will contain exactly one string for each field returned by getFieldNames().
42 | * It will contain these field names, in the same order, with possible prefixes and quoting.
43 | *
44 | * The result array must contain exactly one string for each value required by convertInputValuesToProp(), in the
45 | * same order.
46 | *
47 | * This method may return more values that the number of field names, if several SQL functions have to be called on
48 | * a single field to load the property; for example, loading a Geometry could require selecting both ST_AsText()
49 | * and ST_SRID() on a single field.
50 | *
51 | * If no transformation is required, this method should return the input parameter unchanged.
52 | *
53 | * @psalm-param list $fieldNames
54 | *
55 | * @psalm-return list
56 | *
57 | * @param string[] $fieldNames The field names.
58 | *
59 | * @return string[] The list of fields to read, optionally wrapped with SQL code.
60 | */
61 | public function getFieldToInputValuesSQL(array $fieldNames) : array;
62 |
63 | /**
64 | * Converts the given database values to a property value.
65 | *
66 | * The input array will contain one value for each getFieldToInputValuesSQL() entry, in the same order.
67 | *
68 | * @psalm-param list $values
69 | *
70 | * @param mixed[] $values The list of database values.
71 | *
72 | * @return mixed The property value.
73 | */
74 | public function convertInputValuesToProp(Gateway $gateway, array $values) : mixed;
75 |
76 | /**
77 | * Converts the given property to SQL expressions and values for each database field it is mapped to.
78 | *
79 | * The result array must contain exactly one entry for each field returned by getFieldNames(), in the same order.
80 | * Each entry must be a numeric array whose first entry is a string containing an SQL expression, and whose further
81 | * entries are values to be bound for each question mark placeholder the SQL expression contains.
82 | *
83 | * Example for a simple scalar value mapping:
84 | * [
85 | * ['?', $value]
86 | * ]
87 | *
88 | * Example for a geometry object, mapping 2 values to a single field:
89 | * [
90 | * ['ST_GeomFromText(?, ?)', $wkt, $srid]
91 | * ]
92 | *
93 | * Example for a complex property mapping many values to 4 fields:
94 | * [
95 | * ['NULL'],
96 | * ['?', $value1],
97 | * ['?', $value2],
98 | * ['ST_GeomFromText(?, ?)', $wkt, $srid]
99 | * ]
100 | *
101 | * @psalm-return list>
102 | *
103 | * @param mixed $propValue The property value.
104 | */
105 | public function convertPropToFields(mixed $propValue) : array;
106 | }
107 |
--------------------------------------------------------------------------------
/src/PropertyMapping/BoolMapping.php:
--------------------------------------------------------------------------------
1 | fieldName = $fieldName;
21 | $this->isNullable = $isNullable;
22 | }
23 |
24 | public function isNullable() : bool
25 | {
26 | return $this->isNullable;
27 | }
28 |
29 | public function getFieldNames() : array
30 | {
31 | return [$this->fieldName];
32 | }
33 |
34 | public function getInputValuesCount() : int
35 | {
36 | return 1;
37 | }
38 |
39 | public function getFieldToInputValuesSQL(array $fieldNames) : array
40 | {
41 | return $fieldNames;
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/src/PropertyMapping/EmbeddableMapping.php:
--------------------------------------------------------------------------------
1 | classMetadata = $classMetadata;
35 | $this->fieldNamePrefix = $fieldNamePrefix;
36 | $this->isNullable = $isNullable;
37 | }
38 |
39 | public function getType() : string|null
40 | {
41 | return $this->classMetadata->className;
42 | }
43 |
44 | public function isNullable() : bool
45 | {
46 | return $this->isNullable;
47 | }
48 |
49 | /**
50 | * @todo precompute for better performance
51 | */
52 | public function getFieldNames() : array
53 | {
54 | $names = [];
55 |
56 | foreach ($this->classMetadata->properties as $prop) {
57 | foreach ($this->classMetadata->propertyMappings[$prop]->getFieldNames() as $name) {
58 | $names[] = $this->fieldNamePrefix . $name;
59 | }
60 | }
61 |
62 | return $names;
63 | }
64 |
65 | /**
66 | * @todo precompute for better performance
67 | */
68 | public function getInputValuesCount() : int
69 | {
70 | $count = 0;
71 |
72 | foreach ($this->classMetadata->properties as $prop) {
73 | $propertyMapping = $this->classMetadata->propertyMappings[$prop];
74 | $count += $propertyMapping->getInputValuesCount();
75 | }
76 |
77 | return $count;
78 | }
79 |
80 | /**
81 | * @todo precompute for better performance
82 | */
83 | public function getFieldToInputValuesSQL(array $fieldNames) : array
84 | {
85 | $wrappedFields = [];
86 | $currentIndex = 0;
87 |
88 | foreach ($this->classMetadata->properties as $prop) {
89 | $propertyMapping = $this->classMetadata->propertyMappings[$prop];
90 | $readFieldCount = $propertyMapping->getInputValuesCount();
91 |
92 | $currentFieldNames = array_slice($fieldNames, $currentIndex, $readFieldCount);
93 | $currentIndex += $readFieldCount;
94 |
95 | foreach ($propertyMapping->getFieldToInputValuesSQL($currentFieldNames) as $wrappedField) {
96 | $wrappedFields[] = $wrappedField;
97 | }
98 | }
99 |
100 | return $wrappedFields;
101 | }
102 |
103 | public function convertInputValuesToProp(Gateway $gateway, array $values) : mixed
104 | {
105 | $currentIndex = 0;
106 |
107 | $propValues = [];
108 |
109 | foreach ($this->classMetadata->properties as $prop) {
110 | $propertyMapping = $this->classMetadata->propertyMappings[$prop];
111 | $readFieldCount = $propertyMapping->getInputValuesCount();
112 |
113 | $currentInputValues = array_slice($values, $currentIndex, $readFieldCount);
114 | $currentIndex += $readFieldCount;
115 |
116 | $propValues[$prop] = $propertyMapping->convertInputValuesToProp($gateway, $currentInputValues);
117 | }
118 |
119 | // @todo keep an ObjectFactory cache.
120 | $objectFactory = new ObjectFactory();
121 |
122 | return $objectFactory->instantiate($this->classMetadata, $propValues);
123 | }
124 |
125 | public function convertPropToFields(mixed $propValue) : array
126 | {
127 | $result = [];
128 |
129 | /** @var object|null $entity */
130 | $entity = $propValue;
131 |
132 | $r = null;
133 |
134 | foreach ($this->classMetadata->properties as $prop) {
135 | if ($entity === null) {
136 | $idPropValue = null;
137 | } else {
138 | if ($r === null) {
139 | $r = new ReflectionObject($entity);
140 | }
141 |
142 | $p = $r->getProperty($prop);
143 | $idPropValue = $p->getValue($entity);
144 | }
145 |
146 | foreach ($this->classMetadata->propertyMappings[$prop]->convertPropToFields($idPropValue) as $expressionAndValues) {
147 | $result[] = $expressionAndValues;
148 | }
149 | }
150 |
151 | return $result;
152 | }
153 | }
154 |
--------------------------------------------------------------------------------
/src/PropertyMapping/EntityMapping.php:
--------------------------------------------------------------------------------
1 | classMetadata = $classMetadata;
33 | $this->fieldNamePrefix = $fieldNamePrefix;
34 | $this->isNullable = $isNullable;
35 | }
36 |
37 | public function getType() : string|null
38 | {
39 | return $this->classMetadata->className;
40 | }
41 |
42 | public function isNullable() : bool
43 | {
44 | return $this->isNullable;
45 | }
46 |
47 | /**
48 | * @todo precompute for better performance
49 | */
50 | public function getFieldNames() : array
51 | {
52 | $names = [];
53 |
54 | if ($this->classMetadata->discriminatorColumn !== null) {
55 | $names[] = $this->fieldNamePrefix . $this->classMetadata->discriminatorColumn;
56 | }
57 |
58 | foreach ($this->classMetadata->idProperties as $prop) {
59 | foreach ($this->classMetadata->propertyMappings[$prop]->getFieldNames() as $name) {
60 | $names[] = $this->fieldNamePrefix . $name;
61 | }
62 | }
63 |
64 | return $names;
65 | }
66 |
67 | /**
68 | * @todo precompute for better performance
69 | */
70 | public function getInputValuesCount() : int
71 | {
72 | $count = 0;
73 |
74 | if ($this->classMetadata->discriminatorColumn !== null) {
75 | $count++;
76 | }
77 |
78 | foreach ($this->classMetadata->idProperties as $prop) {
79 | $propertyMapping = $this->classMetadata->propertyMappings[$prop];
80 | $count += $propertyMapping->getInputValuesCount();
81 | }
82 |
83 | return $count;
84 | }
85 |
86 | /**
87 | * @todo precompute for better performance
88 | */
89 | public function getFieldToInputValuesSQL(array $fieldNames) : array
90 | {
91 | $wrappedFields = [];
92 | $currentIndex = 0;
93 |
94 | if ($this->classMetadata->discriminatorColumn !== null) {
95 | $wrappedFields[] = $fieldNames[$currentIndex++];
96 | }
97 |
98 | foreach ($this->classMetadata->idProperties as $prop) {
99 | $propertyMapping = $this->classMetadata->propertyMappings[$prop];
100 | $readFieldCount = $propertyMapping->getInputValuesCount();
101 |
102 | $currentFieldNames = array_slice($fieldNames, $currentIndex, $readFieldCount);
103 | $currentIndex += $readFieldCount;
104 |
105 | foreach ($propertyMapping->getFieldToInputValuesSQL($currentFieldNames) as $wrappedField) {
106 | $wrappedFields[] = $wrappedField;
107 | }
108 | }
109 |
110 | return $wrappedFields;
111 | }
112 |
113 | public function convertInputValuesToProp(Gateway $gateway, array $values) : mixed
114 | {
115 | $currentIndex = 0;
116 |
117 | if ($this->classMetadata->discriminatorColumn !== null) {
118 | /** @var int|string|null $discriminatorValue */
119 | $discriminatorValue = $values[$currentIndex++];
120 |
121 | if ($discriminatorValue === null) {
122 | return null;
123 | }
124 |
125 | $className = $this->classMetadata->discriminatorMap[$discriminatorValue];
126 | } else {
127 | $className = $this->classMetadata->className;
128 | }
129 |
130 | $id = [];
131 |
132 | foreach ($this->classMetadata->idProperties as $prop) {
133 | $propertyMapping = $this->classMetadata->propertyMappings[$prop];
134 | $readFieldCount = $propertyMapping->getInputValuesCount();
135 |
136 | $currentInputValues = array_slice($values, $currentIndex, $readFieldCount);
137 | $currentIndex += $readFieldCount;
138 |
139 | $value = $propertyMapping->convertInputValuesToProp($gateway, $currentInputValues);
140 |
141 | if ($value === null) {
142 | return null;
143 | }
144 |
145 | $id[$prop] = $value;
146 | }
147 |
148 | return $gateway->getReference($className, $id);
149 | }
150 |
151 | /**
152 | * @todo use Gateway::getIdentity() instead; currently does not check that the object has an identity
153 | */
154 | public function convertPropToFields(mixed $propValue) : array
155 | {
156 | $result = [];
157 |
158 | /** @var object|null $entity */
159 | $entity = $propValue;
160 |
161 | if ($this->classMetadata->discriminatorColumn !== null) {
162 | if ($entity === null) {
163 | $result[] = ['NULL'];
164 | } else {
165 | $class = get_class($entity);
166 | $discriminatorValue = $this->classMetadata->inverseDiscriminatorMap[$class];
167 | $result[] = ['?', $discriminatorValue];
168 | }
169 | }
170 |
171 | $idProperties = $this->classMetadata->idProperties;
172 |
173 | $identity = [];
174 |
175 | if ($entity !== null) {
176 | /** @psalm-suppress PossiblyNullFunctionCall bindTo() should never return null here */
177 | (function() use ($idProperties, & $identity) {
178 | foreach ($idProperties as $prop) {
179 | $identity[$prop] = $this->{$prop};
180 | }
181 | })->bindTo($entity, $entity)();
182 | } else {
183 | foreach ($idProperties as $prop) {
184 | $identity[$prop] = null;
185 | }
186 | }
187 |
188 | foreach ($idProperties as $prop) {
189 | $propertyMapping = $this->classMetadata->propertyMappings[$prop];
190 |
191 | foreach ($propertyMapping->convertPropToFields($identity[$prop]) as $expressionAndValues) {
192 | $result[] = $expressionAndValues;
193 | }
194 | }
195 |
196 | return $result;
197 | }
198 | }
199 |
--------------------------------------------------------------------------------
/src/PropertyMapping/IntMapping.php:
--------------------------------------------------------------------------------
1 | fieldName = $fieldName;
27 | $this->isNullable = $isNullable;
28 | $this->objectAsArray = $objectAsArray;
29 | }
30 |
31 | public function isNullable() : bool
32 | {
33 | return $this->isNullable;
34 | }
35 |
36 | public function getFieldNames() : array
37 | {
38 | return [$this->fieldName];
39 | }
40 |
41 | public function getInputValuesCount() : int
42 | {
43 | return 1;
44 | }
45 |
46 | public function getFieldToInputValuesSQL(array $fieldNames) : array
47 | {
48 | return $fieldNames;
49 | }
50 |
51 | public function getType() : string|null
52 | {
53 | return null;
54 | }
55 |
56 | public function convertInputValuesToProp(Gateway $gateway, array $values) : mixed
57 | {
58 | /** @var array{string|null} $values */
59 |
60 | if ($values[0] === null) {
61 | return null;
62 | }
63 |
64 | return json_decode($values[0], $this->objectAsArray);
65 | }
66 |
67 | public function convertPropToFields(mixed $propValue) : array
68 | {
69 | if ($propValue === null) {
70 | return [
71 | ['NULL']
72 | ];
73 | }
74 |
75 | return [
76 | ['?', json_encode($propValue)]
77 | ];
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/src/PropertyMapping/StringMapping.php:
--------------------------------------------------------------------------------
1 | |null
18 | *
19 | * @var string[]|null
20 | */
21 | private array|null $nonIdProps = null;
22 |
23 | /**
24 | * @param string $namespace The namespace of the proxy class.
25 | */
26 | public function setProxyNamespace(string $namespace) : void
27 | {
28 | $this->proxyNamespace = $namespace;
29 | }
30 |
31 | /**
32 | * @psalm-param class-string $className
33 | *
34 | * @param string $className The FQCN of the entity.
35 | */
36 | public function setEntityClassName(string $className) : void
37 | {
38 | $this->entityClassName = $className;
39 | }
40 |
41 | /**
42 | * @psalm-param list $props
43 | *
44 | * @param string[] $props The list of non-identity properties.
45 | */
46 | public function setNonIdProps(array $props) : void
47 | {
48 | $this->nonIdProps = $props;
49 | }
50 |
51 | /**
52 | * Builds and returns the proxy source code.
53 | *
54 | * @throws \RuntimeException If data are missing.
55 | * @throws \ReflectionException If a class does not exist.
56 | */
57 | public function build() : string
58 | {
59 | if ($this->proxyNamespace === null) {
60 | throw new \RuntimeException('Missing proxy namespace.');
61 | }
62 |
63 | if ($this->entityClassName === null) {
64 | throw new \RuntimeException('Missing entity class name.');
65 | }
66 |
67 | if ($this->nonIdProps === null) {
68 | throw new \RuntimeException('Missing non-id props.');
69 | }
70 |
71 | $imports = [
72 | $this->entityClassName
73 | ];
74 |
75 | $entityClassShortName = (new \ReflectionClass($this->entityClassName))->getShortName();
76 |
77 | $code = file_get_contents(__DIR__ . '/ProxyTemplate.php');
78 |
79 | $code = str_replace('PROXY_NAMESPACE', $this->proxyNamespace, $code);
80 | $code = str_replace('CLASS_NAME', $entityClassShortName, $code);
81 |
82 | if ($this->nonIdProps) {
83 | $unsets = "\n";
84 |
85 | $unsets .= implode(",\n", array_map(static function(string $prop) : string {
86 | return str_repeat(' ', 12) . '$this->' . $prop;
87 | }, $this->nonIdProps));
88 |
89 | $unsets .= "\n" . str_repeat(' ' , 8);
90 |
91 | $code = str_replace('$UNSET_NON_ID_PROPS', $unsets, $code);
92 | } else {
93 | $code = str_replace('unset($UNSET_NON_ID_PROPS);', '', $code);
94 | }
95 |
96 | $nonIdProps = array_map(static function(string $prop) : string {
97 | return var_export($prop, true);
98 | }, $this->nonIdProps);
99 |
100 | $code = str_replace('NON_ID_PROPS', implode(', ', $nonIdProps), $code);
101 |
102 | // Imports
103 |
104 | $importString = '';
105 |
106 | $imports = array_values(array_unique($imports));
107 |
108 | foreach ($imports as $key => $import) {
109 | if ($key !== 0) {
110 | $importString .= ' ';
111 | }
112 |
113 | $importString .= $import;
114 |
115 | if ($key !== count($imports) - 1) {
116 | $importString .= ",\n";
117 | }
118 | }
119 |
120 | $code = str_replace('IMPORTS', $importString, $code);
121 |
122 | return $code;
123 | }
124 | }
125 |
--------------------------------------------------------------------------------
/src/ProxyTemplate.php:
--------------------------------------------------------------------------------
1 | $identity
33 | * @psalm-param list $scalarIdentity
34 | *
35 | * @param Gateway $gateway The gateway.
36 | * @param array $identity The identity, as a map of property name to value.
37 | * @param array $scalarIdentity The identity, as a list of scalar values.
38 | */
39 | public function __construct(Gateway $gateway, array $identity, array $scalarIdentity)
40 | {
41 | $this->__gateway = $gateway;
42 | $this->__identity = $identity;
43 | $this->__scalarIdentity = $scalarIdentity;
44 |
45 | foreach ($identity as $prop => $value) {
46 | $this->{$prop} = $value;
47 | }
48 |
49 | unset($UNSET_NON_ID_PROPS);
50 | }
51 |
52 | public function __get(string $name) : mixed
53 | {
54 | if (! $this->__isInitialized) {
55 | $loadProps = [];
56 |
57 | foreach (self::__NON_ID_PROPERTIES as $prop) {
58 | if (! isset($this->{$prop})) { // exclude initialized properties
59 | $loadProps[] = $prop;
60 | }
61 | }
62 |
63 | if ($loadProps) {
64 | $propValues = $this->__gateway->loadProps(CLASS_NAME::class, $this->__identity, $loadProps);
65 |
66 | if ($propValues === null) {
67 | throw EntityNotFoundException::entityNotFound(CLASS_NAME::class, $this->__scalarIdentity);
68 | }
69 |
70 | foreach ($propValues as $prop => $value) {
71 | $this->{$prop} = $value;
72 | }
73 | }
74 |
75 | $this->__isInitialized = true;
76 | }
77 |
78 | return $this->{$name};
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/src/Query.php:
--------------------------------------------------------------------------------
1 | |null
18 | *
19 | * @var string[]|null
20 | */
21 | private array|null $properties = null;
22 |
23 | /**
24 | * @psalm-var list
25 | *
26 | * @var QueryPredicate[]
27 | */
28 | private array $predicates = [];
29 |
30 | /**
31 | * @psalm-var list
32 | *
33 | * @var QueryOrderBy[]
34 | */
35 | private array $orderBy = [];
36 |
37 | private int|null $limit = null;
38 |
39 | private int|null $offset = null;
40 |
41 | /**
42 | * @psalm-param class-string $className
43 | */
44 | public function __construct(string $className)
45 | {
46 | $this->className = $className;
47 | }
48 |
49 | public function setProperties(string ...$properties) : Query
50 | {
51 | $this->properties = array_values($properties);
52 |
53 | return $this;
54 | }
55 |
56 | /**
57 | * @throws \InvalidArgumentException If the operator is invalid.
58 | */
59 | public function addPredicate(string $property, string $operator, mixed $value) : Query
60 | {
61 | $this->predicates[] = new QueryPredicate($property, $operator, $value);
62 |
63 | return $this;
64 | }
65 |
66 | /**
67 | * @param string $property The property to order by.
68 | * @param string $direction The order direction, 'ASC' or 'DESC'.
69 | *
70 | * @throws \InvalidArgumentException If the order direction is invalid.
71 | */
72 | public function addOrderBy(string $property, string $direction = 'ASC') : Query
73 | {
74 | $this->orderBy[] = new QueryOrderBy($property, $direction);
75 |
76 | return $this;
77 | }
78 |
79 | public function setLimit(int $limit, int $offset = 0) : Query
80 | {
81 | $this->limit = $limit;
82 | $this->offset = $offset;
83 |
84 | return $this;
85 | }
86 |
87 | /**
88 | * @psalm-return class-string
89 | */
90 | public function getClassName() : string
91 | {
92 | return $this->className;
93 | }
94 |
95 | /**
96 | * @psalm-return list|null
97 | *
98 | * @return string[]|null
99 | */
100 | public function getProperties() : array|null
101 | {
102 | return $this->properties;
103 | }
104 |
105 | /**
106 | * @psalm-return list
107 | *
108 | * @return QueryPredicate[]
109 | */
110 | public function getPredicates() : array
111 | {
112 | return $this->predicates;
113 | }
114 |
115 | /**
116 | * @psalm-return list
117 | *
118 | * @return QueryOrderBy[]
119 | */
120 | public function getOrderBy() : array
121 | {
122 | return $this->orderBy;
123 | }
124 |
125 | public function getLimit() : int|null
126 | {
127 | return $this->limit;
128 | }
129 |
130 | public function getOffset() : int|null
131 | {
132 | return $this->offset;
133 | }
134 | }
135 |
--------------------------------------------------------------------------------
/src/QueryOrderBy.php:
--------------------------------------------------------------------------------
1 | property = $property;
31 | $this->direction = $direction;
32 | }
33 |
34 | public function getProperty() : string
35 | {
36 | return $this->property;
37 | }
38 |
39 | /**
40 | * @psalm-return 'ASC'|'DESC'
41 | */
42 | public function getDirection() : string
43 | {
44 | return $this->direction;
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/src/QueryPredicate.php:
--------------------------------------------------------------------------------
1 | ".
18 | * @param mixed $value The value to compare against.
19 | *
20 | * @throws \InvalidArgumentException If the operator is invalid.
21 | */
22 | public function __construct(string $property, string $operator, mixed $value)
23 | {
24 | if (! in_array($operator, ['=', '!=', '>', '<', '>=', '<='])) {
25 | throw new \InvalidArgumentException(sprintf('Unknown operator "%s".', $operator));
26 | }
27 |
28 | $this->property = $property;
29 | $this->operator = $operator;
30 | $this->value = $value;
31 | }
32 |
33 | public function getProperty() : string
34 | {
35 | return $this->property;
36 | }
37 |
38 | public function getOperator() : string
39 | {
40 | return $this->operator;
41 | }
42 |
43 | public function getValue() : mixed
44 | {
45 | return $this->value;
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/src/RepositoryBuilder.php:
--------------------------------------------------------------------------------
1 | |null
18 | */
19 | private array|null $identityProps = null;
20 |
21 | /**
22 | * @param string $namespace The namespace of the repository.
23 | */
24 | public function setRepositoryNamespace(string $namespace) : void
25 | {
26 | $this->repositoryNamespace = $namespace;
27 | }
28 |
29 | /**
30 | * @psalm-param class-string $className
31 | *
32 | * @param string $className The FQCN of the entity.
33 | */
34 | public function setEntityClassName(string $className) : void
35 | {
36 | $this->entityClassName = $className;
37 | }
38 |
39 | /**
40 | * @psalm-param array $props
41 | *
42 | * @param array $props An associative array of property name to type.
43 | */
44 | public function setIdentityProps(array $props) : void
45 | {
46 | $this->identityProps = $props;
47 | }
48 |
49 | /**
50 | * Builds and returns the repository source code.
51 | *
52 | * @throws \RuntimeException If data are missing.
53 | * @throws \ReflectionException If a class does not exist.
54 | */
55 | public function build() : string
56 | {
57 | if ($this->repositoryNamespace === null) {
58 | throw new \RuntimeException('Missing repository namespace.');
59 | }
60 |
61 | if ($this->entityClassName === null) {
62 | throw new \RuntimeException('Missing entity class name.');
63 | }
64 |
65 | if ($this->identityProps === null) {
66 | throw new \RuntimeException('Missing identity props.');
67 | }
68 |
69 | $imports = [
70 | $this->entityClassName
71 | ];
72 |
73 | $entityClassShortName = (new \ReflectionClass($this->entityClassName))->getShortName();
74 |
75 | $code = file_get_contents(__DIR__ . '/RepositoryTemplate.php');
76 |
77 | $code = str_replace('REPO_NAMESPACE', $this->repositoryNamespace, $code);
78 | $code = str_replace('CLASS_NAME', $entityClassShortName, $code);
79 | $code = str_replace('ENTITY_PROP_NAME', $this->getParamNameForClassName($entityClassShortName), $code);
80 |
81 | // Identity props & array
82 |
83 | $builtInTypes = [
84 | 'bool',
85 | 'int',
86 | 'float',
87 | 'string',
88 | 'array',
89 | 'object',
90 | 'callable',
91 | 'iterable'
92 | ];
93 |
94 | $identityProps = [];
95 | $identityArray = [];
96 |
97 | foreach ($this->identityProps as $prop => $type) {
98 | $typeLower = strtolower($type);
99 |
100 | if (in_array($typeLower, $builtInTypes, true)) {
101 | $type = $typeLower;
102 | } else {
103 | /** @psalm-var class-string $type */
104 | $imports[] = $type;
105 | $type = (new \ReflectionClass($type))->getShortName();
106 | }
107 |
108 | $identityProps[] = $type . ' $' . $prop;
109 | $identityArray[] = var_export($prop, true) . ' => $' . $prop;
110 | }
111 |
112 | $code = str_replace('$IDENTITY_PROPS', implode(', ', $identityProps), $code);
113 | $code = str_replace('IDENTITY_ARRAY', '[' . implode(', ', $identityArray) . ']', $code);
114 |
115 | // Imports
116 |
117 | $importString = '';
118 |
119 | $imports = array_values(array_unique($imports));
120 |
121 | foreach ($imports as $key => $import) {
122 | if ($key !== 0) {
123 | $importString .= ' ';
124 | }
125 |
126 | $importString .= $import;
127 |
128 | if ($key !== count($imports) - 1) {
129 | $importString .= ",\n";
130 | }
131 | }
132 |
133 | $code = str_replace('IMPORTS', $importString, $code);
134 |
135 | return $code;
136 | }
137 |
138 | /**
139 | * Returns a suitable parameter name for a class name.
140 | *
141 | * Examples: 'User' => 'user', 'ABBREntity' => 'abbrEntity'.
142 | */
143 | private function getParamNameForClassName(string $className) : string
144 | {
145 | $length = strlen($className);
146 |
147 | $upperLength = 0;
148 |
149 | for ($i = 0; $i < $length; $i++) {
150 | if ($this->isUppercase($className[$i])) {
151 | $upperLength++;
152 | } else {
153 | break;
154 | }
155 | }
156 |
157 | if ($upperLength === 0) {
158 | return $className;
159 | }
160 |
161 | if ($upperLength > 1) {
162 | $upperLength--;
163 | }
164 |
165 | return strtolower(substr($className, 0, $upperLength)) . substr($className, $upperLength);
166 | }
167 |
168 | /**
169 | * Checks if an ASCII letter is uppercase.
170 | */
171 | private function isUppercase(string $letter) : bool
172 | {
173 | $ord = ord($letter);
174 |
175 | return ($ord >= 65) && ($ord <= 90);
176 | }
177 | }
178 |
--------------------------------------------------------------------------------
/src/RepositoryTemplate.php:
--------------------------------------------------------------------------------
1 | gateway = $gateway;
26 | }
27 |
28 | public function load($IDENTITY_PROPS, int $options = 0, string ...$props) : CLASS_NAME|null
29 | {
30 | /** @noinspection PhpIncompatibleReturnTypeInspection */
31 | return $this->gateway->load(CLASS_NAME::class, IDENTITY_ARRAY, $options, ...$props);
32 | }
33 |
34 | public function getReference($IDENTITY_PROPS) : CLASS_NAME
35 | {
36 | /** @noinspection PhpIncompatibleReturnTypeInspection */
37 | return $this->gateway->getReference(CLASS_NAME::class, IDENTITY_ARRAY);
38 | }
39 |
40 | public function exists(CLASS_NAME $ENTITY_PROP_NAME) : bool
41 | {
42 | return $this->gateway->exists($ENTITY_PROP_NAME);
43 | }
44 |
45 | public function existsIdentity($IDENTITY_PROPS) : bool
46 | {
47 | return $this->gateway->existsIdentity(CLASS_NAME::class, IDENTITY_ARRAY);
48 | }
49 |
50 | public function add(CLASS_NAME $ENTITY_PROP_NAME) : void
51 | {
52 | $this->gateway->add($ENTITY_PROP_NAME);
53 | }
54 |
55 | public function update(CLASS_NAME $ENTITY_PROP_NAME) : void
56 | {
57 | $this->gateway->update($ENTITY_PROP_NAME);
58 | }
59 |
60 | public function remove(CLASS_NAME $ENTITY_PROP_NAME) : void
61 | {
62 | $this->gateway->remove($ENTITY_PROP_NAME);
63 | }
64 |
65 | public function removeIdentity($IDENTITY_PROPS) : void
66 | {
67 | $this->gateway->removeIdentity(CLASS_NAME::class, IDENTITY_ARRAY);
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/src/SelectQueryBuilder.php:
--------------------------------------------------------------------------------
1 |
18 | *
19 | * @var string[]
20 | */
21 | private array $selectFields;
22 |
23 | /**
24 | * The table name.
25 | */
26 | private string $tableName;
27 |
28 | /**
29 | * An optional table alias.
30 | */
31 | private string|null $tableAlias;
32 |
33 | /**
34 | * @psalm-var list
35 | *
36 | * @var string[]
37 | */
38 | private array $joins = [];
39 |
40 | /**
41 | * @psalm-var list
42 | *
43 | * @var string[]
44 | */
45 | private array $whereConditions = [];
46 |
47 | /**
48 | * @psalm-var list
49 | *
50 | * @var string[]
51 | */
52 | private array $orderBy = [];
53 |
54 | private string $limit = '';
55 |
56 | private int $options = 0;
57 |
58 | /**
59 | * @psalm-param list $selectFields
60 | *
61 | * @param string[] $selectFields The fields or expressions to SELECT.
62 | * @param string $tableName The table name.
63 | * @param string|null $tableAlias An optional table alias.
64 | */
65 | public function __construct(array $selectFields, string $tableName, string|null $tableAlias = null)
66 | {
67 | $this->selectFields = $selectFields;
68 | $this->tableName = $tableName;
69 | $this->tableAlias = $tableAlias;
70 | }
71 |
72 | /**
73 | * @psalm-param list $joinConditions
74 | *
75 | * @param string $joinType The JOIN type, such as INNER or LEFT.
76 | * @param string $tableName The table name.
77 | * @param string $tableAlias The table alias.
78 | * @param string[] $joinConditions The list of A=B join conditions.
79 | */
80 | public function addJoin(string $joinType, string $tableName, string $tableAlias, array $joinConditions) : void
81 | {
82 | $this->joins[] = ' ' . $joinType . ' JOIN ' . $tableName .
83 | ' AS ' . $tableAlias .
84 | ' ON ' . implode(' AND ', $joinConditions);
85 | }
86 |
87 | /**
88 | * Adds WHERE conditions to be AND'ed to the current conditions.
89 | *
90 | * The conditions will be AND'ed or OR'ed together, according to the given operator, and AND'ed as a whole to the
91 | * existing conditions.
92 | *
93 | * @psalm-param list $whereConditions
94 | * @psalm-param 'AND'|'OR' $operator
95 | *
96 | * @param string[] $whereConditions The WHERE conditions.
97 | * @param string $operator The operator, 'AND' or 'OR'.
98 | */
99 | public function addWhereConditions(array $whereConditions, string $operator = 'AND') : void
100 | {
101 | $parentheses = ($operator === 'OR' && count($whereConditions) > 1);
102 |
103 | $whereConditions = implode(' ' . $operator . ' ', $whereConditions);
104 |
105 | if ($parentheses) {
106 | $whereConditions = '(' . $whereConditions . ')';
107 | }
108 |
109 | $this->whereConditions[] = $whereConditions;
110 | }
111 |
112 | /**
113 | * @psalm-param 'ASC'|'DESC' $direction
114 | *
115 | * @param string $expression The expression to order by.
116 | * @param string $direction The order direction, 'ASC' or 'DESC'.
117 | */
118 | public function addOrderBy(string $expression, string $direction = 'ASC') : void
119 | {
120 | $this->orderBy[] = $expression . ' ' . $direction;
121 | }
122 |
123 | public function setLimit(int $limit, int $offset = 0) : void
124 | {
125 | $this->limit = ' LIMIT ' . $limit;
126 |
127 | if ($offset !== 0) {
128 | $this->limit .= ' OFFSET ' . $offset;
129 | }
130 | }
131 |
132 | /**
133 | * @param int $options A bitmask of options.
134 | */
135 | public function setOptions(int $options) : void
136 | {
137 | $this->options = $options;
138 | }
139 |
140 | public function build() : string
141 | {
142 | $query = 'SELECT ' . implode(', ', $this->selectFields) . ' FROM ' . $this->tableName;
143 |
144 | if ($this->tableAlias !== null) {
145 | $query .= ' AS ' . $this->tableAlias;
146 | }
147 |
148 | foreach ($this->joins as $join) {
149 | $query .= $join;
150 | }
151 |
152 | if ($this->whereConditions) {
153 | $query .= ' WHERE ' . implode(' AND ', $this->whereConditions);
154 | }
155 |
156 | if ($this->orderBy) {
157 | $query .= ' ORDER BY ' . implode(', ', $this->orderBy);
158 | }
159 |
160 | $query .= $this->limit;
161 |
162 | // @todo lock mode syntax is MySQL / PostgreSQL only
163 |
164 | if ($this->options & Options::LOCK_READ) {
165 | $query .= ' FOR SHARE';
166 | } elseif ($this->options & Options::LOCK_WRITE) {
167 | $query .= ' FOR UPDATE';
168 | }
169 |
170 | if ($this->options & Options::SKIP_LOCKED) {
171 | $query .= ' SKIP LOCKED';
172 | }
173 |
174 | if ($this->options & Options::NOWAIT) {
175 | $query .= ' NOWAIT';
176 | }
177 |
178 | return $query;
179 | }
180 | }
181 |
--------------------------------------------------------------------------------
/src/TableAliasGenerator.php:
--------------------------------------------------------------------------------
1 | number++;
14 |
15 | if ($this->number <= 25) {
16 | // a to y
17 | return chr(96 + $this->number);
18 | }
19 |
20 | // z1, z2, etc.
21 | return 'z' . ($this->number - 25);
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/tests-config.php:
--------------------------------------------------------------------------------
1 | setRepositoryNamespace('Brick\ORM\Tests\Generated\Repository');
15 | $config->setRepositoryDir(__DIR__ . '/tests/Generated/Repository');
16 |
17 | $config->setProxyNamespace('Brick\ORM\Tests\Generated\Proxy');
18 | $config->setProxyDir(__DIR__ . '/tests/Generated/Proxy');
19 |
20 | $config->setBaseEntityNamespace('Brick\ORM\Tests\Resources\Models');
21 | $config->setClassMetadataFile(__DIR__ . '/tests/Generated/ClassMetadata.php');
22 |
23 | $config->addEntity(Models\User::class)
24 | ->setIdentityProperties('id')
25 | ->setAutoIncrement();
26 |
27 | $config->addEntity(Models\Follow::class)
28 | ->setIdentityProperties('follower', 'followee');
29 |
30 | $config->addEntity(Models\Country::class)
31 | ->setIdentityProperties('code');
32 |
33 | $config->addEntity(Brick\ORM\Tests\Resources\Models\Event::class)
34 | ->setIdentityProperties('id')
35 | ->setAutoIncrement()
36 | ->setInheritanceMapping('type', [
37 | 'CreateCountry' => Models\Event\CountryEvent\CreateCountryEvent::class,
38 | 'EditCountryName' => Models\Event\CountryEvent\EditCountryNameEvent::class,
39 | 'CreateUser' => Models\Event\UserEvent\CreateUserEvent::class,
40 | 'EditUserBillingAddress' => Models\Event\UserEvent\EditUserBillingAddressEvent::class,
41 | 'EditUserDeliveryAddress' => Models\Event\UserEvent\EditUserDeliveryAddressEvent::class,
42 | 'EditUserName' => Models\Event\UserEvent\EditUserNameEvent::class,
43 | 'FollowUser' => Models\Event\FollowUserEvent::class
44 | ]);
45 |
46 | $config->addEmbeddable(Models\Address::class);
47 | $config->addEmbeddable(Models\GeoAddress::class);
48 |
49 | $config->addCustomMapping(Objects\Geometry::class, Mappings\GeometryMapping::class);
50 |
51 | // Set transient properties
52 | $config->setTransientProperties(Models\User::class, 'transient');
53 |
54 | // Override field names / prefixes
55 | $config->setFieldName(Models\Address::class, 'postcode', 'zipcode');
56 | $config->setFieldNamePrefix(Models\User::class, 'billingAddress', '');
57 |
58 | $config->setCustomPropertyMapping(Models\User::class, 'data', new JsonMapping('data', false, true));
59 |
60 | return $config;
61 | })();
62 |
--------------------------------------------------------------------------------
/tests/AbstractTestCase.php:
--------------------------------------------------------------------------------
1 | exec('DROP DATABASE IF EXISTS orm_tests');
60 | $connection->exec('CREATE DATABASE orm_tests');
61 | $connection->exec('USE orm_tests');
62 |
63 | $classMetadata = require __DIR__ . '/Generated/ClassMetadata.php';
64 |
65 | self::$connection = $connection;
66 | self::$gateway = new Gateway($connection, $classMetadata, null, static::useProxies());
67 |
68 | self::$countryRepository = new CountryRepository(self::$gateway);
69 | self::$userRepository = new UserRepository(self::$gateway);
70 | self::$eventRepository = new EventRepository(self::$gateway);
71 |
72 | $connection->exec('DROP TABLE IF EXISTS Country');
73 | $connection->exec('
74 | CREATE TABLE Country (
75 | code CHAR(2) NOT NULL PRIMARY KEY,
76 | name VARCHAR(50) NOT NULL
77 | )
78 | ');
79 |
80 | $connection->exec('DROP TABLE IF EXISTS User');
81 | $connection->exec('
82 | CREATE TABLE User (
83 | id INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
84 | name VARCHAR(50) NOT NULL,
85 | street VARCHAR(50) NULL,
86 | city VARCHAR(50) NULL,
87 | zipcode VARCHAR(50) NULL,
88 | country_code CHAR(2) NULL,
89 | isPoBox TINYINT(1) NULL,
90 | deliveryAddress_address_street VARCHAR(50) NULL,
91 | deliveryAddress_address_city VARCHAR(50) NULL,
92 | deliveryAddress_address_zipcode VARCHAR(50) NULL,
93 | deliveryAddress_address_country_code CHAR(2) NULL,
94 | deliveryAddress_address_isPoBox TINYINT(1) NULL,
95 | deliveryAddress_location GEOMETRY NULL,
96 | lastEvent_type VARCHAR(30) NULL,
97 | lastEvent_id INT(10) UNSIGNED NULL,
98 | data JSON NOT NULL
99 | )
100 | ');
101 |
102 | $connection->exec('DROP TABLE IF EXISTS Event');
103 | $connection->exec('
104 | CREATE TABLE Event (
105 | id INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
106 | type VARCHAR(30) NOT NULL,
107 | time INT(10) UNSIGNED NOT NULL,
108 | user_id INT(10) UNSIGNED NULL,
109 | follower_id INT(10) UNSIGNED NULL,
110 | followee_id INT(10) UNSIGNED NULL,
111 | country_code CHAR(2) NULL,
112 | isFollow TINYINT(1) NULL,
113 | newName VARCHAR(50) NULL,
114 | newAddress_street VARCHAR(50) NULL,
115 | newAddress_city VARCHAR(50) NULL,
116 | newAddress_zipcode VARCHAR(50) NULL,
117 | newAddress_country_code CHAR(2) NULL,
118 | newAddress_isPoBox TINYINT(1) NULL,
119 | newAddress_address_street VARCHAR(50) NULL,
120 | newAddress_address_city VARCHAR(50) NULL,
121 | newAddress_address_zipcode VARCHAR(50) NULL,
122 | newAddress_address_country_code CHAR(2) NULL,
123 | newAddress_address_isPoBox TINYINT(1) NULL,
124 | newAddress_location GEOMETRY NULL
125 | )
126 | ');
127 | }
128 |
129 | /**
130 | * {@inheritdoc}
131 | */
132 | protected function setUp() : void
133 | {
134 | self::$logger->reset();
135 | }
136 |
137 | /**
138 | * @param int $count
139 | *
140 | * @return void
141 | */
142 | protected function assertDebugStatementCount(int $count) : void
143 | {
144 | self::assertSame($count, self::$logger->count());
145 | }
146 |
147 | /**
148 | * @param int $index
149 | * @param string $statement
150 | * @param mixed ...$parameters
151 | *
152 | * @return void
153 | */
154 | protected function assertDebugStatement(int $index, string $statement, ...$parameters) : void
155 | {
156 | $debugStatement = self::$logger->getDebugStatement($index);
157 |
158 | self::assertSame($statement, $debugStatement->getStatement());
159 | self::assertSame($parameters, $debugStatement->getParameters());
160 | self::assertIsFloat($debugStatement->getTime());
161 | self::assertGreaterThan(0.0, $debugStatement->getTime());
162 | }
163 | }
164 |
--------------------------------------------------------------------------------
/tests/GatewayNativeQueryTest.php:
--------------------------------------------------------------------------------
1 | exec(<<lastInsertId();
24 |
25 | for ($i = 1; $i <= 3; $i++) {
26 | self::$connection->exec(<<nativeQuery(UserDTO::class, <<assertCount(1, $users);
48 |
49 | $user = $users[0];
50 |
51 | $this->assertSame($userId, $user->id);
52 | $this->assertSame('Bob', $user->name);
53 | $this->assertSame('Baker Street', $user->address->street);
54 | $this->assertSame('London', $user->address->city);
55 | $this->assertNull($user->address->postcode);
56 | $this->assertSame('GB', $user->address->countryCode);
57 | $this->assertSame(3, $user->eventCount);
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/tests/GatewayTest.php:
--------------------------------------------------------------------------------
1 | add($country);
30 |
31 | $this->assertDebugStatementCount(1);
32 | $this->assertDebugStatement(0, 'INSERT INTO Country (code, name) VALUES (?, ?)', 'GB', 'United Kingdom');
33 | }
34 |
35 | #[Depends('testAddCountry')]
36 | public function testLoadUnknownCountry() : void
37 | {
38 | $this->assertNull(self::$countryRepository->load('XX'));
39 |
40 | $this->assertDebugStatementCount(1);
41 | $this->assertDebugStatement(0, 'SELECT a.code, a.name FROM Country AS a WHERE a.code = ?', 'XX');
42 | }
43 |
44 | #[Depends('testLoadUnknownCountry')]
45 | public function testLoadCountry() : Country
46 | {
47 | $country = self::$countryRepository->load('GB');
48 |
49 | $this->assertSame('GB', $country->getCode());
50 | $this->assertSame('United Kingdom', $country->getName());
51 |
52 | $this->assertDebugStatementCount(1);
53 | $this->assertDebugStatement(0, 'SELECT a.code, a.name FROM Country AS a WHERE a.code = ?', 'GB');
54 |
55 | return $country;
56 | }
57 |
58 | #[Depends('testLoadCountry')]
59 | public function testAddUser(Country $country) : User
60 | {
61 | $user = new User('John Smith');
62 |
63 | $billingAddress = new Address('123 Unknown Road', 'London', 'WC2E9XX', $country, false);
64 | $user->setBillingAddress($billingAddress);
65 |
66 | self::$userRepository->add($user);
67 |
68 | $this->assertDebugStatementCount(1);
69 | $this->assertDebugStatement(0,
70 | 'INSERT INTO User (name, street, city, zipcode, country_code, isPoBox, ' .
71 | 'deliveryAddress_address_street, deliveryAddress_address_city, deliveryAddress_address_zipcode, ' .
72 | 'deliveryAddress_address_country_code, deliveryAddress_address_isPoBox, deliveryAddress_location, ' .
73 | 'lastEvent_type, lastEvent_id, data) ' .
74 | 'VALUES (?, ?, ?, ?, ?, ?, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, ?)',
75 | 'John Smith', '123 Unknown Road', 'London', 'WC2E9XX', 'GB', false, '{"any":"data"}'
76 | );
77 |
78 | // User ID must be set after saving
79 | $this->assertIsInt($user->getId());
80 | $this->assertGreaterThan(0, $user->getId());
81 |
82 | return $user;
83 | }
84 |
85 | #[Depends('testAddUser')]
86 | public function testUpdateUser(User $user) : int
87 | {
88 | $address = $user->getBillingAddress();
89 | $location = new Geometry('POINT (51 0)', 4326);
90 |
91 | $user->setDeliveryAddress(new GeoAddress($address, $location));
92 | self::$userRepository->update($user);
93 |
94 | $this->assertDebugStatementCount(1);
95 | $this->assertDebugStatement(0,
96 | 'UPDATE User SET name = ?, street = ?, city = ?, zipcode = ?, country_code = ?, isPoBox = ?, ' .
97 | 'deliveryAddress_address_street = ?, deliveryAddress_address_city = ?, ' .
98 | 'deliveryAddress_address_zipcode = ?, deliveryAddress_address_country_code = ?, ' .
99 | 'deliveryAddress_address_isPoBox = ?, deliveryAddress_location = ST_GeomFromText(?, ?), ' .
100 | 'lastEvent_type = NULL, lastEvent_id = NULL, data = ? ' .
101 | 'WHERE id = ?',
102 | 'John Smith',
103 | '123 Unknown Road', 'London', 'WC2E9XX', 'GB', false,
104 | '123 Unknown Road', 'London', 'WC2E9XX', 'GB', false,
105 | 'POINT (51 0)', 4326, '{"any":"data"}', $user->getId()
106 | );
107 |
108 | return $user->getId();
109 | }
110 |
111 | #[Depends('testUpdateUser')]
112 | public function testLoadPartialUser(int $userId) : int
113 | {
114 | $user = self::$userRepository->load($userId, 0, 'name');
115 |
116 | $this->assertDebugStatementCount(1);
117 | $this->assertDebugStatement(0, 'SELECT a.name FROM User AS a WHERE a.id = ?', $userId);
118 |
119 | $this->assertSame('John Smith', $user->getName());
120 | $this->assertSame([], $user->getTransient());
121 |
122 | try {
123 | $user->getBillingAddress();
124 | } catch (\Error $error) {
125 | if (strpos($error->getMessage(), 'must not be accessed before initialization') !== false) {
126 | goto ok;
127 | }
128 | }
129 |
130 | $this->fail('This property should not be set in partial object.');
131 |
132 | ok:
133 |
134 | return $userId;
135 | }
136 |
137 | #[Depends('testLoadPartialUser')]
138 | public function testLoadUser(int $userId) : User
139 | {
140 | $user = self::$userRepository->load($userId);
141 |
142 | $this->assertDebugStatementCount(1);
143 | $this->assertDebugStatement(0,
144 | 'SELECT a.id, a.name, a.street, a.city, a.zipcode, a.country_code, a.isPoBox, ' .
145 | 'a.deliveryAddress_address_street, a.deliveryAddress_address_city, a.deliveryAddress_address_zipcode, ' .
146 | 'a.deliveryAddress_address_country_code, a.deliveryAddress_address_isPoBox, ' .
147 | 'ST_AsText(a.deliveryAddress_location), ST_SRID(a.deliveryAddress_location), ' .
148 | 'a.lastEvent_type, a.lastEvent_id, a.data ' .
149 | 'FROM User AS a WHERE a.id = ?',
150 | $userId
151 | );
152 |
153 | $this->assertSame('John Smith', $user->getName());
154 | $this->assertSame('123 Unknown Road', $user->getBillingAddress()->getStreet());
155 | $this->assertSame('London', $user->getBillingAddress()->getCity());
156 | $this->assertSame('WC2E9XX', $user->getBillingAddress()->getPostcode());
157 | $this->assertSame('GB', $user->getBillingAddress()->getCountry()->getCode());
158 | $this->assertSame('123 Unknown Road', $user->getDeliveryAddress()->getAddress()->getStreet());
159 | $this->assertSame('London', $user->getDeliveryAddress()->getAddress()->getCity());
160 | $this->assertSame('WC2E9XX', $user->getDeliveryAddress()->getAddress()->getPostcode());
161 | $this->assertSame('GB', $user->getDeliveryAddress()->getAddress()->getCountry()->getCode());
162 | $this->assertSame('POINT(51 0)', $user->getDeliveryAddress()->getLocation()->getWKT());
163 | $this->assertSame(4326, $user->getDeliveryAddress()->getLocation()->getSRID());
164 | $this->assertSame(['any' => 'data'], $user->getData());
165 |
166 | return $user;
167 | }
168 |
169 | #[Depends('testLoadUser')]
170 | public function testRemoveUser(User $user) : int
171 | {
172 | self::$userRepository->remove($user);
173 |
174 | $this->assertDebugStatementCount(1);
175 | $this->assertDebugStatement(0, 'DELETE FROM User WHERE id = ?', $user->getId());
176 |
177 | return $user->getId();
178 | }
179 |
180 | #[Depends('testRemoveUser')]
181 | public function testLoadRemovedUser(int $userId) : void
182 | {
183 | $user = self::$userRepository->load($userId);
184 | $this->assertNull($user);
185 |
186 | $this->assertDebugStatementCount(1);
187 | $this->assertDebugStatement(0,
188 | 'SELECT a.id, a.name, a.street, a.city, a.zipcode, a.country_code, a.isPoBox, ' .
189 | 'a.deliveryAddress_address_street, a.deliveryAddress_address_city, a.deliveryAddress_address_zipcode, ' .
190 | 'a.deliveryAddress_address_country_code, a.deliveryAddress_address_isPoBox, ' .
191 | 'ST_AsText(a.deliveryAddress_location), ST_SRID(a.deliveryAddress_location), ' .
192 | 'a.lastEvent_type, a.lastEvent_id, a.data ' .
193 | 'FROM User AS a WHERE a.id = ?',
194 | $userId
195 | );
196 | }
197 |
198 | /**
199 | * @param int $options
200 | * @param string $sqlSuffix
201 | *
202 | * @return void
203 | */
204 | #[DataProvider('providerLoadWithLock')]
205 | public function testLoadWithLock(int $options, string $sqlSuffix) : void
206 | {
207 | self::$countryRepository->load('XX', $options);
208 |
209 | $this->assertDebugStatementCount(1);
210 | $this->assertDebugStatement(0, 'SELECT a.code, a.name FROM Country AS a WHERE a.code = ? ' . $sqlSuffix, 'XX');
211 | }
212 |
213 | public static function providerLoadWithLock() : array
214 | {
215 | return [
216 | [Options::LOCK_READ, 'FOR SHARE'],
217 | [Options::LOCK_WRITE, 'FOR UPDATE'],
218 | [Options::LOCK_READ | Options::SKIP_LOCKED, 'FOR SHARE SKIP LOCKED'],
219 | [Options::LOCK_WRITE | Options::SKIP_LOCKED, 'FOR UPDATE SKIP LOCKED'],
220 | [Options::LOCK_READ | Options::NOWAIT, 'FOR SHARE NOWAIT'],
221 | [Options::LOCK_WRITE | Options::NOWAIT, 'FOR UPDATE NOWAIT'],
222 | ];
223 | }
224 | }
225 |
--------------------------------------------------------------------------------
/tests/Generated/Proxy/CountryProxy.php:
--------------------------------------------------------------------------------
1 | __gateway = $gateway;
39 | $this->__identity = $identity;
40 | $this->__scalarIdentity = $scalarIdentity;
41 |
42 | foreach ($identity as $prop => $value) {
43 | $this->{$prop} = $value;
44 | }
45 |
46 | unset(
47 | $this->name
48 | );
49 | }
50 |
51 | /**
52 | * @param string $name
53 | *
54 | * @return mixed
55 | */
56 | public function __get(string $name)
57 | {
58 | if (! $this->__isInitialized) {
59 | $loadProps = [];
60 |
61 | foreach (self::__NON_ID_PROPERTIES as $prop) {
62 | if (! isset($this->{$prop})) { // exclude initialized properties
63 | $loadProps[] = $prop;
64 | }
65 | }
66 |
67 | if ($loadProps) {
68 | $propValues = $this->__gateway->loadProps(Country::class, $this->__identity, $loadProps);
69 |
70 | if ($propValues === null) {
71 | throw EntityNotFoundException::entityNotFound(Country::class, $this->__scalarIdentity);
72 | }
73 |
74 | foreach ($propValues as $prop => $value) {
75 | $this->{$prop} = $value;
76 | }
77 | }
78 |
79 | $this->__isInitialized = true;
80 | }
81 |
82 | return $this->{$name};
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/tests/Generated/Proxy/Event/CountryEvent/CreateCountryEventProxy.php:
--------------------------------------------------------------------------------
1 | __gateway = $gateway;
39 | $this->__identity = $identity;
40 | $this->__scalarIdentity = $scalarIdentity;
41 |
42 | foreach ($identity as $prop => $value) {
43 | $this->{$prop} = $value;
44 | }
45 |
46 | unset(
47 | $this->country,
48 | $this->time
49 | );
50 | }
51 |
52 | /**
53 | * @param string $name
54 | *
55 | * @return mixed
56 | */
57 | public function __get(string $name)
58 | {
59 | if (! $this->__isInitialized) {
60 | $loadProps = [];
61 |
62 | foreach (self::__NON_ID_PROPERTIES as $prop) {
63 | if (! isset($this->{$prop})) { // exclude initialized properties
64 | $loadProps[] = $prop;
65 | }
66 | }
67 |
68 | if ($loadProps) {
69 | $propValues = $this->__gateway->loadProps(CreateCountryEvent::class, $this->__identity, $loadProps);
70 |
71 | if ($propValues === null) {
72 | throw EntityNotFoundException::entityNotFound(CreateCountryEvent::class, $this->__scalarIdentity);
73 | }
74 |
75 | foreach ($propValues as $prop => $value) {
76 | $this->{$prop} = $value;
77 | }
78 | }
79 |
80 | $this->__isInitialized = true;
81 | }
82 |
83 | return $this->{$name};
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/tests/Generated/Proxy/Event/CountryEvent/EditCountryNameEventProxy.php:
--------------------------------------------------------------------------------
1 | __gateway = $gateway;
39 | $this->__identity = $identity;
40 | $this->__scalarIdentity = $scalarIdentity;
41 |
42 | foreach ($identity as $prop => $value) {
43 | $this->{$prop} = $value;
44 | }
45 |
46 | unset(
47 | $this->newName,
48 | $this->country,
49 | $this->time
50 | );
51 | }
52 |
53 | /**
54 | * @param string $name
55 | *
56 | * @return mixed
57 | */
58 | public function __get(string $name)
59 | {
60 | if (! $this->__isInitialized) {
61 | $loadProps = [];
62 |
63 | foreach (self::__NON_ID_PROPERTIES as $prop) {
64 | if (! isset($this->{$prop})) { // exclude initialized properties
65 | $loadProps[] = $prop;
66 | }
67 | }
68 |
69 | if ($loadProps) {
70 | $propValues = $this->__gateway->loadProps(EditCountryNameEvent::class, $this->__identity, $loadProps);
71 |
72 | if ($propValues === null) {
73 | throw EntityNotFoundException::entityNotFound(EditCountryNameEvent::class, $this->__scalarIdentity);
74 | }
75 |
76 | foreach ($propValues as $prop => $value) {
77 | $this->{$prop} = $value;
78 | }
79 | }
80 |
81 | $this->__isInitialized = true;
82 | }
83 |
84 | return $this->{$name};
85 | }
86 | }
87 |
--------------------------------------------------------------------------------
/tests/Generated/Proxy/Event/FollowUserEventProxy.php:
--------------------------------------------------------------------------------
1 | __gateway = $gateway;
39 | $this->__identity = $identity;
40 | $this->__scalarIdentity = $scalarIdentity;
41 |
42 | foreach ($identity as $prop => $value) {
43 | $this->{$prop} = $value;
44 | }
45 |
46 | unset(
47 | $this->follower,
48 | $this->followee,
49 | $this->isFollow,
50 | $this->time
51 | );
52 | }
53 |
54 | /**
55 | * @param string $name
56 | *
57 | * @return mixed
58 | */
59 | public function __get(string $name)
60 | {
61 | if (! $this->__isInitialized) {
62 | $loadProps = [];
63 |
64 | foreach (self::__NON_ID_PROPERTIES as $prop) {
65 | if (! isset($this->{$prop})) { // exclude initialized properties
66 | $loadProps[] = $prop;
67 | }
68 | }
69 |
70 | if ($loadProps) {
71 | $propValues = $this->__gateway->loadProps(FollowUserEvent::class, $this->__identity, $loadProps);
72 |
73 | if ($propValues === null) {
74 | throw EntityNotFoundException::entityNotFound(FollowUserEvent::class, $this->__scalarIdentity);
75 | }
76 |
77 | foreach ($propValues as $prop => $value) {
78 | $this->{$prop} = $value;
79 | }
80 | }
81 |
82 | $this->__isInitialized = true;
83 | }
84 |
85 | return $this->{$name};
86 | }
87 | }
88 |
--------------------------------------------------------------------------------
/tests/Generated/Proxy/Event/UserEvent/CreateUserEventProxy.php:
--------------------------------------------------------------------------------
1 | __gateway = $gateway;
39 | $this->__identity = $identity;
40 | $this->__scalarIdentity = $scalarIdentity;
41 |
42 | foreach ($identity as $prop => $value) {
43 | $this->{$prop} = $value;
44 | }
45 |
46 | unset(
47 | $this->user,
48 | $this->time
49 | );
50 | }
51 |
52 | /**
53 | * @param string $name
54 | *
55 | * @return mixed
56 | */
57 | public function __get(string $name)
58 | {
59 | if (! $this->__isInitialized) {
60 | $loadProps = [];
61 |
62 | foreach (self::__NON_ID_PROPERTIES as $prop) {
63 | if (! isset($this->{$prop})) { // exclude initialized properties
64 | $loadProps[] = $prop;
65 | }
66 | }
67 |
68 | if ($loadProps) {
69 | $propValues = $this->__gateway->loadProps(CreateUserEvent::class, $this->__identity, $loadProps);
70 |
71 | if ($propValues === null) {
72 | throw EntityNotFoundException::entityNotFound(CreateUserEvent::class, $this->__scalarIdentity);
73 | }
74 |
75 | foreach ($propValues as $prop => $value) {
76 | $this->{$prop} = $value;
77 | }
78 | }
79 |
80 | $this->__isInitialized = true;
81 | }
82 |
83 | return $this->{$name};
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/tests/Generated/Proxy/Event/UserEvent/EditUserBillingAddressEventProxy.php:
--------------------------------------------------------------------------------
1 | __gateway = $gateway;
39 | $this->__identity = $identity;
40 | $this->__scalarIdentity = $scalarIdentity;
41 |
42 | foreach ($identity as $prop => $value) {
43 | $this->{$prop} = $value;
44 | }
45 |
46 | unset(
47 | $this->newAddress,
48 | $this->user,
49 | $this->time
50 | );
51 | }
52 |
53 | /**
54 | * @param string $name
55 | *
56 | * @return mixed
57 | */
58 | public function __get(string $name)
59 | {
60 | if (! $this->__isInitialized) {
61 | $loadProps = [];
62 |
63 | foreach (self::__NON_ID_PROPERTIES as $prop) {
64 | if (! isset($this->{$prop})) { // exclude initialized properties
65 | $loadProps[] = $prop;
66 | }
67 | }
68 |
69 | if ($loadProps) {
70 | $propValues = $this->__gateway->loadProps(EditUserBillingAddressEvent::class, $this->__identity, $loadProps);
71 |
72 | if ($propValues === null) {
73 | throw EntityNotFoundException::entityNotFound(EditUserBillingAddressEvent::class, $this->__scalarIdentity);
74 | }
75 |
76 | foreach ($propValues as $prop => $value) {
77 | $this->{$prop} = $value;
78 | }
79 | }
80 |
81 | $this->__isInitialized = true;
82 | }
83 |
84 | return $this->{$name};
85 | }
86 | }
87 |
--------------------------------------------------------------------------------
/tests/Generated/Proxy/Event/UserEvent/EditUserDeliveryAddressEventProxy.php:
--------------------------------------------------------------------------------
1 | __gateway = $gateway;
39 | $this->__identity = $identity;
40 | $this->__scalarIdentity = $scalarIdentity;
41 |
42 | foreach ($identity as $prop => $value) {
43 | $this->{$prop} = $value;
44 | }
45 |
46 | unset(
47 | $this->newAddress,
48 | $this->user,
49 | $this->time
50 | );
51 | }
52 |
53 | /**
54 | * @param string $name
55 | *
56 | * @return mixed
57 | */
58 | public function __get(string $name)
59 | {
60 | if (! $this->__isInitialized) {
61 | $loadProps = [];
62 |
63 | foreach (self::__NON_ID_PROPERTIES as $prop) {
64 | if (! isset($this->{$prop})) { // exclude initialized properties
65 | $loadProps[] = $prop;
66 | }
67 | }
68 |
69 | if ($loadProps) {
70 | $propValues = $this->__gateway->loadProps(EditUserDeliveryAddressEvent::class, $this->__identity, $loadProps);
71 |
72 | if ($propValues === null) {
73 | throw EntityNotFoundException::entityNotFound(EditUserDeliveryAddressEvent::class, $this->__scalarIdentity);
74 | }
75 |
76 | foreach ($propValues as $prop => $value) {
77 | $this->{$prop} = $value;
78 | }
79 | }
80 |
81 | $this->__isInitialized = true;
82 | }
83 |
84 | return $this->{$name};
85 | }
86 | }
87 |
--------------------------------------------------------------------------------
/tests/Generated/Proxy/Event/UserEvent/EditUserNameEventProxy.php:
--------------------------------------------------------------------------------
1 | __gateway = $gateway;
39 | $this->__identity = $identity;
40 | $this->__scalarIdentity = $scalarIdentity;
41 |
42 | foreach ($identity as $prop => $value) {
43 | $this->{$prop} = $value;
44 | }
45 |
46 | unset(
47 | $this->newName,
48 | $this->user,
49 | $this->time
50 | );
51 | }
52 |
53 | /**
54 | * @param string $name
55 | *
56 | * @return mixed
57 | */
58 | public function __get(string $name)
59 | {
60 | if (! $this->__isInitialized) {
61 | $loadProps = [];
62 |
63 | foreach (self::__NON_ID_PROPERTIES as $prop) {
64 | if (! isset($this->{$prop})) { // exclude initialized properties
65 | $loadProps[] = $prop;
66 | }
67 | }
68 |
69 | if ($loadProps) {
70 | $propValues = $this->__gateway->loadProps(EditUserNameEvent::class, $this->__identity, $loadProps);
71 |
72 | if ($propValues === null) {
73 | throw EntityNotFoundException::entityNotFound(EditUserNameEvent::class, $this->__scalarIdentity);
74 | }
75 |
76 | foreach ($propValues as $prop => $value) {
77 | $this->{$prop} = $value;
78 | }
79 | }
80 |
81 | $this->__isInitialized = true;
82 | }
83 |
84 | return $this->{$name};
85 | }
86 | }
87 |
--------------------------------------------------------------------------------
/tests/Generated/Proxy/FollowProxy.php:
--------------------------------------------------------------------------------
1 | __gateway = $gateway;
39 | $this->__identity = $identity;
40 | $this->__scalarIdentity = $scalarIdentity;
41 |
42 | foreach ($identity as $prop => $value) {
43 | $this->{$prop} = $value;
44 | }
45 |
46 | unset(
47 | $this->since
48 | );
49 | }
50 |
51 | /**
52 | * @param string $name
53 | *
54 | * @return mixed
55 | */
56 | public function __get(string $name)
57 | {
58 | if (! $this->__isInitialized) {
59 | $loadProps = [];
60 |
61 | foreach (self::__NON_ID_PROPERTIES as $prop) {
62 | if (! isset($this->{$prop})) { // exclude initialized properties
63 | $loadProps[] = $prop;
64 | }
65 | }
66 |
67 | if ($loadProps) {
68 | $propValues = $this->__gateway->loadProps(Follow::class, $this->__identity, $loadProps);
69 |
70 | if ($propValues === null) {
71 | throw EntityNotFoundException::entityNotFound(Follow::class, $this->__scalarIdentity);
72 | }
73 |
74 | foreach ($propValues as $prop => $value) {
75 | $this->{$prop} = $value;
76 | }
77 | }
78 |
79 | $this->__isInitialized = true;
80 | }
81 |
82 | return $this->{$name};
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/tests/Generated/Proxy/UserProxy.php:
--------------------------------------------------------------------------------
1 | __gateway = $gateway;
39 | $this->__identity = $identity;
40 | $this->__scalarIdentity = $scalarIdentity;
41 |
42 | foreach ($identity as $prop => $value) {
43 | $this->{$prop} = $value;
44 | }
45 |
46 | unset(
47 | $this->name,
48 | $this->billingAddress,
49 | $this->deliveryAddress,
50 | $this->lastEvent,
51 | $this->data
52 | );
53 | }
54 |
55 | /**
56 | * @param string $name
57 | *
58 | * @return mixed
59 | */
60 | public function __get(string $name)
61 | {
62 | if (! $this->__isInitialized) {
63 | $loadProps = [];
64 |
65 | foreach (self::__NON_ID_PROPERTIES as $prop) {
66 | if (! isset($this->{$prop})) { // exclude initialized properties
67 | $loadProps[] = $prop;
68 | }
69 | }
70 |
71 | if ($loadProps) {
72 | $propValues = $this->__gateway->loadProps(User::class, $this->__identity, $loadProps);
73 |
74 | if ($propValues === null) {
75 | throw EntityNotFoundException::entityNotFound(User::class, $this->__scalarIdentity);
76 | }
77 |
78 | foreach ($propValues as $prop => $value) {
79 | $this->{$prop} = $value;
80 | }
81 | }
82 |
83 | $this->__isInitialized = true;
84 | }
85 |
86 | return $this->{$name};
87 | }
88 | }
89 |
--------------------------------------------------------------------------------
/tests/Generated/Repository/CountryRepository.php:
--------------------------------------------------------------------------------
1 | gateway = $gateway;
28 | }
29 |
30 | public function load(string $code, int $options = 0, string ...$props) : ?Country
31 | {
32 | /** @noinspection PhpIncompatibleReturnTypeInspection */
33 | return $this->gateway->load(Country::class, ['code' => $code], $options, ...$props);
34 | }
35 |
36 | public function getReference(string $code) : Country
37 | {
38 | /** @noinspection PhpIncompatibleReturnTypeInspection */
39 | return $this->gateway->getReference(Country::class, ['code' => $code]);
40 | }
41 |
42 | public function exists(Country $country) : bool
43 | {
44 | return $this->gateway->exists($country);
45 | }
46 |
47 | public function existsIdentity(string $code) : bool
48 | {
49 | return $this->gateway->existsIdentity(Country::class, ['code' => $code]);
50 | }
51 |
52 | public function add(Country $country) : void
53 | {
54 | $this->gateway->add($country);
55 | }
56 |
57 | public function update(Country $country) : void
58 | {
59 | $this->gateway->update($country);
60 | }
61 |
62 | public function remove(Country $country) : void
63 | {
64 | $this->gateway->remove($country);
65 | }
66 |
67 | public function removeIdentity(string $code) : void
68 | {
69 | $this->gateway->removeIdentity(Country::class, ['code' => $code]);
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/tests/Generated/Repository/EventRepository.php:
--------------------------------------------------------------------------------
1 | gateway = $gateway;
28 | }
29 |
30 | public function load(int $id, int $options = 0, string ...$props) : ?Event
31 | {
32 | /** @noinspection PhpIncompatibleReturnTypeInspection */
33 | return $this->gateway->load(Event::class, ['id' => $id], $options, ...$props);
34 | }
35 |
36 | public function getReference(int $id) : Event
37 | {
38 | /** @noinspection PhpIncompatibleReturnTypeInspection */
39 | return $this->gateway->getReference(Event::class, ['id' => $id]);
40 | }
41 |
42 | public function exists(Event $event) : bool
43 | {
44 | return $this->gateway->exists($event);
45 | }
46 |
47 | public function existsIdentity(int $id) : bool
48 | {
49 | return $this->gateway->existsIdentity(Event::class, ['id' => $id]);
50 | }
51 |
52 | public function add(Event $event) : void
53 | {
54 | $this->gateway->add($event);
55 | }
56 |
57 | public function update(Event $event) : void
58 | {
59 | $this->gateway->update($event);
60 | }
61 |
62 | public function remove(Event $event) : void
63 | {
64 | $this->gateway->remove($event);
65 | }
66 |
67 | public function removeIdentity(int $id) : void
68 | {
69 | $this->gateway->removeIdentity(Event::class, ['id' => $id]);
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/tests/Generated/Repository/FollowRepository.php:
--------------------------------------------------------------------------------
1 | gateway = $gateway;
29 | }
30 |
31 | public function load(User $follower, User $followee, int $options = 0, string ...$props) : ?Follow
32 | {
33 | /** @noinspection PhpIncompatibleReturnTypeInspection */
34 | return $this->gateway->load(Follow::class, ['follower' => $follower, 'followee' => $followee], $options, ...$props);
35 | }
36 |
37 | public function getReference(User $follower, User $followee) : Follow
38 | {
39 | /** @noinspection PhpIncompatibleReturnTypeInspection */
40 | return $this->gateway->getReference(Follow::class, ['follower' => $follower, 'followee' => $followee]);
41 | }
42 |
43 | public function exists(Follow $follow) : bool
44 | {
45 | return $this->gateway->exists($follow);
46 | }
47 |
48 | public function existsIdentity(User $follower, User $followee) : bool
49 | {
50 | return $this->gateway->existsIdentity(Follow::class, ['follower' => $follower, 'followee' => $followee]);
51 | }
52 |
53 | public function add(Follow $follow) : void
54 | {
55 | $this->gateway->add($follow);
56 | }
57 |
58 | public function update(Follow $follow) : void
59 | {
60 | $this->gateway->update($follow);
61 | }
62 |
63 | public function remove(Follow $follow) : void
64 | {
65 | $this->gateway->remove($follow);
66 | }
67 |
68 | public function removeIdentity(User $follower, User $followee) : void
69 | {
70 | $this->gateway->removeIdentity(Follow::class, ['follower' => $follower, 'followee' => $followee]);
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/tests/Generated/Repository/UserRepository.php:
--------------------------------------------------------------------------------
1 | gateway = $gateway;
28 | }
29 |
30 | public function load(int $id, int $options = 0, string ...$props) : ?User
31 | {
32 | /** @noinspection PhpIncompatibleReturnTypeInspection */
33 | return $this->gateway->load(User::class, ['id' => $id], $options, ...$props);
34 | }
35 |
36 | public function getReference(int $id) : User
37 | {
38 | /** @noinspection PhpIncompatibleReturnTypeInspection */
39 | return $this->gateway->getReference(User::class, ['id' => $id]);
40 | }
41 |
42 | public function exists(User $user) : bool
43 | {
44 | return $this->gateway->exists($user);
45 | }
46 |
47 | public function existsIdentity(int $id) : bool
48 | {
49 | return $this->gateway->existsIdentity(User::class, ['id' => $id]);
50 | }
51 |
52 | public function add(User $user) : void
53 | {
54 | $this->gateway->add($user);
55 | }
56 |
57 | public function update(User $user) : void
58 | {
59 | $this->gateway->update($user);
60 | }
61 |
62 | public function remove(User $user) : void
63 | {
64 | $this->gateway->remove($user);
65 | }
66 |
67 | public function removeIdentity(int $id) : void
68 | {
69 | $this->gateway->removeIdentity(User::class, ['id' => $id]);
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/tests/ObjectFactoryTest.php:
--------------------------------------------------------------------------------
1 | className = User::class;
31 | $testClassMetadata->properties = [];
32 |
33 | $objectFactory = new ObjectFactory();
34 | $user = $objectFactory->instantiate($testClassMetadata);
35 |
36 | $this->assertSame(User::class, get_class($user));
37 |
38 | $this->assertSame([
39 | "\0*\0billingAddress" => null,
40 | "\0*\0deliveryAddress" => null,
41 | "\0*\0lastEvent" => null,
42 | "\0*\0data" => ['any' => 'data'],
43 | "\0*\0transient" => []
44 | ], (array) $user);
45 |
46 | $this->assertSame([
47 | 'billingAddress' => null,
48 | 'deliveryAddress' => null,
49 | 'lastEvent' => null,
50 | 'data' => ['any' => 'data'],
51 | 'transient' => []
52 | ], $objectFactory->read($user));
53 | }
54 |
55 | public function testInstantiateWithPersistentProps()
56 | {
57 | $testClassMetadata = new EntityMetadata();
58 | $testClassMetadata->className = User::class;
59 | $testClassMetadata->properties = ['id', 'name', 'billingAddress', 'deliveryAddress', 'lastEvent', 'data'];
60 |
61 | $objectFactory = new ObjectFactory();
62 | $user = $objectFactory->instantiate($testClassMetadata);
63 |
64 | $this->assertSame(User::class, get_class($user));
65 |
66 | $this->assertSame([
67 | "\0*\0transient" => []
68 | ], (array) $user);
69 |
70 | $this->assertSame([
71 | 'transient' => []
72 | ], $objectFactory->read($user));
73 | }
74 |
75 | public function testWrite()
76 | {
77 | $values = [
78 | 'name' => 'John'
79 | ];
80 |
81 | $testClassMetadata = new EntityMetadata();
82 | $testClassMetadata->className = User::class;
83 | $testClassMetadata->properties = ['id', 'name', 'billingAddress', 'deliveryAddress', 'lastEvent', 'data'];
84 |
85 | $objectFactory = new ObjectFactory();
86 | $user = $objectFactory->instantiate($testClassMetadata);
87 | $objectFactory->write($user, $values);
88 |
89 | $this->assertSame(User::class, get_class($user));
90 |
91 | $this->assertSame([
92 | "\0*\0name" => 'John',
93 | "\0*\0transient" => []
94 | ], (array) $user);
95 |
96 | $this->assertSame([
97 | 'name' => 'John',
98 | 'transient' => []
99 | ], $objectFactory->read($user));
100 | }
101 | }
102 |
--------------------------------------------------------------------------------
/tests/ProxyTest.php:
--------------------------------------------------------------------------------
1 | add($country);
31 |
32 | $user = new User('John Smith');
33 |
34 | $billingAddress = new Address('123 Unknown Road', 'London', 'WC2E9XX', $country, false);
35 | $user->setBillingAddress($billingAddress);
36 |
37 | self::$userRepository->add($user);
38 | self::$logger->reset();
39 |
40 | return $user->getId();
41 | })();
42 |
43 | // reload the user
44 |
45 | $user = self::$userRepository->load($userId);
46 |
47 | $this->assertDebugStatementCount(1);
48 | $this->assertDebugStatement(0,
49 | 'SELECT a.id, a.name, a.street, a.city, a.zipcode, a.country_code, a.isPoBox, a.deliveryAddress_address_street, ' .
50 | 'a.deliveryAddress_address_city, a.deliveryAddress_address_zipcode, a.deliveryAddress_address_country_code, ' .
51 | 'a.deliveryAddress_address_isPoBox, ST_AsText(a.deliveryAddress_location), ST_SRID(a.deliveryAddress_location), ' .
52 | 'a.lastEvent_type, a.lastEvent_id, a.data ' .
53 | 'FROM User AS a WHERE a.id = ?',
54 | $userId
55 | );
56 |
57 | $country = $user->getBillingAddress()->getCountry();
58 |
59 | // Using an identity property: should be readily available and not trigger lazy initialization
60 | $this->assertSame('GB', $country->getCode());
61 | $this->assertDebugStatementCount(1);
62 |
63 | // Using a non-identity property: should initialize the proxy
64 | $this->assertSame('United Kingdom', $country->getName());
65 | $this->assertDebugStatementCount(2);
66 | $this->assertDebugStatement(1, 'SELECT a.name FROM Country AS a WHERE a.code = ?', 'GB');
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/tests/Resources/DTO/AddressDTO.php:
--------------------------------------------------------------------------------
1 | fieldName = $fieldName;
27 | $this->isNullable = $isNullable;
28 | }
29 |
30 | /**
31 | * {@inheritdoc}
32 | */
33 | public function getType() : ?string
34 | {
35 | return Geometry::class;
36 | }
37 |
38 | /**
39 | * {@inheritdoc}
40 | */
41 | public function isNullable() : bool
42 | {
43 | return $this->isNullable;
44 | }
45 |
46 | /**
47 | * {@inheritdoc}
48 | */
49 | public function getFieldNames(): array
50 | {
51 | return [$this->fieldName];
52 | }
53 |
54 | /**
55 | * {@inheritdoc}
56 | */
57 | public function getInputValuesCount() : int
58 | {
59 | return 2;
60 | }
61 |
62 | /**
63 | * {@inheritdoc}
64 | */
65 | public function getFieldToInputValuesSQL(array $fieldNames) : array
66 | {
67 | return [
68 | 'ST_AsText(' . $fieldNames[0] . ')',
69 | 'ST_SRID(' . $fieldNames[0] . ')'
70 | ];
71 | }
72 |
73 | /**
74 | * {@inheritdoc}
75 | */
76 | public function convertInputValuesToProp(Gateway $gateway, array $values) : mixed
77 | {
78 | [$wkt, $srid] = $values;
79 |
80 | if ($wkt === null) {
81 | return null;
82 | }
83 |
84 | return new Geometry($wkt, (int) $srid);
85 | }
86 |
87 | /**
88 | * {@inheritdoc}
89 | */
90 | public function convertPropToFields(mixed $propValue) : array
91 | {
92 | if ($propValue === null) {
93 | return [
94 | ['NULL']
95 | ];
96 | }
97 |
98 | /** @var Geometry $propValue */
99 | return [
100 | ['ST_GeomFromText(?, ?)', $propValue->getWKT(), $propValue->getSRID()]
101 | ];
102 | }
103 | }
104 |
--------------------------------------------------------------------------------
/tests/Resources/Models/Address.php:
--------------------------------------------------------------------------------
1 | street = $street;
28 | $this->city = $city;
29 | $this->postcode = $postcode;
30 | $this->country = $country;
31 | $this->isPoBox = $isPoBox;
32 | }
33 |
34 | public function getStreet() : string
35 | {
36 | return $this->street;
37 | }
38 |
39 | public function getCity() : string
40 | {
41 | return $this->city;
42 | }
43 |
44 | public function getPostcode() : string
45 | {
46 | return $this->postcode;
47 | }
48 |
49 | public function getCountry() : Country
50 | {
51 | return $this->country;
52 | }
53 |
54 | public function isPoBox() : bool
55 | {
56 | return $this->isPoBox;
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/tests/Resources/Models/Country.php:
--------------------------------------------------------------------------------
1 | code = $code;
19 | $this->name = $name;
20 | }
21 |
22 | public function getCode() : string
23 | {
24 | return $this->code;
25 | }
26 |
27 | public function getName() : string
28 | {
29 | return $this->name;
30 | }
31 |
32 | public function setName(string $name) : void
33 | {
34 | $this->name = $name;
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/tests/Resources/Models/Event.php:
--------------------------------------------------------------------------------
1 | time = 1234567890; // hardcoded for tests
19 | }
20 |
21 | public function getId() : int
22 | {
23 | return $this->id;
24 | }
25 |
26 | public function getTime() : int
27 | {
28 | return $this->time;
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/tests/Resources/Models/Event/CountryEvent.php:
--------------------------------------------------------------------------------
1 | country = $country;
19 | }
20 |
21 | public function getCountry() : Country
22 | {
23 | return $this->country;
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/tests/Resources/Models/Event/CountryEvent/CreateCountryEvent.php:
--------------------------------------------------------------------------------
1 | newName = $newName;
19 | }
20 |
21 | public function getNewName() : string
22 | {
23 | return $this->newName;
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/tests/Resources/Models/Event/FollowUserEvent.php:
--------------------------------------------------------------------------------
1 | follower = $follower;
23 | $this->followee = $followee;
24 | $this->isFollow = $isFollow;
25 | }
26 |
27 | public function getFollower() : User
28 | {
29 | return $this->follower;
30 | }
31 |
32 | public function getFollowee() : User
33 | {
34 | return $this->followee;
35 | }
36 |
37 | public function isFollow() : bool
38 | {
39 | return $this->isFollow;
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/tests/Resources/Models/Event/UserEvent.php:
--------------------------------------------------------------------------------
1 | user = $user;
19 | }
20 |
21 | public function getUser() : User
22 | {
23 | return $this->user;
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/tests/Resources/Models/Event/UserEvent/CreateUserEvent.php:
--------------------------------------------------------------------------------
1 | newAddress = $newBillingAddress;
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/tests/Resources/Models/Event/UserEvent/EditUserDeliveryAddressEvent.php:
--------------------------------------------------------------------------------
1 | newAddress = $newDeliveryAddress;
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/tests/Resources/Models/Event/UserEvent/EditUserNameEvent.php:
--------------------------------------------------------------------------------
1 | newName = $newName;
19 | }
20 |
21 | public function getNewName() : string
22 | {
23 | return $this->newName;
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/tests/Resources/Models/Follow.php:
--------------------------------------------------------------------------------
1 | follower = $follower;
21 | $this->followee = $followee;
22 | $this->since = time();
23 | }
24 |
25 | public function getFollower() : User
26 | {
27 | return $this->follower;
28 | }
29 |
30 | public function getFollowee() : User
31 | {
32 | return $this->followee;
33 | }
34 |
35 | public function getSince() : int
36 | {
37 | return $this->since;
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/tests/Resources/Models/GeoAddress.php:
--------------------------------------------------------------------------------
1 | address = $address;
21 | $this->location = $location;
22 | }
23 |
24 | /**
25 | * @return Address
26 | */
27 | public function getAddress() : Address
28 | {
29 | return $this->address;
30 | }
31 |
32 | /**
33 | * @return Geometry
34 | */
35 | public function getLocation() : Geometry
36 | {
37 | return $this->location;
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/tests/Resources/Models/User.php:
--------------------------------------------------------------------------------
1 | 'data'];
38 |
39 | /**
40 | * A transient property, that should not be persisted.
41 | */
42 | protected array $transient = [];
43 |
44 | public function __construct(string $name)
45 | {
46 | $this->name = $name;
47 | }
48 |
49 | public function getId() : int
50 | {
51 | return $this->id;
52 | }
53 |
54 | public function getName() : string
55 | {
56 | return $this->name;
57 | }
58 |
59 | public function setName(string $name) : void
60 | {
61 | $this->name = $name;
62 | }
63 |
64 | public function getBillingAddress() : ?Address
65 | {
66 | return $this->billingAddress;
67 | }
68 |
69 | public function setBillingAddress(?Address $billingAddress) : void
70 | {
71 | $this->billingAddress = $billingAddress;
72 | }
73 |
74 | public function getDeliveryAddress() : ?GeoAddress
75 | {
76 | return $this->deliveryAddress;
77 | }
78 |
79 | public function setDeliveryAddress(?GeoAddress $deliveryAddress) : void
80 | {
81 | $this->deliveryAddress = $deliveryAddress;
82 | }
83 |
84 | public function getData() : array
85 | {
86 | return $this->data;
87 | }
88 |
89 | public function getTransient() : array
90 | {
91 | return $this->transient;
92 | }
93 | }
94 |
--------------------------------------------------------------------------------
/tests/Resources/Objects/Geometry.php:
--------------------------------------------------------------------------------
1 | wkt = $wkt;
16 | $this->srid = $srid;
17 | }
18 |
19 | public function getWKT() : string
20 | {
21 | return $this->wkt;
22 | }
23 |
24 | public function getSRID() : int
25 | {
26 | return $this->srid;
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/tests/TableAliasGeneratorTest.php:
--------------------------------------------------------------------------------
1 | generate());
28 | }
29 | }
30 | }
31 |
--------------------------------------------------------------------------------