diff options
Diffstat (limited to 'vendor/doctrine/orm/src')
425 files changed, 51805 insertions, 0 deletions
diff --git a/vendor/doctrine/orm/src/AbstractQuery.php b/vendor/doctrine/orm/src/AbstractQuery.php new file mode 100644 index 0000000..0ff92c3 --- /dev/null +++ b/vendor/doctrine/orm/src/AbstractQuery.php | |||
| @@ -0,0 +1,1116 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM; | ||
| 6 | |||
| 7 | use BackedEnum; | ||
| 8 | use Doctrine\Common\Collections\ArrayCollection; | ||
| 9 | use Doctrine\Common\Collections\Collection; | ||
| 10 | use Doctrine\DBAL\ArrayParameterType; | ||
| 11 | use Doctrine\DBAL\Cache\QueryCacheProfile; | ||
| 12 | use Doctrine\DBAL\ParameterType; | ||
| 13 | use Doctrine\DBAL\Result; | ||
| 14 | use Doctrine\ORM\Cache\Logging\CacheLogger; | ||
| 15 | use Doctrine\ORM\Cache\QueryCacheKey; | ||
| 16 | use Doctrine\ORM\Cache\TimestampCacheKey; | ||
| 17 | use Doctrine\ORM\Mapping\ClassMetadata; | ||
| 18 | use Doctrine\ORM\Mapping\MappingException as ORMMappingException; | ||
| 19 | use Doctrine\ORM\Proxy\DefaultProxyClassNameResolver; | ||
| 20 | use Doctrine\ORM\Query\Parameter; | ||
| 21 | use Doctrine\ORM\Query\QueryException; | ||
| 22 | use Doctrine\ORM\Query\ResultSetMapping; | ||
| 23 | use Doctrine\Persistence\Mapping\MappingException; | ||
| 24 | use LogicException; | ||
| 25 | use Psr\Cache\CacheItemPoolInterface; | ||
| 26 | use Traversable; | ||
| 27 | |||
| 28 | use function array_map; | ||
| 29 | use function array_shift; | ||
| 30 | use function assert; | ||
| 31 | use function count; | ||
| 32 | use function is_array; | ||
| 33 | use function is_numeric; | ||
| 34 | use function is_object; | ||
| 35 | use function is_scalar; | ||
| 36 | use function is_string; | ||
| 37 | use function iterator_to_array; | ||
| 38 | use function ksort; | ||
| 39 | use function reset; | ||
| 40 | use function serialize; | ||
| 41 | use function sha1; | ||
| 42 | |||
| 43 | /** | ||
| 44 | * Base contract for ORM queries. Base class for Query and NativeQuery. | ||
| 45 | * | ||
| 46 | * @link www.doctrine-project.org | ||
| 47 | */ | ||
| 48 | abstract class AbstractQuery | ||
| 49 | { | ||
| 50 | /* Hydration mode constants */ | ||
| 51 | |||
| 52 | /** | ||
| 53 | * Hydrates an object graph. This is the default behavior. | ||
| 54 | */ | ||
| 55 | public const HYDRATE_OBJECT = 1; | ||
| 56 | |||
| 57 | /** | ||
| 58 | * Hydrates an array graph. | ||
| 59 | */ | ||
| 60 | public const HYDRATE_ARRAY = 2; | ||
| 61 | |||
| 62 | /** | ||
| 63 | * Hydrates a flat, rectangular result set with scalar values. | ||
| 64 | */ | ||
| 65 | public const HYDRATE_SCALAR = 3; | ||
| 66 | |||
| 67 | /** | ||
| 68 | * Hydrates a single scalar value. | ||
| 69 | */ | ||
| 70 | public const HYDRATE_SINGLE_SCALAR = 4; | ||
| 71 | |||
| 72 | /** | ||
| 73 | * Very simple object hydrator (optimized for performance). | ||
| 74 | */ | ||
| 75 | public const HYDRATE_SIMPLEOBJECT = 5; | ||
| 76 | |||
| 77 | /** | ||
| 78 | * Hydrates scalar column value. | ||
| 79 | */ | ||
| 80 | public const HYDRATE_SCALAR_COLUMN = 6; | ||
| 81 | |||
| 82 | /** | ||
| 83 | * The parameter map of this query. | ||
| 84 | * | ||
| 85 | * @var ArrayCollection|Parameter[] | ||
| 86 | * @psalm-var ArrayCollection<int, Parameter> | ||
| 87 | */ | ||
| 88 | protected ArrayCollection $parameters; | ||
| 89 | |||
| 90 | /** | ||
| 91 | * The user-specified ResultSetMapping to use. | ||
| 92 | */ | ||
| 93 | protected ResultSetMapping|null $resultSetMapping = null; | ||
| 94 | |||
| 95 | /** | ||
| 96 | * The map of query hints. | ||
| 97 | * | ||
| 98 | * @psalm-var array<string, mixed> | ||
| 99 | */ | ||
| 100 | protected array $hints = []; | ||
| 101 | |||
| 102 | /** | ||
| 103 | * The hydration mode. | ||
| 104 | * | ||
| 105 | * @psalm-var string|AbstractQuery::HYDRATE_* | ||
| 106 | */ | ||
| 107 | protected string|int $hydrationMode = self::HYDRATE_OBJECT; | ||
| 108 | |||
| 109 | protected QueryCacheProfile|null $queryCacheProfile = null; | ||
| 110 | |||
| 111 | /** | ||
| 112 | * Whether or not expire the result cache. | ||
| 113 | */ | ||
| 114 | protected bool $expireResultCache = false; | ||
| 115 | |||
| 116 | protected QueryCacheProfile|null $hydrationCacheProfile = null; | ||
| 117 | |||
| 118 | /** | ||
| 119 | * Whether to use second level cache, if available. | ||
| 120 | */ | ||
| 121 | protected bool $cacheable = false; | ||
| 122 | |||
| 123 | protected bool $hasCache = false; | ||
| 124 | |||
| 125 | /** | ||
| 126 | * Second level cache region name. | ||
| 127 | */ | ||
| 128 | protected string|null $cacheRegion = null; | ||
| 129 | |||
| 130 | /** | ||
| 131 | * Second level query cache mode. | ||
| 132 | * | ||
| 133 | * @psalm-var Cache::MODE_*|null | ||
| 134 | */ | ||
| 135 | protected int|null $cacheMode = null; | ||
| 136 | |||
| 137 | protected CacheLogger|null $cacheLogger = null; | ||
| 138 | |||
| 139 | protected int $lifetime = 0; | ||
| 140 | |||
| 141 | /** | ||
| 142 | * Initializes a new instance of a class derived from <tt>AbstractQuery</tt>. | ||
| 143 | */ | ||
| 144 | public function __construct( | ||
| 145 | /** | ||
| 146 | * The entity manager used by this query object. | ||
| 147 | */ | ||
| 148 | protected EntityManagerInterface $em, | ||
| 149 | ) { | ||
| 150 | $this->parameters = new ArrayCollection(); | ||
| 151 | $this->hints = $em->getConfiguration()->getDefaultQueryHints(); | ||
| 152 | $this->hasCache = $this->em->getConfiguration()->isSecondLevelCacheEnabled(); | ||
| 153 | |||
| 154 | if ($this->hasCache) { | ||
| 155 | $this->cacheLogger = $em->getConfiguration() | ||
| 156 | ->getSecondLevelCacheConfiguration() | ||
| 157 | ->getCacheLogger(); | ||
| 158 | } | ||
| 159 | } | ||
| 160 | |||
| 161 | /** | ||
| 162 | * Enable/disable second level query (result) caching for this query. | ||
| 163 | * | ||
| 164 | * @return $this | ||
| 165 | */ | ||
| 166 | public function setCacheable(bool $cacheable): static | ||
| 167 | { | ||
| 168 | $this->cacheable = $cacheable; | ||
| 169 | |||
| 170 | return $this; | ||
| 171 | } | ||
| 172 | |||
| 173 | /** @return bool TRUE if the query results are enabled for second level cache, FALSE otherwise. */ | ||
| 174 | public function isCacheable(): bool | ||
| 175 | { | ||
| 176 | return $this->cacheable; | ||
| 177 | } | ||
| 178 | |||
| 179 | /** @return $this */ | ||
| 180 | public function setCacheRegion(string $cacheRegion): static | ||
| 181 | { | ||
| 182 | $this->cacheRegion = $cacheRegion; | ||
| 183 | |||
| 184 | return $this; | ||
| 185 | } | ||
| 186 | |||
| 187 | /** | ||
| 188 | * Obtain the name of the second level query cache region in which query results will be stored | ||
| 189 | * | ||
| 190 | * @return string|null The cache region name; NULL indicates the default region. | ||
| 191 | */ | ||
| 192 | public function getCacheRegion(): string|null | ||
| 193 | { | ||
| 194 | return $this->cacheRegion; | ||
| 195 | } | ||
| 196 | |||
| 197 | /** @return bool TRUE if the query cache and second level cache are enabled, FALSE otherwise. */ | ||
| 198 | protected function isCacheEnabled(): bool | ||
| 199 | { | ||
| 200 | return $this->cacheable && $this->hasCache; | ||
| 201 | } | ||
| 202 | |||
| 203 | public function getLifetime(): int | ||
| 204 | { | ||
| 205 | return $this->lifetime; | ||
| 206 | } | ||
| 207 | |||
| 208 | /** | ||
| 209 | * Sets the life-time for this query into second level cache. | ||
| 210 | * | ||
| 211 | * @return $this | ||
| 212 | */ | ||
| 213 | public function setLifetime(int $lifetime): static | ||
| 214 | { | ||
| 215 | $this->lifetime = $lifetime; | ||
| 216 | |||
| 217 | return $this; | ||
| 218 | } | ||
| 219 | |||
| 220 | /** @psalm-return Cache::MODE_*|null */ | ||
| 221 | public function getCacheMode(): int|null | ||
| 222 | { | ||
| 223 | return $this->cacheMode; | ||
| 224 | } | ||
| 225 | |||
| 226 | /** | ||
| 227 | * @psalm-param Cache::MODE_* $cacheMode | ||
| 228 | * | ||
| 229 | * @return $this | ||
| 230 | */ | ||
| 231 | public function setCacheMode(int $cacheMode): static | ||
| 232 | { | ||
| 233 | $this->cacheMode = $cacheMode; | ||
| 234 | |||
| 235 | return $this; | ||
| 236 | } | ||
| 237 | |||
| 238 | /** | ||
| 239 | * Gets the SQL query that corresponds to this query object. | ||
| 240 | * The returned SQL syntax depends on the connection driver that is used | ||
| 241 | * by this query object at the time of this method call. | ||
| 242 | * | ||
| 243 | * @return list<string>|string SQL query | ||
| 244 | */ | ||
| 245 | abstract public function getSQL(): string|array; | ||
| 246 | |||
| 247 | /** | ||
| 248 | * Retrieves the associated EntityManager of this Query instance. | ||
| 249 | */ | ||
| 250 | public function getEntityManager(): EntityManagerInterface | ||
| 251 | { | ||
| 252 | return $this->em; | ||
| 253 | } | ||
| 254 | |||
| 255 | /** | ||
| 256 | * Frees the resources used by the query object. | ||
| 257 | * | ||
| 258 | * Resets Parameters, Parameter Types and Query Hints. | ||
| 259 | */ | ||
| 260 | public function free(): void | ||
| 261 | { | ||
| 262 | $this->parameters = new ArrayCollection(); | ||
| 263 | |||
| 264 | $this->hints = $this->em->getConfiguration()->getDefaultQueryHints(); | ||
| 265 | } | ||
| 266 | |||
| 267 | /** | ||
| 268 | * Get all defined parameters. | ||
| 269 | * | ||
| 270 | * @psalm-return ArrayCollection<int, Parameter> | ||
| 271 | */ | ||
| 272 | public function getParameters(): ArrayCollection | ||
| 273 | { | ||
| 274 | return $this->parameters; | ||
| 275 | } | ||
| 276 | |||
| 277 | /** | ||
| 278 | * Gets a query parameter. | ||
| 279 | * | ||
| 280 | * @param int|string $key The key (index or name) of the bound parameter. | ||
| 281 | * | ||
| 282 | * @return Parameter|null The value of the bound parameter, or NULL if not available. | ||
| 283 | */ | ||
| 284 | public function getParameter(int|string $key): Parameter|null | ||
| 285 | { | ||
| 286 | $key = Parameter::normalizeName($key); | ||
| 287 | |||
| 288 | $filteredParameters = $this->parameters->filter( | ||
| 289 | static fn (Parameter $parameter): bool => $parameter->getName() === $key | ||
| 290 | ); | ||
| 291 | |||
| 292 | return ! $filteredParameters->isEmpty() ? $filteredParameters->first() : null; | ||
| 293 | } | ||
| 294 | |||
| 295 | /** | ||
| 296 | * Sets a collection of query parameters. | ||
| 297 | * | ||
| 298 | * @param ArrayCollection|mixed[] $parameters | ||
| 299 | * @psalm-param ArrayCollection<int, Parameter>|mixed[] $parameters | ||
| 300 | * | ||
| 301 | * @return $this | ||
| 302 | */ | ||
| 303 | public function setParameters(ArrayCollection|array $parameters): static | ||
| 304 | { | ||
| 305 | if (is_array($parameters)) { | ||
| 306 | /** @psalm-var ArrayCollection<int, Parameter> $parameterCollection */ | ||
| 307 | $parameterCollection = new ArrayCollection(); | ||
| 308 | |||
| 309 | foreach ($parameters as $key => $value) { | ||
| 310 | $parameterCollection->add(new Parameter($key, $value)); | ||
| 311 | } | ||
| 312 | |||
| 313 | $parameters = $parameterCollection; | ||
| 314 | } | ||
| 315 | |||
| 316 | $this->parameters = $parameters; | ||
| 317 | |||
| 318 | return $this; | ||
| 319 | } | ||
| 320 | |||
| 321 | /** | ||
| 322 | * Sets a query parameter. | ||
| 323 | * | ||
| 324 | * @param string|int $key The parameter position or name. | ||
| 325 | * @param mixed $value The parameter value. | ||
| 326 | * @param ParameterType|ArrayParameterType|string|int|null $type The parameter type. If specified, the given value | ||
| 327 | * will be run through the type conversion of this | ||
| 328 | * type. This is usually not needed for strings and | ||
| 329 | * numeric types. | ||
| 330 | * | ||
| 331 | * @return $this | ||
| 332 | */ | ||
| 333 | public function setParameter(string|int $key, mixed $value, ParameterType|ArrayParameterType|string|int|null $type = null): static | ||
| 334 | { | ||
| 335 | $existingParameter = $this->getParameter($key); | ||
| 336 | |||
| 337 | if ($existingParameter !== null) { | ||
| 338 | $existingParameter->setValue($value, $type); | ||
| 339 | |||
| 340 | return $this; | ||
| 341 | } | ||
| 342 | |||
| 343 | $this->parameters->add(new Parameter($key, $value, $type)); | ||
| 344 | |||
| 345 | return $this; | ||
| 346 | } | ||
| 347 | |||
| 348 | /** | ||
| 349 | * Processes an individual parameter value. | ||
| 350 | * | ||
| 351 | * @throws ORMInvalidArgumentException | ||
| 352 | */ | ||
| 353 | public function processParameterValue(mixed $value): mixed | ||
| 354 | { | ||
| 355 | if (is_scalar($value)) { | ||
| 356 | return $value; | ||
| 357 | } | ||
| 358 | |||
| 359 | if ($value instanceof Collection) { | ||
| 360 | $value = iterator_to_array($value); | ||
| 361 | } | ||
| 362 | |||
| 363 | if (is_array($value)) { | ||
| 364 | $value = $this->processArrayParameterValue($value); | ||
| 365 | |||
| 366 | return $value; | ||
| 367 | } | ||
| 368 | |||
| 369 | if ($value instanceof ClassMetadata) { | ||
| 370 | return $value->name; | ||
| 371 | } | ||
| 372 | |||
| 373 | if ($value instanceof BackedEnum) { | ||
| 374 | return $value->value; | ||
| 375 | } | ||
| 376 | |||
| 377 | if (! is_object($value)) { | ||
| 378 | return $value; | ||
| 379 | } | ||
| 380 | |||
| 381 | try { | ||
| 382 | $class = DefaultProxyClassNameResolver::getClass($value); | ||
| 383 | $value = $this->em->getUnitOfWork()->getSingleIdentifierValue($value); | ||
| 384 | |||
| 385 | if ($value === null) { | ||
| 386 | throw ORMInvalidArgumentException::invalidIdentifierBindingEntity($class); | ||
| 387 | } | ||
| 388 | } catch (MappingException | ORMMappingException) { | ||
| 389 | /* Silence any mapping exceptions. These can occur if the object in | ||
| 390 | question is not a mapped entity, in which case we just don't do | ||
| 391 | any preparation on the value. | ||
| 392 | Depending on MappingDriver, either MappingException or | ||
| 393 | ORMMappingException is thrown. */ | ||
| 394 | |||
| 395 | $value = $this->potentiallyProcessIterable($value); | ||
| 396 | } | ||
| 397 | |||
| 398 | return $value; | ||
| 399 | } | ||
| 400 | |||
| 401 | /** | ||
| 402 | * If no mapping is detected, trying to resolve the value as a Traversable | ||
| 403 | */ | ||
| 404 | private function potentiallyProcessIterable(mixed $value): mixed | ||
| 405 | { | ||
| 406 | if ($value instanceof Traversable) { | ||
| 407 | $value = iterator_to_array($value); | ||
| 408 | $value = $this->processArrayParameterValue($value); | ||
| 409 | } | ||
| 410 | |||
| 411 | return $value; | ||
| 412 | } | ||
| 413 | |||
| 414 | /** | ||
| 415 | * Process a parameter value which was previously identified as an array | ||
| 416 | * | ||
| 417 | * @param mixed[] $value | ||
| 418 | * | ||
| 419 | * @return mixed[] | ||
| 420 | */ | ||
| 421 | private function processArrayParameterValue(array $value): array | ||
| 422 | { | ||
| 423 | foreach ($value as $key => $paramValue) { | ||
| 424 | $paramValue = $this->processParameterValue($paramValue); | ||
| 425 | $value[$key] = is_array($paramValue) ? reset($paramValue) : $paramValue; | ||
| 426 | } | ||
| 427 | |||
| 428 | return $value; | ||
| 429 | } | ||
| 430 | |||
| 431 | /** | ||
| 432 | * Sets the ResultSetMapping that should be used for hydration. | ||
| 433 | * | ||
| 434 | * @return $this | ||
| 435 | */ | ||
| 436 | public function setResultSetMapping(ResultSetMapping $rsm): static | ||
| 437 | { | ||
| 438 | $this->translateNamespaces($rsm); | ||
| 439 | $this->resultSetMapping = $rsm; | ||
| 440 | |||
| 441 | return $this; | ||
| 442 | } | ||
| 443 | |||
| 444 | /** | ||
| 445 | * Gets the ResultSetMapping used for hydration. | ||
| 446 | */ | ||
| 447 | protected function getResultSetMapping(): ResultSetMapping|null | ||
| 448 | { | ||
| 449 | return $this->resultSetMapping; | ||
| 450 | } | ||
| 451 | |||
| 452 | /** | ||
| 453 | * Allows to translate entity namespaces to full qualified names. | ||
| 454 | */ | ||
| 455 | private function translateNamespaces(ResultSetMapping $rsm): void | ||
| 456 | { | ||
| 457 | $translate = fn ($alias): string => $this->em->getClassMetadata($alias)->getName(); | ||
| 458 | |||
| 459 | $rsm->aliasMap = array_map($translate, $rsm->aliasMap); | ||
| 460 | $rsm->declaringClasses = array_map($translate, $rsm->declaringClasses); | ||
| 461 | } | ||
| 462 | |||
| 463 | /** | ||
| 464 | * Set a cache profile for hydration caching. | ||
| 465 | * | ||
| 466 | * If no result cache driver is set in the QueryCacheProfile, the default | ||
| 467 | * result cache driver is used from the configuration. | ||
| 468 | * | ||
| 469 | * Important: Hydration caching does NOT register entities in the | ||
| 470 | * UnitOfWork when retrieved from the cache. Never use result cached | ||
| 471 | * entities for requests that also flush the EntityManager. If you want | ||
| 472 | * some form of caching with UnitOfWork registration you should use | ||
| 473 | * {@see AbstractQuery::setResultCacheProfile()}. | ||
| 474 | * | ||
| 475 | * @return $this | ||
| 476 | * | ||
| 477 | * @example | ||
| 478 | * $lifetime = 100; | ||
| 479 | * $resultKey = "abc"; | ||
| 480 | * $query->setHydrationCacheProfile(new QueryCacheProfile()); | ||
| 481 | * $query->setHydrationCacheProfile(new QueryCacheProfile($lifetime, $resultKey)); | ||
| 482 | */ | ||
| 483 | public function setHydrationCacheProfile(QueryCacheProfile|null $profile): static | ||
| 484 | { | ||
| 485 | if ($profile === null) { | ||
| 486 | $this->hydrationCacheProfile = null; | ||
| 487 | |||
| 488 | return $this; | ||
| 489 | } | ||
| 490 | |||
| 491 | if (! $profile->getResultCache()) { | ||
| 492 | $defaultHydrationCacheImpl = $this->em->getConfiguration()->getHydrationCache(); | ||
| 493 | if ($defaultHydrationCacheImpl) { | ||
| 494 | $profile = $profile->setResultCache($defaultHydrationCacheImpl); | ||
| 495 | } | ||
| 496 | } | ||
| 497 | |||
| 498 | $this->hydrationCacheProfile = $profile; | ||
| 499 | |||
| 500 | return $this; | ||
| 501 | } | ||
| 502 | |||
| 503 | public function getHydrationCacheProfile(): QueryCacheProfile|null | ||
| 504 | { | ||
| 505 | return $this->hydrationCacheProfile; | ||
| 506 | } | ||
| 507 | |||
| 508 | /** | ||
| 509 | * Set a cache profile for the result cache. | ||
| 510 | * | ||
| 511 | * If no result cache driver is set in the QueryCacheProfile, the default | ||
| 512 | * result cache driver is used from the configuration. | ||
| 513 | * | ||
| 514 | * @return $this | ||
| 515 | */ | ||
| 516 | public function setResultCacheProfile(QueryCacheProfile|null $profile): static | ||
| 517 | { | ||
| 518 | if ($profile === null) { | ||
| 519 | $this->queryCacheProfile = null; | ||
| 520 | |||
| 521 | return $this; | ||
| 522 | } | ||
| 523 | |||
| 524 | if (! $profile->getResultCache()) { | ||
| 525 | $defaultResultCache = $this->em->getConfiguration()->getResultCache(); | ||
| 526 | if ($defaultResultCache) { | ||
| 527 | $profile = $profile->setResultCache($defaultResultCache); | ||
| 528 | } | ||
| 529 | } | ||
| 530 | |||
| 531 | $this->queryCacheProfile = $profile; | ||
| 532 | |||
| 533 | return $this; | ||
| 534 | } | ||
| 535 | |||
| 536 | /** | ||
| 537 | * Defines a cache driver to be used for caching result sets and implicitly enables caching. | ||
| 538 | */ | ||
| 539 | public function setResultCache(CacheItemPoolInterface|null $resultCache): static | ||
| 540 | { | ||
| 541 | if ($resultCache === null) { | ||
| 542 | if ($this->queryCacheProfile) { | ||
| 543 | $this->queryCacheProfile = new QueryCacheProfile($this->queryCacheProfile->getLifetime(), $this->queryCacheProfile->getCacheKey()); | ||
| 544 | } | ||
| 545 | |||
| 546 | return $this; | ||
| 547 | } | ||
| 548 | |||
| 549 | $this->queryCacheProfile = $this->queryCacheProfile | ||
| 550 | ? $this->queryCacheProfile->setResultCache($resultCache) | ||
| 551 | : new QueryCacheProfile(0, null, $resultCache); | ||
| 552 | |||
| 553 | return $this; | ||
| 554 | } | ||
| 555 | |||
| 556 | /** | ||
| 557 | * Enables caching of the results of this query, for given or default amount of seconds | ||
| 558 | * and optionally specifies which ID to use for the cache entry. | ||
| 559 | * | ||
| 560 | * @param int|null $lifetime How long the cache entry is valid, in seconds. | ||
| 561 | * @param string|null $resultCacheId ID to use for the cache entry. | ||
| 562 | * | ||
| 563 | * @return $this | ||
| 564 | */ | ||
| 565 | public function enableResultCache(int|null $lifetime = null, string|null $resultCacheId = null): static | ||
| 566 | { | ||
| 567 | $this->setResultCacheLifetime($lifetime); | ||
| 568 | $this->setResultCacheId($resultCacheId); | ||
| 569 | |||
| 570 | return $this; | ||
| 571 | } | ||
| 572 | |||
| 573 | /** | ||
| 574 | * Disables caching of the results of this query. | ||
| 575 | * | ||
| 576 | * @return $this | ||
| 577 | */ | ||
| 578 | public function disableResultCache(): static | ||
| 579 | { | ||
| 580 | $this->queryCacheProfile = null; | ||
| 581 | |||
| 582 | return $this; | ||
| 583 | } | ||
| 584 | |||
| 585 | /** | ||
| 586 | * Defines how long the result cache will be active before expire. | ||
| 587 | * | ||
| 588 | * @param int|null $lifetime How long the cache entry is valid, in seconds. | ||
| 589 | * | ||
| 590 | * @return $this | ||
| 591 | */ | ||
| 592 | public function setResultCacheLifetime(int|null $lifetime): static | ||
| 593 | { | ||
| 594 | $lifetime = (int) $lifetime; | ||
| 595 | |||
| 596 | if ($this->queryCacheProfile) { | ||
| 597 | $this->queryCacheProfile = $this->queryCacheProfile->setLifetime($lifetime); | ||
| 598 | |||
| 599 | return $this; | ||
| 600 | } | ||
| 601 | |||
| 602 | $this->queryCacheProfile = new QueryCacheProfile($lifetime); | ||
| 603 | |||
| 604 | $cache = $this->em->getConfiguration()->getResultCache(); | ||
| 605 | if (! $cache) { | ||
| 606 | return $this; | ||
| 607 | } | ||
| 608 | |||
| 609 | $this->queryCacheProfile = $this->queryCacheProfile->setResultCache($cache); | ||
| 610 | |||
| 611 | return $this; | ||
| 612 | } | ||
| 613 | |||
| 614 | /** | ||
| 615 | * Defines if the result cache is active or not. | ||
| 616 | * | ||
| 617 | * @param bool $expire Whether or not to force resultset cache expiration. | ||
| 618 | * | ||
| 619 | * @return $this | ||
| 620 | */ | ||
| 621 | public function expireResultCache(bool $expire = true): static | ||
| 622 | { | ||
| 623 | $this->expireResultCache = $expire; | ||
| 624 | |||
| 625 | return $this; | ||
| 626 | } | ||
| 627 | |||
| 628 | /** | ||
| 629 | * Retrieves if the resultset cache is active or not. | ||
| 630 | */ | ||
| 631 | public function getExpireResultCache(): bool | ||
| 632 | { | ||
| 633 | return $this->expireResultCache; | ||
| 634 | } | ||
| 635 | |||
| 636 | public function getQueryCacheProfile(): QueryCacheProfile|null | ||
| 637 | { | ||
| 638 | return $this->queryCacheProfile; | ||
| 639 | } | ||
| 640 | |||
| 641 | /** | ||
| 642 | * Change the default fetch mode of an association for this query. | ||
| 643 | * | ||
| 644 | * @param class-string $class | ||
| 645 | * @psalm-param Mapping\ClassMetadata::FETCH_EAGER|Mapping\ClassMetadata::FETCH_LAZY $fetchMode | ||
| 646 | */ | ||
| 647 | public function setFetchMode(string $class, string $assocName, int $fetchMode): static | ||
| 648 | { | ||
| 649 | $this->hints['fetchMode'][$class][$assocName] = $fetchMode; | ||
| 650 | |||
| 651 | return $this; | ||
| 652 | } | ||
| 653 | |||
| 654 | /** | ||
| 655 | * Defines the processing mode to be used during hydration / result set transformation. | ||
| 656 | * | ||
| 657 | * @param string|int $hydrationMode Doctrine processing mode to be used during hydration process. | ||
| 658 | * One of the Query::HYDRATE_* constants. | ||
| 659 | * @psalm-param string|AbstractQuery::HYDRATE_* $hydrationMode | ||
| 660 | * | ||
| 661 | * @return $this | ||
| 662 | */ | ||
| 663 | public function setHydrationMode(string|int $hydrationMode): static | ||
| 664 | { | ||
| 665 | $this->hydrationMode = $hydrationMode; | ||
| 666 | |||
| 667 | return $this; | ||
| 668 | } | ||
| 669 | |||
| 670 | /** | ||
| 671 | * Gets the hydration mode currently used by the query. | ||
| 672 | * | ||
| 673 | * @psalm-return string|AbstractQuery::HYDRATE_* | ||
| 674 | */ | ||
| 675 | public function getHydrationMode(): string|int | ||
| 676 | { | ||
| 677 | return $this->hydrationMode; | ||
| 678 | } | ||
| 679 | |||
| 680 | /** | ||
| 681 | * Gets the list of results for the query. | ||
| 682 | * | ||
| 683 | * Alias for execute(null, $hydrationMode = HYDRATE_OBJECT). | ||
| 684 | * | ||
| 685 | * @psalm-param string|AbstractQuery::HYDRATE_* $hydrationMode | ||
| 686 | */ | ||
| 687 | public function getResult(string|int $hydrationMode = self::HYDRATE_OBJECT): mixed | ||
| 688 | { | ||
| 689 | return $this->execute(null, $hydrationMode); | ||
| 690 | } | ||
| 691 | |||
| 692 | /** | ||
| 693 | * Gets the array of results for the query. | ||
| 694 | * | ||
| 695 | * Alias for execute(null, HYDRATE_ARRAY). | ||
| 696 | * | ||
| 697 | * @return mixed[] | ||
| 698 | */ | ||
| 699 | public function getArrayResult(): array | ||
| 700 | { | ||
| 701 | return $this->execute(null, self::HYDRATE_ARRAY); | ||
| 702 | } | ||
| 703 | |||
| 704 | /** | ||
| 705 | * Gets one-dimensional array of results for the query. | ||
| 706 | * | ||
| 707 | * Alias for execute(null, HYDRATE_SCALAR_COLUMN). | ||
| 708 | * | ||
| 709 | * @return mixed[] | ||
| 710 | */ | ||
| 711 | public function getSingleColumnResult(): array | ||
| 712 | { | ||
| 713 | return $this->execute(null, self::HYDRATE_SCALAR_COLUMN); | ||
| 714 | } | ||
| 715 | |||
| 716 | /** | ||
| 717 | * Gets the scalar results for the query. | ||
| 718 | * | ||
| 719 | * Alias for execute(null, HYDRATE_SCALAR). | ||
| 720 | * | ||
| 721 | * @return mixed[] | ||
| 722 | */ | ||
| 723 | public function getScalarResult(): array | ||
| 724 | { | ||
| 725 | return $this->execute(null, self::HYDRATE_SCALAR); | ||
| 726 | } | ||
| 727 | |||
| 728 | /** | ||
| 729 | * Get exactly one result or null. | ||
| 730 | * | ||
| 731 | * @psalm-param string|AbstractQuery::HYDRATE_*|null $hydrationMode | ||
| 732 | * | ||
| 733 | * @throws NonUniqueResultException | ||
| 734 | */ | ||
| 735 | public function getOneOrNullResult(string|int|null $hydrationMode = null): mixed | ||
| 736 | { | ||
| 737 | try { | ||
| 738 | $result = $this->execute(null, $hydrationMode); | ||
| 739 | } catch (NoResultException) { | ||
| 740 | return null; | ||
| 741 | } | ||
| 742 | |||
| 743 | if ($this->hydrationMode !== self::HYDRATE_SINGLE_SCALAR && ! $result) { | ||
| 744 | return null; | ||
| 745 | } | ||
| 746 | |||
| 747 | if (! is_array($result)) { | ||
| 748 | return $result; | ||
| 749 | } | ||
| 750 | |||
| 751 | if (count($result) > 1) { | ||
| 752 | throw new NonUniqueResultException(); | ||
| 753 | } | ||
| 754 | |||
| 755 | return array_shift($result); | ||
| 756 | } | ||
| 757 | |||
| 758 | /** | ||
| 759 | * Gets the single result of the query. | ||
| 760 | * | ||
| 761 | * Enforces the presence as well as the uniqueness of the result. | ||
| 762 | * | ||
| 763 | * If the result is not unique, a NonUniqueResultException is thrown. | ||
| 764 | * If there is no result, a NoResultException is thrown. | ||
| 765 | * | ||
| 766 | * @psalm-param string|AbstractQuery::HYDRATE_*|null $hydrationMode | ||
| 767 | * | ||
| 768 | * @throws NonUniqueResultException If the query result is not unique. | ||
| 769 | * @throws NoResultException If the query returned no result. | ||
| 770 | */ | ||
| 771 | public function getSingleResult(string|int|null $hydrationMode = null): mixed | ||
| 772 | { | ||
| 773 | $result = $this->execute(null, $hydrationMode); | ||
| 774 | |||
| 775 | if ($this->hydrationMode !== self::HYDRATE_SINGLE_SCALAR && ! $result) { | ||
| 776 | throw new NoResultException(); | ||
| 777 | } | ||
| 778 | |||
| 779 | if (! is_array($result)) { | ||
| 780 | return $result; | ||
| 781 | } | ||
| 782 | |||
| 783 | if (count($result) > 1) { | ||
| 784 | throw new NonUniqueResultException(); | ||
| 785 | } | ||
| 786 | |||
| 787 | return array_shift($result); | ||
| 788 | } | ||
| 789 | |||
| 790 | /** | ||
| 791 | * Gets the single scalar result of the query. | ||
| 792 | * | ||
| 793 | * Alias for getSingleResult(HYDRATE_SINGLE_SCALAR). | ||
| 794 | * | ||
| 795 | * @return bool|float|int|string|null The scalar result. | ||
| 796 | * | ||
| 797 | * @throws NoResultException If the query returned no result. | ||
| 798 | * @throws NonUniqueResultException If the query result is not unique. | ||
| 799 | */ | ||
| 800 | public function getSingleScalarResult(): mixed | ||
| 801 | { | ||
| 802 | return $this->getSingleResult(self::HYDRATE_SINGLE_SCALAR); | ||
| 803 | } | ||
| 804 | |||
| 805 | /** | ||
| 806 | * Sets a query hint. If the hint name is not recognized, it is silently ignored. | ||
| 807 | * | ||
| 808 | * @return $this | ||
| 809 | */ | ||
| 810 | public function setHint(string $name, mixed $value): static | ||
| 811 | { | ||
| 812 | $this->hints[$name] = $value; | ||
| 813 | |||
| 814 | return $this; | ||
| 815 | } | ||
| 816 | |||
| 817 | /** | ||
| 818 | * Gets the value of a query hint. If the hint name is not recognized, FALSE is returned. | ||
| 819 | * | ||
| 820 | * @return mixed The value of the hint or FALSE, if the hint name is not recognized. | ||
| 821 | */ | ||
| 822 | public function getHint(string $name): mixed | ||
| 823 | { | ||
| 824 | return $this->hints[$name] ?? false; | ||
| 825 | } | ||
| 826 | |||
| 827 | public function hasHint(string $name): bool | ||
| 828 | { | ||
| 829 | return isset($this->hints[$name]); | ||
| 830 | } | ||
| 831 | |||
| 832 | /** | ||
| 833 | * Return the key value map of query hints that are currently set. | ||
| 834 | * | ||
| 835 | * @return array<string,mixed> | ||
| 836 | */ | ||
| 837 | public function getHints(): array | ||
| 838 | { | ||
| 839 | return $this->hints; | ||
| 840 | } | ||
| 841 | |||
| 842 | /** | ||
| 843 | * Executes the query and returns an iterable that can be used to incrementally | ||
| 844 | * iterate over the result. | ||
| 845 | * | ||
| 846 | * @psalm-param ArrayCollection<int, Parameter>|mixed[] $parameters | ||
| 847 | * @psalm-param string|AbstractQuery::HYDRATE_*|null $hydrationMode | ||
| 848 | * | ||
| 849 | * @return iterable<mixed> | ||
| 850 | */ | ||
| 851 | public function toIterable( | ||
| 852 | ArrayCollection|array $parameters = [], | ||
| 853 | string|int|null $hydrationMode = null, | ||
| 854 | ): iterable { | ||
| 855 | if ($hydrationMode !== null) { | ||
| 856 | $this->setHydrationMode($hydrationMode); | ||
| 857 | } | ||
| 858 | |||
| 859 | if (count($parameters) !== 0) { | ||
| 860 | $this->setParameters($parameters); | ||
| 861 | } | ||
| 862 | |||
| 863 | $rsm = $this->getResultSetMapping(); | ||
| 864 | if ($rsm === null) { | ||
| 865 | throw new LogicException('Uninitialized result set mapping.'); | ||
| 866 | } | ||
| 867 | |||
| 868 | if ($rsm->isMixed && count($rsm->scalarMappings) > 0) { | ||
| 869 | throw QueryException::iterateWithMixedResultNotAllowed(); | ||
| 870 | } | ||
| 871 | |||
| 872 | $stmt = $this->_doExecute(); | ||
| 873 | |||
| 874 | return $this->em->newHydrator($this->hydrationMode)->toIterable($stmt, $rsm, $this->hints); | ||
| 875 | } | ||
| 876 | |||
| 877 | /** | ||
| 878 | * Executes the query. | ||
| 879 | * | ||
| 880 | * @psalm-param ArrayCollection<int, Parameter>|mixed[]|null $parameters | ||
| 881 | * @psalm-param string|AbstractQuery::HYDRATE_*|null $hydrationMode | ||
| 882 | */ | ||
| 883 | public function execute( | ||
| 884 | ArrayCollection|array|null $parameters = null, | ||
| 885 | string|int|null $hydrationMode = null, | ||
| 886 | ): mixed { | ||
| 887 | if ($this->cacheable && $this->isCacheEnabled()) { | ||
| 888 | return $this->executeUsingQueryCache($parameters, $hydrationMode); | ||
| 889 | } | ||
| 890 | |||
| 891 | return $this->executeIgnoreQueryCache($parameters, $hydrationMode); | ||
| 892 | } | ||
| 893 | |||
| 894 | /** | ||
| 895 | * Execute query ignoring second level cache. | ||
| 896 | * | ||
| 897 | * @psalm-param ArrayCollection<int, Parameter>|mixed[]|null $parameters | ||
| 898 | * @psalm-param string|AbstractQuery::HYDRATE_*|null $hydrationMode | ||
| 899 | */ | ||
| 900 | private function executeIgnoreQueryCache( | ||
| 901 | ArrayCollection|array|null $parameters = null, | ||
| 902 | string|int|null $hydrationMode = null, | ||
| 903 | ): mixed { | ||
| 904 | if ($hydrationMode !== null) { | ||
| 905 | $this->setHydrationMode($hydrationMode); | ||
| 906 | } | ||
| 907 | |||
| 908 | if (! empty($parameters)) { | ||
| 909 | $this->setParameters($parameters); | ||
| 910 | } | ||
| 911 | |||
| 912 | $setCacheEntry = static function ($data): void { | ||
| 913 | }; | ||
| 914 | |||
| 915 | if ($this->hydrationCacheProfile !== null) { | ||
| 916 | [$cacheKey, $realCacheKey] = $this->getHydrationCacheId(); | ||
| 917 | |||
| 918 | $cache = $this->getHydrationCache(); | ||
| 919 | $cacheItem = $cache->getItem($cacheKey); | ||
| 920 | $result = $cacheItem->isHit() ? $cacheItem->get() : []; | ||
| 921 | |||
| 922 | if (isset($result[$realCacheKey])) { | ||
| 923 | return $result[$realCacheKey]; | ||
| 924 | } | ||
| 925 | |||
| 926 | if (! $result) { | ||
| 927 | $result = []; | ||
| 928 | } | ||
| 929 | |||
| 930 | $setCacheEntry = static function ($data) use ($cache, $result, $cacheItem, $realCacheKey): void { | ||
| 931 | $cache->save($cacheItem->set($result + [$realCacheKey => $data])); | ||
| 932 | }; | ||
| 933 | } | ||
| 934 | |||
| 935 | $stmt = $this->_doExecute(); | ||
| 936 | |||
| 937 | if (is_numeric($stmt)) { | ||
| 938 | $setCacheEntry($stmt); | ||
| 939 | |||
| 940 | return $stmt; | ||
| 941 | } | ||
| 942 | |||
| 943 | $rsm = $this->getResultSetMapping(); | ||
| 944 | if ($rsm === null) { | ||
| 945 | throw new LogicException('Uninitialized result set mapping.'); | ||
| 946 | } | ||
| 947 | |||
| 948 | $data = $this->em->newHydrator($this->hydrationMode)->hydrateAll($stmt, $rsm, $this->hints); | ||
| 949 | |||
| 950 | $setCacheEntry($data); | ||
| 951 | |||
| 952 | return $data; | ||
| 953 | } | ||
| 954 | |||
| 955 | private function getHydrationCache(): CacheItemPoolInterface | ||
| 956 | { | ||
| 957 | assert($this->hydrationCacheProfile !== null); | ||
| 958 | |||
| 959 | $cache = $this->hydrationCacheProfile->getResultCache(); | ||
| 960 | assert($cache !== null); | ||
| 961 | |||
| 962 | return $cache; | ||
| 963 | } | ||
| 964 | |||
| 965 | /** | ||
| 966 | * Load from second level cache or executes the query and put into cache. | ||
| 967 | * | ||
| 968 | * @psalm-param ArrayCollection<int, Parameter>|mixed[]|null $parameters | ||
| 969 | * @psalm-param string|AbstractQuery::HYDRATE_*|null $hydrationMode | ||
| 970 | */ | ||
| 971 | private function executeUsingQueryCache( | ||
| 972 | ArrayCollection|array|null $parameters = null, | ||
| 973 | string|int|null $hydrationMode = null, | ||
| 974 | ): mixed { | ||
| 975 | $rsm = $this->getResultSetMapping(); | ||
| 976 | if ($rsm === null) { | ||
| 977 | throw new LogicException('Uninitialized result set mapping.'); | ||
| 978 | } | ||
| 979 | |||
| 980 | $queryCache = $this->em->getCache()->getQueryCache($this->cacheRegion); | ||
| 981 | $queryKey = new QueryCacheKey( | ||
| 982 | $this->getHash(), | ||
| 983 | $this->lifetime, | ||
| 984 | $this->cacheMode ?: Cache::MODE_NORMAL, | ||
| 985 | $this->getTimestampKey(), | ||
| 986 | ); | ||
| 987 | |||
| 988 | $result = $queryCache->get($queryKey, $rsm, $this->hints); | ||
| 989 | |||
| 990 | if ($result !== null) { | ||
| 991 | if ($this->cacheLogger) { | ||
| 992 | $this->cacheLogger->queryCacheHit($queryCache->getRegion()->getName(), $queryKey); | ||
| 993 | } | ||
| 994 | |||
| 995 | return $result; | ||
| 996 | } | ||
| 997 | |||
| 998 | $result = $this->executeIgnoreQueryCache($parameters, $hydrationMode); | ||
| 999 | $cached = $queryCache->put($queryKey, $rsm, $result, $this->hints); | ||
| 1000 | |||
| 1001 | if ($this->cacheLogger) { | ||
| 1002 | $this->cacheLogger->queryCacheMiss($queryCache->getRegion()->getName(), $queryKey); | ||
| 1003 | |||
| 1004 | if ($cached) { | ||
| 1005 | $this->cacheLogger->queryCachePut($queryCache->getRegion()->getName(), $queryKey); | ||
| 1006 | } | ||
| 1007 | } | ||
| 1008 | |||
| 1009 | return $result; | ||
| 1010 | } | ||
| 1011 | |||
| 1012 | private function getTimestampKey(): TimestampCacheKey|null | ||
| 1013 | { | ||
| 1014 | assert($this->resultSetMapping !== null); | ||
| 1015 | $entityName = reset($this->resultSetMapping->aliasMap); | ||
| 1016 | |||
| 1017 | if (empty($entityName)) { | ||
| 1018 | return null; | ||
| 1019 | } | ||
| 1020 | |||
| 1021 | $metadata = $this->em->getClassMetadata($entityName); | ||
| 1022 | |||
| 1023 | return new TimestampCacheKey($metadata->rootEntityName); | ||
| 1024 | } | ||
| 1025 | |||
| 1026 | /** | ||
| 1027 | * Get the result cache id to use to store the result set cache entry. | ||
| 1028 | * Will return the configured id if it exists otherwise a hash will be | ||
| 1029 | * automatically generated for you. | ||
| 1030 | * | ||
| 1031 | * @return string[] ($key, $hash) | ||
| 1032 | * @psalm-return array{string, string} ($key, $hash) | ||
| 1033 | */ | ||
| 1034 | protected function getHydrationCacheId(): array | ||
| 1035 | { | ||
| 1036 | $parameters = []; | ||
| 1037 | $types = []; | ||
| 1038 | |||
| 1039 | foreach ($this->getParameters() as $parameter) { | ||
| 1040 | $parameters[$parameter->getName()] = $this->processParameterValue($parameter->getValue()); | ||
| 1041 | $types[$parameter->getName()] = $parameter->getType(); | ||
| 1042 | } | ||
| 1043 | |||
| 1044 | $sql = $this->getSQL(); | ||
| 1045 | assert(is_string($sql)); | ||
| 1046 | $queryCacheProfile = $this->getHydrationCacheProfile(); | ||
| 1047 | $hints = $this->getHints(); | ||
| 1048 | $hints['hydrationMode'] = $this->getHydrationMode(); | ||
| 1049 | |||
| 1050 | ksort($hints); | ||
| 1051 | assert($queryCacheProfile !== null); | ||
| 1052 | |||
| 1053 | return $queryCacheProfile->generateCacheKeys($sql, $parameters, $types, $hints); | ||
| 1054 | } | ||
| 1055 | |||
| 1056 | /** | ||
| 1057 | * Set the result cache id to use to store the result set cache entry. | ||
| 1058 | * If this is not explicitly set by the developer then a hash is automatically | ||
| 1059 | * generated for you. | ||
| 1060 | */ | ||
| 1061 | public function setResultCacheId(string|null $id): static | ||
| 1062 | { | ||
| 1063 | if (! $this->queryCacheProfile) { | ||
| 1064 | return $this->setResultCacheProfile(new QueryCacheProfile(0, $id)); | ||
| 1065 | } | ||
| 1066 | |||
| 1067 | $this->queryCacheProfile = $this->queryCacheProfile->setCacheKey($id); | ||
| 1068 | |||
| 1069 | return $this; | ||
| 1070 | } | ||
| 1071 | |||
| 1072 | /** | ||
| 1073 | * Executes the query and returns a the resulting Statement object. | ||
| 1074 | * | ||
| 1075 | * @return Result|int The executed database statement that holds | ||
| 1076 | * the results, or an integer indicating how | ||
| 1077 | * many rows were affected. | ||
| 1078 | */ | ||
| 1079 | abstract protected function _doExecute(): Result|int; | ||
| 1080 | |||
| 1081 | /** | ||
| 1082 | * Cleanup Query resource when clone is called. | ||
| 1083 | */ | ||
| 1084 | public function __clone() | ||
| 1085 | { | ||
| 1086 | $this->parameters = new ArrayCollection(); | ||
| 1087 | |||
| 1088 | $this->hints = []; | ||
| 1089 | $this->hints = $this->em->getConfiguration()->getDefaultQueryHints(); | ||
| 1090 | } | ||
| 1091 | |||
| 1092 | /** | ||
| 1093 | * Generates a string of currently query to use for the cache second level cache. | ||
| 1094 | */ | ||
| 1095 | protected function getHash(): string | ||
| 1096 | { | ||
| 1097 | $query = $this->getSQL(); | ||
| 1098 | assert(is_string($query)); | ||
| 1099 | $hints = $this->getHints(); | ||
| 1100 | $params = array_map(function (Parameter $parameter) { | ||
| 1101 | $value = $parameter->getValue(); | ||
| 1102 | |||
| 1103 | // Small optimization | ||
| 1104 | // Does not invoke processParameterValue for scalar value | ||
| 1105 | if (is_scalar($value)) { | ||
| 1106 | return $value; | ||
| 1107 | } | ||
| 1108 | |||
| 1109 | return $this->processParameterValue($value); | ||
| 1110 | }, $this->parameters->getValues()); | ||
| 1111 | |||
| 1112 | ksort($hints); | ||
| 1113 | |||
| 1114 | return sha1($query . '-' . serialize($params) . '-' . serialize($hints)); | ||
| 1115 | } | ||
| 1116 | } | ||
diff --git a/vendor/doctrine/orm/src/Cache.php b/vendor/doctrine/orm/src/Cache.php new file mode 100644 index 0000000..8020b27 --- /dev/null +++ b/vendor/doctrine/orm/src/Cache.php | |||
| @@ -0,0 +1,106 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM; | ||
| 6 | |||
| 7 | use Doctrine\ORM\Cache\QueryCache; | ||
| 8 | use Doctrine\ORM\Cache\Region; | ||
| 9 | |||
| 10 | /** | ||
| 11 | * Provides an API for querying/managing the second level cache regions. | ||
| 12 | */ | ||
| 13 | interface Cache | ||
| 14 | { | ||
| 15 | public const DEFAULT_QUERY_REGION_NAME = 'query_cache_region'; | ||
| 16 | |||
| 17 | public const DEFAULT_TIMESTAMP_REGION_NAME = 'timestamp_cache_region'; | ||
| 18 | |||
| 19 | /** | ||
| 20 | * May read items from the cache, but will not add items. | ||
| 21 | */ | ||
| 22 | public const MODE_GET = 1; | ||
| 23 | |||
| 24 | /** | ||
| 25 | * Will never read items from the cache, | ||
| 26 | * but will add items to the cache as it reads them from the database. | ||
| 27 | */ | ||
| 28 | public const MODE_PUT = 2; | ||
| 29 | |||
| 30 | /** | ||
| 31 | * May read items from the cache, and add items to the cache. | ||
| 32 | */ | ||
| 33 | public const MODE_NORMAL = 3; | ||
| 34 | |||
| 35 | /** | ||
| 36 | * The query will never read items from the cache, | ||
| 37 | * but will refresh items to the cache as it reads them from the database. | ||
| 38 | */ | ||
| 39 | public const MODE_REFRESH = 4; | ||
| 40 | |||
| 41 | public function getEntityCacheRegion(string $className): Region|null; | ||
| 42 | |||
| 43 | public function getCollectionCacheRegion(string $className, string $association): Region|null; | ||
| 44 | |||
| 45 | /** | ||
| 46 | * Determine whether the cache contains data for the given entity "instance". | ||
| 47 | */ | ||
| 48 | public function containsEntity(string $className, mixed $identifier): bool; | ||
| 49 | |||
| 50 | /** | ||
| 51 | * Evicts the entity data for a particular entity "instance". | ||
| 52 | */ | ||
| 53 | public function evictEntity(string $className, mixed $identifier): void; | ||
| 54 | |||
| 55 | /** | ||
| 56 | * Evicts all entity data from the given region. | ||
| 57 | */ | ||
| 58 | public function evictEntityRegion(string $className): void; | ||
| 59 | |||
| 60 | /** | ||
| 61 | * Evict data from all entity regions. | ||
| 62 | */ | ||
| 63 | public function evictEntityRegions(): void; | ||
| 64 | |||
| 65 | /** | ||
| 66 | * Determine whether the cache contains data for the given collection. | ||
| 67 | */ | ||
| 68 | public function containsCollection(string $className, string $association, mixed $ownerIdentifier): bool; | ||
| 69 | |||
| 70 | /** | ||
| 71 | * Evicts the cache data for the given identified collection instance. | ||
| 72 | */ | ||
| 73 | public function evictCollection(string $className, string $association, mixed $ownerIdentifier): void; | ||
| 74 | |||
| 75 | /** | ||
| 76 | * Evicts all entity data from the given region. | ||
| 77 | */ | ||
| 78 | public function evictCollectionRegion(string $className, string $association): void; | ||
| 79 | |||
| 80 | /** | ||
| 81 | * Evict data from all collection regions. | ||
| 82 | */ | ||
| 83 | public function evictCollectionRegions(): void; | ||
| 84 | |||
| 85 | /** | ||
| 86 | * Determine whether the cache contains data for the given query. | ||
| 87 | */ | ||
| 88 | public function containsQuery(string $regionName): bool; | ||
| 89 | |||
| 90 | /** | ||
| 91 | * Evicts all cached query results under the given name, or default query cache if the region name is NULL. | ||
| 92 | */ | ||
| 93 | public function evictQueryRegion(string|null $regionName = null): void; | ||
| 94 | |||
| 95 | /** | ||
| 96 | * Evict data from all query regions. | ||
| 97 | */ | ||
| 98 | public function evictQueryRegions(): void; | ||
| 99 | |||
| 100 | /** | ||
| 101 | * Get query cache by region name or create a new one if none exist. | ||
| 102 | * | ||
| 103 | * @param string|null $regionName Query cache region name, or default query cache if the region name is NULL. | ||
| 104 | */ | ||
| 105 | public function getQueryCache(string|null $regionName = null): QueryCache; | ||
| 106 | } | ||
diff --git a/vendor/doctrine/orm/src/Cache/AssociationCacheEntry.php b/vendor/doctrine/orm/src/Cache/AssociationCacheEntry.php new file mode 100644 index 0000000..7dc1fbe --- /dev/null +++ b/vendor/doctrine/orm/src/Cache/AssociationCacheEntry.php | |||
| @@ -0,0 +1,30 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Cache; | ||
| 6 | |||
| 7 | class AssociationCacheEntry implements CacheEntry | ||
| 8 | { | ||
| 9 | /** | ||
| 10 | * @param array<string, mixed> $identifier The entity identifier. | ||
| 11 | * @param class-string $class The entity class name | ||
| 12 | */ | ||
| 13 | public function __construct( | ||
| 14 | public readonly string $class, | ||
| 15 | public readonly array $identifier, | ||
| 16 | ) { | ||
| 17 | } | ||
| 18 | |||
| 19 | /** | ||
| 20 | * Creates a new AssociationCacheEntry | ||
| 21 | * | ||
| 22 | * This method allow Doctrine\Common\Cache\PhpFileCache compatibility | ||
| 23 | * | ||
| 24 | * @param array<string, mixed> $values array containing property values | ||
| 25 | */ | ||
| 26 | public static function __set_state(array $values): self | ||
| 27 | { | ||
| 28 | return new self($values['class'], $values['identifier']); | ||
| 29 | } | ||
| 30 | } | ||
diff --git a/vendor/doctrine/orm/src/Cache/CacheConfiguration.php b/vendor/doctrine/orm/src/Cache/CacheConfiguration.php new file mode 100644 index 0000000..0f8dea7 --- /dev/null +++ b/vendor/doctrine/orm/src/Cache/CacheConfiguration.php | |||
| @@ -0,0 +1,60 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Cache; | ||
| 6 | |||
| 7 | use Doctrine\ORM\Cache\Logging\CacheLogger; | ||
| 8 | |||
| 9 | /** | ||
| 10 | * Configuration container for second-level cache. | ||
| 11 | */ | ||
| 12 | class CacheConfiguration | ||
| 13 | { | ||
| 14 | private CacheFactory|null $cacheFactory = null; | ||
| 15 | private RegionsConfiguration|null $regionsConfig = null; | ||
| 16 | private CacheLogger|null $cacheLogger = null; | ||
| 17 | private QueryCacheValidator|null $queryValidator = null; | ||
| 18 | |||
| 19 | public function getCacheFactory(): CacheFactory|null | ||
| 20 | { | ||
| 21 | return $this->cacheFactory; | ||
| 22 | } | ||
| 23 | |||
| 24 | public function setCacheFactory(CacheFactory $factory): void | ||
| 25 | { | ||
| 26 | $this->cacheFactory = $factory; | ||
| 27 | } | ||
| 28 | |||
| 29 | public function getCacheLogger(): CacheLogger|null | ||
| 30 | { | ||
| 31 | return $this->cacheLogger; | ||
| 32 | } | ||
| 33 | |||
| 34 | public function setCacheLogger(CacheLogger $logger): void | ||
| 35 | { | ||
| 36 | $this->cacheLogger = $logger; | ||
| 37 | } | ||
| 38 | |||
| 39 | public function getRegionsConfiguration(): RegionsConfiguration | ||
| 40 | { | ||
| 41 | return $this->regionsConfig ??= new RegionsConfiguration(); | ||
| 42 | } | ||
| 43 | |||
| 44 | public function setRegionsConfiguration(RegionsConfiguration $regionsConfig): void | ||
| 45 | { | ||
| 46 | $this->regionsConfig = $regionsConfig; | ||
| 47 | } | ||
| 48 | |||
| 49 | public function getQueryValidator(): QueryCacheValidator | ||
| 50 | { | ||
| 51 | return $this->queryValidator ??= new TimestampQueryCacheValidator( | ||
| 52 | $this->cacheFactory->getTimestampRegion(), | ||
| 53 | ); | ||
| 54 | } | ||
| 55 | |||
| 56 | public function setQueryValidator(QueryCacheValidator $validator): void | ||
| 57 | { | ||
| 58 | $this->queryValidator = $validator; | ||
| 59 | } | ||
| 60 | } | ||
diff --git a/vendor/doctrine/orm/src/Cache/CacheEntry.php b/vendor/doctrine/orm/src/Cache/CacheEntry.php new file mode 100644 index 0000000..6e12de1 --- /dev/null +++ b/vendor/doctrine/orm/src/Cache/CacheEntry.php | |||
| @@ -0,0 +1,16 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Cache; | ||
| 6 | |||
| 7 | /** | ||
| 8 | * Cache entry interface | ||
| 9 | * | ||
| 10 | * <b>IMPORTANT NOTE:</b> | ||
| 11 | * | ||
| 12 | * Fields of classes that implement CacheEntry are public for performance reason. | ||
| 13 | */ | ||
| 14 | interface CacheEntry | ||
| 15 | { | ||
| 16 | } | ||
diff --git a/vendor/doctrine/orm/src/Cache/CacheException.php b/vendor/doctrine/orm/src/Cache/CacheException.php new file mode 100644 index 0000000..b422095 --- /dev/null +++ b/vendor/doctrine/orm/src/Cache/CacheException.php | |||
| @@ -0,0 +1,26 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Cache; | ||
| 6 | |||
| 7 | use Doctrine\ORM\Exception\ORMException; | ||
| 8 | use LogicException; | ||
| 9 | |||
| 10 | use function sprintf; | ||
| 11 | |||
| 12 | /** | ||
| 13 | * Exception for cache. | ||
| 14 | */ | ||
| 15 | class CacheException extends LogicException implements ORMException | ||
| 16 | { | ||
| 17 | public static function updateReadOnlyCollection(string $sourceEntity, string $fieldName): self | ||
| 18 | { | ||
| 19 | return new self(sprintf('Cannot update a readonly collection "%s#%s"', $sourceEntity, $fieldName)); | ||
| 20 | } | ||
| 21 | |||
| 22 | public static function nonCacheableEntity(string $entityName): self | ||
| 23 | { | ||
| 24 | return new self(sprintf('Entity "%s" not configured as part of the second-level cache.', $entityName)); | ||
| 25 | } | ||
| 26 | } | ||
diff --git a/vendor/doctrine/orm/src/Cache/CacheFactory.php b/vendor/doctrine/orm/src/Cache/CacheFactory.php new file mode 100644 index 0000000..b15c23e --- /dev/null +++ b/vendor/doctrine/orm/src/Cache/CacheFactory.php | |||
| @@ -0,0 +1,64 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Cache; | ||
| 6 | |||
| 7 | use Doctrine\ORM\Cache; | ||
| 8 | use Doctrine\ORM\Cache\Persister\Collection\CachedCollectionPersister; | ||
| 9 | use Doctrine\ORM\Cache\Persister\Entity\CachedEntityPersister; | ||
| 10 | use Doctrine\ORM\EntityManagerInterface; | ||
| 11 | use Doctrine\ORM\Mapping\AssociationMapping; | ||
| 12 | use Doctrine\ORM\Mapping\ClassMetadata; | ||
| 13 | use Doctrine\ORM\Persisters\Collection\CollectionPersister; | ||
| 14 | use Doctrine\ORM\Persisters\Entity\EntityPersister; | ||
| 15 | |||
| 16 | /** | ||
| 17 | * Contract for building second level cache regions components. | ||
| 18 | */ | ||
| 19 | interface CacheFactory | ||
| 20 | { | ||
| 21 | /** | ||
| 22 | * Build an entity persister for the given entity metadata. | ||
| 23 | */ | ||
| 24 | public function buildCachedEntityPersister(EntityManagerInterface $em, EntityPersister $persister, ClassMetadata $metadata): CachedEntityPersister; | ||
| 25 | |||
| 26 | /** Build a collection persister for the given relation mapping. */ | ||
| 27 | public function buildCachedCollectionPersister( | ||
| 28 | EntityManagerInterface $em, | ||
| 29 | CollectionPersister $persister, | ||
| 30 | AssociationMapping $mapping, | ||
| 31 | ): CachedCollectionPersister; | ||
| 32 | |||
| 33 | /** | ||
| 34 | * Build a query cache based on the given region name | ||
| 35 | */ | ||
| 36 | public function buildQueryCache(EntityManagerInterface $em, string|null $regionName = null): QueryCache; | ||
| 37 | |||
| 38 | /** | ||
| 39 | * Build an entity hydrator | ||
| 40 | */ | ||
| 41 | public function buildEntityHydrator(EntityManagerInterface $em, ClassMetadata $metadata): EntityHydrator; | ||
| 42 | |||
| 43 | /** | ||
| 44 | * Build a collection hydrator | ||
| 45 | */ | ||
| 46 | public function buildCollectionHydrator(EntityManagerInterface $em, AssociationMapping $mapping): CollectionHydrator; | ||
| 47 | |||
| 48 | /** | ||
| 49 | * Build a cache region | ||
| 50 | * | ||
| 51 | * @param array<string,mixed> $cache The cache configuration. | ||
| 52 | */ | ||
| 53 | public function getRegion(array $cache): Region; | ||
| 54 | |||
| 55 | /** | ||
| 56 | * Build timestamp cache region | ||
| 57 | */ | ||
| 58 | public function getTimestampRegion(): TimestampRegion; | ||
| 59 | |||
| 60 | /** | ||
| 61 | * Build \Doctrine\ORM\Cache | ||
| 62 | */ | ||
| 63 | public function createCache(EntityManagerInterface $entityManager): Cache; | ||
| 64 | } | ||
diff --git a/vendor/doctrine/orm/src/Cache/CacheKey.php b/vendor/doctrine/orm/src/Cache/CacheKey.php new file mode 100644 index 0000000..970702c --- /dev/null +++ b/vendor/doctrine/orm/src/Cache/CacheKey.php | |||
| @@ -0,0 +1,16 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Cache; | ||
| 6 | |||
| 7 | /** | ||
| 8 | * Defines entity / collection / query key to be stored in the cache region. | ||
| 9 | * Allows multiple roles to be stored in the same cache region. | ||
| 10 | */ | ||
| 11 | abstract class CacheKey | ||
| 12 | { | ||
| 13 | public function __construct(public readonly string $hash) | ||
| 14 | { | ||
| 15 | } | ||
| 16 | } | ||
diff --git a/vendor/doctrine/orm/src/Cache/CollectionCacheEntry.php b/vendor/doctrine/orm/src/Cache/CollectionCacheEntry.php new file mode 100644 index 0000000..fde4575 --- /dev/null +++ b/vendor/doctrine/orm/src/Cache/CollectionCacheEntry.php | |||
| @@ -0,0 +1,25 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Cache; | ||
| 6 | |||
| 7 | class CollectionCacheEntry implements CacheEntry | ||
| 8 | { | ||
| 9 | /** @param CacheKey[] $identifiers List of entity identifiers hold by the collection */ | ||
| 10 | public function __construct(public readonly array $identifiers) | ||
| 11 | { | ||
| 12 | } | ||
| 13 | |||
| 14 | /** | ||
| 15 | * Creates a new CollectionCacheEntry | ||
| 16 | * | ||
| 17 | * This method allows for Doctrine\Common\Cache\PhpFileCache compatibility | ||
| 18 | * | ||
| 19 | * @param array<string, mixed> $values array containing property values | ||
| 20 | */ | ||
| 21 | public static function __set_state(array $values): CollectionCacheEntry | ||
| 22 | { | ||
| 23 | return new self($values['identifiers']); | ||
| 24 | } | ||
| 25 | } | ||
diff --git a/vendor/doctrine/orm/src/Cache/CollectionCacheKey.php b/vendor/doctrine/orm/src/Cache/CollectionCacheKey.php new file mode 100644 index 0000000..51b408f --- /dev/null +++ b/vendor/doctrine/orm/src/Cache/CollectionCacheKey.php | |||
| @@ -0,0 +1,39 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Cache; | ||
| 6 | |||
| 7 | use function implode; | ||
| 8 | use function ksort; | ||
| 9 | use function str_replace; | ||
| 10 | use function strtolower; | ||
| 11 | |||
| 12 | /** | ||
| 13 | * Defines entity collection roles to be stored in the cache region. | ||
| 14 | */ | ||
| 15 | class CollectionCacheKey extends CacheKey | ||
| 16 | { | ||
| 17 | /** | ||
| 18 | * The owner entity identifier | ||
| 19 | * | ||
| 20 | * @var array<string, mixed> | ||
| 21 | */ | ||
| 22 | public readonly array $ownerIdentifier; | ||
| 23 | |||
| 24 | /** | ||
| 25 | * @param array<string, mixed> $ownerIdentifier The identifier of the owning entity. | ||
| 26 | * @param class-string $entityClass The owner entity class | ||
| 27 | */ | ||
| 28 | public function __construct( | ||
| 29 | public readonly string $entityClass, | ||
| 30 | public readonly string $association, | ||
| 31 | array $ownerIdentifier, | ||
| 32 | ) { | ||
| 33 | ksort($ownerIdentifier); | ||
| 34 | |||
| 35 | $this->ownerIdentifier = $ownerIdentifier; | ||
| 36 | |||
| 37 | parent::__construct(str_replace('\\', '.', strtolower($entityClass)) . '_' . implode(' ', $ownerIdentifier) . '__' . $association); | ||
| 38 | } | ||
| 39 | } | ||
diff --git a/vendor/doctrine/orm/src/Cache/CollectionHydrator.php b/vendor/doctrine/orm/src/Cache/CollectionHydrator.php new file mode 100644 index 0000000..16a6572 --- /dev/null +++ b/vendor/doctrine/orm/src/Cache/CollectionHydrator.php | |||
| @@ -0,0 +1,21 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Cache; | ||
| 6 | |||
| 7 | use Doctrine\Common\Collections\Collection; | ||
| 8 | use Doctrine\ORM\Mapping\ClassMetadata; | ||
| 9 | use Doctrine\ORM\PersistentCollection; | ||
| 10 | |||
| 11 | /** | ||
| 12 | * Hydrator cache entry for collections | ||
| 13 | */ | ||
| 14 | interface CollectionHydrator | ||
| 15 | { | ||
| 16 | /** @param mixed[]|Collection $collection The collection. */ | ||
| 17 | public function buildCacheEntry(ClassMetadata $metadata, CollectionCacheKey $key, array|Collection $collection): CollectionCacheEntry; | ||
| 18 | |||
| 19 | /** @return mixed[]|null */ | ||
| 20 | public function loadCacheEntry(ClassMetadata $metadata, CollectionCacheKey $key, CollectionCacheEntry $entry, PersistentCollection $collection): array|null; | ||
| 21 | } | ||
diff --git a/vendor/doctrine/orm/src/Cache/ConcurrentRegion.php b/vendor/doctrine/orm/src/Cache/ConcurrentRegion.php new file mode 100644 index 0000000..e9ca870 --- /dev/null +++ b/vendor/doctrine/orm/src/Cache/ConcurrentRegion.php | |||
| @@ -0,0 +1,36 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Cache; | ||
| 6 | |||
| 7 | /** | ||
| 8 | * Defines contract for concurrently managed data region. | ||
| 9 | * It should be able to lock an specific cache entry in an atomic operation. | ||
| 10 | * | ||
| 11 | * When a entry is locked another process should not be able to read or write the entry. | ||
| 12 | * All evict operation should not consider locks, even though an entry is locked evict should be able to delete the entry and its lock. | ||
| 13 | */ | ||
| 14 | interface ConcurrentRegion extends Region | ||
| 15 | { | ||
| 16 | /** | ||
| 17 | * Attempts to read lock the mapping for the given key. | ||
| 18 | * | ||
| 19 | * @param CacheKey $key The key of the item to lock. | ||
| 20 | * | ||
| 21 | * @return Lock|null A lock instance or NULL if the lock already exists. | ||
| 22 | * | ||
| 23 | * @throws LockException Indicates a problem accessing the region. | ||
| 24 | */ | ||
| 25 | public function lock(CacheKey $key): Lock|null; | ||
| 26 | |||
| 27 | /** | ||
| 28 | * Attempts to read unlock the mapping for the given key. | ||
| 29 | * | ||
| 30 | * @param CacheKey $key The key of the item to unlock. | ||
| 31 | * @param Lock $lock The lock previously obtained from {@link readLock} | ||
| 32 | * | ||
| 33 | * @throws LockException Indicates a problem accessing the region. | ||
| 34 | */ | ||
| 35 | public function unlock(CacheKey $key, Lock $lock): bool; | ||
| 36 | } | ||
diff --git a/vendor/doctrine/orm/src/Cache/DefaultCache.php b/vendor/doctrine/orm/src/Cache/DefaultCache.php new file mode 100644 index 0000000..685181c --- /dev/null +++ b/vendor/doctrine/orm/src/Cache/DefaultCache.php | |||
| @@ -0,0 +1,245 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Cache; | ||
| 6 | |||
| 7 | use Doctrine\ORM\Cache; | ||
| 8 | use Doctrine\ORM\Cache\Persister\CachedPersister; | ||
| 9 | use Doctrine\ORM\EntityManagerInterface; | ||
| 10 | use Doctrine\ORM\Mapping\ClassMetadata; | ||
| 11 | use Doctrine\ORM\ORMInvalidArgumentException; | ||
| 12 | use Doctrine\ORM\Proxy\DefaultProxyClassNameResolver; | ||
| 13 | use Doctrine\ORM\UnitOfWork; | ||
| 14 | |||
| 15 | use function is_array; | ||
| 16 | use function is_object; | ||
| 17 | |||
| 18 | /** | ||
| 19 | * Provides an API for querying/managing the second level cache regions. | ||
| 20 | */ | ||
| 21 | class DefaultCache implements Cache | ||
| 22 | { | ||
| 23 | private readonly UnitOfWork $uow; | ||
| 24 | private readonly CacheFactory $cacheFactory; | ||
| 25 | |||
| 26 | /** | ||
| 27 | * @var QueryCache[] | ||
| 28 | * @psalm-var array<string, QueryCache> | ||
| 29 | */ | ||
| 30 | private array $queryCaches = []; | ||
| 31 | |||
| 32 | private QueryCache|null $defaultQueryCache = null; | ||
| 33 | |||
| 34 | public function __construct( | ||
| 35 | private readonly EntityManagerInterface $em, | ||
| 36 | ) { | ||
| 37 | $this->uow = $em->getUnitOfWork(); | ||
| 38 | $this->cacheFactory = $em->getConfiguration() | ||
| 39 | ->getSecondLevelCacheConfiguration() | ||
| 40 | ->getCacheFactory(); | ||
| 41 | } | ||
| 42 | |||
| 43 | public function getEntityCacheRegion(string $className): Region|null | ||
| 44 | { | ||
| 45 | $metadata = $this->em->getClassMetadata($className); | ||
| 46 | $persister = $this->uow->getEntityPersister($metadata->rootEntityName); | ||
| 47 | |||
| 48 | if (! ($persister instanceof CachedPersister)) { | ||
| 49 | return null; | ||
| 50 | } | ||
| 51 | |||
| 52 | return $persister->getCacheRegion(); | ||
| 53 | } | ||
| 54 | |||
| 55 | public function getCollectionCacheRegion(string $className, string $association): Region|null | ||
| 56 | { | ||
| 57 | $metadata = $this->em->getClassMetadata($className); | ||
| 58 | $persister = $this->uow->getCollectionPersister($metadata->getAssociationMapping($association)); | ||
| 59 | |||
| 60 | if (! ($persister instanceof CachedPersister)) { | ||
| 61 | return null; | ||
| 62 | } | ||
| 63 | |||
| 64 | return $persister->getCacheRegion(); | ||
| 65 | } | ||
| 66 | |||
| 67 | public function containsEntity(string $className, mixed $identifier): bool | ||
| 68 | { | ||
| 69 | $metadata = $this->em->getClassMetadata($className); | ||
| 70 | $persister = $this->uow->getEntityPersister($metadata->rootEntityName); | ||
| 71 | |||
| 72 | if (! ($persister instanceof CachedPersister)) { | ||
| 73 | return false; | ||
| 74 | } | ||
| 75 | |||
| 76 | return $persister->getCacheRegion()->contains($this->buildEntityCacheKey($metadata, $identifier)); | ||
| 77 | } | ||
| 78 | |||
| 79 | public function evictEntity(string $className, mixed $identifier): void | ||
| 80 | { | ||
| 81 | $metadata = $this->em->getClassMetadata($className); | ||
| 82 | $persister = $this->uow->getEntityPersister($metadata->rootEntityName); | ||
| 83 | |||
| 84 | if (! ($persister instanceof CachedPersister)) { | ||
| 85 | return; | ||
| 86 | } | ||
| 87 | |||
| 88 | $persister->getCacheRegion()->evict($this->buildEntityCacheKey($metadata, $identifier)); | ||
| 89 | } | ||
| 90 | |||
| 91 | public function evictEntityRegion(string $className): void | ||
| 92 | { | ||
| 93 | $metadata = $this->em->getClassMetadata($className); | ||
| 94 | $persister = $this->uow->getEntityPersister($metadata->rootEntityName); | ||
| 95 | |||
| 96 | if (! ($persister instanceof CachedPersister)) { | ||
| 97 | return; | ||
| 98 | } | ||
| 99 | |||
| 100 | $persister->getCacheRegion()->evictAll(); | ||
| 101 | } | ||
| 102 | |||
| 103 | public function evictEntityRegions(): void | ||
| 104 | { | ||
| 105 | $metadatas = $this->em->getMetadataFactory()->getAllMetadata(); | ||
| 106 | |||
| 107 | foreach ($metadatas as $metadata) { | ||
| 108 | $persister = $this->uow->getEntityPersister($metadata->rootEntityName); | ||
| 109 | |||
| 110 | if (! ($persister instanceof CachedPersister)) { | ||
| 111 | continue; | ||
| 112 | } | ||
| 113 | |||
| 114 | $persister->getCacheRegion()->evictAll(); | ||
| 115 | } | ||
| 116 | } | ||
| 117 | |||
| 118 | public function containsCollection(string $className, string $association, mixed $ownerIdentifier): bool | ||
| 119 | { | ||
| 120 | $metadata = $this->em->getClassMetadata($className); | ||
| 121 | $persister = $this->uow->getCollectionPersister($metadata->getAssociationMapping($association)); | ||
| 122 | |||
| 123 | if (! ($persister instanceof CachedPersister)) { | ||
| 124 | return false; | ||
| 125 | } | ||
| 126 | |||
| 127 | return $persister->getCacheRegion()->contains($this->buildCollectionCacheKey($metadata, $association, $ownerIdentifier)); | ||
| 128 | } | ||
| 129 | |||
| 130 | public function evictCollection(string $className, string $association, mixed $ownerIdentifier): void | ||
| 131 | { | ||
| 132 | $metadata = $this->em->getClassMetadata($className); | ||
| 133 | $persister = $this->uow->getCollectionPersister($metadata->getAssociationMapping($association)); | ||
| 134 | |||
| 135 | if (! ($persister instanceof CachedPersister)) { | ||
| 136 | return; | ||
| 137 | } | ||
| 138 | |||
| 139 | $persister->getCacheRegion()->evict($this->buildCollectionCacheKey($metadata, $association, $ownerIdentifier)); | ||
| 140 | } | ||
| 141 | |||
| 142 | public function evictCollectionRegion(string $className, string $association): void | ||
| 143 | { | ||
| 144 | $metadata = $this->em->getClassMetadata($className); | ||
| 145 | $persister = $this->uow->getCollectionPersister($metadata->getAssociationMapping($association)); | ||
| 146 | |||
| 147 | if (! ($persister instanceof CachedPersister)) { | ||
| 148 | return; | ||
| 149 | } | ||
| 150 | |||
| 151 | $persister->getCacheRegion()->evictAll(); | ||
| 152 | } | ||
| 153 | |||
| 154 | public function evictCollectionRegions(): void | ||
| 155 | { | ||
| 156 | $metadatas = $this->em->getMetadataFactory()->getAllMetadata(); | ||
| 157 | |||
| 158 | foreach ($metadatas as $metadata) { | ||
| 159 | foreach ($metadata->associationMappings as $association) { | ||
| 160 | if (! $association->isToMany()) { | ||
| 161 | continue; | ||
| 162 | } | ||
| 163 | |||
| 164 | $persister = $this->uow->getCollectionPersister($association); | ||
| 165 | |||
| 166 | if (! ($persister instanceof CachedPersister)) { | ||
| 167 | continue; | ||
| 168 | } | ||
| 169 | |||
| 170 | $persister->getCacheRegion()->evictAll(); | ||
| 171 | } | ||
| 172 | } | ||
| 173 | } | ||
| 174 | |||
| 175 | public function containsQuery(string $regionName): bool | ||
| 176 | { | ||
| 177 | return isset($this->queryCaches[$regionName]); | ||
| 178 | } | ||
| 179 | |||
| 180 | public function evictQueryRegion(string|null $regionName = null): void | ||
| 181 | { | ||
| 182 | if ($regionName === null && $this->defaultQueryCache !== null) { | ||
| 183 | $this->defaultQueryCache->clear(); | ||
| 184 | |||
| 185 | return; | ||
| 186 | } | ||
| 187 | |||
| 188 | if (isset($this->queryCaches[$regionName])) { | ||
| 189 | $this->queryCaches[$regionName]->clear(); | ||
| 190 | } | ||
| 191 | } | ||
| 192 | |||
| 193 | public function evictQueryRegions(): void | ||
| 194 | { | ||
| 195 | $this->getQueryCache()->clear(); | ||
| 196 | |||
| 197 | foreach ($this->queryCaches as $queryCache) { | ||
| 198 | $queryCache->clear(); | ||
| 199 | } | ||
| 200 | } | ||
| 201 | |||
| 202 | public function getQueryCache(string|null $regionName = null): QueryCache | ||
| 203 | { | ||
| 204 | if ($regionName === null) { | ||
| 205 | return $this->defaultQueryCache ??= $this->cacheFactory->buildQueryCache($this->em); | ||
| 206 | } | ||
| 207 | |||
| 208 | return $this->queryCaches[$regionName] ??= $this->cacheFactory->buildQueryCache($this->em, $regionName); | ||
| 209 | } | ||
| 210 | |||
| 211 | private function buildEntityCacheKey(ClassMetadata $metadata, mixed $identifier): EntityCacheKey | ||
| 212 | { | ||
| 213 | if (! is_array($identifier)) { | ||
| 214 | $identifier = $this->toIdentifierArray($metadata, $identifier); | ||
| 215 | } | ||
| 216 | |||
| 217 | return new EntityCacheKey($metadata->rootEntityName, $identifier); | ||
| 218 | } | ||
| 219 | |||
| 220 | private function buildCollectionCacheKey( | ||
| 221 | ClassMetadata $metadata, | ||
| 222 | string $association, | ||
| 223 | mixed $ownerIdentifier, | ||
| 224 | ): CollectionCacheKey { | ||
| 225 | if (! is_array($ownerIdentifier)) { | ||
| 226 | $ownerIdentifier = $this->toIdentifierArray($metadata, $ownerIdentifier); | ||
| 227 | } | ||
| 228 | |||
| 229 | return new CollectionCacheKey($metadata->rootEntityName, $association, $ownerIdentifier); | ||
| 230 | } | ||
| 231 | |||
| 232 | /** @return array<string, mixed> */ | ||
| 233 | private function toIdentifierArray(ClassMetadata $metadata, mixed $identifier): array | ||
| 234 | { | ||
| 235 | if (is_object($identifier)) { | ||
| 236 | $class = DefaultProxyClassNameResolver::getClass($identifier); | ||
| 237 | if ($this->em->getMetadataFactory()->hasMetadataFor($class)) { | ||
| 238 | $identifier = $this->uow->getSingleIdentifierValue($identifier) | ||
| 239 | ?? throw ORMInvalidArgumentException::invalidIdentifierBindingEntity($class); | ||
| 240 | } | ||
| 241 | } | ||
| 242 | |||
| 243 | return [$metadata->identifier[0] => $identifier]; | ||
| 244 | } | ||
| 245 | } | ||
diff --git a/vendor/doctrine/orm/src/Cache/DefaultCacheFactory.php b/vendor/doctrine/orm/src/Cache/DefaultCacheFactory.php new file mode 100644 index 0000000..84ea490 --- /dev/null +++ b/vendor/doctrine/orm/src/Cache/DefaultCacheFactory.php | |||
| @@ -0,0 +1,189 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Cache; | ||
| 6 | |||
| 7 | use Doctrine\ORM\Cache; | ||
| 8 | use Doctrine\ORM\Cache\Persister\Collection\CachedCollectionPersister; | ||
| 9 | use Doctrine\ORM\Cache\Persister\Collection\NonStrictReadWriteCachedCollectionPersister; | ||
| 10 | use Doctrine\ORM\Cache\Persister\Collection\ReadOnlyCachedCollectionPersister; | ||
| 11 | use Doctrine\ORM\Cache\Persister\Collection\ReadWriteCachedCollectionPersister; | ||
| 12 | use Doctrine\ORM\Cache\Persister\Entity\CachedEntityPersister; | ||
| 13 | use Doctrine\ORM\Cache\Persister\Entity\NonStrictReadWriteCachedEntityPersister; | ||
| 14 | use Doctrine\ORM\Cache\Persister\Entity\ReadOnlyCachedEntityPersister; | ||
| 15 | use Doctrine\ORM\Cache\Persister\Entity\ReadWriteCachedEntityPersister; | ||
| 16 | use Doctrine\ORM\Cache\Region\DefaultRegion; | ||
| 17 | use Doctrine\ORM\Cache\Region\FileLockRegion; | ||
| 18 | use Doctrine\ORM\Cache\Region\UpdateTimestampCache; | ||
| 19 | use Doctrine\ORM\EntityManagerInterface; | ||
| 20 | use Doctrine\ORM\Mapping\AssociationMapping; | ||
| 21 | use Doctrine\ORM\Mapping\ClassMetadata; | ||
| 22 | use Doctrine\ORM\Persisters\Collection\CollectionPersister; | ||
| 23 | use Doctrine\ORM\Persisters\Entity\EntityPersister; | ||
| 24 | use InvalidArgumentException; | ||
| 25 | use LogicException; | ||
| 26 | use Psr\Cache\CacheItemPoolInterface; | ||
| 27 | |||
| 28 | use function assert; | ||
| 29 | use function sprintf; | ||
| 30 | |||
| 31 | use const DIRECTORY_SEPARATOR; | ||
| 32 | |||
| 33 | class DefaultCacheFactory implements CacheFactory | ||
| 34 | { | ||
| 35 | private TimestampRegion|null $timestampRegion = null; | ||
| 36 | |||
| 37 | /** @var Region[] */ | ||
| 38 | private array $regions = []; | ||
| 39 | |||
| 40 | private string|null $fileLockRegionDirectory = null; | ||
| 41 | |||
| 42 | public function __construct(private readonly RegionsConfiguration $regionsConfig, private readonly CacheItemPoolInterface $cacheItemPool) | ||
| 43 | { | ||
| 44 | } | ||
| 45 | |||
| 46 | public function setFileLockRegionDirectory(string $fileLockRegionDirectory): void | ||
| 47 | { | ||
| 48 | $this->fileLockRegionDirectory = $fileLockRegionDirectory; | ||
| 49 | } | ||
| 50 | |||
| 51 | public function getFileLockRegionDirectory(): string|null | ||
| 52 | { | ||
| 53 | return $this->fileLockRegionDirectory; | ||
| 54 | } | ||
| 55 | |||
| 56 | public function setRegion(Region $region): void | ||
| 57 | { | ||
| 58 | $this->regions[$region->getName()] = $region; | ||
| 59 | } | ||
| 60 | |||
| 61 | public function setTimestampRegion(TimestampRegion $region): void | ||
| 62 | { | ||
| 63 | $this->timestampRegion = $region; | ||
| 64 | } | ||
| 65 | |||
| 66 | public function buildCachedEntityPersister(EntityManagerInterface $em, EntityPersister $persister, ClassMetadata $metadata): CachedEntityPersister | ||
| 67 | { | ||
| 68 | assert($metadata->cache !== null); | ||
| 69 | $region = $this->getRegion($metadata->cache); | ||
| 70 | $usage = $metadata->cache['usage']; | ||
| 71 | |||
| 72 | if ($usage === ClassMetadata::CACHE_USAGE_READ_ONLY) { | ||
| 73 | return new ReadOnlyCachedEntityPersister($persister, $region, $em, $metadata); | ||
| 74 | } | ||
| 75 | |||
| 76 | if ($usage === ClassMetadata::CACHE_USAGE_NONSTRICT_READ_WRITE) { | ||
| 77 | return new NonStrictReadWriteCachedEntityPersister($persister, $region, $em, $metadata); | ||
| 78 | } | ||
| 79 | |||
| 80 | if ($usage === ClassMetadata::CACHE_USAGE_READ_WRITE) { | ||
| 81 | if (! $region instanceof ConcurrentRegion) { | ||
| 82 | throw new InvalidArgumentException(sprintf('Unable to use access strategy type of [%s] without a ConcurrentRegion', $usage)); | ||
| 83 | } | ||
| 84 | |||
| 85 | return new ReadWriteCachedEntityPersister($persister, $region, $em, $metadata); | ||
| 86 | } | ||
| 87 | |||
| 88 | throw new InvalidArgumentException(sprintf('Unrecognized access strategy type [%s]', $usage)); | ||
| 89 | } | ||
| 90 | |||
| 91 | public function buildCachedCollectionPersister( | ||
| 92 | EntityManagerInterface $em, | ||
| 93 | CollectionPersister $persister, | ||
| 94 | AssociationMapping $mapping, | ||
| 95 | ): CachedCollectionPersister { | ||
| 96 | assert(isset($mapping->cache)); | ||
| 97 | $usage = $mapping->cache['usage']; | ||
| 98 | $region = $this->getRegion($mapping->cache); | ||
| 99 | |||
| 100 | if ($usage === ClassMetadata::CACHE_USAGE_READ_ONLY) { | ||
| 101 | return new ReadOnlyCachedCollectionPersister($persister, $region, $em, $mapping); | ||
| 102 | } | ||
| 103 | |||
| 104 | if ($usage === ClassMetadata::CACHE_USAGE_NONSTRICT_READ_WRITE) { | ||
| 105 | return new NonStrictReadWriteCachedCollectionPersister($persister, $region, $em, $mapping); | ||
| 106 | } | ||
| 107 | |||
| 108 | if ($usage === ClassMetadata::CACHE_USAGE_READ_WRITE) { | ||
| 109 | if (! $region instanceof ConcurrentRegion) { | ||
| 110 | throw new InvalidArgumentException(sprintf('Unable to use access strategy type of [%s] without a ConcurrentRegion', $usage)); | ||
| 111 | } | ||
| 112 | |||
| 113 | return new ReadWriteCachedCollectionPersister($persister, $region, $em, $mapping); | ||
| 114 | } | ||
| 115 | |||
| 116 | throw new InvalidArgumentException(sprintf('Unrecognized access strategy type [%s]', $usage)); | ||
| 117 | } | ||
| 118 | |||
| 119 | public function buildQueryCache(EntityManagerInterface $em, string|null $regionName = null): QueryCache | ||
| 120 | { | ||
| 121 | return new DefaultQueryCache( | ||
| 122 | $em, | ||
| 123 | $this->getRegion( | ||
| 124 | [ | ||
| 125 | 'region' => $regionName ?: Cache::DEFAULT_QUERY_REGION_NAME, | ||
| 126 | 'usage' => ClassMetadata::CACHE_USAGE_NONSTRICT_READ_WRITE, | ||
| 127 | ], | ||
| 128 | ), | ||
| 129 | ); | ||
| 130 | } | ||
| 131 | |||
| 132 | public function buildCollectionHydrator(EntityManagerInterface $em, AssociationMapping $mapping): CollectionHydrator | ||
| 133 | { | ||
| 134 | return new DefaultCollectionHydrator($em); | ||
| 135 | } | ||
| 136 | |||
| 137 | public function buildEntityHydrator(EntityManagerInterface $em, ClassMetadata $metadata): EntityHydrator | ||
| 138 | { | ||
| 139 | return new DefaultEntityHydrator($em); | ||
| 140 | } | ||
| 141 | |||
| 142 | /** | ||
| 143 | * {@inheritDoc} | ||
| 144 | */ | ||
| 145 | public function getRegion(array $cache): Region | ||
| 146 | { | ||
| 147 | if (isset($this->regions[$cache['region']])) { | ||
| 148 | return $this->regions[$cache['region']]; | ||
| 149 | } | ||
| 150 | |||
| 151 | $name = $cache['region']; | ||
| 152 | $lifetime = $this->regionsConfig->getLifetime($cache['region']); | ||
| 153 | $region = new DefaultRegion($name, $this->cacheItemPool, $lifetime); | ||
| 154 | |||
| 155 | if ($cache['usage'] === ClassMetadata::CACHE_USAGE_READ_WRITE) { | ||
| 156 | if ( | ||
| 157 | $this->fileLockRegionDirectory === '' || | ||
| 158 | $this->fileLockRegionDirectory === null | ||
| 159 | ) { | ||
| 160 | throw new LogicException( | ||
| 161 | 'If you want to use a "READ_WRITE" cache an implementation of "Doctrine\ORM\Cache\ConcurrentRegion" is required, ' . | ||
| 162 | 'The default implementation provided by doctrine is "Doctrine\ORM\Cache\Region\FileLockRegion" if you want to use it please provide a valid directory, DefaultCacheFactory#setFileLockRegionDirectory(). ', | ||
| 163 | ); | ||
| 164 | } | ||
| 165 | |||
| 166 | $directory = $this->fileLockRegionDirectory . DIRECTORY_SEPARATOR . $cache['region']; | ||
| 167 | $region = new FileLockRegion($region, $directory, (string) $this->regionsConfig->getLockLifetime($cache['region'])); | ||
| 168 | } | ||
| 169 | |||
| 170 | return $this->regions[$cache['region']] = $region; | ||
| 171 | } | ||
| 172 | |||
| 173 | public function getTimestampRegion(): TimestampRegion | ||
| 174 | { | ||
| 175 | if ($this->timestampRegion === null) { | ||
| 176 | $name = Cache::DEFAULT_TIMESTAMP_REGION_NAME; | ||
| 177 | $lifetime = $this->regionsConfig->getLifetime($name); | ||
| 178 | |||
| 179 | $this->timestampRegion = new UpdateTimestampCache($name, $this->cacheItemPool, $lifetime); | ||
| 180 | } | ||
| 181 | |||
| 182 | return $this->timestampRegion; | ||
| 183 | } | ||
| 184 | |||
| 185 | public function createCache(EntityManagerInterface $entityManager): Cache | ||
| 186 | { | ||
| 187 | return new DefaultCache($entityManager); | ||
| 188 | } | ||
| 189 | } | ||
diff --git a/vendor/doctrine/orm/src/Cache/DefaultCollectionHydrator.php b/vendor/doctrine/orm/src/Cache/DefaultCollectionHydrator.php new file mode 100644 index 0000000..249d48f --- /dev/null +++ b/vendor/doctrine/orm/src/Cache/DefaultCollectionHydrator.php | |||
| @@ -0,0 +1,75 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Cache; | ||
| 6 | |||
| 7 | use Doctrine\Common\Collections\Collection; | ||
| 8 | use Doctrine\ORM\Cache\Persister\CachedPersister; | ||
| 9 | use Doctrine\ORM\EntityManagerInterface; | ||
| 10 | use Doctrine\ORM\Mapping\ClassMetadata; | ||
| 11 | use Doctrine\ORM\PersistentCollection; | ||
| 12 | use Doctrine\ORM\Query; | ||
| 13 | use Doctrine\ORM\UnitOfWork; | ||
| 14 | |||
| 15 | use function assert; | ||
| 16 | |||
| 17 | /** | ||
| 18 | * Default hydrator cache for collections | ||
| 19 | */ | ||
| 20 | class DefaultCollectionHydrator implements CollectionHydrator | ||
| 21 | { | ||
| 22 | private readonly UnitOfWork $uow; | ||
| 23 | |||
| 24 | /** @var array<string,mixed> */ | ||
| 25 | private static array $hints = [Query::HINT_CACHE_ENABLED => true]; | ||
| 26 | |||
| 27 | public function __construct( | ||
| 28 | private readonly EntityManagerInterface $em, | ||
| 29 | ) { | ||
| 30 | $this->uow = $em->getUnitOfWork(); | ||
| 31 | } | ||
| 32 | |||
| 33 | public function buildCacheEntry(ClassMetadata $metadata, CollectionCacheKey $key, array|Collection $collection): CollectionCacheEntry | ||
| 34 | { | ||
| 35 | $data = []; | ||
| 36 | |||
| 37 | foreach ($collection as $index => $entity) { | ||
| 38 | $data[$index] = new EntityCacheKey($metadata->rootEntityName, $this->uow->getEntityIdentifier($entity)); | ||
| 39 | } | ||
| 40 | |||
| 41 | return new CollectionCacheEntry($data); | ||
| 42 | } | ||
| 43 | |||
| 44 | public function loadCacheEntry(ClassMetadata $metadata, CollectionCacheKey $key, CollectionCacheEntry $entry, PersistentCollection $collection): array|null | ||
| 45 | { | ||
| 46 | $assoc = $metadata->associationMappings[$key->association]; | ||
| 47 | $targetPersister = $this->uow->getEntityPersister($assoc->targetEntity); | ||
| 48 | assert($targetPersister instanceof CachedPersister); | ||
| 49 | $targetRegion = $targetPersister->getCacheRegion(); | ||
| 50 | $list = []; | ||
| 51 | |||
| 52 | /** @var EntityCacheEntry[]|null $entityEntries */ | ||
| 53 | $entityEntries = $targetRegion->getMultiple($entry); | ||
| 54 | |||
| 55 | if ($entityEntries === null) { | ||
| 56 | return null; | ||
| 57 | } | ||
| 58 | |||
| 59 | foreach ($entityEntries as $index => $entityEntry) { | ||
| 60 | $entity = $this->uow->createEntity( | ||
| 61 | $entityEntry->class, | ||
| 62 | $entityEntry->resolveAssociationEntries($this->em), | ||
| 63 | self::$hints, | ||
| 64 | ); | ||
| 65 | |||
| 66 | $collection->hydrateSet($index, $entity); | ||
| 67 | |||
| 68 | $list[$index] = $entity; | ||
| 69 | } | ||
| 70 | |||
| 71 | $this->uow->hydrationComplete(); | ||
| 72 | |||
| 73 | return $list; | ||
| 74 | } | ||
| 75 | } | ||
diff --git a/vendor/doctrine/orm/src/Cache/DefaultEntityHydrator.php b/vendor/doctrine/orm/src/Cache/DefaultEntityHydrator.php new file mode 100644 index 0000000..6bd1524 --- /dev/null +++ b/vendor/doctrine/orm/src/Cache/DefaultEntityHydrator.php | |||
| @@ -0,0 +1,176 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Cache; | ||
| 6 | |||
| 7 | use Doctrine\ORM\EntityManagerInterface; | ||
| 8 | use Doctrine\ORM\Mapping\ClassMetadata; | ||
| 9 | use Doctrine\ORM\Proxy\DefaultProxyClassNameResolver; | ||
| 10 | use Doctrine\ORM\Query; | ||
| 11 | use Doctrine\ORM\UnitOfWork; | ||
| 12 | use Doctrine\ORM\Utility\IdentifierFlattener; | ||
| 13 | |||
| 14 | use function assert; | ||
| 15 | use function is_array; | ||
| 16 | use function is_object; | ||
| 17 | use function reset; | ||
| 18 | |||
| 19 | /** | ||
| 20 | * Default hydrator cache for entities | ||
| 21 | */ | ||
| 22 | class DefaultEntityHydrator implements EntityHydrator | ||
| 23 | { | ||
| 24 | private readonly UnitOfWork $uow; | ||
| 25 | private readonly IdentifierFlattener $identifierFlattener; | ||
| 26 | |||
| 27 | /** @var array<string,mixed> */ | ||
| 28 | private static array $hints = [Query::HINT_CACHE_ENABLED => true]; | ||
| 29 | |||
| 30 | public function __construct( | ||
| 31 | private readonly EntityManagerInterface $em, | ||
| 32 | ) { | ||
| 33 | $this->uow = $em->getUnitOfWork(); | ||
| 34 | $this->identifierFlattener = new IdentifierFlattener($em->getUnitOfWork(), $em->getMetadataFactory()); | ||
| 35 | } | ||
| 36 | |||
| 37 | public function buildCacheEntry(ClassMetadata $metadata, EntityCacheKey $key, object $entity): EntityCacheEntry | ||
| 38 | { | ||
| 39 | $data = $this->uow->getOriginalEntityData($entity); | ||
| 40 | $data = [...$data, ...$metadata->getIdentifierValues($entity)]; // why update has no identifier values ? | ||
| 41 | |||
| 42 | if ($metadata->requiresFetchAfterChange) { | ||
| 43 | if ($metadata->isVersioned) { | ||
| 44 | assert($metadata->versionField !== null); | ||
| 45 | $data[$metadata->versionField] = $metadata->getFieldValue($entity, $metadata->versionField); | ||
| 46 | } | ||
| 47 | |||
| 48 | foreach ($metadata->fieldMappings as $name => $fieldMapping) { | ||
| 49 | if (isset($fieldMapping->generated)) { | ||
| 50 | $data[$name] = $metadata->getFieldValue($entity, $name); | ||
| 51 | } | ||
| 52 | } | ||
| 53 | } | ||
| 54 | |||
| 55 | foreach ($metadata->associationMappings as $name => $assoc) { | ||
| 56 | if (! isset($data[$name])) { | ||
| 57 | continue; | ||
| 58 | } | ||
| 59 | |||
| 60 | if (! $assoc->isToOne()) { | ||
| 61 | unset($data[$name]); | ||
| 62 | |||
| 63 | continue; | ||
| 64 | } | ||
| 65 | |||
| 66 | if (! isset($assoc->cache)) { | ||
| 67 | $targetClassMetadata = $this->em->getClassMetadata($assoc->targetEntity); | ||
| 68 | $owningAssociation = $this->em->getMetadataFactory()->getOwningSide($assoc); | ||
| 69 | $associationIds = $this->identifierFlattener->flattenIdentifier( | ||
| 70 | $targetClassMetadata, | ||
| 71 | $targetClassMetadata->getIdentifierValues($data[$name]), | ||
| 72 | ); | ||
| 73 | |||
| 74 | unset($data[$name]); | ||
| 75 | |||
| 76 | foreach ($associationIds as $fieldName => $fieldValue) { | ||
| 77 | if (isset($targetClassMetadata->fieldMappings[$fieldName])) { | ||
| 78 | assert($owningAssociation->isToOneOwningSide()); | ||
| 79 | $fieldMapping = $targetClassMetadata->fieldMappings[$fieldName]; | ||
| 80 | |||
| 81 | $data[$owningAssociation->targetToSourceKeyColumns[$fieldMapping->columnName]] = $fieldValue; | ||
| 82 | |||
| 83 | continue; | ||
| 84 | } | ||
| 85 | |||
| 86 | $targetAssoc = $targetClassMetadata->associationMappings[$fieldName]; | ||
| 87 | |||
| 88 | assert($assoc->isToOneOwningSide()); | ||
| 89 | foreach ($assoc->targetToSourceKeyColumns as $referencedColumn => $localColumn) { | ||
| 90 | if (isset($targetAssoc->sourceToTargetKeyColumns[$referencedColumn])) { | ||
| 91 | $data[$localColumn] = $fieldValue; | ||
| 92 | } | ||
| 93 | } | ||
| 94 | } | ||
| 95 | |||
| 96 | continue; | ||
| 97 | } | ||
| 98 | |||
| 99 | if (! isset($assoc->id)) { | ||
| 100 | $targetClass = DefaultProxyClassNameResolver::getClass($data[$name]); | ||
| 101 | $targetId = $this->uow->getEntityIdentifier($data[$name]); | ||
| 102 | $data[$name] = new AssociationCacheEntry($targetClass, $targetId); | ||
| 103 | |||
| 104 | continue; | ||
| 105 | } | ||
| 106 | |||
| 107 | // handle association identifier | ||
| 108 | $targetId = is_object($data[$name]) && $this->uow->isInIdentityMap($data[$name]) | ||
| 109 | ? $this->uow->getEntityIdentifier($data[$name]) | ||
| 110 | : $data[$name]; | ||
| 111 | |||
| 112 | // @TODO - fix it ! | ||
| 113 | // handle UnitOfWork#createEntity hash generation | ||
| 114 | if (! is_array($targetId)) { | ||
| 115 | assert($assoc->isToOneOwningSide()); | ||
| 116 | $data[reset($assoc->joinColumnFieldNames)] = $targetId; | ||
| 117 | |||
| 118 | $targetEntity = $this->em->getClassMetadata($assoc->targetEntity); | ||
| 119 | $targetId = [$targetEntity->identifier[0] => $targetId]; | ||
| 120 | } | ||
| 121 | |||
| 122 | $data[$name] = new AssociationCacheEntry($assoc->targetEntity, $targetId); | ||
| 123 | } | ||
| 124 | |||
| 125 | return new EntityCacheEntry($metadata->name, $data); | ||
| 126 | } | ||
| 127 | |||
| 128 | public function loadCacheEntry(ClassMetadata $metadata, EntityCacheKey $key, EntityCacheEntry $entry, object|null $entity = null): object|null | ||
| 129 | { | ||
| 130 | $data = $entry->data; | ||
| 131 | $hints = self::$hints; | ||
| 132 | |||
| 133 | if ($entity !== null) { | ||
| 134 | $hints[Query::HINT_REFRESH] = true; | ||
| 135 | $hints[Query::HINT_REFRESH_ENTITY] = $entity; | ||
| 136 | } | ||
| 137 | |||
| 138 | foreach ($metadata->associationMappings as $name => $assoc) { | ||
| 139 | if (! isset($assoc->cache) || ! isset($data[$name])) { | ||
| 140 | continue; | ||
| 141 | } | ||
| 142 | |||
| 143 | $assocClass = $data[$name]->class; | ||
| 144 | $assocId = $data[$name]->identifier; | ||
| 145 | $isEagerLoad = ($assoc->fetch === ClassMetadata::FETCH_EAGER || ($assoc->isOneToOne() && ! $assoc->isOwningSide())); | ||
| 146 | |||
| 147 | if (! $isEagerLoad) { | ||
| 148 | $data[$name] = $this->em->getReference($assocClass, $assocId); | ||
| 149 | |||
| 150 | continue; | ||
| 151 | } | ||
| 152 | |||
| 153 | $assocMetadata = $this->em->getClassMetadata($assoc->targetEntity); | ||
| 154 | $assocKey = new EntityCacheKey($assocMetadata->rootEntityName, $assocId); | ||
| 155 | $assocPersister = $this->uow->getEntityPersister($assoc->targetEntity); | ||
| 156 | $assocRegion = $assocPersister->getCacheRegion(); | ||
| 157 | $assocEntry = $assocRegion->get($assocKey); | ||
| 158 | |||
| 159 | if ($assocEntry === null) { | ||
| 160 | return null; | ||
| 161 | } | ||
| 162 | |||
| 163 | $data[$name] = $this->uow->createEntity($assocEntry->class, $assocEntry->resolveAssociationEntries($this->em), $hints); | ||
| 164 | } | ||
| 165 | |||
| 166 | if ($entity !== null) { | ||
| 167 | $this->uow->registerManaged($entity, $key->identifier, $data); | ||
| 168 | } | ||
| 169 | |||
| 170 | $result = $this->uow->createEntity($entry->class, $data, $hints); | ||
| 171 | |||
| 172 | $this->uow->hydrationComplete(); | ||
| 173 | |||
| 174 | return $result; | ||
| 175 | } | ||
| 176 | } | ||
diff --git a/vendor/doctrine/orm/src/Cache/DefaultQueryCache.php b/vendor/doctrine/orm/src/Cache/DefaultQueryCache.php new file mode 100644 index 0000000..f3bb8ac --- /dev/null +++ b/vendor/doctrine/orm/src/Cache/DefaultQueryCache.php | |||
| @@ -0,0 +1,414 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Cache; | ||
| 6 | |||
| 7 | use Doctrine\Common\Collections\ArrayCollection; | ||
| 8 | use Doctrine\ORM\Cache; | ||
| 9 | use Doctrine\ORM\Cache\Exception\FeatureNotImplemented; | ||
| 10 | use Doctrine\ORM\Cache\Exception\NonCacheableEntity; | ||
| 11 | use Doctrine\ORM\Cache\Logging\CacheLogger; | ||
| 12 | use Doctrine\ORM\Cache\Persister\Entity\CachedEntityPersister; | ||
| 13 | use Doctrine\ORM\EntityManagerInterface; | ||
| 14 | use Doctrine\ORM\Mapping\AssociationMapping; | ||
| 15 | use Doctrine\ORM\Mapping\ClassMetadata; | ||
| 16 | use Doctrine\ORM\PersistentCollection; | ||
| 17 | use Doctrine\ORM\Query; | ||
| 18 | use Doctrine\ORM\Query\ResultSetMapping; | ||
| 19 | use Doctrine\ORM\UnitOfWork; | ||
| 20 | |||
| 21 | use function array_map; | ||
| 22 | use function array_shift; | ||
| 23 | use function array_unshift; | ||
| 24 | use function assert; | ||
| 25 | use function count; | ||
| 26 | use function is_array; | ||
| 27 | use function key; | ||
| 28 | use function reset; | ||
| 29 | |||
| 30 | /** | ||
| 31 | * Default query cache implementation. | ||
| 32 | */ | ||
| 33 | class DefaultQueryCache implements QueryCache | ||
| 34 | { | ||
| 35 | private readonly UnitOfWork $uow; | ||
| 36 | private readonly QueryCacheValidator $validator; | ||
| 37 | protected CacheLogger|null $cacheLogger = null; | ||
| 38 | |||
| 39 | /** @var array<string,mixed> */ | ||
| 40 | private static array $hints = [Query::HINT_CACHE_ENABLED => true]; | ||
| 41 | |||
| 42 | public function __construct( | ||
| 43 | private readonly EntityManagerInterface $em, | ||
| 44 | private readonly Region $region, | ||
| 45 | ) { | ||
| 46 | $cacheConfig = $em->getConfiguration()->getSecondLevelCacheConfiguration(); | ||
| 47 | |||
| 48 | $this->uow = $em->getUnitOfWork(); | ||
| 49 | $this->cacheLogger = $cacheConfig->getCacheLogger(); | ||
| 50 | $this->validator = $cacheConfig->getQueryValidator(); | ||
| 51 | } | ||
| 52 | |||
| 53 | /** | ||
| 54 | * {@inheritDoc} | ||
| 55 | */ | ||
| 56 | public function get(QueryCacheKey $key, ResultSetMapping $rsm, array $hints = []): array|null | ||
| 57 | { | ||
| 58 | if (! ($key->cacheMode & Cache::MODE_GET)) { | ||
| 59 | return null; | ||
| 60 | } | ||
| 61 | |||
| 62 | $cacheEntry = $this->region->get($key); | ||
| 63 | |||
| 64 | if (! $cacheEntry instanceof QueryCacheEntry) { | ||
| 65 | return null; | ||
| 66 | } | ||
| 67 | |||
| 68 | if (! $this->validator->isValid($key, $cacheEntry)) { | ||
| 69 | $this->region->evict($key); | ||
| 70 | |||
| 71 | return null; | ||
| 72 | } | ||
| 73 | |||
| 74 | $result = []; | ||
| 75 | $entityName = reset($rsm->aliasMap); | ||
| 76 | $hasRelation = ! empty($rsm->relationMap); | ||
| 77 | $persister = $this->uow->getEntityPersister($entityName); | ||
| 78 | assert($persister instanceof CachedEntityPersister); | ||
| 79 | |||
| 80 | $region = $persister->getCacheRegion(); | ||
| 81 | $regionName = $region->getName(); | ||
| 82 | |||
| 83 | $cm = $this->em->getClassMetadata($entityName); | ||
| 84 | |||
| 85 | $generateKeys = static fn (array $entry): EntityCacheKey => new EntityCacheKey($cm->rootEntityName, $entry['identifier']); | ||
| 86 | |||
| 87 | $cacheKeys = new CollectionCacheEntry(array_map($generateKeys, $cacheEntry->result)); | ||
| 88 | $entries = $region->getMultiple($cacheKeys) ?? []; | ||
| 89 | |||
| 90 | // @TODO - move to cache hydration component | ||
| 91 | foreach ($cacheEntry->result as $index => $entry) { | ||
| 92 | $entityEntry = $entries[$index] ?? null; | ||
| 93 | |||
| 94 | if (! $entityEntry instanceof EntityCacheEntry) { | ||
| 95 | $this->cacheLogger?->entityCacheMiss($regionName, $cacheKeys->identifiers[$index]); | ||
| 96 | |||
| 97 | return null; | ||
| 98 | } | ||
| 99 | |||
| 100 | $this->cacheLogger?->entityCacheHit($regionName, $cacheKeys->identifiers[$index]); | ||
| 101 | |||
| 102 | if (! $hasRelation) { | ||
| 103 | $result[$index] = $this->uow->createEntity($entityEntry->class, $entityEntry->resolveAssociationEntries($this->em), self::$hints); | ||
| 104 | |||
| 105 | continue; | ||
| 106 | } | ||
| 107 | |||
| 108 | $data = $entityEntry->data; | ||
| 109 | |||
| 110 | foreach ($entry['associations'] as $name => $assoc) { | ||
| 111 | $assocPersister = $this->uow->getEntityPersister($assoc['targetEntity']); | ||
| 112 | assert($assocPersister instanceof CachedEntityPersister); | ||
| 113 | |||
| 114 | $assocRegion = $assocPersister->getCacheRegion(); | ||
| 115 | $assocMetadata = $this->em->getClassMetadata($assoc['targetEntity']); | ||
| 116 | |||
| 117 | if ($assoc['type'] & ClassMetadata::TO_ONE) { | ||
| 118 | $assocKey = new EntityCacheKey($assocMetadata->rootEntityName, $assoc['identifier']); | ||
| 119 | $assocEntry = $assocRegion->get($assocKey); | ||
| 120 | |||
| 121 | if ($assocEntry === null) { | ||
| 122 | $this->cacheLogger?->entityCacheMiss($assocRegion->getName(), $assocKey); | ||
| 123 | |||
| 124 | $this->uow->hydrationComplete(); | ||
| 125 | |||
| 126 | return null; | ||
| 127 | } | ||
| 128 | |||
| 129 | $data[$name] = $this->uow->createEntity($assocEntry->class, $assocEntry->resolveAssociationEntries($this->em), self::$hints); | ||
| 130 | |||
| 131 | $this->cacheLogger?->entityCacheHit($assocRegion->getName(), $assocKey); | ||
| 132 | |||
| 133 | continue; | ||
| 134 | } | ||
| 135 | |||
| 136 | if (! isset($assoc['list']) || empty($assoc['list'])) { | ||
| 137 | continue; | ||
| 138 | } | ||
| 139 | |||
| 140 | $generateKeys = static fn (array $id): EntityCacheKey => new EntityCacheKey($assocMetadata->rootEntityName, $id); | ||
| 141 | |||
| 142 | $collection = new PersistentCollection($this->em, $assocMetadata, new ArrayCollection()); | ||
| 143 | $assocKeys = new CollectionCacheEntry(array_map($generateKeys, $assoc['list'])); | ||
| 144 | $assocEntries = $assocRegion->getMultiple($assocKeys); | ||
| 145 | |||
| 146 | foreach ($assoc['list'] as $assocIndex => $assocId) { | ||
| 147 | $assocEntry = is_array($assocEntries) ? ($assocEntries[$assocIndex] ?? null) : null; | ||
| 148 | |||
| 149 | if ($assocEntry === null) { | ||
| 150 | $this->cacheLogger?->entityCacheMiss($assocRegion->getName(), $assocKeys->identifiers[$assocIndex]); | ||
| 151 | |||
| 152 | $this->uow->hydrationComplete(); | ||
| 153 | |||
| 154 | return null; | ||
| 155 | } | ||
| 156 | |||
| 157 | $element = $this->uow->createEntity($assocEntry->class, $assocEntry->resolveAssociationEntries($this->em), self::$hints); | ||
| 158 | |||
| 159 | $collection->hydrateSet($assocIndex, $element); | ||
| 160 | |||
| 161 | $this->cacheLogger?->entityCacheHit($assocRegion->getName(), $assocKeys->identifiers[$assocIndex]); | ||
| 162 | } | ||
| 163 | |||
| 164 | $data[$name] = $collection; | ||
| 165 | |||
| 166 | $collection->setInitialized(true); | ||
| 167 | } | ||
| 168 | |||
| 169 | foreach ($data as $fieldName => $unCachedAssociationData) { | ||
| 170 | // In some scenarios, such as EAGER+ASSOCIATION+ID+CACHE, the | ||
| 171 | // cache key information in `$cacheEntry` will not contain details | ||
| 172 | // for fields that are associations. | ||
| 173 | // | ||
| 174 | // This means that `$data` keys for some associations that may | ||
| 175 | // actually not be cached will not be converted to actual association | ||
| 176 | // data, yet they contain L2 cache AssociationCacheEntry objects. | ||
| 177 | // | ||
| 178 | // We need to unwrap those associations into proxy references, | ||
| 179 | // since we don't have actual data for them except for identifiers. | ||
| 180 | if ($unCachedAssociationData instanceof AssociationCacheEntry) { | ||
| 181 | $data[$fieldName] = $this->em->getReference( | ||
| 182 | $unCachedAssociationData->class, | ||
| 183 | $unCachedAssociationData->identifier, | ||
| 184 | ); | ||
| 185 | } | ||
| 186 | } | ||
| 187 | |||
| 188 | $result[$index] = $this->uow->createEntity($entityEntry->class, $data, self::$hints); | ||
| 189 | } | ||
| 190 | |||
| 191 | $this->uow->hydrationComplete(); | ||
| 192 | |||
| 193 | return $result; | ||
| 194 | } | ||
| 195 | |||
| 196 | /** | ||
| 197 | * {@inheritDoc} | ||
| 198 | */ | ||
| 199 | public function put(QueryCacheKey $key, ResultSetMapping $rsm, mixed $result, array $hints = []): bool | ||
| 200 | { | ||
| 201 | if ($rsm->scalarMappings) { | ||
| 202 | throw FeatureNotImplemented::scalarResults(); | ||
| 203 | } | ||
| 204 | |||
| 205 | if (count($rsm->entityMappings) > 1) { | ||
| 206 | throw FeatureNotImplemented::multipleRootEntities(); | ||
| 207 | } | ||
| 208 | |||
| 209 | if (! $rsm->isSelect) { | ||
| 210 | throw FeatureNotImplemented::nonSelectStatements(); | ||
| 211 | } | ||
| 212 | |||
| 213 | if (! ($key->cacheMode & Cache::MODE_PUT)) { | ||
| 214 | return false; | ||
| 215 | } | ||
| 216 | |||
| 217 | $data = []; | ||
| 218 | $entityName = reset($rsm->aliasMap); | ||
| 219 | $rootAlias = key($rsm->aliasMap); | ||
| 220 | $persister = $this->uow->getEntityPersister($entityName); | ||
| 221 | |||
| 222 | if (! $persister instanceof CachedEntityPersister) { | ||
| 223 | throw NonCacheableEntity::fromEntity($entityName); | ||
| 224 | } | ||
| 225 | |||
| 226 | $region = $persister->getCacheRegion(); | ||
| 227 | |||
| 228 | $cm = $this->em->getClassMetadata($entityName); | ||
| 229 | assert($cm instanceof ClassMetadata); | ||
| 230 | |||
| 231 | foreach ($result as $index => $entity) { | ||
| 232 | $identifier = $this->uow->getEntityIdentifier($entity); | ||
| 233 | $entityKey = new EntityCacheKey($cm->rootEntityName, $identifier); | ||
| 234 | |||
| 235 | if (($key->cacheMode & Cache::MODE_REFRESH) || ! $region->contains($entityKey)) { | ||
| 236 | // Cancel put result if entity put fail | ||
| 237 | if (! $persister->storeEntityCache($entity, $entityKey)) { | ||
| 238 | return false; | ||
| 239 | } | ||
| 240 | } | ||
| 241 | |||
| 242 | $data[$index]['identifier'] = $identifier; | ||
| 243 | $data[$index]['associations'] = []; | ||
| 244 | |||
| 245 | // @TODO - move to cache hydration components | ||
| 246 | foreach ($rsm->relationMap as $alias => $name) { | ||
| 247 | $parentAlias = $rsm->parentAliasMap[$alias]; | ||
| 248 | $parentClass = $rsm->aliasMap[$parentAlias]; | ||
| 249 | $metadata = $this->em->getClassMetadata($parentClass); | ||
| 250 | $assoc = $metadata->associationMappings[$name]; | ||
| 251 | $assocValue = $this->getAssociationValue($rsm, $alias, $entity); | ||
| 252 | |||
| 253 | if ($assocValue === null) { | ||
| 254 | continue; | ||
| 255 | } | ||
| 256 | |||
| 257 | // root entity association | ||
| 258 | if ($rootAlias === $parentAlias) { | ||
| 259 | // Cancel put result if association put fail | ||
| 260 | $assocInfo = $this->storeAssociationCache($key, $assoc, $assocValue); | ||
| 261 | if ($assocInfo === null) { | ||
| 262 | return false; | ||
| 263 | } | ||
| 264 | |||
| 265 | $data[$index]['associations'][$name] = $assocInfo; | ||
| 266 | |||
| 267 | continue; | ||
| 268 | } | ||
| 269 | |||
| 270 | // store single nested association | ||
| 271 | if (! is_array($assocValue)) { | ||
| 272 | // Cancel put result if association put fail | ||
| 273 | if ($this->storeAssociationCache($key, $assoc, $assocValue) === null) { | ||
| 274 | return false; | ||
| 275 | } | ||
| 276 | |||
| 277 | continue; | ||
| 278 | } | ||
| 279 | |||
| 280 | // store array of nested association | ||
| 281 | foreach ($assocValue as $aVal) { | ||
| 282 | // Cancel put result if association put fail | ||
| 283 | if ($this->storeAssociationCache($key, $assoc, $aVal) === null) { | ||
| 284 | return false; | ||
| 285 | } | ||
| 286 | } | ||
| 287 | } | ||
| 288 | } | ||
| 289 | |||
| 290 | return $this->region->put($key, new QueryCacheEntry($data)); | ||
| 291 | } | ||
| 292 | |||
| 293 | /** | ||
| 294 | * @return mixed[]|null | ||
| 295 | * @psalm-return array{targetEntity: class-string, type: mixed, list?: array[], identifier?: array}|null | ||
| 296 | */ | ||
| 297 | private function storeAssociationCache(QueryCacheKey $key, AssociationMapping $assoc, mixed $assocValue): array|null | ||
| 298 | { | ||
| 299 | $assocPersister = $this->uow->getEntityPersister($assoc->targetEntity); | ||
| 300 | $assocMetadata = $assocPersister->getClassMetadata(); | ||
| 301 | $assocRegion = $assocPersister->getCacheRegion(); | ||
| 302 | |||
| 303 | // Handle *-to-one associations | ||
| 304 | if ($assoc->isToOne()) { | ||
| 305 | $assocIdentifier = $this->uow->getEntityIdentifier($assocValue); | ||
| 306 | $entityKey = new EntityCacheKey($assocMetadata->rootEntityName, $assocIdentifier); | ||
| 307 | |||
| 308 | if (! $this->uow->isUninitializedObject($assocValue) && ($key->cacheMode & Cache::MODE_REFRESH) || ! $assocRegion->contains($entityKey)) { | ||
| 309 | // Entity put fail | ||
| 310 | if (! $assocPersister->storeEntityCache($assocValue, $entityKey)) { | ||
| 311 | return null; | ||
| 312 | } | ||
| 313 | } | ||
| 314 | |||
| 315 | return [ | ||
| 316 | 'targetEntity' => $assocMetadata->rootEntityName, | ||
| 317 | 'identifier' => $assocIdentifier, | ||
| 318 | 'type' => $assoc->type(), | ||
| 319 | ]; | ||
| 320 | } | ||
| 321 | |||
| 322 | // Handle *-to-many associations | ||
| 323 | $list = []; | ||
| 324 | |||
| 325 | foreach ($assocValue as $assocItemIndex => $assocItem) { | ||
| 326 | $assocIdentifier = $this->uow->getEntityIdentifier($assocItem); | ||
| 327 | $entityKey = new EntityCacheKey($assocMetadata->rootEntityName, $assocIdentifier); | ||
| 328 | |||
| 329 | if (($key->cacheMode & Cache::MODE_REFRESH) || ! $assocRegion->contains($entityKey)) { | ||
| 330 | // Entity put fail | ||
| 331 | if (! $assocPersister->storeEntityCache($assocItem, $entityKey)) { | ||
| 332 | return null; | ||
| 333 | } | ||
| 334 | } | ||
| 335 | |||
| 336 | $list[$assocItemIndex] = $assocIdentifier; | ||
| 337 | } | ||
| 338 | |||
| 339 | return [ | ||
| 340 | 'targetEntity' => $assocMetadata->rootEntityName, | ||
| 341 | 'type' => $assoc->type(), | ||
| 342 | 'list' => $list, | ||
| 343 | ]; | ||
| 344 | } | ||
| 345 | |||
| 346 | /** @psalm-return list<mixed>|object|null */ | ||
| 347 | private function getAssociationValue( | ||
| 348 | ResultSetMapping $rsm, | ||
| 349 | string $assocAlias, | ||
| 350 | object $entity, | ||
| 351 | ): array|object|null { | ||
| 352 | $path = []; | ||
| 353 | $alias = $assocAlias; | ||
| 354 | |||
| 355 | while (isset($rsm->parentAliasMap[$alias])) { | ||
| 356 | $parent = $rsm->parentAliasMap[$alias]; | ||
| 357 | $field = $rsm->relationMap[$alias]; | ||
| 358 | $class = $rsm->aliasMap[$parent]; | ||
| 359 | |||
| 360 | array_unshift($path, [ | ||
| 361 | 'field' => $field, | ||
| 362 | 'class' => $class, | ||
| 363 | ]); | ||
| 364 | |||
| 365 | $alias = $parent; | ||
| 366 | } | ||
| 367 | |||
| 368 | return $this->getAssociationPathValue($entity, $path); | ||
| 369 | } | ||
| 370 | |||
| 371 | /** | ||
| 372 | * @psalm-param array<array-key, array{field: string, class: string}> $path | ||
| 373 | * | ||
| 374 | * @psalm-return list<mixed>|object|null | ||
| 375 | */ | ||
| 376 | private function getAssociationPathValue(mixed $value, array $path): array|object|null | ||
| 377 | { | ||
| 378 | $mapping = array_shift($path); | ||
| 379 | $metadata = $this->em->getClassMetadata($mapping['class']); | ||
| 380 | $assoc = $metadata->associationMappings[$mapping['field']]; | ||
| 381 | $value = $metadata->getFieldValue($value, $mapping['field']); | ||
| 382 | |||
| 383 | if ($value === null) { | ||
| 384 | return null; | ||
| 385 | } | ||
| 386 | |||
| 387 | if ($path === []) { | ||
| 388 | return $value; | ||
| 389 | } | ||
| 390 | |||
| 391 | // Handle *-to-one associations | ||
| 392 | if ($assoc->isToOne()) { | ||
| 393 | return $this->getAssociationPathValue($value, $path); | ||
| 394 | } | ||
| 395 | |||
| 396 | $values = []; | ||
| 397 | |||
| 398 | foreach ($value as $item) { | ||
| 399 | $values[] = $this->getAssociationPathValue($item, $path); | ||
| 400 | } | ||
| 401 | |||
| 402 | return $values; | ||
| 403 | } | ||
| 404 | |||
| 405 | public function clear(): bool | ||
| 406 | { | ||
| 407 | return $this->region->evictAll(); | ||
| 408 | } | ||
| 409 | |||
| 410 | public function getRegion(): Region | ||
| 411 | { | ||
| 412 | return $this->region; | ||
| 413 | } | ||
| 414 | } | ||
diff --git a/vendor/doctrine/orm/src/Cache/EntityCacheEntry.php b/vendor/doctrine/orm/src/Cache/EntityCacheEntry.php new file mode 100644 index 0000000..69bc813 --- /dev/null +++ b/vendor/doctrine/orm/src/Cache/EntityCacheEntry.php | |||
| @@ -0,0 +1,50 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Cache; | ||
| 6 | |||
| 7 | use Doctrine\ORM\EntityManagerInterface; | ||
| 8 | |||
| 9 | use function array_map; | ||
| 10 | |||
| 11 | class EntityCacheEntry implements CacheEntry | ||
| 12 | { | ||
| 13 | /** | ||
| 14 | * @param array<string,mixed> $data The entity map data | ||
| 15 | * @psalm-param class-string $class The entity class name | ||
| 16 | */ | ||
| 17 | public function __construct( | ||
| 18 | public readonly string $class, | ||
| 19 | public readonly array $data, | ||
| 20 | ) { | ||
| 21 | } | ||
| 22 | |||
| 23 | /** | ||
| 24 | * Creates a new EntityCacheEntry | ||
| 25 | * | ||
| 26 | * This method allows Doctrine\Common\Cache\PhpFileCache compatibility | ||
| 27 | * | ||
| 28 | * @param array<string,mixed> $values array containing property values | ||
| 29 | */ | ||
| 30 | public static function __set_state(array $values): self | ||
| 31 | { | ||
| 32 | return new self($values['class'], $values['data']); | ||
| 33 | } | ||
| 34 | |||
| 35 | /** | ||
| 36 | * Retrieves the entity data resolving cache entries | ||
| 37 | * | ||
| 38 | * @return array<string, mixed> | ||
| 39 | */ | ||
| 40 | public function resolveAssociationEntries(EntityManagerInterface $em): array | ||
| 41 | { | ||
| 42 | return array_map(static function ($value) use ($em) { | ||
| 43 | if (! ($value instanceof AssociationCacheEntry)) { | ||
| 44 | return $value; | ||
| 45 | } | ||
| 46 | |||
| 47 | return $em->getReference($value->class, $value->identifier); | ||
| 48 | }, $this->data); | ||
| 49 | } | ||
| 50 | } | ||
diff --git a/vendor/doctrine/orm/src/Cache/EntityCacheKey.php b/vendor/doctrine/orm/src/Cache/EntityCacheKey.php new file mode 100644 index 0000000..095ddaa --- /dev/null +++ b/vendor/doctrine/orm/src/Cache/EntityCacheKey.php | |||
| @@ -0,0 +1,38 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Cache; | ||
| 6 | |||
| 7 | use function implode; | ||
| 8 | use function ksort; | ||
| 9 | use function str_replace; | ||
| 10 | use function strtolower; | ||
| 11 | |||
| 12 | /** | ||
| 13 | * Defines entity classes roles to be stored in the cache region. | ||
| 14 | */ | ||
| 15 | class EntityCacheKey extends CacheKey | ||
| 16 | { | ||
| 17 | /** | ||
| 18 | * The entity identifier | ||
| 19 | * | ||
| 20 | * @var array<string, mixed> | ||
| 21 | */ | ||
| 22 | public readonly array $identifier; | ||
| 23 | |||
| 24 | /** | ||
| 25 | * @param class-string $entityClass The entity class name. In a inheritance hierarchy it should always be the root entity class. | ||
| 26 | * @param array<string, mixed> $identifier The entity identifier | ||
| 27 | */ | ||
| 28 | public function __construct( | ||
| 29 | public readonly string $entityClass, | ||
| 30 | array $identifier, | ||
| 31 | ) { | ||
| 32 | ksort($identifier); | ||
| 33 | |||
| 34 | $this->identifier = $identifier; | ||
| 35 | |||
| 36 | parent::__construct(str_replace('\\', '.', strtolower($entityClass) . '_' . implode(' ', $identifier))); | ||
| 37 | } | ||
| 38 | } | ||
diff --git a/vendor/doctrine/orm/src/Cache/EntityHydrator.php b/vendor/doctrine/orm/src/Cache/EntityHydrator.php new file mode 100644 index 0000000..13cd21f --- /dev/null +++ b/vendor/doctrine/orm/src/Cache/EntityHydrator.php | |||
| @@ -0,0 +1,28 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Cache; | ||
| 6 | |||
| 7 | use Doctrine\ORM\Mapping\ClassMetadata; | ||
| 8 | |||
| 9 | /** | ||
| 10 | * Hydrator cache entry for entities | ||
| 11 | */ | ||
| 12 | interface EntityHydrator | ||
| 13 | { | ||
| 14 | /** | ||
| 15 | * @param ClassMetadata $metadata The entity metadata. | ||
| 16 | * @param EntityCacheKey $key The entity cache key. | ||
| 17 | * @param object $entity The entity. | ||
| 18 | */ | ||
| 19 | public function buildCacheEntry(ClassMetadata $metadata, EntityCacheKey $key, object $entity): EntityCacheEntry; | ||
| 20 | |||
| 21 | /** | ||
| 22 | * @param ClassMetadata $metadata The entity metadata. | ||
| 23 | * @param EntityCacheKey $key The entity cache key. | ||
| 24 | * @param EntityCacheEntry $entry The entity cache entry. | ||
| 25 | * @param object|null $entity The entity to load the cache into. If not specified, a new entity is created. | ||
| 26 | */ | ||
| 27 | public function loadCacheEntry(ClassMetadata $metadata, EntityCacheKey $key, EntityCacheEntry $entry, object|null $entity = null): object|null; | ||
| 28 | } | ||
diff --git a/vendor/doctrine/orm/src/Cache/Exception/CacheException.php b/vendor/doctrine/orm/src/Cache/Exception/CacheException.php new file mode 100644 index 0000000..cae1bde --- /dev/null +++ b/vendor/doctrine/orm/src/Cache/Exception/CacheException.php | |||
| @@ -0,0 +1,14 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Cache\Exception; | ||
| 6 | |||
| 7 | use Doctrine\ORM\Cache\CacheException as BaseCacheException; | ||
| 8 | |||
| 9 | /** | ||
| 10 | * Exception for cache. | ||
| 11 | */ | ||
| 12 | class CacheException extends BaseCacheException | ||
| 13 | { | ||
| 14 | } | ||
diff --git a/vendor/doctrine/orm/src/Cache/Exception/CannotUpdateReadOnlyCollection.php b/vendor/doctrine/orm/src/Cache/Exception/CannotUpdateReadOnlyCollection.php new file mode 100644 index 0000000..7ecb4fe --- /dev/null +++ b/vendor/doctrine/orm/src/Cache/Exception/CannotUpdateReadOnlyCollection.php | |||
| @@ -0,0 +1,19 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Cache\Exception; | ||
| 6 | |||
| 7 | use function sprintf; | ||
| 8 | |||
| 9 | class CannotUpdateReadOnlyCollection extends CacheException | ||
| 10 | { | ||
| 11 | public static function fromEntityAndField(string $sourceEntity, string $fieldName): self | ||
| 12 | { | ||
| 13 | return new self(sprintf( | ||
| 14 | 'Cannot update a readonly collection "%s#%s"', | ||
| 15 | $sourceEntity, | ||
| 16 | $fieldName, | ||
| 17 | )); | ||
| 18 | } | ||
| 19 | } | ||
diff --git a/vendor/doctrine/orm/src/Cache/Exception/CannotUpdateReadOnlyEntity.php b/vendor/doctrine/orm/src/Cache/Exception/CannotUpdateReadOnlyEntity.php new file mode 100644 index 0000000..b945514 --- /dev/null +++ b/vendor/doctrine/orm/src/Cache/Exception/CannotUpdateReadOnlyEntity.php | |||
| @@ -0,0 +1,15 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Cache\Exception; | ||
| 6 | |||
| 7 | use function sprintf; | ||
| 8 | |||
| 9 | class CannotUpdateReadOnlyEntity extends CacheException | ||
| 10 | { | ||
| 11 | public static function fromEntity(string $entityName): self | ||
| 12 | { | ||
| 13 | return new self(sprintf('Cannot update a readonly entity "%s"', $entityName)); | ||
| 14 | } | ||
| 15 | } | ||
diff --git a/vendor/doctrine/orm/src/Cache/Exception/FeatureNotImplemented.php b/vendor/doctrine/orm/src/Cache/Exception/FeatureNotImplemented.php new file mode 100644 index 0000000..8767d57 --- /dev/null +++ b/vendor/doctrine/orm/src/Cache/Exception/FeatureNotImplemented.php | |||
| @@ -0,0 +1,23 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Cache\Exception; | ||
| 6 | |||
| 7 | class FeatureNotImplemented extends CacheException | ||
| 8 | { | ||
| 9 | public static function scalarResults(): self | ||
| 10 | { | ||
| 11 | return new self('Second level cache does not support scalar results.'); | ||
| 12 | } | ||
| 13 | |||
| 14 | public static function multipleRootEntities(): self | ||
| 15 | { | ||
| 16 | return new self('Second level cache does not support multiple root entities.'); | ||
| 17 | } | ||
| 18 | |||
| 19 | public static function nonSelectStatements(): self | ||
| 20 | { | ||
| 21 | return new self('Second-level cache query supports only select statements.'); | ||
| 22 | } | ||
| 23 | } | ||
diff --git a/vendor/doctrine/orm/src/Cache/Exception/NonCacheableEntity.php b/vendor/doctrine/orm/src/Cache/Exception/NonCacheableEntity.php new file mode 100644 index 0000000..4f5da85 --- /dev/null +++ b/vendor/doctrine/orm/src/Cache/Exception/NonCacheableEntity.php | |||
| @@ -0,0 +1,18 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Cache\Exception; | ||
| 6 | |||
| 7 | use function sprintf; | ||
| 8 | |||
| 9 | class NonCacheableEntity extends CacheException | ||
| 10 | { | ||
| 11 | public static function fromEntity(string $entityName): self | ||
| 12 | { | ||
| 13 | return new self(sprintf( | ||
| 14 | 'Entity "%s" not configured as part of the second-level cache.', | ||
| 15 | $entityName, | ||
| 16 | )); | ||
| 17 | } | ||
| 18 | } | ||
diff --git a/vendor/doctrine/orm/src/Cache/Exception/NonCacheableEntityAssociation.php b/vendor/doctrine/orm/src/Cache/Exception/NonCacheableEntityAssociation.php new file mode 100644 index 0000000..984286f --- /dev/null +++ b/vendor/doctrine/orm/src/Cache/Exception/NonCacheableEntityAssociation.php | |||
| @@ -0,0 +1,19 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Cache\Exception; | ||
| 6 | |||
| 7 | use function sprintf; | ||
| 8 | |||
| 9 | class NonCacheableEntityAssociation extends CacheException | ||
| 10 | { | ||
| 11 | public static function fromEntityAndField(string $entityName, string $field): self | ||
| 12 | { | ||
| 13 | return new self(sprintf( | ||
| 14 | 'Entity association field "%s#%s" not configured as part of the second-level cache.', | ||
| 15 | $entityName, | ||
| 16 | $field, | ||
| 17 | )); | ||
| 18 | } | ||
| 19 | } | ||
diff --git a/vendor/doctrine/orm/src/Cache/Lock.php b/vendor/doctrine/orm/src/Cache/Lock.php new file mode 100644 index 0000000..d6d60a3 --- /dev/null +++ b/vendor/doctrine/orm/src/Cache/Lock.php | |||
| @@ -0,0 +1,25 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Cache; | ||
| 6 | |||
| 7 | use function time; | ||
| 8 | use function uniqid; | ||
| 9 | |||
| 10 | class Lock | ||
| 11 | { | ||
| 12 | public int $time; | ||
| 13 | |||
| 14 | public function __construct( | ||
| 15 | public string $value, | ||
| 16 | int|null $time = null, | ||
| 17 | ) { | ||
| 18 | $this->time = $time ?? time(); | ||
| 19 | } | ||
| 20 | |||
| 21 | public static function createLockRead(): Lock | ||
| 22 | { | ||
| 23 | return new self(uniqid((string) time(), true)); | ||
| 24 | } | ||
| 25 | } | ||
diff --git a/vendor/doctrine/orm/src/Cache/LockException.php b/vendor/doctrine/orm/src/Cache/LockException.php new file mode 100644 index 0000000..bb2d4ec --- /dev/null +++ b/vendor/doctrine/orm/src/Cache/LockException.php | |||
| @@ -0,0 +1,14 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Cache; | ||
| 6 | |||
| 7 | use Doctrine\ORM\Cache\Exception\CacheException; | ||
| 8 | |||
| 9 | /** | ||
| 10 | * Lock exception for cache. | ||
| 11 | */ | ||
| 12 | class LockException extends CacheException | ||
| 13 | { | ||
| 14 | } | ||
diff --git a/vendor/doctrine/orm/src/Cache/Logging/CacheLogger.php b/vendor/doctrine/orm/src/Cache/Logging/CacheLogger.php new file mode 100644 index 0000000..64c97c1 --- /dev/null +++ b/vendor/doctrine/orm/src/Cache/Logging/CacheLogger.php | |||
| @@ -0,0 +1,60 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Cache\Logging; | ||
| 6 | |||
| 7 | use Doctrine\ORM\Cache\CollectionCacheKey; | ||
| 8 | use Doctrine\ORM\Cache\EntityCacheKey; | ||
| 9 | use Doctrine\ORM\Cache\QueryCacheKey; | ||
| 10 | |||
| 11 | /** | ||
| 12 | * Interface for logging. | ||
| 13 | */ | ||
| 14 | interface CacheLogger | ||
| 15 | { | ||
| 16 | /** | ||
| 17 | * Log an entity put into second level cache. | ||
| 18 | */ | ||
| 19 | public function entityCachePut(string $regionName, EntityCacheKey $key): void; | ||
| 20 | |||
| 21 | /** | ||
| 22 | * Log an entity get from second level cache resulted in a hit. | ||
| 23 | */ | ||
| 24 | public function entityCacheHit(string $regionName, EntityCacheKey $key): void; | ||
| 25 | |||
| 26 | /** | ||
| 27 | * Log an entity get from second level cache resulted in a miss. | ||
| 28 | */ | ||
| 29 | public function entityCacheMiss(string $regionName, EntityCacheKey $key): void; | ||
| 30 | |||
| 31 | /** | ||
| 32 | * Log an entity put into second level cache. | ||
| 33 | */ | ||
| 34 | public function collectionCachePut(string $regionName, CollectionCacheKey $key): void; | ||
| 35 | |||
| 36 | /** | ||
| 37 | * Log an entity get from second level cache resulted in a hit. | ||
| 38 | */ | ||
| 39 | public function collectionCacheHit(string $regionName, CollectionCacheKey $key): void; | ||
| 40 | |||
| 41 | /** | ||
| 42 | * Log an entity get from second level cache resulted in a miss. | ||
| 43 | */ | ||
| 44 | public function collectionCacheMiss(string $regionName, CollectionCacheKey $key): void; | ||
| 45 | |||
| 46 | /** | ||
| 47 | * Log a query put into the query cache. | ||
| 48 | */ | ||
| 49 | public function queryCachePut(string $regionName, QueryCacheKey $key): void; | ||
| 50 | |||
| 51 | /** | ||
| 52 | * Log a query get from the query cache resulted in a hit. | ||
| 53 | */ | ||
| 54 | public function queryCacheHit(string $regionName, QueryCacheKey $key): void; | ||
| 55 | |||
| 56 | /** | ||
| 57 | * Log a query get from the query cache resulted in a miss. | ||
| 58 | */ | ||
| 59 | public function queryCacheMiss(string $regionName, QueryCacheKey $key): void; | ||
| 60 | } | ||
diff --git a/vendor/doctrine/orm/src/Cache/Logging/CacheLoggerChain.php b/vendor/doctrine/orm/src/Cache/Logging/CacheLoggerChain.php new file mode 100644 index 0000000..8eef3b5 --- /dev/null +++ b/vendor/doctrine/orm/src/Cache/Logging/CacheLoggerChain.php | |||
| @@ -0,0 +1,94 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Cache\Logging; | ||
| 6 | |||
| 7 | use Doctrine\ORM\Cache\CollectionCacheKey; | ||
| 8 | use Doctrine\ORM\Cache\EntityCacheKey; | ||
| 9 | use Doctrine\ORM\Cache\QueryCacheKey; | ||
| 10 | |||
| 11 | class CacheLoggerChain implements CacheLogger | ||
| 12 | { | ||
| 13 | /** @var array<string, CacheLogger> */ | ||
| 14 | private array $loggers = []; | ||
| 15 | |||
| 16 | public function setLogger(string $name, CacheLogger $logger): void | ||
| 17 | { | ||
| 18 | $this->loggers[$name] = $logger; | ||
| 19 | } | ||
| 20 | |||
| 21 | public function getLogger(string $name): CacheLogger|null | ||
| 22 | { | ||
| 23 | return $this->loggers[$name] ?? null; | ||
| 24 | } | ||
| 25 | |||
| 26 | /** @return array<string, CacheLogger> */ | ||
| 27 | public function getLoggers(): array | ||
| 28 | { | ||
| 29 | return $this->loggers; | ||
| 30 | } | ||
| 31 | |||
| 32 | public function collectionCacheHit(string $regionName, CollectionCacheKey $key): void | ||
| 33 | { | ||
| 34 | foreach ($this->loggers as $logger) { | ||
| 35 | $logger->collectionCacheHit($regionName, $key); | ||
| 36 | } | ||
| 37 | } | ||
| 38 | |||
| 39 | public function collectionCacheMiss(string $regionName, CollectionCacheKey $key): void | ||
| 40 | { | ||
| 41 | foreach ($this->loggers as $logger) { | ||
| 42 | $logger->collectionCacheMiss($regionName, $key); | ||
| 43 | } | ||
| 44 | } | ||
| 45 | |||
| 46 | public function collectionCachePut(string $regionName, CollectionCacheKey $key): void | ||
| 47 | { | ||
| 48 | foreach ($this->loggers as $logger) { | ||
| 49 | $logger->collectionCachePut($regionName, $key); | ||
| 50 | } | ||
| 51 | } | ||
| 52 | |||
| 53 | public function entityCacheHit(string $regionName, EntityCacheKey $key): void | ||
| 54 | { | ||
| 55 | foreach ($this->loggers as $logger) { | ||
| 56 | $logger->entityCacheHit($regionName, $key); | ||
| 57 | } | ||
| 58 | } | ||
| 59 | |||
| 60 | public function entityCacheMiss(string $regionName, EntityCacheKey $key): void | ||
| 61 | { | ||
| 62 | foreach ($this->loggers as $logger) { | ||
| 63 | $logger->entityCacheMiss($regionName, $key); | ||
| 64 | } | ||
| 65 | } | ||
| 66 | |||
| 67 | public function entityCachePut(string $regionName, EntityCacheKey $key): void | ||
| 68 | { | ||
| 69 | foreach ($this->loggers as $logger) { | ||
| 70 | $logger->entityCachePut($regionName, $key); | ||
| 71 | } | ||
| 72 | } | ||
| 73 | |||
| 74 | public function queryCacheHit(string $regionName, QueryCacheKey $key): void | ||
| 75 | { | ||
| 76 | foreach ($this->loggers as $logger) { | ||
| 77 | $logger->queryCacheHit($regionName, $key); | ||
| 78 | } | ||
| 79 | } | ||
| 80 | |||
| 81 | public function queryCacheMiss(string $regionName, QueryCacheKey $key): void | ||
| 82 | { | ||
| 83 | foreach ($this->loggers as $logger) { | ||
| 84 | $logger->queryCacheMiss($regionName, $key); | ||
| 85 | } | ||
| 86 | } | ||
| 87 | |||
| 88 | public function queryCachePut(string $regionName, QueryCacheKey $key): void | ||
| 89 | { | ||
| 90 | foreach ($this->loggers as $logger) { | ||
| 91 | $logger->queryCachePut($regionName, $key); | ||
| 92 | } | ||
| 93 | } | ||
| 94 | } | ||
diff --git a/vendor/doctrine/orm/src/Cache/Logging/StatisticsCacheLogger.php b/vendor/doctrine/orm/src/Cache/Logging/StatisticsCacheLogger.php new file mode 100644 index 0000000..092104e --- /dev/null +++ b/vendor/doctrine/orm/src/Cache/Logging/StatisticsCacheLogger.php | |||
| @@ -0,0 +1,174 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Cache\Logging; | ||
| 6 | |||
| 7 | use Doctrine\ORM\Cache\CollectionCacheKey; | ||
| 8 | use Doctrine\ORM\Cache\EntityCacheKey; | ||
| 9 | use Doctrine\ORM\Cache\QueryCacheKey; | ||
| 10 | |||
| 11 | use function array_sum; | ||
| 12 | |||
| 13 | /** | ||
| 14 | * Provide basic second level cache statistics. | ||
| 15 | */ | ||
| 16 | class StatisticsCacheLogger implements CacheLogger | ||
| 17 | { | ||
| 18 | /** @var array<string, int> */ | ||
| 19 | private array $cacheMissCountMap = []; | ||
| 20 | |||
| 21 | /** @var array<string, int> */ | ||
| 22 | private array $cacheHitCountMap = []; | ||
| 23 | |||
| 24 | /** @var array<string, int> */ | ||
| 25 | private array $cachePutCountMap = []; | ||
| 26 | |||
| 27 | public function collectionCacheMiss(string $regionName, CollectionCacheKey $key): void | ||
| 28 | { | ||
| 29 | $this->cacheMissCountMap[$regionName] | ||
| 30 | = ($this->cacheMissCountMap[$regionName] ?? 0) + 1; | ||
| 31 | } | ||
| 32 | |||
| 33 | public function collectionCacheHit(string $regionName, CollectionCacheKey $key): void | ||
| 34 | { | ||
| 35 | $this->cacheHitCountMap[$regionName] | ||
| 36 | = ($this->cacheHitCountMap[$regionName] ?? 0) + 1; | ||
| 37 | } | ||
| 38 | |||
| 39 | public function collectionCachePut(string $regionName, CollectionCacheKey $key): void | ||
| 40 | { | ||
| 41 | $this->cachePutCountMap[$regionName] | ||
| 42 | = ($this->cachePutCountMap[$regionName] ?? 0) + 1; | ||
| 43 | } | ||
| 44 | |||
| 45 | public function entityCacheMiss(string $regionName, EntityCacheKey $key): void | ||
| 46 | { | ||
| 47 | $this->cacheMissCountMap[$regionName] | ||
| 48 | = ($this->cacheMissCountMap[$regionName] ?? 0) + 1; | ||
| 49 | } | ||
| 50 | |||
| 51 | public function entityCacheHit(string $regionName, EntityCacheKey $key): void | ||
| 52 | { | ||
| 53 | $this->cacheHitCountMap[$regionName] | ||
| 54 | = ($this->cacheHitCountMap[$regionName] ?? 0) + 1; | ||
| 55 | } | ||
| 56 | |||
| 57 | public function entityCachePut(string $regionName, EntityCacheKey $key): void | ||
| 58 | { | ||
| 59 | $this->cachePutCountMap[$regionName] | ||
| 60 | = ($this->cachePutCountMap[$regionName] ?? 0) + 1; | ||
| 61 | } | ||
| 62 | |||
| 63 | public function queryCacheHit(string $regionName, QueryCacheKey $key): void | ||
| 64 | { | ||
| 65 | $this->cacheHitCountMap[$regionName] | ||
| 66 | = ($this->cacheHitCountMap[$regionName] ?? 0) + 1; | ||
| 67 | } | ||
| 68 | |||
| 69 | public function queryCacheMiss(string $regionName, QueryCacheKey $key): void | ||
| 70 | { | ||
| 71 | $this->cacheMissCountMap[$regionName] | ||
| 72 | = ($this->cacheMissCountMap[$regionName] ?? 0) + 1; | ||
| 73 | } | ||
| 74 | |||
| 75 | public function queryCachePut(string $regionName, QueryCacheKey $key): void | ||
| 76 | { | ||
| 77 | $this->cachePutCountMap[$regionName] | ||
| 78 | = ($this->cachePutCountMap[$regionName] ?? 0) + 1; | ||
| 79 | } | ||
| 80 | |||
| 81 | /** | ||
| 82 | * Get the number of entries successfully retrieved from cache. | ||
| 83 | * | ||
| 84 | * @param string $regionName The name of the cache region. | ||
| 85 | */ | ||
| 86 | public function getRegionHitCount(string $regionName): int | ||
| 87 | { | ||
| 88 | return $this->cacheHitCountMap[$regionName] ?? 0; | ||
| 89 | } | ||
| 90 | |||
| 91 | /** | ||
| 92 | * Get the number of cached entries *not* found in cache. | ||
| 93 | * | ||
| 94 | * @param string $regionName The name of the cache region. | ||
| 95 | */ | ||
| 96 | public function getRegionMissCount(string $regionName): int | ||
| 97 | { | ||
| 98 | return $this->cacheMissCountMap[$regionName] ?? 0; | ||
| 99 | } | ||
| 100 | |||
| 101 | /** | ||
| 102 | * Get the number of cacheable entries put in cache. | ||
| 103 | * | ||
| 104 | * @param string $regionName The name of the cache region. | ||
| 105 | */ | ||
| 106 | public function getRegionPutCount(string $regionName): int | ||
| 107 | { | ||
| 108 | return $this->cachePutCountMap[$regionName] ?? 0; | ||
| 109 | } | ||
| 110 | |||
| 111 | /** @return array<string, int> */ | ||
| 112 | public function getRegionsMiss(): array | ||
| 113 | { | ||
| 114 | return $this->cacheMissCountMap; | ||
| 115 | } | ||
| 116 | |||
| 117 | /** @return array<string, int> */ | ||
| 118 | public function getRegionsHit(): array | ||
| 119 | { | ||
| 120 | return $this->cacheHitCountMap; | ||
| 121 | } | ||
| 122 | |||
| 123 | /** @return array<string, int> */ | ||
| 124 | public function getRegionsPut(): array | ||
| 125 | { | ||
| 126 | return $this->cachePutCountMap; | ||
| 127 | } | ||
| 128 | |||
| 129 | /** | ||
| 130 | * Clear region statistics | ||
| 131 | * | ||
| 132 | * @param string $regionName The name of the cache region. | ||
| 133 | */ | ||
| 134 | public function clearRegionStats(string $regionName): void | ||
| 135 | { | ||
| 136 | $this->cachePutCountMap[$regionName] = 0; | ||
| 137 | $this->cacheHitCountMap[$regionName] = 0; | ||
| 138 | $this->cacheMissCountMap[$regionName] = 0; | ||
| 139 | } | ||
| 140 | |||
| 141 | /** | ||
| 142 | * Clear all statistics | ||
| 143 | */ | ||
| 144 | public function clearStats(): void | ||
| 145 | { | ||
| 146 | $this->cachePutCountMap = []; | ||
| 147 | $this->cacheHitCountMap = []; | ||
| 148 | $this->cacheMissCountMap = []; | ||
| 149 | } | ||
| 150 | |||
| 151 | /** | ||
| 152 | * Get the total number of put in cache. | ||
| 153 | */ | ||
| 154 | public function getPutCount(): int | ||
| 155 | { | ||
| 156 | return array_sum($this->cachePutCountMap); | ||
| 157 | } | ||
| 158 | |||
| 159 | /** | ||
| 160 | * Get the total number of entries successfully retrieved from cache. | ||
| 161 | */ | ||
| 162 | public function getHitCount(): int | ||
| 163 | { | ||
| 164 | return array_sum($this->cacheHitCountMap); | ||
| 165 | } | ||
| 166 | |||
| 167 | /** | ||
| 168 | * Get the total number of cached entries *not* found in cache. | ||
| 169 | */ | ||
| 170 | public function getMissCount(): int | ||
| 171 | { | ||
| 172 | return array_sum($this->cacheMissCountMap); | ||
| 173 | } | ||
| 174 | } | ||
diff --git a/vendor/doctrine/orm/src/Cache/Persister/CachedPersister.php b/vendor/doctrine/orm/src/Cache/Persister/CachedPersister.php new file mode 100644 index 0000000..223692c --- /dev/null +++ b/vendor/doctrine/orm/src/Cache/Persister/CachedPersister.php | |||
| @@ -0,0 +1,25 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Cache\Persister; | ||
| 6 | |||
| 7 | use Doctrine\ORM\Cache\Region; | ||
| 8 | |||
| 9 | /** | ||
| 10 | * Interface for persister that support second level cache. | ||
| 11 | */ | ||
| 12 | interface CachedPersister | ||
| 13 | { | ||
| 14 | /** | ||
| 15 | * Perform whatever processing is encapsulated here after completion of the transaction. | ||
| 16 | */ | ||
| 17 | public function afterTransactionComplete(): void; | ||
| 18 | |||
| 19 | /** | ||
| 20 | * Perform whatever processing is encapsulated here after completion of the rolled-back. | ||
| 21 | */ | ||
| 22 | public function afterTransactionRolledBack(): void; | ||
| 23 | |||
| 24 | public function getCacheRegion(): Region; | ||
| 25 | } | ||
diff --git a/vendor/doctrine/orm/src/Cache/Persister/Collection/AbstractCollectionPersister.php b/vendor/doctrine/orm/src/Cache/Persister/Collection/AbstractCollectionPersister.php new file mode 100644 index 0000000..8c087a8 --- /dev/null +++ b/vendor/doctrine/orm/src/Cache/Persister/Collection/AbstractCollectionPersister.php | |||
| @@ -0,0 +1,168 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Cache\Persister\Collection; | ||
| 6 | |||
| 7 | use Doctrine\Common\Collections\Collection; | ||
| 8 | use Doctrine\Common\Collections\Criteria; | ||
| 9 | use Doctrine\ORM\Cache\CollectionCacheKey; | ||
| 10 | use Doctrine\ORM\Cache\CollectionHydrator; | ||
| 11 | use Doctrine\ORM\Cache\Logging\CacheLogger; | ||
| 12 | use Doctrine\ORM\Cache\Persister\Entity\CachedEntityPersister; | ||
| 13 | use Doctrine\ORM\Cache\Region; | ||
| 14 | use Doctrine\ORM\EntityManagerInterface; | ||
| 15 | use Doctrine\ORM\Mapping\AssociationMapping; | ||
| 16 | use Doctrine\ORM\Mapping\ClassMetadata; | ||
| 17 | use Doctrine\ORM\Mapping\ClassMetadataFactory; | ||
| 18 | use Doctrine\ORM\PersistentCollection; | ||
| 19 | use Doctrine\ORM\Persisters\Collection\CollectionPersister; | ||
| 20 | use Doctrine\ORM\Proxy\DefaultProxyClassNameResolver; | ||
| 21 | use Doctrine\ORM\UnitOfWork; | ||
| 22 | |||
| 23 | use function array_values; | ||
| 24 | use function assert; | ||
| 25 | use function count; | ||
| 26 | |||
| 27 | abstract class AbstractCollectionPersister implements CachedCollectionPersister | ||
| 28 | { | ||
| 29 | protected UnitOfWork $uow; | ||
| 30 | protected ClassMetadataFactory $metadataFactory; | ||
| 31 | protected ClassMetadata $sourceEntity; | ||
| 32 | protected ClassMetadata $targetEntity; | ||
| 33 | |||
| 34 | /** @var mixed[] */ | ||
| 35 | protected array $queuedCache = []; | ||
| 36 | |||
| 37 | protected string $regionName; | ||
| 38 | protected CollectionHydrator $hydrator; | ||
| 39 | protected CacheLogger|null $cacheLogger; | ||
| 40 | |||
| 41 | public function __construct( | ||
| 42 | protected CollectionPersister $persister, | ||
| 43 | protected Region $region, | ||
| 44 | EntityManagerInterface $em, | ||
| 45 | protected AssociationMapping $association, | ||
| 46 | ) { | ||
| 47 | $configuration = $em->getConfiguration(); | ||
| 48 | $cacheConfig = $configuration->getSecondLevelCacheConfiguration(); | ||
| 49 | $cacheFactory = $cacheConfig->getCacheFactory(); | ||
| 50 | |||
| 51 | $this->regionName = $region->getName(); | ||
| 52 | $this->uow = $em->getUnitOfWork(); | ||
| 53 | $this->metadataFactory = $em->getMetadataFactory(); | ||
| 54 | $this->cacheLogger = $cacheConfig->getCacheLogger(); | ||
| 55 | $this->hydrator = $cacheFactory->buildCollectionHydrator($em, $association); | ||
| 56 | $this->sourceEntity = $em->getClassMetadata($association->sourceEntity); | ||
| 57 | $this->targetEntity = $em->getClassMetadata($association->targetEntity); | ||
| 58 | } | ||
| 59 | |||
| 60 | public function getCacheRegion(): Region | ||
| 61 | { | ||
| 62 | return $this->region; | ||
| 63 | } | ||
| 64 | |||
| 65 | public function getSourceEntityMetadata(): ClassMetadata | ||
| 66 | { | ||
| 67 | return $this->sourceEntity; | ||
| 68 | } | ||
| 69 | |||
| 70 | public function getTargetEntityMetadata(): ClassMetadata | ||
| 71 | { | ||
| 72 | return $this->targetEntity; | ||
| 73 | } | ||
| 74 | |||
| 75 | public function loadCollectionCache(PersistentCollection $collection, CollectionCacheKey $key): array|null | ||
| 76 | { | ||
| 77 | $cache = $this->region->get($key); | ||
| 78 | |||
| 79 | if ($cache === null) { | ||
| 80 | return null; | ||
| 81 | } | ||
| 82 | |||
| 83 | return $this->hydrator->loadCacheEntry($this->sourceEntity, $key, $cache, $collection); | ||
| 84 | } | ||
| 85 | |||
| 86 | public function storeCollectionCache(CollectionCacheKey $key, Collection|array $elements): void | ||
| 87 | { | ||
| 88 | $associationMapping = $this->sourceEntity->associationMappings[$key->association]; | ||
| 89 | $targetPersister = $this->uow->getEntityPersister($this->targetEntity->rootEntityName); | ||
| 90 | assert($targetPersister instanceof CachedEntityPersister); | ||
| 91 | $targetRegion = $targetPersister->getCacheRegion(); | ||
| 92 | $targetHydrator = $targetPersister->getEntityHydrator(); | ||
| 93 | |||
| 94 | // Only preserve ordering if association configured it | ||
| 95 | if (! $associationMapping->isIndexed()) { | ||
| 96 | // Elements may be an array or a Collection | ||
| 97 | $elements = array_values($elements instanceof Collection ? $elements->getValues() : $elements); | ||
| 98 | } | ||
| 99 | |||
| 100 | $entry = $this->hydrator->buildCacheEntry($this->targetEntity, $key, $elements); | ||
| 101 | |||
| 102 | foreach ($entry->identifiers as $index => $entityKey) { | ||
| 103 | if ($targetRegion->contains($entityKey)) { | ||
| 104 | continue; | ||
| 105 | } | ||
| 106 | |||
| 107 | $class = $this->targetEntity; | ||
| 108 | $className = DefaultProxyClassNameResolver::getClass($elements[$index]); | ||
| 109 | |||
| 110 | if ($className !== $this->targetEntity->name) { | ||
| 111 | $class = $this->metadataFactory->getMetadataFor($className); | ||
| 112 | } | ||
| 113 | |||
| 114 | $entity = $elements[$index]; | ||
| 115 | $entityEntry = $targetHydrator->buildCacheEntry($class, $entityKey, $entity); | ||
| 116 | |||
| 117 | $targetRegion->put($entityKey, $entityEntry); | ||
| 118 | } | ||
| 119 | |||
| 120 | if ($this->region->put($key, $entry)) { | ||
| 121 | $this->cacheLogger?->collectionCachePut($this->regionName, $key); | ||
| 122 | } | ||
| 123 | } | ||
| 124 | |||
| 125 | public function contains(PersistentCollection $collection, object $element): bool | ||
| 126 | { | ||
| 127 | return $this->persister->contains($collection, $element); | ||
| 128 | } | ||
| 129 | |||
| 130 | public function containsKey(PersistentCollection $collection, mixed $key): bool | ||
| 131 | { | ||
| 132 | return $this->persister->containsKey($collection, $key); | ||
| 133 | } | ||
| 134 | |||
| 135 | public function count(PersistentCollection $collection): int | ||
| 136 | { | ||
| 137 | $ownerId = $this->uow->getEntityIdentifier($collection->getOwner()); | ||
| 138 | $key = new CollectionCacheKey($this->sourceEntity->rootEntityName, $this->association->fieldName, $ownerId); | ||
| 139 | $entry = $this->region->get($key); | ||
| 140 | |||
| 141 | if ($entry !== null) { | ||
| 142 | return count($entry->identifiers); | ||
| 143 | } | ||
| 144 | |||
| 145 | return $this->persister->count($collection); | ||
| 146 | } | ||
| 147 | |||
| 148 | public function get(PersistentCollection $collection, mixed $index): mixed | ||
| 149 | { | ||
| 150 | return $this->persister->get($collection, $index); | ||
| 151 | } | ||
| 152 | |||
| 153 | /** | ||
| 154 | * {@inheritDoc} | ||
| 155 | */ | ||
| 156 | public function slice(PersistentCollection $collection, int $offset, int|null $length = null): array | ||
| 157 | { | ||
| 158 | return $this->persister->slice($collection, $offset, $length); | ||
| 159 | } | ||
| 160 | |||
| 161 | /** | ||
| 162 | * {@inheritDoc} | ||
| 163 | */ | ||
| 164 | public function loadCriteria(PersistentCollection $collection, Criteria $criteria): array | ||
| 165 | { | ||
| 166 | return $this->persister->loadCriteria($collection, $criteria); | ||
| 167 | } | ||
| 168 | } | ||
diff --git a/vendor/doctrine/orm/src/Cache/Persister/Collection/CachedCollectionPersister.php b/vendor/doctrine/orm/src/Cache/Persister/Collection/CachedCollectionPersister.php new file mode 100644 index 0000000..6b10c80 --- /dev/null +++ b/vendor/doctrine/orm/src/Cache/Persister/Collection/CachedCollectionPersister.php | |||
| @@ -0,0 +1,36 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Cache\Persister\Collection; | ||
| 6 | |||
| 7 | use Doctrine\Common\Collections\Collection; | ||
| 8 | use Doctrine\ORM\Cache\CollectionCacheKey; | ||
| 9 | use Doctrine\ORM\Cache\Persister\CachedPersister; | ||
| 10 | use Doctrine\ORM\Mapping\ClassMetadata; | ||
| 11 | use Doctrine\ORM\PersistentCollection; | ||
| 12 | use Doctrine\ORM\Persisters\Collection\CollectionPersister; | ||
| 13 | |||
| 14 | /** | ||
| 15 | * Interface for second level cache collection persisters. | ||
| 16 | */ | ||
| 17 | interface CachedCollectionPersister extends CachedPersister, CollectionPersister | ||
| 18 | { | ||
| 19 | public function getSourceEntityMetadata(): ClassMetadata; | ||
| 20 | |||
| 21 | public function getTargetEntityMetadata(): ClassMetadata; | ||
| 22 | |||
| 23 | /** | ||
| 24 | * Loads a collection from cache | ||
| 25 | * | ||
| 26 | * @return mixed[]|null | ||
| 27 | */ | ||
| 28 | public function loadCollectionCache(PersistentCollection $collection, CollectionCacheKey $key): array|null; | ||
| 29 | |||
| 30 | /** | ||
| 31 | * Stores a collection into cache | ||
| 32 | * | ||
| 33 | * @param mixed[]|Collection $elements | ||
| 34 | */ | ||
| 35 | public function storeCollectionCache(CollectionCacheKey $key, Collection|array $elements): void; | ||
| 36 | } | ||
diff --git a/vendor/doctrine/orm/src/Cache/Persister/Collection/NonStrictReadWriteCachedCollectionPersister.php b/vendor/doctrine/orm/src/Cache/Persister/Collection/NonStrictReadWriteCachedCollectionPersister.php new file mode 100644 index 0000000..ac861f4 --- /dev/null +++ b/vendor/doctrine/orm/src/Cache/Persister/Collection/NonStrictReadWriteCachedCollectionPersister.php | |||
| @@ -0,0 +1,74 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Cache\Persister\Collection; | ||
| 6 | |||
| 7 | use Doctrine\ORM\Cache\CollectionCacheKey; | ||
| 8 | use Doctrine\ORM\PersistentCollection; | ||
| 9 | |||
| 10 | use function spl_object_id; | ||
| 11 | |||
| 12 | class NonStrictReadWriteCachedCollectionPersister extends AbstractCollectionPersister | ||
| 13 | { | ||
| 14 | public function afterTransactionComplete(): void | ||
| 15 | { | ||
| 16 | if (isset($this->queuedCache['update'])) { | ||
| 17 | foreach ($this->queuedCache['update'] as $item) { | ||
| 18 | $this->storeCollectionCache($item['key'], $item['list']); | ||
| 19 | } | ||
| 20 | } | ||
| 21 | |||
| 22 | if (isset($this->queuedCache['delete'])) { | ||
| 23 | foreach ($this->queuedCache['delete'] as $key) { | ||
| 24 | $this->region->evict($key); | ||
| 25 | } | ||
| 26 | } | ||
| 27 | |||
| 28 | $this->queuedCache = []; | ||
| 29 | } | ||
| 30 | |||
| 31 | public function afterTransactionRolledBack(): void | ||
| 32 | { | ||
| 33 | $this->queuedCache = []; | ||
| 34 | } | ||
| 35 | |||
| 36 | public function delete(PersistentCollection $collection): void | ||
| 37 | { | ||
| 38 | $ownerId = $this->uow->getEntityIdentifier($collection->getOwner()); | ||
| 39 | $key = new CollectionCacheKey($this->sourceEntity->rootEntityName, $this->association->fieldName, $ownerId); | ||
| 40 | |||
| 41 | $this->persister->delete($collection); | ||
| 42 | |||
| 43 | $this->queuedCache['delete'][spl_object_id($collection)] = $key; | ||
| 44 | } | ||
| 45 | |||
| 46 | public function update(PersistentCollection $collection): void | ||
| 47 | { | ||
| 48 | $isInitialized = $collection->isInitialized(); | ||
| 49 | $isDirty = $collection->isDirty(); | ||
| 50 | |||
| 51 | if (! $isInitialized && ! $isDirty) { | ||
| 52 | return; | ||
| 53 | } | ||
| 54 | |||
| 55 | $ownerId = $this->uow->getEntityIdentifier($collection->getOwner()); | ||
| 56 | $key = new CollectionCacheKey($this->sourceEntity->rootEntityName, $this->association->fieldName, $ownerId); | ||
| 57 | |||
| 58 | // Invalidate non initialized collections OR ordered collection | ||
| 59 | if ($isDirty && ! $isInitialized || $this->association->isOrdered()) { | ||
| 60 | $this->persister->update($collection); | ||
| 61 | |||
| 62 | $this->queuedCache['delete'][spl_object_id($collection)] = $key; | ||
| 63 | |||
| 64 | return; | ||
| 65 | } | ||
| 66 | |||
| 67 | $this->persister->update($collection); | ||
| 68 | |||
| 69 | $this->queuedCache['update'][spl_object_id($collection)] = [ | ||
| 70 | 'key' => $key, | ||
| 71 | 'list' => $collection, | ||
| 72 | ]; | ||
| 73 | } | ||
| 74 | } | ||
diff --git a/vendor/doctrine/orm/src/Cache/Persister/Collection/ReadOnlyCachedCollectionPersister.php b/vendor/doctrine/orm/src/Cache/Persister/Collection/ReadOnlyCachedCollectionPersister.php new file mode 100644 index 0000000..96e0a4b --- /dev/null +++ b/vendor/doctrine/orm/src/Cache/Persister/Collection/ReadOnlyCachedCollectionPersister.php | |||
| @@ -0,0 +1,24 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Cache\Persister\Collection; | ||
| 6 | |||
| 7 | use Doctrine\ORM\Cache\Exception\CannotUpdateReadOnlyCollection; | ||
| 8 | use Doctrine\ORM\PersistentCollection; | ||
| 9 | use Doctrine\ORM\Proxy\DefaultProxyClassNameResolver; | ||
| 10 | |||
| 11 | class ReadOnlyCachedCollectionPersister extends NonStrictReadWriteCachedCollectionPersister | ||
| 12 | { | ||
| 13 | public function update(PersistentCollection $collection): void | ||
| 14 | { | ||
| 15 | if ($collection->isDirty() && $collection->getSnapshot()) { | ||
| 16 | throw CannotUpdateReadOnlyCollection::fromEntityAndField( | ||
| 17 | DefaultProxyClassNameResolver::getClass($collection->getOwner()), | ||
| 18 | $this->association->fieldName, | ||
| 19 | ); | ||
| 20 | } | ||
| 21 | |||
| 22 | parent::update($collection); | ||
| 23 | } | ||
| 24 | } | ||
diff --git a/vendor/doctrine/orm/src/Cache/Persister/Collection/ReadWriteCachedCollectionPersister.php b/vendor/doctrine/orm/src/Cache/Persister/Collection/ReadWriteCachedCollectionPersister.php new file mode 100644 index 0000000..347a065 --- /dev/null +++ b/vendor/doctrine/orm/src/Cache/Persister/Collection/ReadWriteCachedCollectionPersister.php | |||
| @@ -0,0 +1,103 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Cache\Persister\Collection; | ||
| 6 | |||
| 7 | use Doctrine\ORM\Cache\CollectionCacheKey; | ||
| 8 | use Doctrine\ORM\Cache\ConcurrentRegion; | ||
| 9 | use Doctrine\ORM\EntityManagerInterface; | ||
| 10 | use Doctrine\ORM\Mapping\AssociationMapping; | ||
| 11 | use Doctrine\ORM\PersistentCollection; | ||
| 12 | use Doctrine\ORM\Persisters\Collection\CollectionPersister; | ||
| 13 | |||
| 14 | use function spl_object_id; | ||
| 15 | |||
| 16 | class ReadWriteCachedCollectionPersister extends AbstractCollectionPersister | ||
| 17 | { | ||
| 18 | public function __construct( | ||
| 19 | CollectionPersister $persister, | ||
| 20 | ConcurrentRegion $region, | ||
| 21 | EntityManagerInterface $em, | ||
| 22 | AssociationMapping $association, | ||
| 23 | ) { | ||
| 24 | parent::__construct($persister, $region, $em, $association); | ||
| 25 | } | ||
| 26 | |||
| 27 | public function afterTransactionComplete(): void | ||
| 28 | { | ||
| 29 | if (isset($this->queuedCache['update'])) { | ||
| 30 | foreach ($this->queuedCache['update'] as $item) { | ||
| 31 | $this->region->evict($item['key']); | ||
| 32 | } | ||
| 33 | } | ||
| 34 | |||
| 35 | if (isset($this->queuedCache['delete'])) { | ||
| 36 | foreach ($this->queuedCache['delete'] as $item) { | ||
| 37 | $this->region->evict($item['key']); | ||
| 38 | } | ||
| 39 | } | ||
| 40 | |||
| 41 | $this->queuedCache = []; | ||
| 42 | } | ||
| 43 | |||
| 44 | public function afterTransactionRolledBack(): void | ||
| 45 | { | ||
| 46 | if (isset($this->queuedCache['update'])) { | ||
| 47 | foreach ($this->queuedCache['update'] as $item) { | ||
| 48 | $this->region->evict($item['key']); | ||
| 49 | } | ||
| 50 | } | ||
| 51 | |||
| 52 | if (isset($this->queuedCache['delete'])) { | ||
| 53 | foreach ($this->queuedCache['delete'] as $item) { | ||
| 54 | $this->region->evict($item['key']); | ||
| 55 | } | ||
| 56 | } | ||
| 57 | |||
| 58 | $this->queuedCache = []; | ||
| 59 | } | ||
| 60 | |||
| 61 | public function delete(PersistentCollection $collection): void | ||
| 62 | { | ||
| 63 | $ownerId = $this->uow->getEntityIdentifier($collection->getOwner()); | ||
| 64 | $key = new CollectionCacheKey($this->sourceEntity->rootEntityName, $this->association->fieldName, $ownerId); | ||
| 65 | $lock = $this->region->lock($key); | ||
| 66 | |||
| 67 | $this->persister->delete($collection); | ||
| 68 | |||
| 69 | if ($lock === null) { | ||
| 70 | return; | ||
| 71 | } | ||
| 72 | |||
| 73 | $this->queuedCache['delete'][spl_object_id($collection)] = [ | ||
| 74 | 'key' => $key, | ||
| 75 | 'lock' => $lock, | ||
| 76 | ]; | ||
| 77 | } | ||
| 78 | |||
| 79 | public function update(PersistentCollection $collection): void | ||
| 80 | { | ||
| 81 | $isInitialized = $collection->isInitialized(); | ||
| 82 | $isDirty = $collection->isDirty(); | ||
| 83 | |||
| 84 | if (! $isInitialized && ! $isDirty) { | ||
| 85 | return; | ||
| 86 | } | ||
| 87 | |||
| 88 | $this->persister->update($collection); | ||
| 89 | |||
| 90 | $ownerId = $this->uow->getEntityIdentifier($collection->getOwner()); | ||
| 91 | $key = new CollectionCacheKey($this->sourceEntity->rootEntityName, $this->association->fieldName, $ownerId); | ||
| 92 | $lock = $this->region->lock($key); | ||
| 93 | |||
| 94 | if ($lock === null) { | ||
| 95 | return; | ||
| 96 | } | ||
| 97 | |||
| 98 | $this->queuedCache['update'][spl_object_id($collection)] = [ | ||
| 99 | 'key' => $key, | ||
| 100 | 'lock' => $lock, | ||
| 101 | ]; | ||
| 102 | } | ||
| 103 | } | ||
diff --git a/vendor/doctrine/orm/src/Cache/Persister/Entity/AbstractEntityPersister.php b/vendor/doctrine/orm/src/Cache/Persister/Entity/AbstractEntityPersister.php new file mode 100644 index 0000000..9f371d8 --- /dev/null +++ b/vendor/doctrine/orm/src/Cache/Persister/Entity/AbstractEntityPersister.php | |||
| @@ -0,0 +1,557 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Cache\Persister\Entity; | ||
| 6 | |||
| 7 | use Doctrine\Common\Collections\Criteria; | ||
| 8 | use Doctrine\Common\Collections\Order; | ||
| 9 | use Doctrine\DBAL\LockMode; | ||
| 10 | use Doctrine\ORM\Cache; | ||
| 11 | use Doctrine\ORM\Cache\CollectionCacheKey; | ||
| 12 | use Doctrine\ORM\Cache\EntityCacheKey; | ||
| 13 | use Doctrine\ORM\Cache\EntityHydrator; | ||
| 14 | use Doctrine\ORM\Cache\Logging\CacheLogger; | ||
| 15 | use Doctrine\ORM\Cache\Persister\CachedPersister; | ||
| 16 | use Doctrine\ORM\Cache\QueryCacheKey; | ||
| 17 | use Doctrine\ORM\Cache\Region; | ||
| 18 | use Doctrine\ORM\Cache\TimestampCacheKey; | ||
| 19 | use Doctrine\ORM\Cache\TimestampRegion; | ||
| 20 | use Doctrine\ORM\EntityManagerInterface; | ||
| 21 | use Doctrine\ORM\Mapping\AssociationMapping; | ||
| 22 | use Doctrine\ORM\Mapping\ClassMetadata; | ||
| 23 | use Doctrine\ORM\Mapping\ClassMetadataFactory; | ||
| 24 | use Doctrine\ORM\PersistentCollection; | ||
| 25 | use Doctrine\ORM\Persisters\Entity\EntityPersister; | ||
| 26 | use Doctrine\ORM\Proxy\DefaultProxyClassNameResolver; | ||
| 27 | use Doctrine\ORM\Query\ResultSetMapping; | ||
| 28 | use Doctrine\ORM\UnitOfWork; | ||
| 29 | |||
| 30 | use function array_merge; | ||
| 31 | use function assert; | ||
| 32 | use function serialize; | ||
| 33 | use function sha1; | ||
| 34 | |||
| 35 | abstract class AbstractEntityPersister implements CachedEntityPersister | ||
| 36 | { | ||
| 37 | protected UnitOfWork $uow; | ||
| 38 | protected ClassMetadataFactory $metadataFactory; | ||
| 39 | |||
| 40 | /** @var mixed[] */ | ||
| 41 | protected array $queuedCache = []; | ||
| 42 | |||
| 43 | protected TimestampRegion $timestampRegion; | ||
| 44 | protected TimestampCacheKey $timestampKey; | ||
| 45 | protected EntityHydrator $hydrator; | ||
| 46 | protected Cache $cache; | ||
| 47 | protected CacheLogger|null $cacheLogger = null; | ||
| 48 | protected string $regionName; | ||
| 49 | |||
| 50 | /** | ||
| 51 | * Associations configured as FETCH_EAGER, as well as all inverse one-to-one associations. | ||
| 52 | * | ||
| 53 | * @var array<string>|null | ||
| 54 | */ | ||
| 55 | protected array|null $joinedAssociations = null; | ||
| 56 | |||
| 57 | public function __construct( | ||
| 58 | protected EntityPersister $persister, | ||
| 59 | protected Region $region, | ||
| 60 | EntityManagerInterface $em, | ||
| 61 | protected ClassMetadata $class, | ||
| 62 | ) { | ||
| 63 | $configuration = $em->getConfiguration(); | ||
| 64 | $cacheConfig = $configuration->getSecondLevelCacheConfiguration(); | ||
| 65 | $cacheFactory = $cacheConfig->getCacheFactory(); | ||
| 66 | |||
| 67 | $this->cache = $em->getCache(); | ||
| 68 | $this->regionName = $region->getName(); | ||
| 69 | $this->uow = $em->getUnitOfWork(); | ||
| 70 | $this->metadataFactory = $em->getMetadataFactory(); | ||
| 71 | $this->cacheLogger = $cacheConfig->getCacheLogger(); | ||
| 72 | $this->timestampRegion = $cacheFactory->getTimestampRegion(); | ||
| 73 | $this->hydrator = $cacheFactory->buildEntityHydrator($em, $class); | ||
| 74 | $this->timestampKey = new TimestampCacheKey($this->class->rootEntityName); | ||
| 75 | } | ||
| 76 | |||
| 77 | public function addInsert(object $entity): void | ||
| 78 | { | ||
| 79 | $this->persister->addInsert($entity); | ||
| 80 | } | ||
| 81 | |||
| 82 | /** | ||
| 83 | * {@inheritDoc} | ||
| 84 | */ | ||
| 85 | public function getInserts(): array | ||
| 86 | { | ||
| 87 | return $this->persister->getInserts(); | ||
| 88 | } | ||
| 89 | |||
| 90 | public function getSelectSQL( | ||
| 91 | array|Criteria $criteria, | ||
| 92 | AssociationMapping|null $assoc = null, | ||
| 93 | LockMode|int|null $lockMode = null, | ||
| 94 | int|null $limit = null, | ||
| 95 | int|null $offset = null, | ||
| 96 | array|null $orderBy = null, | ||
| 97 | ): string { | ||
| 98 | return $this->persister->getSelectSQL($criteria, $assoc, $lockMode, $limit, $offset, $orderBy); | ||
| 99 | } | ||
| 100 | |||
| 101 | public function getCountSQL(array|Criteria $criteria = []): string | ||
| 102 | { | ||
| 103 | return $this->persister->getCountSQL($criteria); | ||
| 104 | } | ||
| 105 | |||
| 106 | public function getInsertSQL(): string | ||
| 107 | { | ||
| 108 | return $this->persister->getInsertSQL(); | ||
| 109 | } | ||
| 110 | |||
| 111 | public function getResultSetMapping(): ResultSetMapping | ||
| 112 | { | ||
| 113 | return $this->persister->getResultSetMapping(); | ||
| 114 | } | ||
| 115 | |||
| 116 | public function getSelectConditionStatementSQL( | ||
| 117 | string $field, | ||
| 118 | mixed $value, | ||
| 119 | AssociationMapping|null $assoc = null, | ||
| 120 | string|null $comparison = null, | ||
| 121 | ): string { | ||
| 122 | return $this->persister->getSelectConditionStatementSQL($field, $value, $assoc, $comparison); | ||
| 123 | } | ||
| 124 | |||
| 125 | public function exists(object $entity, Criteria|null $extraConditions = null): bool | ||
| 126 | { | ||
| 127 | if ($extraConditions === null) { | ||
| 128 | $key = new EntityCacheKey($this->class->rootEntityName, $this->class->getIdentifierValues($entity)); | ||
| 129 | |||
| 130 | if ($this->region->contains($key)) { | ||
| 131 | return true; | ||
| 132 | } | ||
| 133 | } | ||
| 134 | |||
| 135 | return $this->persister->exists($entity, $extraConditions); | ||
| 136 | } | ||
| 137 | |||
| 138 | public function getCacheRegion(): Region | ||
| 139 | { | ||
| 140 | return $this->region; | ||
| 141 | } | ||
| 142 | |||
| 143 | public function getEntityHydrator(): EntityHydrator | ||
| 144 | { | ||
| 145 | return $this->hydrator; | ||
| 146 | } | ||
| 147 | |||
| 148 | public function storeEntityCache(object $entity, EntityCacheKey $key): bool | ||
| 149 | { | ||
| 150 | $class = $this->class; | ||
| 151 | $className = DefaultProxyClassNameResolver::getClass($entity); | ||
| 152 | |||
| 153 | if ($className !== $this->class->name) { | ||
| 154 | $class = $this->metadataFactory->getMetadataFor($className); | ||
| 155 | } | ||
| 156 | |||
| 157 | $entry = $this->hydrator->buildCacheEntry($class, $key, $entity); | ||
| 158 | $cached = $this->region->put($key, $entry); | ||
| 159 | |||
| 160 | if ($cached) { | ||
| 161 | $this->cacheLogger?->entityCachePut($this->regionName, $key); | ||
| 162 | } | ||
| 163 | |||
| 164 | return $cached; | ||
| 165 | } | ||
| 166 | |||
| 167 | private function storeJoinedAssociations(object $entity): void | ||
| 168 | { | ||
| 169 | if ($this->joinedAssociations === null) { | ||
| 170 | $associations = []; | ||
| 171 | |||
| 172 | foreach ($this->class->associationMappings as $name => $assoc) { | ||
| 173 | if ( | ||
| 174 | isset($assoc->cache) && | ||
| 175 | ($assoc->isToOne()) && | ||
| 176 | ($assoc->fetch === ClassMetadata::FETCH_EAGER || ! $assoc->isOwningSide()) | ||
| 177 | ) { | ||
| 178 | $associations[] = $name; | ||
| 179 | } | ||
| 180 | } | ||
| 181 | |||
| 182 | $this->joinedAssociations = $associations; | ||
| 183 | } | ||
| 184 | |||
| 185 | foreach ($this->joinedAssociations as $name) { | ||
| 186 | $assoc = $this->class->associationMappings[$name]; | ||
| 187 | $assocEntity = $this->class->getFieldValue($entity, $name); | ||
| 188 | |||
| 189 | if ($assocEntity === null) { | ||
| 190 | continue; | ||
| 191 | } | ||
| 192 | |||
| 193 | $assocId = $this->uow->getEntityIdentifier($assocEntity); | ||
| 194 | $assocMetadata = $this->metadataFactory->getMetadataFor($assoc->targetEntity); | ||
| 195 | $assocKey = new EntityCacheKey($assocMetadata->rootEntityName, $assocId); | ||
| 196 | $assocPersister = $this->uow->getEntityPersister($assoc->targetEntity); | ||
| 197 | |||
| 198 | $assocPersister->storeEntityCache($assocEntity, $assocKey); | ||
| 199 | } | ||
| 200 | } | ||
| 201 | |||
| 202 | /** | ||
| 203 | * Generates a string of currently query | ||
| 204 | * | ||
| 205 | * @param string[]|Criteria $criteria | ||
| 206 | * @param array<string, Order>|null $orderBy | ||
| 207 | */ | ||
| 208 | protected function getHash( | ||
| 209 | string $query, | ||
| 210 | array|Criteria $criteria, | ||
| 211 | array|null $orderBy = null, | ||
| 212 | int|null $limit = null, | ||
| 213 | int|null $offset = null, | ||
| 214 | ): string { | ||
| 215 | [$params] = $criteria instanceof Criteria | ||
| 216 | ? $this->persister->expandCriteriaParameters($criteria) | ||
| 217 | : $this->persister->expandParameters($criteria); | ||
| 218 | |||
| 219 | return sha1($query . serialize($params) . serialize($orderBy) . $limit . $offset); | ||
| 220 | } | ||
| 221 | |||
| 222 | /** | ||
| 223 | * {@inheritDoc} | ||
| 224 | */ | ||
| 225 | public function expandParameters(array $criteria): array | ||
| 226 | { | ||
| 227 | return $this->persister->expandParameters($criteria); | ||
| 228 | } | ||
| 229 | |||
| 230 | /** | ||
| 231 | * {@inheritDoc} | ||
| 232 | */ | ||
| 233 | public function expandCriteriaParameters(Criteria $criteria): array | ||
| 234 | { | ||
| 235 | return $this->persister->expandCriteriaParameters($criteria); | ||
| 236 | } | ||
| 237 | |||
| 238 | public function getClassMetadata(): ClassMetadata | ||
| 239 | { | ||
| 240 | return $this->persister->getClassMetadata(); | ||
| 241 | } | ||
| 242 | |||
| 243 | /** | ||
| 244 | * {@inheritDoc} | ||
| 245 | */ | ||
| 246 | public function getManyToManyCollection( | ||
| 247 | AssociationMapping $assoc, | ||
| 248 | object $sourceEntity, | ||
| 249 | int|null $offset = null, | ||
| 250 | int|null $limit = null, | ||
| 251 | ): array { | ||
| 252 | return $this->persister->getManyToManyCollection($assoc, $sourceEntity, $offset, $limit); | ||
| 253 | } | ||
| 254 | |||
| 255 | /** | ||
| 256 | * {@inheritDoc} | ||
| 257 | */ | ||
| 258 | public function getOneToManyCollection( | ||
| 259 | AssociationMapping $assoc, | ||
| 260 | object $sourceEntity, | ||
| 261 | int|null $offset = null, | ||
| 262 | int|null $limit = null, | ||
| 263 | ): array { | ||
| 264 | return $this->persister->getOneToManyCollection($assoc, $sourceEntity, $offset, $limit); | ||
| 265 | } | ||
| 266 | |||
| 267 | public function getOwningTable(string $fieldName): string | ||
| 268 | { | ||
| 269 | return $this->persister->getOwningTable($fieldName); | ||
| 270 | } | ||
| 271 | |||
| 272 | public function executeInserts(): void | ||
| 273 | { | ||
| 274 | // The commit order/foreign key relationships may make it necessary that multiple calls to executeInsert() | ||
| 275 | // are performed, so collect all the new entities. | ||
| 276 | $newInserts = $this->persister->getInserts(); | ||
| 277 | |||
| 278 | if ($newInserts) { | ||
| 279 | $this->queuedCache['insert'] = array_merge($this->queuedCache['insert'] ?? [], $newInserts); | ||
| 280 | } | ||
| 281 | |||
| 282 | $this->persister->executeInserts(); | ||
| 283 | } | ||
| 284 | |||
| 285 | /** | ||
| 286 | * {@inheritDoc} | ||
| 287 | */ | ||
| 288 | public function load( | ||
| 289 | array $criteria, | ||
| 290 | object|null $entity = null, | ||
| 291 | AssociationMapping|null $assoc = null, | ||
| 292 | array $hints = [], | ||
| 293 | LockMode|int|null $lockMode = null, | ||
| 294 | int|null $limit = null, | ||
| 295 | array|null $orderBy = null, | ||
| 296 | ): object|null { | ||
| 297 | if ($entity !== null || $assoc !== null || $hints !== [] || $lockMode !== null) { | ||
| 298 | return $this->persister->load($criteria, $entity, $assoc, $hints, $lockMode, $limit, $orderBy); | ||
| 299 | } | ||
| 300 | |||
| 301 | //handle only EntityRepository#findOneBy | ||
| 302 | $query = $this->persister->getSelectSQL($criteria, null, null, $limit, null, $orderBy); | ||
| 303 | $hash = $this->getHash($query, $criteria); | ||
| 304 | $rsm = $this->getResultSetMapping(); | ||
| 305 | $queryKey = new QueryCacheKey($hash, 0, Cache::MODE_NORMAL, $this->timestampKey); | ||
| 306 | $queryCache = $this->cache->getQueryCache($this->regionName); | ||
| 307 | $result = $queryCache->get($queryKey, $rsm); | ||
| 308 | |||
| 309 | if ($result !== null) { | ||
| 310 | $this->cacheLogger?->queryCacheHit($this->regionName, $queryKey); | ||
| 311 | |||
| 312 | return $result[0]; | ||
| 313 | } | ||
| 314 | |||
| 315 | $result = $this->persister->load($criteria, $entity, $assoc, $hints, $lockMode, $limit, $orderBy); | ||
| 316 | |||
| 317 | if ($result === null) { | ||
| 318 | return null; | ||
| 319 | } | ||
| 320 | |||
| 321 | $cached = $queryCache->put($queryKey, $rsm, [$result]); | ||
| 322 | |||
| 323 | $this->cacheLogger?->queryCacheMiss($this->regionName, $queryKey); | ||
| 324 | |||
| 325 | if ($cached) { | ||
| 326 | $this->cacheLogger?->queryCachePut($this->regionName, $queryKey); | ||
| 327 | } | ||
| 328 | |||
| 329 | return $result; | ||
| 330 | } | ||
| 331 | |||
| 332 | /** | ||
| 333 | * {@inheritDoc} | ||
| 334 | */ | ||
| 335 | public function loadAll( | ||
| 336 | array $criteria = [], | ||
| 337 | array|null $orderBy = null, | ||
| 338 | int|null $limit = null, | ||
| 339 | int|null $offset = null, | ||
| 340 | ): array { | ||
| 341 | $query = $this->persister->getSelectSQL($criteria, null, null, $limit, $offset, $orderBy); | ||
| 342 | $hash = $this->getHash($query, $criteria); | ||
| 343 | $rsm = $this->getResultSetMapping(); | ||
| 344 | $queryKey = new QueryCacheKey($hash, 0, Cache::MODE_NORMAL, $this->timestampKey); | ||
| 345 | $queryCache = $this->cache->getQueryCache($this->regionName); | ||
| 346 | $result = $queryCache->get($queryKey, $rsm); | ||
| 347 | |||
| 348 | if ($result !== null) { | ||
| 349 | $this->cacheLogger?->queryCacheHit($this->regionName, $queryKey); | ||
| 350 | |||
| 351 | return $result; | ||
| 352 | } | ||
| 353 | |||
| 354 | $result = $this->persister->loadAll($criteria, $orderBy, $limit, $offset); | ||
| 355 | $cached = $queryCache->put($queryKey, $rsm, $result); | ||
| 356 | |||
| 357 | if ($result) { | ||
| 358 | $this->cacheLogger?->queryCacheMiss($this->regionName, $queryKey); | ||
| 359 | } | ||
| 360 | |||
| 361 | if ($cached) { | ||
| 362 | $this->cacheLogger?->queryCachePut($this->regionName, $queryKey); | ||
| 363 | } | ||
| 364 | |||
| 365 | return $result; | ||
| 366 | } | ||
| 367 | |||
| 368 | /** | ||
| 369 | * {@inheritDoc} | ||
| 370 | */ | ||
| 371 | public function loadById(array $identifier, object|null $entity = null): object|null | ||
| 372 | { | ||
| 373 | $cacheKey = new EntityCacheKey($this->class->rootEntityName, $identifier); | ||
| 374 | $cacheEntry = $this->region->get($cacheKey); | ||
| 375 | $class = $this->class; | ||
| 376 | |||
| 377 | if ($cacheEntry !== null) { | ||
| 378 | if ($cacheEntry->class !== $this->class->name) { | ||
| 379 | $class = $this->metadataFactory->getMetadataFor($cacheEntry->class); | ||
| 380 | } | ||
| 381 | |||
| 382 | $cachedEntity = $this->hydrator->loadCacheEntry($class, $cacheKey, $cacheEntry, $entity); | ||
| 383 | |||
| 384 | if ($cachedEntity !== null) { | ||
| 385 | $this->cacheLogger?->entityCacheHit($this->regionName, $cacheKey); | ||
| 386 | |||
| 387 | return $cachedEntity; | ||
| 388 | } | ||
| 389 | } | ||
| 390 | |||
| 391 | $entity = $this->persister->loadById($identifier, $entity); | ||
| 392 | |||
| 393 | if ($entity === null) { | ||
| 394 | return null; | ||
| 395 | } | ||
| 396 | |||
| 397 | $class = $this->class; | ||
| 398 | $className = DefaultProxyClassNameResolver::getClass($entity); | ||
| 399 | |||
| 400 | if ($className !== $this->class->name) { | ||
| 401 | $class = $this->metadataFactory->getMetadataFor($className); | ||
| 402 | } | ||
| 403 | |||
| 404 | $cacheEntry = $this->hydrator->buildCacheEntry($class, $cacheKey, $entity); | ||
| 405 | $cached = $this->region->put($cacheKey, $cacheEntry); | ||
| 406 | |||
| 407 | if ($cached && ($this->joinedAssociations === null || $this->joinedAssociations)) { | ||
| 408 | $this->storeJoinedAssociations($entity); | ||
| 409 | } | ||
| 410 | |||
| 411 | if ($cached) { | ||
| 412 | $this->cacheLogger?->entityCachePut($this->regionName, $cacheKey); | ||
| 413 | } | ||
| 414 | |||
| 415 | $this->cacheLogger?->entityCacheMiss($this->regionName, $cacheKey); | ||
| 416 | |||
| 417 | return $entity; | ||
| 418 | } | ||
| 419 | |||
| 420 | public function count(array|Criteria $criteria = []): int | ||
| 421 | { | ||
| 422 | return $this->persister->count($criteria); | ||
| 423 | } | ||
| 424 | |||
| 425 | /** | ||
| 426 | * {@inheritDoc} | ||
| 427 | */ | ||
| 428 | public function loadCriteria(Criteria $criteria): array | ||
| 429 | { | ||
| 430 | $orderBy = $criteria->orderings(); | ||
| 431 | $limit = $criteria->getMaxResults(); | ||
| 432 | $offset = $criteria->getFirstResult(); | ||
| 433 | $query = $this->persister->getSelectSQL($criteria); | ||
| 434 | $hash = $this->getHash($query, $criteria, $orderBy, $limit, $offset); | ||
| 435 | $rsm = $this->getResultSetMapping(); | ||
| 436 | $queryKey = new QueryCacheKey($hash, 0, Cache::MODE_NORMAL, $this->timestampKey); | ||
| 437 | $queryCache = $this->cache->getQueryCache($this->regionName); | ||
| 438 | $cacheResult = $queryCache->get($queryKey, $rsm); | ||
| 439 | |||
| 440 | if ($cacheResult !== null) { | ||
| 441 | $this->cacheLogger?->queryCacheHit($this->regionName, $queryKey); | ||
| 442 | |||
| 443 | return $cacheResult; | ||
| 444 | } | ||
| 445 | |||
| 446 | $result = $this->persister->loadCriteria($criteria); | ||
| 447 | $cached = $queryCache->put($queryKey, $rsm, $result); | ||
| 448 | |||
| 449 | if ($result) { | ||
| 450 | $this->cacheLogger?->queryCacheMiss($this->regionName, $queryKey); | ||
| 451 | } | ||
| 452 | |||
| 453 | if ($cached) { | ||
| 454 | $this->cacheLogger?->queryCachePut($this->regionName, $queryKey); | ||
| 455 | } | ||
| 456 | |||
| 457 | return $result; | ||
| 458 | } | ||
| 459 | |||
| 460 | /** | ||
| 461 | * {@inheritDoc} | ||
| 462 | */ | ||
| 463 | public function loadManyToManyCollection( | ||
| 464 | AssociationMapping $assoc, | ||
| 465 | object $sourceEntity, | ||
| 466 | PersistentCollection $collection, | ||
| 467 | ): array { | ||
| 468 | $persister = $this->uow->getCollectionPersister($assoc); | ||
| 469 | $hasCache = ($persister instanceof CachedPersister); | ||
| 470 | |||
| 471 | if (! $hasCache) { | ||
| 472 | return $this->persister->loadManyToManyCollection($assoc, $sourceEntity, $collection); | ||
| 473 | } | ||
| 474 | |||
| 475 | $ownerId = $this->uow->getEntityIdentifier($collection->getOwner()); | ||
| 476 | $key = $this->buildCollectionCacheKey($assoc, $ownerId); | ||
| 477 | $list = $persister->loadCollectionCache($collection, $key); | ||
| 478 | |||
| 479 | if ($list !== null) { | ||
| 480 | $this->cacheLogger?->collectionCacheHit($persister->getCacheRegion()->getName(), $key); | ||
| 481 | |||
| 482 | return $list; | ||
| 483 | } | ||
| 484 | |||
| 485 | $list = $this->persister->loadManyToManyCollection($assoc, $sourceEntity, $collection); | ||
| 486 | |||
| 487 | $persister->storeCollectionCache($key, $list); | ||
| 488 | |||
| 489 | $this->cacheLogger?->collectionCacheMiss($persister->getCacheRegion()->getName(), $key); | ||
| 490 | |||
| 491 | return $list; | ||
| 492 | } | ||
| 493 | |||
| 494 | public function loadOneToManyCollection( | ||
| 495 | AssociationMapping $assoc, | ||
| 496 | object $sourceEntity, | ||
| 497 | PersistentCollection $collection, | ||
| 498 | ): mixed { | ||
| 499 | $persister = $this->uow->getCollectionPersister($assoc); | ||
| 500 | $hasCache = ($persister instanceof CachedPersister); | ||
| 501 | |||
| 502 | if (! $hasCache) { | ||
| 503 | return $this->persister->loadOneToManyCollection($assoc, $sourceEntity, $collection); | ||
| 504 | } | ||
| 505 | |||
| 506 | $ownerId = $this->uow->getEntityIdentifier($collection->getOwner()); | ||
| 507 | $key = $this->buildCollectionCacheKey($assoc, $ownerId); | ||
| 508 | $list = $persister->loadCollectionCache($collection, $key); | ||
| 509 | |||
| 510 | if ($list !== null) { | ||
| 511 | $this->cacheLogger?->collectionCacheHit($persister->getCacheRegion()->getName(), $key); | ||
| 512 | |||
| 513 | return $list; | ||
| 514 | } | ||
| 515 | |||
| 516 | $list = $this->persister->loadOneToManyCollection($assoc, $sourceEntity, $collection); | ||
| 517 | |||
| 518 | $persister->storeCollectionCache($key, $list); | ||
| 519 | |||
| 520 | $this->cacheLogger?->collectionCacheMiss($persister->getCacheRegion()->getName(), $key); | ||
| 521 | |||
| 522 | return $list; | ||
| 523 | } | ||
| 524 | |||
| 525 | /** | ||
| 526 | * {@inheritDoc} | ||
| 527 | */ | ||
| 528 | public function loadOneToOneEntity(AssociationMapping $assoc, object $sourceEntity, array $identifier = []): object|null | ||
| 529 | { | ||
| 530 | return $this->persister->loadOneToOneEntity($assoc, $sourceEntity, $identifier); | ||
| 531 | } | ||
| 532 | |||
| 533 | /** | ||
| 534 | * {@inheritDoc} | ||
| 535 | */ | ||
| 536 | public function lock(array $criteria, LockMode|int $lockMode): void | ||
| 537 | { | ||
| 538 | $this->persister->lock($criteria, $lockMode); | ||
| 539 | } | ||
| 540 | |||
| 541 | /** | ||
| 542 | * {@inheritDoc} | ||
| 543 | */ | ||
| 544 | public function refresh(array $id, object $entity, LockMode|int|null $lockMode = null): void | ||
| 545 | { | ||
| 546 | $this->persister->refresh($id, $entity, $lockMode); | ||
| 547 | } | ||
| 548 | |||
| 549 | /** @param array<string, mixed> $ownerId */ | ||
| 550 | protected function buildCollectionCacheKey(AssociationMapping $association, array $ownerId): CollectionCacheKey | ||
| 551 | { | ||
| 552 | $metadata = $this->metadataFactory->getMetadataFor($association->sourceEntity); | ||
| 553 | assert($metadata instanceof ClassMetadata); | ||
| 554 | |||
| 555 | return new CollectionCacheKey($metadata->rootEntityName, $association->fieldName, $ownerId); | ||
| 556 | } | ||
| 557 | } | ||
diff --git a/vendor/doctrine/orm/src/Cache/Persister/Entity/CachedEntityPersister.php b/vendor/doctrine/orm/src/Cache/Persister/Entity/CachedEntityPersister.php new file mode 100644 index 0000000..5fba56f --- /dev/null +++ b/vendor/doctrine/orm/src/Cache/Persister/Entity/CachedEntityPersister.php | |||
| @@ -0,0 +1,20 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Cache\Persister\Entity; | ||
| 6 | |||
| 7 | use Doctrine\ORM\Cache\EntityCacheKey; | ||
| 8 | use Doctrine\ORM\Cache\EntityHydrator; | ||
| 9 | use Doctrine\ORM\Cache\Persister\CachedPersister; | ||
| 10 | use Doctrine\ORM\Persisters\Entity\EntityPersister; | ||
| 11 | |||
| 12 | /** | ||
| 13 | * Interface for second level cache entity persisters. | ||
| 14 | */ | ||
| 15 | interface CachedEntityPersister extends CachedPersister, EntityPersister | ||
| 16 | { | ||
| 17 | public function getEntityHydrator(): EntityHydrator; | ||
| 18 | |||
| 19 | public function storeEntityCache(object $entity, EntityCacheKey $key): bool; | ||
| 20 | } | ||
diff --git a/vendor/doctrine/orm/src/Cache/Persister/Entity/NonStrictReadWriteCachedEntityPersister.php b/vendor/doctrine/orm/src/Cache/Persister/Entity/NonStrictReadWriteCachedEntityPersister.php new file mode 100644 index 0000000..43c76ab --- /dev/null +++ b/vendor/doctrine/orm/src/Cache/Persister/Entity/NonStrictReadWriteCachedEntityPersister.php | |||
| @@ -0,0 +1,85 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Cache\Persister\Entity; | ||
| 6 | |||
| 7 | use Doctrine\ORM\Cache\EntityCacheKey; | ||
| 8 | |||
| 9 | /** | ||
| 10 | * Specific non-strict read/write cached entity persister | ||
| 11 | */ | ||
| 12 | class NonStrictReadWriteCachedEntityPersister extends AbstractEntityPersister | ||
| 13 | { | ||
| 14 | public function afterTransactionComplete(): void | ||
| 15 | { | ||
| 16 | $isChanged = false; | ||
| 17 | |||
| 18 | if (isset($this->queuedCache['insert'])) { | ||
| 19 | foreach ($this->queuedCache['insert'] as $entity) { | ||
| 20 | $isChanged = $this->updateCache($entity, $isChanged); | ||
| 21 | } | ||
| 22 | } | ||
| 23 | |||
| 24 | if (isset($this->queuedCache['update'])) { | ||
| 25 | foreach ($this->queuedCache['update'] as $entity) { | ||
| 26 | $isChanged = $this->updateCache($entity, $isChanged); | ||
| 27 | } | ||
| 28 | } | ||
| 29 | |||
| 30 | if (isset($this->queuedCache['delete'])) { | ||
| 31 | foreach ($this->queuedCache['delete'] as $key) { | ||
| 32 | $this->region->evict($key); | ||
| 33 | |||
| 34 | $isChanged = true; | ||
| 35 | } | ||
| 36 | } | ||
| 37 | |||
| 38 | if ($isChanged) { | ||
| 39 | $this->timestampRegion->update($this->timestampKey); | ||
| 40 | } | ||
| 41 | |||
| 42 | $this->queuedCache = []; | ||
| 43 | } | ||
| 44 | |||
| 45 | public function afterTransactionRolledBack(): void | ||
| 46 | { | ||
| 47 | $this->queuedCache = []; | ||
| 48 | } | ||
| 49 | |||
| 50 | public function delete(object $entity): bool | ||
| 51 | { | ||
| 52 | $key = new EntityCacheKey($this->class->rootEntityName, $this->uow->getEntityIdentifier($entity)); | ||
| 53 | $deleted = $this->persister->delete($entity); | ||
| 54 | |||
| 55 | if ($deleted) { | ||
| 56 | $this->region->evict($key); | ||
| 57 | } | ||
| 58 | |||
| 59 | $this->queuedCache['delete'][] = $key; | ||
| 60 | |||
| 61 | return $deleted; | ||
| 62 | } | ||
| 63 | |||
| 64 | public function update(object $entity): void | ||
| 65 | { | ||
| 66 | $this->persister->update($entity); | ||
| 67 | |||
| 68 | $this->queuedCache['update'][] = $entity; | ||
| 69 | } | ||
| 70 | |||
| 71 | private function updateCache(object $entity, bool $isChanged): bool | ||
| 72 | { | ||
| 73 | $class = $this->metadataFactory->getMetadataFor($entity::class); | ||
| 74 | $key = new EntityCacheKey($class->rootEntityName, $this->uow->getEntityIdentifier($entity)); | ||
| 75 | $entry = $this->hydrator->buildCacheEntry($class, $key, $entity); | ||
| 76 | $cached = $this->region->put($key, $entry); | ||
| 77 | $isChanged = $isChanged || $cached; | ||
| 78 | |||
| 79 | if ($cached) { | ||
| 80 | $this->cacheLogger?->entityCachePut($this->regionName, $key); | ||
| 81 | } | ||
| 82 | |||
| 83 | return $isChanged; | ||
| 84 | } | ||
| 85 | } | ||
diff --git a/vendor/doctrine/orm/src/Cache/Persister/Entity/ReadOnlyCachedEntityPersister.php b/vendor/doctrine/orm/src/Cache/Persister/Entity/ReadOnlyCachedEntityPersister.php new file mode 100644 index 0000000..4cd1784 --- /dev/null +++ b/vendor/doctrine/orm/src/Cache/Persister/Entity/ReadOnlyCachedEntityPersister.php | |||
| @@ -0,0 +1,19 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Cache\Persister\Entity; | ||
| 6 | |||
| 7 | use Doctrine\ORM\Cache\Exception\CannotUpdateReadOnlyEntity; | ||
| 8 | use Doctrine\ORM\Proxy\DefaultProxyClassNameResolver; | ||
| 9 | |||
| 10 | /** | ||
| 11 | * Specific read-only region entity persister | ||
| 12 | */ | ||
| 13 | class ReadOnlyCachedEntityPersister extends NonStrictReadWriteCachedEntityPersister | ||
| 14 | { | ||
| 15 | public function update(object $entity): void | ||
| 16 | { | ||
| 17 | throw CannotUpdateReadOnlyEntity::fromEntity(DefaultProxyClassNameResolver::getClass($entity)); | ||
| 18 | } | ||
| 19 | } | ||
diff --git a/vendor/doctrine/orm/src/Cache/Persister/Entity/ReadWriteCachedEntityPersister.php b/vendor/doctrine/orm/src/Cache/Persister/Entity/ReadWriteCachedEntityPersister.php new file mode 100644 index 0000000..a1ea0dc --- /dev/null +++ b/vendor/doctrine/orm/src/Cache/Persister/Entity/ReadWriteCachedEntityPersister.php | |||
| @@ -0,0 +1,105 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Cache\Persister\Entity; | ||
| 6 | |||
| 7 | use Doctrine\ORM\Cache\ConcurrentRegion; | ||
| 8 | use Doctrine\ORM\Cache\EntityCacheKey; | ||
| 9 | use Doctrine\ORM\EntityManagerInterface; | ||
| 10 | use Doctrine\ORM\Mapping\ClassMetadata; | ||
| 11 | use Doctrine\ORM\Persisters\Entity\EntityPersister; | ||
| 12 | |||
| 13 | /** | ||
| 14 | * Specific read-write entity persister | ||
| 15 | */ | ||
| 16 | class ReadWriteCachedEntityPersister extends AbstractEntityPersister | ||
| 17 | { | ||
| 18 | public function __construct(EntityPersister $persister, ConcurrentRegion $region, EntityManagerInterface $em, ClassMetadata $class) | ||
| 19 | { | ||
| 20 | parent::__construct($persister, $region, $em, $class); | ||
| 21 | } | ||
| 22 | |||
| 23 | public function afterTransactionComplete(): void | ||
| 24 | { | ||
| 25 | $isChanged = true; | ||
| 26 | |||
| 27 | if (isset($this->queuedCache['update'])) { | ||
| 28 | foreach ($this->queuedCache['update'] as $item) { | ||
| 29 | $this->region->evict($item['key']); | ||
| 30 | |||
| 31 | $isChanged = true; | ||
| 32 | } | ||
| 33 | } | ||
| 34 | |||
| 35 | if (isset($this->queuedCache['delete'])) { | ||
| 36 | foreach ($this->queuedCache['delete'] as $item) { | ||
| 37 | $this->region->evict($item['key']); | ||
| 38 | |||
| 39 | $isChanged = true; | ||
| 40 | } | ||
| 41 | } | ||
| 42 | |||
| 43 | if ($isChanged) { | ||
| 44 | $this->timestampRegion->update($this->timestampKey); | ||
| 45 | } | ||
| 46 | |||
| 47 | $this->queuedCache = []; | ||
| 48 | } | ||
| 49 | |||
| 50 | public function afterTransactionRolledBack(): void | ||
| 51 | { | ||
| 52 | if (isset($this->queuedCache['update'])) { | ||
| 53 | foreach ($this->queuedCache['update'] as $item) { | ||
| 54 | $this->region->evict($item['key']); | ||
| 55 | } | ||
| 56 | } | ||
| 57 | |||
| 58 | if (isset($this->queuedCache['delete'])) { | ||
| 59 | foreach ($this->queuedCache['delete'] as $item) { | ||
| 60 | $this->region->evict($item['key']); | ||
| 61 | } | ||
| 62 | } | ||
| 63 | |||
| 64 | $this->queuedCache = []; | ||
| 65 | } | ||
| 66 | |||
| 67 | public function delete(object $entity): bool | ||
| 68 | { | ||
| 69 | $key = new EntityCacheKey($this->class->rootEntityName, $this->uow->getEntityIdentifier($entity)); | ||
| 70 | $lock = $this->region->lock($key); | ||
| 71 | $deleted = $this->persister->delete($entity); | ||
| 72 | |||
| 73 | if ($deleted) { | ||
| 74 | $this->region->evict($key); | ||
| 75 | } | ||
| 76 | |||
| 77 | if ($lock === null) { | ||
| 78 | return $deleted; | ||
| 79 | } | ||
| 80 | |||
| 81 | $this->queuedCache['delete'][] = [ | ||
| 82 | 'lock' => $lock, | ||
| 83 | 'key' => $key, | ||
| 84 | ]; | ||
| 85 | |||
| 86 | return $deleted; | ||
| 87 | } | ||
| 88 | |||
| 89 | public function update(object $entity): void | ||
| 90 | { | ||
| 91 | $key = new EntityCacheKey($this->class->rootEntityName, $this->uow->getEntityIdentifier($entity)); | ||
| 92 | $lock = $this->region->lock($key); | ||
| 93 | |||
| 94 | $this->persister->update($entity); | ||
| 95 | |||
| 96 | if ($lock === null) { | ||
| 97 | return; | ||
| 98 | } | ||
| 99 | |||
| 100 | $this->queuedCache['update'][] = [ | ||
| 101 | 'lock' => $lock, | ||
| 102 | 'key' => $key, | ||
| 103 | ]; | ||
| 104 | } | ||
| 105 | } | ||
diff --git a/vendor/doctrine/orm/src/Cache/QueryCache.php b/vendor/doctrine/orm/src/Cache/QueryCache.php new file mode 100644 index 0000000..e697680 --- /dev/null +++ b/vendor/doctrine/orm/src/Cache/QueryCache.php | |||
| @@ -0,0 +1,28 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Cache; | ||
| 6 | |||
| 7 | use Doctrine\ORM\Query\ResultSetMapping; | ||
| 8 | |||
| 9 | /** | ||
| 10 | * Defines the contract for caches capable of storing query results. | ||
| 11 | * These caches should only concern themselves with storing the matching result ids. | ||
| 12 | */ | ||
| 13 | interface QueryCache | ||
| 14 | { | ||
| 15 | public function clear(): bool; | ||
| 16 | |||
| 17 | /** @param mixed[] $hints */ | ||
| 18 | public function put(QueryCacheKey $key, ResultSetMapping $rsm, mixed $result, array $hints = []): bool; | ||
| 19 | |||
| 20 | /** | ||
| 21 | * @param mixed[] $hints | ||
| 22 | * | ||
| 23 | * @return mixed[]|null | ||
| 24 | */ | ||
| 25 | public function get(QueryCacheKey $key, ResultSetMapping $rsm, array $hints = []): array|null; | ||
| 26 | |||
| 27 | public function getRegion(): Region; | ||
| 28 | } | ||
diff --git a/vendor/doctrine/orm/src/Cache/QueryCacheEntry.php b/vendor/doctrine/orm/src/Cache/QueryCacheEntry.php new file mode 100644 index 0000000..1e39262 --- /dev/null +++ b/vendor/doctrine/orm/src/Cache/QueryCacheEntry.php | |||
| @@ -0,0 +1,29 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Cache; | ||
| 6 | |||
| 7 | use function microtime; | ||
| 8 | |||
| 9 | class QueryCacheEntry implements CacheEntry | ||
| 10 | { | ||
| 11 | /** | ||
| 12 | * Time creation of this cache entry | ||
| 13 | */ | ||
| 14 | public readonly float $time; | ||
| 15 | |||
| 16 | /** @param array<string, mixed> $result List of entity identifiers */ | ||
| 17 | public function __construct( | ||
| 18 | public readonly array $result, | ||
| 19 | float|null $time = null, | ||
| 20 | ) { | ||
| 21 | $this->time = $time ?: microtime(true); | ||
| 22 | } | ||
| 23 | |||
| 24 | /** @param array<string, mixed> $values */ | ||
| 25 | public static function __set_state(array $values): self | ||
| 26 | { | ||
| 27 | return new self($values['result'], $values['time']); | ||
| 28 | } | ||
| 29 | } | ||
diff --git a/vendor/doctrine/orm/src/Cache/QueryCacheKey.php b/vendor/doctrine/orm/src/Cache/QueryCacheKey.php new file mode 100644 index 0000000..2372e5a --- /dev/null +++ b/vendor/doctrine/orm/src/Cache/QueryCacheKey.php | |||
| @@ -0,0 +1,23 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Cache; | ||
| 6 | |||
| 7 | use Doctrine\ORM\Cache; | ||
| 8 | |||
| 9 | /** | ||
| 10 | * A cache key that identifies a particular query. | ||
| 11 | */ | ||
| 12 | class QueryCacheKey extends CacheKey | ||
| 13 | { | ||
| 14 | /** @param Cache::MODE_* $cacheMode */ | ||
| 15 | public function __construct( | ||
| 16 | string $cacheId, | ||
| 17 | public readonly int $lifetime = 0, | ||
| 18 | public readonly int $cacheMode = Cache::MODE_NORMAL, | ||
| 19 | public readonly TimestampCacheKey|null $timestampKey = null, | ||
| 20 | ) { | ||
| 21 | parent::__construct($cacheId); | ||
| 22 | } | ||
| 23 | } | ||
diff --git a/vendor/doctrine/orm/src/Cache/QueryCacheValidator.php b/vendor/doctrine/orm/src/Cache/QueryCacheValidator.php new file mode 100644 index 0000000..8a0d39f --- /dev/null +++ b/vendor/doctrine/orm/src/Cache/QueryCacheValidator.php | |||
| @@ -0,0 +1,16 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Cache; | ||
| 6 | |||
| 7 | /** | ||
| 8 | * Cache query validator interface. | ||
| 9 | */ | ||
| 10 | interface QueryCacheValidator | ||
| 11 | { | ||
| 12 | /** | ||
| 13 | * Checks if the query entry is valid | ||
| 14 | */ | ||
| 15 | public function isValid(QueryCacheKey $key, QueryCacheEntry $entry): bool; | ||
| 16 | } | ||
diff --git a/vendor/doctrine/orm/src/Cache/Region.php b/vendor/doctrine/orm/src/Cache/Region.php new file mode 100644 index 0000000..f7a1b26 --- /dev/null +++ b/vendor/doctrine/orm/src/Cache/Region.php | |||
| @@ -0,0 +1,73 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Cache; | ||
| 6 | |||
| 7 | use Doctrine\ORM\Cache\Exception\CacheException; | ||
| 8 | |||
| 9 | /** | ||
| 10 | * Defines a contract for accessing a particular named region. | ||
| 11 | */ | ||
| 12 | interface Region | ||
| 13 | { | ||
| 14 | /** | ||
| 15 | * Retrieve the name of this region. | ||
| 16 | */ | ||
| 17 | public function getName(): string; | ||
| 18 | |||
| 19 | /** | ||
| 20 | * Determine whether this region contains data for the given key. | ||
| 21 | * | ||
| 22 | * @param CacheKey $key The cache key | ||
| 23 | */ | ||
| 24 | public function contains(CacheKey $key): bool; | ||
| 25 | |||
| 26 | /** | ||
| 27 | * Get an item from the cache. | ||
| 28 | * | ||
| 29 | * @param CacheKey $key The key of the item to be retrieved. | ||
| 30 | * | ||
| 31 | * @return CacheEntry|null The cached entry or NULL | ||
| 32 | * | ||
| 33 | * @throws CacheException Indicates a problem accessing the item or region. | ||
| 34 | */ | ||
| 35 | public function get(CacheKey $key): CacheEntry|null; | ||
| 36 | |||
| 37 | /** | ||
| 38 | * Get all items from the cache identified by $keys. | ||
| 39 | * It returns NULL if some elements can not be found. | ||
| 40 | * | ||
| 41 | * @param CollectionCacheEntry $collection The collection of the items to be retrieved. | ||
| 42 | * | ||
| 43 | * @return CacheEntry[]|null The cached entries or NULL if one or more entries can not be found | ||
| 44 | */ | ||
| 45 | public function getMultiple(CollectionCacheEntry $collection): array|null; | ||
| 46 | |||
| 47 | /** | ||
| 48 | * Put an item into the cache. | ||
| 49 | * | ||
| 50 | * @param CacheKey $key The key under which to cache the item. | ||
| 51 | * @param CacheEntry $entry The entry to cache. | ||
| 52 | * @param Lock|null $lock The lock previously obtained. | ||
| 53 | * | ||
| 54 | * @throws CacheException Indicates a problem accessing the region. | ||
| 55 | */ | ||
| 56 | public function put(CacheKey $key, CacheEntry $entry, Lock|null $lock = null): bool; | ||
| 57 | |||
| 58 | /** | ||
| 59 | * Remove an item from the cache. | ||
| 60 | * | ||
| 61 | * @param CacheKey $key The key under which to cache the item. | ||
| 62 | * | ||
| 63 | * @throws CacheException Indicates a problem accessing the region. | ||
| 64 | */ | ||
| 65 | public function evict(CacheKey $key): bool; | ||
| 66 | |||
| 67 | /** | ||
| 68 | * Remove all contents of this particular cache region. | ||
| 69 | * | ||
| 70 | * @throws CacheException Indicates problem accessing the region. | ||
| 71 | */ | ||
| 72 | public function evictAll(): bool; | ||
| 73 | } | ||
diff --git a/vendor/doctrine/orm/src/Cache/Region/DefaultRegion.php b/vendor/doctrine/orm/src/Cache/Region/DefaultRegion.php new file mode 100644 index 0000000..0576195 --- /dev/null +++ b/vendor/doctrine/orm/src/Cache/Region/DefaultRegion.php | |||
| @@ -0,0 +1,113 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Cache\Region; | ||
| 6 | |||
| 7 | use Doctrine\ORM\Cache\CacheEntry; | ||
| 8 | use Doctrine\ORM\Cache\CacheKey; | ||
| 9 | use Doctrine\ORM\Cache\CollectionCacheEntry; | ||
| 10 | use Doctrine\ORM\Cache\Lock; | ||
| 11 | use Doctrine\ORM\Cache\Region; | ||
| 12 | use Psr\Cache\CacheItemInterface; | ||
| 13 | use Psr\Cache\CacheItemPoolInterface; | ||
| 14 | use Traversable; | ||
| 15 | |||
| 16 | use function array_map; | ||
| 17 | use function iterator_to_array; | ||
| 18 | use function strtr; | ||
| 19 | |||
| 20 | /** | ||
| 21 | * The simplest cache region compatible with all doctrine-cache drivers. | ||
| 22 | */ | ||
| 23 | class DefaultRegion implements Region | ||
| 24 | { | ||
| 25 | private const REGION_KEY_SEPARATOR = '_'; | ||
| 26 | private const REGION_PREFIX = 'DC2_REGION_'; | ||
| 27 | |||
| 28 | public function __construct( | ||
| 29 | private readonly string $name, | ||
| 30 | private readonly CacheItemPoolInterface $cacheItemPool, | ||
| 31 | private readonly int $lifetime = 0, | ||
| 32 | ) { | ||
| 33 | } | ||
| 34 | |||
| 35 | public function getName(): string | ||
| 36 | { | ||
| 37 | return $this->name; | ||
| 38 | } | ||
| 39 | |||
| 40 | public function contains(CacheKey $key): bool | ||
| 41 | { | ||
| 42 | return $this->cacheItemPool->hasItem($this->getCacheEntryKey($key)); | ||
| 43 | } | ||
| 44 | |||
| 45 | public function get(CacheKey $key): CacheEntry|null | ||
| 46 | { | ||
| 47 | $item = $this->cacheItemPool->getItem($this->getCacheEntryKey($key)); | ||
| 48 | $entry = $item->isHit() ? $item->get() : null; | ||
| 49 | |||
| 50 | if (! $entry instanceof CacheEntry) { | ||
| 51 | return null; | ||
| 52 | } | ||
| 53 | |||
| 54 | return $entry; | ||
| 55 | } | ||
| 56 | |||
| 57 | public function getMultiple(CollectionCacheEntry $collection): array|null | ||
| 58 | { | ||
| 59 | $keys = array_map( | ||
| 60 | $this->getCacheEntryKey(...), | ||
| 61 | $collection->identifiers, | ||
| 62 | ); | ||
| 63 | /** @var iterable<string, CacheItemInterface> $items */ | ||
| 64 | $items = $this->cacheItemPool->getItems($keys); | ||
| 65 | if ($items instanceof Traversable) { | ||
| 66 | $items = iterator_to_array($items); | ||
| 67 | } | ||
| 68 | |||
| 69 | $result = []; | ||
| 70 | foreach ($keys as $arrayKey => $cacheKey) { | ||
| 71 | if (! isset($items[$cacheKey]) || ! $items[$cacheKey]->isHit()) { | ||
| 72 | return null; | ||
| 73 | } | ||
| 74 | |||
| 75 | $entry = $items[$cacheKey]->get(); | ||
| 76 | if (! $entry instanceof CacheEntry) { | ||
| 77 | return null; | ||
| 78 | } | ||
| 79 | |||
| 80 | $result[$arrayKey] = $entry; | ||
| 81 | } | ||
| 82 | |||
| 83 | return $result; | ||
| 84 | } | ||
| 85 | |||
| 86 | public function put(CacheKey $key, CacheEntry $entry, Lock|null $lock = null): bool | ||
| 87 | { | ||
| 88 | $item = $this->cacheItemPool | ||
| 89 | ->getItem($this->getCacheEntryKey($key)) | ||
| 90 | ->set($entry); | ||
| 91 | |||
| 92 | if ($this->lifetime > 0) { | ||
| 93 | $item->expiresAfter($this->lifetime); | ||
| 94 | } | ||
| 95 | |||
| 96 | return $this->cacheItemPool->save($item); | ||
| 97 | } | ||
| 98 | |||
| 99 | public function evict(CacheKey $key): bool | ||
| 100 | { | ||
| 101 | return $this->cacheItemPool->deleteItem($this->getCacheEntryKey($key)); | ||
| 102 | } | ||
| 103 | |||
| 104 | public function evictAll(): bool | ||
| 105 | { | ||
| 106 | return $this->cacheItemPool->clear(self::REGION_PREFIX . $this->name); | ||
| 107 | } | ||
| 108 | |||
| 109 | private function getCacheEntryKey(CacheKey $key): string | ||
| 110 | { | ||
| 111 | return self::REGION_PREFIX . $this->name . self::REGION_KEY_SEPARATOR . strtr($key->hash, '{}()/\@:', '________'); | ||
| 112 | } | ||
| 113 | } | ||
diff --git a/vendor/doctrine/orm/src/Cache/Region/FileLockRegion.php b/vendor/doctrine/orm/src/Cache/Region/FileLockRegion.php new file mode 100644 index 0000000..bedd6a6 --- /dev/null +++ b/vendor/doctrine/orm/src/Cache/Region/FileLockRegion.php | |||
| @@ -0,0 +1,194 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Cache\Region; | ||
| 6 | |||
| 7 | use Doctrine\ORM\Cache\CacheEntry; | ||
| 8 | use Doctrine\ORM\Cache\CacheKey; | ||
| 9 | use Doctrine\ORM\Cache\CollectionCacheEntry; | ||
| 10 | use Doctrine\ORM\Cache\ConcurrentRegion; | ||
| 11 | use Doctrine\ORM\Cache\Lock; | ||
| 12 | use Doctrine\ORM\Cache\Region; | ||
| 13 | use InvalidArgumentException; | ||
| 14 | |||
| 15 | use function array_filter; | ||
| 16 | use function array_map; | ||
| 17 | use function chmod; | ||
| 18 | use function file_get_contents; | ||
| 19 | use function file_put_contents; | ||
| 20 | use function fileatime; | ||
| 21 | use function glob; | ||
| 22 | use function is_dir; | ||
| 23 | use function is_file; | ||
| 24 | use function is_writable; | ||
| 25 | use function mkdir; | ||
| 26 | use function sprintf; | ||
| 27 | use function time; | ||
| 28 | use function unlink; | ||
| 29 | |||
| 30 | use const DIRECTORY_SEPARATOR; | ||
| 31 | use const LOCK_EX; | ||
| 32 | |||
| 33 | /** | ||
| 34 | * Very naive concurrent region, based on file locks. | ||
| 35 | */ | ||
| 36 | class FileLockRegion implements ConcurrentRegion | ||
| 37 | { | ||
| 38 | final public const LOCK_EXTENSION = 'lock'; | ||
| 39 | |||
| 40 | /** | ||
| 41 | * @param numeric-string|int $lockLifetime | ||
| 42 | * | ||
| 43 | * @throws InvalidArgumentException | ||
| 44 | */ | ||
| 45 | public function __construct( | ||
| 46 | private readonly Region $region, | ||
| 47 | private readonly string $directory, | ||
| 48 | private readonly string|int $lockLifetime, | ||
| 49 | ) { | ||
| 50 | if (! is_dir($directory) && ! @mkdir($directory, 0775, true)) { | ||
| 51 | throw new InvalidArgumentException(sprintf('The directory "%s" does not exist and could not be created.', $directory)); | ||
| 52 | } | ||
| 53 | |||
| 54 | if (! is_writable($directory)) { | ||
| 55 | throw new InvalidArgumentException(sprintf('The directory "%s" is not writable.', $directory)); | ||
| 56 | } | ||
| 57 | } | ||
| 58 | |||
| 59 | private function isLocked(CacheKey $key, Lock|null $lock = null): bool | ||
| 60 | { | ||
| 61 | $filename = $this->getLockFileName($key); | ||
| 62 | |||
| 63 | if (! is_file($filename)) { | ||
| 64 | return false; | ||
| 65 | } | ||
| 66 | |||
| 67 | $time = $this->getLockTime($filename); | ||
| 68 | $content = $this->getLockContent($filename); | ||
| 69 | |||
| 70 | if ($content === false || $time === false) { | ||
| 71 | @unlink($filename); | ||
| 72 | |||
| 73 | return false; | ||
| 74 | } | ||
| 75 | |||
| 76 | if ($lock && $content === $lock->value) { | ||
| 77 | return false; | ||
| 78 | } | ||
| 79 | |||
| 80 | // outdated lock | ||
| 81 | if ($time + $this->lockLifetime <= time()) { | ||
| 82 | @unlink($filename); | ||
| 83 | |||
| 84 | return false; | ||
| 85 | } | ||
| 86 | |||
| 87 | return true; | ||
| 88 | } | ||
| 89 | |||
| 90 | private function getLockFileName(CacheKey $key): string | ||
| 91 | { | ||
| 92 | return $this->directory . DIRECTORY_SEPARATOR . $key->hash . '.' . self::LOCK_EXTENSION; | ||
| 93 | } | ||
| 94 | |||
| 95 | private function getLockContent(string $filename): string|false | ||
| 96 | { | ||
| 97 | return @file_get_contents($filename); | ||
| 98 | } | ||
| 99 | |||
| 100 | private function getLockTime(string $filename): int|false | ||
| 101 | { | ||
| 102 | return @fileatime($filename); | ||
| 103 | } | ||
| 104 | |||
| 105 | public function getName(): string | ||
| 106 | { | ||
| 107 | return $this->region->getName(); | ||
| 108 | } | ||
| 109 | |||
| 110 | public function contains(CacheKey $key): bool | ||
| 111 | { | ||
| 112 | if ($this->isLocked($key)) { | ||
| 113 | return false; | ||
| 114 | } | ||
| 115 | |||
| 116 | return $this->region->contains($key); | ||
| 117 | } | ||
| 118 | |||
| 119 | public function get(CacheKey $key): CacheEntry|null | ||
| 120 | { | ||
| 121 | if ($this->isLocked($key)) { | ||
| 122 | return null; | ||
| 123 | } | ||
| 124 | |||
| 125 | return $this->region->get($key); | ||
| 126 | } | ||
| 127 | |||
| 128 | public function getMultiple(CollectionCacheEntry $collection): array|null | ||
| 129 | { | ||
| 130 | if (array_filter(array_map($this->isLocked(...), $collection->identifiers))) { | ||
| 131 | return null; | ||
| 132 | } | ||
| 133 | |||
| 134 | return $this->region->getMultiple($collection); | ||
| 135 | } | ||
| 136 | |||
| 137 | public function put(CacheKey $key, CacheEntry $entry, Lock|null $lock = null): bool | ||
| 138 | { | ||
| 139 | if ($this->isLocked($key, $lock)) { | ||
| 140 | return false; | ||
| 141 | } | ||
| 142 | |||
| 143 | return $this->region->put($key, $entry); | ||
| 144 | } | ||
| 145 | |||
| 146 | public function evict(CacheKey $key): bool | ||
| 147 | { | ||
| 148 | if ($this->isLocked($key)) { | ||
| 149 | @unlink($this->getLockFileName($key)); | ||
| 150 | } | ||
| 151 | |||
| 152 | return $this->region->evict($key); | ||
| 153 | } | ||
| 154 | |||
| 155 | public function evictAll(): bool | ||
| 156 | { | ||
| 157 | // The check below is necessary because on some platforms glob returns false | ||
| 158 | // when nothing matched (even though no errors occurred) | ||
| 159 | $filenames = glob(sprintf('%s/*.%s', $this->directory, self::LOCK_EXTENSION)) ?: []; | ||
| 160 | |||
| 161 | foreach ($filenames as $filename) { | ||
| 162 | @unlink($filename); | ||
| 163 | } | ||
| 164 | |||
| 165 | return $this->region->evictAll(); | ||
| 166 | } | ||
| 167 | |||
| 168 | public function lock(CacheKey $key): Lock|null | ||
| 169 | { | ||
| 170 | if ($this->isLocked($key)) { | ||
| 171 | return null; | ||
| 172 | } | ||
| 173 | |||
| 174 | $lock = Lock::createLockRead(); | ||
| 175 | $filename = $this->getLockFileName($key); | ||
| 176 | |||
| 177 | if (@file_put_contents($filename, $lock->value, LOCK_EX) === false) { | ||
| 178 | return null; | ||
| 179 | } | ||
| 180 | |||
| 181 | chmod($filename, 0664); | ||
| 182 | |||
| 183 | return $lock; | ||
| 184 | } | ||
| 185 | |||
| 186 | public function unlock(CacheKey $key, Lock $lock): bool | ||
| 187 | { | ||
| 188 | if ($this->isLocked($key, $lock)) { | ||
| 189 | return false; | ||
| 190 | } | ||
| 191 | |||
| 192 | return @unlink($this->getLockFileName($key)); | ||
| 193 | } | ||
| 194 | } | ||
diff --git a/vendor/doctrine/orm/src/Cache/Region/UpdateTimestampCache.php b/vendor/doctrine/orm/src/Cache/Region/UpdateTimestampCache.php new file mode 100644 index 0000000..aa75a90 --- /dev/null +++ b/vendor/doctrine/orm/src/Cache/Region/UpdateTimestampCache.php | |||
| @@ -0,0 +1,20 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Cache\Region; | ||
| 6 | |||
| 7 | use Doctrine\ORM\Cache\CacheKey; | ||
| 8 | use Doctrine\ORM\Cache\TimestampCacheEntry; | ||
| 9 | use Doctrine\ORM\Cache\TimestampRegion; | ||
| 10 | |||
| 11 | /** | ||
| 12 | * Tracks the timestamps of the most recent updates to particular keys. | ||
| 13 | */ | ||
| 14 | class UpdateTimestampCache extends DefaultRegion implements TimestampRegion | ||
| 15 | { | ||
| 16 | public function update(CacheKey $key): void | ||
| 17 | { | ||
| 18 | $this->put($key, new TimestampCacheEntry()); | ||
| 19 | } | ||
| 20 | } | ||
diff --git a/vendor/doctrine/orm/src/Cache/RegionsConfiguration.php b/vendor/doctrine/orm/src/Cache/RegionsConfiguration.php new file mode 100644 index 0000000..a852831 --- /dev/null +++ b/vendor/doctrine/orm/src/Cache/RegionsConfiguration.php | |||
| @@ -0,0 +1,63 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Cache; | ||
| 6 | |||
| 7 | /** | ||
| 8 | * Cache regions configuration | ||
| 9 | */ | ||
| 10 | class RegionsConfiguration | ||
| 11 | { | ||
| 12 | /** @var array<string,int> */ | ||
| 13 | private array $lifetimes = []; | ||
| 14 | |||
| 15 | /** @var array<string,int> */ | ||
| 16 | private array $lockLifetimes = []; | ||
| 17 | |||
| 18 | public function __construct( | ||
| 19 | private int $defaultLifetime = 3600, | ||
| 20 | private int $defaultLockLifetime = 60, | ||
| 21 | ) { | ||
| 22 | } | ||
| 23 | |||
| 24 | public function getDefaultLifetime(): int | ||
| 25 | { | ||
| 26 | return $this->defaultLifetime; | ||
| 27 | } | ||
| 28 | |||
| 29 | public function setDefaultLifetime(int $defaultLifetime): void | ||
| 30 | { | ||
| 31 | $this->defaultLifetime = $defaultLifetime; | ||
| 32 | } | ||
| 33 | |||
| 34 | public function getDefaultLockLifetime(): int | ||
| 35 | { | ||
| 36 | return $this->defaultLockLifetime; | ||
| 37 | } | ||
| 38 | |||
| 39 | public function setDefaultLockLifetime(int $defaultLockLifetime): void | ||
| 40 | { | ||
| 41 | $this->defaultLockLifetime = $defaultLockLifetime; | ||
| 42 | } | ||
| 43 | |||
| 44 | public function getLifetime(string $regionName): int | ||
| 45 | { | ||
| 46 | return $this->lifetimes[$regionName] ?? $this->defaultLifetime; | ||
| 47 | } | ||
| 48 | |||
| 49 | public function setLifetime(string $name, int $lifetime): void | ||
| 50 | { | ||
| 51 | $this->lifetimes[$name] = $lifetime; | ||
| 52 | } | ||
| 53 | |||
| 54 | public function getLockLifetime(string $regionName): int | ||
| 55 | { | ||
| 56 | return $this->lockLifetimes[$regionName] ?? $this->defaultLockLifetime; | ||
| 57 | } | ||
| 58 | |||
| 59 | public function setLockLifetime(string $name, int $lifetime): void | ||
| 60 | { | ||
| 61 | $this->lockLifetimes[$name] = $lifetime; | ||
| 62 | } | ||
| 63 | } | ||
diff --git a/vendor/doctrine/orm/src/Cache/TimestampCacheEntry.php b/vendor/doctrine/orm/src/Cache/TimestampCacheEntry.php new file mode 100644 index 0000000..60c9175 --- /dev/null +++ b/vendor/doctrine/orm/src/Cache/TimestampCacheEntry.php | |||
| @@ -0,0 +1,29 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Cache; | ||
| 6 | |||
| 7 | use function microtime; | ||
| 8 | |||
| 9 | class TimestampCacheEntry implements CacheEntry | ||
| 10 | { | ||
| 11 | public readonly float $time; | ||
| 12 | |||
| 13 | public function __construct(float|null $time = null) | ||
| 14 | { | ||
| 15 | $this->time = $time ?? microtime(true); | ||
| 16 | } | ||
| 17 | |||
| 18 | /** | ||
| 19 | * Creates a new TimestampCacheEntry | ||
| 20 | * | ||
| 21 | * This method allow Doctrine\Common\Cache\PhpFileCache compatibility | ||
| 22 | * | ||
| 23 | * @param array<string,float> $values array containing property values | ||
| 24 | */ | ||
| 25 | public static function __set_state(array $values): TimestampCacheEntry | ||
| 26 | { | ||
| 27 | return new self($values['time']); | ||
| 28 | } | ||
| 29 | } | ||
diff --git a/vendor/doctrine/orm/src/Cache/TimestampCacheKey.php b/vendor/doctrine/orm/src/Cache/TimestampCacheKey.php new file mode 100644 index 0000000..5aef4c5 --- /dev/null +++ b/vendor/doctrine/orm/src/Cache/TimestampCacheKey.php | |||
| @@ -0,0 +1,17 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Cache; | ||
| 6 | |||
| 7 | /** | ||
| 8 | * A key that identifies a timestamped space. | ||
| 9 | */ | ||
| 10 | class TimestampCacheKey extends CacheKey | ||
| 11 | { | ||
| 12 | /** @param string $space Result cache id */ | ||
| 13 | public function __construct(string $space) | ||
| 14 | { | ||
| 15 | parent::__construct($space); | ||
| 16 | } | ||
| 17 | } | ||
diff --git a/vendor/doctrine/orm/src/Cache/TimestampQueryCacheValidator.php b/vendor/doctrine/orm/src/Cache/TimestampQueryCacheValidator.php new file mode 100644 index 0000000..9824088 --- /dev/null +++ b/vendor/doctrine/orm/src/Cache/TimestampQueryCacheValidator.php | |||
| @@ -0,0 +1,38 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Cache; | ||
| 6 | |||
| 7 | use function microtime; | ||
| 8 | |||
| 9 | class TimestampQueryCacheValidator implements QueryCacheValidator | ||
| 10 | { | ||
| 11 | public function __construct(private readonly TimestampRegion $timestampRegion) | ||
| 12 | { | ||
| 13 | } | ||
| 14 | |||
| 15 | public function isValid(QueryCacheKey $key, QueryCacheEntry $entry): bool | ||
| 16 | { | ||
| 17 | if ($this->regionUpdated($key, $entry)) { | ||
| 18 | return false; | ||
| 19 | } | ||
| 20 | |||
| 21 | if ($key->lifetime === 0) { | ||
| 22 | return true; | ||
| 23 | } | ||
| 24 | |||
| 25 | return $entry->time + $key->lifetime > microtime(true); | ||
| 26 | } | ||
| 27 | |||
| 28 | private function regionUpdated(QueryCacheKey $key, QueryCacheEntry $entry): bool | ||
| 29 | { | ||
| 30 | if ($key->timestampKey === null) { | ||
| 31 | return false; | ||
| 32 | } | ||
| 33 | |||
| 34 | $timestamp = $this->timestampRegion->get($key->timestampKey); | ||
| 35 | |||
| 36 | return $timestamp && $timestamp->time > $entry->time; | ||
| 37 | } | ||
| 38 | } | ||
diff --git a/vendor/doctrine/orm/src/Cache/TimestampRegion.php b/vendor/doctrine/orm/src/Cache/TimestampRegion.php new file mode 100644 index 0000000..b74fa8d --- /dev/null +++ b/vendor/doctrine/orm/src/Cache/TimestampRegion.php | |||
| @@ -0,0 +1,18 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Cache; | ||
| 6 | |||
| 7 | /** | ||
| 8 | * Defines the contract for a cache region which will specifically be used to store entity "update timestamps". | ||
| 9 | */ | ||
| 10 | interface TimestampRegion extends Region | ||
| 11 | { | ||
| 12 | /** | ||
| 13 | * Update a specific key into the cache region. | ||
| 14 | * | ||
| 15 | * @throws LockException Indicates a problem accessing the region. | ||
| 16 | */ | ||
| 17 | public function update(CacheKey $key): void; | ||
| 18 | } | ||
diff --git a/vendor/doctrine/orm/src/Configuration.php b/vendor/doctrine/orm/src/Configuration.php new file mode 100644 index 0000000..b30764e --- /dev/null +++ b/vendor/doctrine/orm/src/Configuration.php | |||
| @@ -0,0 +1,649 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM; | ||
| 6 | |||
| 7 | use Doctrine\DBAL\Platforms\AbstractPlatform; | ||
| 8 | use Doctrine\ORM\Cache\CacheConfiguration; | ||
| 9 | use Doctrine\ORM\Exception\InvalidEntityRepository; | ||
| 10 | use Doctrine\ORM\Internal\Hydration\AbstractHydrator; | ||
| 11 | use Doctrine\ORM\Mapping\ClassMetadata; | ||
| 12 | use Doctrine\ORM\Mapping\ClassMetadataFactory; | ||
| 13 | use Doctrine\ORM\Mapping\DefaultEntityListenerResolver; | ||
| 14 | use Doctrine\ORM\Mapping\DefaultNamingStrategy; | ||
| 15 | use Doctrine\ORM\Mapping\DefaultQuoteStrategy; | ||
| 16 | use Doctrine\ORM\Mapping\EntityListenerResolver; | ||
| 17 | use Doctrine\ORM\Mapping\NamingStrategy; | ||
| 18 | use Doctrine\ORM\Mapping\QuoteStrategy; | ||
| 19 | use Doctrine\ORM\Mapping\TypedFieldMapper; | ||
| 20 | use Doctrine\ORM\Proxy\ProxyFactory; | ||
| 21 | use Doctrine\ORM\Query\AST\Functions\FunctionNode; | ||
| 22 | use Doctrine\ORM\Query\Filter\SQLFilter; | ||
| 23 | use Doctrine\ORM\Repository\DefaultRepositoryFactory; | ||
| 24 | use Doctrine\ORM\Repository\RepositoryFactory; | ||
| 25 | use Doctrine\Persistence\Mapping\Driver\MappingDriver; | ||
| 26 | use LogicException; | ||
| 27 | use Psr\Cache\CacheItemPoolInterface; | ||
| 28 | |||
| 29 | use function class_exists; | ||
| 30 | use function is_a; | ||
| 31 | use function strtolower; | ||
| 32 | |||
| 33 | /** | ||
| 34 | * Configuration container for all configuration options of Doctrine. | ||
| 35 | * It combines all configuration options from DBAL & ORM. | ||
| 36 | * | ||
| 37 | * Internal note: When adding a new configuration option just write a getter/setter pair. | ||
| 38 | */ | ||
| 39 | class Configuration extends \Doctrine\DBAL\Configuration | ||
| 40 | { | ||
| 41 | /** @var mixed[] */ | ||
| 42 | protected array $attributes = []; | ||
| 43 | |||
| 44 | /** @psalm-var array<class-string<AbstractPlatform>, ClassMetadata::GENERATOR_TYPE_*> */ | ||
| 45 | private $identityGenerationPreferences = []; | ||
| 46 | |||
| 47 | /** @psalm-param array<class-string<AbstractPlatform>, ClassMetadata::GENERATOR_TYPE_*> $value */ | ||
| 48 | public function setIdentityGenerationPreferences(array $value): void | ||
| 49 | { | ||
| 50 | $this->identityGenerationPreferences = $value; | ||
| 51 | } | ||
| 52 | |||
| 53 | /** @psalm-return array<class-string<AbstractPlatform>, ClassMetadata::GENERATOR_TYPE_*> $value */ | ||
| 54 | public function getIdentityGenerationPreferences(): array | ||
| 55 | { | ||
| 56 | return $this->identityGenerationPreferences; | ||
| 57 | } | ||
| 58 | |||
| 59 | /** | ||
| 60 | * Sets the directory where Doctrine generates any necessary proxy class files. | ||
| 61 | */ | ||
| 62 | public function setProxyDir(string $dir): void | ||
| 63 | { | ||
| 64 | $this->attributes['proxyDir'] = $dir; | ||
| 65 | } | ||
| 66 | |||
| 67 | /** | ||
| 68 | * Gets the directory where Doctrine generates any necessary proxy class files. | ||
| 69 | */ | ||
| 70 | public function getProxyDir(): string|null | ||
| 71 | { | ||
| 72 | return $this->attributes['proxyDir'] ?? null; | ||
| 73 | } | ||
| 74 | |||
| 75 | /** | ||
| 76 | * Gets the strategy for automatically generating proxy classes. | ||
| 77 | * | ||
| 78 | * @return ProxyFactory::AUTOGENERATE_* | ||
| 79 | */ | ||
| 80 | public function getAutoGenerateProxyClasses(): int | ||
| 81 | { | ||
| 82 | return $this->attributes['autoGenerateProxyClasses'] ?? ProxyFactory::AUTOGENERATE_ALWAYS; | ||
| 83 | } | ||
| 84 | |||
| 85 | /** | ||
| 86 | * Sets the strategy for automatically generating proxy classes. | ||
| 87 | * | ||
| 88 | * @param bool|ProxyFactory::AUTOGENERATE_* $autoGenerate True is converted to AUTOGENERATE_ALWAYS, false to AUTOGENERATE_NEVER. | ||
| 89 | */ | ||
| 90 | public function setAutoGenerateProxyClasses(bool|int $autoGenerate): void | ||
| 91 | { | ||
| 92 | $this->attributes['autoGenerateProxyClasses'] = (int) $autoGenerate; | ||
| 93 | } | ||
| 94 | |||
| 95 | /** | ||
| 96 | * Gets the namespace where proxy classes reside. | ||
| 97 | */ | ||
| 98 | public function getProxyNamespace(): string|null | ||
| 99 | { | ||
| 100 | return $this->attributes['proxyNamespace'] ?? null; | ||
| 101 | } | ||
| 102 | |||
| 103 | /** | ||
| 104 | * Sets the namespace where proxy classes reside. | ||
| 105 | */ | ||
| 106 | public function setProxyNamespace(string $ns): void | ||
| 107 | { | ||
| 108 | $this->attributes['proxyNamespace'] = $ns; | ||
| 109 | } | ||
| 110 | |||
| 111 | /** | ||
| 112 | * Sets the cache driver implementation that is used for metadata caching. | ||
| 113 | * | ||
| 114 | * @todo Force parameter to be a Closure to ensure lazy evaluation | ||
| 115 | * (as soon as a metadata cache is in effect, the driver never needs to initialize). | ||
| 116 | */ | ||
| 117 | public function setMetadataDriverImpl(MappingDriver $driverImpl): void | ||
| 118 | { | ||
| 119 | $this->attributes['metadataDriverImpl'] = $driverImpl; | ||
| 120 | } | ||
| 121 | |||
| 122 | /** | ||
| 123 | * Sets the entity alias map. | ||
| 124 | * | ||
| 125 | * @psalm-param array<string, string> $entityNamespaces | ||
| 126 | */ | ||
| 127 | public function setEntityNamespaces(array $entityNamespaces): void | ||
| 128 | { | ||
| 129 | $this->attributes['entityNamespaces'] = $entityNamespaces; | ||
| 130 | } | ||
| 131 | |||
| 132 | /** | ||
| 133 | * Retrieves the list of registered entity namespace aliases. | ||
| 134 | * | ||
| 135 | * @psalm-return array<string, string> | ||
| 136 | */ | ||
| 137 | public function getEntityNamespaces(): array | ||
| 138 | { | ||
| 139 | return $this->attributes['entityNamespaces']; | ||
| 140 | } | ||
| 141 | |||
| 142 | /** | ||
| 143 | * Gets the cache driver implementation that is used for the mapping metadata. | ||
| 144 | */ | ||
| 145 | public function getMetadataDriverImpl(): MappingDriver|null | ||
| 146 | { | ||
| 147 | return $this->attributes['metadataDriverImpl'] ?? null; | ||
| 148 | } | ||
| 149 | |||
| 150 | /** | ||
| 151 | * Gets the cache driver implementation that is used for the query cache (SQL cache). | ||
| 152 | */ | ||
| 153 | public function getQueryCache(): CacheItemPoolInterface|null | ||
| 154 | { | ||
| 155 | return $this->attributes['queryCache'] ?? null; | ||
| 156 | } | ||
| 157 | |||
| 158 | /** | ||
| 159 | * Sets the cache driver implementation that is used for the query cache (SQL cache). | ||
| 160 | */ | ||
| 161 | public function setQueryCache(CacheItemPoolInterface $cache): void | ||
| 162 | { | ||
| 163 | $this->attributes['queryCache'] = $cache; | ||
| 164 | } | ||
| 165 | |||
| 166 | public function getHydrationCache(): CacheItemPoolInterface|null | ||
| 167 | { | ||
| 168 | return $this->attributes['hydrationCache'] ?? null; | ||
| 169 | } | ||
| 170 | |||
| 171 | public function setHydrationCache(CacheItemPoolInterface $cache): void | ||
| 172 | { | ||
| 173 | $this->attributes['hydrationCache'] = $cache; | ||
| 174 | } | ||
| 175 | |||
| 176 | public function getMetadataCache(): CacheItemPoolInterface|null | ||
| 177 | { | ||
| 178 | return $this->attributes['metadataCache'] ?? null; | ||
| 179 | } | ||
| 180 | |||
| 181 | public function setMetadataCache(CacheItemPoolInterface $cache): void | ||
| 182 | { | ||
| 183 | $this->attributes['metadataCache'] = $cache; | ||
| 184 | } | ||
| 185 | |||
| 186 | /** | ||
| 187 | * Registers a custom DQL function that produces a string value. | ||
| 188 | * Such a function can then be used in any DQL statement in any place where string | ||
| 189 | * functions are allowed. | ||
| 190 | * | ||
| 191 | * DQL function names are case-insensitive. | ||
| 192 | * | ||
| 193 | * @param class-string|callable $className Class name or a callable that returns the function. | ||
| 194 | * @psalm-param class-string<FunctionNode>|callable(string):FunctionNode $className | ||
| 195 | */ | ||
| 196 | public function addCustomStringFunction(string $name, string|callable $className): void | ||
| 197 | { | ||
| 198 | $this->attributes['customStringFunctions'][strtolower($name)] = $className; | ||
| 199 | } | ||
| 200 | |||
| 201 | /** | ||
| 202 | * Gets the implementation class name of a registered custom string DQL function. | ||
| 203 | * | ||
| 204 | * @psalm-return class-string<FunctionNode>|callable(string):FunctionNode|null | ||
| 205 | */ | ||
| 206 | public function getCustomStringFunction(string $name): string|callable|null | ||
| 207 | { | ||
| 208 | $name = strtolower($name); | ||
| 209 | |||
| 210 | return $this->attributes['customStringFunctions'][$name] ?? null; | ||
| 211 | } | ||
| 212 | |||
| 213 | /** | ||
| 214 | * Sets a map of custom DQL string functions. | ||
| 215 | * | ||
| 216 | * Keys must be function names and values the FQCN of the implementing class. | ||
| 217 | * The function names will be case-insensitive in DQL. | ||
| 218 | * | ||
| 219 | * Any previously added string functions are discarded. | ||
| 220 | * | ||
| 221 | * @psalm-param array<string, class-string<FunctionNode>|callable(string):FunctionNode> $functions The map of custom | ||
| 222 | * DQL string functions. | ||
| 223 | */ | ||
| 224 | public function setCustomStringFunctions(array $functions): void | ||
| 225 | { | ||
| 226 | foreach ($functions as $name => $className) { | ||
| 227 | $this->addCustomStringFunction($name, $className); | ||
| 228 | } | ||
| 229 | } | ||
| 230 | |||
| 231 | /** | ||
| 232 | * Registers a custom DQL function that produces a numeric value. | ||
| 233 | * Such a function can then be used in any DQL statement in any place where numeric | ||
| 234 | * functions are allowed. | ||
| 235 | * | ||
| 236 | * DQL function names are case-insensitive. | ||
| 237 | * | ||
| 238 | * @param class-string|callable $className Class name or a callable that returns the function. | ||
| 239 | * @psalm-param class-string<FunctionNode>|callable(string):FunctionNode $className | ||
| 240 | */ | ||
| 241 | public function addCustomNumericFunction(string $name, string|callable $className): void | ||
| 242 | { | ||
| 243 | $this->attributes['customNumericFunctions'][strtolower($name)] = $className; | ||
| 244 | } | ||
| 245 | |||
| 246 | /** | ||
| 247 | * Gets the implementation class name of a registered custom numeric DQL function. | ||
| 248 | * | ||
| 249 | * @psalm-return ?class-string<FunctionNode>|callable(string):FunctionNode | ||
| 250 | */ | ||
| 251 | public function getCustomNumericFunction(string $name): string|callable|null | ||
| 252 | { | ||
| 253 | $name = strtolower($name); | ||
| 254 | |||
| 255 | return $this->attributes['customNumericFunctions'][$name] ?? null; | ||
| 256 | } | ||
| 257 | |||
| 258 | /** | ||
| 259 | * Sets a map of custom DQL numeric functions. | ||
| 260 | * | ||
| 261 | * Keys must be function names and values the FQCN of the implementing class. | ||
| 262 | * The function names will be case-insensitive in DQL. | ||
| 263 | * | ||
| 264 | * Any previously added numeric functions are discarded. | ||
| 265 | * | ||
| 266 | * @psalm-param array<string, class-string> $functions The map of custom | ||
| 267 | * DQL numeric functions. | ||
| 268 | */ | ||
| 269 | public function setCustomNumericFunctions(array $functions): void | ||
| 270 | { | ||
| 271 | foreach ($functions as $name => $className) { | ||
| 272 | $this->addCustomNumericFunction($name, $className); | ||
| 273 | } | ||
| 274 | } | ||
| 275 | |||
| 276 | /** | ||
| 277 | * Registers a custom DQL function that produces a date/time value. | ||
| 278 | * Such a function can then be used in any DQL statement in any place where date/time | ||
| 279 | * functions are allowed. | ||
| 280 | * | ||
| 281 | * DQL function names are case-insensitive. | ||
| 282 | * | ||
| 283 | * @param string|callable $className Class name or a callable that returns the function. | ||
| 284 | * @psalm-param class-string<FunctionNode>|callable(string):FunctionNode $className | ||
| 285 | */ | ||
| 286 | public function addCustomDatetimeFunction(string $name, string|callable $className): void | ||
| 287 | { | ||
| 288 | $this->attributes['customDatetimeFunctions'][strtolower($name)] = $className; | ||
| 289 | } | ||
| 290 | |||
| 291 | /** | ||
| 292 | * Gets the implementation class name of a registered custom date/time DQL function. | ||
| 293 | * | ||
| 294 | * @psalm-return class-string|callable|null | ||
| 295 | */ | ||
| 296 | public function getCustomDatetimeFunction(string $name): string|callable|null | ||
| 297 | { | ||
| 298 | $name = strtolower($name); | ||
| 299 | |||
| 300 | return $this->attributes['customDatetimeFunctions'][$name] ?? null; | ||
| 301 | } | ||
| 302 | |||
| 303 | /** | ||
| 304 | * Sets a map of custom DQL date/time functions. | ||
| 305 | * | ||
| 306 | * Keys must be function names and values the FQCN of the implementing class. | ||
| 307 | * The function names will be case-insensitive in DQL. | ||
| 308 | * | ||
| 309 | * Any previously added date/time functions are discarded. | ||
| 310 | * | ||
| 311 | * @param array $functions The map of custom DQL date/time functions. | ||
| 312 | * @psalm-param array<string, class-string<FunctionNode>|callable(string):FunctionNode> $functions | ||
| 313 | */ | ||
| 314 | public function setCustomDatetimeFunctions(array $functions): void | ||
| 315 | { | ||
| 316 | foreach ($functions as $name => $className) { | ||
| 317 | $this->addCustomDatetimeFunction($name, $className); | ||
| 318 | } | ||
| 319 | } | ||
| 320 | |||
| 321 | /** | ||
| 322 | * Sets a TypedFieldMapper for php typed fields to DBAL types auto-completion. | ||
| 323 | */ | ||
| 324 | public function setTypedFieldMapper(TypedFieldMapper|null $typedFieldMapper): void | ||
| 325 | { | ||
| 326 | $this->attributes['typedFieldMapper'] = $typedFieldMapper; | ||
| 327 | } | ||
| 328 | |||
| 329 | /** | ||
| 330 | * Gets a TypedFieldMapper for php typed fields to DBAL types auto-completion. | ||
| 331 | */ | ||
| 332 | public function getTypedFieldMapper(): TypedFieldMapper|null | ||
| 333 | { | ||
| 334 | return $this->attributes['typedFieldMapper'] ?? null; | ||
| 335 | } | ||
| 336 | |||
| 337 | /** | ||
| 338 | * Sets the custom hydrator modes in one pass. | ||
| 339 | * | ||
| 340 | * @param array<string, class-string<AbstractHydrator>> $modes An array of ($modeName => $hydrator). | ||
| 341 | */ | ||
| 342 | public function setCustomHydrationModes(array $modes): void | ||
| 343 | { | ||
| 344 | $this->attributes['customHydrationModes'] = []; | ||
| 345 | |||
| 346 | foreach ($modes as $modeName => $hydrator) { | ||
| 347 | $this->addCustomHydrationMode($modeName, $hydrator); | ||
| 348 | } | ||
| 349 | } | ||
| 350 | |||
| 351 | /** | ||
| 352 | * Gets the hydrator class for the given hydration mode name. | ||
| 353 | * | ||
| 354 | * @psalm-return class-string<AbstractHydrator>|null | ||
| 355 | */ | ||
| 356 | public function getCustomHydrationMode(string $modeName): string|null | ||
| 357 | { | ||
| 358 | return $this->attributes['customHydrationModes'][$modeName] ?? null; | ||
| 359 | } | ||
| 360 | |||
| 361 | /** | ||
| 362 | * Adds a custom hydration mode. | ||
| 363 | * | ||
| 364 | * @psalm-param class-string<AbstractHydrator> $hydrator | ||
| 365 | */ | ||
| 366 | public function addCustomHydrationMode(string $modeName, string $hydrator): void | ||
| 367 | { | ||
| 368 | $this->attributes['customHydrationModes'][$modeName] = $hydrator; | ||
| 369 | } | ||
| 370 | |||
| 371 | /** | ||
| 372 | * Sets a class metadata factory. | ||
| 373 | * | ||
| 374 | * @psalm-param class-string $cmfName | ||
| 375 | */ | ||
| 376 | public function setClassMetadataFactoryName(string $cmfName): void | ||
| 377 | { | ||
| 378 | $this->attributes['classMetadataFactoryName'] = $cmfName; | ||
| 379 | } | ||
| 380 | |||
| 381 | /** @psalm-return class-string */ | ||
| 382 | public function getClassMetadataFactoryName(): string | ||
| 383 | { | ||
| 384 | if (! isset($this->attributes['classMetadataFactoryName'])) { | ||
| 385 | $this->attributes['classMetadataFactoryName'] = ClassMetadataFactory::class; | ||
| 386 | } | ||
| 387 | |||
| 388 | return $this->attributes['classMetadataFactoryName']; | ||
| 389 | } | ||
| 390 | |||
| 391 | /** | ||
| 392 | * Adds a filter to the list of possible filters. | ||
| 393 | * | ||
| 394 | * @param string $className The class name of the filter. | ||
| 395 | * @psalm-param class-string<SQLFilter> $className | ||
| 396 | */ | ||
| 397 | public function addFilter(string $name, string $className): void | ||
| 398 | { | ||
| 399 | $this->attributes['filters'][$name] = $className; | ||
| 400 | } | ||
| 401 | |||
| 402 | /** | ||
| 403 | * Gets the class name for a given filter name. | ||
| 404 | * | ||
| 405 | * @return string|null The class name of the filter, or null if it is not | ||
| 406 | * defined. | ||
| 407 | * @psalm-return class-string<SQLFilter>|null | ||
| 408 | */ | ||
| 409 | public function getFilterClassName(string $name): string|null | ||
| 410 | { | ||
| 411 | return $this->attributes['filters'][$name] ?? null; | ||
| 412 | } | ||
| 413 | |||
| 414 | /** | ||
| 415 | * Sets default repository class. | ||
| 416 | * | ||
| 417 | * @psalm-param class-string<EntityRepository> $className | ||
| 418 | * | ||
| 419 | * @throws InvalidEntityRepository If $classname is not an ObjectRepository. | ||
| 420 | */ | ||
| 421 | public function setDefaultRepositoryClassName(string $className): void | ||
| 422 | { | ||
| 423 | if (! class_exists($className) || ! is_a($className, EntityRepository::class, true)) { | ||
| 424 | throw InvalidEntityRepository::fromClassName($className); | ||
| 425 | } | ||
| 426 | |||
| 427 | $this->attributes['defaultRepositoryClassName'] = $className; | ||
| 428 | } | ||
| 429 | |||
| 430 | /** | ||
| 431 | * Get default repository class. | ||
| 432 | * | ||
| 433 | * @psalm-return class-string<EntityRepository> | ||
| 434 | */ | ||
| 435 | public function getDefaultRepositoryClassName(): string | ||
| 436 | { | ||
| 437 | return $this->attributes['defaultRepositoryClassName'] ?? EntityRepository::class; | ||
| 438 | } | ||
| 439 | |||
| 440 | /** | ||
| 441 | * Sets naming strategy. | ||
| 442 | */ | ||
| 443 | public function setNamingStrategy(NamingStrategy $namingStrategy): void | ||
| 444 | { | ||
| 445 | $this->attributes['namingStrategy'] = $namingStrategy; | ||
| 446 | } | ||
| 447 | |||
| 448 | /** | ||
| 449 | * Gets naming strategy.. | ||
| 450 | */ | ||
| 451 | public function getNamingStrategy(): NamingStrategy | ||
| 452 | { | ||
| 453 | if (! isset($this->attributes['namingStrategy'])) { | ||
| 454 | $this->attributes['namingStrategy'] = new DefaultNamingStrategy(); | ||
| 455 | } | ||
| 456 | |||
| 457 | return $this->attributes['namingStrategy']; | ||
| 458 | } | ||
| 459 | |||
| 460 | /** | ||
| 461 | * Sets quote strategy. | ||
| 462 | */ | ||
| 463 | public function setQuoteStrategy(QuoteStrategy $quoteStrategy): void | ||
| 464 | { | ||
| 465 | $this->attributes['quoteStrategy'] = $quoteStrategy; | ||
| 466 | } | ||
| 467 | |||
| 468 | /** | ||
| 469 | * Gets quote strategy. | ||
| 470 | */ | ||
| 471 | public function getQuoteStrategy(): QuoteStrategy | ||
| 472 | { | ||
| 473 | if (! isset($this->attributes['quoteStrategy'])) { | ||
| 474 | $this->attributes['quoteStrategy'] = new DefaultQuoteStrategy(); | ||
| 475 | } | ||
| 476 | |||
| 477 | return $this->attributes['quoteStrategy']; | ||
| 478 | } | ||
| 479 | |||
| 480 | /** | ||
| 481 | * Set the entity listener resolver. | ||
| 482 | */ | ||
| 483 | public function setEntityListenerResolver(EntityListenerResolver $resolver): void | ||
| 484 | { | ||
| 485 | $this->attributes['entityListenerResolver'] = $resolver; | ||
| 486 | } | ||
| 487 | |||
| 488 | /** | ||
| 489 | * Get the entity listener resolver. | ||
| 490 | */ | ||
| 491 | public function getEntityListenerResolver(): EntityListenerResolver | ||
| 492 | { | ||
| 493 | if (! isset($this->attributes['entityListenerResolver'])) { | ||
| 494 | $this->attributes['entityListenerResolver'] = new DefaultEntityListenerResolver(); | ||
| 495 | } | ||
| 496 | |||
| 497 | return $this->attributes['entityListenerResolver']; | ||
| 498 | } | ||
| 499 | |||
| 500 | /** | ||
| 501 | * Set the entity repository factory. | ||
| 502 | */ | ||
| 503 | public function setRepositoryFactory(RepositoryFactory $repositoryFactory): void | ||
| 504 | { | ||
| 505 | $this->attributes['repositoryFactory'] = $repositoryFactory; | ||
| 506 | } | ||
| 507 | |||
| 508 | /** | ||
| 509 | * Get the entity repository factory. | ||
| 510 | */ | ||
| 511 | public function getRepositoryFactory(): RepositoryFactory | ||
| 512 | { | ||
| 513 | return $this->attributes['repositoryFactory'] ?? new DefaultRepositoryFactory(); | ||
| 514 | } | ||
| 515 | |||
| 516 | public function isSecondLevelCacheEnabled(): bool | ||
| 517 | { | ||
| 518 | return $this->attributes['isSecondLevelCacheEnabled'] ?? false; | ||
| 519 | } | ||
| 520 | |||
| 521 | public function setSecondLevelCacheEnabled(bool $flag = true): void | ||
| 522 | { | ||
| 523 | $this->attributes['isSecondLevelCacheEnabled'] = $flag; | ||
| 524 | } | ||
| 525 | |||
| 526 | public function setSecondLevelCacheConfiguration(CacheConfiguration $cacheConfig): void | ||
| 527 | { | ||
| 528 | $this->attributes['secondLevelCacheConfiguration'] = $cacheConfig; | ||
| 529 | } | ||
| 530 | |||
| 531 | public function getSecondLevelCacheConfiguration(): CacheConfiguration|null | ||
| 532 | { | ||
| 533 | if (! isset($this->attributes['secondLevelCacheConfiguration']) && $this->isSecondLevelCacheEnabled()) { | ||
| 534 | $this->attributes['secondLevelCacheConfiguration'] = new CacheConfiguration(); | ||
| 535 | } | ||
| 536 | |||
| 537 | return $this->attributes['secondLevelCacheConfiguration'] ?? null; | ||
| 538 | } | ||
| 539 | |||
| 540 | /** | ||
| 541 | * Returns query hints, which will be applied to every query in application | ||
| 542 | * | ||
| 543 | * @psalm-return array<string, mixed> | ||
| 544 | */ | ||
| 545 | public function getDefaultQueryHints(): array | ||
| 546 | { | ||
| 547 | return $this->attributes['defaultQueryHints'] ?? []; | ||
| 548 | } | ||
| 549 | |||
| 550 | /** | ||
| 551 | * Sets array of query hints, which will be applied to every query in application | ||
| 552 | * | ||
| 553 | * @psalm-param array<string, mixed> $defaultQueryHints | ||
| 554 | */ | ||
| 555 | public function setDefaultQueryHints(array $defaultQueryHints): void | ||
| 556 | { | ||
| 557 | $this->attributes['defaultQueryHints'] = $defaultQueryHints; | ||
| 558 | } | ||
| 559 | |||
| 560 | /** | ||
| 561 | * Gets the value of a default query hint. If the hint name is not recognized, FALSE is returned. | ||
| 562 | * | ||
| 563 | * @return mixed The value of the hint or FALSE, if the hint name is not recognized. | ||
| 564 | */ | ||
| 565 | public function getDefaultQueryHint(string $name): mixed | ||
| 566 | { | ||
| 567 | return $this->attributes['defaultQueryHints'][$name] ?? false; | ||
| 568 | } | ||
| 569 | |||
| 570 | /** | ||
| 571 | * Sets a default query hint. If the hint name is not recognized, it is silently ignored. | ||
| 572 | */ | ||
| 573 | public function setDefaultQueryHint(string $name, mixed $value): void | ||
| 574 | { | ||
| 575 | $this->attributes['defaultQueryHints'][$name] = $value; | ||
| 576 | } | ||
| 577 | |||
| 578 | /** | ||
| 579 | * Gets a list of entity class names to be ignored by the SchemaTool | ||
| 580 | * | ||
| 581 | * @return list<class-string> | ||
| 582 | */ | ||
| 583 | public function getSchemaIgnoreClasses(): array | ||
| 584 | { | ||
| 585 | return $this->attributes['schemaIgnoreClasses'] ?? []; | ||
| 586 | } | ||
| 587 | |||
| 588 | /** | ||
| 589 | * Sets a list of entity class names to be ignored by the SchemaTool | ||
| 590 | * | ||
| 591 | * @param list<class-string> $schemaIgnoreClasses List of entity class names | ||
| 592 | */ | ||
| 593 | public function setSchemaIgnoreClasses(array $schemaIgnoreClasses): void | ||
| 594 | { | ||
| 595 | $this->attributes['schemaIgnoreClasses'] = $schemaIgnoreClasses; | ||
| 596 | } | ||
| 597 | |||
| 598 | /** | ||
| 599 | * To be deprecated in 3.1.0 | ||
| 600 | * | ||
| 601 | * @return true | ||
| 602 | */ | ||
| 603 | public function isLazyGhostObjectEnabled(): bool | ||
| 604 | { | ||
| 605 | return true; | ||
| 606 | } | ||
| 607 | |||
| 608 | /** To be deprecated in 3.1.0 */ | ||
| 609 | public function setLazyGhostObjectEnabled(bool $flag): void | ||
| 610 | { | ||
| 611 | if (! $flag) { | ||
| 612 | throw new LogicException(<<<'EXCEPTION' | ||
| 613 | The lazy ghost object feature cannot be disabled anymore. | ||
| 614 | Please remove the call to setLazyGhostObjectEnabled(false). | ||
| 615 | EXCEPTION); | ||
| 616 | } | ||
| 617 | } | ||
| 618 | |||
| 619 | /** To be deprecated in 3.1.0 */ | ||
| 620 | public function setRejectIdCollisionInIdentityMap(bool $flag): void | ||
| 621 | { | ||
| 622 | if (! $flag) { | ||
| 623 | throw new LogicException(<<<'EXCEPTION' | ||
| 624 | Rejecting ID collisions in the identity map cannot be disabled anymore. | ||
| 625 | Please remove the call to setRejectIdCollisionInIdentityMap(false). | ||
| 626 | EXCEPTION); | ||
| 627 | } | ||
| 628 | } | ||
| 629 | |||
| 630 | /** | ||
| 631 | * To be deprecated in 3.1.0 | ||
| 632 | * | ||
| 633 | * @return true | ||
| 634 | */ | ||
| 635 | public function isRejectIdCollisionInIdentityMapEnabled(): bool | ||
| 636 | { | ||
| 637 | return true; | ||
| 638 | } | ||
| 639 | |||
| 640 | public function setEagerFetchBatchSize(int $batchSize = 100): void | ||
| 641 | { | ||
| 642 | $this->attributes['fetchModeSubselectBatchSize'] = $batchSize; | ||
| 643 | } | ||
| 644 | |||
| 645 | public function getEagerFetchBatchSize(): int | ||
| 646 | { | ||
| 647 | return $this->attributes['fetchModeSubselectBatchSize'] ?? 100; | ||
| 648 | } | ||
| 649 | } | ||
diff --git a/vendor/doctrine/orm/src/Decorator/EntityManagerDecorator.php b/vendor/doctrine/orm/src/Decorator/EntityManagerDecorator.php new file mode 100644 index 0000000..6f1b041 --- /dev/null +++ b/vendor/doctrine/orm/src/Decorator/EntityManagerDecorator.php | |||
| @@ -0,0 +1,174 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Decorator; | ||
| 6 | |||
| 7 | use DateTimeInterface; | ||
| 8 | use Doctrine\Common\EventManager; | ||
| 9 | use Doctrine\DBAL\Connection; | ||
| 10 | use Doctrine\DBAL\LockMode; | ||
| 11 | use Doctrine\ORM\Cache; | ||
| 12 | use Doctrine\ORM\Configuration; | ||
| 13 | use Doctrine\ORM\EntityManagerInterface; | ||
| 14 | use Doctrine\ORM\EntityRepository; | ||
| 15 | use Doctrine\ORM\Internal\Hydration\AbstractHydrator; | ||
| 16 | use Doctrine\ORM\Mapping\ClassMetadata; | ||
| 17 | use Doctrine\ORM\Mapping\ClassMetadataFactory; | ||
| 18 | use Doctrine\ORM\NativeQuery; | ||
| 19 | use Doctrine\ORM\Proxy\ProxyFactory; | ||
| 20 | use Doctrine\ORM\Query; | ||
| 21 | use Doctrine\ORM\Query\Expr; | ||
| 22 | use Doctrine\ORM\Query\FilterCollection; | ||
| 23 | use Doctrine\ORM\Query\ResultSetMapping; | ||
| 24 | use Doctrine\ORM\QueryBuilder; | ||
| 25 | use Doctrine\ORM\UnitOfWork; | ||
| 26 | use Doctrine\Persistence\ObjectManagerDecorator; | ||
| 27 | |||
| 28 | /** | ||
| 29 | * Base class for EntityManager decorators | ||
| 30 | * | ||
| 31 | * @extends ObjectManagerDecorator<EntityManagerInterface> | ||
| 32 | */ | ||
| 33 | abstract class EntityManagerDecorator extends ObjectManagerDecorator implements EntityManagerInterface | ||
| 34 | { | ||
| 35 | public function __construct(EntityManagerInterface $wrapped) | ||
| 36 | { | ||
| 37 | $this->wrapped = $wrapped; | ||
| 38 | } | ||
| 39 | |||
| 40 | public function getRepository(string $className): EntityRepository | ||
| 41 | { | ||
| 42 | return $this->wrapped->getRepository($className); | ||
| 43 | } | ||
| 44 | |||
| 45 | public function getMetadataFactory(): ClassMetadataFactory | ||
| 46 | { | ||
| 47 | return $this->wrapped->getMetadataFactory(); | ||
| 48 | } | ||
| 49 | |||
| 50 | public function getClassMetadata(string $className): ClassMetadata | ||
| 51 | { | ||
| 52 | return $this->wrapped->getClassMetadata($className); | ||
| 53 | } | ||
| 54 | |||
| 55 | public function getConnection(): Connection | ||
| 56 | { | ||
| 57 | return $this->wrapped->getConnection(); | ||
| 58 | } | ||
| 59 | |||
| 60 | public function getExpressionBuilder(): Expr | ||
| 61 | { | ||
| 62 | return $this->wrapped->getExpressionBuilder(); | ||
| 63 | } | ||
| 64 | |||
| 65 | public function beginTransaction(): void | ||
| 66 | { | ||
| 67 | $this->wrapped->beginTransaction(); | ||
| 68 | } | ||
| 69 | |||
| 70 | public function wrapInTransaction(callable $func): mixed | ||
| 71 | { | ||
| 72 | return $this->wrapped->wrapInTransaction($func); | ||
| 73 | } | ||
| 74 | |||
| 75 | public function commit(): void | ||
| 76 | { | ||
| 77 | $this->wrapped->commit(); | ||
| 78 | } | ||
| 79 | |||
| 80 | public function rollback(): void | ||
| 81 | { | ||
| 82 | $this->wrapped->rollback(); | ||
| 83 | } | ||
| 84 | |||
| 85 | public function createQuery(string $dql = ''): Query | ||
| 86 | { | ||
| 87 | return $this->wrapped->createQuery($dql); | ||
| 88 | } | ||
| 89 | |||
| 90 | public function createNativeQuery(string $sql, ResultSetMapping $rsm): NativeQuery | ||
| 91 | { | ||
| 92 | return $this->wrapped->createNativeQuery($sql, $rsm); | ||
| 93 | } | ||
| 94 | |||
| 95 | public function createQueryBuilder(): QueryBuilder | ||
| 96 | { | ||
| 97 | return $this->wrapped->createQueryBuilder(); | ||
| 98 | } | ||
| 99 | |||
| 100 | public function getReference(string $entityName, mixed $id): object|null | ||
| 101 | { | ||
| 102 | return $this->wrapped->getReference($entityName, $id); | ||
| 103 | } | ||
| 104 | |||
| 105 | public function close(): void | ||
| 106 | { | ||
| 107 | $this->wrapped->close(); | ||
| 108 | } | ||
| 109 | |||
| 110 | public function lock(object $entity, LockMode|int $lockMode, DateTimeInterface|int|null $lockVersion = null): void | ||
| 111 | { | ||
| 112 | $this->wrapped->lock($entity, $lockMode, $lockVersion); | ||
| 113 | } | ||
| 114 | |||
| 115 | public function find(string $className, mixed $id, LockMode|int|null $lockMode = null, int|null $lockVersion = null): object|null | ||
| 116 | { | ||
| 117 | return $this->wrapped->find($className, $id, $lockMode, $lockVersion); | ||
| 118 | } | ||
| 119 | |||
| 120 | public function refresh(object $object, LockMode|int|null $lockMode = null): void | ||
| 121 | { | ||
| 122 | $this->wrapped->refresh($object, $lockMode); | ||
| 123 | } | ||
| 124 | |||
| 125 | public function getEventManager(): EventManager | ||
| 126 | { | ||
| 127 | return $this->wrapped->getEventManager(); | ||
| 128 | } | ||
| 129 | |||
| 130 | public function getConfiguration(): Configuration | ||
| 131 | { | ||
| 132 | return $this->wrapped->getConfiguration(); | ||
| 133 | } | ||
| 134 | |||
| 135 | public function isOpen(): bool | ||
| 136 | { | ||
| 137 | return $this->wrapped->isOpen(); | ||
| 138 | } | ||
| 139 | |||
| 140 | public function getUnitOfWork(): UnitOfWork | ||
| 141 | { | ||
| 142 | return $this->wrapped->getUnitOfWork(); | ||
| 143 | } | ||
| 144 | |||
| 145 | public function newHydrator(string|int $hydrationMode): AbstractHydrator | ||
| 146 | { | ||
| 147 | return $this->wrapped->newHydrator($hydrationMode); | ||
| 148 | } | ||
| 149 | |||
| 150 | public function getProxyFactory(): ProxyFactory | ||
| 151 | { | ||
| 152 | return $this->wrapped->getProxyFactory(); | ||
| 153 | } | ||
| 154 | |||
| 155 | public function getFilters(): FilterCollection | ||
| 156 | { | ||
| 157 | return $this->wrapped->getFilters(); | ||
| 158 | } | ||
| 159 | |||
| 160 | public function isFiltersStateClean(): bool | ||
| 161 | { | ||
| 162 | return $this->wrapped->isFiltersStateClean(); | ||
| 163 | } | ||
| 164 | |||
| 165 | public function hasFilters(): bool | ||
| 166 | { | ||
| 167 | return $this->wrapped->hasFilters(); | ||
| 168 | } | ||
| 169 | |||
| 170 | public function getCache(): Cache|null | ||
| 171 | { | ||
| 172 | return $this->wrapped->getCache(); | ||
| 173 | } | ||
| 174 | } | ||
diff --git a/vendor/doctrine/orm/src/EntityManager.php b/vendor/doctrine/orm/src/EntityManager.php new file mode 100644 index 0000000..8045ac2 --- /dev/null +++ b/vendor/doctrine/orm/src/EntityManager.php | |||
| @@ -0,0 +1,626 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM; | ||
| 6 | |||
| 7 | use BackedEnum; | ||
| 8 | use DateTimeInterface; | ||
| 9 | use Doctrine\Common\EventManager; | ||
| 10 | use Doctrine\DBAL\Connection; | ||
| 11 | use Doctrine\DBAL\LockMode; | ||
| 12 | use Doctrine\ORM\Exception\EntityManagerClosed; | ||
| 13 | use Doctrine\ORM\Exception\InvalidHydrationMode; | ||
| 14 | use Doctrine\ORM\Exception\MissingIdentifierField; | ||
| 15 | use Doctrine\ORM\Exception\MissingMappingDriverImplementation; | ||
| 16 | use Doctrine\ORM\Exception\ORMException; | ||
| 17 | use Doctrine\ORM\Exception\UnrecognizedIdentifierFields; | ||
| 18 | use Doctrine\ORM\Internal\Hydration\AbstractHydrator; | ||
| 19 | use Doctrine\ORM\Mapping\ClassMetadata; | ||
| 20 | use Doctrine\ORM\Mapping\ClassMetadataFactory; | ||
| 21 | use Doctrine\ORM\Proxy\DefaultProxyClassNameResolver; | ||
| 22 | use Doctrine\ORM\Proxy\ProxyFactory; | ||
| 23 | use Doctrine\ORM\Query\Expr; | ||
| 24 | use Doctrine\ORM\Query\FilterCollection; | ||
| 25 | use Doctrine\ORM\Query\ResultSetMapping; | ||
| 26 | use Doctrine\ORM\Repository\RepositoryFactory; | ||
| 27 | use Throwable; | ||
| 28 | |||
| 29 | use function array_keys; | ||
| 30 | use function is_array; | ||
| 31 | use function is_object; | ||
| 32 | use function ltrim; | ||
| 33 | use function method_exists; | ||
| 34 | |||
| 35 | /** | ||
| 36 | * The EntityManager is the central access point to ORM functionality. | ||
| 37 | * | ||
| 38 | * It is a facade to all different ORM subsystems such as UnitOfWork, | ||
| 39 | * Query Language and Repository API. The quickest way to obtain a fully | ||
| 40 | * configured EntityManager is: | ||
| 41 | * | ||
| 42 | * use Doctrine\ORM\Tools\ORMSetup; | ||
| 43 | * use Doctrine\ORM\EntityManager; | ||
| 44 | * | ||
| 45 | * $paths = ['/path/to/entity/mapping/files']; | ||
| 46 | * | ||
| 47 | * $config = ORMSetup::createAttributeMetadataConfiguration($paths); | ||
| 48 | * $connection = DriverManager::getConnection(['driver' => 'pdo_sqlite', 'memory' => true], $config); | ||
| 49 | * $entityManager = new EntityManager($connection, $config); | ||
| 50 | * | ||
| 51 | * For more information see | ||
| 52 | * {@link http://docs.doctrine-project.org/projects/doctrine-orm/en/stable/reference/configuration.html} | ||
| 53 | * | ||
| 54 | * You should never attempt to inherit from the EntityManager: Inheritance | ||
| 55 | * is not a valid extension point for the EntityManager. Instead you | ||
| 56 | * should take a look at the {@see \Doctrine\ORM\Decorator\EntityManagerDecorator} | ||
| 57 | * and wrap your entity manager in a decorator. | ||
| 58 | * | ||
| 59 | * @final | ||
| 60 | */ | ||
| 61 | class EntityManager implements EntityManagerInterface | ||
| 62 | { | ||
| 63 | /** | ||
| 64 | * The metadata factory, used to retrieve the ORM metadata of entity classes. | ||
| 65 | */ | ||
| 66 | private ClassMetadataFactory $metadataFactory; | ||
| 67 | |||
| 68 | /** | ||
| 69 | * The UnitOfWork used to coordinate object-level transactions. | ||
| 70 | */ | ||
| 71 | private UnitOfWork $unitOfWork; | ||
| 72 | |||
| 73 | /** | ||
| 74 | * The event manager that is the central point of the event system. | ||
| 75 | */ | ||
| 76 | private EventManager $eventManager; | ||
| 77 | |||
| 78 | /** | ||
| 79 | * The proxy factory used to create dynamic proxies. | ||
| 80 | */ | ||
| 81 | private ProxyFactory $proxyFactory; | ||
| 82 | |||
| 83 | /** | ||
| 84 | * The repository factory used to create dynamic repositories. | ||
| 85 | */ | ||
| 86 | private RepositoryFactory $repositoryFactory; | ||
| 87 | |||
| 88 | /** | ||
| 89 | * The expression builder instance used to generate query expressions. | ||
| 90 | */ | ||
| 91 | private Expr|null $expressionBuilder = null; | ||
| 92 | |||
| 93 | /** | ||
| 94 | * Whether the EntityManager is closed or not. | ||
| 95 | */ | ||
| 96 | private bool $closed = false; | ||
| 97 | |||
| 98 | /** | ||
| 99 | * Collection of query filters. | ||
| 100 | */ | ||
| 101 | private FilterCollection|null $filterCollection = null; | ||
| 102 | |||
| 103 | /** | ||
| 104 | * The second level cache regions API. | ||
| 105 | */ | ||
| 106 | private Cache|null $cache = null; | ||
| 107 | |||
| 108 | /** | ||
| 109 | * Creates a new EntityManager that operates on the given database connection | ||
| 110 | * and uses the given Configuration and EventManager implementations. | ||
| 111 | * | ||
| 112 | * @param Connection $conn The database connection used by the EntityManager. | ||
| 113 | */ | ||
| 114 | public function __construct( | ||
| 115 | private Connection $conn, | ||
| 116 | private Configuration $config, | ||
| 117 | EventManager|null $eventManager = null, | ||
| 118 | ) { | ||
| 119 | if (! $config->getMetadataDriverImpl()) { | ||
| 120 | throw MissingMappingDriverImplementation::create(); | ||
| 121 | } | ||
| 122 | |||
| 123 | $this->eventManager = $eventManager | ||
| 124 | ?? (method_exists($conn, 'getEventManager') | ||
| 125 | ? $conn->getEventManager() | ||
| 126 | : new EventManager() | ||
| 127 | ); | ||
| 128 | |||
| 129 | $metadataFactoryClassName = $config->getClassMetadataFactoryName(); | ||
| 130 | |||
| 131 | $this->metadataFactory = new $metadataFactoryClassName(); | ||
| 132 | $this->metadataFactory->setEntityManager($this); | ||
| 133 | |||
| 134 | $this->configureMetadataCache(); | ||
| 135 | |||
| 136 | $this->repositoryFactory = $config->getRepositoryFactory(); | ||
| 137 | $this->unitOfWork = new UnitOfWork($this); | ||
| 138 | $this->proxyFactory = new ProxyFactory( | ||
| 139 | $this, | ||
| 140 | $config->getProxyDir(), | ||
| 141 | $config->getProxyNamespace(), | ||
| 142 | $config->getAutoGenerateProxyClasses(), | ||
| 143 | ); | ||
| 144 | |||
| 145 | if ($config->isSecondLevelCacheEnabled()) { | ||
| 146 | $cacheConfig = $config->getSecondLevelCacheConfiguration(); | ||
| 147 | $cacheFactory = $cacheConfig->getCacheFactory(); | ||
| 148 | $this->cache = $cacheFactory->createCache($this); | ||
| 149 | } | ||
| 150 | } | ||
| 151 | |||
| 152 | public function getConnection(): Connection | ||
| 153 | { | ||
| 154 | return $this->conn; | ||
| 155 | } | ||
| 156 | |||
| 157 | public function getMetadataFactory(): ClassMetadataFactory | ||
| 158 | { | ||
| 159 | return $this->metadataFactory; | ||
| 160 | } | ||
| 161 | |||
| 162 | public function getExpressionBuilder(): Expr | ||
| 163 | { | ||
| 164 | return $this->expressionBuilder ??= new Expr(); | ||
| 165 | } | ||
| 166 | |||
| 167 | public function beginTransaction(): void | ||
| 168 | { | ||
| 169 | $this->conn->beginTransaction(); | ||
| 170 | } | ||
| 171 | |||
| 172 | public function getCache(): Cache|null | ||
| 173 | { | ||
| 174 | return $this->cache; | ||
| 175 | } | ||
| 176 | |||
| 177 | public function wrapInTransaction(callable $func): mixed | ||
| 178 | { | ||
| 179 | $this->conn->beginTransaction(); | ||
| 180 | |||
| 181 | try { | ||
| 182 | $return = $func($this); | ||
| 183 | |||
| 184 | $this->flush(); | ||
| 185 | $this->conn->commit(); | ||
| 186 | |||
| 187 | return $return; | ||
| 188 | } catch (Throwable $e) { | ||
| 189 | $this->close(); | ||
| 190 | $this->conn->rollBack(); | ||
| 191 | |||
| 192 | throw $e; | ||
| 193 | } | ||
| 194 | } | ||
| 195 | |||
| 196 | public function commit(): void | ||
| 197 | { | ||
| 198 | $this->conn->commit(); | ||
| 199 | } | ||
| 200 | |||
| 201 | public function rollback(): void | ||
| 202 | { | ||
| 203 | $this->conn->rollBack(); | ||
| 204 | } | ||
| 205 | |||
| 206 | /** | ||
| 207 | * Returns the ORM metadata descriptor for a class. | ||
| 208 | * | ||
| 209 | * Internal note: Performance-sensitive method. | ||
| 210 | * | ||
| 211 | * {@inheritDoc} | ||
| 212 | */ | ||
| 213 | public function getClassMetadata(string $className): Mapping\ClassMetadata | ||
| 214 | { | ||
| 215 | return $this->metadataFactory->getMetadataFor($className); | ||
| 216 | } | ||
| 217 | |||
| 218 | public function createQuery(string $dql = ''): Query | ||
| 219 | { | ||
| 220 | $query = new Query($this); | ||
| 221 | |||
| 222 | if (! empty($dql)) { | ||
| 223 | $query->setDQL($dql); | ||
| 224 | } | ||
| 225 | |||
| 226 | return $query; | ||
| 227 | } | ||
| 228 | |||
| 229 | public function createNativeQuery(string $sql, ResultSetMapping $rsm): NativeQuery | ||
| 230 | { | ||
| 231 | $query = new NativeQuery($this); | ||
| 232 | |||
| 233 | $query->setSQL($sql); | ||
| 234 | $query->setResultSetMapping($rsm); | ||
| 235 | |||
| 236 | return $query; | ||
| 237 | } | ||
| 238 | |||
| 239 | public function createQueryBuilder(): QueryBuilder | ||
| 240 | { | ||
| 241 | return new QueryBuilder($this); | ||
| 242 | } | ||
| 243 | |||
| 244 | /** | ||
| 245 | * Flushes all changes to objects that have been queued up to now to the database. | ||
| 246 | * This effectively synchronizes the in-memory state of managed objects with the | ||
| 247 | * database. | ||
| 248 | * | ||
| 249 | * If an entity is explicitly passed to this method only this entity and | ||
| 250 | * the cascade-persist semantics + scheduled inserts/removals are synchronized. | ||
| 251 | * | ||
| 252 | * @throws OptimisticLockException If a version check on an entity that | ||
| 253 | * makes use of optimistic locking fails. | ||
| 254 | * @throws ORMException | ||
| 255 | */ | ||
| 256 | public function flush(): void | ||
| 257 | { | ||
| 258 | $this->errorIfClosed(); | ||
| 259 | $this->unitOfWork->commit(); | ||
| 260 | } | ||
| 261 | |||
| 262 | /** | ||
| 263 | * {@inheritDoc} | ||
| 264 | */ | ||
| 265 | public function find($className, mixed $id, LockMode|int|null $lockMode = null, int|null $lockVersion = null): object|null | ||
| 266 | { | ||
| 267 | $class = $this->metadataFactory->getMetadataFor(ltrim($className, '\\')); | ||
| 268 | |||
| 269 | if ($lockMode !== null) { | ||
| 270 | $this->checkLockRequirements($lockMode, $class); | ||
| 271 | } | ||
| 272 | |||
| 273 | if (! is_array($id)) { | ||
| 274 | if ($class->isIdentifierComposite) { | ||
| 275 | throw ORMInvalidArgumentException::invalidCompositeIdentifier(); | ||
| 276 | } | ||
| 277 | |||
| 278 | $id = [$class->identifier[0] => $id]; | ||
| 279 | } | ||
| 280 | |||
| 281 | foreach ($id as $i => $value) { | ||
| 282 | if (is_object($value)) { | ||
| 283 | $className = DefaultProxyClassNameResolver::getClass($value); | ||
| 284 | if ($this->metadataFactory->hasMetadataFor($className)) { | ||
| 285 | $id[$i] = $this->unitOfWork->getSingleIdentifierValue($value); | ||
| 286 | |||
| 287 | if ($id[$i] === null) { | ||
| 288 | throw ORMInvalidArgumentException::invalidIdentifierBindingEntity($className); | ||
| 289 | } | ||
| 290 | } | ||
| 291 | } | ||
| 292 | } | ||
| 293 | |||
| 294 | $sortedId = []; | ||
| 295 | |||
| 296 | foreach ($class->identifier as $identifier) { | ||
| 297 | if (! isset($id[$identifier])) { | ||
| 298 | throw MissingIdentifierField::fromFieldAndClass($identifier, $class->name); | ||
| 299 | } | ||
| 300 | |||
| 301 | if ($id[$identifier] instanceof BackedEnum) { | ||
| 302 | $sortedId[$identifier] = $id[$identifier]->value; | ||
| 303 | } else { | ||
| 304 | $sortedId[$identifier] = $id[$identifier]; | ||
| 305 | } | ||
| 306 | |||
| 307 | unset($id[$identifier]); | ||
| 308 | } | ||
| 309 | |||
| 310 | if ($id) { | ||
| 311 | throw UnrecognizedIdentifierFields::fromClassAndFieldNames($class->name, array_keys($id)); | ||
| 312 | } | ||
| 313 | |||
| 314 | $unitOfWork = $this->getUnitOfWork(); | ||
| 315 | |||
| 316 | $entity = $unitOfWork->tryGetById($sortedId, $class->rootEntityName); | ||
| 317 | |||
| 318 | // Check identity map first | ||
| 319 | if ($entity !== false) { | ||
| 320 | if (! ($entity instanceof $class->name)) { | ||
| 321 | return null; | ||
| 322 | } | ||
| 323 | |||
| 324 | switch (true) { | ||
| 325 | case $lockMode === LockMode::OPTIMISTIC: | ||
| 326 | $this->lock($entity, $lockMode, $lockVersion); | ||
| 327 | break; | ||
| 328 | |||
| 329 | case $lockMode === LockMode::NONE: | ||
| 330 | case $lockMode === LockMode::PESSIMISTIC_READ: | ||
| 331 | case $lockMode === LockMode::PESSIMISTIC_WRITE: | ||
| 332 | $persister = $unitOfWork->getEntityPersister($class->name); | ||
| 333 | $persister->refresh($sortedId, $entity, $lockMode); | ||
| 334 | break; | ||
| 335 | } | ||
| 336 | |||
| 337 | return $entity; // Hit! | ||
| 338 | } | ||
| 339 | |||
| 340 | $persister = $unitOfWork->getEntityPersister($class->name); | ||
| 341 | |||
| 342 | switch (true) { | ||
| 343 | case $lockMode === LockMode::OPTIMISTIC: | ||
| 344 | $entity = $persister->load($sortedId); | ||
| 345 | |||
| 346 | if ($entity !== null) { | ||
| 347 | $unitOfWork->lock($entity, $lockMode, $lockVersion); | ||
| 348 | } | ||
| 349 | |||
| 350 | return $entity; | ||
| 351 | |||
| 352 | case $lockMode === LockMode::PESSIMISTIC_READ: | ||
| 353 | case $lockMode === LockMode::PESSIMISTIC_WRITE: | ||
| 354 | return $persister->load($sortedId, null, null, [], $lockMode); | ||
| 355 | |||
| 356 | default: | ||
| 357 | return $persister->loadById($sortedId); | ||
| 358 | } | ||
| 359 | } | ||
| 360 | |||
| 361 | public function getReference(string $entityName, mixed $id): object|null | ||
| 362 | { | ||
| 363 | $class = $this->metadataFactory->getMetadataFor(ltrim($entityName, '\\')); | ||
| 364 | |||
| 365 | if (! is_array($id)) { | ||
| 366 | $id = [$class->identifier[0] => $id]; | ||
| 367 | } | ||
| 368 | |||
| 369 | $sortedId = []; | ||
| 370 | |||
| 371 | foreach ($class->identifier as $identifier) { | ||
| 372 | if (! isset($id[$identifier])) { | ||
| 373 | throw MissingIdentifierField::fromFieldAndClass($identifier, $class->name); | ||
| 374 | } | ||
| 375 | |||
| 376 | $sortedId[$identifier] = $id[$identifier]; | ||
| 377 | unset($id[$identifier]); | ||
| 378 | } | ||
| 379 | |||
| 380 | if ($id) { | ||
| 381 | throw UnrecognizedIdentifierFields::fromClassAndFieldNames($class->name, array_keys($id)); | ||
| 382 | } | ||
| 383 | |||
| 384 | $entity = $this->unitOfWork->tryGetById($sortedId, $class->rootEntityName); | ||
| 385 | |||
| 386 | // Check identity map first, if its already in there just return it. | ||
| 387 | if ($entity !== false) { | ||
| 388 | return $entity instanceof $class->name ? $entity : null; | ||
| 389 | } | ||
| 390 | |||
| 391 | if ($class->subClasses) { | ||
| 392 | return $this->find($entityName, $sortedId); | ||
| 393 | } | ||
| 394 | |||
| 395 | $entity = $this->proxyFactory->getProxy($class->name, $sortedId); | ||
| 396 | |||
| 397 | $this->unitOfWork->registerManaged($entity, $sortedId, []); | ||
| 398 | |||
| 399 | return $entity; | ||
| 400 | } | ||
| 401 | |||
| 402 | /** | ||
| 403 | * Clears the EntityManager. All entities that are currently managed | ||
| 404 | * by this EntityManager become detached. | ||
| 405 | */ | ||
| 406 | public function clear(): void | ||
| 407 | { | ||
| 408 | $this->unitOfWork->clear(); | ||
| 409 | } | ||
| 410 | |||
| 411 | public function close(): void | ||
| 412 | { | ||
| 413 | $this->clear(); | ||
| 414 | |||
| 415 | $this->closed = true; | ||
| 416 | } | ||
| 417 | |||
| 418 | /** | ||
| 419 | * Tells the EntityManager to make an instance managed and persistent. | ||
| 420 | * | ||
| 421 | * The entity will be entered into the database at or before transaction | ||
| 422 | * commit or as a result of the flush operation. | ||
| 423 | * | ||
| 424 | * NOTE: The persist operation always considers entities that are not yet known to | ||
| 425 | * this EntityManager as NEW. Do not pass detached entities to the persist operation. | ||
| 426 | * | ||
| 427 | * @throws ORMInvalidArgumentException | ||
| 428 | * @throws ORMException | ||
| 429 | */ | ||
| 430 | public function persist(object $object): void | ||
| 431 | { | ||
| 432 | $this->errorIfClosed(); | ||
| 433 | |||
| 434 | $this->unitOfWork->persist($object); | ||
| 435 | } | ||
| 436 | |||
| 437 | /** | ||
| 438 | * Removes an entity instance. | ||
| 439 | * | ||
| 440 | * A removed entity will be removed from the database at or before transaction commit | ||
| 441 | * or as a result of the flush operation. | ||
| 442 | * | ||
| 443 | * @throws ORMInvalidArgumentException | ||
| 444 | * @throws ORMException | ||
| 445 | */ | ||
| 446 | public function remove(object $object): void | ||
| 447 | { | ||
| 448 | $this->errorIfClosed(); | ||
| 449 | |||
| 450 | $this->unitOfWork->remove($object); | ||
| 451 | } | ||
| 452 | |||
| 453 | public function refresh(object $object, LockMode|int|null $lockMode = null): void | ||
| 454 | { | ||
| 455 | $this->errorIfClosed(); | ||
| 456 | |||
| 457 | $this->unitOfWork->refresh($object, $lockMode); | ||
| 458 | } | ||
| 459 | |||
| 460 | /** | ||
| 461 | * Detaches an entity from the EntityManager, causing a managed entity to | ||
| 462 | * become detached. Unflushed changes made to the entity if any | ||
| 463 | * (including removal of the entity), will not be synchronized to the database. | ||
| 464 | * Entities which previously referenced the detached entity will continue to | ||
| 465 | * reference it. | ||
| 466 | * | ||
| 467 | * @throws ORMInvalidArgumentException | ||
| 468 | */ | ||
| 469 | public function detach(object $object): void | ||
| 470 | { | ||
| 471 | $this->unitOfWork->detach($object); | ||
| 472 | } | ||
| 473 | |||
| 474 | public function lock(object $entity, LockMode|int $lockMode, DateTimeInterface|int|null $lockVersion = null): void | ||
| 475 | { | ||
| 476 | $this->unitOfWork->lock($entity, $lockMode, $lockVersion); | ||
| 477 | } | ||
| 478 | |||
| 479 | /** | ||
| 480 | * Gets the repository for an entity class. | ||
| 481 | * | ||
| 482 | * @psalm-param class-string<T> $className | ||
| 483 | * | ||
| 484 | * @psalm-return EntityRepository<T> | ||
| 485 | * | ||
| 486 | * @template T of object | ||
| 487 | */ | ||
| 488 | public function getRepository(string $className): EntityRepository | ||
| 489 | { | ||
| 490 | return $this->repositoryFactory->getRepository($this, $className); | ||
| 491 | } | ||
| 492 | |||
| 493 | /** | ||
| 494 | * Determines whether an entity instance is managed in this EntityManager. | ||
| 495 | * | ||
| 496 | * @return bool TRUE if this EntityManager currently manages the given entity, FALSE otherwise. | ||
| 497 | */ | ||
| 498 | public function contains(object $object): bool | ||
| 499 | { | ||
| 500 | return $this->unitOfWork->isScheduledForInsert($object) | ||
| 501 | || $this->unitOfWork->isInIdentityMap($object) | ||
| 502 | && ! $this->unitOfWork->isScheduledForDelete($object); | ||
| 503 | } | ||
| 504 | |||
| 505 | public function getEventManager(): EventManager | ||
| 506 | { | ||
| 507 | return $this->eventManager; | ||
| 508 | } | ||
| 509 | |||
| 510 | public function getConfiguration(): Configuration | ||
| 511 | { | ||
| 512 | return $this->config; | ||
| 513 | } | ||
| 514 | |||
| 515 | /** | ||
| 516 | * Throws an exception if the EntityManager is closed or currently not active. | ||
| 517 | * | ||
| 518 | * @throws EntityManagerClosed If the EntityManager is closed. | ||
| 519 | */ | ||
| 520 | private function errorIfClosed(): void | ||
| 521 | { | ||
| 522 | if ($this->closed) { | ||
| 523 | throw EntityManagerClosed::create(); | ||
| 524 | } | ||
| 525 | } | ||
| 526 | |||
| 527 | public function isOpen(): bool | ||
| 528 | { | ||
| 529 | return ! $this->closed; | ||
| 530 | } | ||
| 531 | |||
| 532 | public function getUnitOfWork(): UnitOfWork | ||
| 533 | { | ||
| 534 | return $this->unitOfWork; | ||
| 535 | } | ||
| 536 | |||
| 537 | public function newHydrator(string|int $hydrationMode): AbstractHydrator | ||
| 538 | { | ||
| 539 | return match ($hydrationMode) { | ||
| 540 | Query::HYDRATE_OBJECT => new Internal\Hydration\ObjectHydrator($this), | ||
| 541 | Query::HYDRATE_ARRAY => new Internal\Hydration\ArrayHydrator($this), | ||
| 542 | Query::HYDRATE_SCALAR => new Internal\Hydration\ScalarHydrator($this), | ||
| 543 | Query::HYDRATE_SINGLE_SCALAR => new Internal\Hydration\SingleScalarHydrator($this), | ||
| 544 | Query::HYDRATE_SIMPLEOBJECT => new Internal\Hydration\SimpleObjectHydrator($this), | ||
| 545 | Query::HYDRATE_SCALAR_COLUMN => new Internal\Hydration\ScalarColumnHydrator($this), | ||
| 546 | default => $this->createCustomHydrator((string) $hydrationMode), | ||
| 547 | }; | ||
| 548 | } | ||
| 549 | |||
| 550 | public function getProxyFactory(): ProxyFactory | ||
| 551 | { | ||
| 552 | return $this->proxyFactory; | ||
| 553 | } | ||
| 554 | |||
| 555 | public function initializeObject(object $obj): void | ||
| 556 | { | ||
| 557 | $this->unitOfWork->initializeObject($obj); | ||
| 558 | } | ||
| 559 | |||
| 560 | /** | ||
| 561 | * {@inheritDoc} | ||
| 562 | */ | ||
| 563 | public function isUninitializedObject($obj): bool | ||
| 564 | { | ||
| 565 | return $this->unitOfWork->isUninitializedObject($obj); | ||
| 566 | } | ||
| 567 | |||
| 568 | public function getFilters(): FilterCollection | ||
| 569 | { | ||
| 570 | return $this->filterCollection ??= new FilterCollection($this); | ||
| 571 | } | ||
| 572 | |||
| 573 | public function isFiltersStateClean(): bool | ||
| 574 | { | ||
| 575 | return $this->filterCollection === null || $this->filterCollection->isClean(); | ||
| 576 | } | ||
| 577 | |||
| 578 | public function hasFilters(): bool | ||
| 579 | { | ||
| 580 | return $this->filterCollection !== null; | ||
| 581 | } | ||
| 582 | |||
| 583 | /** | ||
| 584 | * @psalm-param LockMode::* $lockMode | ||
| 585 | * | ||
| 586 | * @throws OptimisticLockException | ||
| 587 | * @throws TransactionRequiredException | ||
| 588 | */ | ||
| 589 | private function checkLockRequirements(LockMode|int $lockMode, ClassMetadata $class): void | ||
| 590 | { | ||
| 591 | switch ($lockMode) { | ||
| 592 | case LockMode::OPTIMISTIC: | ||
| 593 | if (! $class->isVersioned) { | ||
| 594 | throw OptimisticLockException::notVersioned($class->name); | ||
| 595 | } | ||
| 596 | |||
| 597 | break; | ||
| 598 | case LockMode::PESSIMISTIC_READ: | ||
| 599 | case LockMode::PESSIMISTIC_WRITE: | ||
| 600 | if (! $this->getConnection()->isTransactionActive()) { | ||
| 601 | throw TransactionRequiredException::transactionRequired(); | ||
| 602 | } | ||
| 603 | } | ||
| 604 | } | ||
| 605 | |||
| 606 | private function configureMetadataCache(): void | ||
| 607 | { | ||
| 608 | $metadataCache = $this->config->getMetadataCache(); | ||
| 609 | if (! $metadataCache) { | ||
| 610 | return; | ||
| 611 | } | ||
| 612 | |||
| 613 | $this->metadataFactory->setCache($metadataCache); | ||
| 614 | } | ||
| 615 | |||
| 616 | private function createCustomHydrator(string $hydrationMode): AbstractHydrator | ||
| 617 | { | ||
| 618 | $class = $this->config->getCustomHydrationMode($hydrationMode); | ||
| 619 | |||
| 620 | if ($class !== null) { | ||
| 621 | return new $class($this); | ||
| 622 | } | ||
| 623 | |||
| 624 | throw InvalidHydrationMode::fromMode($hydrationMode); | ||
| 625 | } | ||
| 626 | } | ||
diff --git a/vendor/doctrine/orm/src/EntityManagerInterface.php b/vendor/doctrine/orm/src/EntityManagerInterface.php new file mode 100644 index 0000000..cf3102b --- /dev/null +++ b/vendor/doctrine/orm/src/EntityManagerInterface.php | |||
| @@ -0,0 +1,242 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM; | ||
| 6 | |||
| 7 | use DateTimeInterface; | ||
| 8 | use Doctrine\Common\EventManager; | ||
| 9 | use Doctrine\DBAL\Connection; | ||
| 10 | use Doctrine\DBAL\LockMode; | ||
| 11 | use Doctrine\ORM\Exception\ORMException; | ||
| 12 | use Doctrine\ORM\Internal\Hydration\AbstractHydrator; | ||
| 13 | use Doctrine\ORM\Mapping\ClassMetadataFactory; | ||
| 14 | use Doctrine\ORM\Proxy\ProxyFactory; | ||
| 15 | use Doctrine\ORM\Query\Expr; | ||
| 16 | use Doctrine\ORM\Query\FilterCollection; | ||
| 17 | use Doctrine\ORM\Query\ResultSetMapping; | ||
| 18 | use Doctrine\Persistence\ObjectManager; | ||
| 19 | |||
| 20 | interface EntityManagerInterface extends ObjectManager | ||
| 21 | { | ||
| 22 | /** | ||
| 23 | * {@inheritDoc} | ||
| 24 | * | ||
| 25 | * @psalm-param class-string<T> $className | ||
| 26 | * | ||
| 27 | * @psalm-return EntityRepository<T> | ||
| 28 | * | ||
| 29 | * @template T of object | ||
| 30 | */ | ||
| 31 | public function getRepository(string $className): EntityRepository; | ||
| 32 | |||
| 33 | /** | ||
| 34 | * Returns the cache API for managing the second level cache regions or NULL if the cache is not enabled. | ||
| 35 | */ | ||
| 36 | public function getCache(): Cache|null; | ||
| 37 | |||
| 38 | /** | ||
| 39 | * Gets the database connection object used by the EntityManager. | ||
| 40 | */ | ||
| 41 | public function getConnection(): Connection; | ||
| 42 | |||
| 43 | public function getMetadataFactory(): ClassMetadataFactory; | ||
| 44 | |||
| 45 | /** | ||
| 46 | * Gets an ExpressionBuilder used for object-oriented construction of query expressions. | ||
| 47 | * | ||
| 48 | * Example: | ||
| 49 | * | ||
| 50 | * <code> | ||
| 51 | * $qb = $em->createQueryBuilder(); | ||
| 52 | * $expr = $em->getExpressionBuilder(); | ||
| 53 | * $qb->select('u')->from('User', 'u') | ||
| 54 | * ->where($expr->orX($expr->eq('u.id', 1), $expr->eq('u.id', 2))); | ||
| 55 | * </code> | ||
| 56 | */ | ||
| 57 | public function getExpressionBuilder(): Expr; | ||
| 58 | |||
| 59 | /** | ||
| 60 | * Starts a transaction on the underlying database connection. | ||
| 61 | */ | ||
| 62 | public function beginTransaction(): void; | ||
| 63 | |||
| 64 | /** | ||
| 65 | * Executes a function in a transaction. | ||
| 66 | * | ||
| 67 | * The function gets passed this EntityManager instance as an (optional) parameter. | ||
| 68 | * | ||
| 69 | * {@link flush} is invoked prior to transaction commit. | ||
| 70 | * | ||
| 71 | * If an exception occurs during execution of the function or flushing or transaction commit, | ||
| 72 | * the transaction is rolled back, the EntityManager closed and the exception re-thrown. | ||
| 73 | * | ||
| 74 | * @psalm-param callable(self): T $func The function to execute transactionally. | ||
| 75 | * | ||
| 76 | * @return mixed The value returned from the closure. | ||
| 77 | * @psalm-return T | ||
| 78 | * | ||
| 79 | * @template T | ||
| 80 | */ | ||
| 81 | public function wrapInTransaction(callable $func): mixed; | ||
| 82 | |||
| 83 | /** | ||
| 84 | * Commits a transaction on the underlying database connection. | ||
| 85 | */ | ||
| 86 | public function commit(): void; | ||
| 87 | |||
| 88 | /** | ||
| 89 | * Performs a rollback on the underlying database connection. | ||
| 90 | */ | ||
| 91 | public function rollback(): void; | ||
| 92 | |||
| 93 | /** | ||
| 94 | * Creates a new Query object. | ||
| 95 | * | ||
| 96 | * @param string $dql The DQL string. | ||
| 97 | */ | ||
| 98 | public function createQuery(string $dql = ''): Query; | ||
| 99 | |||
| 100 | /** | ||
| 101 | * Creates a native SQL query. | ||
| 102 | */ | ||
| 103 | public function createNativeQuery(string $sql, ResultSetMapping $rsm): NativeQuery; | ||
| 104 | |||
| 105 | /** | ||
| 106 | * Create a QueryBuilder instance | ||
| 107 | */ | ||
| 108 | public function createQueryBuilder(): QueryBuilder; | ||
| 109 | |||
| 110 | /** | ||
| 111 | * Finds an Entity by its identifier. | ||
| 112 | * | ||
| 113 | * @param string $className The class name of the entity to find. | ||
| 114 | * @param mixed $id The identity of the entity to find. | ||
| 115 | * @param LockMode|int|null $lockMode One of the \Doctrine\DBAL\LockMode::* constants | ||
| 116 | * or NULL if no specific lock mode should be used | ||
| 117 | * during the search. | ||
| 118 | * @param int|null $lockVersion The version of the entity to find when using | ||
| 119 | * optimistic locking. | ||
| 120 | * @psalm-param class-string<T> $className | ||
| 121 | * @psalm-param LockMode::*|null $lockMode | ||
| 122 | * | ||
| 123 | * @return object|null The entity instance or NULL if the entity can not be found. | ||
| 124 | * @psalm-return T|null | ||
| 125 | * | ||
| 126 | * @throws OptimisticLockException | ||
| 127 | * @throws ORMInvalidArgumentException | ||
| 128 | * @throws TransactionRequiredException | ||
| 129 | * @throws ORMException | ||
| 130 | * | ||
| 131 | * @template T of object | ||
| 132 | */ | ||
| 133 | public function find(string $className, mixed $id, LockMode|int|null $lockMode = null, int|null $lockVersion = null): object|null; | ||
| 134 | |||
| 135 | /** | ||
| 136 | * Refreshes the persistent state of an object from the database, | ||
| 137 | * overriding any local changes that have not yet been persisted. | ||
| 138 | * | ||
| 139 | * @param LockMode|int|null $lockMode One of the \Doctrine\DBAL\LockMode::* constants | ||
| 140 | * or NULL if no specific lock mode should be used | ||
| 141 | * during the search. | ||
| 142 | * @psalm-param LockMode::*|null $lockMode | ||
| 143 | * | ||
| 144 | * @throws ORMInvalidArgumentException | ||
| 145 | * @throws ORMException | ||
| 146 | * @throws TransactionRequiredException | ||
| 147 | */ | ||
| 148 | public function refresh(object $object, LockMode|int|null $lockMode = null): void; | ||
| 149 | |||
| 150 | /** | ||
| 151 | * Gets a reference to the entity identified by the given type and identifier | ||
| 152 | * without actually loading it, if the entity is not yet loaded. | ||
| 153 | * | ||
| 154 | * @param string $entityName The name of the entity type. | ||
| 155 | * @param mixed $id The entity identifier. | ||
| 156 | * @psalm-param class-string<T> $entityName | ||
| 157 | * | ||
| 158 | * @psalm-return T|null | ||
| 159 | * | ||
| 160 | * @throws ORMException | ||
| 161 | * | ||
| 162 | * @template T of object | ||
| 163 | */ | ||
| 164 | public function getReference(string $entityName, mixed $id): object|null; | ||
| 165 | |||
| 166 | /** | ||
| 167 | * Closes the EntityManager. All entities that are currently managed | ||
| 168 | * by this EntityManager become detached. The EntityManager may no longer | ||
| 169 | * be used after it is closed. | ||
| 170 | */ | ||
| 171 | public function close(): void; | ||
| 172 | |||
| 173 | /** | ||
| 174 | * Acquire a lock on the given entity. | ||
| 175 | * | ||
| 176 | * @psalm-param LockMode::* $lockMode | ||
| 177 | * | ||
| 178 | * @throws OptimisticLockException | ||
| 179 | * @throws PessimisticLockException | ||
| 180 | */ | ||
| 181 | public function lock(object $entity, LockMode|int $lockMode, DateTimeInterface|int|null $lockVersion = null): void; | ||
| 182 | |||
| 183 | /** | ||
| 184 | * Gets the EventManager used by the EntityManager. | ||
| 185 | */ | ||
| 186 | public function getEventManager(): EventManager; | ||
| 187 | |||
| 188 | /** | ||
| 189 | * Gets the Configuration used by the EntityManager. | ||
| 190 | */ | ||
| 191 | public function getConfiguration(): Configuration; | ||
| 192 | |||
| 193 | /** | ||
| 194 | * Check if the Entity manager is open or closed. | ||
| 195 | */ | ||
| 196 | public function isOpen(): bool; | ||
| 197 | |||
| 198 | /** | ||
| 199 | * Gets the UnitOfWork used by the EntityManager to coordinate operations. | ||
| 200 | */ | ||
| 201 | public function getUnitOfWork(): UnitOfWork; | ||
| 202 | |||
| 203 | /** | ||
| 204 | * Create a new instance for the given hydration mode. | ||
| 205 | * | ||
| 206 | * @psalm-param string|AbstractQuery::HYDRATE_* $hydrationMode | ||
| 207 | * | ||
| 208 | * @throws ORMException | ||
| 209 | */ | ||
| 210 | public function newHydrator(string|int $hydrationMode): AbstractHydrator; | ||
| 211 | |||
| 212 | /** | ||
| 213 | * Gets the proxy factory used by the EntityManager to create entity proxies. | ||
| 214 | */ | ||
| 215 | public function getProxyFactory(): ProxyFactory; | ||
| 216 | |||
| 217 | /** | ||
| 218 | * Gets the enabled filters. | ||
| 219 | */ | ||
| 220 | public function getFilters(): FilterCollection; | ||
| 221 | |||
| 222 | /** | ||
| 223 | * Checks whether the state of the filter collection is clean. | ||
| 224 | */ | ||
| 225 | public function isFiltersStateClean(): bool; | ||
| 226 | |||
| 227 | /** | ||
| 228 | * Checks whether the Entity Manager has filters. | ||
| 229 | */ | ||
| 230 | public function hasFilters(): bool; | ||
| 231 | |||
| 232 | /** | ||
| 233 | * {@inheritDoc} | ||
| 234 | * | ||
| 235 | * @psalm-param string|class-string<T> $className | ||
| 236 | * | ||
| 237 | * @psalm-return ($className is class-string<T> ? Mapping\ClassMetadata<T> : Mapping\ClassMetadata<object>) | ||
| 238 | * | ||
| 239 | * @psalm-template T of object | ||
| 240 | */ | ||
| 241 | public function getClassMetadata(string $className): Mapping\ClassMetadata; | ||
| 242 | } | ||
diff --git a/vendor/doctrine/orm/src/EntityNotFoundException.php b/vendor/doctrine/orm/src/EntityNotFoundException.php new file mode 100644 index 0000000..142dc8a --- /dev/null +++ b/vendor/doctrine/orm/src/EntityNotFoundException.php | |||
| @@ -0,0 +1,46 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM; | ||
| 6 | |||
| 7 | use Doctrine\ORM\Exception\ORMException; | ||
| 8 | use RuntimeException; | ||
| 9 | |||
| 10 | use function implode; | ||
| 11 | use function sprintf; | ||
| 12 | |||
| 13 | /** | ||
| 14 | * Exception thrown when a Proxy fails to retrieve an Entity result. | ||
| 15 | */ | ||
| 16 | class EntityNotFoundException extends RuntimeException implements ORMException | ||
| 17 | { | ||
| 18 | /** | ||
| 19 | * Static constructor. | ||
| 20 | * | ||
| 21 | * @param string[] $id | ||
| 22 | */ | ||
| 23 | public static function fromClassNameAndIdentifier(string $className, array $id): self | ||
| 24 | { | ||
| 25 | $ids = []; | ||
| 26 | |||
| 27 | foreach ($id as $key => $value) { | ||
| 28 | $ids[] = $key . '(' . $value . ')'; | ||
| 29 | } | ||
| 30 | |||
| 31 | return new self( | ||
| 32 | 'Entity of type \'' . $className . '\'' . ($ids ? ' for IDs ' . implode(', ', $ids) : '') . ' was not found', | ||
| 33 | ); | ||
| 34 | } | ||
| 35 | |||
| 36 | /** | ||
| 37 | * Instance for which no identifier can be found | ||
| 38 | */ | ||
| 39 | public static function noIdentifierFound(string $className): self | ||
| 40 | { | ||
| 41 | return new self(sprintf( | ||
| 42 | 'Unable to find "%s" entity identifier associated with the UnitOfWork', | ||
| 43 | $className, | ||
| 44 | )); | ||
| 45 | } | ||
| 46 | } | ||
diff --git a/vendor/doctrine/orm/src/EntityRepository.php b/vendor/doctrine/orm/src/EntityRepository.php new file mode 100644 index 0000000..a53c528 --- /dev/null +++ b/vendor/doctrine/orm/src/EntityRepository.php | |||
| @@ -0,0 +1,236 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM; | ||
| 6 | |||
| 7 | use BadMethodCallException; | ||
| 8 | use Doctrine\Common\Collections\AbstractLazyCollection; | ||
| 9 | use Doctrine\Common\Collections\Criteria; | ||
| 10 | use Doctrine\Common\Collections\Selectable; | ||
| 11 | use Doctrine\DBAL\LockMode; | ||
| 12 | use Doctrine\Inflector\Inflector; | ||
| 13 | use Doctrine\Inflector\InflectorFactory; | ||
| 14 | use Doctrine\ORM\Mapping\ClassMetadata; | ||
| 15 | use Doctrine\ORM\Query\ResultSetMappingBuilder; | ||
| 16 | use Doctrine\ORM\Repository\Exception\InvalidMagicMethodCall; | ||
| 17 | use Doctrine\Persistence\ObjectRepository; | ||
| 18 | |||
| 19 | use function array_slice; | ||
| 20 | use function lcfirst; | ||
| 21 | use function sprintf; | ||
| 22 | use function str_starts_with; | ||
| 23 | use function substr; | ||
| 24 | |||
| 25 | /** | ||
| 26 | * An EntityRepository serves as a repository for entities with generic as well as | ||
| 27 | * business specific methods for retrieving entities. | ||
| 28 | * | ||
| 29 | * This class is designed for inheritance and users can subclass this class to | ||
| 30 | * write their own repositories with business-specific methods to locate entities. | ||
| 31 | * | ||
| 32 | * @template T of object | ||
| 33 | * @template-implements Selectable<int,T> | ||
| 34 | * @template-implements ObjectRepository<T> | ||
| 35 | */ | ||
| 36 | class EntityRepository implements ObjectRepository, Selectable | ||
| 37 | { | ||
| 38 | /** @psalm-var class-string<T> */ | ||
| 39 | private readonly string $entityName; | ||
| 40 | private static Inflector|null $inflector = null; | ||
| 41 | |||
| 42 | /** @psalm-param ClassMetadata<T> $class */ | ||
| 43 | public function __construct( | ||
| 44 | private readonly EntityManagerInterface $em, | ||
| 45 | private readonly ClassMetadata $class, | ||
| 46 | ) { | ||
| 47 | $this->entityName = $class->name; | ||
| 48 | } | ||
| 49 | |||
| 50 | /** | ||
| 51 | * Creates a new QueryBuilder instance that is prepopulated for this entity name. | ||
| 52 | */ | ||
| 53 | public function createQueryBuilder(string $alias, string|null $indexBy = null): QueryBuilder | ||
| 54 | { | ||
| 55 | return $this->em->createQueryBuilder() | ||
| 56 | ->select($alias) | ||
| 57 | ->from($this->entityName, $alias, $indexBy); | ||
| 58 | } | ||
| 59 | |||
| 60 | /** | ||
| 61 | * Creates a new result set mapping builder for this entity. | ||
| 62 | * | ||
| 63 | * The column naming strategy is "INCREMENT". | ||
| 64 | */ | ||
| 65 | public function createResultSetMappingBuilder(string $alias): ResultSetMappingBuilder | ||
| 66 | { | ||
| 67 | $rsm = new ResultSetMappingBuilder($this->em, ResultSetMappingBuilder::COLUMN_RENAMING_INCREMENT); | ||
| 68 | $rsm->addRootEntityFromClassMetadata($this->entityName, $alias); | ||
| 69 | |||
| 70 | return $rsm; | ||
| 71 | } | ||
| 72 | |||
| 73 | /** | ||
| 74 | * Finds an entity by its primary key / identifier. | ||
| 75 | * | ||
| 76 | * @param LockMode|int|null $lockMode One of the \Doctrine\DBAL\LockMode::* constants | ||
| 77 | * or NULL if no specific lock mode should be used | ||
| 78 | * during the search. | ||
| 79 | * @psalm-param LockMode::*|null $lockMode | ||
| 80 | * | ||
| 81 | * @return object|null The entity instance or NULL if the entity can not be found. | ||
| 82 | * @psalm-return ?T | ||
| 83 | */ | ||
| 84 | public function find(mixed $id, LockMode|int|null $lockMode = null, int|null $lockVersion = null): object|null | ||
| 85 | { | ||
| 86 | return $this->em->find($this->entityName, $id, $lockMode, $lockVersion); | ||
| 87 | } | ||
| 88 | |||
| 89 | /** | ||
| 90 | * Finds all entities in the repository. | ||
| 91 | * | ||
| 92 | * @psalm-return list<T> The entities. | ||
| 93 | */ | ||
| 94 | public function findAll(): array | ||
| 95 | { | ||
| 96 | return $this->findBy([]); | ||
| 97 | } | ||
| 98 | |||
| 99 | /** | ||
| 100 | * Finds entities by a set of criteria. | ||
| 101 | * | ||
| 102 | * {@inheritDoc} | ||
| 103 | * | ||
| 104 | * @psalm-return list<T> | ||
| 105 | */ | ||
| 106 | public function findBy(array $criteria, array|null $orderBy = null, int|null $limit = null, int|null $offset = null): array | ||
| 107 | { | ||
| 108 | $persister = $this->em->getUnitOfWork()->getEntityPersister($this->entityName); | ||
| 109 | |||
| 110 | return $persister->loadAll($criteria, $orderBy, $limit, $offset); | ||
| 111 | } | ||
| 112 | |||
| 113 | /** | ||
| 114 | * Finds a single entity by a set of criteria. | ||
| 115 | * | ||
| 116 | * @psalm-param array<string, mixed> $criteria | ||
| 117 | * @psalm-param array<string, string>|null $orderBy | ||
| 118 | * | ||
| 119 | * @psalm-return T|null | ||
| 120 | */ | ||
| 121 | public function findOneBy(array $criteria, array|null $orderBy = null): object|null | ||
| 122 | { | ||
| 123 | $persister = $this->em->getUnitOfWork()->getEntityPersister($this->entityName); | ||
| 124 | |||
| 125 | return $persister->load($criteria, null, null, [], null, 1, $orderBy); | ||
| 126 | } | ||
| 127 | |||
| 128 | /** | ||
| 129 | * Counts entities by a set of criteria. | ||
| 130 | * | ||
| 131 | * @psalm-param array<string, mixed> $criteria | ||
| 132 | * | ||
| 133 | * @return int The cardinality of the objects that match the given criteria. | ||
| 134 | * | ||
| 135 | * @todo Add this method to `ObjectRepository` interface in the next major release | ||
| 136 | */ | ||
| 137 | public function count(array $criteria = []): int | ||
| 138 | { | ||
| 139 | return $this->em->getUnitOfWork()->getEntityPersister($this->entityName)->count($criteria); | ||
| 140 | } | ||
| 141 | |||
| 142 | /** | ||
| 143 | * Adds support for magic method calls. | ||
| 144 | * | ||
| 145 | * @param mixed[] $arguments | ||
| 146 | * @psalm-param list<mixed> $arguments | ||
| 147 | * | ||
| 148 | * @throws BadMethodCallException If the method called is invalid. | ||
| 149 | */ | ||
| 150 | public function __call(string $method, array $arguments): mixed | ||
| 151 | { | ||
| 152 | if (str_starts_with($method, 'findBy')) { | ||
| 153 | return $this->resolveMagicCall('findBy', substr($method, 6), $arguments); | ||
| 154 | } | ||
| 155 | |||
| 156 | if (str_starts_with($method, 'findOneBy')) { | ||
| 157 | return $this->resolveMagicCall('findOneBy', substr($method, 9), $arguments); | ||
| 158 | } | ||
| 159 | |||
| 160 | if (str_starts_with($method, 'countBy')) { | ||
| 161 | return $this->resolveMagicCall('count', substr($method, 7), $arguments); | ||
| 162 | } | ||
| 163 | |||
| 164 | throw new BadMethodCallException(sprintf( | ||
| 165 | 'Undefined method "%s". The method name must start with ' . | ||
| 166 | 'either findBy, findOneBy or countBy!', | ||
| 167 | $method, | ||
| 168 | )); | ||
| 169 | } | ||
| 170 | |||
| 171 | /** @psalm-return class-string<T> */ | ||
| 172 | protected function getEntityName(): string | ||
| 173 | { | ||
| 174 | return $this->entityName; | ||
| 175 | } | ||
| 176 | |||
| 177 | public function getClassName(): string | ||
| 178 | { | ||
| 179 | return $this->getEntityName(); | ||
| 180 | } | ||
| 181 | |||
| 182 | protected function getEntityManager(): EntityManagerInterface | ||
| 183 | { | ||
| 184 | return $this->em; | ||
| 185 | } | ||
| 186 | |||
| 187 | /** @psalm-return ClassMetadata<T> */ | ||
| 188 | protected function getClassMetadata(): ClassMetadata | ||
| 189 | { | ||
| 190 | return $this->class; | ||
| 191 | } | ||
| 192 | |||
| 193 | /** | ||
| 194 | * Select all elements from a selectable that match the expression and | ||
| 195 | * return a new collection containing these elements. | ||
| 196 | * | ||
| 197 | * @psalm-return AbstractLazyCollection<int, T>&Selectable<int, T> | ||
| 198 | */ | ||
| 199 | public function matching(Criteria $criteria): AbstractLazyCollection&Selectable | ||
| 200 | { | ||
| 201 | $persister = $this->em->getUnitOfWork()->getEntityPersister($this->entityName); | ||
| 202 | |||
| 203 | return new LazyCriteriaCollection($persister, $criteria); | ||
| 204 | } | ||
| 205 | |||
| 206 | /** | ||
| 207 | * Resolves a magic method call to the proper existent method at `EntityRepository`. | ||
| 208 | * | ||
| 209 | * @param string $method The method to call | ||
| 210 | * @param string $by The property name used as condition | ||
| 211 | * @psalm-param list<mixed> $arguments The arguments to pass at method call | ||
| 212 | * | ||
| 213 | * @throws InvalidMagicMethodCall If the method called is invalid or the | ||
| 214 | * requested field/association does not exist. | ||
| 215 | */ | ||
| 216 | private function resolveMagicCall(string $method, string $by, array $arguments): mixed | ||
| 217 | { | ||
| 218 | if (! $arguments) { | ||
| 219 | throw InvalidMagicMethodCall::onMissingParameter($method . $by); | ||
| 220 | } | ||
| 221 | |||
| 222 | self::$inflector ??= InflectorFactory::create()->build(); | ||
| 223 | |||
| 224 | $fieldName = lcfirst(self::$inflector->classify($by)); | ||
| 225 | |||
| 226 | if (! ($this->class->hasField($fieldName) || $this->class->hasAssociation($fieldName))) { | ||
| 227 | throw InvalidMagicMethodCall::becauseFieldNotFoundIn( | ||
| 228 | $this->entityName, | ||
| 229 | $fieldName, | ||
| 230 | $method . $by, | ||
| 231 | ); | ||
| 232 | } | ||
| 233 | |||
| 234 | return $this->$method([$fieldName => $arguments[0]], ...array_slice($arguments, 1)); | ||
| 235 | } | ||
| 236 | } | ||
diff --git a/vendor/doctrine/orm/src/Event/ListenersInvoker.php b/vendor/doctrine/orm/src/Event/ListenersInvoker.php new file mode 100644 index 0000000..c0c327e --- /dev/null +++ b/vendor/doctrine/orm/src/Event/ListenersInvoker.php | |||
| @@ -0,0 +1,98 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Event; | ||
| 6 | |||
| 7 | use Doctrine\Common\EventArgs; | ||
| 8 | use Doctrine\Common\EventManager; | ||
| 9 | use Doctrine\ORM\EntityManagerInterface; | ||
| 10 | use Doctrine\ORM\Mapping\ClassMetadata; | ||
| 11 | use Doctrine\ORM\Mapping\EntityListenerResolver; | ||
| 12 | |||
| 13 | /** | ||
| 14 | * A method invoker based on entity lifecycle. | ||
| 15 | */ | ||
| 16 | class ListenersInvoker | ||
| 17 | { | ||
| 18 | final public const INVOKE_NONE = 0; | ||
| 19 | final public const INVOKE_LISTENERS = 1; | ||
| 20 | final public const INVOKE_CALLBACKS = 2; | ||
| 21 | final public const INVOKE_MANAGER = 4; | ||
| 22 | |||
| 23 | /** The Entity listener resolver. */ | ||
| 24 | private readonly EntityListenerResolver $resolver; | ||
| 25 | |||
| 26 | /** The EventManager used for dispatching events. */ | ||
| 27 | private readonly EventManager $eventManager; | ||
| 28 | |||
| 29 | public function __construct(EntityManagerInterface $em) | ||
| 30 | { | ||
| 31 | $this->eventManager = $em->getEventManager(); | ||
| 32 | $this->resolver = $em->getConfiguration()->getEntityListenerResolver(); | ||
| 33 | } | ||
| 34 | |||
| 35 | /** | ||
| 36 | * Get the subscribed event systems | ||
| 37 | * | ||
| 38 | * @param ClassMetadata $metadata The entity metadata. | ||
| 39 | * @param string $eventName The entity lifecycle event. | ||
| 40 | * | ||
| 41 | * @psalm-return int-mask-of<self::INVOKE_*> Bitmask of subscribed event systems. | ||
| 42 | */ | ||
| 43 | public function getSubscribedSystems(ClassMetadata $metadata, string $eventName): int | ||
| 44 | { | ||
| 45 | $invoke = self::INVOKE_NONE; | ||
| 46 | |||
| 47 | if (isset($metadata->lifecycleCallbacks[$eventName])) { | ||
| 48 | $invoke |= self::INVOKE_CALLBACKS; | ||
| 49 | } | ||
| 50 | |||
| 51 | if (isset($metadata->entityListeners[$eventName])) { | ||
| 52 | $invoke |= self::INVOKE_LISTENERS; | ||
| 53 | } | ||
| 54 | |||
| 55 | if ($this->eventManager->hasListeners($eventName)) { | ||
| 56 | $invoke |= self::INVOKE_MANAGER; | ||
| 57 | } | ||
| 58 | |||
| 59 | return $invoke; | ||
| 60 | } | ||
| 61 | |||
| 62 | /** | ||
| 63 | * Dispatches the lifecycle event of the given entity. | ||
| 64 | * | ||
| 65 | * @param ClassMetadata $metadata The entity metadata. | ||
| 66 | * @param string $eventName The entity lifecycle event. | ||
| 67 | * @param object $entity The Entity on which the event occurred. | ||
| 68 | * @param EventArgs $event The Event args. | ||
| 69 | * @psalm-param int-mask-of<self::INVOKE_*> $invoke Bitmask to invoke listeners. | ||
| 70 | */ | ||
| 71 | public function invoke( | ||
| 72 | ClassMetadata $metadata, | ||
| 73 | string $eventName, | ||
| 74 | object $entity, | ||
| 75 | EventArgs $event, | ||
| 76 | int $invoke, | ||
| 77 | ): void { | ||
| 78 | if ($invoke & self::INVOKE_CALLBACKS) { | ||
| 79 | foreach ($metadata->lifecycleCallbacks[$eventName] as $callback) { | ||
| 80 | $entity->$callback($event); | ||
| 81 | } | ||
| 82 | } | ||
| 83 | |||
| 84 | if ($invoke & self::INVOKE_LISTENERS) { | ||
| 85 | foreach ($metadata->entityListeners[$eventName] as $listener) { | ||
| 86 | $class = $listener['class']; | ||
| 87 | $method = $listener['method']; | ||
| 88 | $instance = $this->resolver->resolve($class); | ||
| 89 | |||
| 90 | $instance->$method($entity, $event); | ||
| 91 | } | ||
| 92 | } | ||
| 93 | |||
| 94 | if ($invoke & self::INVOKE_MANAGER) { | ||
| 95 | $this->eventManager->dispatchEvent($eventName, $event); | ||
| 96 | } | ||
| 97 | } | ||
| 98 | } | ||
diff --git a/vendor/doctrine/orm/src/Event/LoadClassMetadataEventArgs.php b/vendor/doctrine/orm/src/Event/LoadClassMetadataEventArgs.php new file mode 100644 index 0000000..b450616 --- /dev/null +++ b/vendor/doctrine/orm/src/Event/LoadClassMetadataEventArgs.php | |||
| @@ -0,0 +1,25 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Event; | ||
| 6 | |||
| 7 | use Doctrine\ORM\EntityManagerInterface; | ||
| 8 | use Doctrine\ORM\Mapping\ClassMetadata; | ||
| 9 | use Doctrine\Persistence\Event\LoadClassMetadataEventArgs as BaseLoadClassMetadataEventArgs; | ||
| 10 | |||
| 11 | /** | ||
| 12 | * Class that holds event arguments for a loadMetadata event. | ||
| 13 | * | ||
| 14 | * @extends BaseLoadClassMetadataEventArgs<ClassMetadata<object>, EntityManagerInterface> | ||
| 15 | */ | ||
| 16 | class LoadClassMetadataEventArgs extends BaseLoadClassMetadataEventArgs | ||
| 17 | { | ||
| 18 | /** | ||
| 19 | * Retrieve associated EntityManager. | ||
| 20 | */ | ||
| 21 | public function getEntityManager(): EntityManagerInterface | ||
| 22 | { | ||
| 23 | return $this->getObjectManager(); | ||
| 24 | } | ||
| 25 | } | ||
diff --git a/vendor/doctrine/orm/src/Event/OnClassMetadataNotFoundEventArgs.php b/vendor/doctrine/orm/src/Event/OnClassMetadataNotFoundEventArgs.php new file mode 100644 index 0000000..762c083 --- /dev/null +++ b/vendor/doctrine/orm/src/Event/OnClassMetadataNotFoundEventArgs.php | |||
| @@ -0,0 +1,49 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Event; | ||
| 6 | |||
| 7 | use Doctrine\ORM\EntityManagerInterface; | ||
| 8 | use Doctrine\Persistence\Event\ManagerEventArgs; | ||
| 9 | use Doctrine\Persistence\Mapping\ClassMetadata; | ||
| 10 | use Doctrine\Persistence\ObjectManager; | ||
| 11 | |||
| 12 | /** | ||
| 13 | * Class that holds event arguments for a `onClassMetadataNotFound` event. | ||
| 14 | * | ||
| 15 | * This object is mutable by design, allowing callbacks having access to it to set the | ||
| 16 | * found metadata in it, and therefore "cancelling" a `onClassMetadataNotFound` event | ||
| 17 | * | ||
| 18 | * @extends ManagerEventArgs<EntityManagerInterface> | ||
| 19 | */ | ||
| 20 | class OnClassMetadataNotFoundEventArgs extends ManagerEventArgs | ||
| 21 | { | ||
| 22 | private ClassMetadata|null $foundMetadata = null; | ||
| 23 | |||
| 24 | /** @param EntityManagerInterface $objectManager */ | ||
| 25 | public function __construct( | ||
| 26 | private readonly string $className, | ||
| 27 | ObjectManager $objectManager, | ||
| 28 | ) { | ||
| 29 | parent::__construct($objectManager); | ||
| 30 | } | ||
| 31 | |||
| 32 | public function setFoundMetadata(ClassMetadata|null $classMetadata): void | ||
| 33 | { | ||
| 34 | $this->foundMetadata = $classMetadata; | ||
| 35 | } | ||
| 36 | |||
| 37 | public function getFoundMetadata(): ClassMetadata|null | ||
| 38 | { | ||
| 39 | return $this->foundMetadata; | ||
| 40 | } | ||
| 41 | |||
| 42 | /** | ||
| 43 | * Retrieve class name for which a failed metadata fetch attempt was executed | ||
| 44 | */ | ||
| 45 | public function getClassName(): string | ||
| 46 | { | ||
| 47 | return $this->className; | ||
| 48 | } | ||
| 49 | } | ||
diff --git a/vendor/doctrine/orm/src/Event/OnClearEventArgs.php b/vendor/doctrine/orm/src/Event/OnClearEventArgs.php new file mode 100644 index 0000000..29a42f2 --- /dev/null +++ b/vendor/doctrine/orm/src/Event/OnClearEventArgs.php | |||
| @@ -0,0 +1,19 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Event; | ||
| 6 | |||
| 7 | use Doctrine\ORM\EntityManagerInterface; | ||
| 8 | use Doctrine\Persistence\Event\OnClearEventArgs as BaseOnClearEventArgs; | ||
| 9 | |||
| 10 | /** | ||
| 11 | * Provides event arguments for the onClear event. | ||
| 12 | * | ||
| 13 | * @link www.doctrine-project.org | ||
| 14 | * | ||
| 15 | * @extends BaseOnClearEventArgs<EntityManagerInterface> | ||
| 16 | */ | ||
| 17 | class OnClearEventArgs extends BaseOnClearEventArgs | ||
| 18 | { | ||
| 19 | } | ||
diff --git a/vendor/doctrine/orm/src/Event/OnFlushEventArgs.php b/vendor/doctrine/orm/src/Event/OnFlushEventArgs.php new file mode 100644 index 0000000..b0594ca --- /dev/null +++ b/vendor/doctrine/orm/src/Event/OnFlushEventArgs.php | |||
| @@ -0,0 +1,19 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Event; | ||
| 6 | |||
| 7 | use Doctrine\ORM\EntityManagerInterface; | ||
| 8 | use Doctrine\Persistence\Event\ManagerEventArgs; | ||
| 9 | |||
| 10 | /** | ||
| 11 | * Provides event arguments for the preFlush event. | ||
| 12 | * | ||
| 13 | * @link www.doctrine-project.org | ||
| 14 | * | ||
| 15 | * @extends ManagerEventArgs<EntityManagerInterface> | ||
| 16 | */ | ||
| 17 | class OnFlushEventArgs extends ManagerEventArgs | ||
| 18 | { | ||
| 19 | } | ||
diff --git a/vendor/doctrine/orm/src/Event/PostFlushEventArgs.php b/vendor/doctrine/orm/src/Event/PostFlushEventArgs.php new file mode 100644 index 0000000..ca41ba8 --- /dev/null +++ b/vendor/doctrine/orm/src/Event/PostFlushEventArgs.php | |||
| @@ -0,0 +1,19 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Event; | ||
| 6 | |||
| 7 | use Doctrine\ORM\EntityManagerInterface; | ||
| 8 | use Doctrine\Persistence\Event\ManagerEventArgs; | ||
| 9 | |||
| 10 | /** | ||
| 11 | * Provides event arguments for the postFlush event. | ||
| 12 | * | ||
| 13 | * @link www.doctrine-project.org | ||
| 14 | * | ||
| 15 | * @extends ManagerEventArgs<EntityManagerInterface> | ||
| 16 | */ | ||
| 17 | class PostFlushEventArgs extends ManagerEventArgs | ||
| 18 | { | ||
| 19 | } | ||
diff --git a/vendor/doctrine/orm/src/Event/PostLoadEventArgs.php b/vendor/doctrine/orm/src/Event/PostLoadEventArgs.php new file mode 100644 index 0000000..8344e68 --- /dev/null +++ b/vendor/doctrine/orm/src/Event/PostLoadEventArgs.php | |||
| @@ -0,0 +1,13 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Event; | ||
| 6 | |||
| 7 | use Doctrine\ORM\EntityManagerInterface; | ||
| 8 | use Doctrine\Persistence\Event\LifecycleEventArgs; | ||
| 9 | |||
| 10 | /** @extends LifecycleEventArgs<EntityManagerInterface> */ | ||
| 11 | final class PostLoadEventArgs extends LifecycleEventArgs | ||
| 12 | { | ||
| 13 | } | ||
diff --git a/vendor/doctrine/orm/src/Event/PostPersistEventArgs.php b/vendor/doctrine/orm/src/Event/PostPersistEventArgs.php new file mode 100644 index 0000000..926ac1c --- /dev/null +++ b/vendor/doctrine/orm/src/Event/PostPersistEventArgs.php | |||
| @@ -0,0 +1,13 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Event; | ||
| 6 | |||
| 7 | use Doctrine\ORM\EntityManagerInterface; | ||
| 8 | use Doctrine\Persistence\Event\LifecycleEventArgs; | ||
| 9 | |||
| 10 | /** @extends LifecycleEventArgs<EntityManagerInterface> */ | ||
| 11 | final class PostPersistEventArgs extends LifecycleEventArgs | ||
| 12 | { | ||
| 13 | } | ||
diff --git a/vendor/doctrine/orm/src/Event/PostRemoveEventArgs.php b/vendor/doctrine/orm/src/Event/PostRemoveEventArgs.php new file mode 100644 index 0000000..8bf857e --- /dev/null +++ b/vendor/doctrine/orm/src/Event/PostRemoveEventArgs.php | |||
| @@ -0,0 +1,13 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Event; | ||
| 6 | |||
| 7 | use Doctrine\ORM\EntityManagerInterface; | ||
| 8 | use Doctrine\Persistence\Event\LifecycleEventArgs; | ||
| 9 | |||
| 10 | /** @extends LifecycleEventArgs<EntityManagerInterface> */ | ||
| 11 | final class PostRemoveEventArgs extends LifecycleEventArgs | ||
| 12 | { | ||
| 13 | } | ||
diff --git a/vendor/doctrine/orm/src/Event/PostUpdateEventArgs.php b/vendor/doctrine/orm/src/Event/PostUpdateEventArgs.php new file mode 100644 index 0000000..c9ff004 --- /dev/null +++ b/vendor/doctrine/orm/src/Event/PostUpdateEventArgs.php | |||
| @@ -0,0 +1,13 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Event; | ||
| 6 | |||
| 7 | use Doctrine\ORM\EntityManagerInterface; | ||
| 8 | use Doctrine\Persistence\Event\LifecycleEventArgs; | ||
| 9 | |||
| 10 | /** @extends LifecycleEventArgs<EntityManagerInterface> */ | ||
| 11 | final class PostUpdateEventArgs extends LifecycleEventArgs | ||
| 12 | { | ||
| 13 | } | ||
diff --git a/vendor/doctrine/orm/src/Event/PreFlushEventArgs.php b/vendor/doctrine/orm/src/Event/PreFlushEventArgs.php new file mode 100644 index 0000000..671535c --- /dev/null +++ b/vendor/doctrine/orm/src/Event/PreFlushEventArgs.php | |||
| @@ -0,0 +1,19 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Event; | ||
| 6 | |||
| 7 | use Doctrine\ORM\EntityManagerInterface; | ||
| 8 | use Doctrine\Persistence\Event\ManagerEventArgs; | ||
| 9 | |||
| 10 | /** | ||
| 11 | * Provides event arguments for the preFlush event. | ||
| 12 | * | ||
| 13 | * @link www.doctrine-project.com | ||
| 14 | * | ||
| 15 | * @extends ManagerEventArgs<EntityManagerInterface> | ||
| 16 | */ | ||
| 17 | class PreFlushEventArgs extends ManagerEventArgs | ||
| 18 | { | ||
| 19 | } | ||
diff --git a/vendor/doctrine/orm/src/Event/PrePersistEventArgs.php b/vendor/doctrine/orm/src/Event/PrePersistEventArgs.php new file mode 100644 index 0000000..e70c3cf --- /dev/null +++ b/vendor/doctrine/orm/src/Event/PrePersistEventArgs.php | |||
| @@ -0,0 +1,13 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Event; | ||
| 6 | |||
| 7 | use Doctrine\ORM\EntityManagerInterface; | ||
| 8 | use Doctrine\Persistence\Event\LifecycleEventArgs; | ||
| 9 | |||
| 10 | /** @extends LifecycleEventArgs<EntityManagerInterface> */ | ||
| 11 | final class PrePersistEventArgs extends LifecycleEventArgs | ||
| 12 | { | ||
| 13 | } | ||
diff --git a/vendor/doctrine/orm/src/Event/PreRemoveEventArgs.php b/vendor/doctrine/orm/src/Event/PreRemoveEventArgs.php new file mode 100644 index 0000000..3af0d02 --- /dev/null +++ b/vendor/doctrine/orm/src/Event/PreRemoveEventArgs.php | |||
| @@ -0,0 +1,13 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Event; | ||
| 6 | |||
| 7 | use Doctrine\ORM\EntityManagerInterface; | ||
| 8 | use Doctrine\Persistence\Event\LifecycleEventArgs; | ||
| 9 | |||
| 10 | /** @extends LifecycleEventArgs<EntityManagerInterface> */ | ||
| 11 | final class PreRemoveEventArgs extends LifecycleEventArgs | ||
| 12 | { | ||
| 13 | } | ||
diff --git a/vendor/doctrine/orm/src/Event/PreUpdateEventArgs.php b/vendor/doctrine/orm/src/Event/PreUpdateEventArgs.php new file mode 100644 index 0000000..090487b --- /dev/null +++ b/vendor/doctrine/orm/src/Event/PreUpdateEventArgs.php | |||
| @@ -0,0 +1,100 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Event; | ||
| 6 | |||
| 7 | use Doctrine\ORM\EntityManagerInterface; | ||
| 8 | use Doctrine\ORM\PersistentCollection; | ||
| 9 | use Doctrine\Persistence\Event\LifecycleEventArgs; | ||
| 10 | use InvalidArgumentException; | ||
| 11 | |||
| 12 | use function get_debug_type; | ||
| 13 | use function sprintf; | ||
| 14 | |||
| 15 | /** | ||
| 16 | * Class that holds event arguments for a preUpdate event. | ||
| 17 | * | ||
| 18 | * @extends LifecycleEventArgs<EntityManagerInterface> | ||
| 19 | */ | ||
| 20 | class PreUpdateEventArgs extends LifecycleEventArgs | ||
| 21 | { | ||
| 22 | /** @var array<string, array{mixed, mixed}|PersistentCollection> */ | ||
| 23 | private array $entityChangeSet; | ||
| 24 | |||
| 25 | /** | ||
| 26 | * @param mixed[][] $changeSet | ||
| 27 | * @psalm-param array<string, array{mixed, mixed}|PersistentCollection> $changeSet | ||
| 28 | */ | ||
| 29 | public function __construct(object $entity, EntityManagerInterface $em, array &$changeSet) | ||
| 30 | { | ||
| 31 | parent::__construct($entity, $em); | ||
| 32 | |||
| 33 | $this->entityChangeSet = &$changeSet; | ||
| 34 | } | ||
| 35 | |||
| 36 | /** | ||
| 37 | * Retrieves entity changeset. | ||
| 38 | * | ||
| 39 | * @return mixed[][] | ||
| 40 | * @psalm-return array<string, array{mixed, mixed}|PersistentCollection> | ||
| 41 | */ | ||
| 42 | public function getEntityChangeSet(): array | ||
| 43 | { | ||
| 44 | return $this->entityChangeSet; | ||
| 45 | } | ||
| 46 | |||
| 47 | /** | ||
| 48 | * Checks if field has a changeset. | ||
| 49 | */ | ||
| 50 | public function hasChangedField(string $field): bool | ||
| 51 | { | ||
| 52 | return isset($this->entityChangeSet[$field]); | ||
| 53 | } | ||
| 54 | |||
| 55 | /** | ||
| 56 | * Gets the old value of the changeset of the changed field. | ||
| 57 | */ | ||
| 58 | public function getOldValue(string $field): mixed | ||
| 59 | { | ||
| 60 | $this->assertValidField($field); | ||
| 61 | |||
| 62 | return $this->entityChangeSet[$field][0]; | ||
| 63 | } | ||
| 64 | |||
| 65 | /** | ||
| 66 | * Gets the new value of the changeset of the changed field. | ||
| 67 | */ | ||
| 68 | public function getNewValue(string $field): mixed | ||
| 69 | { | ||
| 70 | $this->assertValidField($field); | ||
| 71 | |||
| 72 | return $this->entityChangeSet[$field][1]; | ||
| 73 | } | ||
| 74 | |||
| 75 | /** | ||
| 76 | * Sets the new value of this field. | ||
| 77 | */ | ||
| 78 | public function setNewValue(string $field, mixed $value): void | ||
| 79 | { | ||
| 80 | $this->assertValidField($field); | ||
| 81 | |||
| 82 | $this->entityChangeSet[$field][1] = $value; | ||
| 83 | } | ||
| 84 | |||
| 85 | /** | ||
| 86 | * Asserts the field exists in changeset. | ||
| 87 | * | ||
| 88 | * @throws InvalidArgumentException | ||
| 89 | */ | ||
| 90 | private function assertValidField(string $field): void | ||
| 91 | { | ||
| 92 | if (! isset($this->entityChangeSet[$field])) { | ||
| 93 | throw new InvalidArgumentException(sprintf( | ||
| 94 | 'Field "%s" is not a valid field of the entity "%s" in PreUpdateEventArgs.', | ||
| 95 | $field, | ||
| 96 | get_debug_type($this->getObject()), | ||
| 97 | )); | ||
| 98 | } | ||
| 99 | } | ||
| 100 | } | ||
diff --git a/vendor/doctrine/orm/src/Events.php b/vendor/doctrine/orm/src/Events.php new file mode 100644 index 0000000..517917c --- /dev/null +++ b/vendor/doctrine/orm/src/Events.php | |||
| @@ -0,0 +1,125 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM; | ||
| 6 | |||
| 7 | /** | ||
| 8 | * Container for all ORM events. | ||
| 9 | * | ||
| 10 | * This class cannot be instantiated. | ||
| 11 | */ | ||
| 12 | final class Events | ||
| 13 | { | ||
| 14 | /** | ||
| 15 | * Private constructor. This class is not meant to be instantiated. | ||
| 16 | */ | ||
| 17 | private function __construct() | ||
| 18 | { | ||
| 19 | } | ||
| 20 | |||
| 21 | /** | ||
| 22 | * The preRemove event occurs for a given entity before the respective | ||
| 23 | * EntityManager remove operation for that entity is executed. | ||
| 24 | * | ||
| 25 | * This is an entity lifecycle event. | ||
| 26 | */ | ||
| 27 | public const preRemove = 'preRemove'; | ||
| 28 | |||
| 29 | /** | ||
| 30 | * The postRemove event occurs for an entity after the entity has | ||
| 31 | * been deleted. It will be invoked after the database delete operations. | ||
| 32 | * | ||
| 33 | * This is an entity lifecycle event. | ||
| 34 | */ | ||
| 35 | public const postRemove = 'postRemove'; | ||
| 36 | |||
| 37 | /** | ||
| 38 | * The prePersist event occurs for a given entity before the respective | ||
| 39 | * EntityManager persist operation for that entity is executed. | ||
| 40 | * | ||
| 41 | * This is an entity lifecycle event. | ||
| 42 | */ | ||
| 43 | public const prePersist = 'prePersist'; | ||
| 44 | |||
| 45 | /** | ||
| 46 | * The postPersist event occurs for an entity after the entity has | ||
| 47 | * been made persistent. It will be invoked after the database insert operations. | ||
| 48 | * Generated primary key values are available in the postPersist event. | ||
| 49 | * | ||
| 50 | * This is an entity lifecycle event. | ||
| 51 | */ | ||
| 52 | public const postPersist = 'postPersist'; | ||
| 53 | |||
| 54 | /** | ||
| 55 | * The preUpdate event occurs before the database update operations to | ||
| 56 | * entity data. | ||
| 57 | * | ||
| 58 | * This is an entity lifecycle event. | ||
| 59 | */ | ||
| 60 | public const preUpdate = 'preUpdate'; | ||
| 61 | |||
| 62 | /** | ||
| 63 | * The postUpdate event occurs after the database update operations to | ||
| 64 | * entity data. | ||
| 65 | * | ||
| 66 | * This is an entity lifecycle event. | ||
| 67 | */ | ||
| 68 | public const postUpdate = 'postUpdate'; | ||
| 69 | |||
| 70 | /** | ||
| 71 | * The postLoad event occurs for an entity after the entity has been loaded | ||
| 72 | * into the current EntityManager from the database or after the refresh operation | ||
| 73 | * has been applied to it. | ||
| 74 | * | ||
| 75 | * Note that the postLoad event occurs for an entity before any associations have been | ||
| 76 | * initialized. Therefore, it is not safe to access associations in a postLoad callback | ||
| 77 | * or event handler. | ||
| 78 | * | ||
| 79 | * This is an entity lifecycle event. | ||
| 80 | */ | ||
| 81 | public const postLoad = 'postLoad'; | ||
| 82 | |||
| 83 | /** | ||
| 84 | * The loadClassMetadata event occurs after the mapping metadata for a class | ||
| 85 | * has been loaded from a mapping source (attributes/xml). | ||
| 86 | */ | ||
| 87 | public const loadClassMetadata = 'loadClassMetadata'; | ||
| 88 | |||
| 89 | /** | ||
| 90 | * The onClassMetadataNotFound event occurs whenever loading metadata for a class | ||
| 91 | * failed. | ||
| 92 | */ | ||
| 93 | public const onClassMetadataNotFound = 'onClassMetadataNotFound'; | ||
| 94 | |||
| 95 | /** | ||
| 96 | * The preFlush event occurs when the EntityManager#flush() operation is invoked, | ||
| 97 | * but before any changes to managed entities have been calculated. This event is | ||
| 98 | * always raised right after EntityManager#flush() call. | ||
| 99 | */ | ||
| 100 | public const preFlush = 'preFlush'; | ||
| 101 | |||
| 102 | /** | ||
| 103 | * The onFlush event occurs when the EntityManager#flush() operation is invoked, | ||
| 104 | * after any changes to managed entities have been determined but before any | ||
| 105 | * actual database operations are executed. The event is only raised if there is | ||
| 106 | * actually something to do for the underlying UnitOfWork. If nothing needs to be done, | ||
| 107 | * the onFlush event is not raised. | ||
| 108 | */ | ||
| 109 | public const onFlush = 'onFlush'; | ||
| 110 | |||
| 111 | /** | ||
| 112 | * The postFlush event occurs when the EntityManager#flush() operation is invoked and | ||
| 113 | * after all actual database operations are executed successfully. The event is only raised if there is | ||
| 114 | * actually something to do for the underlying UnitOfWork. If nothing needs to be done, | ||
| 115 | * the postFlush event is not raised. The event won't be raised if an error occurs during the | ||
| 116 | * flush operation. | ||
| 117 | */ | ||
| 118 | public const postFlush = 'postFlush'; | ||
| 119 | |||
| 120 | /** | ||
| 121 | * The onClear event occurs when the EntityManager#clear() operation is invoked, | ||
| 122 | * after all references to entities have been removed from the unit of work. | ||
| 123 | */ | ||
| 124 | public const onClear = 'onClear'; | ||
| 125 | } | ||
diff --git a/vendor/doctrine/orm/src/Exception/ConfigurationException.php b/vendor/doctrine/orm/src/Exception/ConfigurationException.php new file mode 100644 index 0000000..45cf83f --- /dev/null +++ b/vendor/doctrine/orm/src/Exception/ConfigurationException.php | |||
| @@ -0,0 +1,9 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Exception; | ||
| 6 | |||
| 7 | interface ConfigurationException extends ORMException | ||
| 8 | { | ||
| 9 | } | ||
diff --git a/vendor/doctrine/orm/src/Exception/EntityIdentityCollisionException.php b/vendor/doctrine/orm/src/Exception/EntityIdentityCollisionException.php new file mode 100644 index 0000000..0af3162 --- /dev/null +++ b/vendor/doctrine/orm/src/Exception/EntityIdentityCollisionException.php | |||
| @@ -0,0 +1,39 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Exception; | ||
| 6 | |||
| 7 | use Exception; | ||
| 8 | |||
| 9 | use function sprintf; | ||
| 10 | |||
| 11 | final class EntityIdentityCollisionException extends Exception implements ORMException | ||
| 12 | { | ||
| 13 | public static function create(object $existingEntity, object $newEntity, string $idHash): self | ||
| 14 | { | ||
| 15 | return new self( | ||
| 16 | sprintf( | ||
| 17 | <<<'EXCEPTION' | ||
| 18 | While adding an entity of class %s with an ID hash of "%s" to the identity map, | ||
| 19 | another object of class %s was already present for the same ID. This exception | ||
| 20 | is a safeguard against an internal inconsistency - IDs should uniquely map to | ||
| 21 | entity object instances. This problem may occur if: | ||
| 22 | |||
| 23 | - you use application-provided IDs and reuse ID values; | ||
| 24 | - database-provided IDs are reassigned after truncating the database without | ||
| 25 | clearing the EntityManager; | ||
| 26 | - you might have been using EntityManager#getReference() to create a reference | ||
| 27 | for a nonexistent ID that was subsequently (by the RDBMS) assigned to another | ||
| 28 | entity. | ||
| 29 | |||
| 30 | Otherwise, it might be an ORM-internal inconsistency, please report it. | ||
| 31 | EXCEPTION | ||
| 32 | , | ||
| 33 | $newEntity::class, | ||
| 34 | $idHash, | ||
| 35 | $existingEntity::class, | ||
| 36 | ), | ||
| 37 | ); | ||
| 38 | } | ||
| 39 | } | ||
diff --git a/vendor/doctrine/orm/src/Exception/EntityManagerClosed.php b/vendor/doctrine/orm/src/Exception/EntityManagerClosed.php new file mode 100644 index 0000000..a769202 --- /dev/null +++ b/vendor/doctrine/orm/src/Exception/EntityManagerClosed.php | |||
| @@ -0,0 +1,15 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Exception; | ||
| 6 | |||
| 7 | use RuntimeException; | ||
| 8 | |||
| 9 | final class EntityManagerClosed extends RuntimeException implements ManagerException | ||
| 10 | { | ||
| 11 | public static function create(): self | ||
| 12 | { | ||
| 13 | return new self('The EntityManager is closed.'); | ||
| 14 | } | ||
| 15 | } | ||
diff --git a/vendor/doctrine/orm/src/Exception/EntityMissingAssignedId.php b/vendor/doctrine/orm/src/Exception/EntityMissingAssignedId.php new file mode 100644 index 0000000..d566436 --- /dev/null +++ b/vendor/doctrine/orm/src/Exception/EntityMissingAssignedId.php | |||
| @@ -0,0 +1,20 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Exception; | ||
| 6 | |||
| 7 | use LogicException; | ||
| 8 | |||
| 9 | use function get_debug_type; | ||
| 10 | |||
| 11 | final class EntityMissingAssignedId extends LogicException implements ORMException | ||
| 12 | { | ||
| 13 | public static function forField(object $entity, string $field): self | ||
| 14 | { | ||
| 15 | return new self('Entity of type ' . get_debug_type($entity) . " is missing an assigned ID for field '" . $field . "'. " . | ||
| 16 | 'The identifier generation strategy for this entity requires the ID field to be populated before ' . | ||
| 17 | 'EntityManager#persist() is called. If you want automatically generated identifiers instead ' . | ||
| 18 | 'you need to adjust the metadata mapping accordingly.'); | ||
| 19 | } | ||
| 20 | } | ||
diff --git a/vendor/doctrine/orm/src/Exception/InvalidEntityRepository.php b/vendor/doctrine/orm/src/Exception/InvalidEntityRepository.php new file mode 100644 index 0000000..c28143d --- /dev/null +++ b/vendor/doctrine/orm/src/Exception/InvalidEntityRepository.php | |||
| @@ -0,0 +1,18 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Exception; | ||
| 6 | |||
| 7 | use Doctrine\ORM\EntityRepository; | ||
| 8 | use LogicException; | ||
| 9 | |||
| 10 | final class InvalidEntityRepository extends LogicException implements ConfigurationException | ||
| 11 | { | ||
| 12 | public static function fromClassName(string $className): self | ||
| 13 | { | ||
| 14 | return new self( | ||
| 15 | "Invalid repository class '" . $className . "'. It must be a " . EntityRepository::class . '.', | ||
| 16 | ); | ||
| 17 | } | ||
| 18 | } | ||
diff --git a/vendor/doctrine/orm/src/Exception/InvalidHydrationMode.php b/vendor/doctrine/orm/src/Exception/InvalidHydrationMode.php new file mode 100644 index 0000000..d07d84c --- /dev/null +++ b/vendor/doctrine/orm/src/Exception/InvalidHydrationMode.php | |||
| @@ -0,0 +1,17 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Exception; | ||
| 6 | |||
| 7 | use LogicException; | ||
| 8 | |||
| 9 | use function sprintf; | ||
| 10 | |||
| 11 | final class InvalidHydrationMode extends LogicException implements ManagerException | ||
| 12 | { | ||
| 13 | public static function fromMode(string $mode): self | ||
| 14 | { | ||
| 15 | return new self(sprintf('"%s" is an invalid hydration mode.', $mode)); | ||
| 16 | } | ||
| 17 | } | ||
diff --git a/vendor/doctrine/orm/src/Exception/ManagerException.php b/vendor/doctrine/orm/src/Exception/ManagerException.php new file mode 100644 index 0000000..f9bc7ff --- /dev/null +++ b/vendor/doctrine/orm/src/Exception/ManagerException.php | |||
| @@ -0,0 +1,11 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Exception; | ||
| 6 | |||
| 7 | use Throwable; | ||
| 8 | |||
| 9 | interface ManagerException extends Throwable | ||
| 10 | { | ||
| 11 | } | ||
diff --git a/vendor/doctrine/orm/src/Exception/MissingIdentifierField.php b/vendor/doctrine/orm/src/Exception/MissingIdentifierField.php new file mode 100644 index 0000000..2c02db4 --- /dev/null +++ b/vendor/doctrine/orm/src/Exception/MissingIdentifierField.php | |||
| @@ -0,0 +1,21 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Exception; | ||
| 6 | |||
| 7 | use LogicException; | ||
| 8 | |||
| 9 | use function sprintf; | ||
| 10 | |||
| 11 | final class MissingIdentifierField extends LogicException implements ManagerException | ||
| 12 | { | ||
| 13 | public static function fromFieldAndClass(string $fieldName, string $className): self | ||
| 14 | { | ||
| 15 | return new self(sprintf( | ||
| 16 | 'The identifier %s is missing for a query of %s', | ||
| 17 | $fieldName, | ||
| 18 | $className, | ||
| 19 | )); | ||
| 20 | } | ||
| 21 | } | ||
diff --git a/vendor/doctrine/orm/src/Exception/MissingMappingDriverImplementation.php b/vendor/doctrine/orm/src/Exception/MissingMappingDriverImplementation.php new file mode 100644 index 0000000..ce5104b --- /dev/null +++ b/vendor/doctrine/orm/src/Exception/MissingMappingDriverImplementation.php | |||
| @@ -0,0 +1,18 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Exception; | ||
| 6 | |||
| 7 | use LogicException; | ||
| 8 | |||
| 9 | final class MissingMappingDriverImplementation extends LogicException implements ManagerException | ||
| 10 | { | ||
| 11 | public static function create(): self | ||
| 12 | { | ||
| 13 | return new self( | ||
| 14 | "It's a requirement to specify a Metadata Driver and pass it " . | ||
| 15 | 'to Doctrine\\ORM\\Configuration::setMetadataDriverImpl().', | ||
| 16 | ); | ||
| 17 | } | ||
| 18 | } | ||
diff --git a/vendor/doctrine/orm/src/Exception/MultipleSelectorsFoundException.php b/vendor/doctrine/orm/src/Exception/MultipleSelectorsFoundException.php new file mode 100644 index 0000000..8084d66 --- /dev/null +++ b/vendor/doctrine/orm/src/Exception/MultipleSelectorsFoundException.php | |||
| @@ -0,0 +1,26 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Exception; | ||
| 6 | |||
| 7 | use LogicException; | ||
| 8 | |||
| 9 | use function implode; | ||
| 10 | use function sprintf; | ||
| 11 | |||
| 12 | final class MultipleSelectorsFoundException extends LogicException implements ORMException | ||
| 13 | { | ||
| 14 | public const MULTIPLE_SELECTORS_FOUND_EXCEPTION = 'Multiple selectors found: %s. Please select only one.'; | ||
| 15 | |||
| 16 | /** @param string[] $selectors */ | ||
| 17 | public static function create(array $selectors): self | ||
| 18 | { | ||
| 19 | return new self( | ||
| 20 | sprintf( | ||
| 21 | self::MULTIPLE_SELECTORS_FOUND_EXCEPTION, | ||
| 22 | implode(', ', $selectors), | ||
| 23 | ), | ||
| 24 | ); | ||
| 25 | } | ||
| 26 | } | ||
diff --git a/vendor/doctrine/orm/src/Exception/NotSupported.php b/vendor/doctrine/orm/src/Exception/NotSupported.php new file mode 100644 index 0000000..9192f87 --- /dev/null +++ b/vendor/doctrine/orm/src/Exception/NotSupported.php | |||
| @@ -0,0 +1,44 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Exception; | ||
| 6 | |||
| 7 | use LogicException; | ||
| 8 | |||
| 9 | use function sprintf; | ||
| 10 | |||
| 11 | /** @deprecated */ | ||
| 12 | final class NotSupported extends LogicException implements ORMException | ||
| 13 | { | ||
| 14 | public static function create(): self | ||
| 15 | { | ||
| 16 | return new self('This behaviour is (currently) not supported by Doctrine 2'); | ||
| 17 | } | ||
| 18 | |||
| 19 | public static function createForDbal3(string $context): self | ||
| 20 | { | ||
| 21 | return new self(sprintf( | ||
| 22 | <<<'EXCEPTION' | ||
| 23 | Context: %s | ||
| 24 | Problem: Feature was deprecated in doctrine/dbal 2.x and is not supported by installed doctrine/dbal:3.x | ||
| 25 | Solution: See the doctrine/deprecations logs for new alternative approaches. | ||
| 26 | EXCEPTION | ||
| 27 | , | ||
| 28 | $context, | ||
| 29 | )); | ||
| 30 | } | ||
| 31 | |||
| 32 | public static function createForPersistence3(string $context): self | ||
| 33 | { | ||
| 34 | return new self(sprintf( | ||
| 35 | <<<'EXCEPTION' | ||
| 36 | Context: %s | ||
| 37 | Problem: Feature was deprecated in doctrine/persistence 2.x and is not supported by installed doctrine/persistence:3.x | ||
| 38 | Solution: See the doctrine/deprecations logs for new alternative approaches. | ||
| 39 | EXCEPTION | ||
| 40 | , | ||
| 41 | $context, | ||
| 42 | )); | ||
| 43 | } | ||
| 44 | } | ||
diff --git a/vendor/doctrine/orm/src/Exception/ORMException.php b/vendor/doctrine/orm/src/Exception/ORMException.php new file mode 100644 index 0000000..a59b483 --- /dev/null +++ b/vendor/doctrine/orm/src/Exception/ORMException.php | |||
| @@ -0,0 +1,11 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Exception; | ||
| 6 | |||
| 7 | use Throwable; | ||
| 8 | |||
| 9 | interface ORMException extends Throwable | ||
| 10 | { | ||
| 11 | } | ||
diff --git a/vendor/doctrine/orm/src/Exception/PersisterException.php b/vendor/doctrine/orm/src/Exception/PersisterException.php new file mode 100644 index 0000000..2563a46 --- /dev/null +++ b/vendor/doctrine/orm/src/Exception/PersisterException.php | |||
| @@ -0,0 +1,11 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Exception; | ||
| 6 | |||
| 7 | use Doctrine\ORM\Persisters\PersisterException as BasePersisterException; | ||
| 8 | |||
| 9 | class PersisterException extends BasePersisterException | ||
| 10 | { | ||
| 11 | } | ||
diff --git a/vendor/doctrine/orm/src/Exception/RepositoryException.php b/vendor/doctrine/orm/src/Exception/RepositoryException.php new file mode 100644 index 0000000..3b2d51b --- /dev/null +++ b/vendor/doctrine/orm/src/Exception/RepositoryException.php | |||
| @@ -0,0 +1,13 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Exception; | ||
| 6 | |||
| 7 | /** | ||
| 8 | * This interface should be implemented by all exceptions in the Repository | ||
| 9 | * namespace. | ||
| 10 | */ | ||
| 11 | interface RepositoryException extends ORMException | ||
| 12 | { | ||
| 13 | } | ||
diff --git a/vendor/doctrine/orm/src/Exception/SchemaToolException.php b/vendor/doctrine/orm/src/Exception/SchemaToolException.php new file mode 100644 index 0000000..e4477d0 --- /dev/null +++ b/vendor/doctrine/orm/src/Exception/SchemaToolException.php | |||
| @@ -0,0 +1,11 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Exception; | ||
| 6 | |||
| 7 | use Throwable; | ||
| 8 | |||
| 9 | interface SchemaToolException extends Throwable | ||
| 10 | { | ||
| 11 | } | ||
diff --git a/vendor/doctrine/orm/src/Exception/UnexpectedAssociationValue.php b/vendor/doctrine/orm/src/Exception/UnexpectedAssociationValue.php new file mode 100644 index 0000000..cf3ca18 --- /dev/null +++ b/vendor/doctrine/orm/src/Exception/UnexpectedAssociationValue.php | |||
| @@ -0,0 +1,27 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Exception; | ||
| 6 | |||
| 7 | use Doctrine\ORM\Cache\Exception\CacheException; | ||
| 8 | |||
| 9 | use function sprintf; | ||
| 10 | |||
| 11 | final class UnexpectedAssociationValue extends CacheException | ||
| 12 | { | ||
| 13 | public static function create( | ||
| 14 | string $class, | ||
| 15 | string $association, | ||
| 16 | string $given, | ||
| 17 | string $expected, | ||
| 18 | ): self { | ||
| 19 | return new self(sprintf( | ||
| 20 | 'Found entity of type %s on association %s#%s, but expecting %s', | ||
| 21 | $given, | ||
| 22 | $class, | ||
| 23 | $association, | ||
| 24 | $expected, | ||
| 25 | )); | ||
| 26 | } | ||
| 27 | } | ||
diff --git a/vendor/doctrine/orm/src/Exception/UnrecognizedIdentifierFields.php b/vendor/doctrine/orm/src/Exception/UnrecognizedIdentifierFields.php new file mode 100644 index 0000000..645bdae --- /dev/null +++ b/vendor/doctrine/orm/src/Exception/UnrecognizedIdentifierFields.php | |||
| @@ -0,0 +1,23 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Exception; | ||
| 6 | |||
| 7 | use LogicException; | ||
| 8 | |||
| 9 | use function implode; | ||
| 10 | use function sprintf; | ||
| 11 | |||
| 12 | final class UnrecognizedIdentifierFields extends LogicException implements ManagerException | ||
| 13 | { | ||
| 14 | /** @param string[] $fieldNames */ | ||
| 15 | public static function fromClassAndFieldNames(string $className, array $fieldNames): self | ||
| 16 | { | ||
| 17 | return new self(sprintf( | ||
| 18 | 'Unrecognized identifier fields: "%s" are not present on class "%s".', | ||
| 19 | implode("', '", $fieldNames), | ||
| 20 | $className, | ||
| 21 | )); | ||
| 22 | } | ||
| 23 | } | ||
diff --git a/vendor/doctrine/orm/src/Id/AbstractIdGenerator.php b/vendor/doctrine/orm/src/Id/AbstractIdGenerator.php new file mode 100644 index 0000000..6d981f8 --- /dev/null +++ b/vendor/doctrine/orm/src/Id/AbstractIdGenerator.php | |||
| @@ -0,0 +1,28 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Id; | ||
| 6 | |||
| 7 | use Doctrine\ORM\EntityManagerInterface; | ||
| 8 | |||
| 9 | abstract class AbstractIdGenerator | ||
| 10 | { | ||
| 11 | /** | ||
| 12 | * Generates an identifier for an entity. | ||
| 13 | */ | ||
| 14 | abstract public function generateId(EntityManagerInterface $em, object|null $entity): mixed; | ||
| 15 | |||
| 16 | /** | ||
| 17 | * Gets whether this generator is a post-insert generator which means that | ||
| 18 | * {@link generateId()} must be called after the entity has been inserted | ||
| 19 | * into the database. | ||
| 20 | * | ||
| 21 | * By default, this method returns FALSE. Generators that have this requirement | ||
| 22 | * must override this method and return TRUE. | ||
| 23 | */ | ||
| 24 | public function isPostInsertGenerator(): bool | ||
| 25 | { | ||
| 26 | return false; | ||
| 27 | } | ||
| 28 | } | ||
diff --git a/vendor/doctrine/orm/src/Id/AssignedGenerator.php b/vendor/doctrine/orm/src/Id/AssignedGenerator.php new file mode 100644 index 0000000..e11b341 --- /dev/null +++ b/vendor/doctrine/orm/src/Id/AssignedGenerator.php | |||
| @@ -0,0 +1,45 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Id; | ||
| 6 | |||
| 7 | use Doctrine\ORM\EntityManagerInterface; | ||
| 8 | use Doctrine\ORM\Exception\EntityMissingAssignedId; | ||
| 9 | |||
| 10 | /** | ||
| 11 | * Special generator for application-assigned identifiers (doesn't really generate anything). | ||
| 12 | */ | ||
| 13 | class AssignedGenerator extends AbstractIdGenerator | ||
| 14 | { | ||
| 15 | /** | ||
| 16 | * Returns the identifier assigned to the given entity. | ||
| 17 | * | ||
| 18 | * {@inheritDoc} | ||
| 19 | * | ||
| 20 | * @throws EntityMissingAssignedId | ||
| 21 | */ | ||
| 22 | public function generateId(EntityManagerInterface $em, object|null $entity): array | ||
| 23 | { | ||
| 24 | $class = $em->getClassMetadata($entity::class); | ||
| 25 | $idFields = $class->getIdentifierFieldNames(); | ||
| 26 | $identifier = []; | ||
| 27 | |||
| 28 | foreach ($idFields as $idField) { | ||
| 29 | $value = $class->getFieldValue($entity, $idField); | ||
| 30 | |||
| 31 | if (! isset($value)) { | ||
| 32 | throw EntityMissingAssignedId::forField($entity, $idField); | ||
| 33 | } | ||
| 34 | |||
| 35 | if (isset($class->associationMappings[$idField])) { | ||
| 36 | // NOTE: Single Columns as associated identifiers only allowed - this constraint it is enforced. | ||
| 37 | $value = $em->getUnitOfWork()->getSingleIdentifierValue($value); | ||
| 38 | } | ||
| 39 | |||
| 40 | $identifier[$idField] = $value; | ||
| 41 | } | ||
| 42 | |||
| 43 | return $identifier; | ||
| 44 | } | ||
| 45 | } | ||
diff --git a/vendor/doctrine/orm/src/Id/BigIntegerIdentityGenerator.php b/vendor/doctrine/orm/src/Id/BigIntegerIdentityGenerator.php new file mode 100644 index 0000000..762a7cb --- /dev/null +++ b/vendor/doctrine/orm/src/Id/BigIntegerIdentityGenerator.php | |||
| @@ -0,0 +1,25 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Id; | ||
| 6 | |||
| 7 | use Doctrine\ORM\EntityManagerInterface; | ||
| 8 | |||
| 9 | /** | ||
| 10 | * Id generator that obtains IDs from special "identity" columns. These are columns | ||
| 11 | * that automatically get a database-generated, auto-incremented identifier on INSERT. | ||
| 12 | * This generator obtains the last insert id after such an insert. | ||
| 13 | */ | ||
| 14 | class BigIntegerIdentityGenerator extends AbstractIdGenerator | ||
| 15 | { | ||
| 16 | public function generateId(EntityManagerInterface $em, object|null $entity): string | ||
| 17 | { | ||
| 18 | return (string) $em->getConnection()->lastInsertId(); | ||
| 19 | } | ||
| 20 | |||
| 21 | public function isPostInsertGenerator(): bool | ||
| 22 | { | ||
| 23 | return true; | ||
| 24 | } | ||
| 25 | } | ||
diff --git a/vendor/doctrine/orm/src/Id/IdentityGenerator.php b/vendor/doctrine/orm/src/Id/IdentityGenerator.php new file mode 100644 index 0000000..4610f66 --- /dev/null +++ b/vendor/doctrine/orm/src/Id/IdentityGenerator.php | |||
| @@ -0,0 +1,25 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Id; | ||
| 6 | |||
| 7 | use Doctrine\ORM\EntityManagerInterface; | ||
| 8 | |||
| 9 | /** | ||
| 10 | * Id generator that obtains IDs from special "identity" columns. These are columns | ||
| 11 | * that automatically get a database-generated, auto-incremented identifier on INSERT. | ||
| 12 | * This generator obtains the last insert id after such an insert. | ||
| 13 | */ | ||
| 14 | class IdentityGenerator extends AbstractIdGenerator | ||
| 15 | { | ||
| 16 | public function generateId(EntityManagerInterface $em, object|null $entity): int | ||
| 17 | { | ||
| 18 | return (int) $em->getConnection()->lastInsertId(); | ||
| 19 | } | ||
| 20 | |||
| 21 | public function isPostInsertGenerator(): bool | ||
| 22 | { | ||
| 23 | return true; | ||
| 24 | } | ||
| 25 | } | ||
diff --git a/vendor/doctrine/orm/src/Id/SequenceGenerator.php b/vendor/doctrine/orm/src/Id/SequenceGenerator.php new file mode 100644 index 0000000..659bb58 --- /dev/null +++ b/vendor/doctrine/orm/src/Id/SequenceGenerator.php | |||
| @@ -0,0 +1,112 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Id; | ||
| 6 | |||
| 7 | use Doctrine\DBAL\Connections\PrimaryReadReplicaConnection; | ||
| 8 | use Doctrine\Deprecations\Deprecation; | ||
| 9 | use Doctrine\ORM\EntityManagerInterface; | ||
| 10 | use Serializable; | ||
| 11 | |||
| 12 | use function serialize; | ||
| 13 | use function unserialize; | ||
| 14 | |||
| 15 | /** | ||
| 16 | * Represents an ID generator that uses a database sequence. | ||
| 17 | */ | ||
| 18 | class SequenceGenerator extends AbstractIdGenerator implements Serializable | ||
| 19 | { | ||
| 20 | private int $nextValue = 0; | ||
| 21 | private int|null $maxValue = null; | ||
| 22 | |||
| 23 | /** | ||
| 24 | * Initializes a new sequence generator. | ||
| 25 | * | ||
| 26 | * @param string $sequenceName The name of the sequence. | ||
| 27 | * @param int $allocationSize The allocation size of the sequence. | ||
| 28 | */ | ||
| 29 | public function __construct( | ||
| 30 | private string $sequenceName, | ||
| 31 | private int $allocationSize, | ||
| 32 | ) { | ||
| 33 | } | ||
| 34 | |||
| 35 | public function generateId(EntityManagerInterface $em, object|null $entity): int | ||
| 36 | { | ||
| 37 | if ($this->maxValue === null || $this->nextValue === $this->maxValue) { | ||
| 38 | // Allocate new values | ||
| 39 | $connection = $em->getConnection(); | ||
| 40 | $sql = $connection->getDatabasePlatform()->getSequenceNextValSQL($this->sequenceName); | ||
| 41 | |||
| 42 | if ($connection instanceof PrimaryReadReplicaConnection) { | ||
| 43 | $connection->ensureConnectedToPrimary(); | ||
| 44 | } | ||
| 45 | |||
| 46 | $this->nextValue = (int) $connection->fetchOne($sql); | ||
| 47 | $this->maxValue = $this->nextValue + $this->allocationSize; | ||
| 48 | } | ||
| 49 | |||
| 50 | return $this->nextValue++; | ||
| 51 | } | ||
| 52 | |||
| 53 | /** | ||
| 54 | * Gets the maximum value of the currently allocated bag of values. | ||
| 55 | */ | ||
| 56 | public function getCurrentMaxValue(): int|null | ||
| 57 | { | ||
| 58 | return $this->maxValue; | ||
| 59 | } | ||
| 60 | |||
| 61 | /** | ||
| 62 | * Gets the next value that will be returned by generate(). | ||
| 63 | */ | ||
| 64 | public function getNextValue(): int | ||
| 65 | { | ||
| 66 | return $this->nextValue; | ||
| 67 | } | ||
| 68 | |||
| 69 | /** @deprecated without replacement. */ | ||
| 70 | final public function serialize(): string | ||
| 71 | { | ||
| 72 | Deprecation::trigger( | ||
| 73 | 'doctrine/orm', | ||
| 74 | 'https://github.com/doctrine/orm/pull/11468', | ||
| 75 | '%s() is deprecated, use __serialize() instead. %s won\'t implement the Serializable interface anymore in ORM 4.', | ||
| 76 | __METHOD__, | ||
| 77 | self::class, | ||
| 78 | ); | ||
| 79 | |||
| 80 | return serialize($this->__serialize()); | ||
| 81 | } | ||
| 82 | |||
| 83 | /** @return array<string, mixed> */ | ||
| 84 | public function __serialize(): array | ||
| 85 | { | ||
| 86 | return [ | ||
| 87 | 'allocationSize' => $this->allocationSize, | ||
| 88 | 'sequenceName' => $this->sequenceName, | ||
| 89 | ]; | ||
| 90 | } | ||
| 91 | |||
| 92 | /** @deprecated without replacement. */ | ||
| 93 | final public function unserialize(string $serialized): void | ||
| 94 | { | ||
| 95 | Deprecation::trigger( | ||
| 96 | 'doctrine/orm', | ||
| 97 | 'https://github.com/doctrine/orm/pull/11468', | ||
| 98 | '%s() is deprecated, use __unserialize() instead. %s won\'t implement the Serializable interface anymore in ORM 4.', | ||
| 99 | __METHOD__, | ||
| 100 | self::class, | ||
| 101 | ); | ||
| 102 | |||
| 103 | $this->__unserialize(unserialize($serialized)); | ||
| 104 | } | ||
| 105 | |||
| 106 | /** @param array<string, mixed> $data */ | ||
| 107 | public function __unserialize(array $data): void | ||
| 108 | { | ||
| 109 | $this->sequenceName = $data['sequenceName']; | ||
| 110 | $this->allocationSize = $data['allocationSize']; | ||
| 111 | } | ||
| 112 | } | ||
diff --git a/vendor/doctrine/orm/src/Internal/Hydration/AbstractHydrator.php b/vendor/doctrine/orm/src/Internal/Hydration/AbstractHydrator.php new file mode 100644 index 0000000..d8bffe4 --- /dev/null +++ b/vendor/doctrine/orm/src/Internal/Hydration/AbstractHydrator.php | |||
| @@ -0,0 +1,556 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Internal\Hydration; | ||
| 6 | |||
| 7 | use BackedEnum; | ||
| 8 | use Doctrine\DBAL\Platforms\AbstractPlatform; | ||
| 9 | use Doctrine\DBAL\Result; | ||
| 10 | use Doctrine\DBAL\Types\Type; | ||
| 11 | use Doctrine\ORM\EntityManagerInterface; | ||
| 12 | use Doctrine\ORM\Events; | ||
| 13 | use Doctrine\ORM\Mapping\ClassMetadata; | ||
| 14 | use Doctrine\ORM\Query\ResultSetMapping; | ||
| 15 | use Doctrine\ORM\Tools\Pagination\LimitSubqueryWalker; | ||
| 16 | use Doctrine\ORM\UnitOfWork; | ||
| 17 | use Generator; | ||
| 18 | use LogicException; | ||
| 19 | use ReflectionClass; | ||
| 20 | |||
| 21 | use function array_map; | ||
| 22 | use function array_merge; | ||
| 23 | use function count; | ||
| 24 | use function end; | ||
| 25 | use function in_array; | ||
| 26 | use function is_array; | ||
| 27 | |||
| 28 | /** | ||
| 29 | * Base class for all hydrators. A hydrator is a class that provides some form | ||
| 30 | * of transformation of an SQL result set into another structure. | ||
| 31 | * | ||
| 32 | * @psalm-consistent-constructor | ||
| 33 | */ | ||
| 34 | abstract class AbstractHydrator | ||
| 35 | { | ||
| 36 | /** | ||
| 37 | * The ResultSetMapping. | ||
| 38 | */ | ||
| 39 | protected ResultSetMapping|null $rsm = null; | ||
| 40 | |||
| 41 | /** | ||
| 42 | * The dbms Platform instance. | ||
| 43 | */ | ||
| 44 | protected AbstractPlatform $platform; | ||
| 45 | |||
| 46 | /** | ||
| 47 | * The UnitOfWork of the associated EntityManager. | ||
| 48 | */ | ||
| 49 | protected UnitOfWork $uow; | ||
| 50 | |||
| 51 | /** | ||
| 52 | * Local ClassMetadata cache to avoid going to the EntityManager all the time. | ||
| 53 | * | ||
| 54 | * @var array<string, ClassMetadata<object>> | ||
| 55 | */ | ||
| 56 | protected array $metadataCache = []; | ||
| 57 | |||
| 58 | /** | ||
| 59 | * The cache used during row-by-row hydration. | ||
| 60 | * | ||
| 61 | * @var array<string, mixed[]|null> | ||
| 62 | */ | ||
| 63 | protected array $cache = []; | ||
| 64 | |||
| 65 | /** | ||
| 66 | * The statement that provides the data to hydrate. | ||
| 67 | */ | ||
| 68 | protected Result|null $stmt = null; | ||
| 69 | |||
| 70 | /** | ||
| 71 | * The query hints. | ||
| 72 | * | ||
| 73 | * @var array<string, mixed> | ||
| 74 | */ | ||
| 75 | protected array $hints = []; | ||
| 76 | |||
| 77 | /** | ||
| 78 | * Initializes a new instance of a class derived from <tt>AbstractHydrator</tt>. | ||
| 79 | */ | ||
| 80 | public function __construct(protected EntityManagerInterface $em) | ||
| 81 | { | ||
| 82 | $this->platform = $em->getConnection()->getDatabasePlatform(); | ||
| 83 | $this->uow = $em->getUnitOfWork(); | ||
| 84 | } | ||
| 85 | |||
| 86 | /** | ||
| 87 | * Initiates a row-by-row hydration. | ||
| 88 | * | ||
| 89 | * @psalm-param array<string, mixed> $hints | ||
| 90 | * | ||
| 91 | * @return Generator<array-key, mixed> | ||
| 92 | * | ||
| 93 | * @final | ||
| 94 | */ | ||
| 95 | final public function toIterable(Result $stmt, ResultSetMapping $resultSetMapping, array $hints = []): Generator | ||
| 96 | { | ||
| 97 | $this->stmt = $stmt; | ||
| 98 | $this->rsm = $resultSetMapping; | ||
| 99 | $this->hints = $hints; | ||
| 100 | |||
| 101 | $evm = $this->em->getEventManager(); | ||
| 102 | |||
| 103 | $evm->addEventListener([Events::onClear], $this); | ||
| 104 | |||
| 105 | $this->prepare(); | ||
| 106 | |||
| 107 | try { | ||
| 108 | while (true) { | ||
| 109 | $row = $this->statement()->fetchAssociative(); | ||
| 110 | |||
| 111 | if ($row === false) { | ||
| 112 | break; | ||
| 113 | } | ||
| 114 | |||
| 115 | $result = []; | ||
| 116 | |||
| 117 | $this->hydrateRowData($row, $result); | ||
| 118 | |||
| 119 | $this->cleanupAfterRowIteration(); | ||
| 120 | if (count($result) === 1) { | ||
| 121 | if (count($resultSetMapping->indexByMap) === 0) { | ||
| 122 | yield end($result); | ||
| 123 | } else { | ||
| 124 | yield from $result; | ||
| 125 | } | ||
| 126 | } else { | ||
| 127 | yield $result; | ||
| 128 | } | ||
| 129 | } | ||
| 130 | } finally { | ||
| 131 | $this->cleanup(); | ||
| 132 | } | ||
| 133 | } | ||
| 134 | |||
| 135 | final protected function statement(): Result | ||
| 136 | { | ||
| 137 | if ($this->stmt === null) { | ||
| 138 | throw new LogicException('Uninitialized _stmt property'); | ||
| 139 | } | ||
| 140 | |||
| 141 | return $this->stmt; | ||
| 142 | } | ||
| 143 | |||
| 144 | final protected function resultSetMapping(): ResultSetMapping | ||
| 145 | { | ||
| 146 | if ($this->rsm === null) { | ||
| 147 | throw new LogicException('Uninitialized _rsm property'); | ||
| 148 | } | ||
| 149 | |||
| 150 | return $this->rsm; | ||
| 151 | } | ||
| 152 | |||
| 153 | /** | ||
| 154 | * Hydrates all rows returned by the passed statement instance at once. | ||
| 155 | * | ||
| 156 | * @psalm-param array<string, string> $hints | ||
| 157 | */ | ||
| 158 | public function hydrateAll(Result $stmt, ResultSetMapping $resultSetMapping, array $hints = []): mixed | ||
| 159 | { | ||
| 160 | $this->stmt = $stmt; | ||
| 161 | $this->rsm = $resultSetMapping; | ||
| 162 | $this->hints = $hints; | ||
| 163 | |||
| 164 | $this->em->getEventManager()->addEventListener([Events::onClear], $this); | ||
| 165 | $this->prepare(); | ||
| 166 | |||
| 167 | try { | ||
| 168 | $result = $this->hydrateAllData(); | ||
| 169 | } finally { | ||
| 170 | $this->cleanup(); | ||
| 171 | } | ||
| 172 | |||
| 173 | return $result; | ||
| 174 | } | ||
| 175 | |||
| 176 | /** | ||
| 177 | * When executed in a hydrate() loop we have to clear internal state to | ||
| 178 | * decrease memory consumption. | ||
| 179 | */ | ||
| 180 | public function onClear(mixed $eventArgs): void | ||
| 181 | { | ||
| 182 | } | ||
| 183 | |||
| 184 | /** | ||
| 185 | * Executes one-time preparation tasks, once each time hydration is started | ||
| 186 | * through {@link hydrateAll} or {@link toIterable()}. | ||
| 187 | */ | ||
| 188 | protected function prepare(): void | ||
| 189 | { | ||
| 190 | } | ||
| 191 | |||
| 192 | /** | ||
| 193 | * Executes one-time cleanup tasks at the end of a hydration that was initiated | ||
| 194 | * through {@link hydrateAll} or {@link toIterable()}. | ||
| 195 | */ | ||
| 196 | protected function cleanup(): void | ||
| 197 | { | ||
| 198 | $this->statement()->free(); | ||
| 199 | |||
| 200 | $this->stmt = null; | ||
| 201 | $this->rsm = null; | ||
| 202 | $this->cache = []; | ||
| 203 | $this->metadataCache = []; | ||
| 204 | |||
| 205 | $this | ||
| 206 | ->em | ||
| 207 | ->getEventManager() | ||
| 208 | ->removeEventListener([Events::onClear], $this); | ||
| 209 | } | ||
| 210 | |||
| 211 | protected function cleanupAfterRowIteration(): void | ||
| 212 | { | ||
| 213 | } | ||
| 214 | |||
| 215 | /** | ||
| 216 | * Hydrates a single row from the current statement instance. | ||
| 217 | * | ||
| 218 | * Template method. | ||
| 219 | * | ||
| 220 | * @param mixed[] $row The row data. | ||
| 221 | * @param mixed[] $result The result to fill. | ||
| 222 | * | ||
| 223 | * @throws HydrationException | ||
| 224 | */ | ||
| 225 | protected function hydrateRowData(array $row, array &$result): void | ||
| 226 | { | ||
| 227 | throw new HydrationException('hydrateRowData() not implemented by this hydrator.'); | ||
| 228 | } | ||
| 229 | |||
| 230 | /** | ||
| 231 | * Hydrates all rows from the current statement instance at once. | ||
| 232 | */ | ||
| 233 | abstract protected function hydrateAllData(): mixed; | ||
| 234 | |||
| 235 | /** | ||
| 236 | * Processes a row of the result set. | ||
| 237 | * | ||
| 238 | * Used for identity-based hydration (HYDRATE_OBJECT and HYDRATE_ARRAY). | ||
| 239 | * Puts the elements of a result row into a new array, grouped by the dql alias | ||
| 240 | * they belong to. The column names in the result set are mapped to their | ||
| 241 | * field names during this procedure as well as any necessary conversions on | ||
| 242 | * the values applied. Scalar values are kept in a specific key 'scalars'. | ||
| 243 | * | ||
| 244 | * @param mixed[] $data SQL Result Row. | ||
| 245 | * @psalm-param array<string, string> $id Dql-Alias => ID-Hash. | ||
| 246 | * @psalm-param array<string, bool> $nonemptyComponents Does this DQL-Alias has at least one non NULL value? | ||
| 247 | * | ||
| 248 | * @return array<string, array<string, mixed>> An array with all the fields | ||
| 249 | * (name => value) of the data | ||
| 250 | * row, grouped by their | ||
| 251 | * component alias. | ||
| 252 | * @psalm-return array{ | ||
| 253 | * data: array<array-key, array>, | ||
| 254 | * newObjects?: array<array-key, array{ | ||
| 255 | * class: mixed, | ||
| 256 | * args?: array | ||
| 257 | * }>, | ||
| 258 | * scalars?: array | ||
| 259 | * } | ||
| 260 | */ | ||
| 261 | protected function gatherRowData(array $data, array &$id, array &$nonemptyComponents): array | ||
| 262 | { | ||
| 263 | $rowData = ['data' => []]; | ||
| 264 | |||
| 265 | foreach ($data as $key => $value) { | ||
| 266 | $cacheKeyInfo = $this->hydrateColumnInfo($key); | ||
| 267 | if ($cacheKeyInfo === null) { | ||
| 268 | continue; | ||
| 269 | } | ||
| 270 | |||
| 271 | $fieldName = $cacheKeyInfo['fieldName']; | ||
| 272 | |||
| 273 | switch (true) { | ||
| 274 | case isset($cacheKeyInfo['isNewObjectParameter']): | ||
| 275 | $argIndex = $cacheKeyInfo['argIndex']; | ||
| 276 | $objIndex = $cacheKeyInfo['objIndex']; | ||
| 277 | $type = $cacheKeyInfo['type']; | ||
| 278 | $value = $type->convertToPHPValue($value, $this->platform); | ||
| 279 | |||
| 280 | if ($value !== null && isset($cacheKeyInfo['enumType'])) { | ||
| 281 | $value = $this->buildEnum($value, $cacheKeyInfo['enumType']); | ||
| 282 | } | ||
| 283 | |||
| 284 | $rowData['newObjects'][$objIndex]['class'] = $cacheKeyInfo['class']; | ||
| 285 | $rowData['newObjects'][$objIndex]['args'][$argIndex] = $value; | ||
| 286 | break; | ||
| 287 | |||
| 288 | case isset($cacheKeyInfo['isScalar']): | ||
| 289 | $type = $cacheKeyInfo['type']; | ||
| 290 | $value = $type->convertToPHPValue($value, $this->platform); | ||
| 291 | |||
| 292 | if ($value !== null && isset($cacheKeyInfo['enumType'])) { | ||
| 293 | $value = $this->buildEnum($value, $cacheKeyInfo['enumType']); | ||
| 294 | } | ||
| 295 | |||
| 296 | $rowData['scalars'][$fieldName] = $value; | ||
| 297 | |||
| 298 | break; | ||
| 299 | |||
| 300 | //case (isset($cacheKeyInfo['isMetaColumn'])): | ||
| 301 | default: | ||
| 302 | $dqlAlias = $cacheKeyInfo['dqlAlias']; | ||
| 303 | $type = $cacheKeyInfo['type']; | ||
| 304 | |||
| 305 | // If there are field name collisions in the child class, then we need | ||
| 306 | // to only hydrate if we are looking at the correct discriminator value | ||
| 307 | if ( | ||
| 308 | isset($cacheKeyInfo['discriminatorColumn'], $data[$cacheKeyInfo['discriminatorColumn']]) | ||
| 309 | && ! in_array((string) $data[$cacheKeyInfo['discriminatorColumn']], $cacheKeyInfo['discriminatorValues'], true) | ||
| 310 | ) { | ||
| 311 | break; | ||
| 312 | } | ||
| 313 | |||
| 314 | // in an inheritance hierarchy the same field could be defined several times. | ||
| 315 | // We overwrite this value so long we don't have a non-null value, that value we keep. | ||
| 316 | // Per definition it cannot be that a field is defined several times and has several values. | ||
| 317 | if (isset($rowData['data'][$dqlAlias][$fieldName])) { | ||
| 318 | break; | ||
| 319 | } | ||
| 320 | |||
| 321 | $rowData['data'][$dqlAlias][$fieldName] = $type | ||
| 322 | ? $type->convertToPHPValue($value, $this->platform) | ||
| 323 | : $value; | ||
| 324 | |||
| 325 | if ($rowData['data'][$dqlAlias][$fieldName] !== null && isset($cacheKeyInfo['enumType'])) { | ||
| 326 | $rowData['data'][$dqlAlias][$fieldName] = $this->buildEnum($rowData['data'][$dqlAlias][$fieldName], $cacheKeyInfo['enumType']); | ||
| 327 | } | ||
| 328 | |||
| 329 | if ($cacheKeyInfo['isIdentifier'] && $value !== null) { | ||
| 330 | $id[$dqlAlias] .= '|' . $value; | ||
| 331 | $nonemptyComponents[$dqlAlias] = true; | ||
| 332 | } | ||
| 333 | |||
| 334 | break; | ||
| 335 | } | ||
| 336 | } | ||
| 337 | |||
| 338 | return $rowData; | ||
| 339 | } | ||
| 340 | |||
| 341 | /** | ||
| 342 | * Processes a row of the result set. | ||
| 343 | * | ||
| 344 | * Used for HYDRATE_SCALAR. This is a variant of _gatherRowData() that | ||
| 345 | * simply converts column names to field names and properly converts the | ||
| 346 | * values according to their types. The resulting row has the same number | ||
| 347 | * of elements as before. | ||
| 348 | * | ||
| 349 | * @param mixed[] $data | ||
| 350 | * @psalm-param array<string, mixed> $data | ||
| 351 | * | ||
| 352 | * @return mixed[] The processed row. | ||
| 353 | * @psalm-return array<string, mixed> | ||
| 354 | */ | ||
| 355 | protected function gatherScalarRowData(array &$data): array | ||
| 356 | { | ||
| 357 | $rowData = []; | ||
| 358 | |||
| 359 | foreach ($data as $key => $value) { | ||
| 360 | $cacheKeyInfo = $this->hydrateColumnInfo($key); | ||
| 361 | if ($cacheKeyInfo === null) { | ||
| 362 | continue; | ||
| 363 | } | ||
| 364 | |||
| 365 | $fieldName = $cacheKeyInfo['fieldName']; | ||
| 366 | |||
| 367 | // WARNING: BC break! We know this is the desired behavior to type convert values, but this | ||
| 368 | // erroneous behavior exists since 2.0 and we're forced to keep compatibility. | ||
| 369 | if (! isset($cacheKeyInfo['isScalar'])) { | ||
| 370 | $type = $cacheKeyInfo['type']; | ||
| 371 | $value = $type ? $type->convertToPHPValue($value, $this->platform) : $value; | ||
| 372 | |||
| 373 | $fieldName = $cacheKeyInfo['dqlAlias'] . '_' . $fieldName; | ||
| 374 | } | ||
| 375 | |||
| 376 | $rowData[$fieldName] = $value; | ||
| 377 | } | ||
| 378 | |||
| 379 | return $rowData; | ||
| 380 | } | ||
| 381 | |||
| 382 | /** | ||
| 383 | * Retrieve column information from ResultSetMapping. | ||
| 384 | * | ||
| 385 | * @param string $key Column name | ||
| 386 | * | ||
| 387 | * @return mixed[]|null | ||
| 388 | * @psalm-return array<string, mixed>|null | ||
| 389 | */ | ||
| 390 | protected function hydrateColumnInfo(string $key): array|null | ||
| 391 | { | ||
| 392 | if (isset($this->cache[$key])) { | ||
| 393 | return $this->cache[$key]; | ||
| 394 | } | ||
| 395 | |||
| 396 | switch (true) { | ||
| 397 | // NOTE: Most of the times it's a field mapping, so keep it first!!! | ||
| 398 | case isset($this->rsm->fieldMappings[$key]): | ||
| 399 | $classMetadata = $this->getClassMetadata($this->rsm->declaringClasses[$key]); | ||
| 400 | $fieldName = $this->rsm->fieldMappings[$key]; | ||
| 401 | $fieldMapping = $classMetadata->fieldMappings[$fieldName]; | ||
| 402 | $ownerMap = $this->rsm->columnOwnerMap[$key]; | ||
| 403 | $columnInfo = [ | ||
| 404 | 'isIdentifier' => in_array($fieldName, $classMetadata->identifier, true), | ||
| 405 | 'fieldName' => $fieldName, | ||
| 406 | 'type' => Type::getType($fieldMapping->type), | ||
| 407 | 'dqlAlias' => $ownerMap, | ||
| 408 | 'enumType' => $this->rsm->enumMappings[$key] ?? null, | ||
| 409 | ]; | ||
| 410 | |||
| 411 | // the current discriminator value must be saved in order to disambiguate fields hydration, | ||
| 412 | // should there be field name collisions | ||
| 413 | if ($classMetadata->parentClasses && isset($this->rsm->discriminatorColumns[$ownerMap])) { | ||
| 414 | return $this->cache[$key] = array_merge( | ||
| 415 | $columnInfo, | ||
| 416 | [ | ||
| 417 | 'discriminatorColumn' => $this->rsm->discriminatorColumns[$ownerMap], | ||
| 418 | 'discriminatorValue' => $classMetadata->discriminatorValue, | ||
| 419 | 'discriminatorValues' => $this->getDiscriminatorValues($classMetadata), | ||
| 420 | ], | ||
| 421 | ); | ||
| 422 | } | ||
| 423 | |||
| 424 | return $this->cache[$key] = $columnInfo; | ||
| 425 | |||
| 426 | case isset($this->rsm->newObjectMappings[$key]): | ||
| 427 | // WARNING: A NEW object is also a scalar, so it must be declared before! | ||
| 428 | $mapping = $this->rsm->newObjectMappings[$key]; | ||
| 429 | |||
| 430 | return $this->cache[$key] = [ | ||
| 431 | 'isScalar' => true, | ||
| 432 | 'isNewObjectParameter' => true, | ||
| 433 | 'fieldName' => $this->rsm->scalarMappings[$key], | ||
| 434 | 'type' => Type::getType($this->rsm->typeMappings[$key]), | ||
| 435 | 'argIndex' => $mapping['argIndex'], | ||
| 436 | 'objIndex' => $mapping['objIndex'], | ||
| 437 | 'class' => new ReflectionClass($mapping['className']), | ||
| 438 | 'enumType' => $this->rsm->enumMappings[$key] ?? null, | ||
| 439 | ]; | ||
| 440 | |||
| 441 | case isset($this->rsm->scalarMappings[$key], $this->hints[LimitSubqueryWalker::FORCE_DBAL_TYPE_CONVERSION]): | ||
| 442 | return $this->cache[$key] = [ | ||
| 443 | 'fieldName' => $this->rsm->scalarMappings[$key], | ||
| 444 | 'type' => Type::getType($this->rsm->typeMappings[$key]), | ||
| 445 | 'dqlAlias' => '', | ||
| 446 | 'enumType' => $this->rsm->enumMappings[$key] ?? null, | ||
| 447 | ]; | ||
| 448 | |||
| 449 | case isset($this->rsm->scalarMappings[$key]): | ||
| 450 | return $this->cache[$key] = [ | ||
| 451 | 'isScalar' => true, | ||
| 452 | 'fieldName' => $this->rsm->scalarMappings[$key], | ||
| 453 | 'type' => Type::getType($this->rsm->typeMappings[$key]), | ||
| 454 | 'enumType' => $this->rsm->enumMappings[$key] ?? null, | ||
| 455 | ]; | ||
| 456 | |||
| 457 | case isset($this->rsm->metaMappings[$key]): | ||
| 458 | // Meta column (has meaning in relational schema only, i.e. foreign keys or discriminator columns). | ||
| 459 | $fieldName = $this->rsm->metaMappings[$key]; | ||
| 460 | $dqlAlias = $this->rsm->columnOwnerMap[$key]; | ||
| 461 | $type = isset($this->rsm->typeMappings[$key]) | ||
| 462 | ? Type::getType($this->rsm->typeMappings[$key]) | ||
| 463 | : null; | ||
| 464 | |||
| 465 | // Cache metadata fetch | ||
| 466 | $this->getClassMetadata($this->rsm->aliasMap[$dqlAlias]); | ||
| 467 | |||
| 468 | return $this->cache[$key] = [ | ||
| 469 | 'isIdentifier' => isset($this->rsm->isIdentifierColumn[$dqlAlias][$key]), | ||
| 470 | 'isMetaColumn' => true, | ||
| 471 | 'fieldName' => $fieldName, | ||
| 472 | 'type' => $type, | ||
| 473 | 'dqlAlias' => $dqlAlias, | ||
| 474 | 'enumType' => $this->rsm->enumMappings[$key] ?? null, | ||
| 475 | ]; | ||
| 476 | } | ||
| 477 | |||
| 478 | // this column is a left over, maybe from a LIMIT query hack for example in Oracle or DB2 | ||
| 479 | // maybe from an additional column that has not been defined in a NativeQuery ResultSetMapping. | ||
| 480 | return null; | ||
| 481 | } | ||
| 482 | |||
| 483 | /** | ||
| 484 | * @return string[] | ||
| 485 | * @psalm-return non-empty-list<string> | ||
| 486 | */ | ||
| 487 | private function getDiscriminatorValues(ClassMetadata $classMetadata): array | ||
| 488 | { | ||
| 489 | $values = array_map( | ||
| 490 | fn (string $subClass): string => (string) $this->getClassMetadata($subClass)->discriminatorValue, | ||
| 491 | $classMetadata->subClasses, | ||
| 492 | ); | ||
| 493 | |||
| 494 | $values[] = (string) $classMetadata->discriminatorValue; | ||
| 495 | |||
| 496 | return $values; | ||
| 497 | } | ||
| 498 | |||
| 499 | /** | ||
| 500 | * Retrieve ClassMetadata associated to entity class name. | ||
| 501 | */ | ||
| 502 | protected function getClassMetadata(string $className): ClassMetadata | ||
| 503 | { | ||
| 504 | if (! isset($this->metadataCache[$className])) { | ||
| 505 | $this->metadataCache[$className] = $this->em->getClassMetadata($className); | ||
| 506 | } | ||
| 507 | |||
| 508 | return $this->metadataCache[$className]; | ||
| 509 | } | ||
| 510 | |||
| 511 | /** | ||
| 512 | * Register entity as managed in UnitOfWork. | ||
| 513 | * | ||
| 514 | * @param mixed[] $data | ||
| 515 | * | ||
| 516 | * @todo The "$id" generation is the same of UnitOfWork#createEntity. Remove this duplication somehow | ||
| 517 | */ | ||
| 518 | protected function registerManaged(ClassMetadata $class, object $entity, array $data): void | ||
| 519 | { | ||
| 520 | if ($class->isIdentifierComposite) { | ||
| 521 | $id = []; | ||
| 522 | |||
| 523 | foreach ($class->identifier as $fieldName) { | ||
| 524 | $id[$fieldName] = isset($class->associationMappings[$fieldName]) && $class->associationMappings[$fieldName]->isToOneOwningSide() | ||
| 525 | ? $data[$class->associationMappings[$fieldName]->joinColumns[0]->name] | ||
| 526 | : $data[$fieldName]; | ||
| 527 | } | ||
| 528 | } else { | ||
| 529 | $fieldName = $class->identifier[0]; | ||
| 530 | $id = [ | ||
| 531 | $fieldName => isset($class->associationMappings[$fieldName]) && $class->associationMappings[$fieldName]->isToOneOwningSide() | ||
| 532 | ? $data[$class->associationMappings[$fieldName]->joinColumns[0]->name] | ||
| 533 | : $data[$fieldName], | ||
| 534 | ]; | ||
| 535 | } | ||
| 536 | |||
| 537 | $this->em->getUnitOfWork()->registerManaged($entity, $id, $data); | ||
| 538 | } | ||
| 539 | |||
| 540 | /** | ||
| 541 | * @param class-string<BackedEnum> $enumType | ||
| 542 | * | ||
| 543 | * @return BackedEnum|array<BackedEnum> | ||
| 544 | */ | ||
| 545 | final protected function buildEnum(mixed $value, string $enumType): BackedEnum|array | ||
| 546 | { | ||
| 547 | if (is_array($value)) { | ||
| 548 | return array_map( | ||
| 549 | static fn ($value) => $enumType::from($value), | ||
| 550 | $value, | ||
| 551 | ); | ||
| 552 | } | ||
| 553 | |||
| 554 | return $enumType::from($value); | ||
| 555 | } | ||
| 556 | } | ||
diff --git a/vendor/doctrine/orm/src/Internal/Hydration/ArrayHydrator.php b/vendor/doctrine/orm/src/Internal/Hydration/ArrayHydrator.php new file mode 100644 index 0000000..7115c16 --- /dev/null +++ b/vendor/doctrine/orm/src/Internal/Hydration/ArrayHydrator.php | |||
| @@ -0,0 +1,270 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Internal\Hydration; | ||
| 6 | |||
| 7 | use function array_key_last; | ||
| 8 | use function count; | ||
| 9 | use function is_array; | ||
| 10 | use function key; | ||
| 11 | use function reset; | ||
| 12 | |||
| 13 | /** | ||
| 14 | * The ArrayHydrator produces a nested array "graph" that is often (not always) | ||
| 15 | * interchangeable with the corresponding object graph for read-only access. | ||
| 16 | */ | ||
| 17 | class ArrayHydrator extends AbstractHydrator | ||
| 18 | { | ||
| 19 | /** @var array<string,bool> */ | ||
| 20 | private array $rootAliases = []; | ||
| 21 | |||
| 22 | private bool $isSimpleQuery = false; | ||
| 23 | |||
| 24 | /** @var mixed[] */ | ||
| 25 | private array $identifierMap = []; | ||
| 26 | |||
| 27 | /** @var mixed[] */ | ||
| 28 | private array $resultPointers = []; | ||
| 29 | |||
| 30 | /** @var array<string,string> */ | ||
| 31 | private array $idTemplate = []; | ||
| 32 | |||
| 33 | private int $resultCounter = 0; | ||
| 34 | |||
| 35 | protected function prepare(): void | ||
| 36 | { | ||
| 37 | $this->isSimpleQuery = count($this->resultSetMapping()->aliasMap) <= 1; | ||
| 38 | |||
| 39 | foreach ($this->resultSetMapping()->aliasMap as $dqlAlias => $className) { | ||
| 40 | $this->identifierMap[$dqlAlias] = []; | ||
| 41 | $this->resultPointers[$dqlAlias] = []; | ||
| 42 | $this->idTemplate[$dqlAlias] = ''; | ||
| 43 | } | ||
| 44 | } | ||
| 45 | |||
| 46 | /** | ||
| 47 | * {@inheritDoc} | ||
| 48 | */ | ||
| 49 | protected function hydrateAllData(): array | ||
| 50 | { | ||
| 51 | $result = []; | ||
| 52 | |||
| 53 | while ($data = $this->statement()->fetchAssociative()) { | ||
| 54 | $this->hydrateRowData($data, $result); | ||
| 55 | } | ||
| 56 | |||
| 57 | return $result; | ||
| 58 | } | ||
| 59 | |||
| 60 | /** | ||
| 61 | * {@inheritDoc} | ||
| 62 | */ | ||
| 63 | protected function hydrateRowData(array $row, array &$result): void | ||
| 64 | { | ||
| 65 | // 1) Initialize | ||
| 66 | $id = $this->idTemplate; // initialize the id-memory | ||
| 67 | $nonemptyComponents = []; | ||
| 68 | $rowData = $this->gatherRowData($row, $id, $nonemptyComponents); | ||
| 69 | |||
| 70 | // 2) Now hydrate the data found in the current row. | ||
| 71 | foreach ($rowData['data'] as $dqlAlias => $data) { | ||
| 72 | $index = false; | ||
| 73 | |||
| 74 | if (isset($this->resultSetMapping()->parentAliasMap[$dqlAlias])) { | ||
| 75 | // It's a joined result | ||
| 76 | |||
| 77 | $parent = $this->resultSetMapping()->parentAliasMap[$dqlAlias]; | ||
| 78 | $path = $parent . '.' . $dqlAlias; | ||
| 79 | |||
| 80 | // missing parent data, skipping as RIGHT JOIN hydration is not supported. | ||
| 81 | if (! isset($nonemptyComponents[$parent])) { | ||
| 82 | continue; | ||
| 83 | } | ||
| 84 | |||
| 85 | // Get a reference to the right element in the result tree. | ||
| 86 | // This element will get the associated element attached. | ||
| 87 | if ($this->resultSetMapping()->isMixed && isset($this->rootAliases[$parent])) { | ||
| 88 | $first = reset($this->resultPointers); | ||
| 89 | // TODO: Exception if $key === null ? | ||
| 90 | $baseElement =& $this->resultPointers[$parent][key($first)]; | ||
| 91 | } elseif (isset($this->resultPointers[$parent])) { | ||
| 92 | $baseElement =& $this->resultPointers[$parent]; | ||
| 93 | } else { | ||
| 94 | unset($this->resultPointers[$dqlAlias]); // Ticket #1228 | ||
| 95 | |||
| 96 | continue; | ||
| 97 | } | ||
| 98 | |||
| 99 | $relationAlias = $this->resultSetMapping()->relationMap[$dqlAlias]; | ||
| 100 | $parentClass = $this->metadataCache[$this->resultSetMapping()->aliasMap[$parent]]; | ||
| 101 | $relation = $parentClass->associationMappings[$relationAlias]; | ||
| 102 | |||
| 103 | // Check the type of the relation (many or single-valued) | ||
| 104 | if (! $relation->isToOne()) { | ||
| 105 | $oneToOne = false; | ||
| 106 | |||
| 107 | if (! isset($baseElement[$relationAlias])) { | ||
| 108 | $baseElement[$relationAlias] = []; | ||
| 109 | } | ||
| 110 | |||
| 111 | if (isset($nonemptyComponents[$dqlAlias])) { | ||
| 112 | $indexExists = isset($this->identifierMap[$path][$id[$parent]][$id[$dqlAlias]]); | ||
| 113 | $index = $indexExists ? $this->identifierMap[$path][$id[$parent]][$id[$dqlAlias]] : false; | ||
| 114 | $indexIsValid = $index !== false ? isset($baseElement[$relationAlias][$index]) : false; | ||
| 115 | |||
| 116 | if (! $indexExists || ! $indexIsValid) { | ||
| 117 | $element = $data; | ||
| 118 | |||
| 119 | if (isset($this->resultSetMapping()->indexByMap[$dqlAlias])) { | ||
| 120 | $baseElement[$relationAlias][$row[$this->resultSetMapping()->indexByMap[$dqlAlias]]] = $element; | ||
| 121 | } else { | ||
| 122 | $baseElement[$relationAlias][] = $element; | ||
| 123 | } | ||
| 124 | |||
| 125 | $this->identifierMap[$path][$id[$parent]][$id[$dqlAlias]] = array_key_last($baseElement[$relationAlias]); | ||
| 126 | } | ||
| 127 | } | ||
| 128 | } else { | ||
| 129 | $oneToOne = true; | ||
| 130 | |||
| 131 | if ( | ||
| 132 | ! isset($nonemptyComponents[$dqlAlias]) && | ||
| 133 | ( ! isset($baseElement[$relationAlias])) | ||
| 134 | ) { | ||
| 135 | $baseElement[$relationAlias] = null; | ||
| 136 | } elseif (! isset($baseElement[$relationAlias])) { | ||
| 137 | $baseElement[$relationAlias] = $data; | ||
| 138 | } | ||
| 139 | } | ||
| 140 | |||
| 141 | $coll =& $baseElement[$relationAlias]; | ||
| 142 | |||
| 143 | if (is_array($coll)) { | ||
| 144 | $this->updateResultPointer($coll, $index, $dqlAlias, $oneToOne); | ||
| 145 | } | ||
| 146 | } else { | ||
| 147 | // It's a root result element | ||
| 148 | |||
| 149 | $this->rootAliases[$dqlAlias] = true; // Mark as root | ||
| 150 | $entityKey = $this->resultSetMapping()->entityMappings[$dqlAlias] ?: 0; | ||
| 151 | |||
| 152 | // if this row has a NULL value for the root result id then make it a null result. | ||
| 153 | if (! isset($nonemptyComponents[$dqlAlias])) { | ||
| 154 | $result[] = $this->resultSetMapping()->isMixed | ||
| 155 | ? [$entityKey => null] | ||
| 156 | : null; | ||
| 157 | |||
| 158 | $resultKey = $this->resultCounter; | ||
| 159 | ++$this->resultCounter; | ||
| 160 | |||
| 161 | continue; | ||
| 162 | } | ||
| 163 | |||
| 164 | // Check for an existing element | ||
| 165 | if ($this->isSimpleQuery || ! isset($this->identifierMap[$dqlAlias][$id[$dqlAlias]])) { | ||
| 166 | $element = $this->resultSetMapping()->isMixed | ||
| 167 | ? [$entityKey => $data] | ||
| 168 | : $data; | ||
| 169 | |||
| 170 | if (isset($this->resultSetMapping()->indexByMap[$dqlAlias])) { | ||
| 171 | $resultKey = $row[$this->resultSetMapping()->indexByMap[$dqlAlias]]; | ||
| 172 | $result[$resultKey] = $element; | ||
| 173 | } else { | ||
| 174 | $resultKey = $this->resultCounter; | ||
| 175 | $result[] = $element; | ||
| 176 | |||
| 177 | ++$this->resultCounter; | ||
| 178 | } | ||
| 179 | |||
| 180 | $this->identifierMap[$dqlAlias][$id[$dqlAlias]] = $resultKey; | ||
| 181 | } else { | ||
| 182 | $index = $this->identifierMap[$dqlAlias][$id[$dqlAlias]]; | ||
| 183 | $resultKey = $index; | ||
| 184 | } | ||
| 185 | |||
| 186 | $this->updateResultPointer($result, $index, $dqlAlias, false); | ||
| 187 | } | ||
| 188 | } | ||
| 189 | |||
| 190 | if (! isset($resultKey)) { | ||
| 191 | $this->resultCounter++; | ||
| 192 | } | ||
| 193 | |||
| 194 | // Append scalar values to mixed result sets | ||
| 195 | if (isset($rowData['scalars'])) { | ||
| 196 | if (! isset($resultKey)) { | ||
| 197 | // this only ever happens when no object is fetched (scalar result only) | ||
| 198 | $resultKey = isset($this->resultSetMapping()->indexByMap['scalars']) | ||
| 199 | ? $row[$this->resultSetMapping()->indexByMap['scalars']] | ||
| 200 | : $this->resultCounter - 1; | ||
| 201 | } | ||
| 202 | |||
| 203 | foreach ($rowData['scalars'] as $name => $value) { | ||
| 204 | $result[$resultKey][$name] = $value; | ||
| 205 | } | ||
| 206 | } | ||
| 207 | |||
| 208 | // Append new object to mixed result sets | ||
| 209 | if (isset($rowData['newObjects'])) { | ||
| 210 | if (! isset($resultKey)) { | ||
| 211 | $resultKey = $this->resultCounter - 1; | ||
| 212 | } | ||
| 213 | |||
| 214 | $scalarCount = (isset($rowData['scalars']) ? count($rowData['scalars']) : 0); | ||
| 215 | |||
| 216 | foreach ($rowData['newObjects'] as $objIndex => $newObject) { | ||
| 217 | $class = $newObject['class']; | ||
| 218 | $args = $newObject['args']; | ||
| 219 | $obj = $class->newInstanceArgs($args); | ||
| 220 | |||
| 221 | if (count($args) === $scalarCount || ($scalarCount === 0 && count($rowData['newObjects']) === 1)) { | ||
| 222 | $result[$resultKey] = $obj; | ||
| 223 | |||
| 224 | continue; | ||
| 225 | } | ||
| 226 | |||
| 227 | $result[$resultKey][$objIndex] = $obj; | ||
| 228 | } | ||
| 229 | } | ||
| 230 | } | ||
| 231 | |||
| 232 | /** | ||
| 233 | * Updates the result pointer for an Entity. The result pointers point to the | ||
| 234 | * last seen instance of each Entity type. This is used for graph construction. | ||
| 235 | * | ||
| 236 | * @param mixed[]|null $coll The element. | ||
| 237 | * @param string|int|false $index Index of the element in the collection. | ||
| 238 | * @param bool $oneToOne Whether it is a single-valued association or not. | ||
| 239 | */ | ||
| 240 | private function updateResultPointer( | ||
| 241 | array|null &$coll, | ||
| 242 | string|int|false $index, | ||
| 243 | string $dqlAlias, | ||
| 244 | bool $oneToOne, | ||
| 245 | ): void { | ||
| 246 | if ($coll === null) { | ||
| 247 | unset($this->resultPointers[$dqlAlias]); // Ticket #1228 | ||
| 248 | |||
| 249 | return; | ||
| 250 | } | ||
| 251 | |||
| 252 | if ($oneToOne) { | ||
| 253 | $this->resultPointers[$dqlAlias] =& $coll; | ||
| 254 | |||
| 255 | return; | ||
| 256 | } | ||
| 257 | |||
| 258 | if ($index !== false) { | ||
| 259 | $this->resultPointers[$dqlAlias] =& $coll[$index]; | ||
| 260 | |||
| 261 | return; | ||
| 262 | } | ||
| 263 | |||
| 264 | if (! $coll) { | ||
| 265 | return; | ||
| 266 | } | ||
| 267 | |||
| 268 | $this->resultPointers[$dqlAlias] =& $coll[array_key_last($coll)]; | ||
| 269 | } | ||
| 270 | } | ||
diff --git a/vendor/doctrine/orm/src/Internal/Hydration/HydrationException.php b/vendor/doctrine/orm/src/Internal/Hydration/HydrationException.php new file mode 100644 index 0000000..710114f --- /dev/null +++ b/vendor/doctrine/orm/src/Internal/Hydration/HydrationException.php | |||
| @@ -0,0 +1,67 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Internal\Hydration; | ||
| 6 | |||
| 7 | use Doctrine\ORM\Exception\ORMException; | ||
| 8 | use Exception; | ||
| 9 | |||
| 10 | use function implode; | ||
| 11 | use function sprintf; | ||
| 12 | |||
| 13 | class HydrationException extends Exception implements ORMException | ||
| 14 | { | ||
| 15 | public static function nonUniqueResult(): self | ||
| 16 | { | ||
| 17 | return new self('The result returned by the query was not unique.'); | ||
| 18 | } | ||
| 19 | |||
| 20 | public static function parentObjectOfRelationNotFound(string $alias, string $parentAlias): self | ||
| 21 | { | ||
| 22 | return new self(sprintf( | ||
| 23 | "The parent object of entity result with alias '%s' was not found." | ||
| 24 | . " The parent alias is '%s'.", | ||
| 25 | $alias, | ||
| 26 | $parentAlias, | ||
| 27 | )); | ||
| 28 | } | ||
| 29 | |||
| 30 | public static function emptyDiscriminatorValue(string $dqlAlias): self | ||
| 31 | { | ||
| 32 | return new self("The DQL alias '" . $dqlAlias . "' contains an entity " . | ||
| 33 | 'of an inheritance hierarchy with an empty discriminator value. This means ' . | ||
| 34 | 'that the database contains inconsistent data with an empty ' . | ||
| 35 | 'discriminator value in a table row.'); | ||
| 36 | } | ||
| 37 | |||
| 38 | public static function missingDiscriminatorColumn(string $entityName, string $discrColumnName, string $dqlAlias): self | ||
| 39 | { | ||
| 40 | return new self(sprintf( | ||
| 41 | 'The discriminator column "%s" is missing for "%s" using the DQL alias "%s".', | ||
| 42 | $discrColumnName, | ||
| 43 | $entityName, | ||
| 44 | $dqlAlias, | ||
| 45 | )); | ||
| 46 | } | ||
| 47 | |||
| 48 | public static function missingDiscriminatorMetaMappingColumn(string $entityName, string $discrColumnName, string $dqlAlias): self | ||
| 49 | { | ||
| 50 | return new self(sprintf( | ||
| 51 | 'The meta mapping for the discriminator column "%s" is missing for "%s" using the DQL alias "%s".', | ||
| 52 | $discrColumnName, | ||
| 53 | $entityName, | ||
| 54 | $dqlAlias, | ||
| 55 | )); | ||
| 56 | } | ||
| 57 | |||
| 58 | /** @param list<int|string> $discrValues */ | ||
| 59 | public static function invalidDiscriminatorValue(string $discrValue, array $discrValues): self | ||
| 60 | { | ||
| 61 | return new self(sprintf( | ||
| 62 | 'The discriminator value "%s" is invalid. It must be one of "%s".', | ||
| 63 | $discrValue, | ||
| 64 | implode('", "', $discrValues), | ||
| 65 | )); | ||
| 66 | } | ||
| 67 | } | ||
diff --git a/vendor/doctrine/orm/src/Internal/Hydration/ObjectHydrator.php b/vendor/doctrine/orm/src/Internal/Hydration/ObjectHydrator.php new file mode 100644 index 0000000..d0fc101 --- /dev/null +++ b/vendor/doctrine/orm/src/Internal/Hydration/ObjectHydrator.php | |||
| @@ -0,0 +1,586 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Internal\Hydration; | ||
| 6 | |||
| 7 | use BackedEnum; | ||
| 8 | use Doctrine\Common\Collections\ArrayCollection; | ||
| 9 | use Doctrine\ORM\Mapping\ClassMetadata; | ||
| 10 | use Doctrine\ORM\PersistentCollection; | ||
| 11 | use Doctrine\ORM\Query; | ||
| 12 | use Doctrine\ORM\UnitOfWork; | ||
| 13 | |||
| 14 | use function array_fill_keys; | ||
| 15 | use function array_keys; | ||
| 16 | use function array_map; | ||
| 17 | use function assert; | ||
| 18 | use function count; | ||
| 19 | use function is_array; | ||
| 20 | use function key; | ||
| 21 | use function ltrim; | ||
| 22 | use function spl_object_id; | ||
| 23 | |||
| 24 | /** | ||
| 25 | * The ObjectHydrator constructs an object graph out of an SQL result set. | ||
| 26 | * | ||
| 27 | * Internal note: Highly performance-sensitive code. | ||
| 28 | */ | ||
| 29 | class ObjectHydrator extends AbstractHydrator | ||
| 30 | { | ||
| 31 | /** @var mixed[] */ | ||
| 32 | private array $identifierMap = []; | ||
| 33 | |||
| 34 | /** @var mixed[] */ | ||
| 35 | private array $resultPointers = []; | ||
| 36 | |||
| 37 | /** @var mixed[] */ | ||
| 38 | private array $idTemplate = []; | ||
| 39 | |||
| 40 | private int $resultCounter = 0; | ||
| 41 | |||
| 42 | /** @var mixed[] */ | ||
| 43 | private array $rootAliases = []; | ||
| 44 | |||
| 45 | /** @var mixed[] */ | ||
| 46 | private array $initializedCollections = []; | ||
| 47 | |||
| 48 | /** @var array<string, PersistentCollection> */ | ||
| 49 | private array $uninitializedCollections = []; | ||
| 50 | |||
| 51 | /** @var mixed[] */ | ||
| 52 | private array $existingCollections = []; | ||
| 53 | |||
| 54 | protected function prepare(): void | ||
| 55 | { | ||
| 56 | if (! isset($this->hints[UnitOfWork::HINT_DEFEREAGERLOAD])) { | ||
| 57 | $this->hints[UnitOfWork::HINT_DEFEREAGERLOAD] = true; | ||
| 58 | } | ||
| 59 | |||
| 60 | foreach ($this->resultSetMapping()->aliasMap as $dqlAlias => $className) { | ||
| 61 | $this->identifierMap[$dqlAlias] = []; | ||
| 62 | $this->idTemplate[$dqlAlias] = ''; | ||
| 63 | |||
| 64 | // Remember which associations are "fetch joined", so that we know where to inject | ||
| 65 | // collection stubs or proxies and where not. | ||
| 66 | if (! isset($this->resultSetMapping()->relationMap[$dqlAlias])) { | ||
| 67 | continue; | ||
| 68 | } | ||
| 69 | |||
| 70 | $parent = $this->resultSetMapping()->parentAliasMap[$dqlAlias]; | ||
| 71 | |||
| 72 | if (! isset($this->resultSetMapping()->aliasMap[$parent])) { | ||
| 73 | throw HydrationException::parentObjectOfRelationNotFound($dqlAlias, $parent); | ||
| 74 | } | ||
| 75 | |||
| 76 | $sourceClassName = $this->resultSetMapping()->aliasMap[$parent]; | ||
| 77 | $sourceClass = $this->getClassMetadata($sourceClassName); | ||
| 78 | $assoc = $sourceClass->associationMappings[$this->resultSetMapping()->relationMap[$dqlAlias]]; | ||
| 79 | |||
| 80 | $this->hints['fetched'][$parent][$assoc->fieldName] = true; | ||
| 81 | |||
| 82 | if ($assoc->isManyToMany()) { | ||
| 83 | continue; | ||
| 84 | } | ||
| 85 | |||
| 86 | // Mark any non-collection opposite sides as fetched, too. | ||
| 87 | if (! $assoc->isOwningSide()) { | ||
| 88 | $this->hints['fetched'][$dqlAlias][$assoc->mappedBy] = true; | ||
| 89 | |||
| 90 | continue; | ||
| 91 | } | ||
| 92 | |||
| 93 | // handle fetch-joined owning side bi-directional one-to-one associations | ||
| 94 | if ($assoc->inversedBy !== null) { | ||
| 95 | $class = $this->getClassMetadata($className); | ||
| 96 | $inverseAssoc = $class->associationMappings[$assoc->inversedBy]; | ||
| 97 | |||
| 98 | if (! $inverseAssoc->isToOne()) { | ||
| 99 | continue; | ||
| 100 | } | ||
| 101 | |||
| 102 | $this->hints['fetched'][$dqlAlias][$inverseAssoc->fieldName] = true; | ||
| 103 | } | ||
| 104 | } | ||
| 105 | } | ||
| 106 | |||
| 107 | protected function cleanup(): void | ||
| 108 | { | ||
| 109 | $eagerLoad = isset($this->hints[UnitOfWork::HINT_DEFEREAGERLOAD]) && $this->hints[UnitOfWork::HINT_DEFEREAGERLOAD] === true; | ||
| 110 | |||
| 111 | parent::cleanup(); | ||
| 112 | |||
| 113 | $this->identifierMap = | ||
| 114 | $this->initializedCollections = | ||
| 115 | $this->uninitializedCollections = | ||
| 116 | $this->existingCollections = | ||
| 117 | $this->resultPointers = []; | ||
| 118 | |||
| 119 | if ($eagerLoad) { | ||
| 120 | $this->uow->triggerEagerLoads(); | ||
| 121 | } | ||
| 122 | |||
| 123 | $this->uow->hydrationComplete(); | ||
| 124 | } | ||
| 125 | |||
| 126 | protected function cleanupAfterRowIteration(): void | ||
| 127 | { | ||
| 128 | $this->identifierMap = | ||
| 129 | $this->initializedCollections = | ||
| 130 | $this->uninitializedCollections = | ||
| 131 | $this->existingCollections = | ||
| 132 | $this->resultPointers = []; | ||
| 133 | } | ||
| 134 | |||
| 135 | /** | ||
| 136 | * {@inheritDoc} | ||
| 137 | */ | ||
| 138 | protected function hydrateAllData(): array | ||
| 139 | { | ||
| 140 | $result = []; | ||
| 141 | |||
| 142 | while ($row = $this->statement()->fetchAssociative()) { | ||
| 143 | $this->hydrateRowData($row, $result); | ||
| 144 | } | ||
| 145 | |||
| 146 | // Take snapshots from all newly initialized collections | ||
| 147 | foreach ($this->initializedCollections as $coll) { | ||
| 148 | $coll->takeSnapshot(); | ||
| 149 | } | ||
| 150 | |||
| 151 | foreach ($this->uninitializedCollections as $coll) { | ||
| 152 | if (! $coll->isInitialized()) { | ||
| 153 | $coll->setInitialized(true); | ||
| 154 | } | ||
| 155 | } | ||
| 156 | |||
| 157 | return $result; | ||
| 158 | } | ||
| 159 | |||
| 160 | /** | ||
| 161 | * Initializes a related collection. | ||
| 162 | * | ||
| 163 | * @param string $fieldName The name of the field on the entity that holds the collection. | ||
| 164 | * @param string $parentDqlAlias Alias of the parent fetch joining this collection. | ||
| 165 | */ | ||
| 166 | private function initRelatedCollection( | ||
| 167 | object $entity, | ||
| 168 | ClassMetadata $class, | ||
| 169 | string $fieldName, | ||
| 170 | string $parentDqlAlias, | ||
| 171 | ): PersistentCollection { | ||
| 172 | $oid = spl_object_id($entity); | ||
| 173 | $relation = $class->associationMappings[$fieldName]; | ||
| 174 | $value = $class->reflFields[$fieldName]->getValue($entity); | ||
| 175 | |||
| 176 | if ($value === null || is_array($value)) { | ||
| 177 | $value = new ArrayCollection((array) $value); | ||
| 178 | } | ||
| 179 | |||
| 180 | if (! $value instanceof PersistentCollection) { | ||
| 181 | assert($relation->isToMany()); | ||
| 182 | $value = new PersistentCollection( | ||
| 183 | $this->em, | ||
| 184 | $this->metadataCache[$relation->targetEntity], | ||
| 185 | $value, | ||
| 186 | ); | ||
| 187 | $value->setOwner($entity, $relation); | ||
| 188 | |||
| 189 | $class->reflFields[$fieldName]->setValue($entity, $value); | ||
| 190 | $this->uow->setOriginalEntityProperty($oid, $fieldName, $value); | ||
| 191 | |||
| 192 | $this->initializedCollections[$oid . $fieldName] = $value; | ||
| 193 | } elseif ( | ||
| 194 | isset($this->hints[Query::HINT_REFRESH]) || | ||
| 195 | isset($this->hints['fetched'][$parentDqlAlias][$fieldName]) && | ||
| 196 | ! $value->isInitialized() | ||
| 197 | ) { | ||
| 198 | // Is already PersistentCollection, but either REFRESH or FETCH-JOIN and UNINITIALIZED! | ||
| 199 | $value->setDirty(false); | ||
| 200 | $value->setInitialized(true); | ||
| 201 | $value->unwrap()->clear(); | ||
| 202 | |||
| 203 | $this->initializedCollections[$oid . $fieldName] = $value; | ||
| 204 | } else { | ||
| 205 | // Is already PersistentCollection, and DON'T REFRESH or FETCH-JOIN! | ||
| 206 | $this->existingCollections[$oid . $fieldName] = $value; | ||
| 207 | } | ||
| 208 | |||
| 209 | return $value; | ||
| 210 | } | ||
| 211 | |||
| 212 | /** | ||
| 213 | * Gets an entity instance. | ||
| 214 | * | ||
| 215 | * @param string $dqlAlias The DQL alias of the entity's class. | ||
| 216 | * @psalm-param array<string, mixed> $data The instance data. | ||
| 217 | * | ||
| 218 | * @throws HydrationException | ||
| 219 | */ | ||
| 220 | private function getEntity(array $data, string $dqlAlias): object | ||
| 221 | { | ||
| 222 | $className = $this->resultSetMapping()->aliasMap[$dqlAlias]; | ||
| 223 | |||
| 224 | if (isset($this->resultSetMapping()->discriminatorColumns[$dqlAlias])) { | ||
| 225 | $fieldName = $this->resultSetMapping()->discriminatorColumns[$dqlAlias]; | ||
| 226 | |||
| 227 | if (! isset($this->resultSetMapping()->metaMappings[$fieldName])) { | ||
| 228 | throw HydrationException::missingDiscriminatorMetaMappingColumn($className, $fieldName, $dqlAlias); | ||
| 229 | } | ||
| 230 | |||
| 231 | $discrColumn = $this->resultSetMapping()->metaMappings[$fieldName]; | ||
| 232 | |||
| 233 | if (! isset($data[$discrColumn])) { | ||
| 234 | throw HydrationException::missingDiscriminatorColumn($className, $discrColumn, $dqlAlias); | ||
| 235 | } | ||
| 236 | |||
| 237 | if ($data[$discrColumn] === '') { | ||
| 238 | throw HydrationException::emptyDiscriminatorValue($dqlAlias); | ||
| 239 | } | ||
| 240 | |||
| 241 | $discrMap = $this->metadataCache[$className]->discriminatorMap; | ||
| 242 | $discriminatorValue = $data[$discrColumn]; | ||
| 243 | if ($discriminatorValue instanceof BackedEnum) { | ||
| 244 | $discriminatorValue = $discriminatorValue->value; | ||
| 245 | } | ||
| 246 | |||
| 247 | $discriminatorValue = (string) $discriminatorValue; | ||
| 248 | |||
| 249 | if (! isset($discrMap[$discriminatorValue])) { | ||
| 250 | throw HydrationException::invalidDiscriminatorValue($discriminatorValue, array_keys($discrMap)); | ||
| 251 | } | ||
| 252 | |||
| 253 | $className = $discrMap[$discriminatorValue]; | ||
| 254 | |||
| 255 | unset($data[$discrColumn]); | ||
| 256 | } | ||
| 257 | |||
| 258 | if (isset($this->hints[Query::HINT_REFRESH_ENTITY], $this->rootAliases[$dqlAlias])) { | ||
| 259 | $this->registerManaged($this->metadataCache[$className], $this->hints[Query::HINT_REFRESH_ENTITY], $data); | ||
| 260 | } | ||
| 261 | |||
| 262 | $this->hints['fetchAlias'] = $dqlAlias; | ||
| 263 | |||
| 264 | return $this->uow->createEntity($className, $data, $this->hints); | ||
| 265 | } | ||
| 266 | |||
| 267 | /** | ||
| 268 | * @psalm-param class-string $className | ||
| 269 | * @psalm-param array<string, mixed> $data | ||
| 270 | */ | ||
| 271 | private function getEntityFromIdentityMap(string $className, array $data): object|bool | ||
| 272 | { | ||
| 273 | // TODO: Abstract this code and UnitOfWork::createEntity() equivalent? | ||
| 274 | $class = $this->metadataCache[$className]; | ||
| 275 | |||
| 276 | if ($class->isIdentifierComposite) { | ||
| 277 | $idHash = UnitOfWork::getIdHashByIdentifier( | ||
| 278 | array_map( | ||
| 279 | /** @return mixed */ | ||
| 280 | static fn (string $fieldName) => isset($class->associationMappings[$fieldName]) && assert($class->associationMappings[$fieldName]->isToOneOwningSide()) | ||
| 281 | ? $data[$class->associationMappings[$fieldName]->joinColumns[0]->name] | ||
| 282 | : $data[$fieldName], | ||
| 283 | $class->identifier, | ||
| 284 | ), | ||
| 285 | ); | ||
| 286 | |||
| 287 | return $this->uow->tryGetByIdHash(ltrim($idHash), $class->rootEntityName); | ||
| 288 | } elseif (isset($class->associationMappings[$class->identifier[0]])) { | ||
| 289 | $association = $class->associationMappings[$class->identifier[0]]; | ||
| 290 | assert($association->isToOneOwningSide()); | ||
| 291 | |||
| 292 | return $this->uow->tryGetByIdHash($data[$association->joinColumns[0]->name], $class->rootEntityName); | ||
| 293 | } | ||
| 294 | |||
| 295 | return $this->uow->tryGetByIdHash($data[$class->identifier[0]], $class->rootEntityName); | ||
| 296 | } | ||
| 297 | |||
| 298 | /** | ||
| 299 | * Hydrates a single row in an SQL result set. | ||
| 300 | * | ||
| 301 | * @internal | ||
| 302 | * First, the data of the row is split into chunks where each chunk contains data | ||
| 303 | * that belongs to a particular component/class. Afterwards, all these chunks | ||
| 304 | * are processed, one after the other. For each chunk of class data only one of the | ||
| 305 | * following code paths is executed: | ||
| 306 | * Path A: The data chunk belongs to a joined/associated object and the association | ||
| 307 | * is collection-valued. | ||
| 308 | * Path B: The data chunk belongs to a joined/associated object and the association | ||
| 309 | * is single-valued. | ||
| 310 | * Path C: The data chunk belongs to a root result element/object that appears in the topmost | ||
| 311 | * level of the hydrated result. A typical example are the objects of the type | ||
| 312 | * specified by the FROM clause in a DQL query. | ||
| 313 | * | ||
| 314 | * @param mixed[] $row The data of the row to process. | ||
| 315 | * @param mixed[] $result The result array to fill. | ||
| 316 | */ | ||
| 317 | protected function hydrateRowData(array $row, array &$result): void | ||
| 318 | { | ||
| 319 | // Initialize | ||
| 320 | $id = $this->idTemplate; // initialize the id-memory | ||
| 321 | $nonemptyComponents = []; | ||
| 322 | // Split the row data into chunks of class data. | ||
| 323 | $rowData = $this->gatherRowData($row, $id, $nonemptyComponents); | ||
| 324 | |||
| 325 | // reset result pointers for each data row | ||
| 326 | $this->resultPointers = []; | ||
| 327 | |||
| 328 | // Hydrate the data chunks | ||
| 329 | foreach ($rowData['data'] as $dqlAlias => $data) { | ||
| 330 | $entityName = $this->resultSetMapping()->aliasMap[$dqlAlias]; | ||
| 331 | |||
| 332 | if (isset($this->resultSetMapping()->parentAliasMap[$dqlAlias])) { | ||
| 333 | // It's a joined result | ||
| 334 | |||
| 335 | $parentAlias = $this->resultSetMapping()->parentAliasMap[$dqlAlias]; | ||
| 336 | // we need the $path to save into the identifier map which entities were already | ||
| 337 | // seen for this parent-child relationship | ||
| 338 | $path = $parentAlias . '.' . $dqlAlias; | ||
| 339 | |||
| 340 | // We have a RIGHT JOIN result here. Doctrine cannot hydrate RIGHT JOIN Object-Graphs | ||
| 341 | if (! isset($nonemptyComponents[$parentAlias])) { | ||
| 342 | // TODO: Add special case code where we hydrate the right join objects into identity map at least | ||
| 343 | continue; | ||
| 344 | } | ||
| 345 | |||
| 346 | $parentClass = $this->metadataCache[$this->resultSetMapping()->aliasMap[$parentAlias]]; | ||
| 347 | $relationField = $this->resultSetMapping()->relationMap[$dqlAlias]; | ||
| 348 | $relation = $parentClass->associationMappings[$relationField]; | ||
| 349 | $reflField = $parentClass->reflFields[$relationField]; | ||
| 350 | |||
| 351 | // Get a reference to the parent object to which the joined element belongs. | ||
| 352 | if ($this->resultSetMapping()->isMixed && isset($this->rootAliases[$parentAlias])) { | ||
| 353 | $objectClass = $this->resultPointers[$parentAlias]; | ||
| 354 | $parentObject = $objectClass[key($objectClass)]; | ||
| 355 | } elseif (isset($this->resultPointers[$parentAlias])) { | ||
| 356 | $parentObject = $this->resultPointers[$parentAlias]; | ||
| 357 | } else { | ||
| 358 | // Parent object of relation not found, mark as not-fetched again | ||
| 359 | if (isset($nonemptyComponents[$dqlAlias])) { | ||
| 360 | $element = $this->getEntity($data, $dqlAlias); | ||
| 361 | |||
| 362 | // Update result pointer and provide initial fetch data for parent | ||
| 363 | $this->resultPointers[$dqlAlias] = $element; | ||
| 364 | $rowData['data'][$parentAlias][$relationField] = $element; | ||
| 365 | } else { | ||
| 366 | $element = null; | ||
| 367 | } | ||
| 368 | |||
| 369 | // Mark as not-fetched again | ||
| 370 | unset($this->hints['fetched'][$parentAlias][$relationField]); | ||
| 371 | continue; | ||
| 372 | } | ||
| 373 | |||
| 374 | $oid = spl_object_id($parentObject); | ||
| 375 | |||
| 376 | // Check the type of the relation (many or single-valued) | ||
| 377 | if (! $relation->isToOne()) { | ||
| 378 | // PATH A: Collection-valued association | ||
| 379 | $reflFieldValue = $reflField->getValue($parentObject); | ||
| 380 | |||
| 381 | if (isset($nonemptyComponents[$dqlAlias])) { | ||
| 382 | $collKey = $oid . $relationField; | ||
| 383 | if (isset($this->initializedCollections[$collKey])) { | ||
| 384 | $reflFieldValue = $this->initializedCollections[$collKey]; | ||
| 385 | } elseif (! isset($this->existingCollections[$collKey])) { | ||
| 386 | $reflFieldValue = $this->initRelatedCollection($parentObject, $parentClass, $relationField, $parentAlias); | ||
| 387 | } | ||
| 388 | |||
| 389 | $indexExists = isset($this->identifierMap[$path][$id[$parentAlias]][$id[$dqlAlias]]); | ||
| 390 | $index = $indexExists ? $this->identifierMap[$path][$id[$parentAlias]][$id[$dqlAlias]] : false; | ||
| 391 | $indexIsValid = $index !== false ? isset($reflFieldValue[$index]) : false; | ||
| 392 | |||
| 393 | if (! $indexExists || ! $indexIsValid) { | ||
| 394 | if (isset($this->existingCollections[$collKey])) { | ||
| 395 | // Collection exists, only look for the element in the identity map. | ||
| 396 | $element = $this->getEntityFromIdentityMap($entityName, $data); | ||
| 397 | if ($element) { | ||
| 398 | $this->resultPointers[$dqlAlias] = $element; | ||
| 399 | } else { | ||
| 400 | unset($this->resultPointers[$dqlAlias]); | ||
| 401 | } | ||
| 402 | } else { | ||
| 403 | $element = $this->getEntity($data, $dqlAlias); | ||
| 404 | |||
| 405 | if (isset($this->resultSetMapping()->indexByMap[$dqlAlias])) { | ||
| 406 | $indexValue = $row[$this->resultSetMapping()->indexByMap[$dqlAlias]]; | ||
| 407 | $reflFieldValue->hydrateSet($indexValue, $element); | ||
| 408 | $this->identifierMap[$path][$id[$parentAlias]][$id[$dqlAlias]] = $indexValue; | ||
| 409 | } else { | ||
| 410 | if (! $reflFieldValue->contains($element)) { | ||
| 411 | $reflFieldValue->hydrateAdd($element); | ||
| 412 | $reflFieldValue->last(); | ||
| 413 | } | ||
| 414 | |||
| 415 | $this->identifierMap[$path][$id[$parentAlias]][$id[$dqlAlias]] = $reflFieldValue->key(); | ||
| 416 | } | ||
| 417 | |||
| 418 | // Update result pointer | ||
| 419 | $this->resultPointers[$dqlAlias] = $element; | ||
| 420 | } | ||
| 421 | } else { | ||
| 422 | // Update result pointer | ||
| 423 | $this->resultPointers[$dqlAlias] = $reflFieldValue[$index]; | ||
| 424 | } | ||
| 425 | } elseif (! $reflFieldValue) { | ||
| 426 | $this->initRelatedCollection($parentObject, $parentClass, $relationField, $parentAlias); | ||
| 427 | } elseif ($reflFieldValue instanceof PersistentCollection && $reflFieldValue->isInitialized() === false && ! isset($this->uninitializedCollections[$oid . $relationField])) { | ||
| 428 | $this->uninitializedCollections[$oid . $relationField] = $reflFieldValue; | ||
| 429 | } | ||
| 430 | } else { | ||
| 431 | // PATH B: Single-valued association | ||
| 432 | $reflFieldValue = $reflField->getValue($parentObject); | ||
| 433 | |||
| 434 | if (! $reflFieldValue || isset($this->hints[Query::HINT_REFRESH]) || $this->uow->isUninitializedObject($reflFieldValue)) { | ||
| 435 | // we only need to take action if this value is null, | ||
| 436 | // we refresh the entity or its an uninitialized proxy. | ||
| 437 | if (isset($nonemptyComponents[$dqlAlias])) { | ||
| 438 | $element = $this->getEntity($data, $dqlAlias); | ||
| 439 | $reflField->setValue($parentObject, $element); | ||
| 440 | $this->uow->setOriginalEntityProperty($oid, $relationField, $element); | ||
| 441 | $targetClass = $this->metadataCache[$relation->targetEntity]; | ||
| 442 | |||
| 443 | if ($relation->isOwningSide()) { | ||
| 444 | // TODO: Just check hints['fetched'] here? | ||
| 445 | // If there is an inverse mapping on the target class its bidirectional | ||
| 446 | if ($relation->inversedBy !== null) { | ||
| 447 | $inverseAssoc = $targetClass->associationMappings[$relation->inversedBy]; | ||
| 448 | if ($inverseAssoc->isToOne()) { | ||
| 449 | $targetClass->reflFields[$inverseAssoc->fieldName]->setValue($element, $parentObject); | ||
| 450 | $this->uow->setOriginalEntityProperty(spl_object_id($element), $inverseAssoc->fieldName, $parentObject); | ||
| 451 | } | ||
| 452 | } | ||
| 453 | } else { | ||
| 454 | // For sure bidirectional, as there is no inverse side in unidirectional mappings | ||
| 455 | $targetClass->reflFields[$relation->mappedBy]->setValue($element, $parentObject); | ||
| 456 | $this->uow->setOriginalEntityProperty(spl_object_id($element), $relation->mappedBy, $parentObject); | ||
| 457 | } | ||
| 458 | |||
| 459 | // Update result pointer | ||
| 460 | $this->resultPointers[$dqlAlias] = $element; | ||
| 461 | } else { | ||
| 462 | $this->uow->setOriginalEntityProperty($oid, $relationField, null); | ||
| 463 | $reflField->setValue($parentObject, null); | ||
| 464 | } | ||
| 465 | // else leave $reflFieldValue null for single-valued associations | ||
| 466 | } else { | ||
| 467 | // Update result pointer | ||
| 468 | $this->resultPointers[$dqlAlias] = $reflFieldValue; | ||
| 469 | } | ||
| 470 | } | ||
| 471 | } else { | ||
| 472 | // PATH C: Its a root result element | ||
| 473 | $this->rootAliases[$dqlAlias] = true; // Mark as root alias | ||
| 474 | $entityKey = $this->resultSetMapping()->entityMappings[$dqlAlias] ?: 0; | ||
| 475 | |||
| 476 | // if this row has a NULL value for the root result id then make it a null result. | ||
| 477 | if (! isset($nonemptyComponents[$dqlAlias])) { | ||
| 478 | if ($this->resultSetMapping()->isMixed) { | ||
| 479 | $result[] = [$entityKey => null]; | ||
| 480 | } else { | ||
| 481 | $result[] = null; | ||
| 482 | } | ||
| 483 | |||
| 484 | $resultKey = $this->resultCounter; | ||
| 485 | ++$this->resultCounter; | ||
| 486 | continue; | ||
| 487 | } | ||
| 488 | |||
| 489 | // check for existing result from the iterations before | ||
| 490 | if (! isset($this->identifierMap[$dqlAlias][$id[$dqlAlias]])) { | ||
| 491 | $element = $this->getEntity($data, $dqlAlias); | ||
| 492 | |||
| 493 | if ($this->resultSetMapping()->isMixed) { | ||
| 494 | $element = [$entityKey => $element]; | ||
| 495 | } | ||
| 496 | |||
| 497 | if (isset($this->resultSetMapping()->indexByMap[$dqlAlias])) { | ||
| 498 | $resultKey = $row[$this->resultSetMapping()->indexByMap[$dqlAlias]]; | ||
| 499 | |||
| 500 | if (isset($this->hints['collection'])) { | ||
| 501 | $this->hints['collection']->hydrateSet($resultKey, $element); | ||
| 502 | } | ||
| 503 | |||
| 504 | $result[$resultKey] = $element; | ||
| 505 | } else { | ||
| 506 | $resultKey = $this->resultCounter; | ||
| 507 | ++$this->resultCounter; | ||
| 508 | |||
| 509 | if (isset($this->hints['collection'])) { | ||
| 510 | $this->hints['collection']->hydrateAdd($element); | ||
| 511 | } | ||
| 512 | |||
| 513 | $result[] = $element; | ||
| 514 | } | ||
| 515 | |||
| 516 | $this->identifierMap[$dqlAlias][$id[$dqlAlias]] = $resultKey; | ||
| 517 | |||
| 518 | // Update result pointer | ||
| 519 | $this->resultPointers[$dqlAlias] = $element; | ||
| 520 | } else { | ||
| 521 | // Update result pointer | ||
| 522 | $index = $this->identifierMap[$dqlAlias][$id[$dqlAlias]]; | ||
| 523 | $this->resultPointers[$dqlAlias] = $result[$index]; | ||
| 524 | $resultKey = $index; | ||
| 525 | } | ||
| 526 | } | ||
| 527 | |||
| 528 | if (isset($this->hints[Query::HINT_INTERNAL_ITERATION]) && $this->hints[Query::HINT_INTERNAL_ITERATION]) { | ||
| 529 | $this->uow->hydrationComplete(); | ||
| 530 | } | ||
| 531 | } | ||
| 532 | |||
| 533 | if (! isset($resultKey)) { | ||
| 534 | $this->resultCounter++; | ||
| 535 | } | ||
| 536 | |||
| 537 | // Append scalar values to mixed result sets | ||
| 538 | if (isset($rowData['scalars'])) { | ||
| 539 | if (! isset($resultKey)) { | ||
| 540 | $resultKey = isset($this->resultSetMapping()->indexByMap['scalars']) | ||
| 541 | ? $row[$this->resultSetMapping()->indexByMap['scalars']] | ||
| 542 | : $this->resultCounter - 1; | ||
| 543 | } | ||
| 544 | |||
| 545 | foreach ($rowData['scalars'] as $name => $value) { | ||
| 546 | $result[$resultKey][$name] = $value; | ||
| 547 | } | ||
| 548 | } | ||
| 549 | |||
| 550 | // Append new object to mixed result sets | ||
| 551 | if (isset($rowData['newObjects'])) { | ||
| 552 | if (! isset($resultKey)) { | ||
| 553 | $resultKey = $this->resultCounter - 1; | ||
| 554 | } | ||
| 555 | |||
| 556 | $scalarCount = (isset($rowData['scalars']) ? count($rowData['scalars']) : 0); | ||
| 557 | |||
| 558 | foreach ($rowData['newObjects'] as $objIndex => $newObject) { | ||
| 559 | $class = $newObject['class']; | ||
| 560 | $args = $newObject['args']; | ||
| 561 | $obj = $class->newInstanceArgs($args); | ||
| 562 | |||
| 563 | if ($scalarCount === 0 && count($rowData['newObjects']) === 1) { | ||
| 564 | $result[$resultKey] = $obj; | ||
| 565 | |||
| 566 | continue; | ||
| 567 | } | ||
| 568 | |||
| 569 | $result[$resultKey][$objIndex] = $obj; | ||
| 570 | } | ||
| 571 | } | ||
| 572 | } | ||
| 573 | |||
| 574 | /** | ||
| 575 | * When executed in a hydrate() loop we may have to clear internal state to | ||
| 576 | * decrease memory consumption. | ||
| 577 | */ | ||
| 578 | public function onClear(mixed $eventArgs): void | ||
| 579 | { | ||
| 580 | parent::onClear($eventArgs); | ||
| 581 | |||
| 582 | $aliases = array_keys($this->identifierMap); | ||
| 583 | |||
| 584 | $this->identifierMap = array_fill_keys($aliases, []); | ||
| 585 | } | ||
| 586 | } | ||
diff --git a/vendor/doctrine/orm/src/Internal/Hydration/ScalarColumnHydrator.php b/vendor/doctrine/orm/src/Internal/Hydration/ScalarColumnHydrator.php new file mode 100644 index 0000000..0f10fb4 --- /dev/null +++ b/vendor/doctrine/orm/src/Internal/Hydration/ScalarColumnHydrator.php | |||
| @@ -0,0 +1,34 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Internal\Hydration; | ||
| 6 | |||
| 7 | use Doctrine\DBAL\Driver\Exception; | ||
| 8 | use Doctrine\ORM\Exception\MultipleSelectorsFoundException; | ||
| 9 | |||
| 10 | use function array_column; | ||
| 11 | use function count; | ||
| 12 | |||
| 13 | /** | ||
| 14 | * Hydrator that produces one-dimensional array. | ||
| 15 | */ | ||
| 16 | final class ScalarColumnHydrator extends AbstractHydrator | ||
| 17 | { | ||
| 18 | /** | ||
| 19 | * {@inheritDoc} | ||
| 20 | * | ||
| 21 | * @throws MultipleSelectorsFoundException | ||
| 22 | * @throws Exception | ||
| 23 | */ | ||
| 24 | protected function hydrateAllData(): array | ||
| 25 | { | ||
| 26 | if (count($this->resultSetMapping()->fieldMappings) > 1) { | ||
| 27 | throw MultipleSelectorsFoundException::create($this->resultSetMapping()->fieldMappings); | ||
| 28 | } | ||
| 29 | |||
| 30 | $result = $this->statement()->fetchAllNumeric(); | ||
| 31 | |||
| 32 | return array_column($result, 0); | ||
| 33 | } | ||
| 34 | } | ||
diff --git a/vendor/doctrine/orm/src/Internal/Hydration/ScalarHydrator.php b/vendor/doctrine/orm/src/Internal/Hydration/ScalarHydrator.php new file mode 100644 index 0000000..15f3e7e --- /dev/null +++ b/vendor/doctrine/orm/src/Internal/Hydration/ScalarHydrator.php | |||
| @@ -0,0 +1,35 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Internal\Hydration; | ||
| 6 | |||
| 7 | /** | ||
| 8 | * Hydrator that produces flat, rectangular results of scalar data. | ||
| 9 | * The created result is almost the same as a regular SQL result set, except | ||
| 10 | * that column names are mapped to field names and data type conversions take place. | ||
| 11 | */ | ||
| 12 | class ScalarHydrator extends AbstractHydrator | ||
| 13 | { | ||
| 14 | /** | ||
| 15 | * {@inheritDoc} | ||
| 16 | */ | ||
| 17 | protected function hydrateAllData(): array | ||
| 18 | { | ||
| 19 | $result = []; | ||
| 20 | |||
| 21 | while ($data = $this->statement()->fetchAssociative()) { | ||
| 22 | $this->hydrateRowData($data, $result); | ||
| 23 | } | ||
| 24 | |||
| 25 | return $result; | ||
| 26 | } | ||
| 27 | |||
| 28 | /** | ||
| 29 | * {@inheritDoc} | ||
| 30 | */ | ||
| 31 | protected function hydrateRowData(array $row, array &$result): void | ||
| 32 | { | ||
| 33 | $result[] = $this->gatherScalarRowData($row); | ||
| 34 | } | ||
| 35 | } | ||
diff --git a/vendor/doctrine/orm/src/Internal/Hydration/SimpleObjectHydrator.php b/vendor/doctrine/orm/src/Internal/Hydration/SimpleObjectHydrator.php new file mode 100644 index 0000000..eab7b9b --- /dev/null +++ b/vendor/doctrine/orm/src/Internal/Hydration/SimpleObjectHydrator.php | |||
| @@ -0,0 +1,176 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Internal\Hydration; | ||
| 6 | |||
| 7 | use Doctrine\ORM\Internal\SQLResultCasing; | ||
| 8 | use Doctrine\ORM\Mapping\ClassMetadata; | ||
| 9 | use Doctrine\ORM\Mapping\MappingException; | ||
| 10 | use Doctrine\ORM\Query; | ||
| 11 | use Exception; | ||
| 12 | use RuntimeException; | ||
| 13 | use ValueError; | ||
| 14 | |||
| 15 | use function array_keys; | ||
| 16 | use function array_search; | ||
| 17 | use function assert; | ||
| 18 | use function count; | ||
| 19 | use function in_array; | ||
| 20 | use function key; | ||
| 21 | use function reset; | ||
| 22 | use function sprintf; | ||
| 23 | |||
| 24 | class SimpleObjectHydrator extends AbstractHydrator | ||
| 25 | { | ||
| 26 | use SQLResultCasing; | ||
| 27 | |||
| 28 | private ClassMetadata|null $class = null; | ||
| 29 | |||
| 30 | protected function prepare(): void | ||
| 31 | { | ||
| 32 | if (count($this->resultSetMapping()->aliasMap) !== 1) { | ||
| 33 | throw new RuntimeException('Cannot use SimpleObjectHydrator with a ResultSetMapping that contains more than one object result.'); | ||
| 34 | } | ||
| 35 | |||
| 36 | if ($this->resultSetMapping()->scalarMappings) { | ||
| 37 | throw new RuntimeException('Cannot use SimpleObjectHydrator with a ResultSetMapping that contains scalar mappings.'); | ||
| 38 | } | ||
| 39 | |||
| 40 | $this->class = $this->getClassMetadata(reset($this->resultSetMapping()->aliasMap)); | ||
| 41 | } | ||
| 42 | |||
| 43 | protected function cleanup(): void | ||
| 44 | { | ||
| 45 | parent::cleanup(); | ||
| 46 | |||
| 47 | $this->uow->triggerEagerLoads(); | ||
| 48 | $this->uow->hydrationComplete(); | ||
| 49 | } | ||
| 50 | |||
| 51 | /** | ||
| 52 | * {@inheritDoc} | ||
| 53 | */ | ||
| 54 | protected function hydrateAllData(): array | ||
| 55 | { | ||
| 56 | $result = []; | ||
| 57 | |||
| 58 | while ($row = $this->statement()->fetchAssociative()) { | ||
| 59 | $this->hydrateRowData($row, $result); | ||
| 60 | } | ||
| 61 | |||
| 62 | $this->em->getUnitOfWork()->triggerEagerLoads(); | ||
| 63 | |||
| 64 | return $result; | ||
| 65 | } | ||
| 66 | |||
| 67 | /** | ||
| 68 | * {@inheritDoc} | ||
| 69 | */ | ||
| 70 | protected function hydrateRowData(array $row, array &$result): void | ||
| 71 | { | ||
| 72 | assert($this->class !== null); | ||
| 73 | $entityName = $this->class->name; | ||
| 74 | $data = []; | ||
| 75 | $discrColumnValue = null; | ||
| 76 | |||
| 77 | // We need to find the correct entity class name if we have inheritance in resultset | ||
| 78 | if ($this->class->inheritanceType !== ClassMetadata::INHERITANCE_TYPE_NONE) { | ||
| 79 | $discrColumn = $this->class->getDiscriminatorColumn(); | ||
| 80 | $discrColumnName = $this->getSQLResultCasing($this->platform, $discrColumn->name); | ||
| 81 | |||
| 82 | // Find mapped discriminator column from the result set. | ||
| 83 | $metaMappingDiscrColumnName = array_search($discrColumnName, $this->resultSetMapping()->metaMappings, true); | ||
| 84 | if ($metaMappingDiscrColumnName) { | ||
| 85 | $discrColumnName = $metaMappingDiscrColumnName; | ||
| 86 | } | ||
| 87 | |||
| 88 | if (! isset($row[$discrColumnName])) { | ||
| 89 | throw HydrationException::missingDiscriminatorColumn( | ||
| 90 | $entityName, | ||
| 91 | $discrColumnName, | ||
| 92 | key($this->resultSetMapping()->aliasMap), | ||
| 93 | ); | ||
| 94 | } | ||
| 95 | |||
| 96 | if ($row[$discrColumnName] === '') { | ||
| 97 | throw HydrationException::emptyDiscriminatorValue(key( | ||
| 98 | $this->resultSetMapping()->aliasMap, | ||
| 99 | )); | ||
| 100 | } | ||
| 101 | |||
| 102 | $discrMap = $this->class->discriminatorMap; | ||
| 103 | |||
| 104 | if (! isset($discrMap[$row[$discrColumnName]])) { | ||
| 105 | throw HydrationException::invalidDiscriminatorValue($row[$discrColumnName], array_keys($discrMap)); | ||
| 106 | } | ||
| 107 | |||
| 108 | $entityName = $discrMap[$row[$discrColumnName]]; | ||
| 109 | $discrColumnValue = $row[$discrColumnName]; | ||
| 110 | |||
| 111 | unset($row[$discrColumnName]); | ||
| 112 | } | ||
| 113 | |||
| 114 | foreach ($row as $column => $value) { | ||
| 115 | // An ObjectHydrator should be used instead of SimpleObjectHydrator | ||
| 116 | if (isset($this->resultSetMapping()->relationMap[$column])) { | ||
| 117 | throw new Exception(sprintf('Unable to retrieve association information for column "%s"', $column)); | ||
| 118 | } | ||
| 119 | |||
| 120 | $cacheKeyInfo = $this->hydrateColumnInfo($column); | ||
| 121 | |||
| 122 | if (! $cacheKeyInfo) { | ||
| 123 | continue; | ||
| 124 | } | ||
| 125 | |||
| 126 | // If we have inheritance in resultset, make sure the field belongs to the correct class | ||
| 127 | if (isset($cacheKeyInfo['discriminatorValues']) && ! in_array((string) $discrColumnValue, $cacheKeyInfo['discriminatorValues'], true)) { | ||
| 128 | continue; | ||
| 129 | } | ||
| 130 | |||
| 131 | // Check if value is null before conversion (because some types convert null to something else) | ||
| 132 | $valueIsNull = $value === null; | ||
| 133 | |||
| 134 | // Convert field to a valid PHP value | ||
| 135 | if (isset($cacheKeyInfo['type'])) { | ||
| 136 | $type = $cacheKeyInfo['type']; | ||
| 137 | $value = $type->convertToPHPValue($value, $this->platform); | ||
| 138 | } | ||
| 139 | |||
| 140 | if ($value !== null && isset($cacheKeyInfo['enumType'])) { | ||
| 141 | $originalValue = $value; | ||
| 142 | try { | ||
| 143 | $value = $this->buildEnum($originalValue, $cacheKeyInfo['enumType']); | ||
| 144 | } catch (ValueError $e) { | ||
| 145 | throw MappingException::invalidEnumValue( | ||
| 146 | $entityName, | ||
| 147 | $cacheKeyInfo['fieldName'], | ||
| 148 | (string) $originalValue, | ||
| 149 | $cacheKeyInfo['enumType'], | ||
| 150 | $e, | ||
| 151 | ); | ||
| 152 | } | ||
| 153 | } | ||
| 154 | |||
| 155 | $fieldName = $cacheKeyInfo['fieldName']; | ||
| 156 | |||
| 157 | // Prevent overwrite in case of inherit classes using same property name (See AbstractHydrator) | ||
| 158 | if (! isset($data[$fieldName]) || ! $valueIsNull) { | ||
| 159 | $data[$fieldName] = $value; | ||
| 160 | } | ||
| 161 | } | ||
| 162 | |||
| 163 | if (isset($this->hints[Query::HINT_REFRESH_ENTITY])) { | ||
| 164 | $this->registerManaged($this->class, $this->hints[Query::HINT_REFRESH_ENTITY], $data); | ||
| 165 | } | ||
| 166 | |||
| 167 | $uow = $this->em->getUnitOfWork(); | ||
| 168 | $entity = $uow->createEntity($entityName, $data, $this->hints); | ||
| 169 | |||
| 170 | $result[] = $entity; | ||
| 171 | |||
| 172 | if (isset($this->hints[Query::HINT_INTERNAL_ITERATION]) && $this->hints[Query::HINT_INTERNAL_ITERATION]) { | ||
| 173 | $this->uow->hydrationComplete(); | ||
| 174 | } | ||
| 175 | } | ||
| 176 | } | ||
diff --git a/vendor/doctrine/orm/src/Internal/Hydration/SingleScalarHydrator.php b/vendor/doctrine/orm/src/Internal/Hydration/SingleScalarHydrator.php new file mode 100644 index 0000000..2787bbc --- /dev/null +++ b/vendor/doctrine/orm/src/Internal/Hydration/SingleScalarHydrator.php | |||
| @@ -0,0 +1,40 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Internal\Hydration; | ||
| 6 | |||
| 7 | use Doctrine\ORM\NonUniqueResultException; | ||
| 8 | use Doctrine\ORM\NoResultException; | ||
| 9 | |||
| 10 | use function array_shift; | ||
| 11 | use function count; | ||
| 12 | use function key; | ||
| 13 | |||
| 14 | /** | ||
| 15 | * Hydrator that hydrates a single scalar value from the result set. | ||
| 16 | */ | ||
| 17 | class SingleScalarHydrator extends AbstractHydrator | ||
| 18 | { | ||
| 19 | protected function hydrateAllData(): mixed | ||
| 20 | { | ||
| 21 | $data = $this->statement()->fetchAllAssociative(); | ||
| 22 | $numRows = count($data); | ||
| 23 | |||
| 24 | if ($numRows === 0) { | ||
| 25 | throw new NoResultException(); | ||
| 26 | } | ||
| 27 | |||
| 28 | if ($numRows > 1) { | ||
| 29 | throw new NonUniqueResultException('The query returned multiple rows. Change the query or use a different result function like getScalarResult().'); | ||
| 30 | } | ||
| 31 | |||
| 32 | $result = $this->gatherScalarRowData($data[key($data)]); | ||
| 33 | |||
| 34 | if (count($result) > 1) { | ||
| 35 | throw new NonUniqueResultException('The query returned a row containing multiple columns. Change the query or use a different result function like getScalarResult().'); | ||
| 36 | } | ||
| 37 | |||
| 38 | return array_shift($result); | ||
| 39 | } | ||
| 40 | } | ||
diff --git a/vendor/doctrine/orm/src/Internal/HydrationCompleteHandler.php b/vendor/doctrine/orm/src/Internal/HydrationCompleteHandler.php new file mode 100644 index 0000000..e0fe342 --- /dev/null +++ b/vendor/doctrine/orm/src/Internal/HydrationCompleteHandler.php | |||
| @@ -0,0 +1,64 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Internal; | ||
| 6 | |||
| 7 | use Doctrine\ORM\EntityManagerInterface; | ||
| 8 | use Doctrine\ORM\Event\ListenersInvoker; | ||
| 9 | use Doctrine\ORM\Event\PostLoadEventArgs; | ||
| 10 | use Doctrine\ORM\Events; | ||
| 11 | use Doctrine\ORM\Mapping\ClassMetadata; | ||
| 12 | |||
| 13 | /** | ||
| 14 | * Class, which can handle completion of hydration cycle and produce some of tasks. | ||
| 15 | * In current implementation triggers deferred postLoad event. | ||
| 16 | */ | ||
| 17 | final class HydrationCompleteHandler | ||
| 18 | { | ||
| 19 | /** @var mixed[][] */ | ||
| 20 | private array $deferredPostLoadInvocations = []; | ||
| 21 | |||
| 22 | public function __construct( | ||
| 23 | private readonly ListenersInvoker $listenersInvoker, | ||
| 24 | private readonly EntityManagerInterface $em, | ||
| 25 | ) { | ||
| 26 | } | ||
| 27 | |||
| 28 | /** | ||
| 29 | * Method schedules invoking of postLoad entity to the very end of current hydration cycle. | ||
| 30 | */ | ||
| 31 | public function deferPostLoadInvoking(ClassMetadata $class, object $entity): void | ||
| 32 | { | ||
| 33 | $invoke = $this->listenersInvoker->getSubscribedSystems($class, Events::postLoad); | ||
| 34 | |||
| 35 | if ($invoke === ListenersInvoker::INVOKE_NONE) { | ||
| 36 | return; | ||
| 37 | } | ||
| 38 | |||
| 39 | $this->deferredPostLoadInvocations[] = [$class, $invoke, $entity]; | ||
| 40 | } | ||
| 41 | |||
| 42 | /** | ||
| 43 | * This method should be called after any hydration cycle completed. | ||
| 44 | * | ||
| 45 | * Method fires all deferred invocations of postLoad events | ||
| 46 | */ | ||
| 47 | public function hydrationComplete(): void | ||
| 48 | { | ||
| 49 | $toInvoke = $this->deferredPostLoadInvocations; | ||
| 50 | $this->deferredPostLoadInvocations = []; | ||
| 51 | |||
| 52 | foreach ($toInvoke as $classAndEntity) { | ||
| 53 | [$class, $invoke, $entity] = $classAndEntity; | ||
| 54 | |||
| 55 | $this->listenersInvoker->invoke( | ||
| 56 | $class, | ||
| 57 | Events::postLoad, | ||
| 58 | $entity, | ||
| 59 | new PostLoadEventArgs($entity, $this->em), | ||
| 60 | $invoke, | ||
| 61 | ); | ||
| 62 | } | ||
| 63 | } | ||
| 64 | } | ||
diff --git a/vendor/doctrine/orm/src/Internal/NoUnknownNamedArguments.php b/vendor/doctrine/orm/src/Internal/NoUnknownNamedArguments.php new file mode 100644 index 0000000..7584744 --- /dev/null +++ b/vendor/doctrine/orm/src/Internal/NoUnknownNamedArguments.php | |||
| @@ -0,0 +1,55 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Internal; | ||
| 6 | |||
| 7 | use BadMethodCallException; | ||
| 8 | |||
| 9 | use function array_filter; | ||
| 10 | use function array_is_list; | ||
| 11 | use function array_keys; | ||
| 12 | use function array_values; | ||
| 13 | use function assert; | ||
| 14 | use function debug_backtrace; | ||
| 15 | use function implode; | ||
| 16 | use function is_string; | ||
| 17 | use function sprintf; | ||
| 18 | |||
| 19 | use const DEBUG_BACKTRACE_IGNORE_ARGS; | ||
| 20 | |||
| 21 | /** | ||
| 22 | * Checks if a variadic parameter contains unexpected named arguments. | ||
| 23 | * | ||
| 24 | * @internal | ||
| 25 | */ | ||
| 26 | trait NoUnknownNamedArguments | ||
| 27 | { | ||
| 28 | /** | ||
| 29 | * @param TItem[] $parameter | ||
| 30 | * | ||
| 31 | * @template TItem | ||
| 32 | * @psalm-assert list<TItem> $parameter | ||
| 33 | */ | ||
| 34 | private static function validateVariadicParameter(array $parameter): void | ||
| 35 | { | ||
| 36 | if (array_is_list($parameter)) { | ||
| 37 | return; | ||
| 38 | } | ||
| 39 | |||
| 40 | [, $trace] = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS); | ||
| 41 | assert(isset($trace['class'])); | ||
| 42 | |||
| 43 | $additionalArguments = array_values(array_filter( | ||
| 44 | array_keys($parameter), | ||
| 45 | is_string(...), | ||
| 46 | )); | ||
| 47 | |||
| 48 | throw new BadMethodCallException(sprintf( | ||
| 49 | 'Invalid call to %s::%s(), unknown named arguments: %s', | ||
| 50 | $trace['class'], | ||
| 51 | $trace['function'], | ||
| 52 | implode(', ', $additionalArguments), | ||
| 53 | )); | ||
| 54 | } | ||
| 55 | } | ||
diff --git a/vendor/doctrine/orm/src/Internal/QueryType.php b/vendor/doctrine/orm/src/Internal/QueryType.php new file mode 100644 index 0000000..b5e60c7 --- /dev/null +++ b/vendor/doctrine/orm/src/Internal/QueryType.php | |||
| @@ -0,0 +1,13 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Internal; | ||
| 6 | |||
| 7 | /** @internal To be used inside the QueryBuilder only. */ | ||
| 8 | enum QueryType | ||
| 9 | { | ||
| 10 | case Select; | ||
| 11 | case Delete; | ||
| 12 | case Update; | ||
| 13 | } | ||
diff --git a/vendor/doctrine/orm/src/Internal/SQLResultCasing.php b/vendor/doctrine/orm/src/Internal/SQLResultCasing.php new file mode 100644 index 0000000..53b412e --- /dev/null +++ b/vendor/doctrine/orm/src/Internal/SQLResultCasing.php | |||
| @@ -0,0 +1,30 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Internal; | ||
| 6 | |||
| 7 | use Doctrine\DBAL\Platforms\AbstractPlatform; | ||
| 8 | use Doctrine\DBAL\Platforms\DB2Platform; | ||
| 9 | use Doctrine\DBAL\Platforms\OraclePlatform; | ||
| 10 | use Doctrine\DBAL\Platforms\PostgreSQLPlatform; | ||
| 11 | |||
| 12 | use function strtolower; | ||
| 13 | use function strtoupper; | ||
| 14 | |||
| 15 | /** @internal */ | ||
| 16 | trait SQLResultCasing | ||
| 17 | { | ||
| 18 | private function getSQLResultCasing(AbstractPlatform $platform, string $column): string | ||
| 19 | { | ||
| 20 | if ($platform instanceof DB2Platform || $platform instanceof OraclePlatform) { | ||
| 21 | return strtoupper($column); | ||
| 22 | } | ||
| 23 | |||
| 24 | if ($platform instanceof PostgreSQLPlatform) { | ||
| 25 | return strtolower($column); | ||
| 26 | } | ||
| 27 | |||
| 28 | return $column; | ||
| 29 | } | ||
| 30 | } | ||
diff --git a/vendor/doctrine/orm/src/Internal/StronglyConnectedComponents.php b/vendor/doctrine/orm/src/Internal/StronglyConnectedComponents.php new file mode 100644 index 0000000..dd4fc98 --- /dev/null +++ b/vendor/doctrine/orm/src/Internal/StronglyConnectedComponents.php | |||
| @@ -0,0 +1,159 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Internal; | ||
| 6 | |||
| 7 | use InvalidArgumentException; | ||
| 8 | |||
| 9 | use function array_keys; | ||
| 10 | use function array_pop; | ||
| 11 | use function array_push; | ||
| 12 | use function min; | ||
| 13 | use function spl_object_id; | ||
| 14 | |||
| 15 | /** | ||
| 16 | * StronglyConnectedComponents implements Tarjan's algorithm to find strongly connected | ||
| 17 | * components (SCC) in a directed graph. This algorithm has a linear running time based on | ||
| 18 | * nodes (V) and edges between the nodes (E), resulting in a computational complexity | ||
| 19 | * of O(V + E). | ||
| 20 | * | ||
| 21 | * See https://en.wikipedia.org/wiki/Tarjan%27s_strongly_connected_components_algorithm | ||
| 22 | * for an explanation and the meaning of the DFS and lowlink numbers. | ||
| 23 | * | ||
| 24 | * @internal | ||
| 25 | */ | ||
| 26 | final class StronglyConnectedComponents | ||
| 27 | { | ||
| 28 | private const NOT_VISITED = 1; | ||
| 29 | private const IN_PROGRESS = 2; | ||
| 30 | private const VISITED = 3; | ||
| 31 | |||
| 32 | /** | ||
| 33 | * Array of all nodes, indexed by object ids. | ||
| 34 | * | ||
| 35 | * @var array<int, object> | ||
| 36 | */ | ||
| 37 | private array $nodes = []; | ||
| 38 | |||
| 39 | /** | ||
| 40 | * DFS state for the different nodes, indexed by node object id and using one of | ||
| 41 | * this class' constants as value. | ||
| 42 | * | ||
| 43 | * @var array<int, self::*> | ||
| 44 | */ | ||
| 45 | private array $states = []; | ||
| 46 | |||
| 47 | /** | ||
| 48 | * Edges between the nodes. The first-level key is the object id of the outgoing | ||
| 49 | * node; the second array maps the destination node by object id as key. | ||
| 50 | * | ||
| 51 | * @var array<int, array<int, bool>> | ||
| 52 | */ | ||
| 53 | private array $edges = []; | ||
| 54 | |||
| 55 | /** | ||
| 56 | * DFS numbers, by object ID | ||
| 57 | * | ||
| 58 | * @var array<int, int> | ||
| 59 | */ | ||
| 60 | private array $dfs = []; | ||
| 61 | |||
| 62 | /** | ||
| 63 | * lowlink numbers, by object ID | ||
| 64 | * | ||
| 65 | * @var array<int, int> | ||
| 66 | */ | ||
| 67 | private array $lowlink = []; | ||
| 68 | |||
| 69 | private int $maxdfs = 0; | ||
| 70 | |||
| 71 | /** | ||
| 72 | * Nodes representing the SCC another node is in, indexed by lookup-node object ID | ||
| 73 | * | ||
| 74 | * @var array<int, object> | ||
| 75 | */ | ||
| 76 | private array $representingNodes = []; | ||
| 77 | |||
| 78 | /** | ||
| 79 | * Stack with OIDs of nodes visited in the current state of the DFS | ||
| 80 | * | ||
| 81 | * @var list<int> | ||
| 82 | */ | ||
| 83 | private array $stack = []; | ||
| 84 | |||
| 85 | public function addNode(object $node): void | ||
| 86 | { | ||
| 87 | $id = spl_object_id($node); | ||
| 88 | $this->nodes[$id] = $node; | ||
| 89 | $this->states[$id] = self::NOT_VISITED; | ||
| 90 | $this->edges[$id] = []; | ||
| 91 | } | ||
| 92 | |||
| 93 | public function hasNode(object $node): bool | ||
| 94 | { | ||
| 95 | return isset($this->nodes[spl_object_id($node)]); | ||
| 96 | } | ||
| 97 | |||
| 98 | /** | ||
| 99 | * Adds a new edge between two nodes to the graph | ||
| 100 | */ | ||
| 101 | public function addEdge(object $from, object $to): void | ||
| 102 | { | ||
| 103 | $fromId = spl_object_id($from); | ||
| 104 | $toId = spl_object_id($to); | ||
| 105 | |||
| 106 | $this->edges[$fromId][$toId] = true; | ||
| 107 | } | ||
| 108 | |||
| 109 | public function findStronglyConnectedComponents(): void | ||
| 110 | { | ||
| 111 | foreach (array_keys($this->nodes) as $oid) { | ||
| 112 | if ($this->states[$oid] === self::NOT_VISITED) { | ||
| 113 | $this->tarjan($oid); | ||
| 114 | } | ||
| 115 | } | ||
| 116 | } | ||
| 117 | |||
| 118 | private function tarjan(int $oid): void | ||
| 119 | { | ||
| 120 | $this->dfs[$oid] = $this->lowlink[$oid] = $this->maxdfs++; | ||
| 121 | $this->states[$oid] = self::IN_PROGRESS; | ||
| 122 | array_push($this->stack, $oid); | ||
| 123 | |||
| 124 | foreach ($this->edges[$oid] as $adjacentId => $ignored) { | ||
| 125 | if ($this->states[$adjacentId] === self::NOT_VISITED) { | ||
| 126 | $this->tarjan($adjacentId); | ||
| 127 | $this->lowlink[$oid] = min($this->lowlink[$oid], $this->lowlink[$adjacentId]); | ||
| 128 | } elseif ($this->states[$adjacentId] === self::IN_PROGRESS) { | ||
| 129 | $this->lowlink[$oid] = min($this->lowlink[$oid], $this->dfs[$adjacentId]); | ||
| 130 | } | ||
| 131 | } | ||
| 132 | |||
| 133 | $lowlink = $this->lowlink[$oid]; | ||
| 134 | if ($lowlink === $this->dfs[$oid]) { | ||
| 135 | $representingNode = null; | ||
| 136 | do { | ||
| 137 | $unwindOid = array_pop($this->stack); | ||
| 138 | |||
| 139 | if (! $representingNode) { | ||
| 140 | $representingNode = $this->nodes[$unwindOid]; | ||
| 141 | } | ||
| 142 | |||
| 143 | $this->representingNodes[$unwindOid] = $representingNode; | ||
| 144 | $this->states[$unwindOid] = self::VISITED; | ||
| 145 | } while ($unwindOid !== $oid); | ||
| 146 | } | ||
| 147 | } | ||
| 148 | |||
| 149 | public function getNodeRepresentingStronglyConnectedComponent(object $node): object | ||
| 150 | { | ||
| 151 | $oid = spl_object_id($node); | ||
| 152 | |||
| 153 | if (! isset($this->representingNodes[$oid])) { | ||
| 154 | throw new InvalidArgumentException('unknown node'); | ||
| 155 | } | ||
| 156 | |||
| 157 | return $this->representingNodes[$oid]; | ||
| 158 | } | ||
| 159 | } | ||
diff --git a/vendor/doctrine/orm/src/Internal/TopologicalSort.php b/vendor/doctrine/orm/src/Internal/TopologicalSort.php new file mode 100644 index 0000000..808bc0f --- /dev/null +++ b/vendor/doctrine/orm/src/Internal/TopologicalSort.php | |||
| @@ -0,0 +1,155 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Internal; | ||
| 6 | |||
| 7 | use Doctrine\ORM\Internal\TopologicalSort\CycleDetectedException; | ||
| 8 | |||
| 9 | use function array_keys; | ||
| 10 | use function spl_object_id; | ||
| 11 | |||
| 12 | /** | ||
| 13 | * TopologicalSort implements topological sorting, which is an ordering | ||
| 14 | * algorithm for directed graphs (DG) using a depth-first searching (DFS) | ||
| 15 | * to traverse the graph built in memory. | ||
| 16 | * This algorithm has a linear running time based on nodes (V) and edges | ||
| 17 | * between the nodes (E), resulting in a computational complexity of O(V + E). | ||
| 18 | * | ||
| 19 | * @internal | ||
| 20 | */ | ||
| 21 | final class TopologicalSort | ||
| 22 | { | ||
| 23 | private const NOT_VISITED = 1; | ||
| 24 | private const IN_PROGRESS = 2; | ||
| 25 | private const VISITED = 3; | ||
| 26 | |||
| 27 | /** | ||
| 28 | * Array of all nodes, indexed by object ids. | ||
| 29 | * | ||
| 30 | * @var array<int, object> | ||
| 31 | */ | ||
| 32 | private array $nodes = []; | ||
| 33 | |||
| 34 | /** | ||
| 35 | * DFS state for the different nodes, indexed by node object id and using one of | ||
| 36 | * this class' constants as value. | ||
| 37 | * | ||
| 38 | * @var array<int, self::*> | ||
| 39 | */ | ||
| 40 | private array $states = []; | ||
| 41 | |||
| 42 | /** | ||
| 43 | * Edges between the nodes. The first-level key is the object id of the outgoing | ||
| 44 | * node; the second array maps the destination node by object id as key. The final | ||
| 45 | * boolean value indicates whether the edge is optional or not. | ||
| 46 | * | ||
| 47 | * @var array<int, array<int, bool>> | ||
| 48 | */ | ||
| 49 | private array $edges = []; | ||
| 50 | |||
| 51 | /** | ||
| 52 | * Builds up the result during the DFS. | ||
| 53 | * | ||
| 54 | * @var list<object> | ||
| 55 | */ | ||
| 56 | private array $sortResult = []; | ||
| 57 | |||
| 58 | public function addNode(object $node): void | ||
| 59 | { | ||
| 60 | $id = spl_object_id($node); | ||
| 61 | $this->nodes[$id] = $node; | ||
| 62 | $this->states[$id] = self::NOT_VISITED; | ||
| 63 | $this->edges[$id] = []; | ||
| 64 | } | ||
| 65 | |||
| 66 | public function hasNode(object $node): bool | ||
| 67 | { | ||
| 68 | return isset($this->nodes[spl_object_id($node)]); | ||
| 69 | } | ||
| 70 | |||
| 71 | /** | ||
| 72 | * Adds a new edge between two nodes to the graph | ||
| 73 | * | ||
| 74 | * @param bool $optional This indicates whether the edge may be ignored during the topological sort if it is necessary to break cycles. | ||
| 75 | */ | ||
| 76 | public function addEdge(object $from, object $to, bool $optional): void | ||
| 77 | { | ||
| 78 | $fromId = spl_object_id($from); | ||
| 79 | $toId = spl_object_id($to); | ||
| 80 | |||
| 81 | if (isset($this->edges[$fromId][$toId]) && $this->edges[$fromId][$toId] === false) { | ||
| 82 | return; // we already know about this dependency, and it is not optional | ||
| 83 | } | ||
| 84 | |||
| 85 | $this->edges[$fromId][$toId] = $optional; | ||
| 86 | } | ||
| 87 | |||
| 88 | /** | ||
| 89 | * Returns a topological sort of all nodes. When we have an edge A->B between two nodes | ||
| 90 | * A and B, then B will be listed before A in the result. Visually speaking, when ordering | ||
| 91 | * the nodes in the result order from left to right, all edges point to the left. | ||
| 92 | * | ||
| 93 | * @return list<object> | ||
| 94 | */ | ||
| 95 | public function sort(): array | ||
| 96 | { | ||
| 97 | foreach (array_keys($this->nodes) as $oid) { | ||
| 98 | if ($this->states[$oid] === self::NOT_VISITED) { | ||
| 99 | $this->visit($oid); | ||
| 100 | } | ||
| 101 | } | ||
| 102 | |||
| 103 | return $this->sortResult; | ||
| 104 | } | ||
| 105 | |||
| 106 | private function visit(int $oid): void | ||
| 107 | { | ||
| 108 | if ($this->states[$oid] === self::IN_PROGRESS) { | ||
| 109 | // This node is already on the current DFS stack. We've found a cycle! | ||
| 110 | throw new CycleDetectedException($this->nodes[$oid]); | ||
| 111 | } | ||
| 112 | |||
| 113 | if ($this->states[$oid] === self::VISITED) { | ||
| 114 | // We've reached a node that we've already seen, including all | ||
| 115 | // other nodes that are reachable from here. We're done here, return. | ||
| 116 | return; | ||
| 117 | } | ||
| 118 | |||
| 119 | $this->states[$oid] = self::IN_PROGRESS; | ||
| 120 | |||
| 121 | // Continue the DFS downwards the edge list | ||
| 122 | foreach ($this->edges[$oid] as $adjacentId => $optional) { | ||
| 123 | try { | ||
| 124 | $this->visit($adjacentId); | ||
| 125 | } catch (CycleDetectedException $exception) { | ||
| 126 | if ($exception->isCycleCollected()) { | ||
| 127 | // There is a complete cycle downstream of the current node. We cannot | ||
| 128 | // do anything about that anymore. | ||
| 129 | throw $exception; | ||
| 130 | } | ||
| 131 | |||
| 132 | if ($optional) { | ||
| 133 | // The current edge is part of a cycle, but it is optional and the closest | ||
| 134 | // such edge while backtracking. Break the cycle here by skipping the edge | ||
| 135 | // and continuing with the next one. | ||
| 136 | continue; | ||
| 137 | } | ||
| 138 | |||
| 139 | // We have found a cycle and cannot break it at $edge. Best we can do | ||
| 140 | // is to backtrack from the current vertex, hoping that somewhere up the | ||
| 141 | // stack this can be salvaged. | ||
| 142 | $this->states[$oid] = self::NOT_VISITED; | ||
| 143 | $exception->addToCycle($this->nodes[$oid]); | ||
| 144 | |||
| 145 | throw $exception; | ||
| 146 | } | ||
| 147 | } | ||
| 148 | |||
| 149 | // We have traversed all edges and visited all other nodes reachable from here. | ||
| 150 | // So we're done with this vertex as well. | ||
| 151 | |||
| 152 | $this->states[$oid] = self::VISITED; | ||
| 153 | $this->sortResult[] = $this->nodes[$oid]; | ||
| 154 | } | ||
| 155 | } | ||
diff --git a/vendor/doctrine/orm/src/Internal/TopologicalSort/CycleDetectedException.php b/vendor/doctrine/orm/src/Internal/TopologicalSort/CycleDetectedException.php new file mode 100644 index 0000000..3af5329 --- /dev/null +++ b/vendor/doctrine/orm/src/Internal/TopologicalSort/CycleDetectedException.php | |||
| @@ -0,0 +1,47 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Internal\TopologicalSort; | ||
| 6 | |||
| 7 | use RuntimeException; | ||
| 8 | |||
| 9 | use function array_unshift; | ||
| 10 | |||
| 11 | class CycleDetectedException extends RuntimeException | ||
| 12 | { | ||
| 13 | /** @var list<object> */ | ||
| 14 | private array $cycle; | ||
| 15 | |||
| 16 | /** | ||
| 17 | * Do we have the complete cycle collected? | ||
| 18 | */ | ||
| 19 | private bool $cycleCollected = false; | ||
| 20 | |||
| 21 | public function __construct(private readonly object $startNode) | ||
| 22 | { | ||
| 23 | parent::__construct('A cycle has been detected, so a topological sort is not possible. The getCycle() method provides the list of nodes that form the cycle.'); | ||
| 24 | |||
| 25 | $this->cycle = [$startNode]; | ||
| 26 | } | ||
| 27 | |||
| 28 | /** @return list<object> */ | ||
| 29 | public function getCycle(): array | ||
| 30 | { | ||
| 31 | return $this->cycle; | ||
| 32 | } | ||
| 33 | |||
| 34 | public function addToCycle(object $node): void | ||
| 35 | { | ||
| 36 | array_unshift($this->cycle, $node); | ||
| 37 | |||
| 38 | if ($node === $this->startNode) { | ||
| 39 | $this->cycleCollected = true; | ||
| 40 | } | ||
| 41 | } | ||
| 42 | |||
| 43 | public function isCycleCollected(): bool | ||
| 44 | { | ||
| 45 | return $this->cycleCollected; | ||
| 46 | } | ||
| 47 | } | ||
diff --git a/vendor/doctrine/orm/src/LazyCriteriaCollection.php b/vendor/doctrine/orm/src/LazyCriteriaCollection.php new file mode 100644 index 0000000..ca67914 --- /dev/null +++ b/vendor/doctrine/orm/src/LazyCriteriaCollection.php | |||
| @@ -0,0 +1,96 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM; | ||
| 6 | |||
| 7 | use Doctrine\Common\Collections\AbstractLazyCollection; | ||
| 8 | use Doctrine\Common\Collections\ArrayCollection; | ||
| 9 | use Doctrine\Common\Collections\Criteria; | ||
| 10 | use Doctrine\Common\Collections\ReadableCollection; | ||
| 11 | use Doctrine\Common\Collections\Selectable; | ||
| 12 | use Doctrine\ORM\Persisters\Entity\EntityPersister; | ||
| 13 | |||
| 14 | use function assert; | ||
| 15 | |||
| 16 | /** | ||
| 17 | * A lazy collection that allows a fast count when using criteria object | ||
| 18 | * Once count gets executed once without collection being initialized, result | ||
| 19 | * is cached and returned on subsequent calls until collection gets loaded, | ||
| 20 | * then returning the number of loaded results. | ||
| 21 | * | ||
| 22 | * @template TKey of array-key | ||
| 23 | * @template TValue of object | ||
| 24 | * @extends AbstractLazyCollection<TKey, TValue> | ||
| 25 | * @implements Selectable<TKey, TValue> | ||
| 26 | */ | ||
| 27 | class LazyCriteriaCollection extends AbstractLazyCollection implements Selectable | ||
| 28 | { | ||
| 29 | private int|null $count = null; | ||
| 30 | |||
| 31 | public function __construct( | ||
| 32 | protected EntityPersister $entityPersister, | ||
| 33 | protected Criteria $criteria, | ||
| 34 | ) { | ||
| 35 | } | ||
| 36 | |||
| 37 | /** | ||
| 38 | * Do an efficient count on the collection | ||
| 39 | */ | ||
| 40 | public function count(): int | ||
| 41 | { | ||
| 42 | if ($this->isInitialized()) { | ||
| 43 | return $this->collection->count(); | ||
| 44 | } | ||
| 45 | |||
| 46 | // Return cached result in case count query was already executed | ||
| 47 | if ($this->count !== null) { | ||
| 48 | return $this->count; | ||
| 49 | } | ||
| 50 | |||
| 51 | return $this->count = $this->entityPersister->count($this->criteria); | ||
| 52 | } | ||
| 53 | |||
| 54 | /** | ||
| 55 | * check if collection is empty without loading it | ||
| 56 | */ | ||
| 57 | public function isEmpty(): bool | ||
| 58 | { | ||
| 59 | if ($this->isInitialized()) { | ||
| 60 | return $this->collection->isEmpty(); | ||
| 61 | } | ||
| 62 | |||
| 63 | return ! $this->count(); | ||
| 64 | } | ||
| 65 | |||
| 66 | /** | ||
| 67 | * Do an optimized search of an element | ||
| 68 | * | ||
| 69 | * @param mixed $element The element to search for. | ||
| 70 | * | ||
| 71 | * @return bool TRUE if the collection contains $element, FALSE otherwise. | ||
| 72 | */ | ||
| 73 | public function contains(mixed $element): bool | ||
| 74 | { | ||
| 75 | if ($this->isInitialized()) { | ||
| 76 | return $this->collection->contains($element); | ||
| 77 | } | ||
| 78 | |||
| 79 | return $this->entityPersister->exists($element, $this->criteria); | ||
| 80 | } | ||
| 81 | |||
| 82 | /** @return ReadableCollection<TKey, TValue>&Selectable<TKey, TValue> */ | ||
| 83 | public function matching(Criteria $criteria): ReadableCollection&Selectable | ||
| 84 | { | ||
| 85 | $this->initialize(); | ||
| 86 | assert($this->collection instanceof Selectable); | ||
| 87 | |||
| 88 | return $this->collection->matching($criteria); | ||
| 89 | } | ||
| 90 | |||
| 91 | protected function doInitialize(): void | ||
| 92 | { | ||
| 93 | $elements = $this->entityPersister->loadCriteria($this->criteria); | ||
| 94 | $this->collection = new ArrayCollection($elements); | ||
| 95 | } | ||
| 96 | } | ||
diff --git a/vendor/doctrine/orm/src/Mapping/AnsiQuoteStrategy.php b/vendor/doctrine/orm/src/Mapping/AnsiQuoteStrategy.php new file mode 100644 index 0000000..872d4d6 --- /dev/null +++ b/vendor/doctrine/orm/src/Mapping/AnsiQuoteStrategy.php | |||
| @@ -0,0 +1,76 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Mapping; | ||
| 6 | |||
| 7 | use Doctrine\DBAL\Platforms\AbstractPlatform; | ||
| 8 | use Doctrine\ORM\Internal\SQLResultCasing; | ||
| 9 | |||
| 10 | /** | ||
| 11 | * ANSI compliant quote strategy, this strategy does not apply any quote. | ||
| 12 | * To use this strategy all mapped tables and columns should be ANSI compliant. | ||
| 13 | */ | ||
| 14 | class AnsiQuoteStrategy implements QuoteStrategy | ||
| 15 | { | ||
| 16 | use SQLResultCasing; | ||
| 17 | |||
| 18 | public function getColumnName( | ||
| 19 | string $fieldName, | ||
| 20 | ClassMetadata $class, | ||
| 21 | AbstractPlatform $platform, | ||
| 22 | ): string { | ||
| 23 | return $class->fieldMappings[$fieldName]->columnName; | ||
| 24 | } | ||
| 25 | |||
| 26 | public function getTableName(ClassMetadata $class, AbstractPlatform $platform): string | ||
| 27 | { | ||
| 28 | return $class->table['name']; | ||
| 29 | } | ||
| 30 | |||
| 31 | /** | ||
| 32 | * {@inheritDoc} | ||
| 33 | */ | ||
| 34 | public function getSequenceName(array $definition, ClassMetadata $class, AbstractPlatform $platform): string | ||
| 35 | { | ||
| 36 | return $definition['sequenceName']; | ||
| 37 | } | ||
| 38 | |||
| 39 | public function getJoinColumnName(JoinColumnMapping $joinColumn, ClassMetadata $class, AbstractPlatform $platform): string | ||
| 40 | { | ||
| 41 | return $joinColumn->name; | ||
| 42 | } | ||
| 43 | |||
| 44 | public function getReferencedJoinColumnName( | ||
| 45 | JoinColumnMapping $joinColumn, | ||
| 46 | ClassMetadata $class, | ||
| 47 | AbstractPlatform $platform, | ||
| 48 | ): string { | ||
| 49 | return $joinColumn->referencedColumnName; | ||
| 50 | } | ||
| 51 | |||
| 52 | public function getJoinTableName( | ||
| 53 | ManyToManyOwningSideMapping $association, | ||
| 54 | ClassMetadata $class, | ||
| 55 | AbstractPlatform $platform, | ||
| 56 | ): string { | ||
| 57 | return $association->joinTable->name; | ||
| 58 | } | ||
| 59 | |||
| 60 | /** | ||
| 61 | * {@inheritDoc} | ||
| 62 | */ | ||
| 63 | public function getIdentifierColumnNames(ClassMetadata $class, AbstractPlatform $platform): array | ||
| 64 | { | ||
| 65 | return $class->identifier; | ||
| 66 | } | ||
| 67 | |||
| 68 | public function getColumnAlias( | ||
| 69 | string $columnName, | ||
| 70 | int $counter, | ||
| 71 | AbstractPlatform $platform, | ||
| 72 | ClassMetadata|null $class = null, | ||
| 73 | ): string { | ||
| 74 | return $this->getSQLResultCasing($platform, $columnName . '_' . $counter); | ||
| 75 | } | ||
| 76 | } | ||
diff --git a/vendor/doctrine/orm/src/Mapping/ArrayAccessImplementation.php b/vendor/doctrine/orm/src/Mapping/ArrayAccessImplementation.php new file mode 100644 index 0000000..3fd0988 --- /dev/null +++ b/vendor/doctrine/orm/src/Mapping/ArrayAccessImplementation.php | |||
| @@ -0,0 +1,70 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Mapping; | ||
| 6 | |||
| 7 | use Doctrine\Deprecations\Deprecation; | ||
| 8 | use InvalidArgumentException; | ||
| 9 | |||
| 10 | use function property_exists; | ||
| 11 | |||
| 12 | /** @internal */ | ||
| 13 | trait ArrayAccessImplementation | ||
| 14 | { | ||
| 15 | /** @param string $offset */ | ||
| 16 | public function offsetExists(mixed $offset): bool | ||
| 17 | { | ||
| 18 | Deprecation::trigger( | ||
| 19 | 'doctrine/orm', | ||
| 20 | 'https://github.com/doctrine/orm/pull/11211', | ||
| 21 | 'Using ArrayAccess on %s is deprecated and will not be possible in Doctrine ORM 4.0. Use the corresponding property instead.', | ||
| 22 | static::class, | ||
| 23 | ); | ||
| 24 | |||
| 25 | return isset($this->$offset); | ||
| 26 | } | ||
| 27 | |||
| 28 | /** @param string $offset */ | ||
| 29 | public function offsetGet(mixed $offset): mixed | ||
| 30 | { | ||
| 31 | Deprecation::trigger( | ||
| 32 | 'doctrine/orm', | ||
| 33 | 'https://github.com/doctrine/orm/pull/11211', | ||
| 34 | 'Using ArrayAccess on %s is deprecated and will not be possible in Doctrine ORM 4.0. Use the corresponding property instead.', | ||
| 35 | static::class, | ||
| 36 | ); | ||
| 37 | |||
| 38 | if (! property_exists($this, $offset)) { | ||
| 39 | throw new InvalidArgumentException('Undefined property: ' . $offset); | ||
| 40 | } | ||
| 41 | |||
| 42 | return $this->$offset; | ||
| 43 | } | ||
| 44 | |||
| 45 | /** @param string $offset */ | ||
| 46 | public function offsetSet(mixed $offset, mixed $value): void | ||
| 47 | { | ||
| 48 | Deprecation::trigger( | ||
| 49 | 'doctrine/orm', | ||
| 50 | 'https://github.com/doctrine/orm/pull/11211', | ||
| 51 | 'Using ArrayAccess on %s is deprecated and will not be possible in Doctrine ORM 4.0. Use the corresponding property instead.', | ||
| 52 | static::class, | ||
| 53 | ); | ||
| 54 | |||
| 55 | $this->$offset = $value; | ||
| 56 | } | ||
| 57 | |||
| 58 | /** @param string $offset */ | ||
| 59 | public function offsetUnset(mixed $offset): void | ||
| 60 | { | ||
| 61 | Deprecation::trigger( | ||
| 62 | 'doctrine/orm', | ||
| 63 | 'https://github.com/doctrine/orm/pull/11211', | ||
| 64 | 'Using ArrayAccess on %s is deprecated and will not be possible in Doctrine ORM 4.0. Use the corresponding property instead.', | ||
| 65 | static::class, | ||
| 66 | ); | ||
| 67 | |||
| 68 | $this->$offset = null; | ||
| 69 | } | ||
| 70 | } | ||
diff --git a/vendor/doctrine/orm/src/Mapping/AssociationMapping.php b/vendor/doctrine/orm/src/Mapping/AssociationMapping.php new file mode 100644 index 0000000..ce7bdb4 --- /dev/null +++ b/vendor/doctrine/orm/src/Mapping/AssociationMapping.php | |||
| @@ -0,0 +1,359 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Mapping; | ||
| 6 | |||
| 7 | use ArrayAccess; | ||
| 8 | use Exception; | ||
| 9 | use OutOfRangeException; | ||
| 10 | |||
| 11 | use function assert; | ||
| 12 | use function count; | ||
| 13 | use function in_array; | ||
| 14 | use function property_exists; | ||
| 15 | use function sprintf; | ||
| 16 | |||
| 17 | /** @template-implements ArrayAccess<string, mixed> */ | ||
| 18 | abstract class AssociationMapping implements ArrayAccess | ||
| 19 | { | ||
| 20 | /** | ||
| 21 | * The names of persistence operations to cascade on the association. | ||
| 22 | * | ||
| 23 | * @var list<'persist'|'remove'|'detach'|'refresh'|'all'> | ||
| 24 | */ | ||
| 25 | public array $cascade = []; | ||
| 26 | |||
| 27 | /** | ||
| 28 | * The fetching strategy to use for the association, usually defaults to FETCH_LAZY. | ||
| 29 | * | ||
| 30 | * @var ClassMetadata::FETCH_*|null | ||
| 31 | */ | ||
| 32 | public int|null $fetch = null; | ||
| 33 | |||
| 34 | /** | ||
| 35 | * This is set when the association is inherited by this class from another | ||
| 36 | * (inheritance) parent <em>entity</em> class. The value is the FQCN of the | ||
| 37 | * topmost entity class that contains this association. (If there are | ||
| 38 | * transient classes in the class hierarchy, these are ignored, so the | ||
| 39 | * class property may in fact come from a class further up in the PHP class | ||
| 40 | * hierarchy.) To-many associations initially declared in mapped | ||
| 41 | * superclasses are <em>not</em> considered 'inherited' in the nearest | ||
| 42 | * entity subclasses. | ||
| 43 | * | ||
| 44 | * @var class-string|null | ||
| 45 | */ | ||
| 46 | public string|null $inherited = null; | ||
| 47 | |||
| 48 | /** | ||
| 49 | * This is set when the association does not appear in the current class | ||
| 50 | * for the first time, but is initially declared in another parent | ||
| 51 | * <em>entity or mapped superclass</em>. The value is the FQCN of the | ||
| 52 | * topmost non-transient class that contains association information for | ||
| 53 | * this relationship. | ||
| 54 | * | ||
| 55 | * @var class-string|null | ||
| 56 | */ | ||
| 57 | public string|null $declared = null; | ||
| 58 | |||
| 59 | public array|null $cache = null; | ||
| 60 | |||
| 61 | public bool|null $id = null; | ||
| 62 | |||
| 63 | public bool|null $isOnDeleteCascade = null; | ||
| 64 | |||
| 65 | /** @var class-string|null */ | ||
| 66 | public string|null $originalClass = null; | ||
| 67 | |||
| 68 | public string|null $originalField = null; | ||
| 69 | |||
| 70 | public bool $orphanRemoval = false; | ||
| 71 | |||
| 72 | public bool|null $unique = null; | ||
| 73 | |||
| 74 | /** | ||
| 75 | * @param string $fieldName The name of the field in the entity | ||
| 76 | * the association is mapped to. | ||
| 77 | * @param class-string $sourceEntity The class name of the source entity. | ||
| 78 | * In the case of to-many associations | ||
| 79 | * initially present in mapped | ||
| 80 | * superclasses, the nearest | ||
| 81 | * <em>entity</em> subclasses will be | ||
| 82 | * considered the respective source | ||
| 83 | * entities. | ||
| 84 | * @param class-string $targetEntity The class name of the target entity. | ||
| 85 | * If it is fully-qualified it is used as | ||
| 86 | * is. If it is a simple, unqualified | ||
| 87 | * class name the namespace is assumed to | ||
| 88 | * be the same as the namespace of the | ||
| 89 | * source entity. | ||
| 90 | */ | ||
| 91 | final public function __construct( | ||
| 92 | public readonly string $fieldName, | ||
| 93 | public string $sourceEntity, | ||
| 94 | public readonly string $targetEntity, | ||
| 95 | ) { | ||
| 96 | } | ||
| 97 | |||
| 98 | /** | ||
| 99 | * @param mixed[] $mappingArray | ||
| 100 | * @psalm-param array{ | ||
| 101 | * fieldName: string, | ||
| 102 | * sourceEntity: class-string, | ||
| 103 | * targetEntity: class-string, | ||
| 104 | * cascade?: list<'persist'|'remove'|'detach'|'refresh'|'all'>, | ||
| 105 | * fetch?: ClassMetadata::FETCH_*|null, | ||
| 106 | * inherited?: class-string|null, | ||
| 107 | * declared?: class-string|null, | ||
| 108 | * cache?: array<mixed>|null, | ||
| 109 | * id?: bool|null, | ||
| 110 | * isOnDeleteCascade?: bool|null, | ||
| 111 | * originalClass?: class-string|null, | ||
| 112 | * originalField?: string|null, | ||
| 113 | * orphanRemoval?: bool, | ||
| 114 | * unique?: bool|null, | ||
| 115 | * joinTable?: mixed[]|null, | ||
| 116 | * type?: int, | ||
| 117 | * isOwningSide: bool, | ||
| 118 | * } $mappingArray | ||
| 119 | */ | ||
| 120 | public static function fromMappingArray(array $mappingArray): static | ||
| 121 | { | ||
| 122 | unset($mappingArray['isOwningSide'], $mappingArray['type']); | ||
| 123 | $mapping = new static( | ||
| 124 | $mappingArray['fieldName'], | ||
| 125 | $mappingArray['sourceEntity'], | ||
| 126 | $mappingArray['targetEntity'], | ||
| 127 | ); | ||
| 128 | unset($mappingArray['fieldName'], $mappingArray['sourceEntity'], $mappingArray['targetEntity']); | ||
| 129 | |||
| 130 | foreach ($mappingArray as $key => $value) { | ||
| 131 | if ($key === 'joinTable') { | ||
| 132 | assert($mapping instanceof ManyToManyAssociationMapping); | ||
| 133 | |||
| 134 | if ($value === [] || $value === null) { | ||
| 135 | continue; | ||
| 136 | } | ||
| 137 | |||
| 138 | assert($mapping instanceof ManyToManyOwningSideMapping); | ||
| 139 | |||
| 140 | $mapping->joinTable = JoinTableMapping::fromMappingArray($value); | ||
| 141 | |||
| 142 | continue; | ||
| 143 | } | ||
| 144 | |||
| 145 | if (property_exists($mapping, $key)) { | ||
| 146 | $mapping->$key = $value; | ||
| 147 | } else { | ||
| 148 | throw new OutOfRangeException('Unknown property ' . $key . ' on class ' . static::class); | ||
| 149 | } | ||
| 150 | } | ||
| 151 | |||
| 152 | return $mapping; | ||
| 153 | } | ||
| 154 | |||
| 155 | /** | ||
| 156 | * @psalm-assert-if-true OwningSideMapping $this | ||
| 157 | * @psalm-assert-if-false InverseSideMapping $this | ||
| 158 | */ | ||
| 159 | final public function isOwningSide(): bool | ||
| 160 | { | ||
| 161 | return $this instanceof OwningSideMapping; | ||
| 162 | } | ||
| 163 | |||
| 164 | /** @psalm-assert-if-true ToOneAssociationMapping $this */ | ||
| 165 | final public function isToOne(): bool | ||
| 166 | { | ||
| 167 | return $this instanceof ToOneAssociationMapping; | ||
| 168 | } | ||
| 169 | |||
| 170 | /** @psalm-assert-if-true ToManyAssociationMapping $this */ | ||
| 171 | final public function isToMany(): bool | ||
| 172 | { | ||
| 173 | return $this instanceof ToManyAssociationMapping; | ||
| 174 | } | ||
| 175 | |||
| 176 | /** @psalm-assert-if-true OneToOneOwningSideMapping $this */ | ||
| 177 | final public function isOneToOneOwningSide(): bool | ||
| 178 | { | ||
| 179 | return $this->isOneToOne() && $this->isOwningSide(); | ||
| 180 | } | ||
| 181 | |||
| 182 | /** @psalm-assert-if-true OneToOneOwningSideMapping|ManyToOneAssociationMapping $this */ | ||
| 183 | final public function isToOneOwningSide(): bool | ||
| 184 | { | ||
| 185 | return $this->isToOne() && $this->isOwningSide(); | ||
| 186 | } | ||
| 187 | |||
| 188 | /** @psalm-assert-if-true ManyToManyOwningSideMapping $this */ | ||
| 189 | final public function isManyToManyOwningSide(): bool | ||
| 190 | { | ||
| 191 | return $this instanceof ManyToManyOwningSideMapping; | ||
| 192 | } | ||
| 193 | |||
| 194 | /** @psalm-assert-if-true OneToOneAssociationMapping $this */ | ||
| 195 | final public function isOneToOne(): bool | ||
| 196 | { | ||
| 197 | return $this instanceof OneToOneAssociationMapping; | ||
| 198 | } | ||
| 199 | |||
| 200 | /** @psalm-assert-if-true OneToManyAssociationMapping $this */ | ||
| 201 | final public function isOneToMany(): bool | ||
| 202 | { | ||
| 203 | return $this instanceof OneToManyAssociationMapping; | ||
| 204 | } | ||
| 205 | |||
| 206 | /** @psalm-assert-if-true ManyToOneAssociationMapping $this */ | ||
| 207 | final public function isManyToOne(): bool | ||
| 208 | { | ||
| 209 | return $this instanceof ManyToOneAssociationMapping; | ||
| 210 | } | ||
| 211 | |||
| 212 | /** @psalm-assert-if-true ManyToManyAssociationMapping $this */ | ||
| 213 | final public function isManyToMany(): bool | ||
| 214 | { | ||
| 215 | return $this instanceof ManyToManyAssociationMapping; | ||
| 216 | } | ||
| 217 | |||
| 218 | /** @psalm-assert-if-true ToManyAssociationMapping $this */ | ||
| 219 | final public function isOrdered(): bool | ||
| 220 | { | ||
| 221 | return $this->isToMany() && $this->orderBy() !== []; | ||
| 222 | } | ||
| 223 | |||
| 224 | /** @psalm-assert-if-true ToManyAssociationMapping $this */ | ||
| 225 | public function isIndexed(): bool | ||
| 226 | { | ||
| 227 | return false; | ||
| 228 | } | ||
| 229 | |||
| 230 | final public function type(): int | ||
| 231 | { | ||
| 232 | return match (true) { | ||
| 233 | $this instanceof OneToOneAssociationMapping => ClassMetadata::ONE_TO_ONE, | ||
| 234 | $this instanceof OneToManyAssociationMapping => ClassMetadata::ONE_TO_MANY, | ||
| 235 | $this instanceof ManyToOneAssociationMapping => ClassMetadata::MANY_TO_ONE, | ||
| 236 | $this instanceof ManyToManyAssociationMapping => ClassMetadata::MANY_TO_MANY, | ||
| 237 | default => throw new Exception('Cannot determine type for ' . static::class), | ||
| 238 | }; | ||
| 239 | } | ||
| 240 | |||
| 241 | /** @param string $offset */ | ||
| 242 | public function offsetExists(mixed $offset): bool | ||
| 243 | { | ||
| 244 | return isset($this->$offset) || in_array($offset, ['isOwningSide', 'type'], true); | ||
| 245 | } | ||
| 246 | |||
| 247 | final public function offsetGet(mixed $offset): mixed | ||
| 248 | { | ||
| 249 | return match ($offset) { | ||
| 250 | 'isOwningSide' => $this->isOwningSide(), | ||
| 251 | 'type' => $this->type(), | ||
| 252 | 'isCascadeRemove' => $this->isCascadeRemove(), | ||
| 253 | 'isCascadePersist' => $this->isCascadePersist(), | ||
| 254 | 'isCascadeRefresh' => $this->isCascadeRefresh(), | ||
| 255 | 'isCascadeDetach' => $this->isCascadeDetach(), | ||
| 256 | default => property_exists($this, $offset) ? $this->$offset : throw new OutOfRangeException(sprintf( | ||
| 257 | 'Unknown property "%s" on class %s', | ||
| 258 | $offset, | ||
| 259 | static::class, | ||
| 260 | )), | ||
| 261 | }; | ||
| 262 | } | ||
| 263 | |||
| 264 | public function offsetSet(mixed $offset, mixed $value): void | ||
| 265 | { | ||
| 266 | assert($offset !== null); | ||
| 267 | if (! property_exists($this, $offset)) { | ||
| 268 | throw new OutOfRangeException(sprintf( | ||
| 269 | 'Unknown property "%s" on class %s', | ||
| 270 | $offset, | ||
| 271 | static::class, | ||
| 272 | )); | ||
| 273 | } | ||
| 274 | |||
| 275 | if ($offset === 'joinTable') { | ||
| 276 | $value = JoinTableMapping::fromMappingArray($value); | ||
| 277 | } | ||
| 278 | |||
| 279 | $this->$offset = $value; | ||
| 280 | } | ||
| 281 | |||
| 282 | /** @param string $offset */ | ||
| 283 | public function offsetUnset(mixed $offset): void | ||
| 284 | { | ||
| 285 | if (! property_exists($this, $offset)) { | ||
| 286 | throw new OutOfRangeException(sprintf( | ||
| 287 | 'Unknown property "%s" on class %s', | ||
| 288 | $offset, | ||
| 289 | static::class, | ||
| 290 | )); | ||
| 291 | } | ||
| 292 | |||
| 293 | $this->$offset = null; | ||
| 294 | } | ||
| 295 | |||
| 296 | final public function isCascadeRemove(): bool | ||
| 297 | { | ||
| 298 | return in_array('remove', $this->cascade, true); | ||
| 299 | } | ||
| 300 | |||
| 301 | final public function isCascadePersist(): bool | ||
| 302 | { | ||
| 303 | return in_array('persist', $this->cascade, true); | ||
| 304 | } | ||
| 305 | |||
| 306 | final public function isCascadeRefresh(): bool | ||
| 307 | { | ||
| 308 | return in_array('refresh', $this->cascade, true); | ||
| 309 | } | ||
| 310 | |||
| 311 | final public function isCascadeDetach(): bool | ||
| 312 | { | ||
| 313 | return in_array('detach', $this->cascade, true); | ||
| 314 | } | ||
| 315 | |||
| 316 | /** @return array<string, mixed> */ | ||
| 317 | public function toArray(): array | ||
| 318 | { | ||
| 319 | $array = (array) $this; | ||
| 320 | |||
| 321 | $array['isOwningSide'] = $this->isOwningSide(); | ||
| 322 | $array['type'] = $this->type(); | ||
| 323 | |||
| 324 | return $array; | ||
| 325 | } | ||
| 326 | |||
| 327 | /** @return list<string> */ | ||
| 328 | public function __sleep(): array | ||
| 329 | { | ||
| 330 | $serialized = ['fieldName', 'sourceEntity', 'targetEntity']; | ||
| 331 | |||
| 332 | if (count($this->cascade) > 0) { | ||
| 333 | $serialized[] = 'cascade'; | ||
| 334 | } | ||
| 335 | |||
| 336 | foreach ( | ||
| 337 | [ | ||
| 338 | 'fetch', | ||
| 339 | 'inherited', | ||
| 340 | 'declared', | ||
| 341 | 'cache', | ||
| 342 | 'originalClass', | ||
| 343 | 'originalField', | ||
| 344 | ] as $stringOrArrayProperty | ||
| 345 | ) { | ||
| 346 | if ($this->$stringOrArrayProperty !== null) { | ||
| 347 | $serialized[] = $stringOrArrayProperty; | ||
| 348 | } | ||
| 349 | } | ||
| 350 | |||
| 351 | foreach (['id', 'orphanRemoval', 'isOnDeleteCascade', 'unique'] as $boolProperty) { | ||
| 352 | if ($this->$boolProperty) { | ||
| 353 | $serialized[] = $boolProperty; | ||
| 354 | } | ||
| 355 | } | ||
| 356 | |||
| 357 | return $serialized; | ||
| 358 | } | ||
| 359 | } | ||
diff --git a/vendor/doctrine/orm/src/Mapping/AssociationOverride.php b/vendor/doctrine/orm/src/Mapping/AssociationOverride.php new file mode 100644 index 0000000..e0ebc07 --- /dev/null +++ b/vendor/doctrine/orm/src/Mapping/AssociationOverride.php | |||
| @@ -0,0 +1,51 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Mapping; | ||
| 6 | |||
| 7 | /** This attribute is used to override association mapping of property for an entity relationship. */ | ||
| 8 | final class AssociationOverride implements MappingAttribute | ||
| 9 | { | ||
| 10 | /** | ||
| 11 | * The join column that is being mapped to the persistent attribute. | ||
| 12 | * | ||
| 13 | * @var array<JoinColumn>|null | ||
| 14 | */ | ||
| 15 | public readonly array|null $joinColumns; | ||
| 16 | |||
| 17 | /** | ||
| 18 | * The join column that is being mapped to the persistent attribute. | ||
| 19 | * | ||
| 20 | * @var array<JoinColumn>|null | ||
| 21 | */ | ||
| 22 | public readonly array|null $inverseJoinColumns; | ||
| 23 | |||
| 24 | /** | ||
| 25 | * @param string $name The name of the relationship property whose mapping is being overridden. | ||
| 26 | * @param JoinColumn|array<JoinColumn> $joinColumns | ||
| 27 | * @param JoinColumn|array<JoinColumn> $inverseJoinColumns | ||
| 28 | * @param JoinTable|null $joinTable The join table that maps the relationship. | ||
| 29 | * @param string|null $inversedBy The name of the association-field on the inverse-side. | ||
| 30 | * @psalm-param 'LAZY'|'EAGER'|'EXTRA_LAZY'|null $fetch | ||
| 31 | */ | ||
| 32 | public function __construct( | ||
| 33 | public readonly string $name, | ||
| 34 | array|JoinColumn|null $joinColumns = null, | ||
| 35 | array|JoinColumn|null $inverseJoinColumns = null, | ||
| 36 | public readonly JoinTable|null $joinTable = null, | ||
| 37 | public readonly string|null $inversedBy = null, | ||
| 38 | public readonly string|null $fetch = null, | ||
| 39 | ) { | ||
| 40 | if ($joinColumns instanceof JoinColumn) { | ||
| 41 | $joinColumns = [$joinColumns]; | ||
| 42 | } | ||
| 43 | |||
| 44 | if ($inverseJoinColumns instanceof JoinColumn) { | ||
| 45 | $inverseJoinColumns = [$inverseJoinColumns]; | ||
| 46 | } | ||
| 47 | |||
| 48 | $this->joinColumns = $joinColumns; | ||
| 49 | $this->inverseJoinColumns = $inverseJoinColumns; | ||
| 50 | } | ||
| 51 | } | ||
diff --git a/vendor/doctrine/orm/src/Mapping/AssociationOverrides.php b/vendor/doctrine/orm/src/Mapping/AssociationOverrides.php new file mode 100644 index 0000000..9fc6807 --- /dev/null +++ b/vendor/doctrine/orm/src/Mapping/AssociationOverrides.php | |||
| @@ -0,0 +1,38 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Mapping; | ||
| 6 | |||
| 7 | use Attribute; | ||
| 8 | |||
| 9 | use function array_values; | ||
| 10 | use function is_array; | ||
| 11 | |||
| 12 | /** This attribute is used to override association mappings of relationship properties. */ | ||
| 13 | #[Attribute(Attribute::TARGET_CLASS)] | ||
| 14 | final class AssociationOverrides implements MappingAttribute | ||
| 15 | { | ||
| 16 | /** | ||
| 17 | * Mapping overrides of relationship properties. | ||
| 18 | * | ||
| 19 | * @var list<AssociationOverride> | ||
| 20 | */ | ||
| 21 | public readonly array $overrides; | ||
| 22 | |||
| 23 | /** @param array<AssociationOverride>|AssociationOverride $overrides */ | ||
| 24 | public function __construct(array|AssociationOverride $overrides) | ||
| 25 | { | ||
| 26 | if (! is_array($overrides)) { | ||
| 27 | $overrides = [$overrides]; | ||
| 28 | } | ||
| 29 | |||
| 30 | foreach ($overrides as $override) { | ||
| 31 | if (! ($override instanceof AssociationOverride)) { | ||
| 32 | throw MappingException::invalidOverrideType('AssociationOverride', $override); | ||
| 33 | } | ||
| 34 | } | ||
| 35 | |||
| 36 | $this->overrides = array_values($overrides); | ||
| 37 | } | ||
| 38 | } | ||
diff --git a/vendor/doctrine/orm/src/Mapping/AttributeOverride.php b/vendor/doctrine/orm/src/Mapping/AttributeOverride.php new file mode 100644 index 0000000..8f0e70c --- /dev/null +++ b/vendor/doctrine/orm/src/Mapping/AttributeOverride.php | |||
| @@ -0,0 +1,15 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Mapping; | ||
| 6 | |||
| 7 | /** This attribute is used to override the mapping of a entity property. */ | ||
| 8 | final class AttributeOverride implements MappingAttribute | ||
| 9 | { | ||
| 10 | public function __construct( | ||
| 11 | public string $name, | ||
| 12 | public Column $column, | ||
| 13 | ) { | ||
| 14 | } | ||
| 15 | } | ||
diff --git a/vendor/doctrine/orm/src/Mapping/AttributeOverrides.php b/vendor/doctrine/orm/src/Mapping/AttributeOverrides.php new file mode 100644 index 0000000..9c7b9db --- /dev/null +++ b/vendor/doctrine/orm/src/Mapping/AttributeOverrides.php | |||
| @@ -0,0 +1,38 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Mapping; | ||
| 6 | |||
| 7 | use Attribute; | ||
| 8 | |||
| 9 | use function array_values; | ||
| 10 | use function is_array; | ||
| 11 | |||
| 12 | /** This attribute is used to override the mapping of a entity property. */ | ||
| 13 | #[Attribute(Attribute::TARGET_CLASS)] | ||
| 14 | final class AttributeOverrides implements MappingAttribute | ||
| 15 | { | ||
| 16 | /** | ||
| 17 | * One or more field or property mapping overrides. | ||
| 18 | * | ||
| 19 | * @var list<AttributeOverride> | ||
| 20 | */ | ||
| 21 | public readonly array $overrides; | ||
| 22 | |||
| 23 | /** @param array<AttributeOverride>|AttributeOverride $overrides */ | ||
| 24 | public function __construct(array|AttributeOverride $overrides) | ||
| 25 | { | ||
| 26 | if (! is_array($overrides)) { | ||
| 27 | $overrides = [$overrides]; | ||
| 28 | } | ||
| 29 | |||
| 30 | foreach ($overrides as $override) { | ||
| 31 | if (! ($override instanceof AttributeOverride)) { | ||
| 32 | throw MappingException::invalidOverrideType('AttributeOverride', $override); | ||
| 33 | } | ||
| 34 | } | ||
| 35 | |||
| 36 | $this->overrides = array_values($overrides); | ||
| 37 | } | ||
| 38 | } | ||
diff --git a/vendor/doctrine/orm/src/Mapping/Builder/AssociationBuilder.php b/vendor/doctrine/orm/src/Mapping/Builder/AssociationBuilder.php new file mode 100644 index 0000000..ea9e13c --- /dev/null +++ b/vendor/doctrine/orm/src/Mapping/Builder/AssociationBuilder.php | |||
| @@ -0,0 +1,171 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Mapping\Builder; | ||
| 6 | |||
| 7 | use Doctrine\ORM\Mapping\ClassMetadata; | ||
| 8 | use InvalidArgumentException; | ||
| 9 | |||
| 10 | class AssociationBuilder | ||
| 11 | { | ||
| 12 | /** @var mixed[]|null */ | ||
| 13 | protected array|null $joinColumns = null; | ||
| 14 | |||
| 15 | /** @param mixed[] $mapping */ | ||
| 16 | public function __construct( | ||
| 17 | protected readonly ClassMetadataBuilder $builder, | ||
| 18 | protected array $mapping, | ||
| 19 | protected readonly int $type, | ||
| 20 | ) { | ||
| 21 | } | ||
| 22 | |||
| 23 | /** @return $this */ | ||
| 24 | public function mappedBy(string $fieldName): static | ||
| 25 | { | ||
| 26 | $this->mapping['mappedBy'] = $fieldName; | ||
| 27 | |||
| 28 | return $this; | ||
| 29 | } | ||
| 30 | |||
| 31 | /** @return $this */ | ||
| 32 | public function inversedBy(string $fieldName): static | ||
| 33 | { | ||
| 34 | $this->mapping['inversedBy'] = $fieldName; | ||
| 35 | |||
| 36 | return $this; | ||
| 37 | } | ||
| 38 | |||
| 39 | /** @return $this */ | ||
| 40 | public function cascadeAll(): static | ||
| 41 | { | ||
| 42 | $this->mapping['cascade'] = ['ALL']; | ||
| 43 | |||
| 44 | return $this; | ||
| 45 | } | ||
| 46 | |||
| 47 | /** @return $this */ | ||
| 48 | public function cascadePersist(): static | ||
| 49 | { | ||
| 50 | $this->mapping['cascade'][] = 'persist'; | ||
| 51 | |||
| 52 | return $this; | ||
| 53 | } | ||
| 54 | |||
| 55 | /** @return $this */ | ||
| 56 | public function cascadeRemove(): static | ||
| 57 | { | ||
| 58 | $this->mapping['cascade'][] = 'remove'; | ||
| 59 | |||
| 60 | return $this; | ||
| 61 | } | ||
| 62 | |||
| 63 | /** @return $this */ | ||
| 64 | public function cascadeDetach(): static | ||
| 65 | { | ||
| 66 | $this->mapping['cascade'][] = 'detach'; | ||
| 67 | |||
| 68 | return $this; | ||
| 69 | } | ||
| 70 | |||
| 71 | /** @return $this */ | ||
| 72 | public function cascadeRefresh(): static | ||
| 73 | { | ||
| 74 | $this->mapping['cascade'][] = 'refresh'; | ||
| 75 | |||
| 76 | return $this; | ||
| 77 | } | ||
| 78 | |||
| 79 | /** @return $this */ | ||
| 80 | public function fetchExtraLazy(): static | ||
| 81 | { | ||
| 82 | $this->mapping['fetch'] = ClassMetadata::FETCH_EXTRA_LAZY; | ||
| 83 | |||
| 84 | return $this; | ||
| 85 | } | ||
| 86 | |||
| 87 | /** @return $this */ | ||
| 88 | public function fetchEager(): static | ||
| 89 | { | ||
| 90 | $this->mapping['fetch'] = ClassMetadata::FETCH_EAGER; | ||
| 91 | |||
| 92 | return $this; | ||
| 93 | } | ||
| 94 | |||
| 95 | /** @return $this */ | ||
| 96 | public function fetchLazy(): static | ||
| 97 | { | ||
| 98 | $this->mapping['fetch'] = ClassMetadata::FETCH_LAZY; | ||
| 99 | |||
| 100 | return $this; | ||
| 101 | } | ||
| 102 | |||
| 103 | /** | ||
| 104 | * Add Join Columns. | ||
| 105 | * | ||
| 106 | * @return $this | ||
| 107 | */ | ||
| 108 | public function addJoinColumn( | ||
| 109 | string $columnName, | ||
| 110 | string $referencedColumnName, | ||
| 111 | bool $nullable = true, | ||
| 112 | bool $unique = false, | ||
| 113 | string|null $onDelete = null, | ||
| 114 | string|null $columnDef = null, | ||
| 115 | ): static { | ||
| 116 | $this->joinColumns[] = [ | ||
| 117 | 'name' => $columnName, | ||
| 118 | 'referencedColumnName' => $referencedColumnName, | ||
| 119 | 'nullable' => $nullable, | ||
| 120 | 'unique' => $unique, | ||
| 121 | 'onDelete' => $onDelete, | ||
| 122 | 'columnDefinition' => $columnDef, | ||
| 123 | ]; | ||
| 124 | |||
| 125 | return $this; | ||
| 126 | } | ||
| 127 | |||
| 128 | /** | ||
| 129 | * Sets field as primary key. | ||
| 130 | * | ||
| 131 | * @return $this | ||
| 132 | */ | ||
| 133 | public function makePrimaryKey(): static | ||
| 134 | { | ||
| 135 | $this->mapping['id'] = true; | ||
| 136 | |||
| 137 | return $this; | ||
| 138 | } | ||
| 139 | |||
| 140 | /** | ||
| 141 | * Removes orphan entities when detached from their parent. | ||
| 142 | * | ||
| 143 | * @return $this | ||
| 144 | */ | ||
| 145 | public function orphanRemoval(): static | ||
| 146 | { | ||
| 147 | $this->mapping['orphanRemoval'] = true; | ||
| 148 | |||
| 149 | return $this; | ||
| 150 | } | ||
| 151 | |||
| 152 | /** @throws InvalidArgumentException */ | ||
| 153 | public function build(): ClassMetadataBuilder | ||
| 154 | { | ||
| 155 | $mapping = $this->mapping; | ||
| 156 | if ($this->joinColumns) { | ||
| 157 | $mapping['joinColumns'] = $this->joinColumns; | ||
| 158 | } | ||
| 159 | |||
| 160 | $cm = $this->builder->getClassMetadata(); | ||
| 161 | if ($this->type === ClassMetadata::MANY_TO_ONE) { | ||
| 162 | $cm->mapManyToOne($mapping); | ||
| 163 | } elseif ($this->type === ClassMetadata::ONE_TO_ONE) { | ||
| 164 | $cm->mapOneToOne($mapping); | ||
| 165 | } else { | ||
| 166 | throw new InvalidArgumentException('Type should be a ToOne Association here'); | ||
| 167 | } | ||
| 168 | |||
| 169 | return $this->builder; | ||
| 170 | } | ||
| 171 | } | ||
diff --git a/vendor/doctrine/orm/src/Mapping/Builder/ClassMetadataBuilder.php b/vendor/doctrine/orm/src/Mapping/Builder/ClassMetadataBuilder.php new file mode 100644 index 0000000..b9d3cc8 --- /dev/null +++ b/vendor/doctrine/orm/src/Mapping/Builder/ClassMetadataBuilder.php | |||
| @@ -0,0 +1,426 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Mapping\Builder; | ||
| 6 | |||
| 7 | use BackedEnum; | ||
| 8 | use Doctrine\ORM\Mapping\ClassMetadata; | ||
| 9 | |||
| 10 | /** | ||
| 11 | * Builder Object for ClassMetadata | ||
| 12 | * | ||
| 13 | * @link www.doctrine-project.com | ||
| 14 | */ | ||
| 15 | class ClassMetadataBuilder | ||
| 16 | { | ||
| 17 | public function __construct( | ||
| 18 | private readonly ClassMetadata $cm, | ||
| 19 | ) { | ||
| 20 | } | ||
| 21 | |||
| 22 | public function getClassMetadata(): ClassMetadata | ||
| 23 | { | ||
| 24 | return $this->cm; | ||
| 25 | } | ||
| 26 | |||
| 27 | /** | ||
| 28 | * Marks the class as mapped superclass. | ||
| 29 | * | ||
| 30 | * @return $this | ||
| 31 | */ | ||
| 32 | public function setMappedSuperClass(): static | ||
| 33 | { | ||
| 34 | $this->cm->isMappedSuperclass = true; | ||
| 35 | $this->cm->isEmbeddedClass = false; | ||
| 36 | |||
| 37 | return $this; | ||
| 38 | } | ||
| 39 | |||
| 40 | /** | ||
| 41 | * Marks the class as embeddable. | ||
| 42 | * | ||
| 43 | * @return $this | ||
| 44 | */ | ||
| 45 | public function setEmbeddable(): static | ||
| 46 | { | ||
| 47 | $this->cm->isEmbeddedClass = true; | ||
| 48 | $this->cm->isMappedSuperclass = false; | ||
| 49 | |||
| 50 | return $this; | ||
| 51 | } | ||
| 52 | |||
| 53 | /** | ||
| 54 | * Adds and embedded class | ||
| 55 | * | ||
| 56 | * @param class-string $class | ||
| 57 | * | ||
| 58 | * @return $this | ||
| 59 | */ | ||
| 60 | public function addEmbedded(string $fieldName, string $class, string|false|null $columnPrefix = null): static | ||
| 61 | { | ||
| 62 | $this->cm->mapEmbedded( | ||
| 63 | [ | ||
| 64 | 'fieldName' => $fieldName, | ||
| 65 | 'class' => $class, | ||
| 66 | 'columnPrefix' => $columnPrefix, | ||
| 67 | ], | ||
| 68 | ); | ||
| 69 | |||
| 70 | return $this; | ||
| 71 | } | ||
| 72 | |||
| 73 | /** | ||
| 74 | * Sets custom Repository class name. | ||
| 75 | * | ||
| 76 | * @return $this | ||
| 77 | */ | ||
| 78 | public function setCustomRepositoryClass(string $repositoryClassName): static | ||
| 79 | { | ||
| 80 | $this->cm->setCustomRepositoryClass($repositoryClassName); | ||
| 81 | |||
| 82 | return $this; | ||
| 83 | } | ||
| 84 | |||
| 85 | /** | ||
| 86 | * Marks class read only. | ||
| 87 | * | ||
| 88 | * @return $this | ||
| 89 | */ | ||
| 90 | public function setReadOnly(): static | ||
| 91 | { | ||
| 92 | $this->cm->markReadOnly(); | ||
| 93 | |||
| 94 | return $this; | ||
| 95 | } | ||
| 96 | |||
| 97 | /** | ||
| 98 | * Sets the table name. | ||
| 99 | * | ||
| 100 | * @return $this | ||
| 101 | */ | ||
| 102 | public function setTable(string $name): static | ||
| 103 | { | ||
| 104 | $this->cm->setPrimaryTable(['name' => $name]); | ||
| 105 | |||
| 106 | return $this; | ||
| 107 | } | ||
| 108 | |||
| 109 | /** | ||
| 110 | * Adds Index. | ||
| 111 | * | ||
| 112 | * @psalm-param list<string> $columns | ||
| 113 | * | ||
| 114 | * @return $this | ||
| 115 | */ | ||
| 116 | public function addIndex(array $columns, string $name): static | ||
| 117 | { | ||
| 118 | if (! isset($this->cm->table['indexes'])) { | ||
| 119 | $this->cm->table['indexes'] = []; | ||
| 120 | } | ||
| 121 | |||
| 122 | $this->cm->table['indexes'][$name] = ['columns' => $columns]; | ||
| 123 | |||
| 124 | return $this; | ||
| 125 | } | ||
| 126 | |||
| 127 | /** | ||
| 128 | * Adds Unique Constraint. | ||
| 129 | * | ||
| 130 | * @psalm-param list<string> $columns | ||
| 131 | * | ||
| 132 | * @return $this | ||
| 133 | */ | ||
| 134 | public function addUniqueConstraint(array $columns, string $name): static | ||
| 135 | { | ||
| 136 | if (! isset($this->cm->table['uniqueConstraints'])) { | ||
| 137 | $this->cm->table['uniqueConstraints'] = []; | ||
| 138 | } | ||
| 139 | |||
| 140 | $this->cm->table['uniqueConstraints'][$name] = ['columns' => $columns]; | ||
| 141 | |||
| 142 | return $this; | ||
| 143 | } | ||
| 144 | |||
| 145 | /** | ||
| 146 | * Sets class as root of a joined table inheritance hierarchy. | ||
| 147 | * | ||
| 148 | * @return $this | ||
| 149 | */ | ||
| 150 | public function setJoinedTableInheritance(): static | ||
| 151 | { | ||
| 152 | $this->cm->setInheritanceType(ClassMetadata::INHERITANCE_TYPE_JOINED); | ||
| 153 | |||
| 154 | return $this; | ||
| 155 | } | ||
| 156 | |||
| 157 | /** | ||
| 158 | * Sets class as root of a single table inheritance hierarchy. | ||
| 159 | * | ||
| 160 | * @return $this | ||
| 161 | */ | ||
| 162 | public function setSingleTableInheritance(): static | ||
| 163 | { | ||
| 164 | $this->cm->setInheritanceType(ClassMetadata::INHERITANCE_TYPE_SINGLE_TABLE); | ||
| 165 | |||
| 166 | return $this; | ||
| 167 | } | ||
| 168 | |||
| 169 | /** | ||
| 170 | * Sets the discriminator column details. | ||
| 171 | * | ||
| 172 | * @psalm-param class-string<BackedEnum>|null $enumType | ||
| 173 | * @psalm-param array<string, mixed> $options | ||
| 174 | * | ||
| 175 | * @return $this | ||
| 176 | */ | ||
| 177 | public function setDiscriminatorColumn( | ||
| 178 | string $name, | ||
| 179 | string $type = 'string', | ||
| 180 | int $length = 255, | ||
| 181 | string|null $columnDefinition = null, | ||
| 182 | string|null $enumType = null, | ||
| 183 | array $options = [], | ||
| 184 | ): static { | ||
| 185 | $this->cm->setDiscriminatorColumn( | ||
| 186 | [ | ||
| 187 | 'name' => $name, | ||
| 188 | 'type' => $type, | ||
| 189 | 'length' => $length, | ||
| 190 | 'columnDefinition' => $columnDefinition, | ||
| 191 | 'enumType' => $enumType, | ||
| 192 | 'options' => $options, | ||
| 193 | ], | ||
| 194 | ); | ||
| 195 | |||
| 196 | return $this; | ||
| 197 | } | ||
| 198 | |||
| 199 | /** | ||
| 200 | * Adds a subclass to this inheritance hierarchy. | ||
| 201 | * | ||
| 202 | * @return $this | ||
| 203 | */ | ||
| 204 | public function addDiscriminatorMapClass(string $name, string $class): static | ||
| 205 | { | ||
| 206 | $this->cm->addDiscriminatorMapClass($name, $class); | ||
| 207 | |||
| 208 | return $this; | ||
| 209 | } | ||
| 210 | |||
| 211 | /** | ||
| 212 | * Sets deferred explicit change tracking policy. | ||
| 213 | * | ||
| 214 | * @return $this | ||
| 215 | */ | ||
| 216 | public function setChangeTrackingPolicyDeferredExplicit(): static | ||
| 217 | { | ||
| 218 | $this->cm->setChangeTrackingPolicy(ClassMetadata::CHANGETRACKING_DEFERRED_EXPLICIT); | ||
| 219 | |||
| 220 | return $this; | ||
| 221 | } | ||
| 222 | |||
| 223 | /** | ||
| 224 | * Adds lifecycle event. | ||
| 225 | * | ||
| 226 | * @return $this | ||
| 227 | */ | ||
| 228 | public function addLifecycleEvent(string $methodName, string $event): static | ||
| 229 | { | ||
| 230 | $this->cm->addLifecycleCallback($methodName, $event); | ||
| 231 | |||
| 232 | return $this; | ||
| 233 | } | ||
| 234 | |||
| 235 | /** | ||
| 236 | * Adds Field. | ||
| 237 | * | ||
| 238 | * @psalm-param array<string, mixed> $mapping | ||
| 239 | * | ||
| 240 | * @return $this | ||
| 241 | */ | ||
| 242 | public function addField(string $name, string $type, array $mapping = []): static | ||
| 243 | { | ||
| 244 | $mapping['fieldName'] = $name; | ||
| 245 | $mapping['type'] = $type; | ||
| 246 | |||
| 247 | $this->cm->mapField($mapping); | ||
| 248 | |||
| 249 | return $this; | ||
| 250 | } | ||
| 251 | |||
| 252 | /** | ||
| 253 | * Creates a field builder. | ||
| 254 | */ | ||
| 255 | public function createField(string $name, string $type): FieldBuilder | ||
| 256 | { | ||
| 257 | return new FieldBuilder( | ||
| 258 | $this, | ||
| 259 | [ | ||
| 260 | 'fieldName' => $name, | ||
| 261 | 'type' => $type, | ||
| 262 | ], | ||
| 263 | ); | ||
| 264 | } | ||
| 265 | |||
| 266 | /** | ||
| 267 | * Creates an embedded builder. | ||
| 268 | */ | ||
| 269 | public function createEmbedded(string $fieldName, string $class): EmbeddedBuilder | ||
| 270 | { | ||
| 271 | return new EmbeddedBuilder( | ||
| 272 | $this, | ||
| 273 | [ | ||
| 274 | 'fieldName' => $fieldName, | ||
| 275 | 'class' => $class, | ||
| 276 | 'columnPrefix' => null, | ||
| 277 | ], | ||
| 278 | ); | ||
| 279 | } | ||
| 280 | |||
| 281 | /** | ||
| 282 | * Adds a simple many to one association, optionally with the inversed by field. | ||
| 283 | */ | ||
| 284 | public function addManyToOne( | ||
| 285 | string $name, | ||
| 286 | string $targetEntity, | ||
| 287 | string|null $inversedBy = null, | ||
| 288 | ): ClassMetadataBuilder { | ||
| 289 | $builder = $this->createManyToOne($name, $targetEntity); | ||
| 290 | |||
| 291 | if ($inversedBy !== null) { | ||
| 292 | $builder->inversedBy($inversedBy); | ||
| 293 | } | ||
| 294 | |||
| 295 | return $builder->build(); | ||
| 296 | } | ||
| 297 | |||
| 298 | /** | ||
| 299 | * Creates a ManyToOne Association Builder. | ||
| 300 | * | ||
| 301 | * Note: This method does not add the association, you have to call build() on the AssociationBuilder. | ||
| 302 | */ | ||
| 303 | public function createManyToOne(string $name, string $targetEntity): AssociationBuilder | ||
| 304 | { | ||
| 305 | return new AssociationBuilder( | ||
| 306 | $this, | ||
| 307 | [ | ||
| 308 | 'fieldName' => $name, | ||
| 309 | 'targetEntity' => $targetEntity, | ||
| 310 | ], | ||
| 311 | ClassMetadata::MANY_TO_ONE, | ||
| 312 | ); | ||
| 313 | } | ||
| 314 | |||
| 315 | /** | ||
| 316 | * Creates a OneToOne Association Builder. | ||
| 317 | */ | ||
| 318 | public function createOneToOne(string $name, string $targetEntity): AssociationBuilder | ||
| 319 | { | ||
| 320 | return new AssociationBuilder( | ||
| 321 | $this, | ||
| 322 | [ | ||
| 323 | 'fieldName' => $name, | ||
| 324 | 'targetEntity' => $targetEntity, | ||
| 325 | ], | ||
| 326 | ClassMetadata::ONE_TO_ONE, | ||
| 327 | ); | ||
| 328 | } | ||
| 329 | |||
| 330 | /** | ||
| 331 | * Adds simple inverse one-to-one association. | ||
| 332 | */ | ||
| 333 | public function addInverseOneToOne(string $name, string $targetEntity, string $mappedBy): ClassMetadataBuilder | ||
| 334 | { | ||
| 335 | $builder = $this->createOneToOne($name, $targetEntity); | ||
| 336 | $builder->mappedBy($mappedBy); | ||
| 337 | |||
| 338 | return $builder->build(); | ||
| 339 | } | ||
| 340 | |||
| 341 | /** | ||
| 342 | * Adds simple owning one-to-one association. | ||
| 343 | */ | ||
| 344 | public function addOwningOneToOne( | ||
| 345 | string $name, | ||
| 346 | string $targetEntity, | ||
| 347 | string|null $inversedBy = null, | ||
| 348 | ): ClassMetadataBuilder { | ||
| 349 | $builder = $this->createOneToOne($name, $targetEntity); | ||
| 350 | |||
| 351 | if ($inversedBy !== null) { | ||
| 352 | $builder->inversedBy($inversedBy); | ||
| 353 | } | ||
| 354 | |||
| 355 | return $builder->build(); | ||
| 356 | } | ||
| 357 | |||
| 358 | /** | ||
| 359 | * Creates a ManyToMany Association Builder. | ||
| 360 | */ | ||
| 361 | public function createManyToMany(string $name, string $targetEntity): ManyToManyAssociationBuilder | ||
| 362 | { | ||
| 363 | return new ManyToManyAssociationBuilder( | ||
| 364 | $this, | ||
| 365 | [ | ||
| 366 | 'fieldName' => $name, | ||
| 367 | 'targetEntity' => $targetEntity, | ||
| 368 | ], | ||
| 369 | ClassMetadata::MANY_TO_MANY, | ||
| 370 | ); | ||
| 371 | } | ||
| 372 | |||
| 373 | /** | ||
| 374 | * Adds a simple owning many to many association. | ||
| 375 | */ | ||
| 376 | public function addOwningManyToMany( | ||
| 377 | string $name, | ||
| 378 | string $targetEntity, | ||
| 379 | string|null $inversedBy = null, | ||
| 380 | ): ClassMetadataBuilder { | ||
| 381 | $builder = $this->createManyToMany($name, $targetEntity); | ||
| 382 | |||
| 383 | if ($inversedBy !== null) { | ||
| 384 | $builder->inversedBy($inversedBy); | ||
| 385 | } | ||
| 386 | |||
| 387 | return $builder->build(); | ||
| 388 | } | ||
| 389 | |||
| 390 | /** | ||
| 391 | * Adds a simple inverse many to many association. | ||
| 392 | */ | ||
| 393 | public function addInverseManyToMany(string $name, string $targetEntity, string $mappedBy): ClassMetadataBuilder | ||
| 394 | { | ||
| 395 | $builder = $this->createManyToMany($name, $targetEntity); | ||
| 396 | $builder->mappedBy($mappedBy); | ||
| 397 | |||
| 398 | return $builder->build(); | ||
| 399 | } | ||
| 400 | |||
| 401 | /** | ||
| 402 | * Creates a one to many association builder. | ||
| 403 | */ | ||
| 404 | public function createOneToMany(string $name, string $targetEntity): OneToManyAssociationBuilder | ||
| 405 | { | ||
| 406 | return new OneToManyAssociationBuilder( | ||
| 407 | $this, | ||
| 408 | [ | ||
| 409 | 'fieldName' => $name, | ||
| 410 | 'targetEntity' => $targetEntity, | ||
| 411 | ], | ||
| 412 | ClassMetadata::ONE_TO_MANY, | ||
| 413 | ); | ||
| 414 | } | ||
| 415 | |||
| 416 | /** | ||
| 417 | * Adds simple OneToMany association. | ||
| 418 | */ | ||
| 419 | public function addOneToMany(string $name, string $targetEntity, string $mappedBy): ClassMetadataBuilder | ||
| 420 | { | ||
| 421 | $builder = $this->createOneToMany($name, $targetEntity); | ||
| 422 | $builder->mappedBy($mappedBy); | ||
| 423 | |||
| 424 | return $builder->build(); | ||
| 425 | } | ||
| 426 | } | ||
diff --git a/vendor/doctrine/orm/src/Mapping/Builder/EmbeddedBuilder.php b/vendor/doctrine/orm/src/Mapping/Builder/EmbeddedBuilder.php new file mode 100644 index 0000000..b9d2127 --- /dev/null +++ b/vendor/doctrine/orm/src/Mapping/Builder/EmbeddedBuilder.php | |||
| @@ -0,0 +1,46 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Mapping\Builder; | ||
| 6 | |||
| 7 | /** | ||
| 8 | * Embedded Builder | ||
| 9 | * | ||
| 10 | * @link www.doctrine-project.com | ||
| 11 | */ | ||
| 12 | class EmbeddedBuilder | ||
| 13 | { | ||
| 14 | /** @param mixed[] $mapping */ | ||
| 15 | public function __construct( | ||
| 16 | private readonly ClassMetadataBuilder $builder, | ||
| 17 | private array $mapping, | ||
| 18 | ) { | ||
| 19 | } | ||
| 20 | |||
| 21 | /** | ||
| 22 | * Sets the column prefix for all of the embedded columns. | ||
| 23 | * | ||
| 24 | * @return $this | ||
| 25 | */ | ||
| 26 | public function setColumnPrefix(string $columnPrefix): static | ||
| 27 | { | ||
| 28 | $this->mapping['columnPrefix'] = $columnPrefix; | ||
| 29 | |||
| 30 | return $this; | ||
| 31 | } | ||
| 32 | |||
| 33 | /** | ||
| 34 | * Finalizes this embeddable and attach it to the ClassMetadata. | ||
| 35 | * | ||
| 36 | * Without this call an EmbeddedBuilder has no effect on the ClassMetadata. | ||
| 37 | */ | ||
| 38 | public function build(): ClassMetadataBuilder | ||
| 39 | { | ||
| 40 | $cm = $this->builder->getClassMetadata(); | ||
| 41 | |||
| 42 | $cm->mapEmbedded($this->mapping); | ||
| 43 | |||
| 44 | return $this->builder; | ||
| 45 | } | ||
| 46 | } | ||
diff --git a/vendor/doctrine/orm/src/Mapping/Builder/EntityListenerBuilder.php b/vendor/doctrine/orm/src/Mapping/Builder/EntityListenerBuilder.php new file mode 100644 index 0000000..a0b14b9 --- /dev/null +++ b/vendor/doctrine/orm/src/Mapping/Builder/EntityListenerBuilder.php | |||
| @@ -0,0 +1,55 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Mapping\Builder; | ||
| 6 | |||
| 7 | use Doctrine\ORM\Events; | ||
| 8 | use Doctrine\ORM\Mapping\ClassMetadata; | ||
| 9 | use Doctrine\ORM\Mapping\MappingException; | ||
| 10 | |||
| 11 | use function class_exists; | ||
| 12 | use function get_class_methods; | ||
| 13 | |||
| 14 | /** | ||
| 15 | * Builder for entity listeners. | ||
| 16 | */ | ||
| 17 | class EntityListenerBuilder | ||
| 18 | { | ||
| 19 | /** Hash-map to handle event names. */ | ||
| 20 | private const EVENTS = [ | ||
| 21 | Events::preRemove => true, | ||
| 22 | Events::postRemove => true, | ||
| 23 | Events::prePersist => true, | ||
| 24 | Events::postPersist => true, | ||
| 25 | Events::preUpdate => true, | ||
| 26 | Events::postUpdate => true, | ||
| 27 | Events::postLoad => true, | ||
| 28 | Events::preFlush => true, | ||
| 29 | ]; | ||
| 30 | |||
| 31 | /** | ||
| 32 | * Lookup the entity class to find methods that match to event lifecycle names | ||
| 33 | * | ||
| 34 | * @param ClassMetadata $metadata The entity metadata. | ||
| 35 | * @param string $className The listener class name. | ||
| 36 | * | ||
| 37 | * @throws MappingException When the listener class not found. | ||
| 38 | */ | ||
| 39 | public static function bindEntityListener(ClassMetadata $metadata, string $className): void | ||
| 40 | { | ||
| 41 | $class = $metadata->fullyQualifiedClassName($className); | ||
| 42 | |||
| 43 | if (! class_exists($class)) { | ||
| 44 | throw MappingException::entityListenerClassNotFound($class, $className); | ||
| 45 | } | ||
| 46 | |||
| 47 | foreach (get_class_methods($class) as $method) { | ||
| 48 | if (! isset(self::EVENTS[$method])) { | ||
| 49 | continue; | ||
| 50 | } | ||
| 51 | |||
| 52 | $metadata->addEntityListener($method, $class, $method); | ||
| 53 | } | ||
| 54 | } | ||
| 55 | } | ||
diff --git a/vendor/doctrine/orm/src/Mapping/Builder/FieldBuilder.php b/vendor/doctrine/orm/src/Mapping/Builder/FieldBuilder.php new file mode 100644 index 0000000..8326ff5 --- /dev/null +++ b/vendor/doctrine/orm/src/Mapping/Builder/FieldBuilder.php | |||
| @@ -0,0 +1,243 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Mapping\Builder; | ||
| 6 | |||
| 7 | use function constant; | ||
| 8 | |||
| 9 | /** | ||
| 10 | * Field Builder | ||
| 11 | * | ||
| 12 | * @link www.doctrine-project.com | ||
| 13 | */ | ||
| 14 | class FieldBuilder | ||
| 15 | { | ||
| 16 | private bool $version = false; | ||
| 17 | private string|null $generatedValue = null; | ||
| 18 | |||
| 19 | /** @var mixed[]|null */ | ||
| 20 | private array|null $sequenceDef = null; | ||
| 21 | |||
| 22 | private string|null $customIdGenerator = null; | ||
| 23 | |||
| 24 | /** @param mixed[] $mapping */ | ||
| 25 | public function __construct( | ||
| 26 | private readonly ClassMetadataBuilder $builder, | ||
| 27 | private array $mapping, | ||
| 28 | ) { | ||
| 29 | } | ||
| 30 | |||
| 31 | /** | ||
| 32 | * Sets length. | ||
| 33 | * | ||
| 34 | * @return $this | ||
| 35 | */ | ||
| 36 | public function length(int $length): static | ||
| 37 | { | ||
| 38 | $this->mapping['length'] = $length; | ||
| 39 | |||
| 40 | return $this; | ||
| 41 | } | ||
| 42 | |||
| 43 | /** | ||
| 44 | * Sets nullable. | ||
| 45 | * | ||
| 46 | * @return $this | ||
| 47 | */ | ||
| 48 | public function nullable(bool $flag = true): static | ||
| 49 | { | ||
| 50 | $this->mapping['nullable'] = $flag; | ||
| 51 | |||
| 52 | return $this; | ||
| 53 | } | ||
| 54 | |||
| 55 | /** | ||
| 56 | * Sets Unique. | ||
| 57 | * | ||
| 58 | * @return $this | ||
| 59 | */ | ||
| 60 | public function unique(bool $flag = true): static | ||
| 61 | { | ||
| 62 | $this->mapping['unique'] = $flag; | ||
| 63 | |||
| 64 | return $this; | ||
| 65 | } | ||
| 66 | |||
| 67 | /** | ||
| 68 | * Sets column name. | ||
| 69 | * | ||
| 70 | * @return $this | ||
| 71 | */ | ||
| 72 | public function columnName(string $name): static | ||
| 73 | { | ||
| 74 | $this->mapping['columnName'] = $name; | ||
| 75 | |||
| 76 | return $this; | ||
| 77 | } | ||
| 78 | |||
| 79 | /** | ||
| 80 | * Sets Precision. | ||
| 81 | * | ||
| 82 | * @return $this | ||
| 83 | */ | ||
| 84 | public function precision(int $p): static | ||
| 85 | { | ||
| 86 | $this->mapping['precision'] = $p; | ||
| 87 | |||
| 88 | return $this; | ||
| 89 | } | ||
| 90 | |||
| 91 | /** | ||
| 92 | * Sets insertable. | ||
| 93 | * | ||
| 94 | * @return $this | ||
| 95 | */ | ||
| 96 | public function insertable(bool $flag = true): self | ||
| 97 | { | ||
| 98 | if (! $flag) { | ||
| 99 | $this->mapping['notInsertable'] = true; | ||
| 100 | } | ||
| 101 | |||
| 102 | return $this; | ||
| 103 | } | ||
| 104 | |||
| 105 | /** | ||
| 106 | * Sets updatable. | ||
| 107 | * | ||
| 108 | * @return $this | ||
| 109 | */ | ||
| 110 | public function updatable(bool $flag = true): self | ||
| 111 | { | ||
| 112 | if (! $flag) { | ||
| 113 | $this->mapping['notUpdatable'] = true; | ||
| 114 | } | ||
| 115 | |||
| 116 | return $this; | ||
| 117 | } | ||
| 118 | |||
| 119 | /** | ||
| 120 | * Sets scale. | ||
| 121 | * | ||
| 122 | * @return $this | ||
| 123 | */ | ||
| 124 | public function scale(int $s): static | ||
| 125 | { | ||
| 126 | $this->mapping['scale'] = $s; | ||
| 127 | |||
| 128 | return $this; | ||
| 129 | } | ||
| 130 | |||
| 131 | /** | ||
| 132 | * Sets field as primary key. | ||
| 133 | * | ||
| 134 | * @return $this | ||
| 135 | */ | ||
| 136 | public function makePrimaryKey(): static | ||
| 137 | { | ||
| 138 | $this->mapping['id'] = true; | ||
| 139 | |||
| 140 | return $this; | ||
| 141 | } | ||
| 142 | |||
| 143 | /** | ||
| 144 | * Sets an option. | ||
| 145 | * | ||
| 146 | * @return $this | ||
| 147 | */ | ||
| 148 | public function option(string $name, mixed $value): static | ||
| 149 | { | ||
| 150 | $this->mapping['options'][$name] = $value; | ||
| 151 | |||
| 152 | return $this; | ||
| 153 | } | ||
| 154 | |||
| 155 | /** @return $this */ | ||
| 156 | public function generatedValue(string $strategy = 'AUTO'): static | ||
| 157 | { | ||
| 158 | $this->generatedValue = $strategy; | ||
| 159 | |||
| 160 | return $this; | ||
| 161 | } | ||
| 162 | |||
| 163 | /** | ||
| 164 | * Sets field versioned. | ||
| 165 | * | ||
| 166 | * @return $this | ||
| 167 | */ | ||
| 168 | public function isVersionField(): static | ||
| 169 | { | ||
| 170 | $this->version = true; | ||
| 171 | |||
| 172 | return $this; | ||
| 173 | } | ||
| 174 | |||
| 175 | /** | ||
| 176 | * Sets Sequence Generator. | ||
| 177 | * | ||
| 178 | * @return $this | ||
| 179 | */ | ||
| 180 | public function setSequenceGenerator(string $sequenceName, int $allocationSize = 1, int $initialValue = 1): static | ||
| 181 | { | ||
| 182 | $this->sequenceDef = [ | ||
| 183 | 'sequenceName' => $sequenceName, | ||
| 184 | 'allocationSize' => $allocationSize, | ||
| 185 | 'initialValue' => $initialValue, | ||
| 186 | ]; | ||
| 187 | |||
| 188 | return $this; | ||
| 189 | } | ||
| 190 | |||
| 191 | /** | ||
| 192 | * Sets column definition. | ||
| 193 | * | ||
| 194 | * @return $this | ||
| 195 | */ | ||
| 196 | public function columnDefinition(string $def): static | ||
| 197 | { | ||
| 198 | $this->mapping['columnDefinition'] = $def; | ||
| 199 | |||
| 200 | return $this; | ||
| 201 | } | ||
| 202 | |||
| 203 | /** | ||
| 204 | * Set the FQCN of the custom ID generator. | ||
| 205 | * This class must extend \Doctrine\ORM\Id\AbstractIdGenerator. | ||
| 206 | * | ||
| 207 | * @return $this | ||
| 208 | */ | ||
| 209 | public function setCustomIdGenerator(string $customIdGenerator): static | ||
| 210 | { | ||
| 211 | $this->customIdGenerator = $customIdGenerator; | ||
| 212 | |||
| 213 | return $this; | ||
| 214 | } | ||
| 215 | |||
| 216 | /** | ||
| 217 | * Finalizes this field and attach it to the ClassMetadata. | ||
| 218 | * | ||
| 219 | * Without this call a FieldBuilder has no effect on the ClassMetadata. | ||
| 220 | */ | ||
| 221 | public function build(): ClassMetadataBuilder | ||
| 222 | { | ||
| 223 | $cm = $this->builder->getClassMetadata(); | ||
| 224 | if ($this->generatedValue) { | ||
| 225 | $cm->setIdGeneratorType(constant('Doctrine\ORM\Mapping\ClassMetadata::GENERATOR_TYPE_' . $this->generatedValue)); | ||
| 226 | } | ||
| 227 | |||
| 228 | if ($this->version) { | ||
| 229 | $cm->setVersionMapping($this->mapping); | ||
| 230 | } | ||
| 231 | |||
| 232 | $cm->mapField($this->mapping); | ||
| 233 | if ($this->sequenceDef) { | ||
| 234 | $cm->setSequenceGeneratorDefinition($this->sequenceDef); | ||
| 235 | } | ||
| 236 | |||
| 237 | if ($this->customIdGenerator) { | ||
| 238 | $cm->setCustomGeneratorDefinition(['class' => $this->customIdGenerator]); | ||
| 239 | } | ||
| 240 | |||
| 241 | return $this->builder; | ||
| 242 | } | ||
| 243 | } | ||
diff --git a/vendor/doctrine/orm/src/Mapping/Builder/ManyToManyAssociationBuilder.php b/vendor/doctrine/orm/src/Mapping/Builder/ManyToManyAssociationBuilder.php new file mode 100644 index 0000000..b83a8ba --- /dev/null +++ b/vendor/doctrine/orm/src/Mapping/Builder/ManyToManyAssociationBuilder.php | |||
| @@ -0,0 +1,73 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Mapping\Builder; | ||
| 6 | |||
| 7 | /** | ||
| 8 | * ManyToMany Association Builder | ||
| 9 | * | ||
| 10 | * @link www.doctrine-project.com | ||
| 11 | */ | ||
| 12 | class ManyToManyAssociationBuilder extends OneToManyAssociationBuilder | ||
| 13 | { | ||
| 14 | private string|null $joinTableName = null; | ||
| 15 | |||
| 16 | /** @var mixed[] */ | ||
| 17 | private array $inverseJoinColumns = []; | ||
| 18 | |||
| 19 | /** @return $this */ | ||
| 20 | public function setJoinTable(string $name): static | ||
| 21 | { | ||
| 22 | $this->joinTableName = $name; | ||
| 23 | |||
| 24 | return $this; | ||
| 25 | } | ||
| 26 | |||
| 27 | /** | ||
| 28 | * Adds Inverse Join Columns. | ||
| 29 | * | ||
| 30 | * @return $this | ||
| 31 | */ | ||
| 32 | public function addInverseJoinColumn( | ||
| 33 | string $columnName, | ||
| 34 | string $referencedColumnName, | ||
| 35 | bool $nullable = true, | ||
| 36 | bool $unique = false, | ||
| 37 | string|null $onDelete = null, | ||
| 38 | string|null $columnDef = null, | ||
| 39 | ): static { | ||
| 40 | $this->inverseJoinColumns[] = [ | ||
| 41 | 'name' => $columnName, | ||
| 42 | 'referencedColumnName' => $referencedColumnName, | ||
| 43 | 'nullable' => $nullable, | ||
| 44 | 'unique' => $unique, | ||
| 45 | 'onDelete' => $onDelete, | ||
| 46 | 'columnDefinition' => $columnDef, | ||
| 47 | ]; | ||
| 48 | |||
| 49 | return $this; | ||
| 50 | } | ||
| 51 | |||
| 52 | public function build(): ClassMetadataBuilder | ||
| 53 | { | ||
| 54 | $mapping = $this->mapping; | ||
| 55 | $mapping['joinTable'] = []; | ||
| 56 | if ($this->joinColumns) { | ||
| 57 | $mapping['joinTable']['joinColumns'] = $this->joinColumns; | ||
| 58 | } | ||
| 59 | |||
| 60 | if ($this->inverseJoinColumns) { | ||
| 61 | $mapping['joinTable']['inverseJoinColumns'] = $this->inverseJoinColumns; | ||
| 62 | } | ||
| 63 | |||
| 64 | if ($this->joinTableName) { | ||
| 65 | $mapping['joinTable']['name'] = $this->joinTableName; | ||
| 66 | } | ||
| 67 | |||
| 68 | $cm = $this->builder->getClassMetadata(); | ||
| 69 | $cm->mapManyToMany($mapping); | ||
| 70 | |||
| 71 | return $this->builder; | ||
| 72 | } | ||
| 73 | } | ||
diff --git a/vendor/doctrine/orm/src/Mapping/Builder/OneToManyAssociationBuilder.php b/vendor/doctrine/orm/src/Mapping/Builder/OneToManyAssociationBuilder.php new file mode 100644 index 0000000..077c558 --- /dev/null +++ b/vendor/doctrine/orm/src/Mapping/Builder/OneToManyAssociationBuilder.php | |||
| @@ -0,0 +1,46 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Mapping\Builder; | ||
| 6 | |||
| 7 | /** | ||
| 8 | * OneToMany Association Builder | ||
| 9 | * | ||
| 10 | * @link www.doctrine-project.com | ||
| 11 | */ | ||
| 12 | class OneToManyAssociationBuilder extends AssociationBuilder | ||
| 13 | { | ||
| 14 | /** | ||
| 15 | * @psalm-param array<string, string> $fieldNames | ||
| 16 | * | ||
| 17 | * @return $this | ||
| 18 | */ | ||
| 19 | public function setOrderBy(array $fieldNames): static | ||
| 20 | { | ||
| 21 | $this->mapping['orderBy'] = $fieldNames; | ||
| 22 | |||
| 23 | return $this; | ||
| 24 | } | ||
| 25 | |||
| 26 | /** @return $this */ | ||
| 27 | public function setIndexBy(string $fieldName): static | ||
| 28 | { | ||
| 29 | $this->mapping['indexBy'] = $fieldName; | ||
| 30 | |||
| 31 | return $this; | ||
| 32 | } | ||
| 33 | |||
| 34 | public function build(): ClassMetadataBuilder | ||
| 35 | { | ||
| 36 | $mapping = $this->mapping; | ||
| 37 | if ($this->joinColumns) { | ||
| 38 | $mapping['joinColumns'] = $this->joinColumns; | ||
| 39 | } | ||
| 40 | |||
| 41 | $cm = $this->builder->getClassMetadata(); | ||
| 42 | $cm->mapOneToMany($mapping); | ||
| 43 | |||
| 44 | return $this->builder; | ||
| 45 | } | ||
| 46 | } | ||
diff --git a/vendor/doctrine/orm/src/Mapping/Cache.php b/vendor/doctrine/orm/src/Mapping/Cache.php new file mode 100644 index 0000000..3161ab3 --- /dev/null +++ b/vendor/doctrine/orm/src/Mapping/Cache.php | |||
| @@ -0,0 +1,19 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Mapping; | ||
| 6 | |||
| 7 | use Attribute; | ||
| 8 | |||
| 9 | /** Caching to an entity or a collection. */ | ||
| 10 | #[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_PROPERTY)] | ||
| 11 | final class Cache implements MappingAttribute | ||
| 12 | { | ||
| 13 | /** @psalm-param 'READ_ONLY'|'NONSTRICT_READ_WRITE'|'READ_WRITE' $usage */ | ||
| 14 | public function __construct( | ||
| 15 | public readonly string $usage = 'READ_ONLY', | ||
| 16 | public readonly string|null $region = null, | ||
| 17 | ) { | ||
| 18 | } | ||
| 19 | } | ||
diff --git a/vendor/doctrine/orm/src/Mapping/ChainTypedFieldMapper.php b/vendor/doctrine/orm/src/Mapping/ChainTypedFieldMapper.php new file mode 100644 index 0000000..ed1ba93 --- /dev/null +++ b/vendor/doctrine/orm/src/Mapping/ChainTypedFieldMapper.php | |||
| @@ -0,0 +1,35 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Mapping; | ||
| 6 | |||
| 7 | use Doctrine\ORM\Internal\NoUnknownNamedArguments; | ||
| 8 | use ReflectionProperty; | ||
| 9 | |||
| 10 | final class ChainTypedFieldMapper implements TypedFieldMapper | ||
| 11 | { | ||
| 12 | use NoUnknownNamedArguments; | ||
| 13 | |||
| 14 | /** @var list<TypedFieldMapper> $typedFieldMappers */ | ||
| 15 | private readonly array $typedFieldMappers; | ||
| 16 | |||
| 17 | public function __construct(TypedFieldMapper ...$typedFieldMappers) | ||
| 18 | { | ||
| 19 | self::validateVariadicParameter($typedFieldMappers); | ||
| 20 | |||
| 21 | $this->typedFieldMappers = $typedFieldMappers; | ||
| 22 | } | ||
| 23 | |||
| 24 | /** | ||
| 25 | * {@inheritDoc} | ||
| 26 | */ | ||
| 27 | public function validateAndComplete(array $mapping, ReflectionProperty $field): array | ||
| 28 | { | ||
| 29 | foreach ($this->typedFieldMappers as $typedFieldMapper) { | ||
| 30 | $mapping = $typedFieldMapper->validateAndComplete($mapping, $field); | ||
| 31 | } | ||
| 32 | |||
| 33 | return $mapping; | ||
| 34 | } | ||
| 35 | } | ||
diff --git a/vendor/doctrine/orm/src/Mapping/ChangeTrackingPolicy.php b/vendor/doctrine/orm/src/Mapping/ChangeTrackingPolicy.php new file mode 100644 index 0000000..7181d9f --- /dev/null +++ b/vendor/doctrine/orm/src/Mapping/ChangeTrackingPolicy.php | |||
| @@ -0,0 +1,17 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Mapping; | ||
| 6 | |||
| 7 | use Attribute; | ||
| 8 | |||
| 9 | #[Attribute(Attribute::TARGET_CLASS)] | ||
| 10 | final class ChangeTrackingPolicy implements MappingAttribute | ||
| 11 | { | ||
| 12 | /** @psalm-param 'DEFERRED_IMPLICIT'|'DEFERRED_EXPLICIT' $value */ | ||
| 13 | public function __construct( | ||
| 14 | public readonly string $value, | ||
| 15 | ) { | ||
| 16 | } | ||
| 17 | } | ||
diff --git a/vendor/doctrine/orm/src/Mapping/ClassMetadata.php b/vendor/doctrine/orm/src/Mapping/ClassMetadata.php new file mode 100644 index 0000000..f58e00e --- /dev/null +++ b/vendor/doctrine/orm/src/Mapping/ClassMetadata.php | |||
| @@ -0,0 +1,2649 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Mapping; | ||
| 6 | |||
| 7 | use BackedEnum; | ||
| 8 | use BadMethodCallException; | ||
| 9 | use Doctrine\DBAL\Platforms\AbstractPlatform; | ||
| 10 | use Doctrine\Deprecations\Deprecation; | ||
| 11 | use Doctrine\Instantiator\Instantiator; | ||
| 12 | use Doctrine\Instantiator\InstantiatorInterface; | ||
| 13 | use Doctrine\ORM\Cache\Exception\NonCacheableEntityAssociation; | ||
| 14 | use Doctrine\ORM\EntityRepository; | ||
| 15 | use Doctrine\ORM\Id\AbstractIdGenerator; | ||
| 16 | use Doctrine\Persistence\Mapping\ClassMetadata as PersistenceClassMetadata; | ||
| 17 | use Doctrine\Persistence\Mapping\ReflectionService; | ||
| 18 | use Doctrine\Persistence\Reflection\EnumReflectionProperty; | ||
| 19 | use InvalidArgumentException; | ||
| 20 | use LogicException; | ||
| 21 | use ReflectionClass; | ||
| 22 | use ReflectionNamedType; | ||
| 23 | use ReflectionProperty; | ||
| 24 | use Stringable; | ||
| 25 | |||
| 26 | use function array_diff; | ||
| 27 | use function array_intersect; | ||
| 28 | use function array_key_exists; | ||
| 29 | use function array_keys; | ||
| 30 | use function array_map; | ||
| 31 | use function array_merge; | ||
| 32 | use function array_pop; | ||
| 33 | use function array_values; | ||
| 34 | use function assert; | ||
| 35 | use function class_exists; | ||
| 36 | use function count; | ||
| 37 | use function enum_exists; | ||
| 38 | use function explode; | ||
| 39 | use function in_array; | ||
| 40 | use function interface_exists; | ||
| 41 | use function is_string; | ||
| 42 | use function is_subclass_of; | ||
| 43 | use function ltrim; | ||
| 44 | use function method_exists; | ||
| 45 | use function spl_object_id; | ||
| 46 | use function sprintf; | ||
| 47 | use function str_contains; | ||
| 48 | use function str_replace; | ||
| 49 | use function strtolower; | ||
| 50 | use function trait_exists; | ||
| 51 | use function trim; | ||
| 52 | |||
| 53 | /** | ||
| 54 | * A <tt>ClassMetadata</tt> instance holds all the object-relational mapping metadata | ||
| 55 | * of an entity and its associations. | ||
| 56 | * | ||
| 57 | * Once populated, ClassMetadata instances are usually cached in a serialized form. | ||
| 58 | * | ||
| 59 | * <b>IMPORTANT NOTE:</b> | ||
| 60 | * | ||
| 61 | * The fields of this class are only public for 2 reasons: | ||
| 62 | * 1) To allow fast READ access. | ||
| 63 | * 2) To drastically reduce the size of a serialized instance (private/protected members | ||
| 64 | * get the whole class name, namespace inclusive, prepended to every property in | ||
| 65 | * the serialized representation). | ||
| 66 | * | ||
| 67 | * @psalm-type ConcreteAssociationMapping = OneToOneOwningSideMapping|OneToOneInverseSideMapping|ManyToOneAssociationMapping|OneToManyAssociationMapping|ManyToManyOwningSideMapping|ManyToManyInverseSideMapping | ||
| 68 | * @template-covariant T of object | ||
| 69 | * @template-implements PersistenceClassMetadata<T> | ||
| 70 | */ | ||
| 71 | class ClassMetadata implements PersistenceClassMetadata, Stringable | ||
| 72 | { | ||
| 73 | /* The inheritance mapping types */ | ||
| 74 | /** | ||
| 75 | * NONE means the class does not participate in an inheritance hierarchy | ||
| 76 | * and therefore does not need an inheritance mapping type. | ||
| 77 | */ | ||
| 78 | public const INHERITANCE_TYPE_NONE = 1; | ||
| 79 | |||
| 80 | /** | ||
| 81 | * JOINED means the class will be persisted according to the rules of | ||
| 82 | * <tt>Class Table Inheritance</tt>. | ||
| 83 | */ | ||
| 84 | public const INHERITANCE_TYPE_JOINED = 2; | ||
| 85 | |||
| 86 | /** | ||
| 87 | * SINGLE_TABLE means the class will be persisted according to the rules of | ||
| 88 | * <tt>Single Table Inheritance</tt>. | ||
| 89 | */ | ||
| 90 | public const INHERITANCE_TYPE_SINGLE_TABLE = 3; | ||
| 91 | |||
| 92 | /* The Id generator types. */ | ||
| 93 | /** | ||
| 94 | * AUTO means the generator type will depend on what the used platform prefers. | ||
| 95 | * Offers full portability. | ||
| 96 | */ | ||
| 97 | public const GENERATOR_TYPE_AUTO = 1; | ||
| 98 | |||
| 99 | /** | ||
| 100 | * SEQUENCE means a separate sequence object will be used. Platforms that do | ||
| 101 | * not have native sequence support may emulate it. Full portability is currently | ||
| 102 | * not guaranteed. | ||
| 103 | */ | ||
| 104 | public const GENERATOR_TYPE_SEQUENCE = 2; | ||
| 105 | |||
| 106 | /** | ||
| 107 | * IDENTITY means an identity column is used for id generation. The database | ||
| 108 | * will fill in the id column on insertion. Platforms that do not support | ||
| 109 | * native identity columns may emulate them. Full portability is currently | ||
| 110 | * not guaranteed. | ||
| 111 | */ | ||
| 112 | public const GENERATOR_TYPE_IDENTITY = 4; | ||
| 113 | |||
| 114 | /** | ||
| 115 | * NONE means the class does not have a generated id. That means the class | ||
| 116 | * must have a natural, manually assigned id. | ||
| 117 | */ | ||
| 118 | public const GENERATOR_TYPE_NONE = 5; | ||
| 119 | |||
| 120 | /** | ||
| 121 | * CUSTOM means that customer will use own ID generator that supposedly work | ||
| 122 | */ | ||
| 123 | public const GENERATOR_TYPE_CUSTOM = 7; | ||
| 124 | |||
| 125 | /** | ||
| 126 | * DEFERRED_IMPLICIT means that changes of entities are calculated at commit-time | ||
| 127 | * by doing a property-by-property comparison with the original data. This will | ||
| 128 | * be done for all entities that are in MANAGED state at commit-time. | ||
| 129 | * | ||
| 130 | * This is the default change tracking policy. | ||
| 131 | */ | ||
| 132 | public const CHANGETRACKING_DEFERRED_IMPLICIT = 1; | ||
| 133 | |||
| 134 | /** | ||
| 135 | * DEFERRED_EXPLICIT means that changes of entities are calculated at commit-time | ||
| 136 | * by doing a property-by-property comparison with the original data. This will | ||
| 137 | * be done only for entities that were explicitly saved (through persist() or a cascade). | ||
| 138 | */ | ||
| 139 | public const CHANGETRACKING_DEFERRED_EXPLICIT = 2; | ||
| 140 | |||
| 141 | /** | ||
| 142 | * Specifies that an association is to be fetched when it is first accessed. | ||
| 143 | */ | ||
| 144 | public const FETCH_LAZY = 2; | ||
| 145 | |||
| 146 | /** | ||
| 147 | * Specifies that an association is to be fetched when the owner of the | ||
| 148 | * association is fetched. | ||
| 149 | */ | ||
| 150 | public const FETCH_EAGER = 3; | ||
| 151 | |||
| 152 | /** | ||
| 153 | * Specifies that an association is to be fetched lazy (on first access) and that | ||
| 154 | * commands such as Collection#count, Collection#slice are issued directly against | ||
| 155 | * the database if the collection is not yet initialized. | ||
| 156 | */ | ||
| 157 | public const FETCH_EXTRA_LAZY = 4; | ||
| 158 | |||
| 159 | /** | ||
| 160 | * Identifies a one-to-one association. | ||
| 161 | */ | ||
| 162 | public const ONE_TO_ONE = 1; | ||
| 163 | |||
| 164 | /** | ||
| 165 | * Identifies a many-to-one association. | ||
| 166 | */ | ||
| 167 | public const MANY_TO_ONE = 2; | ||
| 168 | |||
| 169 | /** | ||
| 170 | * Identifies a one-to-many association. | ||
| 171 | */ | ||
| 172 | public const ONE_TO_MANY = 4; | ||
| 173 | |||
| 174 | /** | ||
| 175 | * Identifies a many-to-many association. | ||
| 176 | */ | ||
| 177 | public const MANY_TO_MANY = 8; | ||
| 178 | |||
| 179 | /** | ||
| 180 | * Combined bitmask for to-one (single-valued) associations. | ||
| 181 | */ | ||
| 182 | public const TO_ONE = 3; | ||
| 183 | |||
| 184 | /** | ||
| 185 | * Combined bitmask for to-many (collection-valued) associations. | ||
| 186 | */ | ||
| 187 | public const TO_MANY = 12; | ||
| 188 | |||
| 189 | /** | ||
| 190 | * ReadOnly cache can do reads, inserts and deletes, cannot perform updates or employ any locks, | ||
| 191 | */ | ||
| 192 | public const CACHE_USAGE_READ_ONLY = 1; | ||
| 193 | |||
| 194 | /** | ||
| 195 | * Nonstrict Read Write Cache doesnāt employ any locks but can do inserts, update and deletes. | ||
| 196 | */ | ||
| 197 | public const CACHE_USAGE_NONSTRICT_READ_WRITE = 2; | ||
| 198 | |||
| 199 | /** | ||
| 200 | * Read Write Attempts to lock the entity before update/delete. | ||
| 201 | */ | ||
| 202 | public const CACHE_USAGE_READ_WRITE = 3; | ||
| 203 | |||
| 204 | /** | ||
| 205 | * The value of this column is never generated by the database. | ||
| 206 | */ | ||
| 207 | public const GENERATED_NEVER = 0; | ||
| 208 | |||
| 209 | /** | ||
| 210 | * The value of this column is generated by the database on INSERT, but not on UPDATE. | ||
| 211 | */ | ||
| 212 | public const GENERATED_INSERT = 1; | ||
| 213 | |||
| 214 | /** | ||
| 215 | * The value of this column is generated by the database on both INSERT and UDPATE statements. | ||
| 216 | */ | ||
| 217 | public const GENERATED_ALWAYS = 2; | ||
| 218 | |||
| 219 | /** | ||
| 220 | * READ-ONLY: The namespace the entity class is contained in. | ||
| 221 | * | ||
| 222 | * @todo Not really needed. Usage could be localized. | ||
| 223 | */ | ||
| 224 | public string|null $namespace = null; | ||
| 225 | |||
| 226 | /** | ||
| 227 | * READ-ONLY: The name of the entity class that is at the root of the mapped entity inheritance | ||
| 228 | * hierarchy. If the entity is not part of a mapped inheritance hierarchy this is the same | ||
| 229 | * as {@link $name}. | ||
| 230 | * | ||
| 231 | * @psalm-var class-string | ||
| 232 | */ | ||
| 233 | public string $rootEntityName; | ||
| 234 | |||
| 235 | /** | ||
| 236 | * READ-ONLY: The definition of custom generator. Only used for CUSTOM | ||
| 237 | * generator type | ||
| 238 | * | ||
| 239 | * The definition has the following structure: | ||
| 240 | * <code> | ||
| 241 | * array( | ||
| 242 | * 'class' => 'ClassName', | ||
| 243 | * ) | ||
| 244 | * </code> | ||
| 245 | * | ||
| 246 | * @todo Merge with tableGeneratorDefinition into generic generatorDefinition | ||
| 247 | * @var array<string, string>|null | ||
| 248 | */ | ||
| 249 | public array|null $customGeneratorDefinition = null; | ||
| 250 | |||
| 251 | /** | ||
| 252 | * The name of the custom repository class used for the entity class. | ||
| 253 | * (Optional). | ||
| 254 | * | ||
| 255 | * @psalm-var ?class-string<EntityRepository> | ||
| 256 | */ | ||
| 257 | public string|null $customRepositoryClassName = null; | ||
| 258 | |||
| 259 | /** | ||
| 260 | * READ-ONLY: Whether this class describes the mapping of a mapped superclass. | ||
| 261 | */ | ||
| 262 | public bool $isMappedSuperclass = false; | ||
| 263 | |||
| 264 | /** | ||
| 265 | * READ-ONLY: Whether this class describes the mapping of an embeddable class. | ||
| 266 | */ | ||
| 267 | public bool $isEmbeddedClass = false; | ||
| 268 | |||
| 269 | /** | ||
| 270 | * READ-ONLY: The names of the parent <em>entity</em> classes (ancestors), starting with the | ||
| 271 | * nearest one and ending with the root entity class. | ||
| 272 | * | ||
| 273 | * @psalm-var list<class-string> | ||
| 274 | */ | ||
| 275 | public array $parentClasses = []; | ||
| 276 | |||
| 277 | /** | ||
| 278 | * READ-ONLY: For classes in inheritance mapping hierarchies, this field contains the names of all | ||
| 279 | * <em>entity</em> subclasses of this class. These may also be abstract classes. | ||
| 280 | * | ||
| 281 | * This list is used, for example, to enumerate all necessary tables in JTI when querying for root | ||
| 282 | * or subclass entities, or to gather all fields comprised in an entity inheritance tree. | ||
| 283 | * | ||
| 284 | * For classes that do not use STI/JTI, this list is empty. | ||
| 285 | * | ||
| 286 | * Implementation note: | ||
| 287 | * | ||
| 288 | * In PHP, there is no general way to discover all subclasses of a given class at runtime. For that | ||
| 289 | * reason, the list of classes given in the discriminator map at the root entity is considered | ||
| 290 | * authoritative. The discriminator map must contain all <em>concrete</em> classes that can | ||
| 291 | * appear in the particular inheritance hierarchy tree. Since there can be no instances of abstract | ||
| 292 | * entity classes, users are not required to list such classes with a discriminator value. | ||
| 293 | * | ||
| 294 | * The possibly remaining "gaps" for abstract entity classes are filled after the class metadata for the | ||
| 295 | * root entity has been loaded. | ||
| 296 | * | ||
| 297 | * For subclasses of such root entities, the list can be reused/passed downwards, it only needs to | ||
| 298 | * be filtered accordingly (only keep remaining subclasses) | ||
| 299 | * | ||
| 300 | * @psalm-var list<class-string> | ||
| 301 | */ | ||
| 302 | public array $subClasses = []; | ||
| 303 | |||
| 304 | /** | ||
| 305 | * READ-ONLY: The names of all embedded classes based on properties. | ||
| 306 | * | ||
| 307 | * @psalm-var array<string, EmbeddedClassMapping> | ||
| 308 | */ | ||
| 309 | public array $embeddedClasses = []; | ||
| 310 | |||
| 311 | /** | ||
| 312 | * READ-ONLY: The field names of all fields that are part of the identifier/primary key | ||
| 313 | * of the mapped entity class. | ||
| 314 | * | ||
| 315 | * @psalm-var list<string> | ||
| 316 | */ | ||
| 317 | public array $identifier = []; | ||
| 318 | |||
| 319 | /** | ||
| 320 | * READ-ONLY: The inheritance mapping type used by the class. | ||
| 321 | * | ||
| 322 | * @psalm-var self::INHERITANCE_TYPE_* | ||
| 323 | */ | ||
| 324 | public int $inheritanceType = self::INHERITANCE_TYPE_NONE; | ||
| 325 | |||
| 326 | /** | ||
| 327 | * READ-ONLY: The Id generator type used by the class. | ||
| 328 | * | ||
| 329 | * @psalm-var self::GENERATOR_TYPE_* | ||
| 330 | */ | ||
| 331 | public int $generatorType = self::GENERATOR_TYPE_NONE; | ||
| 332 | |||
| 333 | /** | ||
| 334 | * READ-ONLY: The field mappings of the class. | ||
| 335 | * Keys are field names and values are FieldMapping instances | ||
| 336 | * | ||
| 337 | * @var array<string, FieldMapping> | ||
| 338 | */ | ||
| 339 | public array $fieldMappings = []; | ||
| 340 | |||
| 341 | /** | ||
| 342 | * READ-ONLY: An array of field names. Used to look up field names from column names. | ||
| 343 | * Keys are column names and values are field names. | ||
| 344 | * | ||
| 345 | * @psalm-var array<string, string> | ||
| 346 | */ | ||
| 347 | public array $fieldNames = []; | ||
| 348 | |||
| 349 | /** | ||
| 350 | * READ-ONLY: A map of field names to column names. Keys are field names and values column names. | ||
| 351 | * Used to look up column names from field names. | ||
| 352 | * This is the reverse lookup map of $_fieldNames. | ||
| 353 | * | ||
| 354 | * @deprecated 3.0 Remove this. | ||
| 355 | * | ||
| 356 | * @var mixed[] | ||
| 357 | */ | ||
| 358 | public array $columnNames = []; | ||
| 359 | |||
| 360 | /** | ||
| 361 | * READ-ONLY: The discriminator value of this class. | ||
| 362 | * | ||
| 363 | * <b>This does only apply to the JOINED and SINGLE_TABLE inheritance mapping strategies | ||
| 364 | * where a discriminator column is used.</b> | ||
| 365 | * | ||
| 366 | * @see discriminatorColumn | ||
| 367 | */ | ||
| 368 | public mixed $discriminatorValue = null; | ||
| 369 | |||
| 370 | /** | ||
| 371 | * READ-ONLY: The discriminator map of all mapped classes in the hierarchy. | ||
| 372 | * | ||
| 373 | * <b>This does only apply to the JOINED and SINGLE_TABLE inheritance mapping strategies | ||
| 374 | * where a discriminator column is used.</b> | ||
| 375 | * | ||
| 376 | * @see discriminatorColumn | ||
| 377 | * | ||
| 378 | * @var array<int|string, string> | ||
| 379 | * | ||
| 380 | * @psalm-var array<int|string, class-string> | ||
| 381 | */ | ||
| 382 | public array $discriminatorMap = []; | ||
| 383 | |||
| 384 | /** | ||
| 385 | * READ-ONLY: The definition of the discriminator column used in JOINED and SINGLE_TABLE | ||
| 386 | * inheritance mappings. | ||
| 387 | */ | ||
| 388 | public DiscriminatorColumnMapping|null $discriminatorColumn = null; | ||
| 389 | |||
| 390 | /** | ||
| 391 | * READ-ONLY: The primary table definition. The definition is an array with the | ||
| 392 | * following entries: | ||
| 393 | * | ||
| 394 | * name => <tableName> | ||
| 395 | * schema => <schemaName> | ||
| 396 | * indexes => array | ||
| 397 | * uniqueConstraints => array | ||
| 398 | * | ||
| 399 | * @var mixed[] | ||
| 400 | * @psalm-var array{ | ||
| 401 | * name: string, | ||
| 402 | * schema?: string, | ||
| 403 | * indexes?: array, | ||
| 404 | * uniqueConstraints?: array, | ||
| 405 | * options?: array<string, mixed>, | ||
| 406 | * quoted?: bool | ||
| 407 | * } | ||
| 408 | */ | ||
| 409 | public array $table; | ||
| 410 | |||
| 411 | /** | ||
| 412 | * READ-ONLY: The registered lifecycle callbacks for entities of this class. | ||
| 413 | * | ||
| 414 | * @psalm-var array<string, list<string>> | ||
| 415 | */ | ||
| 416 | public array $lifecycleCallbacks = []; | ||
| 417 | |||
| 418 | /** | ||
| 419 | * READ-ONLY: The registered entity listeners. | ||
| 420 | * | ||
| 421 | * @psalm-var array<string, list<array{class: class-string, method: string}>> | ||
| 422 | */ | ||
| 423 | public array $entityListeners = []; | ||
| 424 | |||
| 425 | /** | ||
| 426 | * READ-ONLY: The association mappings of this class. | ||
| 427 | * | ||
| 428 | * A join table definition has the following structure: | ||
| 429 | * <pre> | ||
| 430 | * array( | ||
| 431 | * 'name' => <join table name>, | ||
| 432 | * 'joinColumns' => array(<join column mapping from join table to source table>), | ||
| 433 | * 'inverseJoinColumns' => array(<join column mapping from join table to target table>) | ||
| 434 | * ) | ||
| 435 | * </pre> | ||
| 436 | * | ||
| 437 | * @psalm-var array<string, ConcreteAssociationMapping> | ||
| 438 | */ | ||
| 439 | public array $associationMappings = []; | ||
| 440 | |||
| 441 | /** | ||
| 442 | * READ-ONLY: Flag indicating whether the identifier/primary key of the class is composite. | ||
| 443 | */ | ||
| 444 | public bool $isIdentifierComposite = false; | ||
| 445 | |||
| 446 | /** | ||
| 447 | * READ-ONLY: Flag indicating whether the identifier/primary key contains at least one foreign key association. | ||
| 448 | * | ||
| 449 | * This flag is necessary because some code blocks require special treatment of this cases. | ||
| 450 | */ | ||
| 451 | public bool $containsForeignIdentifier = false; | ||
| 452 | |||
| 453 | /** | ||
| 454 | * READ-ONLY: Flag indicating whether the identifier/primary key contains at least one ENUM type. | ||
| 455 | * | ||
| 456 | * This flag is necessary because some code blocks require special treatment of this cases. | ||
| 457 | */ | ||
| 458 | public bool $containsEnumIdentifier = false; | ||
| 459 | |||
| 460 | /** | ||
| 461 | * READ-ONLY: The ID generator used for generating IDs for this class. | ||
| 462 | * | ||
| 463 | * @todo Remove! | ||
| 464 | */ | ||
| 465 | public AbstractIdGenerator $idGenerator; | ||
| 466 | |||
| 467 | /** | ||
| 468 | * READ-ONLY: The definition of the sequence generator of this class. Only used for the | ||
| 469 | * SEQUENCE generation strategy. | ||
| 470 | * | ||
| 471 | * The definition has the following structure: | ||
| 472 | * <code> | ||
| 473 | * array( | ||
| 474 | * 'sequenceName' => 'name', | ||
| 475 | * 'allocationSize' => '20', | ||
| 476 | * 'initialValue' => '1' | ||
| 477 | * ) | ||
| 478 | * </code> | ||
| 479 | * | ||
| 480 | * @var array<string, mixed>|null | ||
| 481 | * @psalm-var array{sequenceName: string, allocationSize: string, initialValue: string, quoted?: mixed}|null | ||
| 482 | * @todo Merge with tableGeneratorDefinition into generic generatorDefinition | ||
| 483 | */ | ||
| 484 | public array|null $sequenceGeneratorDefinition = null; | ||
| 485 | |||
| 486 | /** | ||
| 487 | * READ-ONLY: The policy used for change-tracking on entities of this class. | ||
| 488 | */ | ||
| 489 | public int $changeTrackingPolicy = self::CHANGETRACKING_DEFERRED_IMPLICIT; | ||
| 490 | |||
| 491 | /** | ||
| 492 | * READ-ONLY: A Flag indicating whether one or more columns of this class | ||
| 493 | * have to be reloaded after insert / update operations. | ||
| 494 | */ | ||
| 495 | public bool $requiresFetchAfterChange = false; | ||
| 496 | |||
| 497 | /** | ||
| 498 | * READ-ONLY: A flag for whether or not instances of this class are to be versioned | ||
| 499 | * with optimistic locking. | ||
| 500 | */ | ||
| 501 | public bool $isVersioned = false; | ||
| 502 | |||
| 503 | /** | ||
| 504 | * READ-ONLY: The name of the field which is used for versioning in optimistic locking (if any). | ||
| 505 | */ | ||
| 506 | public string|null $versionField = null; | ||
| 507 | |||
| 508 | /** @var mixed[]|null */ | ||
| 509 | public array|null $cache = null; | ||
| 510 | |||
| 511 | /** | ||
| 512 | * The ReflectionClass instance of the mapped class. | ||
| 513 | * | ||
| 514 | * @var ReflectionClass<T>|null | ||
| 515 | */ | ||
| 516 | public ReflectionClass|null $reflClass = null; | ||
| 517 | |||
| 518 | /** | ||
| 519 | * Is this entity marked as "read-only"? | ||
| 520 | * | ||
| 521 | * That means it is never considered for change-tracking in the UnitOfWork. It is a very helpful performance | ||
| 522 | * optimization for entities that are immutable, either in your domain or through the relation database | ||
| 523 | * (coming from a view, or a history table for example). | ||
| 524 | */ | ||
| 525 | public bool $isReadOnly = false; | ||
| 526 | |||
| 527 | /** | ||
| 528 | * NamingStrategy determining the default column and table names. | ||
| 529 | */ | ||
| 530 | protected NamingStrategy $namingStrategy; | ||
| 531 | |||
| 532 | /** | ||
| 533 | * The ReflectionProperty instances of the mapped class. | ||
| 534 | * | ||
| 535 | * @var array<string, ReflectionProperty|null> | ||
| 536 | */ | ||
| 537 | public array $reflFields = []; | ||
| 538 | |||
| 539 | private InstantiatorInterface|null $instantiator = null; | ||
| 540 | |||
| 541 | private readonly TypedFieldMapper $typedFieldMapper; | ||
| 542 | |||
| 543 | /** | ||
| 544 | * Initializes a new ClassMetadata instance that will hold the object-relational mapping | ||
| 545 | * metadata of the class with the given name. | ||
| 546 | * | ||
| 547 | * @param string $name The name of the entity class the new instance is used for. | ||
| 548 | * @psalm-param class-string<T> $name | ||
| 549 | */ | ||
| 550 | public function __construct(public string $name, NamingStrategy|null $namingStrategy = null, TypedFieldMapper|null $typedFieldMapper = null) | ||
| 551 | { | ||
| 552 | $this->rootEntityName = $name; | ||
| 553 | $this->namingStrategy = $namingStrategy ?? new DefaultNamingStrategy(); | ||
| 554 | $this->instantiator = new Instantiator(); | ||
| 555 | $this->typedFieldMapper = $typedFieldMapper ?? new DefaultTypedFieldMapper(); | ||
| 556 | } | ||
| 557 | |||
| 558 | /** | ||
| 559 | * Gets the ReflectionProperties of the mapped class. | ||
| 560 | * | ||
| 561 | * @return ReflectionProperty[]|null[] An array of ReflectionProperty instances. | ||
| 562 | * @psalm-return array<ReflectionProperty|null> | ||
| 563 | */ | ||
| 564 | public function getReflectionProperties(): array | ||
| 565 | { | ||
| 566 | return $this->reflFields; | ||
| 567 | } | ||
| 568 | |||
| 569 | /** | ||
| 570 | * Gets a ReflectionProperty for a specific field of the mapped class. | ||
| 571 | */ | ||
| 572 | public function getReflectionProperty(string $name): ReflectionProperty|null | ||
| 573 | { | ||
| 574 | return $this->reflFields[$name]; | ||
| 575 | } | ||
| 576 | |||
| 577 | /** | ||
| 578 | * Gets the ReflectionProperty for the single identifier field. | ||
| 579 | * | ||
| 580 | * @throws BadMethodCallException If the class has a composite identifier. | ||
| 581 | */ | ||
| 582 | public function getSingleIdReflectionProperty(): ReflectionProperty|null | ||
| 583 | { | ||
| 584 | if ($this->isIdentifierComposite) { | ||
| 585 | throw new BadMethodCallException('Class ' . $this->name . ' has a composite identifier.'); | ||
| 586 | } | ||
| 587 | |||
| 588 | return $this->reflFields[$this->identifier[0]]; | ||
| 589 | } | ||
| 590 | |||
| 591 | /** | ||
| 592 | * Extracts the identifier values of an entity of this class. | ||
| 593 | * | ||
| 594 | * For composite identifiers, the identifier values are returned as an array | ||
| 595 | * with the same order as the field order in {@link identifier}. | ||
| 596 | * | ||
| 597 | * @return array<string, mixed> | ||
| 598 | */ | ||
| 599 | public function getIdentifierValues(object $entity): array | ||
| 600 | { | ||
| 601 | if ($this->isIdentifierComposite) { | ||
| 602 | $id = []; | ||
| 603 | |||
| 604 | foreach ($this->identifier as $idField) { | ||
| 605 | $value = $this->reflFields[$idField]->getValue($entity); | ||
| 606 | |||
| 607 | if ($value !== null) { | ||
| 608 | $id[$idField] = $value; | ||
| 609 | } | ||
| 610 | } | ||
| 611 | |||
| 612 | return $id; | ||
| 613 | } | ||
| 614 | |||
| 615 | $id = $this->identifier[0]; | ||
| 616 | $value = $this->reflFields[$id]->getValue($entity); | ||
| 617 | |||
| 618 | if ($value === null) { | ||
| 619 | return []; | ||
| 620 | } | ||
| 621 | |||
| 622 | return [$id => $value]; | ||
| 623 | } | ||
| 624 | |||
| 625 | /** | ||
| 626 | * Populates the entity identifier of an entity. | ||
| 627 | * | ||
| 628 | * @psalm-param array<string, mixed> $id | ||
| 629 | * | ||
| 630 | * @todo Rename to assignIdentifier() | ||
| 631 | */ | ||
| 632 | public function setIdentifierValues(object $entity, array $id): void | ||
| 633 | { | ||
| 634 | foreach ($id as $idField => $idValue) { | ||
| 635 | $this->reflFields[$idField]->setValue($entity, $idValue); | ||
| 636 | } | ||
| 637 | } | ||
| 638 | |||
| 639 | /** | ||
| 640 | * Sets the specified field to the specified value on the given entity. | ||
| 641 | */ | ||
| 642 | public function setFieldValue(object $entity, string $field, mixed $value): void | ||
| 643 | { | ||
| 644 | $this->reflFields[$field]->setValue($entity, $value); | ||
| 645 | } | ||
| 646 | |||
| 647 | /** | ||
| 648 | * Gets the specified field's value off the given entity. | ||
| 649 | */ | ||
| 650 | public function getFieldValue(object $entity, string $field): mixed | ||
| 651 | { | ||
| 652 | return $this->reflFields[$field]->getValue($entity); | ||
| 653 | } | ||
| 654 | |||
| 655 | /** | ||
| 656 | * Creates a string representation of this instance. | ||
| 657 | * | ||
| 658 | * @return string The string representation of this instance. | ||
| 659 | * | ||
| 660 | * @todo Construct meaningful string representation. | ||
| 661 | */ | ||
| 662 | public function __toString(): string | ||
| 663 | { | ||
| 664 | return self::class . '@' . spl_object_id($this); | ||
| 665 | } | ||
| 666 | |||
| 667 | /** | ||
| 668 | * Determines which fields get serialized. | ||
| 669 | * | ||
| 670 | * It is only serialized what is necessary for best unserialization performance. | ||
| 671 | * That means any metadata properties that are not set or empty or simply have | ||
| 672 | * their default value are NOT serialized. | ||
| 673 | * | ||
| 674 | * Parts that are also NOT serialized because they can not be properly unserialized: | ||
| 675 | * - reflClass (ReflectionClass) | ||
| 676 | * - reflFields (ReflectionProperty array) | ||
| 677 | * | ||
| 678 | * @return string[] The names of all the fields that should be serialized. | ||
| 679 | */ | ||
| 680 | public function __sleep(): array | ||
| 681 | { | ||
| 682 | // This metadata is always serialized/cached. | ||
| 683 | $serialized = [ | ||
| 684 | 'associationMappings', | ||
| 685 | 'columnNames', //TODO: 3.0 Remove this. Can use fieldMappings[$fieldName]['columnName'] | ||
| 686 | 'fieldMappings', | ||
| 687 | 'fieldNames', | ||
| 688 | 'embeddedClasses', | ||
| 689 | 'identifier', | ||
| 690 | 'isIdentifierComposite', // TODO: REMOVE | ||
| 691 | 'name', | ||
| 692 | 'namespace', // TODO: REMOVE | ||
| 693 | 'table', | ||
| 694 | 'rootEntityName', | ||
| 695 | 'idGenerator', //TODO: Does not really need to be serialized. Could be moved to runtime. | ||
| 696 | ]; | ||
| 697 | |||
| 698 | // The rest of the metadata is only serialized if necessary. | ||
| 699 | if ($this->changeTrackingPolicy !== self::CHANGETRACKING_DEFERRED_IMPLICIT) { | ||
| 700 | $serialized[] = 'changeTrackingPolicy'; | ||
| 701 | } | ||
| 702 | |||
| 703 | if ($this->customRepositoryClassName) { | ||
| 704 | $serialized[] = 'customRepositoryClassName'; | ||
| 705 | } | ||
| 706 | |||
| 707 | if ($this->inheritanceType !== self::INHERITANCE_TYPE_NONE) { | ||
| 708 | $serialized[] = 'inheritanceType'; | ||
| 709 | $serialized[] = 'discriminatorColumn'; | ||
| 710 | $serialized[] = 'discriminatorValue'; | ||
| 711 | $serialized[] = 'discriminatorMap'; | ||
| 712 | $serialized[] = 'parentClasses'; | ||
| 713 | $serialized[] = 'subClasses'; | ||
| 714 | } | ||
| 715 | |||
| 716 | if ($this->generatorType !== self::GENERATOR_TYPE_NONE) { | ||
| 717 | $serialized[] = 'generatorType'; | ||
| 718 | if ($this->generatorType === self::GENERATOR_TYPE_SEQUENCE) { | ||
| 719 | $serialized[] = 'sequenceGeneratorDefinition'; | ||
| 720 | } | ||
| 721 | } | ||
| 722 | |||
| 723 | if ($this->isMappedSuperclass) { | ||
| 724 | $serialized[] = 'isMappedSuperclass'; | ||
| 725 | } | ||
| 726 | |||
| 727 | if ($this->isEmbeddedClass) { | ||
| 728 | $serialized[] = 'isEmbeddedClass'; | ||
| 729 | } | ||
| 730 | |||
| 731 | if ($this->containsForeignIdentifier) { | ||
| 732 | $serialized[] = 'containsForeignIdentifier'; | ||
| 733 | } | ||
| 734 | |||
| 735 | if ($this->containsEnumIdentifier) { | ||
| 736 | $serialized[] = 'containsEnumIdentifier'; | ||
| 737 | } | ||
| 738 | |||
| 739 | if ($this->isVersioned) { | ||
| 740 | $serialized[] = 'isVersioned'; | ||
| 741 | $serialized[] = 'versionField'; | ||
| 742 | } | ||
| 743 | |||
| 744 | if ($this->lifecycleCallbacks) { | ||
| 745 | $serialized[] = 'lifecycleCallbacks'; | ||
| 746 | } | ||
| 747 | |||
| 748 | if ($this->entityListeners) { | ||
| 749 | $serialized[] = 'entityListeners'; | ||
| 750 | } | ||
| 751 | |||
| 752 | if ($this->isReadOnly) { | ||
| 753 | $serialized[] = 'isReadOnly'; | ||
| 754 | } | ||
| 755 | |||
| 756 | if ($this->customGeneratorDefinition) { | ||
| 757 | $serialized[] = 'customGeneratorDefinition'; | ||
| 758 | } | ||
| 759 | |||
| 760 | if ($this->cache) { | ||
| 761 | $serialized[] = 'cache'; | ||
| 762 | } | ||
| 763 | |||
| 764 | if ($this->requiresFetchAfterChange) { | ||
| 765 | $serialized[] = 'requiresFetchAfterChange'; | ||
| 766 | } | ||
| 767 | |||
| 768 | return $serialized; | ||
| 769 | } | ||
| 770 | |||
| 771 | /** | ||
| 772 | * Creates a new instance of the mapped class, without invoking the constructor. | ||
| 773 | */ | ||
| 774 | public function newInstance(): object | ||
| 775 | { | ||
| 776 | return $this->instantiator->instantiate($this->name); | ||
| 777 | } | ||
| 778 | |||
| 779 | /** | ||
| 780 | * Restores some state that can not be serialized/unserialized. | ||
| 781 | */ | ||
| 782 | public function wakeupReflection(ReflectionService $reflService): void | ||
| 783 | { | ||
| 784 | // Restore ReflectionClass and properties | ||
| 785 | $this->reflClass = $reflService->getClass($this->name); | ||
| 786 | $this->instantiator = $this->instantiator ?: new Instantiator(); | ||
| 787 | |||
| 788 | $parentReflFields = []; | ||
| 789 | |||
| 790 | foreach ($this->embeddedClasses as $property => $embeddedClass) { | ||
| 791 | if (isset($embeddedClass->declaredField)) { | ||
| 792 | assert($embeddedClass->originalField !== null); | ||
| 793 | $childProperty = $this->getAccessibleProperty( | ||
| 794 | $reflService, | ||
| 795 | $this->embeddedClasses[$embeddedClass->declaredField]->class, | ||
| 796 | $embeddedClass->originalField, | ||
| 797 | ); | ||
| 798 | assert($childProperty !== null); | ||
| 799 | $parentReflFields[$property] = new ReflectionEmbeddedProperty( | ||
| 800 | $parentReflFields[$embeddedClass->declaredField], | ||
| 801 | $childProperty, | ||
| 802 | $this->embeddedClasses[$embeddedClass->declaredField]->class, | ||
| 803 | ); | ||
| 804 | |||
| 805 | continue; | ||
| 806 | } | ||
| 807 | |||
| 808 | $fieldRefl = $this->getAccessibleProperty( | ||
| 809 | $reflService, | ||
| 810 | $embeddedClass->declared ?? $this->name, | ||
| 811 | $property, | ||
| 812 | ); | ||
| 813 | |||
| 814 | $parentReflFields[$property] = $fieldRefl; | ||
| 815 | $this->reflFields[$property] = $fieldRefl; | ||
| 816 | } | ||
| 817 | |||
| 818 | foreach ($this->fieldMappings as $field => $mapping) { | ||
| 819 | if (isset($mapping->declaredField) && isset($parentReflFields[$mapping->declaredField])) { | ||
| 820 | assert($mapping->originalField !== null); | ||
| 821 | assert($mapping->originalClass !== null); | ||
| 822 | $childProperty = $this->getAccessibleProperty($reflService, $mapping->originalClass, $mapping->originalField); | ||
| 823 | assert($childProperty !== null); | ||
| 824 | |||
| 825 | if (isset($mapping->enumType)) { | ||
| 826 | $childProperty = new EnumReflectionProperty( | ||
| 827 | $childProperty, | ||
| 828 | $mapping->enumType, | ||
| 829 | ); | ||
| 830 | } | ||
| 831 | |||
| 832 | $this->reflFields[$field] = new ReflectionEmbeddedProperty( | ||
| 833 | $parentReflFields[$mapping->declaredField], | ||
| 834 | $childProperty, | ||
| 835 | $mapping->originalClass, | ||
| 836 | ); | ||
| 837 | continue; | ||
| 838 | } | ||
| 839 | |||
| 840 | $this->reflFields[$field] = isset($mapping->declared) | ||
| 841 | ? $this->getAccessibleProperty($reflService, $mapping->declared, $field) | ||
| 842 | : $this->getAccessibleProperty($reflService, $this->name, $field); | ||
| 843 | |||
| 844 | if (isset($mapping->enumType) && $this->reflFields[$field] !== null) { | ||
| 845 | $this->reflFields[$field] = new EnumReflectionProperty( | ||
| 846 | $this->reflFields[$field], | ||
| 847 | $mapping->enumType, | ||
| 848 | ); | ||
| 849 | } | ||
| 850 | } | ||
| 851 | |||
| 852 | foreach ($this->associationMappings as $field => $mapping) { | ||
| 853 | $this->reflFields[$field] = isset($mapping->declared) | ||
| 854 | ? $this->getAccessibleProperty($reflService, $mapping->declared, $field) | ||
| 855 | : $this->getAccessibleProperty($reflService, $this->name, $field); | ||
| 856 | } | ||
| 857 | } | ||
| 858 | |||
| 859 | /** | ||
| 860 | * Initializes a new ClassMetadata instance that will hold the object-relational mapping | ||
| 861 | * metadata of the class with the given name. | ||
| 862 | * | ||
| 863 | * @param ReflectionService $reflService The reflection service. | ||
| 864 | */ | ||
| 865 | public function initializeReflection(ReflectionService $reflService): void | ||
| 866 | { | ||
| 867 | $this->reflClass = $reflService->getClass($this->name); | ||
| 868 | $this->namespace = $reflService->getClassNamespace($this->name); | ||
| 869 | |||
| 870 | if ($this->reflClass) { | ||
| 871 | $this->name = $this->rootEntityName = $this->reflClass->name; | ||
| 872 | } | ||
| 873 | |||
| 874 | $this->table['name'] = $this->namingStrategy->classToTableName($this->name); | ||
| 875 | } | ||
| 876 | |||
| 877 | /** | ||
| 878 | * Validates Identifier. | ||
| 879 | * | ||
| 880 | * @throws MappingException | ||
| 881 | */ | ||
| 882 | public function validateIdentifier(): void | ||
| 883 | { | ||
| 884 | if ($this->isMappedSuperclass || $this->isEmbeddedClass) { | ||
| 885 | return; | ||
| 886 | } | ||
| 887 | |||
| 888 | // Verify & complete identifier mapping | ||
| 889 | if (! $this->identifier) { | ||
| 890 | throw MappingException::identifierRequired($this->name); | ||
| 891 | } | ||
| 892 | |||
| 893 | if ($this->usesIdGenerator() && $this->isIdentifierComposite) { | ||
| 894 | throw MappingException::compositeKeyAssignedIdGeneratorRequired($this->name); | ||
| 895 | } | ||
| 896 | } | ||
| 897 | |||
| 898 | /** | ||
| 899 | * Validates association targets actually exist. | ||
| 900 | * | ||
| 901 | * @throws MappingException | ||
| 902 | */ | ||
| 903 | public function validateAssociations(): void | ||
| 904 | { | ||
| 905 | foreach ($this->associationMappings as $mapping) { | ||
| 906 | if ( | ||
| 907 | ! class_exists($mapping->targetEntity) | ||
| 908 | && ! interface_exists($mapping->targetEntity) | ||
| 909 | && ! trait_exists($mapping->targetEntity) | ||
| 910 | ) { | ||
| 911 | throw MappingException::invalidTargetEntityClass($mapping->targetEntity, $this->name, $mapping->fieldName); | ||
| 912 | } | ||
| 913 | } | ||
| 914 | } | ||
| 915 | |||
| 916 | /** | ||
| 917 | * Validates lifecycle callbacks. | ||
| 918 | * | ||
| 919 | * @throws MappingException | ||
| 920 | */ | ||
| 921 | public function validateLifecycleCallbacks(ReflectionService $reflService): void | ||
| 922 | { | ||
| 923 | foreach ($this->lifecycleCallbacks as $callbacks) { | ||
| 924 | foreach ($callbacks as $callbackFuncName) { | ||
| 925 | if (! $reflService->hasPublicMethod($this->name, $callbackFuncName)) { | ||
| 926 | throw MappingException::lifecycleCallbackMethodNotFound($this->name, $callbackFuncName); | ||
| 927 | } | ||
| 928 | } | ||
| 929 | } | ||
| 930 | } | ||
| 931 | |||
| 932 | /** | ||
| 933 | * {@inheritDoc} | ||
| 934 | * | ||
| 935 | * Can return null when using static reflection, in violation of the LSP | ||
| 936 | */ | ||
| 937 | public function getReflectionClass(): ReflectionClass|null | ||
| 938 | { | ||
| 939 | return $this->reflClass; | ||
| 940 | } | ||
| 941 | |||
| 942 | /** @psalm-param array{usage?: mixed, region?: mixed} $cache */ | ||
| 943 | public function enableCache(array $cache): void | ||
| 944 | { | ||
| 945 | if (! isset($cache['usage'])) { | ||
| 946 | $cache['usage'] = self::CACHE_USAGE_READ_ONLY; | ||
| 947 | } | ||
| 948 | |||
| 949 | if (! isset($cache['region'])) { | ||
| 950 | $cache['region'] = strtolower(str_replace('\\', '_', $this->rootEntityName)); | ||
| 951 | } | ||
| 952 | |||
| 953 | $this->cache = $cache; | ||
| 954 | } | ||
| 955 | |||
| 956 | /** @psalm-param array{usage?: int, region?: string} $cache */ | ||
| 957 | public function enableAssociationCache(string $fieldName, array $cache): void | ||
| 958 | { | ||
| 959 | $this->associationMappings[$fieldName]->cache = $this->getAssociationCacheDefaults($fieldName, $cache); | ||
| 960 | } | ||
| 961 | |||
| 962 | /** | ||
| 963 | * @psalm-param array{usage?: int, region?: string|null} $cache | ||
| 964 | * | ||
| 965 | * @return int[]|string[] | ||
| 966 | * @psalm-return array{usage: int, region: string|null} | ||
| 967 | */ | ||
| 968 | public function getAssociationCacheDefaults(string $fieldName, array $cache): array | ||
| 969 | { | ||
| 970 | if (! isset($cache['usage'])) { | ||
| 971 | $cache['usage'] = $this->cache['usage'] ?? self::CACHE_USAGE_READ_ONLY; | ||
| 972 | } | ||
| 973 | |||
| 974 | if (! isset($cache['region'])) { | ||
| 975 | $cache['region'] = strtolower(str_replace('\\', '_', $this->rootEntityName)) . '__' . $fieldName; | ||
| 976 | } | ||
| 977 | |||
| 978 | return $cache; | ||
| 979 | } | ||
| 980 | |||
| 981 | /** | ||
| 982 | * Sets the change tracking policy used by this class. | ||
| 983 | */ | ||
| 984 | public function setChangeTrackingPolicy(int $policy): void | ||
| 985 | { | ||
| 986 | $this->changeTrackingPolicy = $policy; | ||
| 987 | } | ||
| 988 | |||
| 989 | /** | ||
| 990 | * Whether the change tracking policy of this class is "deferred explicit". | ||
| 991 | */ | ||
| 992 | public function isChangeTrackingDeferredExplicit(): bool | ||
| 993 | { | ||
| 994 | return $this->changeTrackingPolicy === self::CHANGETRACKING_DEFERRED_EXPLICIT; | ||
| 995 | } | ||
| 996 | |||
| 997 | /** | ||
| 998 | * Whether the change tracking policy of this class is "deferred implicit". | ||
| 999 | */ | ||
| 1000 | public function isChangeTrackingDeferredImplicit(): bool | ||
| 1001 | { | ||
| 1002 | return $this->changeTrackingPolicy === self::CHANGETRACKING_DEFERRED_IMPLICIT; | ||
| 1003 | } | ||
| 1004 | |||
| 1005 | /** | ||
| 1006 | * Checks whether a field is part of the identifier/primary key field(s). | ||
| 1007 | */ | ||
| 1008 | public function isIdentifier(string $fieldName): bool | ||
| 1009 | { | ||
| 1010 | if (! $this->identifier) { | ||
| 1011 | return false; | ||
| 1012 | } | ||
| 1013 | |||
| 1014 | if (! $this->isIdentifierComposite) { | ||
| 1015 | return $fieldName === $this->identifier[0]; | ||
| 1016 | } | ||
| 1017 | |||
| 1018 | return in_array($fieldName, $this->identifier, true); | ||
| 1019 | } | ||
| 1020 | |||
| 1021 | public function isUniqueField(string $fieldName): bool | ||
| 1022 | { | ||
| 1023 | $mapping = $this->getFieldMapping($fieldName); | ||
| 1024 | |||
| 1025 | return $mapping !== false && isset($mapping->unique) && $mapping->unique; | ||
| 1026 | } | ||
| 1027 | |||
| 1028 | public function isNullable(string $fieldName): bool | ||
| 1029 | { | ||
| 1030 | $mapping = $this->getFieldMapping($fieldName); | ||
| 1031 | |||
| 1032 | return $mapping !== false && isset($mapping->nullable) && $mapping->nullable; | ||
| 1033 | } | ||
| 1034 | |||
| 1035 | /** | ||
| 1036 | * Gets a column name for a field name. | ||
| 1037 | * If the column name for the field cannot be found, the given field name | ||
| 1038 | * is returned. | ||
| 1039 | */ | ||
| 1040 | public function getColumnName(string $fieldName): string | ||
| 1041 | { | ||
| 1042 | return $this->columnNames[$fieldName] ?? $fieldName; | ||
| 1043 | } | ||
| 1044 | |||
| 1045 | /** | ||
| 1046 | * Gets the mapping of a (regular) field that holds some data but not a | ||
| 1047 | * reference to another object. | ||
| 1048 | * | ||
| 1049 | * @throws MappingException | ||
| 1050 | */ | ||
| 1051 | public function getFieldMapping(string $fieldName): FieldMapping | ||
| 1052 | { | ||
| 1053 | if (! isset($this->fieldMappings[$fieldName])) { | ||
| 1054 | throw MappingException::mappingNotFound($this->name, $fieldName); | ||
| 1055 | } | ||
| 1056 | |||
| 1057 | return $this->fieldMappings[$fieldName]; | ||
| 1058 | } | ||
| 1059 | |||
| 1060 | /** | ||
| 1061 | * Gets the mapping of an association. | ||
| 1062 | * | ||
| 1063 | * @see ClassMetadata::$associationMappings | ||
| 1064 | * | ||
| 1065 | * @param string $fieldName The field name that represents the association in | ||
| 1066 | * the object model. | ||
| 1067 | * | ||
| 1068 | * @throws MappingException | ||
| 1069 | */ | ||
| 1070 | public function getAssociationMapping(string $fieldName): AssociationMapping | ||
| 1071 | { | ||
| 1072 | if (! isset($this->associationMappings[$fieldName])) { | ||
| 1073 | throw MappingException::mappingNotFound($this->name, $fieldName); | ||
| 1074 | } | ||
| 1075 | |||
| 1076 | return $this->associationMappings[$fieldName]; | ||
| 1077 | } | ||
| 1078 | |||
| 1079 | /** | ||
| 1080 | * Gets all association mappings of the class. | ||
| 1081 | * | ||
| 1082 | * @psalm-return array<string, AssociationMapping> | ||
| 1083 | */ | ||
| 1084 | public function getAssociationMappings(): array | ||
| 1085 | { | ||
| 1086 | return $this->associationMappings; | ||
| 1087 | } | ||
| 1088 | |||
| 1089 | /** | ||
| 1090 | * Gets the field name for a column name. | ||
| 1091 | * If no field name can be found the column name is returned. | ||
| 1092 | * | ||
| 1093 | * @return string The column alias. | ||
| 1094 | */ | ||
| 1095 | public function getFieldName(string $columnName): string | ||
| 1096 | { | ||
| 1097 | return $this->fieldNames[$columnName] ?? $columnName; | ||
| 1098 | } | ||
| 1099 | |||
| 1100 | /** | ||
| 1101 | * Checks whether given property has type | ||
| 1102 | */ | ||
| 1103 | private function isTypedProperty(string $name): bool | ||
| 1104 | { | ||
| 1105 | return isset($this->reflClass) | ||
| 1106 | && $this->reflClass->hasProperty($name) | ||
| 1107 | && $this->reflClass->getProperty($name)->hasType(); | ||
| 1108 | } | ||
| 1109 | |||
| 1110 | /** | ||
| 1111 | * Validates & completes the given field mapping based on typed property. | ||
| 1112 | * | ||
| 1113 | * @param array{fieldName: string, type?: string} $mapping The field mapping to validate & complete. | ||
| 1114 | * | ||
| 1115 | * @return array{fieldName: string, enumType?: class-string<BackedEnum>, type?: string} The updated mapping. | ||
| 1116 | */ | ||
| 1117 | private function validateAndCompleteTypedFieldMapping(array $mapping): array | ||
| 1118 | { | ||
| 1119 | $field = $this->reflClass->getProperty($mapping['fieldName']); | ||
| 1120 | |||
| 1121 | $mapping = $this->typedFieldMapper->validateAndComplete($mapping, $field); | ||
| 1122 | |||
| 1123 | return $mapping; | ||
| 1124 | } | ||
| 1125 | |||
| 1126 | /** | ||
| 1127 | * Validates & completes the basic mapping information based on typed property. | ||
| 1128 | * | ||
| 1129 | * @param array{type: self::ONE_TO_ONE|self::MANY_TO_ONE|self::ONE_TO_MANY|self::MANY_TO_MANY, fieldName: string, targetEntity?: class-string} $mapping The mapping. | ||
| 1130 | * | ||
| 1131 | * @return mixed[] The updated mapping. | ||
| 1132 | */ | ||
| 1133 | private function validateAndCompleteTypedAssociationMapping(array $mapping): array | ||
| 1134 | { | ||
| 1135 | $type = $this->reflClass->getProperty($mapping['fieldName'])->getType(); | ||
| 1136 | |||
| 1137 | if ($type === null || ($mapping['type'] & self::TO_ONE) === 0) { | ||
| 1138 | return $mapping; | ||
| 1139 | } | ||
| 1140 | |||
| 1141 | if (! isset($mapping['targetEntity']) && $type instanceof ReflectionNamedType) { | ||
| 1142 | $mapping['targetEntity'] = $type->getName(); | ||
| 1143 | } | ||
| 1144 | |||
| 1145 | return $mapping; | ||
| 1146 | } | ||
| 1147 | |||
| 1148 | /** | ||
| 1149 | * Validates & completes the given field mapping. | ||
| 1150 | * | ||
| 1151 | * @psalm-param array{ | ||
| 1152 | * fieldName?: string, | ||
| 1153 | * columnName?: string, | ||
| 1154 | * id?: bool, | ||
| 1155 | * generated?: self::GENERATED_*, | ||
| 1156 | * enumType?: class-string, | ||
| 1157 | * } $mapping The field mapping to validate & complete. | ||
| 1158 | * | ||
| 1159 | * @return FieldMapping The updated mapping. | ||
| 1160 | * | ||
| 1161 | * @throws MappingException | ||
| 1162 | */ | ||
| 1163 | protected function validateAndCompleteFieldMapping(array $mapping): FieldMapping | ||
| 1164 | { | ||
| 1165 | // Check mandatory fields | ||
| 1166 | if (! isset($mapping['fieldName']) || ! $mapping['fieldName']) { | ||
| 1167 | throw MappingException::missingFieldName($this->name); | ||
| 1168 | } | ||
| 1169 | |||
| 1170 | if ($this->isTypedProperty($mapping['fieldName'])) { | ||
| 1171 | $mapping = $this->validateAndCompleteTypedFieldMapping($mapping); | ||
| 1172 | } | ||
| 1173 | |||
| 1174 | if (! isset($mapping['type'])) { | ||
| 1175 | // Default to string | ||
| 1176 | $mapping['type'] = 'string'; | ||
| 1177 | } | ||
| 1178 | |||
| 1179 | // Complete fieldName and columnName mapping | ||
| 1180 | if (! isset($mapping['columnName'])) { | ||
| 1181 | $mapping['columnName'] = $this->namingStrategy->propertyToColumnName($mapping['fieldName'], $this->name); | ||
| 1182 | } | ||
| 1183 | |||
| 1184 | $mapping = FieldMapping::fromMappingArray($mapping); | ||
| 1185 | |||
| 1186 | if ($mapping->columnName[0] === '`') { | ||
| 1187 | $mapping->columnName = trim($mapping->columnName, '`'); | ||
| 1188 | $mapping->quoted = true; | ||
| 1189 | } | ||
| 1190 | |||
| 1191 | $this->columnNames[$mapping->fieldName] = $mapping->columnName; | ||
| 1192 | |||
| 1193 | if (isset($this->fieldNames[$mapping->columnName]) || ($this->discriminatorColumn && $this->discriminatorColumn->name === $mapping->columnName)) { | ||
| 1194 | throw MappingException::duplicateColumnName($this->name, $mapping->columnName); | ||
| 1195 | } | ||
| 1196 | |||
| 1197 | $this->fieldNames[$mapping->columnName] = $mapping->fieldName; | ||
| 1198 | |||
| 1199 | // Complete id mapping | ||
| 1200 | if (isset($mapping->id) && $mapping->id === true) { | ||
| 1201 | if ($this->versionField === $mapping->fieldName) { | ||
| 1202 | throw MappingException::cannotVersionIdField($this->name, $mapping->fieldName); | ||
| 1203 | } | ||
| 1204 | |||
| 1205 | if (! in_array($mapping->fieldName, $this->identifier, true)) { | ||
| 1206 | $this->identifier[] = $mapping->fieldName; | ||
| 1207 | } | ||
| 1208 | |||
| 1209 | // Check for composite key | ||
| 1210 | if (! $this->isIdentifierComposite && count($this->identifier) > 1) { | ||
| 1211 | $this->isIdentifierComposite = true; | ||
| 1212 | } | ||
| 1213 | } | ||
| 1214 | |||
| 1215 | if (isset($mapping->generated)) { | ||
| 1216 | if (! in_array($mapping->generated, [self::GENERATED_NEVER, self::GENERATED_INSERT, self::GENERATED_ALWAYS])) { | ||
| 1217 | throw MappingException::invalidGeneratedMode($mapping->generated); | ||
| 1218 | } | ||
| 1219 | |||
| 1220 | if ($mapping->generated === self::GENERATED_NEVER) { | ||
| 1221 | unset($mapping->generated); | ||
| 1222 | } | ||
| 1223 | } | ||
| 1224 | |||
| 1225 | if (isset($mapping->enumType)) { | ||
| 1226 | if (! enum_exists($mapping->enumType)) { | ||
| 1227 | throw MappingException::nonEnumTypeMapped($this->name, $mapping->fieldName, $mapping->enumType); | ||
| 1228 | } | ||
| 1229 | |||
| 1230 | if (! empty($mapping->id)) { | ||
| 1231 | $this->containsEnumIdentifier = true; | ||
| 1232 | } | ||
| 1233 | } | ||
| 1234 | |||
| 1235 | return $mapping; | ||
| 1236 | } | ||
| 1237 | |||
| 1238 | /** | ||
| 1239 | * Validates & completes the basic mapping information that is common to all | ||
| 1240 | * association mappings (one-to-one, many-ot-one, one-to-many, many-to-many). | ||
| 1241 | * | ||
| 1242 | * @psalm-param array<string, mixed> $mapping The mapping. | ||
| 1243 | * | ||
| 1244 | * @return ConcreteAssociationMapping | ||
| 1245 | * | ||
| 1246 | * @throws MappingException If something is wrong with the mapping. | ||
| 1247 | */ | ||
| 1248 | protected function _validateAndCompleteAssociationMapping(array $mapping): AssociationMapping | ||
| 1249 | { | ||
| 1250 | if (array_key_exists('mappedBy', $mapping) && $mapping['mappedBy'] === null) { | ||
| 1251 | unset($mapping['mappedBy']); | ||
| 1252 | } | ||
| 1253 | |||
| 1254 | if (array_key_exists('inversedBy', $mapping) && $mapping['inversedBy'] === null) { | ||
| 1255 | unset($mapping['inversedBy']); | ||
| 1256 | } | ||
| 1257 | |||
| 1258 | if (array_key_exists('joinColumns', $mapping) && in_array($mapping['joinColumns'], [null, []], true)) { | ||
| 1259 | unset($mapping['joinColumns']); | ||
| 1260 | } | ||
| 1261 | |||
| 1262 | $mapping['isOwningSide'] = true; // assume owning side until we hit mappedBy | ||
| 1263 | |||
| 1264 | if (empty($mapping['indexBy'])) { | ||
| 1265 | unset($mapping['indexBy']); | ||
| 1266 | } | ||
| 1267 | |||
| 1268 | // If targetEntity is unqualified, assume it is in the same namespace as | ||
| 1269 | // the sourceEntity. | ||
| 1270 | $mapping['sourceEntity'] = $this->name; | ||
| 1271 | |||
| 1272 | if ($this->isTypedProperty($mapping['fieldName'])) { | ||
| 1273 | $mapping = $this->validateAndCompleteTypedAssociationMapping($mapping); | ||
| 1274 | } | ||
| 1275 | |||
| 1276 | if (isset($mapping['targetEntity'])) { | ||
| 1277 | $mapping['targetEntity'] = $this->fullyQualifiedClassName($mapping['targetEntity']); | ||
| 1278 | $mapping['targetEntity'] = ltrim($mapping['targetEntity'], '\\'); | ||
| 1279 | } | ||
| 1280 | |||
| 1281 | if (($mapping['type'] & self::MANY_TO_ONE) > 0 && isset($mapping['orphanRemoval']) && $mapping['orphanRemoval']) { | ||
| 1282 | throw MappingException::illegalOrphanRemoval($this->name, $mapping['fieldName']); | ||
| 1283 | } | ||
| 1284 | |||
| 1285 | // Complete id mapping | ||
| 1286 | if (isset($mapping['id']) && $mapping['id'] === true) { | ||
| 1287 | if (isset($mapping['orphanRemoval']) && $mapping['orphanRemoval']) { | ||
| 1288 | throw MappingException::illegalOrphanRemovalOnIdentifierAssociation($this->name, $mapping['fieldName']); | ||
| 1289 | } | ||
| 1290 | |||
| 1291 | if (! in_array($mapping['fieldName'], $this->identifier, true)) { | ||
| 1292 | if (isset($mapping['joinColumns']) && count($mapping['joinColumns']) >= 2) { | ||
| 1293 | throw MappingException::cannotMapCompositePrimaryKeyEntitiesAsForeignId( | ||
| 1294 | $mapping['targetEntity'], | ||
| 1295 | $this->name, | ||
| 1296 | $mapping['fieldName'], | ||
| 1297 | ); | ||
| 1298 | } | ||
| 1299 | |||
| 1300 | assert(is_string($mapping['fieldName'])); | ||
| 1301 | $this->identifier[] = $mapping['fieldName']; | ||
| 1302 | $this->containsForeignIdentifier = true; | ||
| 1303 | } | ||
| 1304 | |||
| 1305 | // Check for composite key | ||
| 1306 | if (! $this->isIdentifierComposite && count($this->identifier) > 1) { | ||
| 1307 | $this->isIdentifierComposite = true; | ||
| 1308 | } | ||
| 1309 | |||
| 1310 | if ($this->cache && ! isset($mapping['cache'])) { | ||
| 1311 | throw NonCacheableEntityAssociation::fromEntityAndField( | ||
| 1312 | $this->name, | ||
| 1313 | $mapping['fieldName'], | ||
| 1314 | ); | ||
| 1315 | } | ||
| 1316 | } | ||
| 1317 | |||
| 1318 | // Mandatory attributes for both sides | ||
| 1319 | // Mandatory: fieldName, targetEntity | ||
| 1320 | if (! isset($mapping['fieldName']) || ! $mapping['fieldName']) { | ||
| 1321 | throw MappingException::missingFieldName($this->name); | ||
| 1322 | } | ||
| 1323 | |||
| 1324 | if (! isset($mapping['targetEntity'])) { | ||
| 1325 | throw MappingException::missingTargetEntity($mapping['fieldName']); | ||
| 1326 | } | ||
| 1327 | |||
| 1328 | // Mandatory and optional attributes for either side | ||
| 1329 | if (! isset($mapping['mappedBy'])) { | ||
| 1330 | if (isset($mapping['joinTable'])) { | ||
| 1331 | if (isset($mapping['joinTable']['name']) && $mapping['joinTable']['name'][0] === '`') { | ||
| 1332 | $mapping['joinTable']['name'] = trim($mapping['joinTable']['name'], '`'); | ||
| 1333 | $mapping['joinTable']['quoted'] = true; | ||
| 1334 | } | ||
| 1335 | } | ||
| 1336 | } else { | ||
| 1337 | $mapping['isOwningSide'] = false; | ||
| 1338 | } | ||
| 1339 | |||
| 1340 | if (isset($mapping['id']) && $mapping['id'] === true && $mapping['type'] & self::TO_MANY) { | ||
| 1341 | throw MappingException::illegalToManyIdentifierAssociation($this->name, $mapping['fieldName']); | ||
| 1342 | } | ||
| 1343 | |||
| 1344 | // Fetch mode. Default fetch mode to LAZY, if not set. | ||
| 1345 | if (! isset($mapping['fetch'])) { | ||
| 1346 | $mapping['fetch'] = self::FETCH_LAZY; | ||
| 1347 | } | ||
| 1348 | |||
| 1349 | // Cascades | ||
| 1350 | $cascades = isset($mapping['cascade']) ? array_map('strtolower', $mapping['cascade']) : []; | ||
| 1351 | |||
| 1352 | $allCascades = ['remove', 'persist', 'refresh', 'detach']; | ||
| 1353 | if (in_array('all', $cascades, true)) { | ||
| 1354 | $cascades = $allCascades; | ||
| 1355 | } elseif (count($cascades) !== count(array_intersect($cascades, $allCascades))) { | ||
| 1356 | throw MappingException::invalidCascadeOption( | ||
| 1357 | array_diff($cascades, $allCascades), | ||
| 1358 | $this->name, | ||
| 1359 | $mapping['fieldName'], | ||
| 1360 | ); | ||
| 1361 | } | ||
| 1362 | |||
| 1363 | $mapping['cascade'] = $cascades; | ||
| 1364 | |||
| 1365 | switch ($mapping['type']) { | ||
| 1366 | case self::ONE_TO_ONE: | ||
| 1367 | if (isset($mapping['joinColumns']) && $mapping['joinColumns'] && ! $mapping['isOwningSide']) { | ||
| 1368 | throw MappingException::joinColumnNotAllowedOnOneToOneInverseSide( | ||
| 1369 | $this->name, | ||
| 1370 | $mapping['fieldName'], | ||
| 1371 | ); | ||
| 1372 | } | ||
| 1373 | |||
| 1374 | return $mapping['isOwningSide'] ? | ||
| 1375 | OneToOneOwningSideMapping::fromMappingArrayAndName( | ||
| 1376 | $mapping, | ||
| 1377 | $this->namingStrategy, | ||
| 1378 | $this->name, | ||
| 1379 | $this->table ?? null, | ||
| 1380 | $this->isInheritanceTypeSingleTable(), | ||
| 1381 | ) : | ||
| 1382 | OneToOneInverseSideMapping::fromMappingArrayAndName($mapping, $this->name); | ||
| 1383 | |||
| 1384 | case self::MANY_TO_ONE: | ||
| 1385 | return ManyToOneAssociationMapping::fromMappingArrayAndName( | ||
| 1386 | $mapping, | ||
| 1387 | $this->namingStrategy, | ||
| 1388 | $this->name, | ||
| 1389 | $this->table ?? null, | ||
| 1390 | $this->isInheritanceTypeSingleTable(), | ||
| 1391 | ); | ||
| 1392 | |||
| 1393 | case self::ONE_TO_MANY: | ||
| 1394 | return OneToManyAssociationMapping::fromMappingArrayAndName($mapping, $this->name); | ||
| 1395 | |||
| 1396 | case self::MANY_TO_MANY: | ||
| 1397 | if (isset($mapping['joinColumns'])) { | ||
| 1398 | unset($mapping['joinColumns']); | ||
| 1399 | } | ||
| 1400 | |||
| 1401 | return $mapping['isOwningSide'] ? | ||
| 1402 | ManyToManyOwningSideMapping::fromMappingArrayAndNamingStrategy($mapping, $this->namingStrategy) : | ||
| 1403 | ManyToManyInverseSideMapping::fromMappingArray($mapping); | ||
| 1404 | |||
| 1405 | default: | ||
| 1406 | throw MappingException::invalidAssociationType( | ||
| 1407 | $this->name, | ||
| 1408 | $mapping['fieldName'], | ||
| 1409 | $mapping['type'], | ||
| 1410 | ); | ||
| 1411 | } | ||
| 1412 | } | ||
| 1413 | |||
| 1414 | /** | ||
| 1415 | * {@inheritDoc} | ||
| 1416 | */ | ||
| 1417 | public function getIdentifierFieldNames(): array | ||
| 1418 | { | ||
| 1419 | return $this->identifier; | ||
| 1420 | } | ||
| 1421 | |||
| 1422 | /** | ||
| 1423 | * Gets the name of the single id field. Note that this only works on | ||
| 1424 | * entity classes that have a single-field pk. | ||
| 1425 | * | ||
| 1426 | * @throws MappingException If the class doesn't have an identifier or it has a composite primary key. | ||
| 1427 | */ | ||
| 1428 | public function getSingleIdentifierFieldName(): string | ||
| 1429 | { | ||
| 1430 | if ($this->isIdentifierComposite) { | ||
| 1431 | throw MappingException::singleIdNotAllowedOnCompositePrimaryKey($this->name); | ||
| 1432 | } | ||
| 1433 | |||
| 1434 | if (! isset($this->identifier[0])) { | ||
| 1435 | throw MappingException::noIdDefined($this->name); | ||
| 1436 | } | ||
| 1437 | |||
| 1438 | return $this->identifier[0]; | ||
| 1439 | } | ||
| 1440 | |||
| 1441 | /** | ||
| 1442 | * Gets the column name of the single id column. Note that this only works on | ||
| 1443 | * entity classes that have a single-field pk. | ||
| 1444 | * | ||
| 1445 | * @throws MappingException If the class doesn't have an identifier or it has a composite primary key. | ||
| 1446 | */ | ||
| 1447 | public function getSingleIdentifierColumnName(): string | ||
| 1448 | { | ||
| 1449 | return $this->getColumnName($this->getSingleIdentifierFieldName()); | ||
| 1450 | } | ||
| 1451 | |||
| 1452 | /** | ||
| 1453 | * INTERNAL: | ||
| 1454 | * Sets the mapped identifier/primary key fields of this class. | ||
| 1455 | * Mainly used by the ClassMetadataFactory to assign inherited identifiers. | ||
| 1456 | * | ||
| 1457 | * @psalm-param list<mixed> $identifier | ||
| 1458 | */ | ||
| 1459 | public function setIdentifier(array $identifier): void | ||
| 1460 | { | ||
| 1461 | $this->identifier = $identifier; | ||
| 1462 | $this->isIdentifierComposite = (count($this->identifier) > 1); | ||
| 1463 | } | ||
| 1464 | |||
| 1465 | /** | ||
| 1466 | * {@inheritDoc} | ||
| 1467 | */ | ||
| 1468 | public function getIdentifier(): array | ||
| 1469 | { | ||
| 1470 | return $this->identifier; | ||
| 1471 | } | ||
| 1472 | |||
| 1473 | public function hasField(string $fieldName): bool | ||
| 1474 | { | ||
| 1475 | return isset($this->fieldMappings[$fieldName]) || isset($this->embeddedClasses[$fieldName]); | ||
| 1476 | } | ||
| 1477 | |||
| 1478 | /** | ||
| 1479 | * Gets an array containing all the column names. | ||
| 1480 | * | ||
| 1481 | * @psalm-param list<string>|null $fieldNames | ||
| 1482 | * | ||
| 1483 | * @return mixed[] | ||
| 1484 | * @psalm-return list<string> | ||
| 1485 | */ | ||
| 1486 | public function getColumnNames(array|null $fieldNames = null): array | ||
| 1487 | { | ||
| 1488 | if ($fieldNames === null) { | ||
| 1489 | return array_keys($this->fieldNames); | ||
| 1490 | } | ||
| 1491 | |||
| 1492 | return array_values(array_map($this->getColumnName(...), $fieldNames)); | ||
| 1493 | } | ||
| 1494 | |||
| 1495 | /** | ||
| 1496 | * Returns an array with all the identifier column names. | ||
| 1497 | * | ||
| 1498 | * @psalm-return list<string> | ||
| 1499 | */ | ||
| 1500 | public function getIdentifierColumnNames(): array | ||
| 1501 | { | ||
| 1502 | $columnNames = []; | ||
| 1503 | |||
| 1504 | foreach ($this->identifier as $idProperty) { | ||
| 1505 | if (isset($this->fieldMappings[$idProperty])) { | ||
| 1506 | $columnNames[] = $this->fieldMappings[$idProperty]->columnName; | ||
| 1507 | |||
| 1508 | continue; | ||
| 1509 | } | ||
| 1510 | |||
| 1511 | // Association defined as Id field | ||
| 1512 | assert($this->associationMappings[$idProperty]->isToOneOwningSide()); | ||
| 1513 | $joinColumns = $this->associationMappings[$idProperty]->joinColumns; | ||
| 1514 | $assocColumnNames = array_map(static fn (JoinColumnMapping $joinColumn): string => $joinColumn->name, $joinColumns); | ||
| 1515 | |||
| 1516 | $columnNames = array_merge($columnNames, $assocColumnNames); | ||
| 1517 | } | ||
| 1518 | |||
| 1519 | return $columnNames; | ||
| 1520 | } | ||
| 1521 | |||
| 1522 | /** | ||
| 1523 | * Sets the type of Id generator to use for the mapped class. | ||
| 1524 | * | ||
| 1525 | * @psalm-param self::GENERATOR_TYPE_* $generatorType | ||
| 1526 | */ | ||
| 1527 | public function setIdGeneratorType(int $generatorType): void | ||
| 1528 | { | ||
| 1529 | $this->generatorType = $generatorType; | ||
| 1530 | } | ||
| 1531 | |||
| 1532 | /** | ||
| 1533 | * Checks whether the mapped class uses an Id generator. | ||
| 1534 | */ | ||
| 1535 | public function usesIdGenerator(): bool | ||
| 1536 | { | ||
| 1537 | return $this->generatorType !== self::GENERATOR_TYPE_NONE; | ||
| 1538 | } | ||
| 1539 | |||
| 1540 | public function isInheritanceTypeNone(): bool | ||
| 1541 | { | ||
| 1542 | return $this->inheritanceType === self::INHERITANCE_TYPE_NONE; | ||
| 1543 | } | ||
| 1544 | |||
| 1545 | /** | ||
| 1546 | * Checks whether the mapped class uses the JOINED inheritance mapping strategy. | ||
| 1547 | * | ||
| 1548 | * @return bool TRUE if the class participates in a JOINED inheritance mapping, | ||
| 1549 | * FALSE otherwise. | ||
| 1550 | */ | ||
| 1551 | public function isInheritanceTypeJoined(): bool | ||
| 1552 | { | ||
| 1553 | return $this->inheritanceType === self::INHERITANCE_TYPE_JOINED; | ||
| 1554 | } | ||
| 1555 | |||
| 1556 | /** | ||
| 1557 | * Checks whether the mapped class uses the SINGLE_TABLE inheritance mapping strategy. | ||
| 1558 | * | ||
| 1559 | * @return bool TRUE if the class participates in a SINGLE_TABLE inheritance mapping, | ||
| 1560 | * FALSE otherwise. | ||
| 1561 | */ | ||
| 1562 | public function isInheritanceTypeSingleTable(): bool | ||
| 1563 | { | ||
| 1564 | return $this->inheritanceType === self::INHERITANCE_TYPE_SINGLE_TABLE; | ||
| 1565 | } | ||
| 1566 | |||
| 1567 | /** | ||
| 1568 | * Checks whether the class uses an identity column for the Id generation. | ||
| 1569 | */ | ||
| 1570 | public function isIdGeneratorIdentity(): bool | ||
| 1571 | { | ||
| 1572 | return $this->generatorType === self::GENERATOR_TYPE_IDENTITY; | ||
| 1573 | } | ||
| 1574 | |||
| 1575 | /** | ||
| 1576 | * Checks whether the class uses a sequence for id generation. | ||
| 1577 | * | ||
| 1578 | * @psalm-assert-if-true !null $this->sequenceGeneratorDefinition | ||
| 1579 | */ | ||
| 1580 | public function isIdGeneratorSequence(): bool | ||
| 1581 | { | ||
| 1582 | return $this->generatorType === self::GENERATOR_TYPE_SEQUENCE; | ||
| 1583 | } | ||
| 1584 | |||
| 1585 | /** | ||
| 1586 | * Checks whether the class has a natural identifier/pk (which means it does | ||
| 1587 | * not use any Id generator. | ||
| 1588 | */ | ||
| 1589 | public function isIdentifierNatural(): bool | ||
| 1590 | { | ||
| 1591 | return $this->generatorType === self::GENERATOR_TYPE_NONE; | ||
| 1592 | } | ||
| 1593 | |||
| 1594 | /** | ||
| 1595 | * Gets the type of a field. | ||
| 1596 | * | ||
| 1597 | * @todo 3.0 Remove this. PersisterHelper should fix it somehow | ||
| 1598 | */ | ||
| 1599 | public function getTypeOfField(string $fieldName): string|null | ||
| 1600 | { | ||
| 1601 | return isset($this->fieldMappings[$fieldName]) | ||
| 1602 | ? $this->fieldMappings[$fieldName]->type | ||
| 1603 | : null; | ||
| 1604 | } | ||
| 1605 | |||
| 1606 | /** | ||
| 1607 | * Gets the name of the primary table. | ||
| 1608 | */ | ||
| 1609 | public function getTableName(): string | ||
| 1610 | { | ||
| 1611 | return $this->table['name']; | ||
| 1612 | } | ||
| 1613 | |||
| 1614 | /** | ||
| 1615 | * Gets primary table's schema name. | ||
| 1616 | */ | ||
| 1617 | public function getSchemaName(): string|null | ||
| 1618 | { | ||
| 1619 | return $this->table['schema'] ?? null; | ||
| 1620 | } | ||
| 1621 | |||
| 1622 | /** | ||
| 1623 | * Gets the table name to use for temporary identifier tables of this class. | ||
| 1624 | */ | ||
| 1625 | public function getTemporaryIdTableName(): string | ||
| 1626 | { | ||
| 1627 | // replace dots with underscores because PostgreSQL creates temporary tables in a special schema | ||
| 1628 | return str_replace('.', '_', $this->getTableName() . '_id_tmp'); | ||
| 1629 | } | ||
| 1630 | |||
| 1631 | /** | ||
| 1632 | * Sets the mapped subclasses of this class. | ||
| 1633 | * | ||
| 1634 | * @psalm-param list<string> $subclasses The names of all mapped subclasses. | ||
| 1635 | */ | ||
| 1636 | public function setSubclasses(array $subclasses): void | ||
| 1637 | { | ||
| 1638 | foreach ($subclasses as $subclass) { | ||
| 1639 | $this->subClasses[] = $this->fullyQualifiedClassName($subclass); | ||
| 1640 | } | ||
| 1641 | } | ||
| 1642 | |||
| 1643 | /** | ||
| 1644 | * Sets the parent class names. Only <em>entity</em> classes may be given. | ||
| 1645 | * | ||
| 1646 | * Assumes that the class names in the passed array are in the order: | ||
| 1647 | * directParent -> directParentParent -> directParentParentParent ... -> root. | ||
| 1648 | * | ||
| 1649 | * @psalm-param list<class-string> $classNames | ||
| 1650 | */ | ||
| 1651 | public function setParentClasses(array $classNames): void | ||
| 1652 | { | ||
| 1653 | $this->parentClasses = $classNames; | ||
| 1654 | |||
| 1655 | if (count($classNames) > 0) { | ||
| 1656 | $this->rootEntityName = array_pop($classNames); | ||
| 1657 | } | ||
| 1658 | } | ||
| 1659 | |||
| 1660 | /** | ||
| 1661 | * Sets the inheritance type used by the class and its subclasses. | ||
| 1662 | * | ||
| 1663 | * @psalm-param self::INHERITANCE_TYPE_* $type | ||
| 1664 | * | ||
| 1665 | * @throws MappingException | ||
| 1666 | */ | ||
| 1667 | public function setInheritanceType(int $type): void | ||
| 1668 | { | ||
| 1669 | if (! $this->isInheritanceType($type)) { | ||
| 1670 | throw MappingException::invalidInheritanceType($this->name, $type); | ||
| 1671 | } | ||
| 1672 | |||
| 1673 | $this->inheritanceType = $type; | ||
| 1674 | } | ||
| 1675 | |||
| 1676 | /** | ||
| 1677 | * Sets the association to override association mapping of property for an entity relationship. | ||
| 1678 | * | ||
| 1679 | * @psalm-param array<string, mixed> $overrideMapping | ||
| 1680 | * | ||
| 1681 | * @throws MappingException | ||
| 1682 | */ | ||
| 1683 | public function setAssociationOverride(string $fieldName, array $overrideMapping): void | ||
| 1684 | { | ||
| 1685 | if (! isset($this->associationMappings[$fieldName])) { | ||
| 1686 | throw MappingException::invalidOverrideFieldName($this->name, $fieldName); | ||
| 1687 | } | ||
| 1688 | |||
| 1689 | $mapping = $this->associationMappings[$fieldName]->toArray(); | ||
| 1690 | |||
| 1691 | if (isset($mapping['inherited'])) { | ||
| 1692 | throw MappingException::illegalOverrideOfInheritedProperty( | ||
| 1693 | $this->name, | ||
| 1694 | $fieldName, | ||
| 1695 | $mapping['inherited'], | ||
| 1696 | ); | ||
| 1697 | } | ||
| 1698 | |||
| 1699 | if (isset($overrideMapping['joinColumns'])) { | ||
| 1700 | $mapping['joinColumns'] = $overrideMapping['joinColumns']; | ||
| 1701 | } | ||
| 1702 | |||
| 1703 | if (isset($overrideMapping['inversedBy'])) { | ||
| 1704 | $mapping['inversedBy'] = $overrideMapping['inversedBy']; | ||
| 1705 | } | ||
| 1706 | |||
| 1707 | if (isset($overrideMapping['joinTable'])) { | ||
| 1708 | $mapping['joinTable'] = $overrideMapping['joinTable']; | ||
| 1709 | } | ||
| 1710 | |||
| 1711 | if (isset($overrideMapping['fetch'])) { | ||
| 1712 | $mapping['fetch'] = $overrideMapping['fetch']; | ||
| 1713 | } | ||
| 1714 | |||
| 1715 | switch ($mapping['type']) { | ||
| 1716 | case self::ONE_TO_ONE: | ||
| 1717 | case self::MANY_TO_ONE: | ||
| 1718 | $mapping['joinColumnFieldNames'] = []; | ||
| 1719 | $mapping['sourceToTargetKeyColumns'] = []; | ||
| 1720 | break; | ||
| 1721 | case self::MANY_TO_MANY: | ||
| 1722 | $mapping['relationToSourceKeyColumns'] = []; | ||
| 1723 | $mapping['relationToTargetKeyColumns'] = []; | ||
| 1724 | break; | ||
| 1725 | } | ||
| 1726 | |||
| 1727 | $this->associationMappings[$fieldName] = $this->_validateAndCompleteAssociationMapping($mapping); | ||
| 1728 | } | ||
| 1729 | |||
| 1730 | /** | ||
| 1731 | * Sets the override for a mapped field. | ||
| 1732 | * | ||
| 1733 | * @psalm-param array<string, mixed> $overrideMapping | ||
| 1734 | * | ||
| 1735 | * @throws MappingException | ||
| 1736 | */ | ||
| 1737 | public function setAttributeOverride(string $fieldName, array $overrideMapping): void | ||
| 1738 | { | ||
| 1739 | if (! isset($this->fieldMappings[$fieldName])) { | ||
| 1740 | throw MappingException::invalidOverrideFieldName($this->name, $fieldName); | ||
| 1741 | } | ||
| 1742 | |||
| 1743 | $mapping = $this->fieldMappings[$fieldName]; | ||
| 1744 | |||
| 1745 | if (isset($mapping->inherited)) { | ||
| 1746 | throw MappingException::illegalOverrideOfInheritedProperty($this->name, $fieldName, $mapping->inherited); | ||
| 1747 | } | ||
| 1748 | |||
| 1749 | if (isset($mapping->id)) { | ||
| 1750 | $overrideMapping['id'] = $mapping->id; | ||
| 1751 | } | ||
| 1752 | |||
| 1753 | if (isset($mapping->declared)) { | ||
| 1754 | $overrideMapping['declared'] = $mapping->declared; | ||
| 1755 | } | ||
| 1756 | |||
| 1757 | if (! isset($overrideMapping['type'])) { | ||
| 1758 | $overrideMapping['type'] = $mapping->type; | ||
| 1759 | } | ||
| 1760 | |||
| 1761 | if (! isset($overrideMapping['fieldName'])) { | ||
| 1762 | $overrideMapping['fieldName'] = $mapping->fieldName; | ||
| 1763 | } | ||
| 1764 | |||
| 1765 | if ($overrideMapping['type'] !== $mapping->type) { | ||
| 1766 | throw MappingException::invalidOverrideFieldType($this->name, $fieldName); | ||
| 1767 | } | ||
| 1768 | |||
| 1769 | unset($this->fieldMappings[$fieldName]); | ||
| 1770 | unset($this->fieldNames[$mapping->columnName]); | ||
| 1771 | unset($this->columnNames[$mapping->fieldName]); | ||
| 1772 | |||
| 1773 | $overrideMapping = $this->validateAndCompleteFieldMapping($overrideMapping); | ||
| 1774 | |||
| 1775 | $this->fieldMappings[$fieldName] = $overrideMapping; | ||
| 1776 | } | ||
| 1777 | |||
| 1778 | /** | ||
| 1779 | * Checks whether a mapped field is inherited from an entity superclass. | ||
| 1780 | */ | ||
| 1781 | public function isInheritedField(string $fieldName): bool | ||
| 1782 | { | ||
| 1783 | return isset($this->fieldMappings[$fieldName]->inherited); | ||
| 1784 | } | ||
| 1785 | |||
| 1786 | /** | ||
| 1787 | * Checks if this entity is the root in any entity-inheritance-hierarchy. | ||
| 1788 | */ | ||
| 1789 | public function isRootEntity(): bool | ||
| 1790 | { | ||
| 1791 | return $this->name === $this->rootEntityName; | ||
| 1792 | } | ||
| 1793 | |||
| 1794 | /** | ||
| 1795 | * Checks whether a mapped association field is inherited from a superclass. | ||
| 1796 | */ | ||
| 1797 | public function isInheritedAssociation(string $fieldName): bool | ||
| 1798 | { | ||
| 1799 | return isset($this->associationMappings[$fieldName]->inherited); | ||
| 1800 | } | ||
| 1801 | |||
| 1802 | public function isInheritedEmbeddedClass(string $fieldName): bool | ||
| 1803 | { | ||
| 1804 | return isset($this->embeddedClasses[$fieldName]->inherited); | ||
| 1805 | } | ||
| 1806 | |||
| 1807 | /** | ||
| 1808 | * Sets the name of the primary table the class is mapped to. | ||
| 1809 | * | ||
| 1810 | * @deprecated Use {@link setPrimaryTable}. | ||
| 1811 | */ | ||
| 1812 | public function setTableName(string $tableName): void | ||
| 1813 | { | ||
| 1814 | $this->table['name'] = $tableName; | ||
| 1815 | } | ||
| 1816 | |||
| 1817 | /** | ||
| 1818 | * Sets the primary table definition. The provided array supports the | ||
| 1819 | * following structure: | ||
| 1820 | * | ||
| 1821 | * name => <tableName> (optional, defaults to class name) | ||
| 1822 | * indexes => array of indexes (optional) | ||
| 1823 | * uniqueConstraints => array of constraints (optional) | ||
| 1824 | * | ||
| 1825 | * If a key is omitted, the current value is kept. | ||
| 1826 | * | ||
| 1827 | * @psalm-param array<string, mixed> $table The table description. | ||
| 1828 | */ | ||
| 1829 | public function setPrimaryTable(array $table): void | ||
| 1830 | { | ||
| 1831 | if (isset($table['name'])) { | ||
| 1832 | // Split schema and table name from a table name like "myschema.mytable" | ||
| 1833 | if (str_contains($table['name'], '.')) { | ||
| 1834 | [$this->table['schema'], $table['name']] = explode('.', $table['name'], 2); | ||
| 1835 | } | ||
| 1836 | |||
| 1837 | if ($table['name'][0] === '`') { | ||
| 1838 | $table['name'] = trim($table['name'], '`'); | ||
| 1839 | $this->table['quoted'] = true; | ||
| 1840 | } | ||
| 1841 | |||
| 1842 | $this->table['name'] = $table['name']; | ||
| 1843 | } | ||
| 1844 | |||
| 1845 | if (isset($table['quoted'])) { | ||
| 1846 | $this->table['quoted'] = $table['quoted']; | ||
| 1847 | } | ||
| 1848 | |||
| 1849 | if (isset($table['schema'])) { | ||
| 1850 | $this->table['schema'] = $table['schema']; | ||
| 1851 | } | ||
| 1852 | |||
| 1853 | if (isset($table['indexes'])) { | ||
| 1854 | $this->table['indexes'] = $table['indexes']; | ||
| 1855 | } | ||
| 1856 | |||
| 1857 | if (isset($table['uniqueConstraints'])) { | ||
| 1858 | $this->table['uniqueConstraints'] = $table['uniqueConstraints']; | ||
| 1859 | } | ||
| 1860 | |||
| 1861 | if (isset($table['options'])) { | ||
| 1862 | $this->table['options'] = $table['options']; | ||
| 1863 | } | ||
| 1864 | } | ||
| 1865 | |||
| 1866 | /** | ||
| 1867 | * Checks whether the given type identifies an inheritance type. | ||
| 1868 | */ | ||
| 1869 | private function isInheritanceType(int $type): bool | ||
| 1870 | { | ||
| 1871 | return $type === self::INHERITANCE_TYPE_NONE || | ||
| 1872 | $type === self::INHERITANCE_TYPE_SINGLE_TABLE || | ||
| 1873 | $type === self::INHERITANCE_TYPE_JOINED; | ||
| 1874 | } | ||
| 1875 | |||
| 1876 | /** | ||
| 1877 | * Adds a mapped field to the class. | ||
| 1878 | * | ||
| 1879 | * @psalm-param array<string, mixed> $mapping The field mapping. | ||
| 1880 | * | ||
| 1881 | * @throws MappingException | ||
| 1882 | */ | ||
| 1883 | public function mapField(array $mapping): void | ||
| 1884 | { | ||
| 1885 | $mapping = $this->validateAndCompleteFieldMapping($mapping); | ||
| 1886 | $this->assertFieldNotMapped($mapping->fieldName); | ||
| 1887 | |||
| 1888 | if (isset($mapping->generated)) { | ||
| 1889 | $this->requiresFetchAfterChange = true; | ||
| 1890 | } | ||
| 1891 | |||
| 1892 | $this->fieldMappings[$mapping->fieldName] = $mapping; | ||
| 1893 | } | ||
| 1894 | |||
| 1895 | /** | ||
| 1896 | * INTERNAL: | ||
| 1897 | * Adds an association mapping without completing/validating it. | ||
| 1898 | * This is mainly used to add inherited association mappings to derived classes. | ||
| 1899 | * | ||
| 1900 | * @param ConcreteAssociationMapping $mapping | ||
| 1901 | * | ||
| 1902 | * @throws MappingException | ||
| 1903 | */ | ||
| 1904 | public function addInheritedAssociationMapping(AssociationMapping $mapping/*, $owningClassName = null*/): void | ||
| 1905 | { | ||
| 1906 | if (isset($this->associationMappings[$mapping->fieldName])) { | ||
| 1907 | throw MappingException::duplicateAssociationMapping($this->name, $mapping->fieldName); | ||
| 1908 | } | ||
| 1909 | |||
| 1910 | $this->associationMappings[$mapping->fieldName] = $mapping; | ||
| 1911 | } | ||
| 1912 | |||
| 1913 | /** | ||
| 1914 | * INTERNAL: | ||
| 1915 | * Adds a field mapping without completing/validating it. | ||
| 1916 | * This is mainly used to add inherited field mappings to derived classes. | ||
| 1917 | */ | ||
| 1918 | public function addInheritedFieldMapping(FieldMapping $fieldMapping): void | ||
| 1919 | { | ||
| 1920 | $this->fieldMappings[$fieldMapping->fieldName] = $fieldMapping; | ||
| 1921 | $this->columnNames[$fieldMapping->fieldName] = $fieldMapping->columnName; | ||
| 1922 | $this->fieldNames[$fieldMapping->columnName] = $fieldMapping->fieldName; | ||
| 1923 | |||
| 1924 | if (isset($fieldMapping->generated)) { | ||
| 1925 | $this->requiresFetchAfterChange = true; | ||
| 1926 | } | ||
| 1927 | } | ||
| 1928 | |||
| 1929 | /** | ||
| 1930 | * Adds a one-to-one mapping. | ||
| 1931 | * | ||
| 1932 | * @param array<string, mixed> $mapping The mapping. | ||
| 1933 | */ | ||
| 1934 | public function mapOneToOne(array $mapping): void | ||
| 1935 | { | ||
| 1936 | $mapping['type'] = self::ONE_TO_ONE; | ||
| 1937 | |||
| 1938 | $mapping = $this->_validateAndCompleteAssociationMapping($mapping); | ||
| 1939 | |||
| 1940 | $this->_storeAssociationMapping($mapping); | ||
| 1941 | } | ||
| 1942 | |||
| 1943 | /** | ||
| 1944 | * Adds a one-to-many mapping. | ||
| 1945 | * | ||
| 1946 | * @psalm-param array<string, mixed> $mapping The mapping. | ||
| 1947 | */ | ||
| 1948 | public function mapOneToMany(array $mapping): void | ||
| 1949 | { | ||
| 1950 | $mapping['type'] = self::ONE_TO_MANY; | ||
| 1951 | |||
| 1952 | $mapping = $this->_validateAndCompleteAssociationMapping($mapping); | ||
| 1953 | |||
| 1954 | $this->_storeAssociationMapping($mapping); | ||
| 1955 | } | ||
| 1956 | |||
| 1957 | /** | ||
| 1958 | * Adds a many-to-one mapping. | ||
| 1959 | * | ||
| 1960 | * @psalm-param array<string, mixed> $mapping The mapping. | ||
| 1961 | */ | ||
| 1962 | public function mapManyToOne(array $mapping): void | ||
| 1963 | { | ||
| 1964 | $mapping['type'] = self::MANY_TO_ONE; | ||
| 1965 | |||
| 1966 | $mapping = $this->_validateAndCompleteAssociationMapping($mapping); | ||
| 1967 | |||
| 1968 | $this->_storeAssociationMapping($mapping); | ||
| 1969 | } | ||
| 1970 | |||
| 1971 | /** | ||
| 1972 | * Adds a many-to-many mapping. | ||
| 1973 | * | ||
| 1974 | * @psalm-param array<string, mixed> $mapping The mapping. | ||
| 1975 | */ | ||
| 1976 | public function mapManyToMany(array $mapping): void | ||
| 1977 | { | ||
| 1978 | $mapping['type'] = self::MANY_TO_MANY; | ||
| 1979 | |||
| 1980 | $mapping = $this->_validateAndCompleteAssociationMapping($mapping); | ||
| 1981 | |||
| 1982 | $this->_storeAssociationMapping($mapping); | ||
| 1983 | } | ||
| 1984 | |||
| 1985 | /** | ||
| 1986 | * Stores the association mapping. | ||
| 1987 | * | ||
| 1988 | * @param ConcreteAssociationMapping $assocMapping | ||
| 1989 | * | ||
| 1990 | * @throws MappingException | ||
| 1991 | */ | ||
| 1992 | protected function _storeAssociationMapping(AssociationMapping $assocMapping): void | ||
| 1993 | { | ||
| 1994 | $sourceFieldName = $assocMapping->fieldName; | ||
| 1995 | |||
| 1996 | $this->assertFieldNotMapped($sourceFieldName); | ||
| 1997 | |||
| 1998 | $this->associationMappings[$sourceFieldName] = $assocMapping; | ||
| 1999 | } | ||
| 2000 | |||
| 2001 | /** | ||
| 2002 | * Registers a custom repository class for the entity class. | ||
| 2003 | * | ||
| 2004 | * @param string|null $repositoryClassName The class name of the custom mapper. | ||
| 2005 | * @psalm-param class-string<EntityRepository>|null $repositoryClassName | ||
| 2006 | */ | ||
| 2007 | public function setCustomRepositoryClass(string|null $repositoryClassName): void | ||
| 2008 | { | ||
| 2009 | if ($repositoryClassName === null) { | ||
| 2010 | $this->customRepositoryClassName = null; | ||
| 2011 | |||
| 2012 | return; | ||
| 2013 | } | ||
| 2014 | |||
| 2015 | $this->customRepositoryClassName = $this->fullyQualifiedClassName($repositoryClassName); | ||
| 2016 | } | ||
| 2017 | |||
| 2018 | /** | ||
| 2019 | * Dispatches the lifecycle event of the given entity to the registered | ||
| 2020 | * lifecycle callbacks and lifecycle listeners. | ||
| 2021 | * | ||
| 2022 | * @deprecated Deprecated since version 2.4 in favor of \Doctrine\ORM\Event\ListenersInvoker | ||
| 2023 | * | ||
| 2024 | * @param string $lifecycleEvent The lifecycle event. | ||
| 2025 | */ | ||
| 2026 | public function invokeLifecycleCallbacks(string $lifecycleEvent, object $entity): void | ||
| 2027 | { | ||
| 2028 | foreach ($this->lifecycleCallbacks[$lifecycleEvent] as $callback) { | ||
| 2029 | $entity->$callback(); | ||
| 2030 | } | ||
| 2031 | } | ||
| 2032 | |||
| 2033 | /** | ||
| 2034 | * Whether the class has any attached lifecycle listeners or callbacks for a lifecycle event. | ||
| 2035 | */ | ||
| 2036 | public function hasLifecycleCallbacks(string $lifecycleEvent): bool | ||
| 2037 | { | ||
| 2038 | return isset($this->lifecycleCallbacks[$lifecycleEvent]); | ||
| 2039 | } | ||
| 2040 | |||
| 2041 | /** | ||
| 2042 | * Gets the registered lifecycle callbacks for an event. | ||
| 2043 | * | ||
| 2044 | * @return string[] | ||
| 2045 | * @psalm-return list<string> | ||
| 2046 | */ | ||
| 2047 | public function getLifecycleCallbacks(string $event): array | ||
| 2048 | { | ||
| 2049 | return $this->lifecycleCallbacks[$event] ?? []; | ||
| 2050 | } | ||
| 2051 | |||
| 2052 | /** | ||
| 2053 | * Adds a lifecycle callback for entities of this class. | ||
| 2054 | */ | ||
| 2055 | public function addLifecycleCallback(string $callback, string $event): void | ||
| 2056 | { | ||
| 2057 | if ($this->isEmbeddedClass) { | ||
| 2058 | throw MappingException::illegalLifecycleCallbackOnEmbeddedClass($callback, $this->name); | ||
| 2059 | } | ||
| 2060 | |||
| 2061 | if (isset($this->lifecycleCallbacks[$event]) && in_array($callback, $this->lifecycleCallbacks[$event], true)) { | ||
| 2062 | return; | ||
| 2063 | } | ||
| 2064 | |||
| 2065 | $this->lifecycleCallbacks[$event][] = $callback; | ||
| 2066 | } | ||
| 2067 | |||
| 2068 | /** | ||
| 2069 | * Sets the lifecycle callbacks for entities of this class. | ||
| 2070 | * Any previously registered callbacks are overwritten. | ||
| 2071 | * | ||
| 2072 | * @psalm-param array<string, list<string>> $callbacks | ||
| 2073 | */ | ||
| 2074 | public function setLifecycleCallbacks(array $callbacks): void | ||
| 2075 | { | ||
| 2076 | $this->lifecycleCallbacks = $callbacks; | ||
| 2077 | } | ||
| 2078 | |||
| 2079 | /** | ||
| 2080 | * Adds a entity listener for entities of this class. | ||
| 2081 | * | ||
| 2082 | * @param string $eventName The entity lifecycle event. | ||
| 2083 | * @param string $class The listener class. | ||
| 2084 | * @param string $method The listener callback method. | ||
| 2085 | * | ||
| 2086 | * @throws MappingException | ||
| 2087 | */ | ||
| 2088 | public function addEntityListener(string $eventName, string $class, string $method): void | ||
| 2089 | { | ||
| 2090 | $class = $this->fullyQualifiedClassName($class); | ||
| 2091 | |||
| 2092 | $listener = [ | ||
| 2093 | 'class' => $class, | ||
| 2094 | 'method' => $method, | ||
| 2095 | ]; | ||
| 2096 | |||
| 2097 | if (! class_exists($class)) { | ||
| 2098 | throw MappingException::entityListenerClassNotFound($class, $this->name); | ||
| 2099 | } | ||
| 2100 | |||
| 2101 | if (! method_exists($class, $method)) { | ||
| 2102 | throw MappingException::entityListenerMethodNotFound($class, $method, $this->name); | ||
| 2103 | } | ||
| 2104 | |||
| 2105 | if (isset($this->entityListeners[$eventName]) && in_array($listener, $this->entityListeners[$eventName], true)) { | ||
| 2106 | throw MappingException::duplicateEntityListener($class, $method, $this->name); | ||
| 2107 | } | ||
| 2108 | |||
| 2109 | $this->entityListeners[$eventName][] = $listener; | ||
| 2110 | } | ||
| 2111 | |||
| 2112 | /** | ||
| 2113 | * Sets the discriminator column definition. | ||
| 2114 | * | ||
| 2115 | * @see getDiscriminatorColumn() | ||
| 2116 | * | ||
| 2117 | * @param DiscriminatorColumnMapping|mixed[]|null $columnDef | ||
| 2118 | * @psalm-param DiscriminatorColumnMapping|array{ | ||
| 2119 | * name: string|null, | ||
| 2120 | * fieldName?: string|null, | ||
| 2121 | * type?: string|null, | ||
| 2122 | * length?: int|null, | ||
| 2123 | * columnDefinition?: string|null, | ||
| 2124 | * enumType?: class-string<BackedEnum>|null, | ||
| 2125 | * options?: array<string, mixed>|null | ||
| 2126 | * }|null $columnDef | ||
| 2127 | * | ||
| 2128 | * @throws MappingException | ||
| 2129 | */ | ||
| 2130 | public function setDiscriminatorColumn(DiscriminatorColumnMapping|array|null $columnDef): void | ||
| 2131 | { | ||
| 2132 | if ($columnDef instanceof DiscriminatorColumnMapping) { | ||
| 2133 | $this->discriminatorColumn = $columnDef; | ||
| 2134 | |||
| 2135 | return; | ||
| 2136 | } | ||
| 2137 | |||
| 2138 | if ($columnDef !== null) { | ||
| 2139 | if (! isset($columnDef['name'])) { | ||
| 2140 | throw MappingException::nameIsMandatoryForDiscriminatorColumns($this->name); | ||
| 2141 | } | ||
| 2142 | |||
| 2143 | if (isset($this->fieldNames[$columnDef['name']])) { | ||
| 2144 | throw MappingException::duplicateColumnName($this->name, $columnDef['name']); | ||
| 2145 | } | ||
| 2146 | |||
| 2147 | $columnDef['fieldName'] ??= $columnDef['name']; | ||
| 2148 | $columnDef['type'] ??= 'string'; | ||
| 2149 | $columnDef['options'] ??= []; | ||
| 2150 | |||
| 2151 | if (in_array($columnDef['type'], ['boolean', 'array', 'object', 'datetime', 'time', 'date'], true)) { | ||
| 2152 | throw MappingException::invalidDiscriminatorColumnType($this->name, $columnDef['type']); | ||
| 2153 | } | ||
| 2154 | |||
| 2155 | $this->discriminatorColumn = DiscriminatorColumnMapping::fromMappingArray($columnDef); | ||
| 2156 | } | ||
| 2157 | } | ||
| 2158 | |||
| 2159 | final public function getDiscriminatorColumn(): DiscriminatorColumnMapping | ||
| 2160 | { | ||
| 2161 | if ($this->discriminatorColumn === null) { | ||
| 2162 | throw new LogicException('The discriminator column was not set.'); | ||
| 2163 | } | ||
| 2164 | |||
| 2165 | return $this->discriminatorColumn; | ||
| 2166 | } | ||
| 2167 | |||
| 2168 | /** | ||
| 2169 | * Sets the discriminator values used by this class. | ||
| 2170 | * Used for JOINED and SINGLE_TABLE inheritance mapping strategies. | ||
| 2171 | * | ||
| 2172 | * @param array<int|string, string> $map | ||
| 2173 | */ | ||
| 2174 | public function setDiscriminatorMap(array $map): void | ||
| 2175 | { | ||
| 2176 | foreach ($map as $value => $className) { | ||
| 2177 | $this->addDiscriminatorMapClass($value, $className); | ||
| 2178 | } | ||
| 2179 | } | ||
| 2180 | |||
| 2181 | /** | ||
| 2182 | * Adds one entry of the discriminator map with a new class and corresponding name. | ||
| 2183 | * | ||
| 2184 | * @throws MappingException | ||
| 2185 | */ | ||
| 2186 | public function addDiscriminatorMapClass(int|string $name, string $className): void | ||
| 2187 | { | ||
| 2188 | $className = $this->fullyQualifiedClassName($className); | ||
| 2189 | $className = ltrim($className, '\\'); | ||
| 2190 | |||
| 2191 | $this->discriminatorMap[$name] = $className; | ||
| 2192 | |||
| 2193 | if ($this->name === $className) { | ||
| 2194 | $this->discriminatorValue = $name; | ||
| 2195 | |||
| 2196 | return; | ||
| 2197 | } | ||
| 2198 | |||
| 2199 | if (! (class_exists($className) || interface_exists($className))) { | ||
| 2200 | throw MappingException::invalidClassInDiscriminatorMap($className, $this->name); | ||
| 2201 | } | ||
| 2202 | |||
| 2203 | $this->addSubClass($className); | ||
| 2204 | } | ||
| 2205 | |||
| 2206 | /** @param array<class-string> $classes */ | ||
| 2207 | public function addSubClasses(array $classes): void | ||
| 2208 | { | ||
| 2209 | foreach ($classes as $className) { | ||
| 2210 | $this->addSubClass($className); | ||
| 2211 | } | ||
| 2212 | } | ||
| 2213 | |||
| 2214 | public function addSubClass(string $className): void | ||
| 2215 | { | ||
| 2216 | // By ignoring classes that are not subclasses of the current class, we simplify inheriting | ||
| 2217 | // the subclass list from a parent class at the beginning of \Doctrine\ORM\Mapping\ClassMetadataFactory::doLoadMetadata. | ||
| 2218 | |||
| 2219 | if (is_subclass_of($className, $this->name) && ! in_array($className, $this->subClasses, true)) { | ||
| 2220 | $this->subClasses[] = $className; | ||
| 2221 | } | ||
| 2222 | } | ||
| 2223 | |||
| 2224 | public function hasAssociation(string $fieldName): bool | ||
| 2225 | { | ||
| 2226 | return isset($this->associationMappings[$fieldName]); | ||
| 2227 | } | ||
| 2228 | |||
| 2229 | public function isSingleValuedAssociation(string $fieldName): bool | ||
| 2230 | { | ||
| 2231 | return isset($this->associationMappings[$fieldName]) | ||
| 2232 | && ($this->associationMappings[$fieldName]->isToOne()); | ||
| 2233 | } | ||
| 2234 | |||
| 2235 | public function isCollectionValuedAssociation(string $fieldName): bool | ||
| 2236 | { | ||
| 2237 | return isset($this->associationMappings[$fieldName]) | ||
| 2238 | && ! $this->associationMappings[$fieldName]->isToOne(); | ||
| 2239 | } | ||
| 2240 | |||
| 2241 | /** | ||
| 2242 | * Is this an association that only has a single join column? | ||
| 2243 | */ | ||
| 2244 | public function isAssociationWithSingleJoinColumn(string $fieldName): bool | ||
| 2245 | { | ||
| 2246 | return isset($this->associationMappings[$fieldName]) | ||
| 2247 | && isset($this->associationMappings[$fieldName]->joinColumns[0]) | ||
| 2248 | && ! isset($this->associationMappings[$fieldName]->joinColumns[1]); | ||
| 2249 | } | ||
| 2250 | |||
| 2251 | /** | ||
| 2252 | * Returns the single association join column (if any). | ||
| 2253 | * | ||
| 2254 | * @throws MappingException | ||
| 2255 | */ | ||
| 2256 | public function getSingleAssociationJoinColumnName(string $fieldName): string | ||
| 2257 | { | ||
| 2258 | if (! $this->isAssociationWithSingleJoinColumn($fieldName)) { | ||
| 2259 | throw MappingException::noSingleAssociationJoinColumnFound($this->name, $fieldName); | ||
| 2260 | } | ||
| 2261 | |||
| 2262 | $assoc = $this->associationMappings[$fieldName]; | ||
| 2263 | |||
| 2264 | assert($assoc->isToOneOwningSide()); | ||
| 2265 | |||
| 2266 | return $assoc->joinColumns[0]->name; | ||
| 2267 | } | ||
| 2268 | |||
| 2269 | /** | ||
| 2270 | * Returns the single association referenced join column name (if any). | ||
| 2271 | * | ||
| 2272 | * @throws MappingException | ||
| 2273 | */ | ||
| 2274 | public function getSingleAssociationReferencedJoinColumnName(string $fieldName): string | ||
| 2275 | { | ||
| 2276 | if (! $this->isAssociationWithSingleJoinColumn($fieldName)) { | ||
| 2277 | throw MappingException::noSingleAssociationJoinColumnFound($this->name, $fieldName); | ||
| 2278 | } | ||
| 2279 | |||
| 2280 | $assoc = $this->associationMappings[$fieldName]; | ||
| 2281 | |||
| 2282 | assert($assoc->isToOneOwningSide()); | ||
| 2283 | |||
| 2284 | return $assoc->joinColumns[0]->referencedColumnName; | ||
| 2285 | } | ||
| 2286 | |||
| 2287 | /** | ||
| 2288 | * Used to retrieve a fieldname for either field or association from a given column. | ||
| 2289 | * | ||
| 2290 | * This method is used in foreign-key as primary-key contexts. | ||
| 2291 | * | ||
| 2292 | * @throws MappingException | ||
| 2293 | */ | ||
| 2294 | public function getFieldForColumn(string $columnName): string | ||
| 2295 | { | ||
| 2296 | if (isset($this->fieldNames[$columnName])) { | ||
| 2297 | return $this->fieldNames[$columnName]; | ||
| 2298 | } | ||
| 2299 | |||
| 2300 | foreach ($this->associationMappings as $assocName => $mapping) { | ||
| 2301 | if ( | ||
| 2302 | $this->isAssociationWithSingleJoinColumn($assocName) && | ||
| 2303 | assert($this->associationMappings[$assocName]->isToOneOwningSide()) && | ||
| 2304 | $this->associationMappings[$assocName]->joinColumns[0]->name === $columnName | ||
| 2305 | ) { | ||
| 2306 | return $assocName; | ||
| 2307 | } | ||
| 2308 | } | ||
| 2309 | |||
| 2310 | throw MappingException::noFieldNameFoundForColumn($this->name, $columnName); | ||
| 2311 | } | ||
| 2312 | |||
| 2313 | /** | ||
| 2314 | * Sets the ID generator used to generate IDs for instances of this class. | ||
| 2315 | */ | ||
| 2316 | public function setIdGenerator(AbstractIdGenerator $generator): void | ||
| 2317 | { | ||
| 2318 | $this->idGenerator = $generator; | ||
| 2319 | } | ||
| 2320 | |||
| 2321 | /** | ||
| 2322 | * Sets definition. | ||
| 2323 | * | ||
| 2324 | * @psalm-param array<string, string|null> $definition | ||
| 2325 | */ | ||
| 2326 | public function setCustomGeneratorDefinition(array $definition): void | ||
| 2327 | { | ||
| 2328 | $this->customGeneratorDefinition = $definition; | ||
| 2329 | } | ||
| 2330 | |||
| 2331 | /** | ||
| 2332 | * Sets the definition of the sequence ID generator for this class. | ||
| 2333 | * | ||
| 2334 | * The definition must have the following structure: | ||
| 2335 | * <code> | ||
| 2336 | * array( | ||
| 2337 | * 'sequenceName' => 'name', | ||
| 2338 | * 'allocationSize' => 20, | ||
| 2339 | * 'initialValue' => 1 | ||
| 2340 | * 'quoted' => 1 | ||
| 2341 | * ) | ||
| 2342 | * </code> | ||
| 2343 | * | ||
| 2344 | * @psalm-param array{sequenceName?: string, allocationSize?: int|string, initialValue?: int|string, quoted?: mixed} $definition | ||
| 2345 | * | ||
| 2346 | * @throws MappingException | ||
| 2347 | */ | ||
| 2348 | public function setSequenceGeneratorDefinition(array $definition): void | ||
| 2349 | { | ||
| 2350 | if (! isset($definition['sequenceName']) || trim($definition['sequenceName']) === '') { | ||
| 2351 | throw MappingException::missingSequenceName($this->name); | ||
| 2352 | } | ||
| 2353 | |||
| 2354 | if ($definition['sequenceName'][0] === '`') { | ||
| 2355 | $definition['sequenceName'] = trim($definition['sequenceName'], '`'); | ||
| 2356 | $definition['quoted'] = true; | ||
| 2357 | } | ||
| 2358 | |||
| 2359 | if (! isset($definition['allocationSize']) || trim((string) $definition['allocationSize']) === '') { | ||
| 2360 | $definition['allocationSize'] = '1'; | ||
| 2361 | } | ||
| 2362 | |||
| 2363 | if (! isset($definition['initialValue']) || trim((string) $definition['initialValue']) === '') { | ||
| 2364 | $definition['initialValue'] = '1'; | ||
| 2365 | } | ||
| 2366 | |||
| 2367 | $definition['allocationSize'] = (string) $definition['allocationSize']; | ||
| 2368 | $definition['initialValue'] = (string) $definition['initialValue']; | ||
| 2369 | |||
| 2370 | $this->sequenceGeneratorDefinition = $definition; | ||
| 2371 | } | ||
| 2372 | |||
| 2373 | /** | ||
| 2374 | * Sets the version field mapping used for versioning. Sets the default | ||
| 2375 | * value to use depending on the column type. | ||
| 2376 | * | ||
| 2377 | * @psalm-param array<string, mixed> $mapping The version field mapping array. | ||
| 2378 | * | ||
| 2379 | * @throws MappingException | ||
| 2380 | */ | ||
| 2381 | public function setVersionMapping(array &$mapping): void | ||
| 2382 | { | ||
| 2383 | $this->isVersioned = true; | ||
| 2384 | $this->versionField = $mapping['fieldName']; | ||
| 2385 | $this->requiresFetchAfterChange = true; | ||
| 2386 | |||
| 2387 | if (! isset($mapping['default'])) { | ||
| 2388 | if (in_array($mapping['type'], ['integer', 'bigint', 'smallint'], true)) { | ||
| 2389 | $mapping['default'] = 1; | ||
| 2390 | } elseif ($mapping['type'] === 'datetime') { | ||
| 2391 | $mapping['default'] = 'CURRENT_TIMESTAMP'; | ||
| 2392 | } else { | ||
| 2393 | throw MappingException::unsupportedOptimisticLockingType($this->name, $mapping['fieldName'], $mapping['type']); | ||
| 2394 | } | ||
| 2395 | } | ||
| 2396 | } | ||
| 2397 | |||
| 2398 | /** | ||
| 2399 | * Sets whether this class is to be versioned for optimistic locking. | ||
| 2400 | */ | ||
| 2401 | public function setVersioned(bool $bool): void | ||
| 2402 | { | ||
| 2403 | $this->isVersioned = $bool; | ||
| 2404 | |||
| 2405 | if ($bool) { | ||
| 2406 | $this->requiresFetchAfterChange = true; | ||
| 2407 | } | ||
| 2408 | } | ||
| 2409 | |||
| 2410 | /** | ||
| 2411 | * Sets the name of the field that is to be used for versioning if this class is | ||
| 2412 | * versioned for optimistic locking. | ||
| 2413 | */ | ||
| 2414 | public function setVersionField(string|null $versionField): void | ||
| 2415 | { | ||
| 2416 | $this->versionField = $versionField; | ||
| 2417 | } | ||
| 2418 | |||
| 2419 | /** | ||
| 2420 | * Marks this class as read only, no change tracking is applied to it. | ||
| 2421 | */ | ||
| 2422 | public function markReadOnly(): void | ||
| 2423 | { | ||
| 2424 | $this->isReadOnly = true; | ||
| 2425 | } | ||
| 2426 | |||
| 2427 | /** | ||
| 2428 | * {@inheritDoc} | ||
| 2429 | */ | ||
| 2430 | public function getFieldNames(): array | ||
| 2431 | { | ||
| 2432 | return array_keys($this->fieldMappings); | ||
| 2433 | } | ||
| 2434 | |||
| 2435 | /** | ||
| 2436 | * {@inheritDoc} | ||
| 2437 | */ | ||
| 2438 | public function getAssociationNames(): array | ||
| 2439 | { | ||
| 2440 | return array_keys($this->associationMappings); | ||
| 2441 | } | ||
| 2442 | |||
| 2443 | /** | ||
| 2444 | * {@inheritDoc} | ||
| 2445 | * | ||
| 2446 | * @psalm-return class-string | ||
| 2447 | * | ||
| 2448 | * @throws InvalidArgumentException | ||
| 2449 | */ | ||
| 2450 | public function getAssociationTargetClass(string $assocName): string | ||
| 2451 | { | ||
| 2452 | return $this->associationMappings[$assocName]->targetEntity | ||
| 2453 | ?? throw new InvalidArgumentException("Association name expected, '" . $assocName . "' is not an association."); | ||
| 2454 | } | ||
| 2455 | |||
| 2456 | public function getName(): string | ||
| 2457 | { | ||
| 2458 | return $this->name; | ||
| 2459 | } | ||
| 2460 | |||
| 2461 | public function isAssociationInverseSide(string $assocName): bool | ||
| 2462 | { | ||
| 2463 | return isset($this->associationMappings[$assocName]) | ||
| 2464 | && ! $this->associationMappings[$assocName]->isOwningSide(); | ||
| 2465 | } | ||
| 2466 | |||
| 2467 | public function getAssociationMappedByTargetField(string $assocName): string | ||
| 2468 | { | ||
| 2469 | $assoc = $this->getAssociationMapping($assocName); | ||
| 2470 | |||
| 2471 | if (! $assoc instanceof InverseSideMapping) { | ||
| 2472 | throw new LogicException(sprintf( | ||
| 2473 | <<<'EXCEPTION' | ||
| 2474 | Context: Calling %s() with "%s", which is the owning side of an association. | ||
| 2475 | Problem: The owning side of an association has no "mappedBy" field. | ||
| 2476 | Solution: Call %s::isAssociationInverseSide() to check first. | ||
| 2477 | EXCEPTION, | ||
| 2478 | __METHOD__, | ||
| 2479 | $assocName, | ||
| 2480 | self::class, | ||
| 2481 | )); | ||
| 2482 | } | ||
| 2483 | |||
| 2484 | return $assoc->mappedBy; | ||
| 2485 | } | ||
| 2486 | |||
| 2487 | /** | ||
| 2488 | * @param C $className | ||
| 2489 | * | ||
| 2490 | * @return string|null null if and only if the input value is null | ||
| 2491 | * @psalm-return (C is class-string ? class-string : (C is string ? string : null)) | ||
| 2492 | * | ||
| 2493 | * @template C of string|null | ||
| 2494 | */ | ||
| 2495 | public function fullyQualifiedClassName(string|null $className): string|null | ||
| 2496 | { | ||
| 2497 | if ($className === null) { | ||
| 2498 | Deprecation::trigger( | ||
| 2499 | 'doctrine/orm', | ||
| 2500 | 'https://github.com/doctrine/orm/pull/11294', | ||
| 2501 | 'Passing null to %s is deprecated and will not be supported in Doctrine ORM 4.0', | ||
| 2502 | __METHOD__, | ||
| 2503 | ); | ||
| 2504 | |||
| 2505 | return null; | ||
| 2506 | } | ||
| 2507 | |||
| 2508 | if (! str_contains($className, '\\') && $this->namespace) { | ||
| 2509 | return $this->namespace . '\\' . $className; | ||
| 2510 | } | ||
| 2511 | |||
| 2512 | return $className; | ||
| 2513 | } | ||
| 2514 | |||
| 2515 | public function getMetadataValue(string $name): mixed | ||
| 2516 | { | ||
| 2517 | return $this->$name ?? null; | ||
| 2518 | } | ||
| 2519 | |||
| 2520 | /** | ||
| 2521 | * Map Embedded Class | ||
| 2522 | * | ||
| 2523 | * @psalm-param array{ | ||
| 2524 | * fieldName: string, | ||
| 2525 | * class?: class-string, | ||
| 2526 | * declaredField?: string, | ||
| 2527 | * columnPrefix?: string|false|null, | ||
| 2528 | * originalField?: string | ||
| 2529 | * } $mapping | ||
| 2530 | * | ||
| 2531 | * @throws MappingException | ||
| 2532 | */ | ||
| 2533 | public function mapEmbedded(array $mapping): void | ||
| 2534 | { | ||
| 2535 | $this->assertFieldNotMapped($mapping['fieldName']); | ||
| 2536 | |||
| 2537 | if (! isset($mapping['class']) && $this->isTypedProperty($mapping['fieldName'])) { | ||
| 2538 | $type = $this->reflClass->getProperty($mapping['fieldName'])->getType(); | ||
| 2539 | if ($type instanceof ReflectionNamedType) { | ||
| 2540 | $mapping['class'] = $type->getName(); | ||
| 2541 | } | ||
| 2542 | } | ||
| 2543 | |||
| 2544 | if (! (isset($mapping['class']) && $mapping['class'])) { | ||
| 2545 | throw MappingException::missingEmbeddedClass($mapping['fieldName']); | ||
| 2546 | } | ||
| 2547 | |||
| 2548 | $this->embeddedClasses[$mapping['fieldName']] = EmbeddedClassMapping::fromMappingArray([ | ||
| 2549 | 'class' => $this->fullyQualifiedClassName($mapping['class']), | ||
| 2550 | 'columnPrefix' => $mapping['columnPrefix'] ?? null, | ||
| 2551 | 'declaredField' => $mapping['declaredField'] ?? null, | ||
| 2552 | 'originalField' => $mapping['originalField'] ?? null, | ||
| 2553 | ]); | ||
| 2554 | } | ||
| 2555 | |||
| 2556 | /** | ||
| 2557 | * Inline the embeddable class | ||
| 2558 | */ | ||
| 2559 | public function inlineEmbeddable(string $property, ClassMetadata $embeddable): void | ||
| 2560 | { | ||
| 2561 | foreach ($embeddable->fieldMappings as $originalFieldMapping) { | ||
| 2562 | $fieldMapping = (array) $originalFieldMapping; | ||
| 2563 | $fieldMapping['originalClass'] ??= $embeddable->name; | ||
| 2564 | $fieldMapping['declaredField'] = isset($fieldMapping['declaredField']) | ||
| 2565 | ? $property . '.' . $fieldMapping['declaredField'] | ||
| 2566 | : $property; | ||
| 2567 | $fieldMapping['originalField'] ??= $fieldMapping['fieldName']; | ||
| 2568 | $fieldMapping['fieldName'] = $property . '.' . $fieldMapping['fieldName']; | ||
| 2569 | |||
| 2570 | if (! empty($this->embeddedClasses[$property]->columnPrefix)) { | ||
| 2571 | $fieldMapping['columnName'] = $this->embeddedClasses[$property]->columnPrefix . $fieldMapping['columnName']; | ||
| 2572 | } elseif ($this->embeddedClasses[$property]->columnPrefix !== false) { | ||
| 2573 | assert($this->reflClass !== null); | ||
| 2574 | assert($embeddable->reflClass !== null); | ||
| 2575 | $fieldMapping['columnName'] = $this->namingStrategy | ||
| 2576 | ->embeddedFieldToColumnName( | ||
| 2577 | $property, | ||
| 2578 | $fieldMapping['columnName'], | ||
| 2579 | $this->reflClass->name, | ||
| 2580 | $embeddable->reflClass->name, | ||
| 2581 | ); | ||
| 2582 | } | ||
| 2583 | |||
| 2584 | $this->mapField($fieldMapping); | ||
| 2585 | } | ||
| 2586 | } | ||
| 2587 | |||
| 2588 | /** @throws MappingException */ | ||
| 2589 | private function assertFieldNotMapped(string $fieldName): void | ||
| 2590 | { | ||
| 2591 | if ( | ||
| 2592 | isset($this->fieldMappings[$fieldName]) || | ||
| 2593 | isset($this->associationMappings[$fieldName]) || | ||
| 2594 | isset($this->embeddedClasses[$fieldName]) | ||
| 2595 | ) { | ||
| 2596 | throw MappingException::duplicateFieldMapping($this->name, $fieldName); | ||
| 2597 | } | ||
| 2598 | } | ||
| 2599 | |||
| 2600 | /** | ||
| 2601 | * Gets the sequence name based on class metadata. | ||
| 2602 | * | ||
| 2603 | * @todo Sequence names should be computed in DBAL depending on the platform | ||
| 2604 | */ | ||
| 2605 | public function getSequenceName(AbstractPlatform $platform): string | ||
| 2606 | { | ||
| 2607 | $sequencePrefix = $this->getSequencePrefix($platform); | ||
| 2608 | $columnName = $this->getSingleIdentifierColumnName(); | ||
| 2609 | |||
| 2610 | return $sequencePrefix . '_' . $columnName . '_seq'; | ||
| 2611 | } | ||
| 2612 | |||
| 2613 | /** | ||
| 2614 | * Gets the sequence name prefix based on class metadata. | ||
| 2615 | * | ||
| 2616 | * @todo Sequence names should be computed in DBAL depending on the platform | ||
| 2617 | */ | ||
| 2618 | public function getSequencePrefix(AbstractPlatform $platform): string | ||
| 2619 | { | ||
| 2620 | $tableName = $this->getTableName(); | ||
| 2621 | $sequencePrefix = $tableName; | ||
| 2622 | |||
| 2623 | // Prepend the schema name to the table name if there is one | ||
| 2624 | $schemaName = $this->getSchemaName(); | ||
| 2625 | if ($schemaName) { | ||
| 2626 | $sequencePrefix = $schemaName . '.' . $tableName; | ||
| 2627 | } | ||
| 2628 | |||
| 2629 | return $sequencePrefix; | ||
| 2630 | } | ||
| 2631 | |||
| 2632 | /** @psalm-param class-string $class */ | ||
| 2633 | private function getAccessibleProperty(ReflectionService $reflService, string $class, string $field): ReflectionProperty|null | ||
| 2634 | { | ||
| 2635 | $reflectionProperty = $reflService->getAccessibleProperty($class, $field); | ||
| 2636 | if ($reflectionProperty?->isReadOnly()) { | ||
| 2637 | $declaringClass = $reflectionProperty->class; | ||
| 2638 | if ($declaringClass !== $class) { | ||
| 2639 | $reflectionProperty = $reflService->getAccessibleProperty($declaringClass, $field); | ||
| 2640 | } | ||
| 2641 | |||
| 2642 | if ($reflectionProperty !== null) { | ||
| 2643 | $reflectionProperty = new ReflectionReadonlyProperty($reflectionProperty); | ||
| 2644 | } | ||
| 2645 | } | ||
| 2646 | |||
| 2647 | return $reflectionProperty; | ||
| 2648 | } | ||
| 2649 | } | ||
diff --git a/vendor/doctrine/orm/src/Mapping/ClassMetadataFactory.php b/vendor/doctrine/orm/src/Mapping/ClassMetadataFactory.php new file mode 100644 index 0000000..a3a4701 --- /dev/null +++ b/vendor/doctrine/orm/src/Mapping/ClassMetadataFactory.php | |||
| @@ -0,0 +1,729 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Mapping; | ||
| 6 | |||
| 7 | use Doctrine\Common\EventManager; | ||
| 8 | use Doctrine\DBAL\Platforms; | ||
| 9 | use Doctrine\DBAL\Platforms\AbstractPlatform; | ||
| 10 | use Doctrine\Deprecations\Deprecation; | ||
| 11 | use Doctrine\ORM\EntityManagerInterface; | ||
| 12 | use Doctrine\ORM\Event\LoadClassMetadataEventArgs; | ||
| 13 | use Doctrine\ORM\Event\OnClassMetadataNotFoundEventArgs; | ||
| 14 | use Doctrine\ORM\Events; | ||
| 15 | use Doctrine\ORM\Exception\ORMException; | ||
| 16 | use Doctrine\ORM\Id\AssignedGenerator; | ||
| 17 | use Doctrine\ORM\Id\BigIntegerIdentityGenerator; | ||
| 18 | use Doctrine\ORM\Id\IdentityGenerator; | ||
| 19 | use Doctrine\ORM\Id\SequenceGenerator; | ||
| 20 | use Doctrine\ORM\Mapping\Exception\InvalidCustomGenerator; | ||
| 21 | use Doctrine\ORM\Mapping\Exception\UnknownGeneratorType; | ||
| 22 | use Doctrine\ORM\Proxy\DefaultProxyClassNameResolver; | ||
| 23 | use Doctrine\Persistence\Mapping\AbstractClassMetadataFactory; | ||
| 24 | use Doctrine\Persistence\Mapping\ClassMetadata as ClassMetadataInterface; | ||
| 25 | use Doctrine\Persistence\Mapping\Driver\MappingDriver; | ||
| 26 | use Doctrine\Persistence\Mapping\ReflectionService; | ||
| 27 | use ReflectionClass; | ||
| 28 | use ReflectionException; | ||
| 29 | |||
| 30 | use function assert; | ||
| 31 | use function class_exists; | ||
| 32 | use function count; | ||
| 33 | use function end; | ||
| 34 | use function explode; | ||
| 35 | use function in_array; | ||
| 36 | use function is_a; | ||
| 37 | use function is_subclass_of; | ||
| 38 | use function method_exists; | ||
| 39 | use function str_contains; | ||
| 40 | use function strlen; | ||
| 41 | use function strtolower; | ||
| 42 | use function substr; | ||
| 43 | |||
| 44 | /** | ||
| 45 | * The ClassMetadataFactory is used to create ClassMetadata objects that contain all the | ||
| 46 | * metadata mapping information of a class which describes how a class should be mapped | ||
| 47 | * to a relational database. | ||
| 48 | * | ||
| 49 | * @extends AbstractClassMetadataFactory<ClassMetadata> | ||
| 50 | */ | ||
| 51 | class ClassMetadataFactory extends AbstractClassMetadataFactory | ||
| 52 | { | ||
| 53 | private EntityManagerInterface|null $em = null; | ||
| 54 | private AbstractPlatform|null $targetPlatform = null; | ||
| 55 | private MappingDriver|null $driver = null; | ||
| 56 | private EventManager|null $evm = null; | ||
| 57 | |||
| 58 | /** @var mixed[] */ | ||
| 59 | private array $embeddablesActiveNesting = []; | ||
| 60 | |||
| 61 | private const NON_IDENTITY_DEFAULT_STRATEGY = [ | ||
| 62 | Platforms\OraclePlatform::class => ClassMetadata::GENERATOR_TYPE_SEQUENCE, | ||
| 63 | ]; | ||
| 64 | |||
| 65 | public function setEntityManager(EntityManagerInterface $em): void | ||
| 66 | { | ||
| 67 | parent::setProxyClassNameResolver(new DefaultProxyClassNameResolver()); | ||
| 68 | |||
| 69 | $this->em = $em; | ||
| 70 | } | ||
| 71 | |||
| 72 | /** | ||
| 73 | * @param A $maybeOwningSide | ||
| 74 | * | ||
| 75 | * @return (A is ManyToManyAssociationMapping ? ManyToManyOwningSideMapping : ( | ||
| 76 | * A is OneToOneAssociationMapping ? OneToOneOwningSideMapping : ( | ||
| 77 | * A is OneToManyAssociationMapping ? ManyToOneAssociationMapping : ( | ||
| 78 | * A is ManyToOneAssociationMapping ? ManyToOneAssociationMapping : | ||
| 79 | * ManyToManyOwningSideMapping|OneToOneOwningSideMapping|ManyToOneAssociationMapping | ||
| 80 | * )))) | ||
| 81 | * | ||
| 82 | * @template A of AssociationMapping | ||
| 83 | */ | ||
| 84 | final public function getOwningSide(AssociationMapping $maybeOwningSide): OwningSideMapping | ||
| 85 | { | ||
| 86 | if ($maybeOwningSide instanceof OwningSideMapping) { | ||
| 87 | assert($maybeOwningSide instanceof ManyToManyOwningSideMapping || | ||
| 88 | $maybeOwningSide instanceof OneToOneOwningSideMapping || | ||
| 89 | $maybeOwningSide instanceof ManyToOneAssociationMapping); | ||
| 90 | |||
| 91 | return $maybeOwningSide; | ||
| 92 | } | ||
| 93 | |||
| 94 | assert($maybeOwningSide instanceof InverseSideMapping); | ||
| 95 | |||
| 96 | $owningSide = $this->getMetadataFor($maybeOwningSide->targetEntity) | ||
| 97 | ->associationMappings[$maybeOwningSide->mappedBy]; | ||
| 98 | |||
| 99 | assert($owningSide instanceof ManyToManyOwningSideMapping || | ||
| 100 | $owningSide instanceof OneToOneOwningSideMapping || | ||
| 101 | $owningSide instanceof ManyToOneAssociationMapping); | ||
| 102 | |||
| 103 | return $owningSide; | ||
| 104 | } | ||
| 105 | |||
| 106 | protected function initialize(): void | ||
| 107 | { | ||
| 108 | $this->driver = $this->em->getConfiguration()->getMetadataDriverImpl(); | ||
| 109 | $this->evm = $this->em->getEventManager(); | ||
| 110 | $this->initialized = true; | ||
| 111 | } | ||
| 112 | |||
| 113 | protected function onNotFoundMetadata(string $className): ClassMetadata|null | ||
| 114 | { | ||
| 115 | if (! $this->evm->hasListeners(Events::onClassMetadataNotFound)) { | ||
| 116 | return null; | ||
| 117 | } | ||
| 118 | |||
| 119 | $eventArgs = new OnClassMetadataNotFoundEventArgs($className, $this->em); | ||
| 120 | |||
| 121 | $this->evm->dispatchEvent(Events::onClassMetadataNotFound, $eventArgs); | ||
| 122 | $classMetadata = $eventArgs->getFoundMetadata(); | ||
| 123 | assert($classMetadata instanceof ClassMetadata || $classMetadata === null); | ||
| 124 | |||
| 125 | return $classMetadata; | ||
| 126 | } | ||
| 127 | |||
| 128 | /** | ||
| 129 | * {@inheritDoc} | ||
| 130 | */ | ||
| 131 | protected function doLoadMetadata( | ||
| 132 | ClassMetadataInterface $class, | ||
| 133 | ClassMetadataInterface|null $parent, | ||
| 134 | bool $rootEntityFound, | ||
| 135 | array $nonSuperclassParents, | ||
| 136 | ): void { | ||
| 137 | if ($parent) { | ||
| 138 | $class->setInheritanceType($parent->inheritanceType); | ||
| 139 | $class->setDiscriminatorColumn($parent->discriminatorColumn === null ? null : clone $parent->discriminatorColumn); | ||
| 140 | $class->setIdGeneratorType($parent->generatorType); | ||
| 141 | $this->addInheritedFields($class, $parent); | ||
| 142 | $this->addInheritedRelations($class, $parent); | ||
| 143 | $this->addInheritedEmbeddedClasses($class, $parent); | ||
| 144 | $class->setIdentifier($parent->identifier); | ||
| 145 | $class->setVersioned($parent->isVersioned); | ||
| 146 | $class->setVersionField($parent->versionField); | ||
| 147 | $class->setDiscriminatorMap($parent->discriminatorMap); | ||
| 148 | $class->addSubClasses($parent->subClasses); | ||
| 149 | $class->setLifecycleCallbacks($parent->lifecycleCallbacks); | ||
| 150 | $class->setChangeTrackingPolicy($parent->changeTrackingPolicy); | ||
| 151 | |||
| 152 | if (! empty($parent->customGeneratorDefinition)) { | ||
| 153 | $class->setCustomGeneratorDefinition($parent->customGeneratorDefinition); | ||
| 154 | } | ||
| 155 | |||
| 156 | if ($parent->isMappedSuperclass) { | ||
| 157 | $class->setCustomRepositoryClass($parent->customRepositoryClassName); | ||
| 158 | } | ||
| 159 | } | ||
| 160 | |||
| 161 | // Invoke driver | ||
| 162 | try { | ||
| 163 | $this->driver->loadMetadataForClass($class->getName(), $class); | ||
| 164 | } catch (ReflectionException $e) { | ||
| 165 | throw MappingException::reflectionFailure($class->getName(), $e); | ||
| 166 | } | ||
| 167 | |||
| 168 | // If this class has a parent the id generator strategy is inherited. | ||
| 169 | // However this is only true if the hierarchy of parents contains the root entity, | ||
| 170 | // if it consists of mapped superclasses these don't necessarily include the id field. | ||
| 171 | if ($parent && $rootEntityFound) { | ||
| 172 | $this->inheritIdGeneratorMapping($class, $parent); | ||
| 173 | } else { | ||
| 174 | $this->completeIdGeneratorMapping($class); | ||
| 175 | } | ||
| 176 | |||
| 177 | if (! $class->isMappedSuperclass) { | ||
| 178 | if ($rootEntityFound && $class->isInheritanceTypeNone()) { | ||
| 179 | throw MappingException::missingInheritanceTypeDeclaration(end($nonSuperclassParents), $class->name); | ||
| 180 | } | ||
| 181 | |||
| 182 | foreach ($class->embeddedClasses as $property => $embeddableClass) { | ||
| 183 | if (isset($embeddableClass->inherited)) { | ||
| 184 | continue; | ||
| 185 | } | ||
| 186 | |||
| 187 | if (isset($this->embeddablesActiveNesting[$embeddableClass->class])) { | ||
| 188 | throw MappingException::infiniteEmbeddableNesting($class->name, $property); | ||
| 189 | } | ||
| 190 | |||
| 191 | $this->embeddablesActiveNesting[$class->name] = true; | ||
| 192 | |||
| 193 | $embeddableMetadata = $this->getMetadataFor($embeddableClass->class); | ||
| 194 | |||
| 195 | if ($embeddableMetadata->isEmbeddedClass) { | ||
| 196 | $this->addNestedEmbeddedClasses($embeddableMetadata, $class, $property); | ||
| 197 | } | ||
| 198 | |||
| 199 | $identifier = $embeddableMetadata->getIdentifier(); | ||
| 200 | |||
| 201 | if (! empty($identifier)) { | ||
| 202 | $this->inheritIdGeneratorMapping($class, $embeddableMetadata); | ||
| 203 | } | ||
| 204 | |||
| 205 | $class->inlineEmbeddable($property, $embeddableMetadata); | ||
| 206 | |||
| 207 | unset($this->embeddablesActiveNesting[$class->name]); | ||
| 208 | } | ||
| 209 | } | ||
| 210 | |||
| 211 | if ($parent) { | ||
| 212 | if ($parent->isInheritanceTypeSingleTable()) { | ||
| 213 | $class->setPrimaryTable($parent->table); | ||
| 214 | } | ||
| 215 | |||
| 216 | $this->addInheritedIndexes($class, $parent); | ||
| 217 | |||
| 218 | if ($parent->cache) { | ||
| 219 | $class->cache = $parent->cache; | ||
| 220 | } | ||
| 221 | |||
| 222 | if ($parent->containsForeignIdentifier) { | ||
| 223 | $class->containsForeignIdentifier = true; | ||
| 224 | } | ||
| 225 | |||
| 226 | if ($parent->containsEnumIdentifier) { | ||
| 227 | $class->containsEnumIdentifier = true; | ||
| 228 | } | ||
| 229 | |||
| 230 | if (! empty($parent->entityListeners) && empty($class->entityListeners)) { | ||
| 231 | $class->entityListeners = $parent->entityListeners; | ||
| 232 | } | ||
| 233 | } | ||
| 234 | |||
| 235 | $class->setParentClasses($nonSuperclassParents); | ||
| 236 | |||
| 237 | if ($class->isRootEntity() && ! $class->isInheritanceTypeNone() && ! $class->discriminatorMap) { | ||
| 238 | $this->addDefaultDiscriminatorMap($class); | ||
| 239 | } | ||
| 240 | |||
| 241 | // During the following event, there may also be updates to the discriminator map as per GH-1257/GH-8402. | ||
| 242 | // So, we must not discover the missing subclasses before that. | ||
| 243 | |||
| 244 | if ($this->evm->hasListeners(Events::loadClassMetadata)) { | ||
| 245 | $eventArgs = new LoadClassMetadataEventArgs($class, $this->em); | ||
| 246 | $this->evm->dispatchEvent(Events::loadClassMetadata, $eventArgs); | ||
| 247 | } | ||
| 248 | |||
| 249 | $this->findAbstractEntityClassesNotListedInDiscriminatorMap($class); | ||
| 250 | |||
| 251 | $this->validateRuntimeMetadata($class, $parent); | ||
| 252 | } | ||
| 253 | |||
| 254 | /** | ||
| 255 | * Validate runtime metadata is correctly defined. | ||
| 256 | * | ||
| 257 | * @throws MappingException | ||
| 258 | */ | ||
| 259 | protected function validateRuntimeMetadata(ClassMetadata $class, ClassMetadataInterface|null $parent): void | ||
| 260 | { | ||
| 261 | if (! $class->reflClass) { | ||
| 262 | // only validate if there is a reflection class instance | ||
| 263 | return; | ||
| 264 | } | ||
| 265 | |||
| 266 | $class->validateIdentifier(); | ||
| 267 | $class->validateAssociations(); | ||
| 268 | $class->validateLifecycleCallbacks($this->getReflectionService()); | ||
| 269 | |||
| 270 | // verify inheritance | ||
| 271 | if (! $class->isMappedSuperclass && ! $class->isInheritanceTypeNone()) { | ||
| 272 | if (! $parent) { | ||
| 273 | if (count($class->discriminatorMap) === 0) { | ||
| 274 | throw MappingException::missingDiscriminatorMap($class->name); | ||
| 275 | } | ||
| 276 | |||
| 277 | if (! $class->discriminatorColumn) { | ||
| 278 | throw MappingException::missingDiscriminatorColumn($class->name); | ||
| 279 | } | ||
| 280 | |||
| 281 | foreach ($class->subClasses as $subClass) { | ||
| 282 | if ((new ReflectionClass($subClass))->name !== $subClass) { | ||
| 283 | throw MappingException::invalidClassInDiscriminatorMap($subClass, $class->name); | ||
| 284 | } | ||
| 285 | } | ||
| 286 | } else { | ||
| 287 | assert($parent instanceof ClassMetadata); // https://github.com/doctrine/orm/issues/8746 | ||
| 288 | if ( | ||
| 289 | ! $class->reflClass->isAbstract() | ||
| 290 | && ! in_array($class->name, $class->discriminatorMap, true) | ||
| 291 | ) { | ||
| 292 | throw MappingException::mappedClassNotPartOfDiscriminatorMap($class->name, $class->rootEntityName); | ||
| 293 | } | ||
| 294 | } | ||
| 295 | } elseif ($class->isMappedSuperclass && $class->name === $class->rootEntityName && (count($class->discriminatorMap) || $class->discriminatorColumn)) { | ||
| 296 | // second condition is necessary for mapped superclasses in the middle of an inheritance hierarchy | ||
| 297 | throw MappingException::noInheritanceOnMappedSuperClass($class->name); | ||
| 298 | } | ||
| 299 | } | ||
| 300 | |||
| 301 | protected function newClassMetadataInstance(string $className): ClassMetadata | ||
| 302 | { | ||
| 303 | return new ClassMetadata( | ||
| 304 | $className, | ||
| 305 | $this->em->getConfiguration()->getNamingStrategy(), | ||
| 306 | $this->em->getConfiguration()->getTypedFieldMapper(), | ||
| 307 | ); | ||
| 308 | } | ||
| 309 | |||
| 310 | /** | ||
| 311 | * Adds a default discriminator map if no one is given | ||
| 312 | * | ||
| 313 | * If an entity is of any inheritance type and does not contain a | ||
| 314 | * discriminator map, then the map is generated automatically. This process | ||
| 315 | * is expensive computation wise. | ||
| 316 | * | ||
| 317 | * The automatically generated discriminator map contains the lowercase short name of | ||
| 318 | * each class as key. | ||
| 319 | * | ||
| 320 | * @throws MappingException | ||
| 321 | */ | ||
| 322 | private function addDefaultDiscriminatorMap(ClassMetadata $class): void | ||
| 323 | { | ||
| 324 | $allClasses = $this->driver->getAllClassNames(); | ||
| 325 | $fqcn = $class->getName(); | ||
| 326 | $map = [$this->getShortName($class->name) => $fqcn]; | ||
| 327 | |||
| 328 | $duplicates = []; | ||
| 329 | foreach ($allClasses as $subClassCandidate) { | ||
| 330 | if (is_subclass_of($subClassCandidate, $fqcn)) { | ||
| 331 | $shortName = $this->getShortName($subClassCandidate); | ||
| 332 | |||
| 333 | if (isset($map[$shortName])) { | ||
| 334 | $duplicates[] = $shortName; | ||
| 335 | } | ||
| 336 | |||
| 337 | $map[$shortName] = $subClassCandidate; | ||
| 338 | } | ||
| 339 | } | ||
| 340 | |||
| 341 | if ($duplicates) { | ||
| 342 | throw MappingException::duplicateDiscriminatorEntry($class->name, $duplicates, $map); | ||
| 343 | } | ||
| 344 | |||
| 345 | $class->setDiscriminatorMap($map); | ||
| 346 | } | ||
| 347 | |||
| 348 | private function findAbstractEntityClassesNotListedInDiscriminatorMap(ClassMetadata $rootEntityClass): void | ||
| 349 | { | ||
| 350 | // Only root classes in inheritance hierarchies need contain a discriminator map, | ||
| 351 | // so skip for other classes. | ||
| 352 | if (! $rootEntityClass->isRootEntity() || $rootEntityClass->isInheritanceTypeNone()) { | ||
| 353 | return; | ||
| 354 | } | ||
| 355 | |||
| 356 | $processedClasses = [$rootEntityClass->name => true]; | ||
| 357 | foreach ($rootEntityClass->subClasses as $knownSubClass) { | ||
| 358 | $processedClasses[$knownSubClass] = true; | ||
| 359 | } | ||
| 360 | |||
| 361 | foreach ($rootEntityClass->discriminatorMap as $declaredClassName) { | ||
| 362 | // This fetches non-transient parent classes only | ||
| 363 | $parentClasses = $this->getParentClasses($declaredClassName); | ||
| 364 | |||
| 365 | foreach ($parentClasses as $parentClass) { | ||
| 366 | if (isset($processedClasses[$parentClass])) { | ||
| 367 | continue; | ||
| 368 | } | ||
| 369 | |||
| 370 | $processedClasses[$parentClass] = true; | ||
| 371 | |||
| 372 | // All non-abstract entity classes must be listed in the discriminator map, and | ||
| 373 | // this will be validated/enforced at runtime (possibly at a later time, when the | ||
| 374 | // subclass is loaded, but anyways). Also, subclasses is about entity classes only. | ||
| 375 | // That means we can ignore non-abstract classes here. The (expensive) driver | ||
| 376 | // check for mapped superclasses need only be run for abstract candidate classes. | ||
| 377 | if (! (new ReflectionClass($parentClass))->isAbstract() || $this->peekIfIsMappedSuperclass($parentClass)) { | ||
| 378 | continue; | ||
| 379 | } | ||
| 380 | |||
| 381 | // We have found a non-transient, non-mapped-superclass = an entity class (possibly abstract, but that does not matter) | ||
| 382 | $rootEntityClass->addSubClass($parentClass); | ||
| 383 | } | ||
| 384 | } | ||
| 385 | } | ||
| 386 | |||
| 387 | /** @param class-string $className */ | ||
| 388 | private function peekIfIsMappedSuperclass(string $className): bool | ||
| 389 | { | ||
| 390 | $reflService = $this->getReflectionService(); | ||
| 391 | $class = $this->newClassMetadataInstance($className); | ||
| 392 | $this->initializeReflection($class, $reflService); | ||
| 393 | |||
| 394 | $this->getDriver()->loadMetadataForClass($className, $class); | ||
| 395 | |||
| 396 | return $class->isMappedSuperclass; | ||
| 397 | } | ||
| 398 | |||
| 399 | /** | ||
| 400 | * Gets the lower-case short name of a class. | ||
| 401 | * | ||
| 402 | * @psalm-param class-string $className | ||
| 403 | */ | ||
| 404 | private function getShortName(string $className): string | ||
| 405 | { | ||
| 406 | if (! str_contains($className, '\\')) { | ||
| 407 | return strtolower($className); | ||
| 408 | } | ||
| 409 | |||
| 410 | $parts = explode('\\', $className); | ||
| 411 | |||
| 412 | return strtolower(end($parts)); | ||
| 413 | } | ||
| 414 | |||
| 415 | /** | ||
| 416 | * Puts the `inherited` and `declared` values into mapping information for fields, associations | ||
| 417 | * and embedded classes. | ||
| 418 | */ | ||
| 419 | private function addMappingInheritanceInformation( | ||
| 420 | AssociationMapping|EmbeddedClassMapping|FieldMapping $mapping, | ||
| 421 | ClassMetadata $parentClass, | ||
| 422 | ): void { | ||
| 423 | if (! isset($mapping->inherited) && ! $parentClass->isMappedSuperclass) { | ||
| 424 | $mapping->inherited = $parentClass->name; | ||
| 425 | } | ||
| 426 | |||
| 427 | if (! isset($mapping->declared)) { | ||
| 428 | $mapping->declared = $parentClass->name; | ||
| 429 | } | ||
| 430 | } | ||
| 431 | |||
| 432 | /** | ||
| 433 | * Adds inherited fields to the subclass mapping. | ||
| 434 | */ | ||
| 435 | private function addInheritedFields(ClassMetadata $subClass, ClassMetadata $parentClass): void | ||
| 436 | { | ||
| 437 | foreach ($parentClass->fieldMappings as $mapping) { | ||
| 438 | $subClassMapping = clone $mapping; | ||
| 439 | $this->addMappingInheritanceInformation($subClassMapping, $parentClass); | ||
| 440 | $subClass->addInheritedFieldMapping($subClassMapping); | ||
| 441 | } | ||
| 442 | |||
| 443 | foreach ($parentClass->reflFields as $name => $field) { | ||
| 444 | $subClass->reflFields[$name] = $field; | ||
| 445 | } | ||
| 446 | } | ||
| 447 | |||
| 448 | /** | ||
| 449 | * Adds inherited association mappings to the subclass mapping. | ||
| 450 | * | ||
| 451 | * @throws MappingException | ||
| 452 | */ | ||
| 453 | private function addInheritedRelations(ClassMetadata $subClass, ClassMetadata $parentClass): void | ||
| 454 | { | ||
| 455 | foreach ($parentClass->associationMappings as $field => $mapping) { | ||
| 456 | $subClassMapping = clone $mapping; | ||
| 457 | $this->addMappingInheritanceInformation($subClassMapping, $parentClass); | ||
| 458 | // When the class inheriting the relation ($subClass) is the first entity class since the | ||
| 459 | // relation has been defined in a mapped superclass (or in a chain | ||
| 460 | // of mapped superclasses) above, then declare this current entity class as the source of | ||
| 461 | // the relationship. | ||
| 462 | // According to the definitions given in https://github.com/doctrine/orm/pull/10396/, | ||
| 463 | // this is the case <=> ! isset($mapping['inherited']). | ||
| 464 | if (! isset($subClassMapping->inherited)) { | ||
| 465 | $subClassMapping->sourceEntity = $subClass->name; | ||
| 466 | } | ||
| 467 | |||
| 468 | $subClass->addInheritedAssociationMapping($subClassMapping); | ||
| 469 | } | ||
| 470 | } | ||
| 471 | |||
| 472 | private function addInheritedEmbeddedClasses(ClassMetadata $subClass, ClassMetadata $parentClass): void | ||
| 473 | { | ||
| 474 | foreach ($parentClass->embeddedClasses as $field => $embeddedClass) { | ||
| 475 | $subClassMapping = clone $embeddedClass; | ||
| 476 | $this->addMappingInheritanceInformation($subClassMapping, $parentClass); | ||
| 477 | $subClass->embeddedClasses[$field] = $subClassMapping; | ||
| 478 | } | ||
| 479 | } | ||
| 480 | |||
| 481 | /** | ||
| 482 | * Adds nested embedded classes metadata to a parent class. | ||
| 483 | * | ||
| 484 | * @param ClassMetadata $subClass Sub embedded class metadata to add nested embedded classes metadata from. | ||
| 485 | * @param ClassMetadata $parentClass Parent class to add nested embedded classes metadata to. | ||
| 486 | * @param string $prefix Embedded classes' prefix to use for nested embedded classes field names. | ||
| 487 | */ | ||
| 488 | private function addNestedEmbeddedClasses( | ||
| 489 | ClassMetadata $subClass, | ||
| 490 | ClassMetadata $parentClass, | ||
| 491 | string $prefix, | ||
| 492 | ): void { | ||
| 493 | foreach ($subClass->embeddedClasses as $property => $embeddableClass) { | ||
| 494 | if (isset($embeddableClass->inherited)) { | ||
| 495 | continue; | ||
| 496 | } | ||
| 497 | |||
| 498 | $embeddableMetadata = $this->getMetadataFor($embeddableClass->class); | ||
| 499 | |||
| 500 | $parentClass->mapEmbedded( | ||
| 501 | [ | ||
| 502 | 'fieldName' => $prefix . '.' . $property, | ||
| 503 | 'class' => $embeddableMetadata->name, | ||
| 504 | 'columnPrefix' => $embeddableClass->columnPrefix, | ||
| 505 | 'declaredField' => $embeddableClass->declaredField | ||
| 506 | ? $prefix . '.' . $embeddableClass->declaredField | ||
| 507 | : $prefix, | ||
| 508 | 'originalField' => $embeddableClass->originalField ?: $property, | ||
| 509 | ], | ||
| 510 | ); | ||
| 511 | } | ||
| 512 | } | ||
| 513 | |||
| 514 | /** | ||
| 515 | * Copy the table indices from the parent class superclass to the child class | ||
| 516 | */ | ||
| 517 | private function addInheritedIndexes(ClassMetadata $subClass, ClassMetadata $parentClass): void | ||
| 518 | { | ||
| 519 | if (! $parentClass->isMappedSuperclass) { | ||
| 520 | return; | ||
| 521 | } | ||
| 522 | |||
| 523 | foreach (['uniqueConstraints', 'indexes'] as $indexType) { | ||
| 524 | if (isset($parentClass->table[$indexType])) { | ||
| 525 | foreach ($parentClass->table[$indexType] as $indexName => $index) { | ||
| 526 | if (isset($subClass->table[$indexType][$indexName])) { | ||
| 527 | continue; // Let the inheriting table override indices | ||
| 528 | } | ||
| 529 | |||
| 530 | $subClass->table[$indexType][$indexName] = $index; | ||
| 531 | } | ||
| 532 | } | ||
| 533 | } | ||
| 534 | } | ||
| 535 | |||
| 536 | /** | ||
| 537 | * Completes the ID generator mapping. If "auto" is specified we choose the generator | ||
| 538 | * most appropriate for the targeted database platform. | ||
| 539 | * | ||
| 540 | * @throws ORMException | ||
| 541 | */ | ||
| 542 | private function completeIdGeneratorMapping(ClassMetadata $class): void | ||
| 543 | { | ||
| 544 | $idGenType = $class->generatorType; | ||
| 545 | if ($idGenType === ClassMetadata::GENERATOR_TYPE_AUTO) { | ||
| 546 | $class->setIdGeneratorType($this->determineIdGeneratorStrategy($this->getTargetPlatform())); | ||
| 547 | } | ||
| 548 | |||
| 549 | // Create & assign an appropriate ID generator instance | ||
| 550 | switch ($class->generatorType) { | ||
| 551 | case ClassMetadata::GENERATOR_TYPE_IDENTITY: | ||
| 552 | $sequenceName = null; | ||
| 553 | $fieldName = $class->identifier ? $class->getSingleIdentifierFieldName() : null; | ||
| 554 | $platform = $this->getTargetPlatform(); | ||
| 555 | |||
| 556 | $generator = $fieldName && $class->fieldMappings[$fieldName]->type === 'bigint' | ||
| 557 | ? new BigIntegerIdentityGenerator() | ||
| 558 | : new IdentityGenerator(); | ||
| 559 | |||
| 560 | $class->setIdGenerator($generator); | ||
| 561 | |||
| 562 | break; | ||
| 563 | |||
| 564 | case ClassMetadata::GENERATOR_TYPE_SEQUENCE: | ||
| 565 | // If there is no sequence definition yet, create a default definition | ||
| 566 | $definition = $class->sequenceGeneratorDefinition; | ||
| 567 | |||
| 568 | if (! $definition) { | ||
| 569 | $fieldName = $class->getSingleIdentifierFieldName(); | ||
| 570 | $sequenceName = $class->getSequenceName($this->getTargetPlatform()); | ||
| 571 | $quoted = isset($class->fieldMappings[$fieldName]->quoted) || isset($class->table['quoted']); | ||
| 572 | |||
| 573 | $definition = [ | ||
| 574 | 'sequenceName' => $this->truncateSequenceName($sequenceName), | ||
| 575 | 'allocationSize' => 1, | ||
| 576 | 'initialValue' => 1, | ||
| 577 | ]; | ||
| 578 | |||
| 579 | if ($quoted) { | ||
| 580 | $definition['quoted'] = true; | ||
| 581 | } | ||
| 582 | |||
| 583 | $class->setSequenceGeneratorDefinition($definition); | ||
| 584 | } | ||
| 585 | |||
| 586 | $sequenceGenerator = new SequenceGenerator( | ||
| 587 | $this->em->getConfiguration()->getQuoteStrategy()->getSequenceName($definition, $class, $this->getTargetPlatform()), | ||
| 588 | (int) $definition['allocationSize'], | ||
| 589 | ); | ||
| 590 | $class->setIdGenerator($sequenceGenerator); | ||
| 591 | break; | ||
| 592 | |||
| 593 | case ClassMetadata::GENERATOR_TYPE_NONE: | ||
| 594 | $class->setIdGenerator(new AssignedGenerator()); | ||
| 595 | break; | ||
| 596 | |||
| 597 | case ClassMetadata::GENERATOR_TYPE_CUSTOM: | ||
| 598 | $definition = $class->customGeneratorDefinition; | ||
| 599 | if ($definition === null) { | ||
| 600 | throw InvalidCustomGenerator::onClassNotConfigured(); | ||
| 601 | } | ||
| 602 | |||
| 603 | if (! class_exists($definition['class'])) { | ||
| 604 | throw InvalidCustomGenerator::onMissingClass($definition); | ||
| 605 | } | ||
| 606 | |||
| 607 | $class->setIdGenerator(new $definition['class']()); | ||
| 608 | break; | ||
| 609 | |||
| 610 | default: | ||
| 611 | throw UnknownGeneratorType::create($class->generatorType); | ||
| 612 | } | ||
| 613 | } | ||
| 614 | |||
| 615 | /** @psalm-return ClassMetadata::GENERATOR_TYPE_* */ | ||
| 616 | private function determineIdGeneratorStrategy(AbstractPlatform $platform): int | ||
| 617 | { | ||
| 618 | assert($this->em !== null); | ||
| 619 | foreach ($this->em->getConfiguration()->getIdentityGenerationPreferences() as $platformFamily => $strategy) { | ||
| 620 | if (is_a($platform, $platformFamily)) { | ||
| 621 | return $strategy; | ||
| 622 | } | ||
| 623 | } | ||
| 624 | |||
| 625 | $nonIdentityDefaultStrategy = self::NON_IDENTITY_DEFAULT_STRATEGY; | ||
| 626 | |||
| 627 | // DBAL 3 | ||
| 628 | if (method_exists($platform, 'getIdentitySequenceName')) { | ||
| 629 | $nonIdentityDefaultStrategy[Platforms\PostgreSQLPlatform::class] = ClassMetadata::GENERATOR_TYPE_SEQUENCE; | ||
| 630 | } | ||
| 631 | |||
| 632 | foreach ($nonIdentityDefaultStrategy as $platformFamily => $strategy) { | ||
| 633 | if (is_a($platform, $platformFamily)) { | ||
| 634 | if ($platform instanceof Platforms\PostgreSQLPlatform) { | ||
| 635 | Deprecation::trigger( | ||
| 636 | 'doctrine/orm', | ||
| 637 | 'https://github.com/doctrine/orm/issues/8893', | ||
| 638 | <<<'DEPRECATION' | ||
| 639 | Relying on non-optimal defaults for ID generation is deprecated, and IDENTITY | ||
| 640 | results in SERIAL, which is not recommended. | ||
| 641 | Instead, configure identifier generation strategies explicitly through | ||
| 642 | configuration. | ||
| 643 | We currently recommend "SEQUENCE" for "%s", when using DBAL 3, | ||
| 644 | and "IDENTITY" when using DBAL 4, | ||
| 645 | so you should probably use the following configuration before upgrading to DBAL 4, | ||
| 646 | and remove it after deploying that upgrade: | ||
| 647 | |||
| 648 | $configuration->setIdentityGenerationPreferences([ | ||
| 649 | "%s" => ClassMetadata::GENERATOR_TYPE_SEQUENCE, | ||
| 650 | ]); | ||
| 651 | |||
| 652 | DEPRECATION, | ||
| 653 | $platformFamily, | ||
| 654 | $platformFamily, | ||
| 655 | ); | ||
| 656 | } | ||
| 657 | |||
| 658 | return $strategy; | ||
| 659 | } | ||
| 660 | } | ||
| 661 | |||
| 662 | return ClassMetadata::GENERATOR_TYPE_IDENTITY; | ||
| 663 | } | ||
| 664 | |||
| 665 | private function truncateSequenceName(string $schemaElementName): string | ||
| 666 | { | ||
| 667 | $platform = $this->getTargetPlatform(); | ||
| 668 | if (! $platform instanceof Platforms\OraclePlatform) { | ||
| 669 | return $schemaElementName; | ||
| 670 | } | ||
| 671 | |||
| 672 | $maxIdentifierLength = $platform->getMaxIdentifierLength(); | ||
| 673 | |||
| 674 | if (strlen($schemaElementName) > $maxIdentifierLength) { | ||
| 675 | return substr($schemaElementName, 0, $maxIdentifierLength); | ||
| 676 | } | ||
| 677 | |||
| 678 | return $schemaElementName; | ||
| 679 | } | ||
| 680 | |||
| 681 | /** | ||
| 682 | * Inherits the ID generator mapping from a parent class. | ||
| 683 | */ | ||
| 684 | private function inheritIdGeneratorMapping(ClassMetadata $class, ClassMetadata $parent): void | ||
| 685 | { | ||
| 686 | if ($parent->isIdGeneratorSequence()) { | ||
| 687 | $class->setSequenceGeneratorDefinition($parent->sequenceGeneratorDefinition); | ||
| 688 | } | ||
| 689 | |||
| 690 | if ($parent->generatorType) { | ||
| 691 | $class->setIdGeneratorType($parent->generatorType); | ||
| 692 | } | ||
| 693 | |||
| 694 | if ($parent->idGenerator ?? null) { | ||
| 695 | $class->setIdGenerator($parent->idGenerator); | ||
| 696 | } | ||
| 697 | } | ||
| 698 | |||
| 699 | protected function wakeupReflection(ClassMetadataInterface $class, ReflectionService $reflService): void | ||
| 700 | { | ||
| 701 | $class->wakeupReflection($reflService); | ||
| 702 | } | ||
| 703 | |||
| 704 | protected function initializeReflection(ClassMetadataInterface $class, ReflectionService $reflService): void | ||
| 705 | { | ||
| 706 | $class->initializeReflection($reflService); | ||
| 707 | } | ||
| 708 | |||
| 709 | protected function getDriver(): MappingDriver | ||
| 710 | { | ||
| 711 | assert($this->driver !== null); | ||
| 712 | |||
| 713 | return $this->driver; | ||
| 714 | } | ||
| 715 | |||
| 716 | protected function isEntity(ClassMetadataInterface $class): bool | ||
| 717 | { | ||
| 718 | return ! $class->isMappedSuperclass; | ||
| 719 | } | ||
| 720 | |||
| 721 | private function getTargetPlatform(): Platforms\AbstractPlatform | ||
| 722 | { | ||
| 723 | if (! $this->targetPlatform) { | ||
| 724 | $this->targetPlatform = $this->em->getConnection()->getDatabasePlatform(); | ||
| 725 | } | ||
| 726 | |||
| 727 | return $this->targetPlatform; | ||
| 728 | } | ||
| 729 | } | ||
diff --git a/vendor/doctrine/orm/src/Mapping/Column.php b/vendor/doctrine/orm/src/Mapping/Column.php new file mode 100644 index 0000000..68121e6 --- /dev/null +++ b/vendor/doctrine/orm/src/Mapping/Column.php | |||
| @@ -0,0 +1,36 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Mapping; | ||
| 6 | |||
| 7 | use Attribute; | ||
| 8 | use BackedEnum; | ||
| 9 | |||
| 10 | #[Attribute(Attribute::TARGET_PROPERTY)] | ||
| 11 | final class Column implements MappingAttribute | ||
| 12 | { | ||
| 13 | /** | ||
| 14 | * @param int|null $precision The precision for a decimal (exact numeric) column (Applies only for decimal column). | ||
| 15 | * @param int|null $scale The scale for a decimal (exact numeric) column (Applies only for decimal column). | ||
| 16 | * @param class-string<BackedEnum>|null $enumType | ||
| 17 | * @param array<string,mixed> $options | ||
| 18 | * @psalm-param 'NEVER'|'INSERT'|'ALWAYS'|null $generated | ||
| 19 | */ | ||
| 20 | public function __construct( | ||
| 21 | public readonly string|null $name = null, | ||
| 22 | public readonly string|null $type = null, | ||
| 23 | public readonly int|null $length = null, | ||
| 24 | public readonly int|null $precision = null, | ||
| 25 | public readonly int|null $scale = null, | ||
| 26 | public readonly bool $unique = false, | ||
| 27 | public readonly bool $nullable = false, | ||
| 28 | public readonly bool $insertable = true, | ||
| 29 | public readonly bool $updatable = true, | ||
| 30 | public readonly string|null $enumType = null, | ||
| 31 | public readonly array $options = [], | ||
| 32 | public readonly string|null $columnDefinition = null, | ||
| 33 | public readonly string|null $generated = null, | ||
| 34 | ) { | ||
| 35 | } | ||
| 36 | } | ||
diff --git a/vendor/doctrine/orm/src/Mapping/CustomIdGenerator.php b/vendor/doctrine/orm/src/Mapping/CustomIdGenerator.php new file mode 100644 index 0000000..7b31dc3 --- /dev/null +++ b/vendor/doctrine/orm/src/Mapping/CustomIdGenerator.php | |||
| @@ -0,0 +1,16 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Mapping; | ||
| 6 | |||
| 7 | use Attribute; | ||
| 8 | |||
| 9 | #[Attribute(Attribute::TARGET_PROPERTY)] | ||
| 10 | final class CustomIdGenerator implements MappingAttribute | ||
| 11 | { | ||
| 12 | public function __construct( | ||
| 13 | public readonly string|null $class = null, | ||
| 14 | ) { | ||
| 15 | } | ||
| 16 | } | ||
diff --git a/vendor/doctrine/orm/src/Mapping/DefaultEntityListenerResolver.php b/vendor/doctrine/orm/src/Mapping/DefaultEntityListenerResolver.php new file mode 100644 index 0000000..0b3e7a2 --- /dev/null +++ b/vendor/doctrine/orm/src/Mapping/DefaultEntityListenerResolver.php | |||
| @@ -0,0 +1,40 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Mapping; | ||
| 6 | |||
| 7 | use function trim; | ||
| 8 | |||
| 9 | /** | ||
| 10 | * The default DefaultEntityListener | ||
| 11 | */ | ||
| 12 | class DefaultEntityListenerResolver implements EntityListenerResolver | ||
| 13 | { | ||
| 14 | /** @psalm-var array<class-string, object> Map to store entity listener instances. */ | ||
| 15 | private array $instances = []; | ||
| 16 | |||
| 17 | public function clear(string|null $className = null): void | ||
| 18 | { | ||
| 19 | if ($className === null) { | ||
| 20 | $this->instances = []; | ||
| 21 | |||
| 22 | return; | ||
| 23 | } | ||
| 24 | |||
| 25 | $className = trim($className, '\\'); | ||
| 26 | unset($this->instances[$className]); | ||
| 27 | } | ||
| 28 | |||
| 29 | public function register(object $object): void | ||
| 30 | { | ||
| 31 | $this->instances[$object::class] = $object; | ||
| 32 | } | ||
| 33 | |||
| 34 | public function resolve(string $className): object | ||
| 35 | { | ||
| 36 | $className = trim($className, '\\'); | ||
| 37 | |||
| 38 | return $this->instances[$className] ??= new $className(); | ||
| 39 | } | ||
| 40 | } | ||
diff --git a/vendor/doctrine/orm/src/Mapping/DefaultNamingStrategy.php b/vendor/doctrine/orm/src/Mapping/DefaultNamingStrategy.php new file mode 100644 index 0000000..15218f9 --- /dev/null +++ b/vendor/doctrine/orm/src/Mapping/DefaultNamingStrategy.php | |||
| @@ -0,0 +1,68 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Mapping; | ||
| 6 | |||
| 7 | use function str_contains; | ||
| 8 | use function strrpos; | ||
| 9 | use function strtolower; | ||
| 10 | use function substr; | ||
| 11 | |||
| 12 | /** | ||
| 13 | * The default NamingStrategy | ||
| 14 | * | ||
| 15 | * @link www.doctrine-project.org | ||
| 16 | */ | ||
| 17 | class DefaultNamingStrategy implements NamingStrategy | ||
| 18 | { | ||
| 19 | public function classToTableName(string $className): string | ||
| 20 | { | ||
| 21 | if (str_contains($className, '\\')) { | ||
| 22 | return substr($className, strrpos($className, '\\') + 1); | ||
| 23 | } | ||
| 24 | |||
| 25 | return $className; | ||
| 26 | } | ||
| 27 | |||
| 28 | public function propertyToColumnName(string $propertyName, string $className): string | ||
| 29 | { | ||
| 30 | return $propertyName; | ||
| 31 | } | ||
| 32 | |||
| 33 | public function embeddedFieldToColumnName( | ||
| 34 | string $propertyName, | ||
| 35 | string $embeddedColumnName, | ||
| 36 | string $className, | ||
| 37 | string $embeddedClassName, | ||
| 38 | ): string { | ||
| 39 | return $propertyName . '_' . $embeddedColumnName; | ||
| 40 | } | ||
| 41 | |||
| 42 | public function referenceColumnName(): string | ||
| 43 | { | ||
| 44 | return 'id'; | ||
| 45 | } | ||
| 46 | |||
| 47 | public function joinColumnName(string $propertyName, string $className): string | ||
| 48 | { | ||
| 49 | return $propertyName . '_' . $this->referenceColumnName(); | ||
| 50 | } | ||
| 51 | |||
| 52 | public function joinTableName( | ||
| 53 | string $sourceEntity, | ||
| 54 | string $targetEntity, | ||
| 55 | string $propertyName, | ||
| 56 | ): string { | ||
| 57 | return strtolower($this->classToTableName($sourceEntity) . '_' . | ||
| 58 | $this->classToTableName($targetEntity)); | ||
| 59 | } | ||
| 60 | |||
| 61 | public function joinKeyColumnName( | ||
| 62 | string $entityName, | ||
| 63 | string|null $referencedColumnName, | ||
| 64 | ): string { | ||
| 65 | return strtolower($this->classToTableName($entityName) . '_' . | ||
| 66 | ($referencedColumnName ?: $this->referenceColumnName())); | ||
| 67 | } | ||
| 68 | } | ||
diff --git a/vendor/doctrine/orm/src/Mapping/DefaultQuoteStrategy.php b/vendor/doctrine/orm/src/Mapping/DefaultQuoteStrategy.php new file mode 100644 index 0000000..6260336 --- /dev/null +++ b/vendor/doctrine/orm/src/Mapping/DefaultQuoteStrategy.php | |||
| @@ -0,0 +1,145 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Mapping; | ||
| 6 | |||
| 7 | use Doctrine\DBAL\Platforms\AbstractPlatform; | ||
| 8 | use Doctrine\ORM\Internal\SQLResultCasing; | ||
| 9 | |||
| 10 | use function array_map; | ||
| 11 | use function array_merge; | ||
| 12 | use function assert; | ||
| 13 | use function is_numeric; | ||
| 14 | use function preg_replace; | ||
| 15 | use function substr; | ||
| 16 | |||
| 17 | /** | ||
| 18 | * A set of rules for determining the physical column, alias and table quotes | ||
| 19 | */ | ||
| 20 | class DefaultQuoteStrategy implements QuoteStrategy | ||
| 21 | { | ||
| 22 | use SQLResultCasing; | ||
| 23 | |||
| 24 | public function getColumnName(string $fieldName, ClassMetadata $class, AbstractPlatform $platform): string | ||
| 25 | { | ||
| 26 | return isset($class->fieldMappings[$fieldName]->quoted) | ||
| 27 | ? $platform->quoteIdentifier($class->fieldMappings[$fieldName]->columnName) | ||
| 28 | : $class->fieldMappings[$fieldName]->columnName; | ||
| 29 | } | ||
| 30 | |||
| 31 | /** | ||
| 32 | * {@inheritDoc} | ||
| 33 | * | ||
| 34 | * @todo Table names should be computed in DBAL depending on the platform | ||
| 35 | */ | ||
| 36 | public function getTableName(ClassMetadata $class, AbstractPlatform $platform): string | ||
| 37 | { | ||
| 38 | $tableName = $class->table['name']; | ||
| 39 | |||
| 40 | if (! empty($class->table['schema'])) { | ||
| 41 | $tableName = $class->table['schema'] . '.' . $class->table['name']; | ||
| 42 | } | ||
| 43 | |||
| 44 | return isset($class->table['quoted']) | ||
| 45 | ? $platform->quoteIdentifier($tableName) | ||
| 46 | : $tableName; | ||
| 47 | } | ||
| 48 | |||
| 49 | /** | ||
| 50 | * {@inheritDoc} | ||
| 51 | */ | ||
| 52 | public function getSequenceName(array $definition, ClassMetadata $class, AbstractPlatform $platform): string | ||
| 53 | { | ||
| 54 | return isset($definition['quoted']) | ||
| 55 | ? $platform->quoteIdentifier($definition['sequenceName']) | ||
| 56 | : $definition['sequenceName']; | ||
| 57 | } | ||
| 58 | |||
| 59 | public function getJoinColumnName(JoinColumnMapping $joinColumn, ClassMetadata $class, AbstractPlatform $platform): string | ||
| 60 | { | ||
| 61 | return isset($joinColumn->quoted) | ||
| 62 | ? $platform->quoteIdentifier($joinColumn->name) | ||
| 63 | : $joinColumn->name; | ||
| 64 | } | ||
| 65 | |||
| 66 | public function getReferencedJoinColumnName( | ||
| 67 | JoinColumnMapping $joinColumn, | ||
| 68 | ClassMetadata $class, | ||
| 69 | AbstractPlatform $platform, | ||
| 70 | ): string { | ||
| 71 | return isset($joinColumn->quoted) | ||
| 72 | ? $platform->quoteIdentifier($joinColumn->referencedColumnName) | ||
| 73 | : $joinColumn->referencedColumnName; | ||
| 74 | } | ||
| 75 | |||
| 76 | public function getJoinTableName( | ||
| 77 | ManyToManyOwningSideMapping $association, | ||
| 78 | ClassMetadata $class, | ||
| 79 | AbstractPlatform $platform, | ||
| 80 | ): string { | ||
| 81 | $schema = ''; | ||
| 82 | |||
| 83 | if (isset($association->joinTable->schema)) { | ||
| 84 | $schema = $association->joinTable->schema . '.'; | ||
| 85 | } | ||
| 86 | |||
| 87 | $tableName = $association->joinTable->name; | ||
| 88 | |||
| 89 | if (isset($association->joinTable->quoted)) { | ||
| 90 | $tableName = $platform->quoteIdentifier($tableName); | ||
| 91 | } | ||
| 92 | |||
| 93 | return $schema . $tableName; | ||
| 94 | } | ||
| 95 | |||
| 96 | /** | ||
| 97 | * {@inheritDoc} | ||
| 98 | */ | ||
| 99 | public function getIdentifierColumnNames(ClassMetadata $class, AbstractPlatform $platform): array | ||
| 100 | { | ||
| 101 | $quotedColumnNames = []; | ||
| 102 | |||
| 103 | foreach ($class->identifier as $fieldName) { | ||
| 104 | if (isset($class->fieldMappings[$fieldName])) { | ||
| 105 | $quotedColumnNames[] = $this->getColumnName($fieldName, $class, $platform); | ||
| 106 | |||
| 107 | continue; | ||
| 108 | } | ||
| 109 | |||
| 110 | // Association defined as Id field | ||
| 111 | $assoc = $class->associationMappings[$fieldName]; | ||
| 112 | assert($assoc->isToOneOwningSide()); | ||
| 113 | $joinColumns = $assoc->joinColumns; | ||
| 114 | $assocQuotedColumnNames = array_map( | ||
| 115 | static fn (JoinColumnMapping $joinColumn) => isset($joinColumn->quoted) | ||
| 116 | ? $platform->quoteIdentifier($joinColumn->name) | ||
| 117 | : $joinColumn->name, | ||
| 118 | $joinColumns, | ||
| 119 | ); | ||
| 120 | |||
| 121 | $quotedColumnNames = array_merge($quotedColumnNames, $assocQuotedColumnNames); | ||
| 122 | } | ||
| 123 | |||
| 124 | return $quotedColumnNames; | ||
| 125 | } | ||
| 126 | |||
| 127 | public function getColumnAlias( | ||
| 128 | string $columnName, | ||
| 129 | int $counter, | ||
| 130 | AbstractPlatform $platform, | ||
| 131 | ClassMetadata|null $class = null, | ||
| 132 | ): string { | ||
| 133 | // 1 ) Concatenate column name and counter | ||
| 134 | // 2 ) Trim the column alias to the maximum identifier length of the platform. | ||
| 135 | // If the alias is to long, characters are cut off from the beginning. | ||
| 136 | // 3 ) Strip non alphanumeric characters | ||
| 137 | // 4 ) Prefix with "_" if the result its numeric | ||
| 138 | $columnName .= '_' . $counter; | ||
| 139 | $columnName = substr($columnName, -$platform->getMaxIdentifierLength()); | ||
| 140 | $columnName = preg_replace('/[^A-Za-z0-9_]/', '', $columnName); | ||
| 141 | $columnName = is_numeric($columnName) ? '_' . $columnName : $columnName; | ||
| 142 | |||
| 143 | return $this->getSQLResultCasing($platform, $columnName); | ||
| 144 | } | ||
| 145 | } | ||
diff --git a/vendor/doctrine/orm/src/Mapping/DefaultTypedFieldMapper.php b/vendor/doctrine/orm/src/Mapping/DefaultTypedFieldMapper.php new file mode 100644 index 0000000..49144b8 --- /dev/null +++ b/vendor/doctrine/orm/src/Mapping/DefaultTypedFieldMapper.php | |||
| @@ -0,0 +1,80 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Mapping; | ||
| 6 | |||
| 7 | use BackedEnum; | ||
| 8 | use DateInterval; | ||
| 9 | use DateTime; | ||
| 10 | use DateTimeImmutable; | ||
| 11 | use Doctrine\DBAL\Types\Type; | ||
| 12 | use Doctrine\DBAL\Types\Types; | ||
| 13 | use ReflectionEnum; | ||
| 14 | use ReflectionNamedType; | ||
| 15 | use ReflectionProperty; | ||
| 16 | |||
| 17 | use function array_merge; | ||
| 18 | use function assert; | ||
| 19 | use function enum_exists; | ||
| 20 | use function is_a; | ||
| 21 | |||
| 22 | /** @psalm-type ScalarName = 'array'|'bool'|'float'|'int'|'string' */ | ||
| 23 | final class DefaultTypedFieldMapper implements TypedFieldMapper | ||
| 24 | { | ||
| 25 | /** @var array<class-string|ScalarName, class-string<Type>|string> $typedFieldMappings */ | ||
| 26 | private array $typedFieldMappings; | ||
| 27 | |||
| 28 | private const DEFAULT_TYPED_FIELD_MAPPINGS = [ | ||
| 29 | DateInterval::class => Types::DATEINTERVAL, | ||
| 30 | DateTime::class => Types::DATETIME_MUTABLE, | ||
| 31 | DateTimeImmutable::class => Types::DATETIME_IMMUTABLE, | ||
| 32 | 'array' => Types::JSON, | ||
| 33 | 'bool' => Types::BOOLEAN, | ||
| 34 | 'float' => Types::FLOAT, | ||
| 35 | 'int' => Types::INTEGER, | ||
| 36 | 'string' => Types::STRING, | ||
| 37 | ]; | ||
| 38 | |||
| 39 | /** @param array<class-string|ScalarName, class-string<Type>|string> $typedFieldMappings */ | ||
| 40 | public function __construct(array $typedFieldMappings = []) | ||
| 41 | { | ||
| 42 | $this->typedFieldMappings = array_merge(self::DEFAULT_TYPED_FIELD_MAPPINGS, $typedFieldMappings); | ||
| 43 | } | ||
| 44 | |||
| 45 | /** | ||
| 46 | * {@inheritDoc} | ||
| 47 | */ | ||
| 48 | public function validateAndComplete(array $mapping, ReflectionProperty $field): array | ||
| 49 | { | ||
| 50 | $type = $field->getType(); | ||
| 51 | |||
| 52 | if ( | ||
| 53 | ! isset($mapping['type']) | ||
| 54 | && ($type instanceof ReflectionNamedType) | ||
| 55 | ) { | ||
| 56 | if (! $type->isBuiltin() && enum_exists($type->getName())) { | ||
| 57 | $reflection = new ReflectionEnum($type->getName()); | ||
| 58 | if (! $reflection->isBacked()) { | ||
| 59 | throw MappingException::backedEnumTypeRequired( | ||
| 60 | $field->class, | ||
| 61 | $mapping['fieldName'], | ||
| 62 | $type->getName(), | ||
| 63 | ); | ||
| 64 | } | ||
| 65 | |||
| 66 | assert(is_a($type->getName(), BackedEnum::class, true)); | ||
| 67 | $mapping['enumType'] = $type->getName(); | ||
| 68 | $type = $reflection->getBackingType(); | ||
| 69 | |||
| 70 | assert($type instanceof ReflectionNamedType); | ||
| 71 | } | ||
| 72 | |||
| 73 | if (isset($this->typedFieldMappings[$type->getName()])) { | ||
| 74 | $mapping['type'] = $this->typedFieldMappings[$type->getName()]; | ||
| 75 | } | ||
| 76 | } | ||
| 77 | |||
| 78 | return $mapping; | ||
| 79 | } | ||
| 80 | } | ||
diff --git a/vendor/doctrine/orm/src/Mapping/DiscriminatorColumn.php b/vendor/doctrine/orm/src/Mapping/DiscriminatorColumn.php new file mode 100644 index 0000000..fb9c7d3 --- /dev/null +++ b/vendor/doctrine/orm/src/Mapping/DiscriminatorColumn.php | |||
| @@ -0,0 +1,24 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Mapping; | ||
| 6 | |||
| 7 | use Attribute; | ||
| 8 | use BackedEnum; | ||
| 9 | |||
| 10 | #[Attribute(Attribute::TARGET_CLASS)] | ||
| 11 | final class DiscriminatorColumn implements MappingAttribute | ||
| 12 | { | ||
| 13 | public function __construct( | ||
| 14 | public readonly string|null $name = null, | ||
| 15 | public readonly string|null $type = null, | ||
| 16 | public readonly int|null $length = null, | ||
| 17 | public readonly string|null $columnDefinition = null, | ||
| 18 | /** @var class-string<BackedEnum>|null */ | ||
| 19 | public readonly string|null $enumType = null, | ||
| 20 | /** @var array<string, mixed> */ | ||
| 21 | public readonly array $options = [], | ||
| 22 | ) { | ||
| 23 | } | ||
| 24 | } | ||
diff --git a/vendor/doctrine/orm/src/Mapping/DiscriminatorColumnMapping.php b/vendor/doctrine/orm/src/Mapping/DiscriminatorColumnMapping.php new file mode 100644 index 0000000..4ccb71c --- /dev/null +++ b/vendor/doctrine/orm/src/Mapping/DiscriminatorColumnMapping.php | |||
| @@ -0,0 +1,83 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Mapping; | ||
| 6 | |||
| 7 | use ArrayAccess; | ||
| 8 | use BackedEnum; | ||
| 9 | use Exception; | ||
| 10 | |||
| 11 | use function in_array; | ||
| 12 | use function property_exists; | ||
| 13 | |||
| 14 | /** @template-implements ArrayAccess<string, mixed> */ | ||
| 15 | final class DiscriminatorColumnMapping implements ArrayAccess | ||
| 16 | { | ||
| 17 | use ArrayAccessImplementation; | ||
| 18 | |||
| 19 | /** The database length of the column. Optional. Default value taken from the type. */ | ||
| 20 | public int|null $length = null; | ||
| 21 | |||
| 22 | public string|null $columnDefinition = null; | ||
| 23 | |||
| 24 | /** @var class-string<BackedEnum>|null */ | ||
| 25 | public string|null $enumType = null; | ||
| 26 | |||
| 27 | /** @var array<string, mixed> */ | ||
| 28 | public array $options = []; | ||
| 29 | |||
| 30 | public function __construct( | ||
| 31 | public string $type, | ||
| 32 | public string $fieldName, | ||
| 33 | public string $name, | ||
| 34 | ) { | ||
| 35 | } | ||
| 36 | |||
| 37 | /** | ||
| 38 | * @psalm-param array{ | ||
| 39 | * type: string, | ||
| 40 | * fieldName: string, | ||
| 41 | * name: string, | ||
| 42 | * length?: int|null, | ||
| 43 | * columnDefinition?: string|null, | ||
| 44 | * enumType?: class-string<BackedEnum>|null, | ||
| 45 | * options?: array<string, mixed>|null, | ||
| 46 | * } $mappingArray | ||
| 47 | */ | ||
| 48 | public static function fromMappingArray(array $mappingArray): self | ||
| 49 | { | ||
| 50 | $mapping = new self( | ||
| 51 | $mappingArray['type'], | ||
| 52 | $mappingArray['fieldName'], | ||
| 53 | $mappingArray['name'], | ||
| 54 | ); | ||
| 55 | foreach ($mappingArray as $key => $value) { | ||
| 56 | if (in_array($key, ['type', 'fieldName', 'name'])) { | ||
| 57 | continue; | ||
| 58 | } | ||
| 59 | |||
| 60 | if (property_exists($mapping, $key)) { | ||
| 61 | $mapping->$key = $value ?? $mapping->$key; | ||
| 62 | } else { | ||
| 63 | throw new Exception('Unknown property ' . $key . ' on class ' . static::class); | ||
| 64 | } | ||
| 65 | } | ||
| 66 | |||
| 67 | return $mapping; | ||
| 68 | } | ||
| 69 | |||
| 70 | /** @return list<string> */ | ||
| 71 | public function __sleep(): array | ||
| 72 | { | ||
| 73 | $serialized = ['type', 'fieldName', 'name']; | ||
| 74 | |||
| 75 | foreach (['length', 'columnDefinition', 'enumType', 'options'] as $stringOrArrayKey) { | ||
| 76 | if ($this->$stringOrArrayKey !== null) { | ||
| 77 | $serialized[] = $stringOrArrayKey; | ||
| 78 | } | ||
| 79 | } | ||
| 80 | |||
| 81 | return $serialized; | ||
| 82 | } | ||
| 83 | } | ||
diff --git a/vendor/doctrine/orm/src/Mapping/DiscriminatorMap.php b/vendor/doctrine/orm/src/Mapping/DiscriminatorMap.php new file mode 100644 index 0000000..2b204a9 --- /dev/null +++ b/vendor/doctrine/orm/src/Mapping/DiscriminatorMap.php | |||
| @@ -0,0 +1,17 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Mapping; | ||
| 6 | |||
| 7 | use Attribute; | ||
| 8 | |||
| 9 | #[Attribute(Attribute::TARGET_CLASS)] | ||
| 10 | final class DiscriminatorMap implements MappingAttribute | ||
| 11 | { | ||
| 12 | /** @param array<int|string, string> $value */ | ||
| 13 | public function __construct( | ||
| 14 | public readonly array $value, | ||
| 15 | ) { | ||
| 16 | } | ||
| 17 | } | ||
diff --git a/vendor/doctrine/orm/src/Mapping/Driver/AttributeDriver.php b/vendor/doctrine/orm/src/Mapping/Driver/AttributeDriver.php new file mode 100644 index 0000000..6fed1a2 --- /dev/null +++ b/vendor/doctrine/orm/src/Mapping/Driver/AttributeDriver.php | |||
| @@ -0,0 +1,768 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Mapping\Driver; | ||
| 6 | |||
| 7 | use Doctrine\ORM\Events; | ||
| 8 | use Doctrine\ORM\Mapping; | ||
| 9 | use Doctrine\ORM\Mapping\Builder\EntityListenerBuilder; | ||
| 10 | use Doctrine\ORM\Mapping\ClassMetadata; | ||
| 11 | use Doctrine\ORM\Mapping\MappingException; | ||
| 12 | use Doctrine\Persistence\Mapping\ClassMetadata as PersistenceClassMetadata; | ||
| 13 | use Doctrine\Persistence\Mapping\Driver\ColocatedMappingDriver; | ||
| 14 | use Doctrine\Persistence\Mapping\Driver\MappingDriver; | ||
| 15 | use InvalidArgumentException; | ||
| 16 | use ReflectionClass; | ||
| 17 | use ReflectionMethod; | ||
| 18 | use ReflectionProperty; | ||
| 19 | |||
| 20 | use function assert; | ||
| 21 | use function class_exists; | ||
| 22 | use function constant; | ||
| 23 | use function defined; | ||
| 24 | use function sprintf; | ||
| 25 | |||
| 26 | class AttributeDriver implements MappingDriver | ||
| 27 | { | ||
| 28 | use ColocatedMappingDriver; | ||
| 29 | use ReflectionBasedDriver; | ||
| 30 | |||
| 31 | private const ENTITY_ATTRIBUTE_CLASSES = [ | ||
| 32 | Mapping\Entity::class => 1, | ||
| 33 | Mapping\MappedSuperclass::class => 2, | ||
| 34 | ]; | ||
| 35 | |||
| 36 | private readonly AttributeReader $reader; | ||
| 37 | |||
| 38 | /** | ||
| 39 | * @param array<string> $paths | ||
| 40 | * @param true $reportFieldsWhereDeclared no-op, to be removed in 4.0 | ||
| 41 | */ | ||
| 42 | public function __construct(array $paths, bool $reportFieldsWhereDeclared = true) | ||
| 43 | { | ||
| 44 | if (! $reportFieldsWhereDeclared) { | ||
| 45 | throw new InvalidArgumentException(sprintf( | ||
| 46 | 'The $reportFieldsWhereDeclared argument is no longer supported, make sure to omit it when calling %s.', | ||
| 47 | __METHOD__, | ||
| 48 | )); | ||
| 49 | } | ||
| 50 | |||
| 51 | $this->reader = new AttributeReader(); | ||
| 52 | $this->addPaths($paths); | ||
| 53 | } | ||
| 54 | |||
| 55 | public function isTransient(string $className): bool | ||
| 56 | { | ||
| 57 | $classAttributes = $this->reader->getClassAttributes(new ReflectionClass($className)); | ||
| 58 | |||
| 59 | foreach ($classAttributes as $a) { | ||
| 60 | $attr = $a instanceof RepeatableAttributeCollection ? $a[0] : $a; | ||
| 61 | if (isset(self::ENTITY_ATTRIBUTE_CLASSES[$attr::class])) { | ||
| 62 | return false; | ||
| 63 | } | ||
| 64 | } | ||
| 65 | |||
| 66 | return true; | ||
| 67 | } | ||
| 68 | |||
| 69 | /** | ||
| 70 | * {@inheritDoc} | ||
| 71 | * | ||
| 72 | * @psalm-param class-string<T> $className | ||
| 73 | * @psalm-param ClassMetadata<T> $metadata | ||
| 74 | * | ||
| 75 | * @template T of object | ||
| 76 | */ | ||
| 77 | public function loadMetadataForClass(string $className, PersistenceClassMetadata $metadata): void | ||
| 78 | { | ||
| 79 | $reflectionClass = $metadata->getReflectionClass() | ||
| 80 | // this happens when running attribute driver in combination with | ||
| 81 | // static reflection services. This is not the nicest fix | ||
| 82 | ?? new ReflectionClass($metadata->name); | ||
| 83 | |||
| 84 | $classAttributes = $this->reader->getClassAttributes($reflectionClass); | ||
| 85 | |||
| 86 | // Evaluate Entity attribute | ||
| 87 | if (isset($classAttributes[Mapping\Entity::class])) { | ||
| 88 | $entityAttribute = $classAttributes[Mapping\Entity::class]; | ||
| 89 | if ($entityAttribute->repositoryClass !== null) { | ||
| 90 | $metadata->setCustomRepositoryClass($entityAttribute->repositoryClass); | ||
| 91 | } | ||
| 92 | |||
| 93 | if ($entityAttribute->readOnly) { | ||
| 94 | $metadata->markReadOnly(); | ||
| 95 | } | ||
| 96 | } elseif (isset($classAttributes[Mapping\MappedSuperclass::class])) { | ||
| 97 | $mappedSuperclassAttribute = $classAttributes[Mapping\MappedSuperclass::class]; | ||
| 98 | |||
| 99 | $metadata->setCustomRepositoryClass($mappedSuperclassAttribute->repositoryClass); | ||
| 100 | $metadata->isMappedSuperclass = true; | ||
| 101 | } elseif (isset($classAttributes[Mapping\Embeddable::class])) { | ||
| 102 | $metadata->isEmbeddedClass = true; | ||
| 103 | } else { | ||
| 104 | throw MappingException::classIsNotAValidEntityOrMappedSuperClass($className); | ||
| 105 | } | ||
| 106 | |||
| 107 | $primaryTable = []; | ||
| 108 | |||
| 109 | if (isset($classAttributes[Mapping\Table::class])) { | ||
| 110 | $tableAnnot = $classAttributes[Mapping\Table::class]; | ||
| 111 | $primaryTable['name'] = $tableAnnot->name; | ||
| 112 | $primaryTable['schema'] = $tableAnnot->schema; | ||
| 113 | |||
| 114 | if ($tableAnnot->options) { | ||
| 115 | $primaryTable['options'] = $tableAnnot->options; | ||
| 116 | } | ||
| 117 | } | ||
| 118 | |||
| 119 | if (isset($classAttributes[Mapping\Index::class])) { | ||
| 120 | if ($metadata->isEmbeddedClass) { | ||
| 121 | throw MappingException::invalidAttributeOnEmbeddable($metadata->name, Mapping\Index::class); | ||
| 122 | } | ||
| 123 | |||
| 124 | foreach ($classAttributes[Mapping\Index::class] as $idx => $indexAnnot) { | ||
| 125 | $index = []; | ||
| 126 | |||
| 127 | if (! empty($indexAnnot->columns)) { | ||
| 128 | $index['columns'] = $indexAnnot->columns; | ||
| 129 | } | ||
| 130 | |||
| 131 | if (! empty($indexAnnot->fields)) { | ||
| 132 | $index['fields'] = $indexAnnot->fields; | ||
| 133 | } | ||
| 134 | |||
| 135 | if ( | ||
| 136 | isset($index['columns'], $index['fields']) | ||
| 137 | || ( | ||
| 138 | ! isset($index['columns']) | ||
| 139 | && ! isset($index['fields']) | ||
| 140 | ) | ||
| 141 | ) { | ||
| 142 | throw MappingException::invalidIndexConfiguration( | ||
| 143 | $className, | ||
| 144 | (string) ($indexAnnot->name ?? $idx), | ||
| 145 | ); | ||
| 146 | } | ||
| 147 | |||
| 148 | if (! empty($indexAnnot->flags)) { | ||
| 149 | $index['flags'] = $indexAnnot->flags; | ||
| 150 | } | ||
| 151 | |||
| 152 | if (! empty($indexAnnot->options)) { | ||
| 153 | $index['options'] = $indexAnnot->options; | ||
| 154 | } | ||
| 155 | |||
| 156 | if (! empty($indexAnnot->name)) { | ||
| 157 | $primaryTable['indexes'][$indexAnnot->name] = $index; | ||
| 158 | } else { | ||
| 159 | $primaryTable['indexes'][] = $index; | ||
| 160 | } | ||
| 161 | } | ||
| 162 | } | ||
| 163 | |||
| 164 | if (isset($classAttributes[Mapping\UniqueConstraint::class])) { | ||
| 165 | if ($metadata->isEmbeddedClass) { | ||
| 166 | throw MappingException::invalidAttributeOnEmbeddable($metadata->name, Mapping\UniqueConstraint::class); | ||
| 167 | } | ||
| 168 | |||
| 169 | foreach ($classAttributes[Mapping\UniqueConstraint::class] as $idx => $uniqueConstraintAnnot) { | ||
| 170 | $uniqueConstraint = []; | ||
| 171 | |||
| 172 | if (! empty($uniqueConstraintAnnot->columns)) { | ||
| 173 | $uniqueConstraint['columns'] = $uniqueConstraintAnnot->columns; | ||
| 174 | } | ||
| 175 | |||
| 176 | if (! empty($uniqueConstraintAnnot->fields)) { | ||
| 177 | $uniqueConstraint['fields'] = $uniqueConstraintAnnot->fields; | ||
| 178 | } | ||
| 179 | |||
| 180 | if ( | ||
| 181 | isset($uniqueConstraint['columns'], $uniqueConstraint['fields']) | ||
| 182 | || ( | ||
| 183 | ! isset($uniqueConstraint['columns']) | ||
| 184 | && ! isset($uniqueConstraint['fields']) | ||
| 185 | ) | ||
| 186 | ) { | ||
| 187 | throw MappingException::invalidUniqueConstraintConfiguration( | ||
| 188 | $className, | ||
| 189 | (string) ($uniqueConstraintAnnot->name ?? $idx), | ||
| 190 | ); | ||
| 191 | } | ||
| 192 | |||
| 193 | if (! empty($uniqueConstraintAnnot->options)) { | ||
| 194 | $uniqueConstraint['options'] = $uniqueConstraintAnnot->options; | ||
| 195 | } | ||
| 196 | |||
| 197 | if (! empty($uniqueConstraintAnnot->name)) { | ||
| 198 | $primaryTable['uniqueConstraints'][$uniqueConstraintAnnot->name] = $uniqueConstraint; | ||
| 199 | } else { | ||
| 200 | $primaryTable['uniqueConstraints'][] = $uniqueConstraint; | ||
| 201 | } | ||
| 202 | } | ||
| 203 | } | ||
| 204 | |||
| 205 | $metadata->setPrimaryTable($primaryTable); | ||
| 206 | |||
| 207 | // Evaluate #[Cache] attribute | ||
| 208 | if (isset($classAttributes[Mapping\Cache::class])) { | ||
| 209 | if ($metadata->isEmbeddedClass) { | ||
| 210 | throw MappingException::invalidAttributeOnEmbeddable($metadata->name, Mapping\Cache::class); | ||
| 211 | } | ||
| 212 | |||
| 213 | $cacheAttribute = $classAttributes[Mapping\Cache::class]; | ||
| 214 | $cacheMap = [ | ||
| 215 | 'region' => $cacheAttribute->region, | ||
| 216 | 'usage' => constant('Doctrine\ORM\Mapping\ClassMetadata::CACHE_USAGE_' . $cacheAttribute->usage), | ||
| 217 | ]; | ||
| 218 | |||
| 219 | $metadata->enableCache($cacheMap); | ||
| 220 | } | ||
| 221 | |||
| 222 | // Evaluate InheritanceType attribute | ||
| 223 | if (isset($classAttributes[Mapping\InheritanceType::class])) { | ||
| 224 | if ($metadata->isEmbeddedClass) { | ||
| 225 | throw MappingException::invalidAttributeOnEmbeddable($metadata->name, Mapping\InheritanceType::class); | ||
| 226 | } | ||
| 227 | |||
| 228 | $inheritanceTypeAttribute = $classAttributes[Mapping\InheritanceType::class]; | ||
| 229 | |||
| 230 | $metadata->setInheritanceType( | ||
| 231 | constant('Doctrine\ORM\Mapping\ClassMetadata::INHERITANCE_TYPE_' . $inheritanceTypeAttribute->value), | ||
| 232 | ); | ||
| 233 | |||
| 234 | if ($metadata->inheritanceType !== ClassMetadata::INHERITANCE_TYPE_NONE) { | ||
| 235 | // Evaluate DiscriminatorColumn attribute | ||
| 236 | if (isset($classAttributes[Mapping\DiscriminatorColumn::class])) { | ||
| 237 | $discrColumnAttribute = $classAttributes[Mapping\DiscriminatorColumn::class]; | ||
| 238 | assert($discrColumnAttribute instanceof Mapping\DiscriminatorColumn); | ||
| 239 | |||
| 240 | $columnDef = [ | ||
| 241 | 'name' => $discrColumnAttribute->name, | ||
| 242 | 'type' => $discrColumnAttribute->type ?? 'string', | ||
| 243 | 'length' => $discrColumnAttribute->length ?? 255, | ||
| 244 | 'columnDefinition' => $discrColumnAttribute->columnDefinition, | ||
| 245 | 'enumType' => $discrColumnAttribute->enumType, | ||
| 246 | ]; | ||
| 247 | |||
| 248 | if ($discrColumnAttribute->options) { | ||
| 249 | $columnDef['options'] = $discrColumnAttribute->options; | ||
| 250 | } | ||
| 251 | |||
| 252 | $metadata->setDiscriminatorColumn($columnDef); | ||
| 253 | } else { | ||
| 254 | $metadata->setDiscriminatorColumn(['name' => 'dtype', 'type' => 'string', 'length' => 255]); | ||
| 255 | } | ||
| 256 | |||
| 257 | // Evaluate DiscriminatorMap attribute | ||
| 258 | if (isset($classAttributes[Mapping\DiscriminatorMap::class])) { | ||
| 259 | $discrMapAttribute = $classAttributes[Mapping\DiscriminatorMap::class]; | ||
| 260 | $metadata->setDiscriminatorMap($discrMapAttribute->value); | ||
| 261 | } | ||
| 262 | } | ||
| 263 | } | ||
| 264 | |||
| 265 | // Evaluate DoctrineChangeTrackingPolicy attribute | ||
| 266 | if (isset($classAttributes[Mapping\ChangeTrackingPolicy::class])) { | ||
| 267 | if ($metadata->isEmbeddedClass) { | ||
| 268 | throw MappingException::invalidAttributeOnEmbeddable($metadata->name, Mapping\ChangeTrackingPolicy::class); | ||
| 269 | } | ||
| 270 | |||
| 271 | $changeTrackingAttribute = $classAttributes[Mapping\ChangeTrackingPolicy::class]; | ||
| 272 | $metadata->setChangeTrackingPolicy(constant('Doctrine\ORM\Mapping\ClassMetadata::CHANGETRACKING_' . $changeTrackingAttribute->value)); | ||
| 273 | } | ||
| 274 | |||
| 275 | foreach ($reflectionClass->getProperties() as $property) { | ||
| 276 | assert($property instanceof ReflectionProperty); | ||
| 277 | |||
| 278 | if ($this->isRepeatedPropertyDeclaration($property, $metadata)) { | ||
| 279 | continue; | ||
| 280 | } | ||
| 281 | |||
| 282 | $mapping = []; | ||
| 283 | $mapping['fieldName'] = $property->name; | ||
| 284 | |||
| 285 | // Evaluate #[Cache] attribute | ||
| 286 | $cacheAttribute = $this->reader->getPropertyAttribute($property, Mapping\Cache::class); | ||
| 287 | if ($cacheAttribute !== null) { | ||
| 288 | assert($cacheAttribute instanceof Mapping\Cache); | ||
| 289 | |||
| 290 | $mapping['cache'] = $metadata->getAssociationCacheDefaults( | ||
| 291 | $mapping['fieldName'], | ||
| 292 | [ | ||
| 293 | 'usage' => (int) constant('Doctrine\ORM\Mapping\ClassMetadata::CACHE_USAGE_' . $cacheAttribute->usage), | ||
| 294 | 'region' => $cacheAttribute->region, | ||
| 295 | ], | ||
| 296 | ); | ||
| 297 | } | ||
| 298 | |||
| 299 | // Check for JoinColumn/JoinColumns attributes | ||
| 300 | $joinColumns = []; | ||
| 301 | |||
| 302 | $joinColumnAttributes = $this->reader->getPropertyAttributeCollection($property, Mapping\JoinColumn::class); | ||
| 303 | |||
| 304 | foreach ($joinColumnAttributes as $joinColumnAttribute) { | ||
| 305 | $joinColumns[] = $this->joinColumnToArray($joinColumnAttribute); | ||
| 306 | } | ||
| 307 | |||
| 308 | // Field can only be attributed with one of: | ||
| 309 | // Column, OneToOne, OneToMany, ManyToOne, ManyToMany, Embedded | ||
| 310 | $columnAttribute = $this->reader->getPropertyAttribute($property, Mapping\Column::class); | ||
| 311 | $oneToOneAttribute = $this->reader->getPropertyAttribute($property, Mapping\OneToOne::class); | ||
| 312 | $oneToManyAttribute = $this->reader->getPropertyAttribute($property, Mapping\OneToMany::class); | ||
| 313 | $manyToOneAttribute = $this->reader->getPropertyAttribute($property, Mapping\ManyToOne::class); | ||
| 314 | $manyToManyAttribute = $this->reader->getPropertyAttribute($property, Mapping\ManyToMany::class); | ||
| 315 | $embeddedAttribute = $this->reader->getPropertyAttribute($property, Mapping\Embedded::class); | ||
| 316 | |||
| 317 | if ($columnAttribute !== null) { | ||
| 318 | $mapping = $this->columnToArray($property->name, $columnAttribute); | ||
| 319 | |||
| 320 | if ($this->reader->getPropertyAttribute($property, Mapping\Id::class)) { | ||
| 321 | $mapping['id'] = true; | ||
| 322 | } | ||
| 323 | |||
| 324 | $generatedValueAttribute = $this->reader->getPropertyAttribute($property, Mapping\GeneratedValue::class); | ||
| 325 | |||
| 326 | if ($generatedValueAttribute !== null) { | ||
| 327 | $metadata->setIdGeneratorType(constant('Doctrine\ORM\Mapping\ClassMetadata::GENERATOR_TYPE_' . $generatedValueAttribute->strategy)); | ||
| 328 | } | ||
| 329 | |||
| 330 | if ($this->reader->getPropertyAttribute($property, Mapping\Version::class)) { | ||
| 331 | $metadata->setVersionMapping($mapping); | ||
| 332 | } | ||
| 333 | |||
| 334 | $metadata->mapField($mapping); | ||
| 335 | |||
| 336 | // Check for SequenceGenerator/TableGenerator definition | ||
| 337 | $seqGeneratorAttribute = $this->reader->getPropertyAttribute($property, Mapping\SequenceGenerator::class); | ||
| 338 | $customGeneratorAttribute = $this->reader->getPropertyAttribute($property, Mapping\CustomIdGenerator::class); | ||
| 339 | |||
| 340 | if ($seqGeneratorAttribute !== null) { | ||
| 341 | $metadata->setSequenceGeneratorDefinition( | ||
| 342 | [ | ||
| 343 | 'sequenceName' => $seqGeneratorAttribute->sequenceName, | ||
| 344 | 'allocationSize' => $seqGeneratorAttribute->allocationSize, | ||
| 345 | 'initialValue' => $seqGeneratorAttribute->initialValue, | ||
| 346 | ], | ||
| 347 | ); | ||
| 348 | } elseif ($customGeneratorAttribute !== null) { | ||
| 349 | $metadata->setCustomGeneratorDefinition( | ||
| 350 | [ | ||
| 351 | 'class' => $customGeneratorAttribute->class, | ||
| 352 | ], | ||
| 353 | ); | ||
| 354 | } | ||
| 355 | } elseif ($oneToOneAttribute !== null) { | ||
| 356 | if ($metadata->isEmbeddedClass) { | ||
| 357 | throw MappingException::invalidAttributeOnEmbeddable($metadata->name, Mapping\OneToOne::class); | ||
| 358 | } | ||
| 359 | |||
| 360 | if ($this->reader->getPropertyAttribute($property, Mapping\Id::class)) { | ||
| 361 | $mapping['id'] = true; | ||
| 362 | } | ||
| 363 | |||
| 364 | $mapping['targetEntity'] = $oneToOneAttribute->targetEntity; | ||
| 365 | $mapping['joinColumns'] = $joinColumns; | ||
| 366 | $mapping['mappedBy'] = $oneToOneAttribute->mappedBy; | ||
| 367 | $mapping['inversedBy'] = $oneToOneAttribute->inversedBy; | ||
| 368 | $mapping['cascade'] = $oneToOneAttribute->cascade; | ||
| 369 | $mapping['orphanRemoval'] = $oneToOneAttribute->orphanRemoval; | ||
| 370 | $mapping['fetch'] = $this->getFetchMode($className, $oneToOneAttribute->fetch); | ||
| 371 | $metadata->mapOneToOne($mapping); | ||
| 372 | } elseif ($oneToManyAttribute !== null) { | ||
| 373 | if ($metadata->isEmbeddedClass) { | ||
| 374 | throw MappingException::invalidAttributeOnEmbeddable($metadata->name, Mapping\OneToMany::class); | ||
| 375 | } | ||
| 376 | |||
| 377 | $mapping['mappedBy'] = $oneToManyAttribute->mappedBy; | ||
| 378 | $mapping['targetEntity'] = $oneToManyAttribute->targetEntity; | ||
| 379 | $mapping['cascade'] = $oneToManyAttribute->cascade; | ||
| 380 | $mapping['indexBy'] = $oneToManyAttribute->indexBy; | ||
| 381 | $mapping['orphanRemoval'] = $oneToManyAttribute->orphanRemoval; | ||
| 382 | $mapping['fetch'] = $this->getFetchMode($className, $oneToManyAttribute->fetch); | ||
| 383 | |||
| 384 | $orderByAttribute = $this->reader->getPropertyAttribute($property, Mapping\OrderBy::class); | ||
| 385 | |||
| 386 | if ($orderByAttribute !== null) { | ||
| 387 | $mapping['orderBy'] = $orderByAttribute->value; | ||
| 388 | } | ||
| 389 | |||
| 390 | $metadata->mapOneToMany($mapping); | ||
| 391 | } elseif ($manyToOneAttribute !== null) { | ||
| 392 | if ($metadata->isEmbeddedClass) { | ||
| 393 | throw MappingException::invalidAttributeOnEmbeddable($metadata->name, Mapping\OneToMany::class); | ||
| 394 | } | ||
| 395 | |||
| 396 | $idAttribute = $this->reader->getPropertyAttribute($property, Mapping\Id::class); | ||
| 397 | |||
| 398 | if ($idAttribute !== null) { | ||
| 399 | $mapping['id'] = true; | ||
| 400 | } | ||
| 401 | |||
| 402 | $mapping['joinColumns'] = $joinColumns; | ||
| 403 | $mapping['cascade'] = $manyToOneAttribute->cascade; | ||
| 404 | $mapping['inversedBy'] = $manyToOneAttribute->inversedBy; | ||
| 405 | $mapping['targetEntity'] = $manyToOneAttribute->targetEntity; | ||
| 406 | $mapping['fetch'] = $this->getFetchMode($className, $manyToOneAttribute->fetch); | ||
| 407 | $metadata->mapManyToOne($mapping); | ||
| 408 | } elseif ($manyToManyAttribute !== null) { | ||
| 409 | if ($metadata->isEmbeddedClass) { | ||
| 410 | throw MappingException::invalidAttributeOnEmbeddable($metadata->name, Mapping\ManyToMany::class); | ||
| 411 | } | ||
| 412 | |||
| 413 | $joinTable = []; | ||
| 414 | $joinTableAttribute = $this->reader->getPropertyAttribute($property, Mapping\JoinTable::class); | ||
| 415 | |||
| 416 | if ($joinTableAttribute !== null) { | ||
| 417 | $joinTable = [ | ||
| 418 | 'name' => $joinTableAttribute->name, | ||
| 419 | 'schema' => $joinTableAttribute->schema, | ||
| 420 | ]; | ||
| 421 | |||
| 422 | if ($joinTableAttribute->options) { | ||
| 423 | $joinTable['options'] = $joinTableAttribute->options; | ||
| 424 | } | ||
| 425 | |||
| 426 | foreach ($joinTableAttribute->joinColumns as $joinColumn) { | ||
| 427 | $joinTable['joinColumns'][] = $this->joinColumnToArray($joinColumn); | ||
| 428 | } | ||
| 429 | |||
| 430 | foreach ($joinTableAttribute->inverseJoinColumns as $joinColumn) { | ||
| 431 | $joinTable['inverseJoinColumns'][] = $this->joinColumnToArray($joinColumn); | ||
| 432 | } | ||
| 433 | } | ||
| 434 | |||
| 435 | foreach ($this->reader->getPropertyAttributeCollection($property, Mapping\JoinColumn::class) as $joinColumn) { | ||
| 436 | $joinTable['joinColumns'][] = $this->joinColumnToArray($joinColumn); | ||
| 437 | } | ||
| 438 | |||
| 439 | foreach ($this->reader->getPropertyAttributeCollection($property, Mapping\InverseJoinColumn::class) as $joinColumn) { | ||
| 440 | $joinTable['inverseJoinColumns'][] = $this->joinColumnToArray($joinColumn); | ||
| 441 | } | ||
| 442 | |||
| 443 | $mapping['joinTable'] = $joinTable; | ||
| 444 | $mapping['targetEntity'] = $manyToManyAttribute->targetEntity; | ||
| 445 | $mapping['mappedBy'] = $manyToManyAttribute->mappedBy; | ||
| 446 | $mapping['inversedBy'] = $manyToManyAttribute->inversedBy; | ||
| 447 | $mapping['cascade'] = $manyToManyAttribute->cascade; | ||
| 448 | $mapping['indexBy'] = $manyToManyAttribute->indexBy; | ||
| 449 | $mapping['orphanRemoval'] = $manyToManyAttribute->orphanRemoval; | ||
| 450 | $mapping['fetch'] = $this->getFetchMode($className, $manyToManyAttribute->fetch); | ||
| 451 | |||
| 452 | $orderByAttribute = $this->reader->getPropertyAttribute($property, Mapping\OrderBy::class); | ||
| 453 | |||
| 454 | if ($orderByAttribute !== null) { | ||
| 455 | $mapping['orderBy'] = $orderByAttribute->value; | ||
| 456 | } | ||
| 457 | |||
| 458 | $metadata->mapManyToMany($mapping); | ||
| 459 | } elseif ($embeddedAttribute !== null) { | ||
| 460 | $mapping['class'] = $embeddedAttribute->class; | ||
| 461 | $mapping['columnPrefix'] = $embeddedAttribute->columnPrefix; | ||
| 462 | |||
| 463 | $metadata->mapEmbedded($mapping); | ||
| 464 | } | ||
| 465 | } | ||
| 466 | |||
| 467 | // Evaluate AssociationOverrides attribute | ||
| 468 | if (isset($classAttributes[Mapping\AssociationOverrides::class])) { | ||
| 469 | if ($metadata->isEmbeddedClass) { | ||
| 470 | throw MappingException::invalidAttributeOnEmbeddable($metadata->name, Mapping\AssociationOverride::class); | ||
| 471 | } | ||
| 472 | |||
| 473 | $associationOverride = $classAttributes[Mapping\AssociationOverrides::class]; | ||
| 474 | |||
| 475 | foreach ($associationOverride->overrides as $associationOverride) { | ||
| 476 | $override = []; | ||
| 477 | $fieldName = $associationOverride->name; | ||
| 478 | |||
| 479 | // Check for JoinColumn/JoinColumns attributes | ||
| 480 | if ($associationOverride->joinColumns) { | ||
| 481 | $joinColumns = []; | ||
| 482 | |||
| 483 | foreach ($associationOverride->joinColumns as $joinColumn) { | ||
| 484 | $joinColumns[] = $this->joinColumnToArray($joinColumn); | ||
| 485 | } | ||
| 486 | |||
| 487 | $override['joinColumns'] = $joinColumns; | ||
| 488 | } | ||
| 489 | |||
| 490 | if ($associationOverride->inverseJoinColumns) { | ||
| 491 | $joinColumns = []; | ||
| 492 | |||
| 493 | foreach ($associationOverride->inverseJoinColumns as $joinColumn) { | ||
| 494 | $joinColumns[] = $this->joinColumnToArray($joinColumn); | ||
| 495 | } | ||
| 496 | |||
| 497 | $override['inverseJoinColumns'] = $joinColumns; | ||
| 498 | } | ||
| 499 | |||
| 500 | // Check for JoinTable attributes | ||
| 501 | if ($associationOverride->joinTable) { | ||
| 502 | $joinTableAnnot = $associationOverride->joinTable; | ||
| 503 | $joinTable = [ | ||
| 504 | 'name' => $joinTableAnnot->name, | ||
| 505 | 'schema' => $joinTableAnnot->schema, | ||
| 506 | 'joinColumns' => $override['joinColumns'] ?? [], | ||
| 507 | 'inverseJoinColumns' => $override['inverseJoinColumns'] ?? [], | ||
| 508 | ]; | ||
| 509 | |||
| 510 | unset($override['joinColumns'], $override['inverseJoinColumns']); | ||
| 511 | |||
| 512 | $override['joinTable'] = $joinTable; | ||
| 513 | } | ||
| 514 | |||
| 515 | // Check for inversedBy | ||
| 516 | if ($associationOverride->inversedBy) { | ||
| 517 | $override['inversedBy'] = $associationOverride->inversedBy; | ||
| 518 | } | ||
| 519 | |||
| 520 | // Check for `fetch` | ||
| 521 | if ($associationOverride->fetch) { | ||
| 522 | $override['fetch'] = constant(ClassMetadata::class . '::FETCH_' . $associationOverride->fetch); | ||
| 523 | } | ||
| 524 | |||
| 525 | $metadata->setAssociationOverride($fieldName, $override); | ||
| 526 | } | ||
| 527 | } | ||
| 528 | |||
| 529 | // Evaluate AttributeOverrides attribute | ||
| 530 | if (isset($classAttributes[Mapping\AttributeOverrides::class])) { | ||
| 531 | if ($metadata->isEmbeddedClass) { | ||
| 532 | throw MappingException::invalidAttributeOnEmbeddable($metadata->name, Mapping\AttributeOverrides::class); | ||
| 533 | } | ||
| 534 | |||
| 535 | $attributeOverridesAnnot = $classAttributes[Mapping\AttributeOverrides::class]; | ||
| 536 | |||
| 537 | foreach ($attributeOverridesAnnot->overrides as $attributeOverride) { | ||
| 538 | $mapping = $this->columnToArray($attributeOverride->name, $attributeOverride->column); | ||
| 539 | |||
| 540 | $metadata->setAttributeOverride($attributeOverride->name, $mapping); | ||
| 541 | } | ||
| 542 | } | ||
| 543 | |||
| 544 | // Evaluate EntityListeners attribute | ||
| 545 | if (isset($classAttributes[Mapping\EntityListeners::class])) { | ||
| 546 | if ($metadata->isEmbeddedClass) { | ||
| 547 | throw MappingException::invalidAttributeOnEmbeddable($metadata->name, Mapping\EntityListeners::class); | ||
| 548 | } | ||
| 549 | |||
| 550 | $entityListenersAttribute = $classAttributes[Mapping\EntityListeners::class]; | ||
| 551 | |||
| 552 | foreach ($entityListenersAttribute->value as $item) { | ||
| 553 | $listenerClassName = $metadata->fullyQualifiedClassName($item); | ||
| 554 | |||
| 555 | if (! class_exists($listenerClassName)) { | ||
| 556 | throw MappingException::entityListenerClassNotFound($listenerClassName, $className); | ||
| 557 | } | ||
| 558 | |||
| 559 | $hasMapping = false; | ||
| 560 | $listenerClass = new ReflectionClass($listenerClassName); | ||
| 561 | |||
| 562 | foreach ($listenerClass->getMethods(ReflectionMethod::IS_PUBLIC) as $method) { | ||
| 563 | assert($method instanceof ReflectionMethod); | ||
| 564 | // find method callbacks. | ||
| 565 | $callbacks = $this->getMethodCallbacks($method); | ||
| 566 | $hasMapping = $hasMapping ?: ! empty($callbacks); | ||
| 567 | |||
| 568 | foreach ($callbacks as $value) { | ||
| 569 | $metadata->addEntityListener($value[1], $listenerClassName, $value[0]); | ||
| 570 | } | ||
| 571 | } | ||
| 572 | |||
| 573 | // Evaluate the listener using naming convention. | ||
| 574 | if (! $hasMapping) { | ||
| 575 | EntityListenerBuilder::bindEntityListener($metadata, $listenerClassName); | ||
| 576 | } | ||
| 577 | } | ||
| 578 | } | ||
| 579 | |||
| 580 | // Evaluate #[HasLifecycleCallbacks] attribute | ||
| 581 | if (isset($classAttributes[Mapping\HasLifecycleCallbacks::class])) { | ||
| 582 | if ($metadata->isEmbeddedClass) { | ||
| 583 | throw MappingException::invalidAttributeOnEmbeddable($metadata->name, Mapping\HasLifecycleCallbacks::class); | ||
| 584 | } | ||
| 585 | |||
| 586 | foreach ($reflectionClass->getMethods(ReflectionMethod::IS_PUBLIC) as $method) { | ||
| 587 | assert($method instanceof ReflectionMethod); | ||
| 588 | foreach ($this->getMethodCallbacks($method) as $value) { | ||
| 589 | $metadata->addLifecycleCallback($value[0], $value[1]); | ||
| 590 | } | ||
| 591 | } | ||
| 592 | } | ||
| 593 | } | ||
| 594 | |||
| 595 | /** | ||
| 596 | * Attempts to resolve the fetch mode. | ||
| 597 | * | ||
| 598 | * @param class-string $className The class name. | ||
| 599 | * @param string $fetchMode The fetch mode. | ||
| 600 | * | ||
| 601 | * @return ClassMetadata::FETCH_* The fetch mode as defined in ClassMetadata. | ||
| 602 | * | ||
| 603 | * @throws MappingException If the fetch mode is not valid. | ||
| 604 | */ | ||
| 605 | private function getFetchMode(string $className, string $fetchMode): int | ||
| 606 | { | ||
| 607 | if (! defined('Doctrine\ORM\Mapping\ClassMetadata::FETCH_' . $fetchMode)) { | ||
| 608 | throw MappingException::invalidFetchMode($className, $fetchMode); | ||
| 609 | } | ||
| 610 | |||
| 611 | return constant('Doctrine\ORM\Mapping\ClassMetadata::FETCH_' . $fetchMode); | ||
| 612 | } | ||
| 613 | |||
| 614 | /** | ||
| 615 | * Attempts to resolve the generated mode. | ||
| 616 | * | ||
| 617 | * @throws MappingException If the fetch mode is not valid. | ||
| 618 | */ | ||
| 619 | private function getGeneratedMode(string $generatedMode): int | ||
| 620 | { | ||
| 621 | if (! defined('Doctrine\ORM\Mapping\ClassMetadata::GENERATED_' . $generatedMode)) { | ||
| 622 | throw MappingException::invalidGeneratedMode($generatedMode); | ||
| 623 | } | ||
| 624 | |||
| 625 | return constant('Doctrine\ORM\Mapping\ClassMetadata::GENERATED_' . $generatedMode); | ||
| 626 | } | ||
| 627 | |||
| 628 | /** | ||
| 629 | * Parses the given method. | ||
| 630 | * | ||
| 631 | * @return list<array{string, string}> | ||
| 632 | * @psalm-return list<array{string, (Events::*)}> | ||
| 633 | */ | ||
| 634 | private function getMethodCallbacks(ReflectionMethod $method): array | ||
| 635 | { | ||
| 636 | $callbacks = []; | ||
| 637 | $attributes = $this->reader->getMethodAttributes($method); | ||
| 638 | |||
| 639 | foreach ($attributes as $attribute) { | ||
| 640 | if ($attribute instanceof Mapping\PrePersist) { | ||
| 641 | $callbacks[] = [$method->name, Events::prePersist]; | ||
| 642 | } | ||
| 643 | |||
| 644 | if ($attribute instanceof Mapping\PostPersist) { | ||
| 645 | $callbacks[] = [$method->name, Events::postPersist]; | ||
| 646 | } | ||
| 647 | |||
| 648 | if ($attribute instanceof Mapping\PreUpdate) { | ||
| 649 | $callbacks[] = [$method->name, Events::preUpdate]; | ||
| 650 | } | ||
| 651 | |||
| 652 | if ($attribute instanceof Mapping\PostUpdate) { | ||
| 653 | $callbacks[] = [$method->name, Events::postUpdate]; | ||
| 654 | } | ||
| 655 | |||
| 656 | if ($attribute instanceof Mapping\PreRemove) { | ||
| 657 | $callbacks[] = [$method->name, Events::preRemove]; | ||
| 658 | } | ||
| 659 | |||
| 660 | if ($attribute instanceof Mapping\PostRemove) { | ||
| 661 | $callbacks[] = [$method->name, Events::postRemove]; | ||
| 662 | } | ||
| 663 | |||
| 664 | if ($attribute instanceof Mapping\PostLoad) { | ||
| 665 | $callbacks[] = [$method->name, Events::postLoad]; | ||
| 666 | } | ||
| 667 | |||
| 668 | if ($attribute instanceof Mapping\PreFlush) { | ||
| 669 | $callbacks[] = [$method->name, Events::preFlush]; | ||
| 670 | } | ||
| 671 | } | ||
| 672 | |||
| 673 | return $callbacks; | ||
| 674 | } | ||
| 675 | |||
| 676 | /** | ||
| 677 | * Parse the given JoinColumn as array | ||
| 678 | * | ||
| 679 | * @return mixed[] | ||
| 680 | * @psalm-return array{ | ||
| 681 | * name: string|null, | ||
| 682 | * unique: bool, | ||
| 683 | * nullable: bool, | ||
| 684 | * onDelete: mixed, | ||
| 685 | * columnDefinition: string|null, | ||
| 686 | * referencedColumnName: string, | ||
| 687 | * options?: array<string, mixed> | ||
| 688 | * } | ||
| 689 | */ | ||
| 690 | private function joinColumnToArray(Mapping\JoinColumn|Mapping\InverseJoinColumn $joinColumn): array | ||
| 691 | { | ||
| 692 | $mapping = [ | ||
| 693 | 'name' => $joinColumn->name, | ||
| 694 | 'unique' => $joinColumn->unique, | ||
| 695 | 'nullable' => $joinColumn->nullable, | ||
| 696 | 'onDelete' => $joinColumn->onDelete, | ||
| 697 | 'columnDefinition' => $joinColumn->columnDefinition, | ||
| 698 | 'referencedColumnName' => $joinColumn->referencedColumnName, | ||
| 699 | ]; | ||
| 700 | |||
| 701 | if ($joinColumn->options) { | ||
| 702 | $mapping['options'] = $joinColumn->options; | ||
| 703 | } | ||
| 704 | |||
| 705 | return $mapping; | ||
| 706 | } | ||
| 707 | |||
| 708 | /** | ||
| 709 | * Parse the given Column as array | ||
| 710 | * | ||
| 711 | * @return mixed[] | ||
| 712 | * @psalm-return array{ | ||
| 713 | * fieldName: string, | ||
| 714 | * type: mixed, | ||
| 715 | * scale: int, | ||
| 716 | * length: int, | ||
| 717 | * unique: bool, | ||
| 718 | * nullable: bool, | ||
| 719 | * precision: int, | ||
| 720 | * enumType?: class-string, | ||
| 721 | * options?: mixed[], | ||
| 722 | * columnName?: string, | ||
| 723 | * columnDefinition?: string | ||
| 724 | * } | ||
| 725 | */ | ||
| 726 | private function columnToArray(string $fieldName, Mapping\Column $column): array | ||
| 727 | { | ||
| 728 | $mapping = [ | ||
| 729 | 'fieldName' => $fieldName, | ||
| 730 | 'type' => $column->type, | ||
| 731 | 'scale' => $column->scale, | ||
| 732 | 'length' => $column->length, | ||
| 733 | 'unique' => $column->unique, | ||
| 734 | 'nullable' => $column->nullable, | ||
| 735 | 'precision' => $column->precision, | ||
| 736 | ]; | ||
| 737 | |||
| 738 | if ($column->options) { | ||
| 739 | $mapping['options'] = $column->options; | ||
| 740 | } | ||
| 741 | |||
| 742 | if (isset($column->name)) { | ||
| 743 | $mapping['columnName'] = $column->name; | ||
| 744 | } | ||
| 745 | |||
| 746 | if (isset($column->columnDefinition)) { | ||
| 747 | $mapping['columnDefinition'] = $column->columnDefinition; | ||
| 748 | } | ||
| 749 | |||
| 750 | if ($column->updatable === false) { | ||
| 751 | $mapping['notUpdatable'] = true; | ||
| 752 | } | ||
| 753 | |||
| 754 | if ($column->insertable === false) { | ||
| 755 | $mapping['notInsertable'] = true; | ||
| 756 | } | ||
| 757 | |||
| 758 | if ($column->generated !== null) { | ||
| 759 | $mapping['generated'] = $this->getGeneratedMode($column->generated); | ||
| 760 | } | ||
| 761 | |||
| 762 | if ($column->enumType) { | ||
| 763 | $mapping['enumType'] = $column->enumType; | ||
| 764 | } | ||
| 765 | |||
| 766 | return $mapping; | ||
| 767 | } | ||
| 768 | } | ||
diff --git a/vendor/doctrine/orm/src/Mapping/Driver/AttributeReader.php b/vendor/doctrine/orm/src/Mapping/Driver/AttributeReader.php new file mode 100644 index 0000000..2de622a --- /dev/null +++ b/vendor/doctrine/orm/src/Mapping/Driver/AttributeReader.php | |||
| @@ -0,0 +1,146 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Mapping\Driver; | ||
| 6 | |||
| 7 | use Attribute; | ||
| 8 | use Doctrine\ORM\Mapping\MappingAttribute; | ||
| 9 | use LogicException; | ||
| 10 | use ReflectionAttribute; | ||
| 11 | use ReflectionClass; | ||
| 12 | use ReflectionMethod; | ||
| 13 | use ReflectionProperty; | ||
| 14 | |||
| 15 | use function assert; | ||
| 16 | use function is_string; | ||
| 17 | use function is_subclass_of; | ||
| 18 | use function sprintf; | ||
| 19 | |||
| 20 | /** @internal */ | ||
| 21 | final class AttributeReader | ||
| 22 | { | ||
| 23 | /** @var array<class-string<MappingAttribute>, bool> */ | ||
| 24 | private array $isRepeatableAttribute = []; | ||
| 25 | |||
| 26 | /** | ||
| 27 | * @psalm-return class-string-map<T, T|RepeatableAttributeCollection<T>> | ||
| 28 | * | ||
| 29 | * @template T of MappingAttribute | ||
| 30 | */ | ||
| 31 | public function getClassAttributes(ReflectionClass $class): array | ||
| 32 | { | ||
| 33 | return $this->convertToAttributeInstances($class->getAttributes()); | ||
| 34 | } | ||
| 35 | |||
| 36 | /** | ||
| 37 | * @return class-string-map<T, T|RepeatableAttributeCollection<T>> | ||
| 38 | * | ||
| 39 | * @template T of MappingAttribute | ||
| 40 | */ | ||
| 41 | public function getMethodAttributes(ReflectionMethod $method): array | ||
| 42 | { | ||
| 43 | return $this->convertToAttributeInstances($method->getAttributes()); | ||
| 44 | } | ||
| 45 | |||
| 46 | /** | ||
| 47 | * @return class-string-map<T, T|RepeatableAttributeCollection<T>> | ||
| 48 | * | ||
| 49 | * @template T of MappingAttribute | ||
| 50 | */ | ||
| 51 | public function getPropertyAttributes(ReflectionProperty $property): array | ||
| 52 | { | ||
| 53 | return $this->convertToAttributeInstances($property->getAttributes()); | ||
| 54 | } | ||
| 55 | |||
| 56 | /** | ||
| 57 | * @param class-string<T> $attributeName The name of the annotation. | ||
| 58 | * | ||
| 59 | * @return T|null | ||
| 60 | * | ||
| 61 | * @template T of MappingAttribute | ||
| 62 | */ | ||
| 63 | public function getPropertyAttribute(ReflectionProperty $property, string $attributeName) | ||
| 64 | { | ||
| 65 | if ($this->isRepeatable($attributeName)) { | ||
| 66 | throw new LogicException(sprintf( | ||
| 67 | 'The attribute "%s" is repeatable. Call getPropertyAttributeCollection() instead.', | ||
| 68 | $attributeName, | ||
| 69 | )); | ||
| 70 | } | ||
| 71 | |||
| 72 | return $this->getPropertyAttributes($property)[$attributeName] ?? null; | ||
| 73 | } | ||
| 74 | |||
| 75 | /** | ||
| 76 | * @param class-string<T> $attributeName The name of the annotation. | ||
| 77 | * | ||
| 78 | * @return RepeatableAttributeCollection<T> | ||
| 79 | * | ||
| 80 | * @template T of MappingAttribute | ||
| 81 | */ | ||
| 82 | public function getPropertyAttributeCollection( | ||
| 83 | ReflectionProperty $property, | ||
| 84 | string $attributeName, | ||
| 85 | ): RepeatableAttributeCollection { | ||
| 86 | if (! $this->isRepeatable($attributeName)) { | ||
| 87 | throw new LogicException(sprintf( | ||
| 88 | 'The attribute "%s" is not repeatable. Call getPropertyAttribute() instead.', | ||
| 89 | $attributeName, | ||
| 90 | )); | ||
| 91 | } | ||
| 92 | |||
| 93 | return $this->getPropertyAttributes($property)[$attributeName] ?? new RepeatableAttributeCollection(); | ||
| 94 | } | ||
| 95 | |||
| 96 | /** | ||
| 97 | * @param array<ReflectionAttribute> $attributes | ||
| 98 | * | ||
| 99 | * @return class-string-map<T, T|RepeatableAttributeCollection<T>> | ||
| 100 | * | ||
| 101 | * @template T of MappingAttribute | ||
| 102 | */ | ||
| 103 | private function convertToAttributeInstances(array $attributes): array | ||
| 104 | { | ||
| 105 | $instances = []; | ||
| 106 | |||
| 107 | foreach ($attributes as $attribute) { | ||
| 108 | $attributeName = $attribute->getName(); | ||
| 109 | assert(is_string($attributeName)); | ||
| 110 | // Make sure we only get Doctrine Attributes | ||
| 111 | if (! is_subclass_of($attributeName, MappingAttribute::class)) { | ||
| 112 | continue; | ||
| 113 | } | ||
| 114 | |||
| 115 | $instance = $attribute->newInstance(); | ||
| 116 | assert($instance instanceof MappingAttribute); | ||
| 117 | |||
| 118 | if ($this->isRepeatable($attributeName)) { | ||
| 119 | if (! isset($instances[$attributeName])) { | ||
| 120 | $instances[$attributeName] = new RepeatableAttributeCollection(); | ||
| 121 | } | ||
| 122 | |||
| 123 | $collection = $instances[$attributeName]; | ||
| 124 | assert($collection instanceof RepeatableAttributeCollection); | ||
| 125 | $collection[] = $instance; | ||
| 126 | } else { | ||
| 127 | $instances[$attributeName] = $instance; | ||
| 128 | } | ||
| 129 | } | ||
| 130 | |||
| 131 | return $instances; | ||
| 132 | } | ||
| 133 | |||
| 134 | /** @param class-string<MappingAttribute> $attributeClassName */ | ||
| 135 | private function isRepeatable(string $attributeClassName): bool | ||
| 136 | { | ||
| 137 | if (isset($this->isRepeatableAttribute[$attributeClassName])) { | ||
| 138 | return $this->isRepeatableAttribute[$attributeClassName]; | ||
| 139 | } | ||
| 140 | |||
| 141 | $reflectionClass = new ReflectionClass($attributeClassName); | ||
| 142 | $attribute = $reflectionClass->getAttributes()[0]->newInstance(); | ||
| 143 | |||
| 144 | return $this->isRepeatableAttribute[$attributeClassName] = ($attribute->flags & Attribute::IS_REPEATABLE) > 0; | ||
| 145 | } | ||
| 146 | } | ||
diff --git a/vendor/doctrine/orm/src/Mapping/Driver/DatabaseDriver.php b/vendor/doctrine/orm/src/Mapping/Driver/DatabaseDriver.php new file mode 100644 index 0000000..49e2e93 --- /dev/null +++ b/vendor/doctrine/orm/src/Mapping/Driver/DatabaseDriver.php | |||
| @@ -0,0 +1,528 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Mapping\Driver; | ||
| 6 | |||
| 7 | use Doctrine\DBAL\Schema\AbstractSchemaManager; | ||
| 8 | use Doctrine\DBAL\Schema\Column; | ||
| 9 | use Doctrine\DBAL\Schema\SchemaException; | ||
| 10 | use Doctrine\DBAL\Schema\Table; | ||
| 11 | use Doctrine\DBAL\Types\Type; | ||
| 12 | use Doctrine\DBAL\Types\Types; | ||
| 13 | use Doctrine\Inflector\Inflector; | ||
| 14 | use Doctrine\Inflector\InflectorFactory; | ||
| 15 | use Doctrine\ORM\Mapping\ClassMetadata; | ||
| 16 | use Doctrine\ORM\Mapping\MappingException; | ||
| 17 | use Doctrine\Persistence\Mapping\ClassMetadata as PersistenceClassMetadata; | ||
| 18 | use Doctrine\Persistence\Mapping\Driver\MappingDriver; | ||
| 19 | use InvalidArgumentException; | ||
| 20 | use TypeError; | ||
| 21 | |||
| 22 | use function array_diff; | ||
| 23 | use function array_keys; | ||
| 24 | use function array_merge; | ||
| 25 | use function assert; | ||
| 26 | use function count; | ||
| 27 | use function current; | ||
| 28 | use function get_debug_type; | ||
| 29 | use function in_array; | ||
| 30 | use function preg_replace; | ||
| 31 | use function sort; | ||
| 32 | use function sprintf; | ||
| 33 | use function strtolower; | ||
| 34 | |||
| 35 | /** | ||
| 36 | * The DatabaseDriver reverse engineers the mapping metadata from a database. | ||
| 37 | * | ||
| 38 | * @link www.doctrine-project.org | ||
| 39 | */ | ||
| 40 | class DatabaseDriver implements MappingDriver | ||
| 41 | { | ||
| 42 | /** | ||
| 43 | * Replacement for {@see Types::ARRAY}. | ||
| 44 | * | ||
| 45 | * To be removed as soon as support for DBAL 3 is dropped. | ||
| 46 | */ | ||
| 47 | private const ARRAY = 'array'; | ||
| 48 | |||
| 49 | /** | ||
| 50 | * Replacement for {@see Types::OBJECT}. | ||
| 51 | * | ||
| 52 | * To be removed as soon as support for DBAL 3 is dropped. | ||
| 53 | */ | ||
| 54 | private const OBJECT = 'object'; | ||
| 55 | |||
| 56 | /** @var array<string,Table>|null */ | ||
| 57 | private array|null $tables = null; | ||
| 58 | |||
| 59 | /** @var array<class-string, string> */ | ||
| 60 | private array $classToTableNames = []; | ||
| 61 | |||
| 62 | /** @psalm-var array<string, Table> */ | ||
| 63 | private array $manyToManyTables = []; | ||
| 64 | |||
| 65 | /** @var mixed[] */ | ||
| 66 | private array $classNamesForTables = []; | ||
| 67 | |||
| 68 | /** @var mixed[] */ | ||
| 69 | private array $fieldNamesForColumns = []; | ||
| 70 | |||
| 71 | /** | ||
| 72 | * The namespace for the generated entities. | ||
| 73 | */ | ||
| 74 | private string|null $namespace = null; | ||
| 75 | |||
| 76 | private Inflector $inflector; | ||
| 77 | |||
| 78 | public function __construct(private readonly AbstractSchemaManager $sm) | ||
| 79 | { | ||
| 80 | $this->inflector = InflectorFactory::create()->build(); | ||
| 81 | } | ||
| 82 | |||
| 83 | /** | ||
| 84 | * Set the namespace for the generated entities. | ||
| 85 | */ | ||
| 86 | public function setNamespace(string $namespace): void | ||
| 87 | { | ||
| 88 | $this->namespace = $namespace; | ||
| 89 | } | ||
| 90 | |||
| 91 | public function isTransient(string $className): bool | ||
| 92 | { | ||
| 93 | return true; | ||
| 94 | } | ||
| 95 | |||
| 96 | /** | ||
| 97 | * {@inheritDoc} | ||
| 98 | */ | ||
| 99 | public function getAllClassNames(): array | ||
| 100 | { | ||
| 101 | $this->reverseEngineerMappingFromDatabase(); | ||
| 102 | |||
| 103 | return array_keys($this->classToTableNames); | ||
| 104 | } | ||
| 105 | |||
| 106 | /** | ||
| 107 | * Sets class name for a table. | ||
| 108 | */ | ||
| 109 | public function setClassNameForTable(string $tableName, string $className): void | ||
| 110 | { | ||
| 111 | $this->classNamesForTables[$tableName] = $className; | ||
| 112 | } | ||
| 113 | |||
| 114 | /** | ||
| 115 | * Sets field name for a column on a specific table. | ||
| 116 | */ | ||
| 117 | public function setFieldNameForColumn(string $tableName, string $columnName, string $fieldName): void | ||
| 118 | { | ||
| 119 | $this->fieldNamesForColumns[$tableName][$columnName] = $fieldName; | ||
| 120 | } | ||
| 121 | |||
| 122 | /** | ||
| 123 | * Sets tables manually instead of relying on the reverse engineering capabilities of SchemaManager. | ||
| 124 | * | ||
| 125 | * @param Table[] $entityTables | ||
| 126 | * @param Table[] $manyToManyTables | ||
| 127 | * @psalm-param list<Table> $entityTables | ||
| 128 | * @psalm-param list<Table> $manyToManyTables | ||
| 129 | */ | ||
| 130 | public function setTables(array $entityTables, array $manyToManyTables): void | ||
| 131 | { | ||
| 132 | $this->tables = $this->manyToManyTables = $this->classToTableNames = []; | ||
| 133 | |||
| 134 | foreach ($entityTables as $table) { | ||
| 135 | $className = $this->getClassNameForTable($table->getName()); | ||
| 136 | |||
| 137 | $this->classToTableNames[$className] = $table->getName(); | ||
| 138 | $this->tables[$table->getName()] = $table; | ||
| 139 | } | ||
| 140 | |||
| 141 | foreach ($manyToManyTables as $table) { | ||
| 142 | $this->manyToManyTables[$table->getName()] = $table; | ||
| 143 | } | ||
| 144 | } | ||
| 145 | |||
| 146 | public function setInflector(Inflector $inflector): void | ||
| 147 | { | ||
| 148 | $this->inflector = $inflector; | ||
| 149 | } | ||
| 150 | |||
| 151 | /** | ||
| 152 | * {@inheritDoc} | ||
| 153 | * | ||
| 154 | * @psalm-param class-string<T> $className | ||
| 155 | * @psalm-param ClassMetadata<T> $metadata | ||
| 156 | * | ||
| 157 | * @template T of object | ||
| 158 | */ | ||
| 159 | public function loadMetadataForClass(string $className, PersistenceClassMetadata $metadata): void | ||
| 160 | { | ||
| 161 | if (! $metadata instanceof ClassMetadata) { | ||
| 162 | throw new TypeError(sprintf( | ||
| 163 | 'Argument #2 passed to %s() must be an instance of %s, %s given.', | ||
| 164 | __METHOD__, | ||
| 165 | ClassMetadata::class, | ||
| 166 | get_debug_type($metadata), | ||
| 167 | )); | ||
| 168 | } | ||
| 169 | |||
| 170 | $this->reverseEngineerMappingFromDatabase(); | ||
| 171 | |||
| 172 | if (! isset($this->classToTableNames[$className])) { | ||
| 173 | throw new InvalidArgumentException('Unknown class ' . $className); | ||
| 174 | } | ||
| 175 | |||
| 176 | $tableName = $this->classToTableNames[$className]; | ||
| 177 | |||
| 178 | $metadata->name = $className; | ||
| 179 | $metadata->table['name'] = $tableName; | ||
| 180 | |||
| 181 | $this->buildIndexes($metadata); | ||
| 182 | $this->buildFieldMappings($metadata); | ||
| 183 | $this->buildToOneAssociationMappings($metadata); | ||
| 184 | |||
| 185 | foreach ($this->manyToManyTables as $manyTable) { | ||
| 186 | foreach ($manyTable->getForeignKeys() as $foreignKey) { | ||
| 187 | // foreign key maps to the table of the current entity, many to many association probably exists | ||
| 188 | if (! (strtolower($tableName) === strtolower($foreignKey->getForeignTableName()))) { | ||
| 189 | continue; | ||
| 190 | } | ||
| 191 | |||
| 192 | $myFk = $foreignKey; | ||
| 193 | $otherFk = null; | ||
| 194 | |||
| 195 | foreach ($manyTable->getForeignKeys() as $foreignKey) { | ||
| 196 | if ($foreignKey !== $myFk) { | ||
| 197 | $otherFk = $foreignKey; | ||
| 198 | break; | ||
| 199 | } | ||
| 200 | } | ||
| 201 | |||
| 202 | if (! $otherFk) { | ||
| 203 | // the definition of this many to many table does not contain | ||
| 204 | // enough foreign key information to continue reverse engineering. | ||
| 205 | continue; | ||
| 206 | } | ||
| 207 | |||
| 208 | $localColumn = current($myFk->getLocalColumns()); | ||
| 209 | |||
| 210 | $associationMapping = []; | ||
| 211 | $associationMapping['fieldName'] = $this->getFieldNameForColumn($manyTable->getName(), current($otherFk->getLocalColumns()), true); | ||
| 212 | $associationMapping['targetEntity'] = $this->getClassNameForTable($otherFk->getForeignTableName()); | ||
| 213 | |||
| 214 | if (current($manyTable->getColumns())->getName() === $localColumn) { | ||
| 215 | $associationMapping['inversedBy'] = $this->getFieldNameForColumn($manyTable->getName(), current($myFk->getLocalColumns()), true); | ||
| 216 | $associationMapping['joinTable'] = [ | ||
| 217 | 'name' => strtolower($manyTable->getName()), | ||
| 218 | 'joinColumns' => [], | ||
| 219 | 'inverseJoinColumns' => [], | ||
| 220 | ]; | ||
| 221 | |||
| 222 | $fkCols = $myFk->getForeignColumns(); | ||
| 223 | $cols = $myFk->getLocalColumns(); | ||
| 224 | |||
| 225 | for ($i = 0, $colsCount = count($cols); $i < $colsCount; $i++) { | ||
| 226 | $associationMapping['joinTable']['joinColumns'][] = [ | ||
| 227 | 'name' => $cols[$i], | ||
| 228 | 'referencedColumnName' => $fkCols[$i], | ||
| 229 | ]; | ||
| 230 | } | ||
| 231 | |||
| 232 | $fkCols = $otherFk->getForeignColumns(); | ||
| 233 | $cols = $otherFk->getLocalColumns(); | ||
| 234 | |||
| 235 | for ($i = 0, $colsCount = count($cols); $i < $colsCount; $i++) { | ||
| 236 | $associationMapping['joinTable']['inverseJoinColumns'][] = [ | ||
| 237 | 'name' => $cols[$i], | ||
| 238 | 'referencedColumnName' => $fkCols[$i], | ||
| 239 | ]; | ||
| 240 | } | ||
| 241 | } else { | ||
| 242 | $associationMapping['mappedBy'] = $this->getFieldNameForColumn($manyTable->getName(), current($myFk->getLocalColumns()), true); | ||
| 243 | } | ||
| 244 | |||
| 245 | $metadata->mapManyToMany($associationMapping); | ||
| 246 | |||
| 247 | break; | ||
| 248 | } | ||
| 249 | } | ||
| 250 | } | ||
| 251 | |||
| 252 | /** @throws MappingException */ | ||
| 253 | private function reverseEngineerMappingFromDatabase(): void | ||
| 254 | { | ||
| 255 | if ($this->tables !== null) { | ||
| 256 | return; | ||
| 257 | } | ||
| 258 | |||
| 259 | $this->tables = $this->manyToManyTables = $this->classToTableNames = []; | ||
| 260 | |||
| 261 | foreach ($this->sm->listTables() as $table) { | ||
| 262 | $tableName = $table->getName(); | ||
| 263 | $foreignKeys = $table->getForeignKeys(); | ||
| 264 | |||
| 265 | $allForeignKeyColumns = []; | ||
| 266 | |||
| 267 | foreach ($foreignKeys as $foreignKey) { | ||
| 268 | $allForeignKeyColumns = array_merge($allForeignKeyColumns, $foreignKey->getLocalColumns()); | ||
| 269 | } | ||
| 270 | |||
| 271 | $primaryKey = $table->getPrimaryKey(); | ||
| 272 | if ($primaryKey === null) { | ||
| 273 | throw new MappingException( | ||
| 274 | 'Table ' . $tableName . ' has no primary key. Doctrine does not ' . | ||
| 275 | "support reverse engineering from tables that don't have a primary key.", | ||
| 276 | ); | ||
| 277 | } | ||
| 278 | |||
| 279 | $pkColumns = $primaryKey->getColumns(); | ||
| 280 | |||
| 281 | sort($pkColumns); | ||
| 282 | sort($allForeignKeyColumns); | ||
| 283 | |||
| 284 | if ($pkColumns === $allForeignKeyColumns && count($foreignKeys) === 2) { | ||
| 285 | $this->manyToManyTables[$tableName] = $table; | ||
| 286 | } else { | ||
| 287 | // lower-casing is necessary because of Oracle Uppercase Tablenames, | ||
| 288 | // assumption is lower-case + underscore separated. | ||
| 289 | $className = $this->getClassNameForTable($tableName); | ||
| 290 | |||
| 291 | $this->tables[$tableName] = $table; | ||
| 292 | $this->classToTableNames[$className] = $tableName; | ||
| 293 | } | ||
| 294 | } | ||
| 295 | } | ||
| 296 | |||
| 297 | /** | ||
| 298 | * Build indexes from a class metadata. | ||
| 299 | */ | ||
| 300 | private function buildIndexes(ClassMetadata $metadata): void | ||
| 301 | { | ||
| 302 | $tableName = $metadata->table['name']; | ||
| 303 | $indexes = $this->tables[$tableName]->getIndexes(); | ||
| 304 | |||
| 305 | foreach ($indexes as $index) { | ||
| 306 | if ($index->isPrimary()) { | ||
| 307 | continue; | ||
| 308 | } | ||
| 309 | |||
| 310 | $indexName = $index->getName(); | ||
| 311 | $indexColumns = $index->getColumns(); | ||
| 312 | $constraintType = $index->isUnique() | ||
| 313 | ? 'uniqueConstraints' | ||
| 314 | : 'indexes'; | ||
| 315 | |||
| 316 | $metadata->table[$constraintType][$indexName]['columns'] = $indexColumns; | ||
| 317 | } | ||
| 318 | } | ||
| 319 | |||
| 320 | /** | ||
| 321 | * Build field mapping from class metadata. | ||
| 322 | */ | ||
| 323 | private function buildFieldMappings(ClassMetadata $metadata): void | ||
| 324 | { | ||
| 325 | $tableName = $metadata->table['name']; | ||
| 326 | $columns = $this->tables[$tableName]->getColumns(); | ||
| 327 | $primaryKeys = $this->getTablePrimaryKeys($this->tables[$tableName]); | ||
| 328 | $foreignKeys = $this->tables[$tableName]->getForeignKeys(); | ||
| 329 | $allForeignKeys = []; | ||
| 330 | |||
| 331 | foreach ($foreignKeys as $foreignKey) { | ||
| 332 | $allForeignKeys = array_merge($allForeignKeys, $foreignKey->getLocalColumns()); | ||
| 333 | } | ||
| 334 | |||
| 335 | $ids = []; | ||
| 336 | $fieldMappings = []; | ||
| 337 | |||
| 338 | foreach ($columns as $column) { | ||
| 339 | if (in_array($column->getName(), $allForeignKeys, true)) { | ||
| 340 | continue; | ||
| 341 | } | ||
| 342 | |||
| 343 | $fieldMapping = $this->buildFieldMapping($tableName, $column); | ||
| 344 | |||
| 345 | if ($primaryKeys && in_array($column->getName(), $primaryKeys, true)) { | ||
| 346 | $fieldMapping['id'] = true; | ||
| 347 | $ids[] = $fieldMapping; | ||
| 348 | } | ||
| 349 | |||
| 350 | $fieldMappings[] = $fieldMapping; | ||
| 351 | } | ||
| 352 | |||
| 353 | // We need to check for the columns here, because we might have associations as id as well. | ||
| 354 | if ($ids && count($primaryKeys) === 1) { | ||
| 355 | $metadata->setIdGeneratorType(ClassMetadata::GENERATOR_TYPE_AUTO); | ||
| 356 | } | ||
| 357 | |||
| 358 | foreach ($fieldMappings as $fieldMapping) { | ||
| 359 | $metadata->mapField($fieldMapping); | ||
| 360 | } | ||
| 361 | } | ||
| 362 | |||
| 363 | /** | ||
| 364 | * Build field mapping from a schema column definition | ||
| 365 | * | ||
| 366 | * @return mixed[] | ||
| 367 | * @psalm-return array{ | ||
| 368 | * fieldName: string, | ||
| 369 | * columnName: string, | ||
| 370 | * type: string, | ||
| 371 | * nullable: bool, | ||
| 372 | * options: array{ | ||
| 373 | * unsigned?: bool, | ||
| 374 | * fixed?: bool, | ||
| 375 | * comment: string|null, | ||
| 376 | * default?: mixed | ||
| 377 | * }, | ||
| 378 | * precision?: int, | ||
| 379 | * scale?: int, | ||
| 380 | * length?: int|null | ||
| 381 | * } | ||
| 382 | */ | ||
| 383 | private function buildFieldMapping(string $tableName, Column $column): array | ||
| 384 | { | ||
| 385 | $fieldMapping = [ | ||
| 386 | 'fieldName' => $this->getFieldNameForColumn($tableName, $column->getName(), false), | ||
| 387 | 'columnName' => $column->getName(), | ||
| 388 | 'type' => Type::getTypeRegistry()->lookupName($column->getType()), | ||
| 389 | 'nullable' => ! $column->getNotnull(), | ||
| 390 | 'options' => [ | ||
| 391 | 'comment' => $column->getComment(), | ||
| 392 | ], | ||
| 393 | ]; | ||
| 394 | |||
| 395 | // Type specific elements | ||
| 396 | switch ($fieldMapping['type']) { | ||
| 397 | case self::ARRAY: | ||
| 398 | case Types::BLOB: | ||
| 399 | case Types::GUID: | ||
| 400 | case self::OBJECT: | ||
| 401 | case Types::SIMPLE_ARRAY: | ||
| 402 | case Types::STRING: | ||
| 403 | case Types::TEXT: | ||
| 404 | $fieldMapping['length'] = $column->getLength(); | ||
| 405 | $fieldMapping['options']['fixed'] = $column->getFixed(); | ||
| 406 | break; | ||
| 407 | |||
| 408 | case Types::DECIMAL: | ||
| 409 | case Types::FLOAT: | ||
| 410 | $fieldMapping['precision'] = $column->getPrecision(); | ||
| 411 | $fieldMapping['scale'] = $column->getScale(); | ||
| 412 | break; | ||
| 413 | |||
| 414 | case Types::INTEGER: | ||
| 415 | case Types::BIGINT: | ||
| 416 | case Types::SMALLINT: | ||
| 417 | $fieldMapping['options']['unsigned'] = $column->getUnsigned(); | ||
| 418 | break; | ||
| 419 | } | ||
| 420 | |||
| 421 | // Default | ||
| 422 | $default = $column->getDefault(); | ||
| 423 | if ($default !== null) { | ||
| 424 | $fieldMapping['options']['default'] = $default; | ||
| 425 | } | ||
| 426 | |||
| 427 | return $fieldMapping; | ||
| 428 | } | ||
| 429 | |||
| 430 | /** | ||
| 431 | * Build to one (one to one, many to one) association mapping from class metadata. | ||
| 432 | */ | ||
| 433 | private function buildToOneAssociationMappings(ClassMetadata $metadata): void | ||
| 434 | { | ||
| 435 | assert($this->tables !== null); | ||
| 436 | |||
| 437 | $tableName = $metadata->table['name']; | ||
| 438 | $primaryKeys = $this->getTablePrimaryKeys($this->tables[$tableName]); | ||
| 439 | $foreignKeys = $this->tables[$tableName]->getForeignKeys(); | ||
| 440 | |||
| 441 | foreach ($foreignKeys as $foreignKey) { | ||
| 442 | $foreignTableName = $foreignKey->getForeignTableName(); | ||
| 443 | $fkColumns = $foreignKey->getLocalColumns(); | ||
| 444 | $fkForeignColumns = $foreignKey->getForeignColumns(); | ||
| 445 | $localColumn = current($fkColumns); | ||
| 446 | $associationMapping = [ | ||
| 447 | 'fieldName' => $this->getFieldNameForColumn($tableName, $localColumn, true), | ||
| 448 | 'targetEntity' => $this->getClassNameForTable($foreignTableName), | ||
| 449 | ]; | ||
| 450 | |||
| 451 | if (isset($metadata->fieldMappings[$associationMapping['fieldName']])) { | ||
| 452 | $associationMapping['fieldName'] .= '2'; // "foo" => "foo2" | ||
| 453 | } | ||
| 454 | |||
| 455 | if ($primaryKeys && in_array($localColumn, $primaryKeys, true)) { | ||
| 456 | $associationMapping['id'] = true; | ||
| 457 | } | ||
| 458 | |||
| 459 | for ($i = 0, $fkColumnsCount = count($fkColumns); $i < $fkColumnsCount; $i++) { | ||
| 460 | $associationMapping['joinColumns'][] = [ | ||
| 461 | 'name' => $fkColumns[$i], | ||
| 462 | 'referencedColumnName' => $fkForeignColumns[$i], | ||
| 463 | ]; | ||
| 464 | } | ||
| 465 | |||
| 466 | // Here we need to check if $fkColumns are the same as $primaryKeys | ||
| 467 | if (! array_diff($fkColumns, $primaryKeys)) { | ||
| 468 | $metadata->mapOneToOne($associationMapping); | ||
| 469 | } else { | ||
| 470 | $metadata->mapManyToOne($associationMapping); | ||
| 471 | } | ||
| 472 | } | ||
| 473 | } | ||
| 474 | |||
| 475 | /** | ||
| 476 | * Retrieve schema table definition primary keys. | ||
| 477 | * | ||
| 478 | * @return string[] | ||
| 479 | */ | ||
| 480 | private function getTablePrimaryKeys(Table $table): array | ||
| 481 | { | ||
| 482 | try { | ||
| 483 | return $table->getPrimaryKey()->getColumns(); | ||
| 484 | } catch (SchemaException) { | ||
| 485 | // Do nothing | ||
| 486 | } | ||
| 487 | |||
| 488 | return []; | ||
| 489 | } | ||
| 490 | |||
| 491 | /** | ||
| 492 | * Returns the mapped class name for a table if it exists. Otherwise return "classified" version. | ||
| 493 | * | ||
| 494 | * @psalm-return class-string | ||
| 495 | */ | ||
| 496 | private function getClassNameForTable(string $tableName): string | ||
| 497 | { | ||
| 498 | if (isset($this->classNamesForTables[$tableName])) { | ||
| 499 | return $this->namespace . $this->classNamesForTables[$tableName]; | ||
| 500 | } | ||
| 501 | |||
| 502 | return $this->namespace . $this->inflector->classify(strtolower($tableName)); | ||
| 503 | } | ||
| 504 | |||
| 505 | /** | ||
| 506 | * Return the mapped field name for a column, if it exists. Otherwise return camelized version. | ||
| 507 | * | ||
| 508 | * @param bool $fk Whether the column is a foreignkey or not. | ||
| 509 | */ | ||
| 510 | private function getFieldNameForColumn( | ||
| 511 | string $tableName, | ||
| 512 | string $columnName, | ||
| 513 | bool $fk = false, | ||
| 514 | ): string { | ||
| 515 | if (isset($this->fieldNamesForColumns[$tableName], $this->fieldNamesForColumns[$tableName][$columnName])) { | ||
| 516 | return $this->fieldNamesForColumns[$tableName][$columnName]; | ||
| 517 | } | ||
| 518 | |||
| 519 | $columnName = strtolower($columnName); | ||
| 520 | |||
| 521 | // Replace _id if it is a foreignkey column | ||
| 522 | if ($fk) { | ||
| 523 | $columnName = preg_replace('/_id$/', '', $columnName); | ||
| 524 | } | ||
| 525 | |||
| 526 | return $this->inflector->camelize($columnName); | ||
| 527 | } | ||
| 528 | } | ||
diff --git a/vendor/doctrine/orm/src/Mapping/Driver/ReflectionBasedDriver.php b/vendor/doctrine/orm/src/Mapping/Driver/ReflectionBasedDriver.php new file mode 100644 index 0000000..7d85471 --- /dev/null +++ b/vendor/doctrine/orm/src/Mapping/Driver/ReflectionBasedDriver.php | |||
| @@ -0,0 +1,44 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Mapping\Driver; | ||
| 6 | |||
| 7 | use Doctrine\ORM\Mapping\ClassMetadata; | ||
| 8 | use ReflectionProperty; | ||
| 9 | |||
| 10 | /** @internal */ | ||
| 11 | trait ReflectionBasedDriver | ||
| 12 | { | ||
| 13 | /** | ||
| 14 | * Helps to deal with the case that reflection may report properties inherited from parent classes. | ||
| 15 | * When we know about the fields already (inheritance has been anticipated in ClassMetadataFactory), | ||
| 16 | * the driver must skip them. | ||
| 17 | * | ||
| 18 | * The declaring classes may mismatch when there are private properties: The same property name may be | ||
| 19 | * reported multiple times, but since it is private, it is in fact multiple (different) properties in | ||
| 20 | * different classes. In that case, report the property as an individual field. (ClassMetadataFactory will | ||
| 21 | * probably fail in that case, though.) | ||
| 22 | */ | ||
| 23 | private function isRepeatedPropertyDeclaration(ReflectionProperty $property, ClassMetadata $metadata): bool | ||
| 24 | { | ||
| 25 | $declaringClass = $property->class; | ||
| 26 | |||
| 27 | if ( | ||
| 28 | isset($metadata->fieldMappings[$property->name]->declared) | ||
| 29 | && $metadata->fieldMappings[$property->name]->declared === $declaringClass | ||
| 30 | ) { | ||
| 31 | return true; | ||
| 32 | } | ||
| 33 | |||
| 34 | if ( | ||
| 35 | isset($metadata->associationMappings[$property->name]->declared) | ||
| 36 | && $metadata->associationMappings[$property->name]->declared === $declaringClass | ||
| 37 | ) { | ||
| 38 | return true; | ||
| 39 | } | ||
| 40 | |||
| 41 | return isset($metadata->embeddedClasses[$property->name]->declared) | ||
| 42 | && $metadata->embeddedClasses[$property->name]->declared === $declaringClass; | ||
| 43 | } | ||
| 44 | } | ||
diff --git a/vendor/doctrine/orm/src/Mapping/Driver/RepeatableAttributeCollection.php b/vendor/doctrine/orm/src/Mapping/Driver/RepeatableAttributeCollection.php new file mode 100644 index 0000000..2f6ae93 --- /dev/null +++ b/vendor/doctrine/orm/src/Mapping/Driver/RepeatableAttributeCollection.php | |||
| @@ -0,0 +1,16 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Mapping\Driver; | ||
| 6 | |||
| 7 | use ArrayObject; | ||
| 8 | use Doctrine\ORM\Mapping\MappingAttribute; | ||
| 9 | |||
| 10 | /** | ||
| 11 | * @template-extends ArrayObject<int, T> | ||
| 12 | * @template T of MappingAttribute | ||
| 13 | */ | ||
| 14 | final class RepeatableAttributeCollection extends ArrayObject | ||
| 15 | { | ||
| 16 | } | ||
diff --git a/vendor/doctrine/orm/src/Mapping/Driver/SimplifiedXmlDriver.php b/vendor/doctrine/orm/src/Mapping/Driver/SimplifiedXmlDriver.php new file mode 100644 index 0000000..486185f --- /dev/null +++ b/vendor/doctrine/orm/src/Mapping/Driver/SimplifiedXmlDriver.php | |||
| @@ -0,0 +1,25 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Mapping\Driver; | ||
| 6 | |||
| 7 | use Doctrine\Persistence\Mapping\Driver\SymfonyFileLocator; | ||
| 8 | |||
| 9 | /** | ||
| 10 | * XmlDriver that additionally looks for mapping information in a global file. | ||
| 11 | */ | ||
| 12 | class SimplifiedXmlDriver extends XmlDriver | ||
| 13 | { | ||
| 14 | public const DEFAULT_FILE_EXTENSION = '.orm.xml'; | ||
| 15 | |||
| 16 | /** | ||
| 17 | * {@inheritDoc} | ||
| 18 | */ | ||
| 19 | public function __construct($prefixes, $fileExtension = self::DEFAULT_FILE_EXTENSION, bool $isXsdValidationEnabled = true) | ||
| 20 | { | ||
| 21 | $locator = new SymfonyFileLocator((array) $prefixes, $fileExtension); | ||
| 22 | |||
| 23 | parent::__construct($locator, $fileExtension, $isXsdValidationEnabled); | ||
| 24 | } | ||
| 25 | } | ||
diff --git a/vendor/doctrine/orm/src/Mapping/Driver/XmlDriver.php b/vendor/doctrine/orm/src/Mapping/Driver/XmlDriver.php new file mode 100644 index 0000000..ff473ce --- /dev/null +++ b/vendor/doctrine/orm/src/Mapping/Driver/XmlDriver.php | |||
| @@ -0,0 +1,940 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Mapping\Driver; | ||
| 6 | |||
| 7 | use Doctrine\Common\Collections\Criteria; | ||
| 8 | use Doctrine\Common\Collections\Order; | ||
| 9 | use Doctrine\ORM\Mapping\Builder\EntityListenerBuilder; | ||
| 10 | use Doctrine\ORM\Mapping\ClassMetadata; | ||
| 11 | use Doctrine\ORM\Mapping\MappingException; | ||
| 12 | use Doctrine\Persistence\Mapping\ClassMetadata as PersistenceClassMetadata; | ||
| 13 | use Doctrine\Persistence\Mapping\Driver\FileDriver; | ||
| 14 | use Doctrine\Persistence\Mapping\Driver\FileLocator; | ||
| 15 | use DOMDocument; | ||
| 16 | use InvalidArgumentException; | ||
| 17 | use LogicException; | ||
| 18 | use SimpleXMLElement; | ||
| 19 | |||
| 20 | use function assert; | ||
| 21 | use function constant; | ||
| 22 | use function count; | ||
| 23 | use function defined; | ||
| 24 | use function enum_exists; | ||
| 25 | use function explode; | ||
| 26 | use function extension_loaded; | ||
| 27 | use function file_get_contents; | ||
| 28 | use function in_array; | ||
| 29 | use function libxml_clear_errors; | ||
| 30 | use function libxml_get_errors; | ||
| 31 | use function libxml_use_internal_errors; | ||
| 32 | use function simplexml_load_string; | ||
| 33 | use function sprintf; | ||
| 34 | use function str_replace; | ||
| 35 | use function strtoupper; | ||
| 36 | |||
| 37 | /** | ||
| 38 | * XmlDriver is a metadata driver that enables mapping through XML files. | ||
| 39 | * | ||
| 40 | * @link www.doctrine-project.org | ||
| 41 | * | ||
| 42 | * @template-extends FileDriver<SimpleXMLElement> | ||
| 43 | */ | ||
| 44 | class XmlDriver extends FileDriver | ||
| 45 | { | ||
| 46 | public const DEFAULT_FILE_EXTENSION = '.dcm.xml'; | ||
| 47 | |||
| 48 | /** | ||
| 49 | * {@inheritDoc} | ||
| 50 | */ | ||
| 51 | public function __construct( | ||
| 52 | string|array|FileLocator $locator, | ||
| 53 | string $fileExtension = self::DEFAULT_FILE_EXTENSION, | ||
| 54 | private readonly bool $isXsdValidationEnabled = true, | ||
| 55 | ) { | ||
| 56 | if (! extension_loaded('simplexml')) { | ||
| 57 | throw new LogicException( | ||
| 58 | 'The XML metadata driver cannot be enabled because the SimpleXML PHP extension is missing.' | ||
| 59 | . ' Please configure PHP with SimpleXML or choose a different metadata driver.', | ||
| 60 | ); | ||
| 61 | } | ||
| 62 | |||
| 63 | if ($isXsdValidationEnabled && ! extension_loaded('dom')) { | ||
| 64 | throw new LogicException( | ||
| 65 | 'XSD validation cannot be enabled because the DOM extension is missing.', | ||
| 66 | ); | ||
| 67 | } | ||
| 68 | |||
| 69 | parent::__construct($locator, $fileExtension); | ||
| 70 | } | ||
| 71 | |||
| 72 | /** | ||
| 73 | * {@inheritDoc} | ||
| 74 | * | ||
| 75 | * @psalm-param class-string<T> $className | ||
| 76 | * @psalm-param ClassMetadata<T> $metadata | ||
| 77 | * | ||
| 78 | * @template T of object | ||
| 79 | */ | ||
| 80 | public function loadMetadataForClass($className, PersistenceClassMetadata $metadata): void | ||
| 81 | { | ||
| 82 | $xmlRoot = $this->getElement($className); | ||
| 83 | |||
| 84 | if ($xmlRoot->getName() === 'entity') { | ||
| 85 | if (isset($xmlRoot['repository-class'])) { | ||
| 86 | $metadata->setCustomRepositoryClass((string) $xmlRoot['repository-class']); | ||
| 87 | } | ||
| 88 | |||
| 89 | if (isset($xmlRoot['read-only']) && $this->evaluateBoolean($xmlRoot['read-only'])) { | ||
| 90 | $metadata->markReadOnly(); | ||
| 91 | } | ||
| 92 | } elseif ($xmlRoot->getName() === 'mapped-superclass') { | ||
| 93 | $metadata->setCustomRepositoryClass( | ||
| 94 | isset($xmlRoot['repository-class']) ? (string) $xmlRoot['repository-class'] : null, | ||
| 95 | ); | ||
| 96 | $metadata->isMappedSuperclass = true; | ||
| 97 | } elseif ($xmlRoot->getName() === 'embeddable') { | ||
| 98 | $metadata->isEmbeddedClass = true; | ||
| 99 | } else { | ||
| 100 | throw MappingException::classIsNotAValidEntityOrMappedSuperClass($className); | ||
| 101 | } | ||
| 102 | |||
| 103 | // Evaluate <entity...> attributes | ||
| 104 | $primaryTable = []; | ||
| 105 | |||
| 106 | if (isset($xmlRoot['table'])) { | ||
| 107 | $primaryTable['name'] = (string) $xmlRoot['table']; | ||
| 108 | } | ||
| 109 | |||
| 110 | if (isset($xmlRoot['schema'])) { | ||
| 111 | $primaryTable['schema'] = (string) $xmlRoot['schema']; | ||
| 112 | } | ||
| 113 | |||
| 114 | $metadata->setPrimaryTable($primaryTable); | ||
| 115 | |||
| 116 | // Evaluate second level cache | ||
| 117 | if (isset($xmlRoot->cache)) { | ||
| 118 | $metadata->enableCache($this->cacheToArray($xmlRoot->cache)); | ||
| 119 | } | ||
| 120 | |||
| 121 | if (isset($xmlRoot['inheritance-type'])) { | ||
| 122 | $inheritanceType = (string) $xmlRoot['inheritance-type']; | ||
| 123 | $metadata->setInheritanceType(constant('Doctrine\ORM\Mapping\ClassMetadata::INHERITANCE_TYPE_' . $inheritanceType)); | ||
| 124 | |||
| 125 | if ($metadata->inheritanceType !== ClassMetadata::INHERITANCE_TYPE_NONE) { | ||
| 126 | // Evaluate <discriminator-column...> | ||
| 127 | if (isset($xmlRoot->{'discriminator-column'})) { | ||
| 128 | $discrColumn = $xmlRoot->{'discriminator-column'}; | ||
| 129 | $columnDef = [ | ||
| 130 | 'name' => isset($discrColumn['name']) ? (string) $discrColumn['name'] : null, | ||
| 131 | 'type' => isset($discrColumn['type']) ? (string) $discrColumn['type'] : 'string', | ||
| 132 | 'length' => isset($discrColumn['length']) ? (int) $discrColumn['length'] : 255, | ||
| 133 | 'columnDefinition' => isset($discrColumn['column-definition']) ? (string) $discrColumn['column-definition'] : null, | ||
| 134 | 'enumType' => isset($discrColumn['enum-type']) ? (string) $discrColumn['enum-type'] : null, | ||
| 135 | ]; | ||
| 136 | |||
| 137 | if (isset($discrColumn['options'])) { | ||
| 138 | assert($discrColumn['options'] instanceof SimpleXMLElement); | ||
| 139 | $columnDef['options'] = $this->parseOptions($discrColumn['options']->children()); | ||
| 140 | } | ||
| 141 | |||
| 142 | $metadata->setDiscriminatorColumn($columnDef); | ||
| 143 | } else { | ||
| 144 | $metadata->setDiscriminatorColumn(['name' => 'dtype', 'type' => 'string', 'length' => 255]); | ||
| 145 | } | ||
| 146 | |||
| 147 | // Evaluate <discriminator-map...> | ||
| 148 | if (isset($xmlRoot->{'discriminator-map'})) { | ||
| 149 | $map = []; | ||
| 150 | assert($xmlRoot->{'discriminator-map'}->{'discriminator-mapping'} instanceof SimpleXMLElement); | ||
| 151 | foreach ($xmlRoot->{'discriminator-map'}->{'discriminator-mapping'} as $discrMapElement) { | ||
| 152 | $map[(string) $discrMapElement['value']] = (string) $discrMapElement['class']; | ||
| 153 | } | ||
| 154 | |||
| 155 | $metadata->setDiscriminatorMap($map); | ||
| 156 | } | ||
| 157 | } | ||
| 158 | } | ||
| 159 | |||
| 160 | // Evaluate <change-tracking-policy...> | ||
| 161 | if (isset($xmlRoot['change-tracking-policy'])) { | ||
| 162 | $metadata->setChangeTrackingPolicy(constant('Doctrine\ORM\Mapping\ClassMetadata::CHANGETRACKING_' | ||
| 163 | . strtoupper((string) $xmlRoot['change-tracking-policy']))); | ||
| 164 | } | ||
| 165 | |||
| 166 | // Evaluate <indexes...> | ||
| 167 | if (isset($xmlRoot->indexes)) { | ||
| 168 | $metadata->table['indexes'] = []; | ||
| 169 | foreach ($xmlRoot->indexes->index ?? [] as $indexXml) { | ||
| 170 | $index = []; | ||
| 171 | |||
| 172 | if (isset($indexXml['columns']) && ! empty($indexXml['columns'])) { | ||
| 173 | $index['columns'] = explode(',', (string) $indexXml['columns']); | ||
| 174 | } | ||
| 175 | |||
| 176 | if (isset($indexXml['fields'])) { | ||
| 177 | $index['fields'] = explode(',', (string) $indexXml['fields']); | ||
| 178 | } | ||
| 179 | |||
| 180 | if ( | ||
| 181 | isset($index['columns'], $index['fields']) | ||
| 182 | || ( | ||
| 183 | ! isset($index['columns']) | ||
| 184 | && ! isset($index['fields']) | ||
| 185 | ) | ||
| 186 | ) { | ||
| 187 | throw MappingException::invalidIndexConfiguration( | ||
| 188 | $className, | ||
| 189 | (string) ($indexXml['name'] ?? count($metadata->table['indexes'])), | ||
| 190 | ); | ||
| 191 | } | ||
| 192 | |||
| 193 | if (isset($indexXml['flags'])) { | ||
| 194 | $index['flags'] = explode(',', (string) $indexXml['flags']); | ||
| 195 | } | ||
| 196 | |||
| 197 | if (isset($indexXml->options)) { | ||
| 198 | $index['options'] = $this->parseOptions($indexXml->options->children()); | ||
| 199 | } | ||
| 200 | |||
| 201 | if (isset($indexXml['name'])) { | ||
| 202 | $metadata->table['indexes'][(string) $indexXml['name']] = $index; | ||
| 203 | } else { | ||
| 204 | $metadata->table['indexes'][] = $index; | ||
| 205 | } | ||
| 206 | } | ||
| 207 | } | ||
| 208 | |||
| 209 | // Evaluate <unique-constraints..> | ||
| 210 | if (isset($xmlRoot->{'unique-constraints'})) { | ||
| 211 | $metadata->table['uniqueConstraints'] = []; | ||
| 212 | foreach ($xmlRoot->{'unique-constraints'}->{'unique-constraint'} ?? [] as $uniqueXml) { | ||
| 213 | $unique = []; | ||
| 214 | |||
| 215 | if (isset($uniqueXml['columns']) && ! empty($uniqueXml['columns'])) { | ||
| 216 | $unique['columns'] = explode(',', (string) $uniqueXml['columns']); | ||
| 217 | } | ||
| 218 | |||
| 219 | if (isset($uniqueXml['fields'])) { | ||
| 220 | $unique['fields'] = explode(',', (string) $uniqueXml['fields']); | ||
| 221 | } | ||
| 222 | |||
| 223 | if ( | ||
| 224 | isset($unique['columns'], $unique['fields']) | ||
| 225 | || ( | ||
| 226 | ! isset($unique['columns']) | ||
| 227 | && ! isset($unique['fields']) | ||
| 228 | ) | ||
| 229 | ) { | ||
| 230 | throw MappingException::invalidUniqueConstraintConfiguration( | ||
| 231 | $className, | ||
| 232 | (string) ($uniqueXml['name'] ?? count($metadata->table['uniqueConstraints'])), | ||
| 233 | ); | ||
| 234 | } | ||
| 235 | |||
| 236 | if (isset($uniqueXml->options)) { | ||
| 237 | $unique['options'] = $this->parseOptions($uniqueXml->options->children()); | ||
| 238 | } | ||
| 239 | |||
| 240 | if (isset($uniqueXml['name'])) { | ||
| 241 | $metadata->table['uniqueConstraints'][(string) $uniqueXml['name']] = $unique; | ||
| 242 | } else { | ||
| 243 | $metadata->table['uniqueConstraints'][] = $unique; | ||
| 244 | } | ||
| 245 | } | ||
| 246 | } | ||
| 247 | |||
| 248 | if (isset($xmlRoot->options)) { | ||
| 249 | $metadata->table['options'] = $this->parseOptions($xmlRoot->options->children()); | ||
| 250 | } | ||
| 251 | |||
| 252 | // The mapping assignment is done in 2 times as a bug might occurs on some php/xml lib versions | ||
| 253 | // The internal SimpleXmlIterator get resetted, to this generate a duplicate field exception | ||
| 254 | // Evaluate <field ...> mappings | ||
| 255 | if (isset($xmlRoot->field)) { | ||
| 256 | foreach ($xmlRoot->field as $fieldMapping) { | ||
| 257 | $mapping = $this->columnToArray($fieldMapping); | ||
| 258 | |||
| 259 | if (isset($mapping['version'])) { | ||
| 260 | $metadata->setVersionMapping($mapping); | ||
| 261 | unset($mapping['version']); | ||
| 262 | } | ||
| 263 | |||
| 264 | $metadata->mapField($mapping); | ||
| 265 | } | ||
| 266 | } | ||
| 267 | |||
| 268 | if (isset($xmlRoot->embedded)) { | ||
| 269 | foreach ($xmlRoot->embedded as $embeddedMapping) { | ||
| 270 | $columnPrefix = isset($embeddedMapping['column-prefix']) | ||
| 271 | ? (string) $embeddedMapping['column-prefix'] | ||
| 272 | : null; | ||
| 273 | |||
| 274 | $useColumnPrefix = isset($embeddedMapping['use-column-prefix']) | ||
| 275 | ? $this->evaluateBoolean($embeddedMapping['use-column-prefix']) | ||
| 276 | : true; | ||
| 277 | |||
| 278 | $mapping = [ | ||
| 279 | 'fieldName' => (string) $embeddedMapping['name'], | ||
| 280 | 'class' => isset($embeddedMapping['class']) ? (string) $embeddedMapping['class'] : null, | ||
| 281 | 'columnPrefix' => $useColumnPrefix ? $columnPrefix : false, | ||
| 282 | ]; | ||
| 283 | |||
| 284 | $metadata->mapEmbedded($mapping); | ||
| 285 | } | ||
| 286 | } | ||
| 287 | |||
| 288 | // Evaluate <id ...> mappings | ||
| 289 | $associationIds = []; | ||
| 290 | foreach ($xmlRoot->id ?? [] as $idElement) { | ||
| 291 | if (isset($idElement['association-key']) && $this->evaluateBoolean($idElement['association-key'])) { | ||
| 292 | $associationIds[(string) $idElement['name']] = true; | ||
| 293 | continue; | ||
| 294 | } | ||
| 295 | |||
| 296 | $mapping = $this->columnToArray($idElement); | ||
| 297 | $mapping['id'] = true; | ||
| 298 | |||
| 299 | $metadata->mapField($mapping); | ||
| 300 | |||
| 301 | if (isset($idElement->generator)) { | ||
| 302 | $strategy = isset($idElement->generator['strategy']) ? | ||
| 303 | (string) $idElement->generator['strategy'] : 'AUTO'; | ||
| 304 | $metadata->setIdGeneratorType(constant('Doctrine\ORM\Mapping\ClassMetadata::GENERATOR_TYPE_' | ||
| 305 | . $strategy)); | ||
| 306 | } | ||
| 307 | |||
| 308 | // Check for SequenceGenerator/TableGenerator definition | ||
| 309 | if (isset($idElement->{'sequence-generator'})) { | ||
| 310 | $seqGenerator = $idElement->{'sequence-generator'}; | ||
| 311 | $metadata->setSequenceGeneratorDefinition( | ||
| 312 | [ | ||
| 313 | 'sequenceName' => (string) $seqGenerator['sequence-name'], | ||
| 314 | 'allocationSize' => (string) $seqGenerator['allocation-size'], | ||
| 315 | 'initialValue' => (string) $seqGenerator['initial-value'], | ||
| 316 | ], | ||
| 317 | ); | ||
| 318 | } elseif (isset($idElement->{'custom-id-generator'})) { | ||
| 319 | $customGenerator = $idElement->{'custom-id-generator'}; | ||
| 320 | $metadata->setCustomGeneratorDefinition( | ||
| 321 | [ | ||
| 322 | 'class' => (string) $customGenerator['class'], | ||
| 323 | ], | ||
| 324 | ); | ||
| 325 | } | ||
| 326 | } | ||
| 327 | |||
| 328 | // Evaluate <one-to-one ...> mappings | ||
| 329 | if (isset($xmlRoot->{'one-to-one'})) { | ||
| 330 | foreach ($xmlRoot->{'one-to-one'} as $oneToOneElement) { | ||
| 331 | $mapping = [ | ||
| 332 | 'fieldName' => (string) $oneToOneElement['field'], | ||
| 333 | ]; | ||
| 334 | |||
| 335 | if (isset($oneToOneElement['target-entity'])) { | ||
| 336 | $mapping['targetEntity'] = (string) $oneToOneElement['target-entity']; | ||
| 337 | } | ||
| 338 | |||
| 339 | if (isset($associationIds[$mapping['fieldName']])) { | ||
| 340 | $mapping['id'] = true; | ||
| 341 | } | ||
| 342 | |||
| 343 | if (isset($oneToOneElement['fetch'])) { | ||
| 344 | $mapping['fetch'] = constant('Doctrine\ORM\Mapping\ClassMetadata::FETCH_' . (string) $oneToOneElement['fetch']); | ||
| 345 | } | ||
| 346 | |||
| 347 | if (isset($oneToOneElement['mapped-by'])) { | ||
| 348 | $mapping['mappedBy'] = (string) $oneToOneElement['mapped-by']; | ||
| 349 | } else { | ||
| 350 | if (isset($oneToOneElement['inversed-by'])) { | ||
| 351 | $mapping['inversedBy'] = (string) $oneToOneElement['inversed-by']; | ||
| 352 | } | ||
| 353 | |||
| 354 | $joinColumns = []; | ||
| 355 | |||
| 356 | if (isset($oneToOneElement->{'join-column'})) { | ||
| 357 | $joinColumns[] = $this->joinColumnToArray($oneToOneElement->{'join-column'}); | ||
| 358 | } elseif (isset($oneToOneElement->{'join-columns'})) { | ||
| 359 | foreach ($oneToOneElement->{'join-columns'}->{'join-column'} ?? [] as $joinColumnElement) { | ||
| 360 | $joinColumns[] = $this->joinColumnToArray($joinColumnElement); | ||
| 361 | } | ||
| 362 | } | ||
| 363 | |||
| 364 | $mapping['joinColumns'] = $joinColumns; | ||
| 365 | } | ||
| 366 | |||
| 367 | if (isset($oneToOneElement->cascade)) { | ||
| 368 | $mapping['cascade'] = $this->getCascadeMappings($oneToOneElement->cascade); | ||
| 369 | } | ||
| 370 | |||
| 371 | if (isset($oneToOneElement['orphan-removal'])) { | ||
| 372 | $mapping['orphanRemoval'] = $this->evaluateBoolean($oneToOneElement['orphan-removal']); | ||
| 373 | } | ||
| 374 | |||
| 375 | // Evaluate second level cache | ||
| 376 | if (isset($oneToOneElement->cache)) { | ||
| 377 | $mapping['cache'] = $metadata->getAssociationCacheDefaults($mapping['fieldName'], $this->cacheToArray($oneToOneElement->cache)); | ||
| 378 | } | ||
| 379 | |||
| 380 | $metadata->mapOneToOne($mapping); | ||
| 381 | } | ||
| 382 | } | ||
| 383 | |||
| 384 | // Evaluate <one-to-many ...> mappings | ||
| 385 | if (isset($xmlRoot->{'one-to-many'})) { | ||
| 386 | foreach ($xmlRoot->{'one-to-many'} as $oneToManyElement) { | ||
| 387 | $mapping = [ | ||
| 388 | 'fieldName' => (string) $oneToManyElement['field'], | ||
| 389 | 'mappedBy' => (string) $oneToManyElement['mapped-by'], | ||
| 390 | ]; | ||
| 391 | |||
| 392 | if (isset($oneToManyElement['target-entity'])) { | ||
| 393 | $mapping['targetEntity'] = (string) $oneToManyElement['target-entity']; | ||
| 394 | } | ||
| 395 | |||
| 396 | if (isset($oneToManyElement['fetch'])) { | ||
| 397 | $mapping['fetch'] = constant('Doctrine\ORM\Mapping\ClassMetadata::FETCH_' . (string) $oneToManyElement['fetch']); | ||
| 398 | } | ||
| 399 | |||
| 400 | if (isset($oneToManyElement->cascade)) { | ||
| 401 | $mapping['cascade'] = $this->getCascadeMappings($oneToManyElement->cascade); | ||
| 402 | } | ||
| 403 | |||
| 404 | if (isset($oneToManyElement['orphan-removal'])) { | ||
| 405 | $mapping['orphanRemoval'] = $this->evaluateBoolean($oneToManyElement['orphan-removal']); | ||
| 406 | } | ||
| 407 | |||
| 408 | if (isset($oneToManyElement->{'order-by'})) { | ||
| 409 | $orderBy = []; | ||
| 410 | foreach ($oneToManyElement->{'order-by'}->{'order-by-field'} ?? [] as $orderByField) { | ||
| 411 | /** @psalm-suppress DeprecatedConstant */ | ||
| 412 | $orderBy[(string) $orderByField['name']] = isset($orderByField['direction']) | ||
| 413 | ? (string) $orderByField['direction'] | ||
| 414 | : (enum_exists(Order::class) ? Order::Ascending->value : Criteria::ASC); | ||
| 415 | } | ||
| 416 | |||
| 417 | $mapping['orderBy'] = $orderBy; | ||
| 418 | } | ||
| 419 | |||
| 420 | if (isset($oneToManyElement['index-by'])) { | ||
| 421 | $mapping['indexBy'] = (string) $oneToManyElement['index-by']; | ||
| 422 | } elseif (isset($oneToManyElement->{'index-by'})) { | ||
| 423 | throw new InvalidArgumentException('<index-by /> is not a valid tag'); | ||
| 424 | } | ||
| 425 | |||
| 426 | // Evaluate second level cache | ||
| 427 | if (isset($oneToManyElement->cache)) { | ||
| 428 | $mapping['cache'] = $metadata->getAssociationCacheDefaults($mapping['fieldName'], $this->cacheToArray($oneToManyElement->cache)); | ||
| 429 | } | ||
| 430 | |||
| 431 | $metadata->mapOneToMany($mapping); | ||
| 432 | } | ||
| 433 | } | ||
| 434 | |||
| 435 | // Evaluate <many-to-one ...> mappings | ||
| 436 | if (isset($xmlRoot->{'many-to-one'})) { | ||
| 437 | foreach ($xmlRoot->{'many-to-one'} as $manyToOneElement) { | ||
| 438 | $mapping = [ | ||
| 439 | 'fieldName' => (string) $manyToOneElement['field'], | ||
| 440 | ]; | ||
| 441 | |||
| 442 | if (isset($manyToOneElement['target-entity'])) { | ||
| 443 | $mapping['targetEntity'] = (string) $manyToOneElement['target-entity']; | ||
| 444 | } | ||
| 445 | |||
| 446 | if (isset($associationIds[$mapping['fieldName']])) { | ||
| 447 | $mapping['id'] = true; | ||
| 448 | } | ||
| 449 | |||
| 450 | if (isset($manyToOneElement['fetch'])) { | ||
| 451 | $mapping['fetch'] = constant('Doctrine\ORM\Mapping\ClassMetadata::FETCH_' . (string) $manyToOneElement['fetch']); | ||
| 452 | } | ||
| 453 | |||
| 454 | if (isset($manyToOneElement['inversed-by'])) { | ||
| 455 | $mapping['inversedBy'] = (string) $manyToOneElement['inversed-by']; | ||
| 456 | } | ||
| 457 | |||
| 458 | $joinColumns = []; | ||
| 459 | |||
| 460 | if (isset($manyToOneElement->{'join-column'})) { | ||
| 461 | $joinColumns[] = $this->joinColumnToArray($manyToOneElement->{'join-column'}); | ||
| 462 | } elseif (isset($manyToOneElement->{'join-columns'})) { | ||
| 463 | foreach ($manyToOneElement->{'join-columns'}->{'join-column'} ?? [] as $joinColumnElement) { | ||
| 464 | $joinColumns[] = $this->joinColumnToArray($joinColumnElement); | ||
| 465 | } | ||
| 466 | } | ||
| 467 | |||
| 468 | $mapping['joinColumns'] = $joinColumns; | ||
| 469 | |||
| 470 | if (isset($manyToOneElement->cascade)) { | ||
| 471 | $mapping['cascade'] = $this->getCascadeMappings($manyToOneElement->cascade); | ||
| 472 | } | ||
| 473 | |||
| 474 | // Evaluate second level cache | ||
| 475 | if (isset($manyToOneElement->cache)) { | ||
| 476 | $mapping['cache'] = $metadata->getAssociationCacheDefaults($mapping['fieldName'], $this->cacheToArray($manyToOneElement->cache)); | ||
| 477 | } | ||
| 478 | |||
| 479 | $metadata->mapManyToOne($mapping); | ||
| 480 | } | ||
| 481 | } | ||
| 482 | |||
| 483 | // Evaluate <many-to-many ...> mappings | ||
| 484 | if (isset($xmlRoot->{'many-to-many'})) { | ||
| 485 | foreach ($xmlRoot->{'many-to-many'} as $manyToManyElement) { | ||
| 486 | $mapping = [ | ||
| 487 | 'fieldName' => (string) $manyToManyElement['field'], | ||
| 488 | ]; | ||
| 489 | |||
| 490 | if (isset($manyToManyElement['target-entity'])) { | ||
| 491 | $mapping['targetEntity'] = (string) $manyToManyElement['target-entity']; | ||
| 492 | } | ||
| 493 | |||
| 494 | if (isset($manyToManyElement['fetch'])) { | ||
| 495 | $mapping['fetch'] = constant('Doctrine\ORM\Mapping\ClassMetadata::FETCH_' . (string) $manyToManyElement['fetch']); | ||
| 496 | } | ||
| 497 | |||
| 498 | if (isset($manyToManyElement['orphan-removal'])) { | ||
| 499 | $mapping['orphanRemoval'] = $this->evaluateBoolean($manyToManyElement['orphan-removal']); | ||
| 500 | } | ||
| 501 | |||
| 502 | if (isset($manyToManyElement['mapped-by'])) { | ||
| 503 | $mapping['mappedBy'] = (string) $manyToManyElement['mapped-by']; | ||
| 504 | } elseif (isset($manyToManyElement->{'join-table'})) { | ||
| 505 | if (isset($manyToManyElement['inversed-by'])) { | ||
| 506 | $mapping['inversedBy'] = (string) $manyToManyElement['inversed-by']; | ||
| 507 | } | ||
| 508 | |||
| 509 | $joinTableElement = $manyToManyElement->{'join-table'}; | ||
| 510 | $joinTable = [ | ||
| 511 | 'name' => (string) $joinTableElement['name'], | ||
| 512 | ]; | ||
| 513 | |||
| 514 | if (isset($joinTableElement['schema'])) { | ||
| 515 | $joinTable['schema'] = (string) $joinTableElement['schema']; | ||
| 516 | } | ||
| 517 | |||
| 518 | if (isset($joinTableElement->options)) { | ||
| 519 | $joinTable['options'] = $this->parseOptions($joinTableElement->options->children()); | ||
| 520 | } | ||
| 521 | |||
| 522 | foreach ($joinTableElement->{'join-columns'}->{'join-column'} ?? [] as $joinColumnElement) { | ||
| 523 | $joinTable['joinColumns'][] = $this->joinColumnToArray($joinColumnElement); | ||
| 524 | } | ||
| 525 | |||
| 526 | foreach ($joinTableElement->{'inverse-join-columns'}->{'join-column'} ?? [] as $joinColumnElement) { | ||
| 527 | $joinTable['inverseJoinColumns'][] = $this->joinColumnToArray($joinColumnElement); | ||
| 528 | } | ||
| 529 | |||
| 530 | $mapping['joinTable'] = $joinTable; | ||
| 531 | } | ||
| 532 | |||
| 533 | if (isset($manyToManyElement->cascade)) { | ||
| 534 | $mapping['cascade'] = $this->getCascadeMappings($manyToManyElement->cascade); | ||
| 535 | } | ||
| 536 | |||
| 537 | if (isset($manyToManyElement->{'order-by'})) { | ||
| 538 | $orderBy = []; | ||
| 539 | foreach ($manyToManyElement->{'order-by'}->{'order-by-field'} ?? [] as $orderByField) { | ||
| 540 | /** @psalm-suppress DeprecatedConstant */ | ||
| 541 | $orderBy[(string) $orderByField['name']] = isset($orderByField['direction']) | ||
| 542 | ? (string) $orderByField['direction'] | ||
| 543 | : (enum_exists(Order::class) ? Order::Ascending->value : Criteria::ASC); | ||
| 544 | } | ||
| 545 | |||
| 546 | $mapping['orderBy'] = $orderBy; | ||
| 547 | } | ||
| 548 | |||
| 549 | if (isset($manyToManyElement['index-by'])) { | ||
| 550 | $mapping['indexBy'] = (string) $manyToManyElement['index-by']; | ||
| 551 | } elseif (isset($manyToManyElement->{'index-by'})) { | ||
| 552 | throw new InvalidArgumentException('<index-by /> is not a valid tag'); | ||
| 553 | } | ||
| 554 | |||
| 555 | // Evaluate second level cache | ||
| 556 | if (isset($manyToManyElement->cache)) { | ||
| 557 | $mapping['cache'] = $metadata->getAssociationCacheDefaults($mapping['fieldName'], $this->cacheToArray($manyToManyElement->cache)); | ||
| 558 | } | ||
| 559 | |||
| 560 | $metadata->mapManyToMany($mapping); | ||
| 561 | } | ||
| 562 | } | ||
| 563 | |||
| 564 | // Evaluate association-overrides | ||
| 565 | if (isset($xmlRoot->{'attribute-overrides'})) { | ||
| 566 | foreach ($xmlRoot->{'attribute-overrides'}->{'attribute-override'} ?? [] as $overrideElement) { | ||
| 567 | $fieldName = (string) $overrideElement['name']; | ||
| 568 | foreach ($overrideElement->field ?? [] as $field) { | ||
| 569 | $mapping = $this->columnToArray($field); | ||
| 570 | $mapping['fieldName'] = $fieldName; | ||
| 571 | $metadata->setAttributeOverride($fieldName, $mapping); | ||
| 572 | } | ||
| 573 | } | ||
| 574 | } | ||
| 575 | |||
| 576 | // Evaluate association-overrides | ||
| 577 | if (isset($xmlRoot->{'association-overrides'})) { | ||
| 578 | foreach ($xmlRoot->{'association-overrides'}->{'association-override'} ?? [] as $overrideElement) { | ||
| 579 | $fieldName = (string) $overrideElement['name']; | ||
| 580 | $override = []; | ||
| 581 | |||
| 582 | // Check for join-columns | ||
| 583 | if (isset($overrideElement->{'join-columns'})) { | ||
| 584 | $joinColumns = []; | ||
| 585 | foreach ($overrideElement->{'join-columns'}->{'join-column'} ?? [] as $joinColumnElement) { | ||
| 586 | $joinColumns[] = $this->joinColumnToArray($joinColumnElement); | ||
| 587 | } | ||
| 588 | |||
| 589 | $override['joinColumns'] = $joinColumns; | ||
| 590 | } | ||
| 591 | |||
| 592 | // Check for join-table | ||
| 593 | if ($overrideElement->{'join-table'}) { | ||
| 594 | $joinTable = null; | ||
| 595 | $joinTableElement = $overrideElement->{'join-table'}; | ||
| 596 | |||
| 597 | $joinTable = [ | ||
| 598 | 'name' => (string) $joinTableElement['name'], | ||
| 599 | 'schema' => (string) $joinTableElement['schema'], | ||
| 600 | ]; | ||
| 601 | |||
| 602 | if (isset($joinTableElement->options)) { | ||
| 603 | $joinTable['options'] = $this->parseOptions($joinTableElement->options->children()); | ||
| 604 | } | ||
| 605 | |||
| 606 | if (isset($joinTableElement->{'join-columns'})) { | ||
| 607 | foreach ($joinTableElement->{'join-columns'}->{'join-column'} ?? [] as $joinColumnElement) { | ||
| 608 | $joinTable['joinColumns'][] = $this->joinColumnToArray($joinColumnElement); | ||
| 609 | } | ||
| 610 | } | ||
| 611 | |||
| 612 | if (isset($joinTableElement->{'inverse-join-columns'})) { | ||
| 613 | foreach ($joinTableElement->{'inverse-join-columns'}->{'join-column'} ?? [] as $joinColumnElement) { | ||
| 614 | $joinTable['inverseJoinColumns'][] = $this->joinColumnToArray($joinColumnElement); | ||
| 615 | } | ||
| 616 | } | ||
| 617 | |||
| 618 | $override['joinTable'] = $joinTable; | ||
| 619 | } | ||
| 620 | |||
| 621 | // Check for inversed-by | ||
| 622 | if (isset($overrideElement->{'inversed-by'})) { | ||
| 623 | $override['inversedBy'] = (string) $overrideElement->{'inversed-by'}['name']; | ||
| 624 | } | ||
| 625 | |||
| 626 | // Check for `fetch` | ||
| 627 | if (isset($overrideElement['fetch'])) { | ||
| 628 | $override['fetch'] = constant(ClassMetadata::class . '::FETCH_' . (string) $overrideElement['fetch']); | ||
| 629 | } | ||
| 630 | |||
| 631 | $metadata->setAssociationOverride($fieldName, $override); | ||
| 632 | } | ||
| 633 | } | ||
| 634 | |||
| 635 | // Evaluate <lifecycle-callbacks...> | ||
| 636 | if (isset($xmlRoot->{'lifecycle-callbacks'})) { | ||
| 637 | foreach ($xmlRoot->{'lifecycle-callbacks'}->{'lifecycle-callback'} ?? [] as $lifecycleCallback) { | ||
| 638 | $metadata->addLifecycleCallback((string) $lifecycleCallback['method'], constant('Doctrine\ORM\Events::' . (string) $lifecycleCallback['type'])); | ||
| 639 | } | ||
| 640 | } | ||
| 641 | |||
| 642 | // Evaluate entity listener | ||
| 643 | if (isset($xmlRoot->{'entity-listeners'})) { | ||
| 644 | foreach ($xmlRoot->{'entity-listeners'}->{'entity-listener'} ?? [] as $listenerElement) { | ||
| 645 | $className = (string) $listenerElement['class']; | ||
| 646 | // Evaluate the listener using naming convention. | ||
| 647 | if ($listenerElement->count() === 0) { | ||
| 648 | EntityListenerBuilder::bindEntityListener($metadata, $className); | ||
| 649 | |||
| 650 | continue; | ||
| 651 | } | ||
| 652 | |||
| 653 | foreach ($listenerElement as $callbackElement) { | ||
| 654 | $eventName = (string) $callbackElement['type']; | ||
| 655 | $methodName = (string) $callbackElement['method']; | ||
| 656 | |||
| 657 | $metadata->addEntityListener($eventName, $className, $methodName); | ||
| 658 | } | ||
| 659 | } | ||
| 660 | } | ||
| 661 | } | ||
| 662 | |||
| 663 | /** | ||
| 664 | * Parses (nested) option elements. | ||
| 665 | * | ||
| 666 | * @return mixed[] The options array. | ||
| 667 | * @psalm-return array<int|string, array<int|string, mixed|string>|bool|string> | ||
| 668 | */ | ||
| 669 | private function parseOptions(SimpleXMLElement|null $options): array | ||
| 670 | { | ||
| 671 | $array = []; | ||
| 672 | |||
| 673 | foreach ($options ?? [] as $option) { | ||
| 674 | if ($option->count()) { | ||
| 675 | $value = $this->parseOptions($option->children()); | ||
| 676 | } else { | ||
| 677 | $value = (string) $option; | ||
| 678 | } | ||
| 679 | |||
| 680 | $attributes = $option->attributes(); | ||
| 681 | |||
| 682 | if (isset($attributes->name)) { | ||
| 683 | $nameAttribute = (string) $attributes->name; | ||
| 684 | $array[$nameAttribute] = in_array($nameAttribute, ['unsigned', 'fixed'], true) | ||
| 685 | ? $this->evaluateBoolean($value) | ||
| 686 | : $value; | ||
| 687 | } else { | ||
| 688 | $array[] = $value; | ||
| 689 | } | ||
| 690 | } | ||
| 691 | |||
| 692 | return $array; | ||
| 693 | } | ||
| 694 | |||
| 695 | /** | ||
| 696 | * Constructs a joinColumn mapping array based on the information | ||
| 697 | * found in the given SimpleXMLElement. | ||
| 698 | * | ||
| 699 | * @param SimpleXMLElement $joinColumnElement The XML element. | ||
| 700 | * | ||
| 701 | * @return mixed[] The mapping array. | ||
| 702 | * @psalm-return array{ | ||
| 703 | * name: string, | ||
| 704 | * referencedColumnName: string, | ||
| 705 | * unique?: bool, | ||
| 706 | * nullable?: bool, | ||
| 707 | * onDelete?: string, | ||
| 708 | * columnDefinition?: string, | ||
| 709 | * options?: mixed[] | ||
| 710 | * } | ||
| 711 | */ | ||
| 712 | private function joinColumnToArray(SimpleXMLElement $joinColumnElement): array | ||
| 713 | { | ||
| 714 | $joinColumn = [ | ||
| 715 | 'name' => (string) $joinColumnElement['name'], | ||
| 716 | 'referencedColumnName' => (string) $joinColumnElement['referenced-column-name'], | ||
| 717 | ]; | ||
| 718 | |||
| 719 | if (isset($joinColumnElement['unique'])) { | ||
| 720 | $joinColumn['unique'] = $this->evaluateBoolean($joinColumnElement['unique']); | ||
| 721 | } | ||
| 722 | |||
| 723 | if (isset($joinColumnElement['nullable'])) { | ||
| 724 | $joinColumn['nullable'] = $this->evaluateBoolean($joinColumnElement['nullable']); | ||
| 725 | } | ||
| 726 | |||
| 727 | if (isset($joinColumnElement['on-delete'])) { | ||
| 728 | $joinColumn['onDelete'] = (string) $joinColumnElement['on-delete']; | ||
| 729 | } | ||
| 730 | |||
| 731 | if (isset($joinColumnElement['column-definition'])) { | ||
| 732 | $joinColumn['columnDefinition'] = (string) $joinColumnElement['column-definition']; | ||
| 733 | } | ||
| 734 | |||
| 735 | if (isset($joinColumnElement['options'])) { | ||
| 736 | $joinColumn['options'] = $this->parseOptions($joinColumnElement['options'] ? $joinColumnElement['options']->children() : null); | ||
| 737 | } | ||
| 738 | |||
| 739 | return $joinColumn; | ||
| 740 | } | ||
| 741 | |||
| 742 | /** | ||
| 743 | * Parses the given field as array. | ||
| 744 | * | ||
| 745 | * @return mixed[] | ||
| 746 | * @psalm-return array{ | ||
| 747 | * fieldName: string, | ||
| 748 | * type?: string, | ||
| 749 | * columnName?: string, | ||
| 750 | * length?: int, | ||
| 751 | * precision?: int, | ||
| 752 | * scale?: int, | ||
| 753 | * unique?: bool, | ||
| 754 | * nullable?: bool, | ||
| 755 | * notInsertable?: bool, | ||
| 756 | * notUpdatable?: bool, | ||
| 757 | * enumType?: string, | ||
| 758 | * version?: bool, | ||
| 759 | * columnDefinition?: string, | ||
| 760 | * options?: array | ||
| 761 | * } | ||
| 762 | */ | ||
| 763 | private function columnToArray(SimpleXMLElement $fieldMapping): array | ||
| 764 | { | ||
| 765 | $mapping = [ | ||
| 766 | 'fieldName' => (string) $fieldMapping['name'], | ||
| 767 | ]; | ||
| 768 | |||
| 769 | if (isset($fieldMapping['type'])) { | ||
| 770 | $mapping['type'] = (string) $fieldMapping['type']; | ||
| 771 | } | ||
| 772 | |||
| 773 | if (isset($fieldMapping['column'])) { | ||
| 774 | $mapping['columnName'] = (string) $fieldMapping['column']; | ||
| 775 | } | ||
| 776 | |||
| 777 | if (isset($fieldMapping['length'])) { | ||
| 778 | $mapping['length'] = (int) $fieldMapping['length']; | ||
| 779 | } | ||
| 780 | |||
| 781 | if (isset($fieldMapping['precision'])) { | ||
| 782 | $mapping['precision'] = (int) $fieldMapping['precision']; | ||
| 783 | } | ||
| 784 | |||
| 785 | if (isset($fieldMapping['scale'])) { | ||
| 786 | $mapping['scale'] = (int) $fieldMapping['scale']; | ||
| 787 | } | ||
| 788 | |||
| 789 | if (isset($fieldMapping['unique'])) { | ||
| 790 | $mapping['unique'] = $this->evaluateBoolean($fieldMapping['unique']); | ||
| 791 | } | ||
| 792 | |||
| 793 | if (isset($fieldMapping['nullable'])) { | ||
| 794 | $mapping['nullable'] = $this->evaluateBoolean($fieldMapping['nullable']); | ||
| 795 | } | ||
| 796 | |||
| 797 | if (isset($fieldMapping['insertable']) && ! $this->evaluateBoolean($fieldMapping['insertable'])) { | ||
| 798 | $mapping['notInsertable'] = true; | ||
| 799 | } | ||
| 800 | |||
| 801 | if (isset($fieldMapping['updatable']) && ! $this->evaluateBoolean($fieldMapping['updatable'])) { | ||
| 802 | $mapping['notUpdatable'] = true; | ||
| 803 | } | ||
| 804 | |||
| 805 | if (isset($fieldMapping['generated'])) { | ||
| 806 | $mapping['generated'] = constant('Doctrine\ORM\Mapping\ClassMetadata::GENERATED_' . (string) $fieldMapping['generated']); | ||
| 807 | } | ||
| 808 | |||
| 809 | if (isset($fieldMapping['version']) && $fieldMapping['version']) { | ||
| 810 | $mapping['version'] = $this->evaluateBoolean($fieldMapping['version']); | ||
| 811 | } | ||
| 812 | |||
| 813 | if (isset($fieldMapping['column-definition'])) { | ||
| 814 | $mapping['columnDefinition'] = (string) $fieldMapping['column-definition']; | ||
| 815 | } | ||
| 816 | |||
| 817 | if (isset($fieldMapping['enum-type'])) { | ||
| 818 | $mapping['enumType'] = (string) $fieldMapping['enum-type']; | ||
| 819 | } | ||
| 820 | |||
| 821 | if (isset($fieldMapping->options)) { | ||
| 822 | $mapping['options'] = $this->parseOptions($fieldMapping->options->children()); | ||
| 823 | } | ||
| 824 | |||
| 825 | return $mapping; | ||
| 826 | } | ||
| 827 | |||
| 828 | /** | ||
| 829 | * Parse / Normalize the cache configuration | ||
| 830 | * | ||
| 831 | * @return mixed[] | ||
| 832 | * @psalm-return array{usage: int|null, region?: string} | ||
| 833 | */ | ||
| 834 | private function cacheToArray(SimpleXMLElement $cacheMapping): array | ||
| 835 | { | ||
| 836 | $region = isset($cacheMapping['region']) ? (string) $cacheMapping['region'] : null; | ||
| 837 | $usage = isset($cacheMapping['usage']) ? strtoupper((string) $cacheMapping['usage']) : null; | ||
| 838 | |||
| 839 | if ($usage && ! defined('Doctrine\ORM\Mapping\ClassMetadata::CACHE_USAGE_' . $usage)) { | ||
| 840 | throw new InvalidArgumentException(sprintf('Invalid cache usage "%s"', $usage)); | ||
| 841 | } | ||
| 842 | |||
| 843 | if ($usage) { | ||
| 844 | $usage = (int) constant('Doctrine\ORM\Mapping\ClassMetadata::CACHE_USAGE_' . $usage); | ||
| 845 | } | ||
| 846 | |||
| 847 | return [ | ||
| 848 | 'usage' => $usage, | ||
| 849 | 'region' => $region, | ||
| 850 | ]; | ||
| 851 | } | ||
| 852 | |||
| 853 | /** | ||
| 854 | * Gathers a list of cascade options found in the given cascade element. | ||
| 855 | * | ||
| 856 | * @param SimpleXMLElement $cascadeElement The cascade element. | ||
| 857 | * | ||
| 858 | * @return string[] The list of cascade options. | ||
| 859 | * @psalm-return list<string> | ||
| 860 | */ | ||
| 861 | private function getCascadeMappings(SimpleXMLElement $cascadeElement): array | ||
| 862 | { | ||
| 863 | $cascades = []; | ||
| 864 | $children = $cascadeElement->children(); | ||
| 865 | assert($children !== null); | ||
| 866 | |||
| 867 | foreach ($children as $action) { | ||
| 868 | // According to the JPA specifications, XML uses "cascade-persist" | ||
| 869 | // instead of "persist". Here, both variations | ||
| 870 | // are supported because Attribute uses "persist" | ||
| 871 | // and we want to make sure that this driver doesn't need to know | ||
| 872 | // anything about the supported cascading actions | ||
| 873 | $cascades[] = str_replace('cascade-', '', $action->getName()); | ||
| 874 | } | ||
| 875 | |||
| 876 | return $cascades; | ||
| 877 | } | ||
| 878 | |||
| 879 | /** | ||
| 880 | * {@inheritDoc} | ||
| 881 | */ | ||
| 882 | protected function loadMappingFile($file) | ||
| 883 | { | ||
| 884 | $this->validateMapping($file); | ||
| 885 | $result = []; | ||
| 886 | // Note: we do not use `simplexml_load_file()` because of https://bugs.php.net/bug.php?id=62577 | ||
| 887 | $xmlElement = simplexml_load_string(file_get_contents($file)); | ||
| 888 | assert($xmlElement !== false); | ||
| 889 | |||
| 890 | if (isset($xmlElement->entity)) { | ||
| 891 | foreach ($xmlElement->entity as $entityElement) { | ||
| 892 | /** @psalm-var class-string $entityName */ | ||
| 893 | $entityName = (string) $entityElement['name']; | ||
| 894 | $result[$entityName] = $entityElement; | ||
| 895 | } | ||
| 896 | } elseif (isset($xmlElement->{'mapped-superclass'})) { | ||
| 897 | foreach ($xmlElement->{'mapped-superclass'} as $mappedSuperClass) { | ||
| 898 | /** @psalm-var class-string $className */ | ||
| 899 | $className = (string) $mappedSuperClass['name']; | ||
| 900 | $result[$className] = $mappedSuperClass; | ||
| 901 | } | ||
| 902 | } elseif (isset($xmlElement->embeddable)) { | ||
| 903 | foreach ($xmlElement->embeddable as $embeddableElement) { | ||
| 904 | /** @psalm-var class-string $embeddableName */ | ||
| 905 | $embeddableName = (string) $embeddableElement['name']; | ||
| 906 | $result[$embeddableName] = $embeddableElement; | ||
| 907 | } | ||
| 908 | } | ||
| 909 | |||
| 910 | return $result; | ||
| 911 | } | ||
| 912 | |||
| 913 | private function validateMapping(string $file): void | ||
| 914 | { | ||
| 915 | if (! $this->isXsdValidationEnabled) { | ||
| 916 | return; | ||
| 917 | } | ||
| 918 | |||
| 919 | $backedUpErrorSetting = libxml_use_internal_errors(true); | ||
| 920 | |||
| 921 | try { | ||
| 922 | $document = new DOMDocument(); | ||
| 923 | $document->load($file); | ||
| 924 | |||
| 925 | if (! $document->schemaValidate(__DIR__ . '/../../../doctrine-mapping.xsd')) { | ||
| 926 | throw MappingException::fromLibXmlErrors(libxml_get_errors()); | ||
| 927 | } | ||
| 928 | } finally { | ||
| 929 | libxml_clear_errors(); | ||
| 930 | libxml_use_internal_errors($backedUpErrorSetting); | ||
| 931 | } | ||
| 932 | } | ||
| 933 | |||
| 934 | protected function evaluateBoolean(mixed $element): bool | ||
| 935 | { | ||
| 936 | $flag = (string) $element; | ||
| 937 | |||
| 938 | return $flag === 'true' || $flag === '1'; | ||
| 939 | } | ||
| 940 | } | ||
diff --git a/vendor/doctrine/orm/src/Mapping/Embeddable.php b/vendor/doctrine/orm/src/Mapping/Embeddable.php new file mode 100644 index 0000000..b8dfea0 --- /dev/null +++ b/vendor/doctrine/orm/src/Mapping/Embeddable.php | |||
| @@ -0,0 +1,12 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Mapping; | ||
| 6 | |||
| 7 | use Attribute; | ||
| 8 | |||
| 9 | #[Attribute(Attribute::TARGET_CLASS)] | ||
| 10 | final class Embeddable implements MappingAttribute | ||
| 11 | { | ||
| 12 | } | ||
diff --git a/vendor/doctrine/orm/src/Mapping/Embedded.php b/vendor/doctrine/orm/src/Mapping/Embedded.php new file mode 100644 index 0000000..be69b4f --- /dev/null +++ b/vendor/doctrine/orm/src/Mapping/Embedded.php | |||
| @@ -0,0 +1,17 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Mapping; | ||
| 6 | |||
| 7 | use Attribute; | ||
| 8 | |||
| 9 | #[Attribute(Attribute::TARGET_PROPERTY)] | ||
| 10 | final class Embedded implements MappingAttribute | ||
| 11 | { | ||
| 12 | public function __construct( | ||
| 13 | public readonly string|null $class = null, | ||
| 14 | public readonly string|bool|null $columnPrefix = null, | ||
| 15 | ) { | ||
| 16 | } | ||
| 17 | } | ||
diff --git a/vendor/doctrine/orm/src/Mapping/EmbeddedClassMapping.php b/vendor/doctrine/orm/src/Mapping/EmbeddedClassMapping.php new file mode 100644 index 0000000..8fd02c9 --- /dev/null +++ b/vendor/doctrine/orm/src/Mapping/EmbeddedClassMapping.php | |||
| @@ -0,0 +1,93 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Mapping; | ||
| 6 | |||
| 7 | use ArrayAccess; | ||
| 8 | |||
| 9 | use function property_exists; | ||
| 10 | |||
| 11 | /** @template-implements ArrayAccess<string, mixed> */ | ||
| 12 | final class EmbeddedClassMapping implements ArrayAccess | ||
| 13 | { | ||
| 14 | use ArrayAccessImplementation; | ||
| 15 | |||
| 16 | public string|false|null $columnPrefix = null; | ||
| 17 | public string|null $declaredField = null; | ||
| 18 | public string|null $originalField = null; | ||
| 19 | |||
| 20 | /** | ||
| 21 | * This is set when this embedded-class field is inherited by this class | ||
| 22 | * from another (inheritance) parent <em>entity</em> class. The value is | ||
| 23 | * the FQCN of the topmost entity class that contains mapping information | ||
| 24 | * for this field. (If there are transient classes in the class hierarchy, | ||
| 25 | * these are ignored, so the class property may in fact come from a class | ||
| 26 | * further up in the PHP class hierarchy.) Fields initially declared in | ||
| 27 | * mapped superclasses are <em>not</em> considered 'inherited' in the | ||
| 28 | * nearest entity subclasses. | ||
| 29 | * | ||
| 30 | * @var class-string|null | ||
| 31 | */ | ||
| 32 | public string|null $inherited = null; | ||
| 33 | |||
| 34 | /** | ||
| 35 | * This is set when the embedded-class field does not appear for the first | ||
| 36 | * time in this class, but is originally declared in another parent | ||
| 37 | * <em>entity or mapped superclass</em>. The value is the FQCN of the | ||
| 38 | * topmost non-transient class that contains mapping information for this | ||
| 39 | * field. | ||
| 40 | * | ||
| 41 | * @var class-string|null | ||
| 42 | */ | ||
| 43 | public string|null $declared = null; | ||
| 44 | |||
| 45 | /** @param class-string $class */ | ||
| 46 | public function __construct(public string $class) | ||
| 47 | { | ||
| 48 | } | ||
| 49 | |||
| 50 | /** | ||
| 51 | * @psalm-param array{ | ||
| 52 | * class: class-string, | ||
| 53 | * columnPrefix?: false|string|null, | ||
| 54 | * declaredField?: string|null, | ||
| 55 | * originalField?: string|null, | ||
| 56 | * inherited?: class-string|null, | ||
| 57 | * declared?: class-string|null, | ||
| 58 | * } $mappingArray | ||
| 59 | */ | ||
| 60 | public static function fromMappingArray(array $mappingArray): self | ||
| 61 | { | ||
| 62 | $mapping = new self($mappingArray['class']); | ||
| 63 | foreach ($mappingArray as $key => $value) { | ||
| 64 | if ($key === 'class') { | ||
| 65 | continue; | ||
| 66 | } | ||
| 67 | |||
| 68 | if (property_exists($mapping, $key)) { | ||
| 69 | $mapping->$key = $value; | ||
| 70 | } | ||
| 71 | } | ||
| 72 | |||
| 73 | return $mapping; | ||
| 74 | } | ||
| 75 | |||
| 76 | /** @return list<string> */ | ||
| 77 | public function __sleep(): array | ||
| 78 | { | ||
| 79 | $serialized = ['class']; | ||
| 80 | |||
| 81 | if ($this->columnPrefix) { | ||
| 82 | $serialized[] = 'columnPrefix'; | ||
| 83 | } | ||
| 84 | |||
| 85 | foreach (['declaredField', 'originalField', 'inherited', 'declared'] as $property) { | ||
| 86 | if ($this->$property !== null) { | ||
| 87 | $serialized[] = $property; | ||
| 88 | } | ||
| 89 | } | ||
| 90 | |||
| 91 | return $serialized; | ||
| 92 | } | ||
| 93 | } | ||
diff --git a/vendor/doctrine/orm/src/Mapping/Entity.php b/vendor/doctrine/orm/src/Mapping/Entity.php new file mode 100644 index 0000000..0e27913 --- /dev/null +++ b/vendor/doctrine/orm/src/Mapping/Entity.php | |||
| @@ -0,0 +1,20 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Mapping; | ||
| 6 | |||
| 7 | use Attribute; | ||
| 8 | use Doctrine\ORM\EntityRepository; | ||
| 9 | |||
| 10 | /** @template T of object */ | ||
| 11 | #[Attribute(Attribute::TARGET_CLASS)] | ||
| 12 | final class Entity implements MappingAttribute | ||
| 13 | { | ||
| 14 | /** @psalm-param class-string<EntityRepository<T>>|null $repositoryClass */ | ||
| 15 | public function __construct( | ||
| 16 | public readonly string|null $repositoryClass = null, | ||
| 17 | public readonly bool $readOnly = false, | ||
| 18 | ) { | ||
| 19 | } | ||
| 20 | } | ||
diff --git a/vendor/doctrine/orm/src/Mapping/EntityListenerResolver.php b/vendor/doctrine/orm/src/Mapping/EntityListenerResolver.php new file mode 100644 index 0000000..eabc217 --- /dev/null +++ b/vendor/doctrine/orm/src/Mapping/EntityListenerResolver.php | |||
| @@ -0,0 +1,30 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Mapping; | ||
| 6 | |||
| 7 | /** | ||
| 8 | * A resolver is used to instantiate an entity listener. | ||
| 9 | */ | ||
| 10 | interface EntityListenerResolver | ||
| 11 | { | ||
| 12 | /** | ||
| 13 | * Clear all instances from the set, or a specific instance when given its identifier. | ||
| 14 | * | ||
| 15 | * @param string|null $className May be any arbitrary string. Name kept for BC only. | ||
| 16 | */ | ||
| 17 | public function clear(string|null $className = null): void; | ||
| 18 | |||
| 19 | /** | ||
| 20 | * Returns a entity listener instance for the given identifier. | ||
| 21 | * | ||
| 22 | * @param string $className May be any arbitrary string. Name kept for BC only. | ||
| 23 | */ | ||
| 24 | public function resolve(string $className): object; | ||
| 25 | |||
| 26 | /** | ||
| 27 | * Register a entity listener instance. | ||
| 28 | */ | ||
| 29 | public function register(object $object): void; | ||
| 30 | } | ||
diff --git a/vendor/doctrine/orm/src/Mapping/EntityListeners.php b/vendor/doctrine/orm/src/Mapping/EntityListeners.php new file mode 100644 index 0000000..8f822ea --- /dev/null +++ b/vendor/doctrine/orm/src/Mapping/EntityListeners.php | |||
| @@ -0,0 +1,21 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Mapping; | ||
| 6 | |||
| 7 | use Attribute; | ||
| 8 | |||
| 9 | /** | ||
| 10 | * The EntityListeners attribute specifies the callback listener classes to be used for an entity or mapped superclass. | ||
| 11 | * The EntityListeners attribute may be applied to an entity class or mapped superclass. | ||
| 12 | */ | ||
| 13 | #[Attribute(Attribute::TARGET_CLASS)] | ||
| 14 | final class EntityListeners implements MappingAttribute | ||
| 15 | { | ||
| 16 | /** @param array<string> $value */ | ||
| 17 | public function __construct( | ||
| 18 | public readonly array $value = [], | ||
| 19 | ) { | ||
| 20 | } | ||
| 21 | } | ||
diff --git a/vendor/doctrine/orm/src/Mapping/Exception/InvalidCustomGenerator.php b/vendor/doctrine/orm/src/Mapping/Exception/InvalidCustomGenerator.php new file mode 100644 index 0000000..b9e10bf --- /dev/null +++ b/vendor/doctrine/orm/src/Mapping/Exception/InvalidCustomGenerator.php | |||
| @@ -0,0 +1,28 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Mapping\Exception; | ||
| 6 | |||
| 7 | use Doctrine\ORM\Exception\ORMException; | ||
| 8 | use LogicException; | ||
| 9 | |||
| 10 | use function sprintf; | ||
| 11 | use function var_export; | ||
| 12 | |||
| 13 | final class InvalidCustomGenerator extends LogicException implements ORMException | ||
| 14 | { | ||
| 15 | public static function onClassNotConfigured(): self | ||
| 16 | { | ||
| 17 | return new self('Cannot instantiate custom generator, no class has been defined'); | ||
| 18 | } | ||
| 19 | |||
| 20 | /** @param mixed[] $definition */ | ||
| 21 | public static function onMissingClass(array $definition): self | ||
| 22 | { | ||
| 23 | return new self(sprintf( | ||
| 24 | 'Cannot instantiate custom generator : %s', | ||
| 25 | var_export($definition, true), | ||
| 26 | )); | ||
| 27 | } | ||
| 28 | } | ||
diff --git a/vendor/doctrine/orm/src/Mapping/Exception/UnknownGeneratorType.php b/vendor/doctrine/orm/src/Mapping/Exception/UnknownGeneratorType.php new file mode 100644 index 0000000..c8970bf --- /dev/null +++ b/vendor/doctrine/orm/src/Mapping/Exception/UnknownGeneratorType.php | |||
| @@ -0,0 +1,16 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Mapping\Exception; | ||
| 6 | |||
| 7 | use Doctrine\ORM\Exception\ORMException; | ||
| 8 | use LogicException; | ||
| 9 | |||
| 10 | final class UnknownGeneratorType extends LogicException implements ORMException | ||
| 11 | { | ||
| 12 | public static function create(int $generatorType): self | ||
| 13 | { | ||
| 14 | return new self('Unknown generator type: ' . $generatorType); | ||
| 15 | } | ||
| 16 | } | ||
diff --git a/vendor/doctrine/orm/src/Mapping/FieldMapping.php b/vendor/doctrine/orm/src/Mapping/FieldMapping.php new file mode 100644 index 0000000..4c09196 --- /dev/null +++ b/vendor/doctrine/orm/src/Mapping/FieldMapping.php | |||
| @@ -0,0 +1,169 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Mapping; | ||
| 6 | |||
| 7 | use ArrayAccess; | ||
| 8 | use BackedEnum; | ||
| 9 | |||
| 10 | use function in_array; | ||
| 11 | use function property_exists; | ||
| 12 | |||
| 13 | /** @template-implements ArrayAccess<string, mixed> */ | ||
| 14 | final class FieldMapping implements ArrayAccess | ||
| 15 | { | ||
| 16 | use ArrayAccessImplementation; | ||
| 17 | |||
| 18 | /** The database length of the column. Optional. Default value taken from the type. */ | ||
| 19 | public int|null $length = null; | ||
| 20 | /** | ||
| 21 | * Marks the field as the primary key of the entity. Multiple | ||
| 22 | * fields of an entity can have the id attribute, forming a composite key. | ||
| 23 | */ | ||
| 24 | public bool|null $id = null; | ||
| 25 | public bool|null $nullable = null; | ||
| 26 | public bool|null $notInsertable = null; | ||
| 27 | public bool|null $notUpdatable = null; | ||
| 28 | public string|null $columnDefinition = null; | ||
| 29 | /** @psalm-var ClassMetadata::GENERATED_*|null */ | ||
| 30 | public int|null $generated = null; | ||
| 31 | /** @var class-string<BackedEnum>|null */ | ||
| 32 | public string|null $enumType = null; | ||
| 33 | /** | ||
| 34 | * The precision of a decimal column. | ||
| 35 | * Only valid if the column type is decimal | ||
| 36 | */ | ||
| 37 | public int|null $precision = null; | ||
| 38 | /** | ||
| 39 | * The scale of a decimal column. | ||
| 40 | * Only valid if the column type is decimal | ||
| 41 | */ | ||
| 42 | public int|null $scale = null; | ||
| 43 | /** Whether a unique constraint should be generated for the column. */ | ||
| 44 | public bool|null $unique = null; | ||
| 45 | /** | ||
| 46 | * @var class-string|null This is set when the field is inherited by this | ||
| 47 | * class from another (inheritance) parent <em>entity</em> class. The value | ||
| 48 | * is the FQCN of the topmost entity class that contains mapping information | ||
| 49 | * for this field. (If there are transient classes in the class hierarchy, | ||
| 50 | * these are ignored, so the class property may in fact come from a class | ||
| 51 | * further up in the PHP class hierarchy.) | ||
| 52 | * Fields initially declared in mapped superclasses are | ||
| 53 | * <em>not</em> considered 'inherited' in the nearest entity subclasses. | ||
| 54 | */ | ||
| 55 | public string|null $inherited = null; | ||
| 56 | |||
| 57 | public string|null $originalClass = null; | ||
| 58 | public string|null $originalField = null; | ||
| 59 | public bool|null $quoted = null; | ||
| 60 | /** | ||
| 61 | * @var class-string|null This is set when the field does not appear for | ||
| 62 | * the first time in this class, but is originally declared in another | ||
| 63 | * parent <em>entity or mapped superclass</em>. The value is the FQCN of | ||
| 64 | * the topmost non-transient class that contains mapping information for | ||
| 65 | * this field. | ||
| 66 | */ | ||
| 67 | public string|null $declared = null; | ||
| 68 | public string|null $declaredField = null; | ||
| 69 | public array|null $options = null; | ||
| 70 | public bool|null $version = null; | ||
| 71 | public string|int|null $default = null; | ||
| 72 | |||
| 73 | /** | ||
| 74 | * @param string $type The type name of the mapped field. Can be one of | ||
| 75 | * Doctrine's mapping types or a custom mapping type. | ||
| 76 | * @param string $fieldName The name of the field in the Entity. | ||
| 77 | * @param string $columnName The column name. Optional. Defaults to the field name. | ||
| 78 | */ | ||
| 79 | public function __construct( | ||
| 80 | public string $type, | ||
| 81 | public string $fieldName, | ||
| 82 | public string $columnName, | ||
| 83 | ) { | ||
| 84 | } | ||
| 85 | |||
| 86 | /** | ||
| 87 | * @param array<string, mixed> $mappingArray | ||
| 88 | * @psalm-param array{ | ||
| 89 | * type: string, | ||
| 90 | * fieldName: string, | ||
| 91 | * columnName: string, | ||
| 92 | * length?: int|null, | ||
| 93 | * id?: bool|null, | ||
| 94 | * nullable?: bool|null, | ||
| 95 | * notInsertable?: bool|null, | ||
| 96 | * notUpdatable?: bool|null, | ||
| 97 | * columnDefinition?: string|null, | ||
| 98 | * generated?: ClassMetadata::GENERATED_*|null, | ||
| 99 | * enumType?: string|null, | ||
| 100 | * precision?: int|null, | ||
| 101 | * scale?: int|null, | ||
| 102 | * unique?: bool|null, | ||
| 103 | * inherited?: string|null, | ||
| 104 | * originalClass?: string|null, | ||
| 105 | * originalField?: string|null, | ||
| 106 | * quoted?: bool|null, | ||
| 107 | * declared?: string|null, | ||
| 108 | * declaredField?: string|null, | ||
| 109 | * options?: array<string, mixed>|null, | ||
| 110 | * version?: bool|null, | ||
| 111 | * default?: string|int|null, | ||
| 112 | * } $mappingArray | ||
| 113 | */ | ||
| 114 | public static function fromMappingArray(array $mappingArray): self | ||
| 115 | { | ||
| 116 | $mapping = new self( | ||
| 117 | $mappingArray['type'], | ||
| 118 | $mappingArray['fieldName'], | ||
| 119 | $mappingArray['columnName'], | ||
| 120 | ); | ||
| 121 | foreach ($mappingArray as $key => $value) { | ||
| 122 | if (in_array($key, ['type', 'fieldName', 'columnName'])) { | ||
| 123 | continue; | ||
| 124 | } | ||
| 125 | |||
| 126 | if (property_exists($mapping, $key)) { | ||
| 127 | $mapping->$key = $value; | ||
| 128 | } | ||
| 129 | } | ||
| 130 | |||
| 131 | return $mapping; | ||
| 132 | } | ||
| 133 | |||
| 134 | /** @return list<string> */ | ||
| 135 | public function __sleep(): array | ||
| 136 | { | ||
| 137 | $serialized = ['type', 'fieldName', 'columnName']; | ||
| 138 | |||
| 139 | foreach (['nullable', 'notInsertable', 'notUpdatable', 'id', 'unique', 'version', 'quoted'] as $boolKey) { | ||
| 140 | if ($this->$boolKey) { | ||
| 141 | $serialized[] = $boolKey; | ||
| 142 | } | ||
| 143 | } | ||
| 144 | |||
| 145 | foreach ( | ||
| 146 | [ | ||
| 147 | 'length', | ||
| 148 | 'columnDefinition', | ||
| 149 | 'generated', | ||
| 150 | 'enumType', | ||
| 151 | 'precision', | ||
| 152 | 'scale', | ||
| 153 | 'inherited', | ||
| 154 | 'originalClass', | ||
| 155 | 'originalField', | ||
| 156 | 'declared', | ||
| 157 | 'declaredField', | ||
| 158 | 'options', | ||
| 159 | 'default', | ||
| 160 | ] as $key | ||
| 161 | ) { | ||
| 162 | if ($this->$key !== null) { | ||
| 163 | $serialized[] = $key; | ||
| 164 | } | ||
| 165 | } | ||
| 166 | |||
| 167 | return $serialized; | ||
| 168 | } | ||
| 169 | } | ||
diff --git a/vendor/doctrine/orm/src/Mapping/GeneratedValue.php b/vendor/doctrine/orm/src/Mapping/GeneratedValue.php new file mode 100644 index 0000000..aca5f4b --- /dev/null +++ b/vendor/doctrine/orm/src/Mapping/GeneratedValue.php | |||
| @@ -0,0 +1,17 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Mapping; | ||
| 6 | |||
| 7 | use Attribute; | ||
| 8 | |||
| 9 | #[Attribute(Attribute::TARGET_PROPERTY)] | ||
| 10 | final class GeneratedValue implements MappingAttribute | ||
| 11 | { | ||
| 12 | /** @psalm-param 'AUTO'|'SEQUENCE'|'IDENTITY'|'NONE'|'CUSTOM' $strategy */ | ||
| 13 | public function __construct( | ||
| 14 | public readonly string $strategy = 'AUTO', | ||
| 15 | ) { | ||
| 16 | } | ||
| 17 | } | ||
diff --git a/vendor/doctrine/orm/src/Mapping/HasLifecycleCallbacks.php b/vendor/doctrine/orm/src/Mapping/HasLifecycleCallbacks.php new file mode 100644 index 0000000..d41a696 --- /dev/null +++ b/vendor/doctrine/orm/src/Mapping/HasLifecycleCallbacks.php | |||
| @@ -0,0 +1,12 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Mapping; | ||
| 6 | |||
| 7 | use Attribute; | ||
| 8 | |||
| 9 | #[Attribute(Attribute::TARGET_CLASS)] | ||
| 10 | final class HasLifecycleCallbacks implements MappingAttribute | ||
| 11 | { | ||
| 12 | } | ||
diff --git a/vendor/doctrine/orm/src/Mapping/Id.php b/vendor/doctrine/orm/src/Mapping/Id.php new file mode 100644 index 0000000..e85dd2f --- /dev/null +++ b/vendor/doctrine/orm/src/Mapping/Id.php | |||
| @@ -0,0 +1,12 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Mapping; | ||
| 6 | |||
| 7 | use Attribute; | ||
| 8 | |||
| 9 | #[Attribute(Attribute::TARGET_PROPERTY)] | ||
| 10 | final class Id implements MappingAttribute | ||
| 11 | { | ||
| 12 | } | ||
diff --git a/vendor/doctrine/orm/src/Mapping/Index.php b/vendor/doctrine/orm/src/Mapping/Index.php new file mode 100644 index 0000000..1de939e --- /dev/null +++ b/vendor/doctrine/orm/src/Mapping/Index.php | |||
| @@ -0,0 +1,26 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Mapping; | ||
| 6 | |||
| 7 | use Attribute; | ||
| 8 | |||
| 9 | #[Attribute(Attribute::TARGET_CLASS | Attribute::IS_REPEATABLE)] | ||
| 10 | final class Index implements MappingAttribute | ||
| 11 | { | ||
| 12 | /** | ||
| 13 | * @param array<string>|null $columns | ||
| 14 | * @param array<string>|null $fields | ||
| 15 | * @param array<string>|null $flags | ||
| 16 | * @param array<string,mixed>|null $options | ||
| 17 | */ | ||
| 18 | public function __construct( | ||
| 19 | public readonly string|null $name = null, | ||
| 20 | public readonly array|null $columns = null, | ||
| 21 | public readonly array|null $fields = null, | ||
| 22 | public readonly array|null $flags = null, | ||
| 23 | public readonly array|null $options = null, | ||
| 24 | ) { | ||
| 25 | } | ||
| 26 | } | ||
diff --git a/vendor/doctrine/orm/src/Mapping/InheritanceType.php b/vendor/doctrine/orm/src/Mapping/InheritanceType.php new file mode 100644 index 0000000..c042ee7 --- /dev/null +++ b/vendor/doctrine/orm/src/Mapping/InheritanceType.php | |||
| @@ -0,0 +1,17 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Mapping; | ||
| 6 | |||
| 7 | use Attribute; | ||
| 8 | |||
| 9 | #[Attribute(Attribute::TARGET_CLASS)] | ||
| 10 | final class InheritanceType implements MappingAttribute | ||
| 11 | { | ||
| 12 | /** @psalm-param 'NONE'|'JOINED'|'SINGLE_TABLE' $value */ | ||
| 13 | public function __construct( | ||
| 14 | public readonly string $value, | ||
| 15 | ) { | ||
| 16 | } | ||
| 17 | } | ||
diff --git a/vendor/doctrine/orm/src/Mapping/InverseJoinColumn.php b/vendor/doctrine/orm/src/Mapping/InverseJoinColumn.php new file mode 100644 index 0000000..2a77f3f --- /dev/null +++ b/vendor/doctrine/orm/src/Mapping/InverseJoinColumn.php | |||
| @@ -0,0 +1,13 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Mapping; | ||
| 6 | |||
| 7 | use Attribute; | ||
| 8 | |||
| 9 | #[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)] | ||
| 10 | final class InverseJoinColumn implements MappingAttribute | ||
| 11 | { | ||
| 12 | use JoinColumnProperties; | ||
| 13 | } | ||
diff --git a/vendor/doctrine/orm/src/Mapping/InverseSideMapping.php b/vendor/doctrine/orm/src/Mapping/InverseSideMapping.php new file mode 100644 index 0000000..56dce9f --- /dev/null +++ b/vendor/doctrine/orm/src/Mapping/InverseSideMapping.php | |||
| @@ -0,0 +1,30 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Mapping; | ||
| 6 | |||
| 7 | abstract class InverseSideMapping extends AssociationMapping | ||
| 8 | { | ||
| 9 | /** | ||
| 10 | * required for bidirectional associations | ||
| 11 | * The name of the field that completes the bidirectional association on | ||
| 12 | * the owning side. This key must be specified on the inverse side of a | ||
| 13 | * bidirectional association. | ||
| 14 | */ | ||
| 15 | public string $mappedBy; | ||
| 16 | |||
| 17 | final public function backRefFieldName(): string | ||
| 18 | { | ||
| 19 | return $this->mappedBy; | ||
| 20 | } | ||
| 21 | |||
| 22 | /** @return list<string> */ | ||
| 23 | public function __sleep(): array | ||
| 24 | { | ||
| 25 | return [ | ||
| 26 | ...parent::__sleep(), | ||
| 27 | 'mappedBy', | ||
| 28 | ]; | ||
| 29 | } | ||
| 30 | } | ||
diff --git a/vendor/doctrine/orm/src/Mapping/JoinColumn.php b/vendor/doctrine/orm/src/Mapping/JoinColumn.php new file mode 100644 index 0000000..9a049fb --- /dev/null +++ b/vendor/doctrine/orm/src/Mapping/JoinColumn.php | |||
| @@ -0,0 +1,13 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Mapping; | ||
| 6 | |||
| 7 | use Attribute; | ||
| 8 | |||
| 9 | #[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)] | ||
| 10 | final class JoinColumn implements MappingAttribute | ||
| 11 | { | ||
| 12 | use JoinColumnProperties; | ||
| 13 | } | ||
diff --git a/vendor/doctrine/orm/src/Mapping/JoinColumnMapping.php b/vendor/doctrine/orm/src/Mapping/JoinColumnMapping.php new file mode 100644 index 0000000..172c256 --- /dev/null +++ b/vendor/doctrine/orm/src/Mapping/JoinColumnMapping.php | |||
| @@ -0,0 +1,77 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Mapping; | ||
| 6 | |||
| 7 | use ArrayAccess; | ||
| 8 | |||
| 9 | use function property_exists; | ||
| 10 | |||
| 11 | /** @template-implements ArrayAccess<string, mixed> */ | ||
| 12 | final class JoinColumnMapping implements ArrayAccess | ||
| 13 | { | ||
| 14 | use ArrayAccessImplementation; | ||
| 15 | |||
| 16 | public bool|null $unique = null; | ||
| 17 | public bool|null $quoted = null; | ||
| 18 | public string|null $fieldName = null; | ||
| 19 | public string|null $onDelete = null; | ||
| 20 | public string|null $columnDefinition = null; | ||
| 21 | public bool|null $nullable = null; | ||
| 22 | |||
| 23 | /** @var array<string, mixed>|null */ | ||
| 24 | public array|null $options = null; | ||
| 25 | |||
| 26 | public function __construct( | ||
| 27 | public string $name, | ||
| 28 | public string $referencedColumnName, | ||
| 29 | ) { | ||
| 30 | } | ||
| 31 | |||
| 32 | /** | ||
| 33 | * @param array<string, mixed> $mappingArray | ||
| 34 | * @psalm-param array{ | ||
| 35 | * name: string, | ||
| 36 | * referencedColumnName: string, | ||
| 37 | * unique?: bool|null, | ||
| 38 | * quoted?: bool|null, | ||
| 39 | * fieldName?: string|null, | ||
| 40 | * onDelete?: string|null, | ||
| 41 | * columnDefinition?: string|null, | ||
| 42 | * nullable?: bool|null, | ||
| 43 | * options?: array<string, mixed>|null, | ||
| 44 | * } $mappingArray | ||
| 45 | */ | ||
| 46 | public static function fromMappingArray(array $mappingArray): self | ||
| 47 | { | ||
| 48 | $mapping = new self($mappingArray['name'], $mappingArray['referencedColumnName']); | ||
| 49 | foreach ($mappingArray as $key => $value) { | ||
| 50 | if (property_exists($mapping, $key) && $value !== null) { | ||
| 51 | $mapping->$key = $value; | ||
| 52 | } | ||
| 53 | } | ||
| 54 | |||
| 55 | return $mapping; | ||
| 56 | } | ||
| 57 | |||
| 58 | /** @return list<string> */ | ||
| 59 | public function __sleep(): array | ||
| 60 | { | ||
| 61 | $serialized = []; | ||
| 62 | |||
| 63 | foreach (['name', 'fieldName', 'onDelete', 'columnDefinition', 'referencedColumnName', 'options'] as $stringOrArrayKey) { | ||
| 64 | if ($this->$stringOrArrayKey !== null) { | ||
| 65 | $serialized[] = $stringOrArrayKey; | ||
| 66 | } | ||
| 67 | } | ||
| 68 | |||
| 69 | foreach (['unique', 'quoted', 'nullable'] as $boolKey) { | ||
| 70 | if ($this->$boolKey !== null) { | ||
| 71 | $serialized[] = $boolKey; | ||
| 72 | } | ||
| 73 | } | ||
| 74 | |||
| 75 | return $serialized; | ||
| 76 | } | ||
| 77 | } | ||
diff --git a/vendor/doctrine/orm/src/Mapping/JoinColumnProperties.php b/vendor/doctrine/orm/src/Mapping/JoinColumnProperties.php new file mode 100644 index 0000000..7d13295 --- /dev/null +++ b/vendor/doctrine/orm/src/Mapping/JoinColumnProperties.php | |||
| @@ -0,0 +1,21 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Mapping; | ||
| 6 | |||
| 7 | trait JoinColumnProperties | ||
| 8 | { | ||
| 9 | /** @param array<string, mixed> $options */ | ||
| 10 | public function __construct( | ||
| 11 | public readonly string|null $name = null, | ||
| 12 | public readonly string $referencedColumnName = 'id', | ||
| 13 | public readonly bool $unique = false, | ||
| 14 | public readonly bool $nullable = true, | ||
| 15 | public readonly mixed $onDelete = null, | ||
| 16 | public readonly string|null $columnDefinition = null, | ||
| 17 | public readonly string|null $fieldName = null, | ||
| 18 | public readonly array $options = [], | ||
| 19 | ) { | ||
| 20 | } | ||
| 21 | } | ||
diff --git a/vendor/doctrine/orm/src/Mapping/JoinColumns.php b/vendor/doctrine/orm/src/Mapping/JoinColumns.php new file mode 100644 index 0000000..f166b76 --- /dev/null +++ b/vendor/doctrine/orm/src/Mapping/JoinColumns.php | |||
| @@ -0,0 +1,14 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Mapping; | ||
| 6 | |||
| 7 | final class JoinColumns implements MappingAttribute | ||
| 8 | { | ||
| 9 | /** @param array<JoinColumn> $value */ | ||
| 10 | public function __construct( | ||
| 11 | public readonly array $value, | ||
| 12 | ) { | ||
| 13 | } | ||
| 14 | } | ||
diff --git a/vendor/doctrine/orm/src/Mapping/JoinTable.php b/vendor/doctrine/orm/src/Mapping/JoinTable.php new file mode 100644 index 0000000..0558761 --- /dev/null +++ b/vendor/doctrine/orm/src/Mapping/JoinTable.php | |||
| @@ -0,0 +1,35 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Mapping; | ||
| 6 | |||
| 7 | use Attribute; | ||
| 8 | |||
| 9 | #[Attribute(Attribute::TARGET_PROPERTY)] | ||
| 10 | final class JoinTable implements MappingAttribute | ||
| 11 | { | ||
| 12 | /** @var array<JoinColumn> */ | ||
| 13 | public readonly array $joinColumns; | ||
| 14 | |||
| 15 | /** @var array<JoinColumn> */ | ||
| 16 | public readonly array $inverseJoinColumns; | ||
| 17 | |||
| 18 | /** | ||
| 19 | * @param array<JoinColumn>|JoinColumn $joinColumns | ||
| 20 | * @param array<JoinColumn>|JoinColumn $inverseJoinColumns | ||
| 21 | * @param array<string, mixed> $options | ||
| 22 | */ | ||
| 23 | public function __construct( | ||
| 24 | public readonly string|null $name = null, | ||
| 25 | public readonly string|null $schema = null, | ||
| 26 | array|JoinColumn $joinColumns = [], | ||
| 27 | array|JoinColumn $inverseJoinColumns = [], | ||
| 28 | public readonly array $options = [], | ||
| 29 | ) { | ||
| 30 | $this->joinColumns = $joinColumns instanceof JoinColumn ? [$joinColumns] : $joinColumns; | ||
| 31 | $this->inverseJoinColumns = $inverseJoinColumns instanceof JoinColumn | ||
| 32 | ? [$inverseJoinColumns] | ||
| 33 | : $inverseJoinColumns; | ||
| 34 | } | ||
| 35 | } | ||
diff --git a/vendor/doctrine/orm/src/Mapping/JoinTableMapping.php b/vendor/doctrine/orm/src/Mapping/JoinTableMapping.php new file mode 100644 index 0000000..c8b4968 --- /dev/null +++ b/vendor/doctrine/orm/src/Mapping/JoinTableMapping.php | |||
| @@ -0,0 +1,115 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Mapping; | ||
| 6 | |||
| 7 | use ArrayAccess; | ||
| 8 | |||
| 9 | use function array_map; | ||
| 10 | use function in_array; | ||
| 11 | |||
| 12 | /** @template-implements ArrayAccess<string, mixed> */ | ||
| 13 | final class JoinTableMapping implements ArrayAccess | ||
| 14 | { | ||
| 15 | use ArrayAccessImplementation; | ||
| 16 | |||
| 17 | public bool|null $quoted = null; | ||
| 18 | |||
| 19 | /** @var list<JoinColumnMapping> */ | ||
| 20 | public array $joinColumns = []; | ||
| 21 | |||
| 22 | /** @var list<JoinColumnMapping> */ | ||
| 23 | public array $inverseJoinColumns = []; | ||
| 24 | |||
| 25 | /** @var array<string, mixed> */ | ||
| 26 | public array $options = []; | ||
| 27 | |||
| 28 | public string|null $schema = null; | ||
| 29 | |||
| 30 | public function __construct(public string $name) | ||
| 31 | { | ||
| 32 | } | ||
| 33 | |||
| 34 | /** | ||
| 35 | * @param mixed[] $mappingArray | ||
| 36 | * @psalm-param array{ | ||
| 37 | * name: string, | ||
| 38 | * quoted?: bool|null, | ||
| 39 | * joinColumns?: mixed[], | ||
| 40 | * inverseJoinColumns?: mixed[], | ||
| 41 | * schema?: string|null, | ||
| 42 | * options?: array<string, mixed> | ||
| 43 | * } $mappingArray | ||
| 44 | */ | ||
| 45 | public static function fromMappingArray(array $mappingArray): self | ||
| 46 | { | ||
| 47 | $mapping = new self($mappingArray['name']); | ||
| 48 | |||
| 49 | foreach (['quoted', 'schema', 'options'] as $key) { | ||
| 50 | if (isset($mappingArray[$key])) { | ||
| 51 | $mapping->$key = $mappingArray[$key]; | ||
| 52 | } | ||
| 53 | } | ||
| 54 | |||
| 55 | if (isset($mappingArray['joinColumns'])) { | ||
| 56 | foreach ($mappingArray['joinColumns'] as $column) { | ||
| 57 | $mapping->joinColumns[] = JoinColumnMapping::fromMappingArray($column); | ||
| 58 | } | ||
| 59 | } | ||
| 60 | |||
| 61 | if (isset($mappingArray['inverseJoinColumns'])) { | ||
| 62 | foreach ($mappingArray['inverseJoinColumns'] as $column) { | ||
| 63 | $mapping->inverseJoinColumns[] = JoinColumnMapping::fromMappingArray($column); | ||
| 64 | } | ||
| 65 | } | ||
| 66 | |||
| 67 | return $mapping; | ||
| 68 | } | ||
| 69 | |||
| 70 | public function offsetSet(mixed $offset, mixed $value): void | ||
| 71 | { | ||
| 72 | if (in_array($offset, ['joinColumns', 'inverseJoinColumns'], true)) { | ||
| 73 | $joinColumns = []; | ||
| 74 | foreach ($value as $column) { | ||
| 75 | $joinColumns[] = JoinColumnMapping::fromMappingArray($column); | ||
| 76 | } | ||
| 77 | |||
| 78 | $value = $joinColumns; | ||
| 79 | } | ||
| 80 | |||
| 81 | $this->$offset = $value; | ||
| 82 | } | ||
| 83 | |||
| 84 | /** @return mixed[] */ | ||
| 85 | public function toArray(): array | ||
| 86 | { | ||
| 87 | $array = (array) $this; | ||
| 88 | |||
| 89 | $toArray = static fn (JoinColumnMapping $column): array => (array) $column; | ||
| 90 | $array['joinColumns'] = array_map($toArray, $array['joinColumns']); | ||
| 91 | $array['inverseJoinColumns'] = array_map($toArray, $array['inverseJoinColumns']); | ||
| 92 | |||
| 93 | return $array; | ||
| 94 | } | ||
| 95 | |||
| 96 | /** @return list<string> */ | ||
| 97 | public function __sleep(): array | ||
| 98 | { | ||
| 99 | $serialized = []; | ||
| 100 | |||
| 101 | foreach (['joinColumns', 'inverseJoinColumns', 'name', 'schema', 'options'] as $stringOrArrayKey) { | ||
| 102 | if ($this->$stringOrArrayKey !== null) { | ||
| 103 | $serialized[] = $stringOrArrayKey; | ||
| 104 | } | ||
| 105 | } | ||
| 106 | |||
| 107 | foreach (['quoted'] as $boolKey) { | ||
| 108 | if ($this->$boolKey) { | ||
| 109 | $serialized[] = $boolKey; | ||
| 110 | } | ||
| 111 | } | ||
| 112 | |||
| 113 | return $serialized; | ||
| 114 | } | ||
| 115 | } | ||
diff --git a/vendor/doctrine/orm/src/Mapping/ManyToMany.php b/vendor/doctrine/orm/src/Mapping/ManyToMany.php new file mode 100644 index 0000000..d90a762 --- /dev/null +++ b/vendor/doctrine/orm/src/Mapping/ManyToMany.php | |||
| @@ -0,0 +1,27 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Mapping; | ||
| 6 | |||
| 7 | use Attribute; | ||
| 8 | |||
| 9 | #[Attribute(Attribute::TARGET_PROPERTY)] | ||
| 10 | final class ManyToMany implements MappingAttribute | ||
| 11 | { | ||
| 12 | /** | ||
| 13 | * @param class-string $targetEntity | ||
| 14 | * @param string[]|null $cascade | ||
| 15 | * @psalm-param 'LAZY'|'EAGER'|'EXTRA_LAZY' $fetch | ||
| 16 | */ | ||
| 17 | public function __construct( | ||
| 18 | public readonly string $targetEntity, | ||
| 19 | public readonly string|null $mappedBy = null, | ||
| 20 | public readonly string|null $inversedBy = null, | ||
| 21 | public readonly array|null $cascade = null, | ||
| 22 | public readonly string $fetch = 'LAZY', | ||
| 23 | public readonly bool $orphanRemoval = false, | ||
| 24 | public readonly string|null $indexBy = null, | ||
| 25 | ) { | ||
| 26 | } | ||
| 27 | } | ||
diff --git a/vendor/doctrine/orm/src/Mapping/ManyToManyAssociationMapping.php b/vendor/doctrine/orm/src/Mapping/ManyToManyAssociationMapping.php new file mode 100644 index 0000000..8d963c2 --- /dev/null +++ b/vendor/doctrine/orm/src/Mapping/ManyToManyAssociationMapping.php | |||
| @@ -0,0 +1,9 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Mapping; | ||
| 6 | |||
| 7 | interface ManyToManyAssociationMapping extends ToManyAssociationMapping | ||
| 8 | { | ||
| 9 | } | ||
diff --git a/vendor/doctrine/orm/src/Mapping/ManyToManyInverseSideMapping.php b/vendor/doctrine/orm/src/Mapping/ManyToManyInverseSideMapping.php new file mode 100644 index 0000000..8cd5cbd --- /dev/null +++ b/vendor/doctrine/orm/src/Mapping/ManyToManyInverseSideMapping.php | |||
| @@ -0,0 +1,9 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Mapping; | ||
| 6 | |||
| 7 | final class ManyToManyInverseSideMapping extends ToManyInverseSideMapping implements ManyToManyAssociationMapping | ||
| 8 | { | ||
| 9 | } | ||
diff --git a/vendor/doctrine/orm/src/Mapping/ManyToManyOwningSideMapping.php b/vendor/doctrine/orm/src/Mapping/ManyToManyOwningSideMapping.php new file mode 100644 index 0000000..b09d56c --- /dev/null +++ b/vendor/doctrine/orm/src/Mapping/ManyToManyOwningSideMapping.php | |||
| @@ -0,0 +1,185 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Mapping; | ||
| 6 | |||
| 7 | use function strtolower; | ||
| 8 | use function trim; | ||
| 9 | |||
| 10 | final class ManyToManyOwningSideMapping extends ToManyOwningSideMapping implements ManyToManyAssociationMapping | ||
| 11 | { | ||
| 12 | /** | ||
| 13 | * Specification of the join table and its join columns (foreign keys). | ||
| 14 | * Only valid for many-to-many mappings. Note that one-to-many associations | ||
| 15 | * can be mapped through a join table by simply mapping the association as | ||
| 16 | * many-to-many with a unique constraint on the join table. | ||
| 17 | */ | ||
| 18 | public JoinTableMapping $joinTable; | ||
| 19 | |||
| 20 | /** @var list<mixed> */ | ||
| 21 | public array $joinTableColumns = []; | ||
| 22 | |||
| 23 | /** @var array<string, string> */ | ||
| 24 | public array $relationToSourceKeyColumns = []; | ||
| 25 | /** @var array<string, string> */ | ||
| 26 | public array $relationToTargetKeyColumns = []; | ||
| 27 | |||
| 28 | /** @return array<string, mixed> */ | ||
| 29 | public function toArray(): array | ||
| 30 | { | ||
| 31 | $array = parent::toArray(); | ||
| 32 | |||
| 33 | $array['joinTable'] = $this->joinTable->toArray(); | ||
| 34 | |||
| 35 | return $array; | ||
| 36 | } | ||
| 37 | |||
| 38 | /** | ||
| 39 | * @param mixed[] $mappingArray | ||
| 40 | * @psalm-param array{ | ||
| 41 | * fieldName: string, | ||
| 42 | * sourceEntity: class-string, | ||
| 43 | * targetEntity: class-string, | ||
| 44 | * cascade?: list<'persist'|'remove'|'detach'|'refresh'|'all'>, | ||
| 45 | * fetch?: ClassMetadata::FETCH_*|null, | ||
| 46 | * inherited?: class-string|null, | ||
| 47 | * declared?: class-string|null, | ||
| 48 | * cache?: array<mixed>|null, | ||
| 49 | * id?: bool|null, | ||
| 50 | * isOnDeleteCascade?: bool|null, | ||
| 51 | * originalClass?: class-string|null, | ||
| 52 | * originalField?: string|null, | ||
| 53 | * orphanRemoval?: bool, | ||
| 54 | * unique?: bool|null, | ||
| 55 | * joinTable?: mixed[]|null, | ||
| 56 | * type?: int, | ||
| 57 | * isOwningSide: bool, | ||
| 58 | * } $mappingArray | ||
| 59 | */ | ||
| 60 | public static function fromMappingArrayAndNamingStrategy(array $mappingArray, NamingStrategy $namingStrategy): self | ||
| 61 | { | ||
| 62 | if (isset($mappingArray['joinTable']['joinColumns'])) { | ||
| 63 | foreach ($mappingArray['joinTable']['joinColumns'] as $key => $joinColumn) { | ||
| 64 | if (empty($joinColumn['name'])) { | ||
| 65 | $mappingArray['joinTable']['joinColumns'][$key]['name'] = $namingStrategy->joinKeyColumnName( | ||
| 66 | $mappingArray['sourceEntity'], | ||
| 67 | $joinColumn['referencedColumnName'] ?? null, | ||
| 68 | ); | ||
| 69 | } | ||
| 70 | } | ||
| 71 | } | ||
| 72 | |||
| 73 | if (isset($mappingArray['joinTable']['inverseJoinColumns'])) { | ||
| 74 | foreach ($mappingArray['joinTable']['inverseJoinColumns'] as $key => $joinColumn) { | ||
| 75 | if (empty($joinColumn['name'])) { | ||
| 76 | $mappingArray['joinTable']['inverseJoinColumns'][$key]['name'] = $namingStrategy->joinKeyColumnName( | ||
| 77 | $mappingArray['targetEntity'], | ||
| 78 | $joinColumn['referencedColumnName'] ?? null, | ||
| 79 | ); | ||
| 80 | } | ||
| 81 | } | ||
| 82 | } | ||
| 83 | |||
| 84 | // owning side MUST have a join table | ||
| 85 | if (! isset($mappingArray['joinTable']) || ! isset($mappingArray['joinTable']['name'])) { | ||
| 86 | $mappingArray['joinTable']['name'] = $namingStrategy->joinTableName( | ||
| 87 | $mappingArray['sourceEntity'], | ||
| 88 | $mappingArray['targetEntity'], | ||
| 89 | $mappingArray['fieldName'], | ||
| 90 | ); | ||
| 91 | } | ||
| 92 | |||
| 93 | $mapping = parent::fromMappingArray($mappingArray); | ||
| 94 | |||
| 95 | $selfReferencingEntityWithoutJoinColumns = $mapping->sourceEntity === $mapping->targetEntity | ||
| 96 | && $mapping->joinTable->joinColumns === [] | ||
| 97 | && $mapping->joinTable->inverseJoinColumns === []; | ||
| 98 | |||
| 99 | if ($mapping->joinTable->joinColumns === []) { | ||
| 100 | $mapping->joinTable->joinColumns = [ | ||
| 101 | JoinColumnMapping::fromMappingArray([ | ||
| 102 | 'name' => $namingStrategy->joinKeyColumnName($mapping->sourceEntity, $selfReferencingEntityWithoutJoinColumns ? 'source' : null), | ||
| 103 | 'referencedColumnName' => $namingStrategy->referenceColumnName(), | ||
| 104 | 'onDelete' => 'CASCADE', | ||
| 105 | ]), | ||
| 106 | ]; | ||
| 107 | } | ||
| 108 | |||
| 109 | if ($mapping->joinTable->inverseJoinColumns === []) { | ||
| 110 | $mapping->joinTable->inverseJoinColumns = [ | ||
| 111 | JoinColumnMapping::fromMappingArray([ | ||
| 112 | 'name' => $namingStrategy->joinKeyColumnName($mapping->targetEntity, $selfReferencingEntityWithoutJoinColumns ? 'target' : null), | ||
| 113 | 'referencedColumnName' => $namingStrategy->referenceColumnName(), | ||
| 114 | 'onDelete' => 'CASCADE', | ||
| 115 | ]), | ||
| 116 | ]; | ||
| 117 | } | ||
| 118 | |||
| 119 | $mapping->joinTableColumns = []; | ||
| 120 | |||
| 121 | foreach ($mapping->joinTable->joinColumns as $joinColumn) { | ||
| 122 | if (empty($joinColumn->referencedColumnName)) { | ||
| 123 | $joinColumn->referencedColumnName = $namingStrategy->referenceColumnName(); | ||
| 124 | } | ||
| 125 | |||
| 126 | if ($joinColumn->name[0] === '`') { | ||
| 127 | $joinColumn->name = trim($joinColumn->name, '`'); | ||
| 128 | $joinColumn->quoted = true; | ||
| 129 | } | ||
| 130 | |||
| 131 | if ($joinColumn->referencedColumnName[0] === '`') { | ||
| 132 | $joinColumn->referencedColumnName = trim($joinColumn->referencedColumnName, '`'); | ||
| 133 | $joinColumn->quoted = true; | ||
| 134 | } | ||
| 135 | |||
| 136 | if (isset($joinColumn->onDelete) && strtolower($joinColumn->onDelete) === 'cascade') { | ||
| 137 | $mapping->isOnDeleteCascade = true; | ||
| 138 | } | ||
| 139 | |||
| 140 | $mapping->relationToSourceKeyColumns[$joinColumn->name] = $joinColumn->referencedColumnName; | ||
| 141 | $mapping->joinTableColumns[] = $joinColumn->name; | ||
| 142 | } | ||
| 143 | |||
| 144 | foreach ($mapping->joinTable->inverseJoinColumns as $inverseJoinColumn) { | ||
| 145 | if (empty($inverseJoinColumn->referencedColumnName)) { | ||
| 146 | $inverseJoinColumn->referencedColumnName = $namingStrategy->referenceColumnName(); | ||
| 147 | } | ||
| 148 | |||
| 149 | if ($inverseJoinColumn->name[0] === '`') { | ||
| 150 | $inverseJoinColumn->name = trim($inverseJoinColumn->name, '`'); | ||
| 151 | $inverseJoinColumn->quoted = true; | ||
| 152 | } | ||
| 153 | |||
| 154 | if ($inverseJoinColumn->referencedColumnName[0] === '`') { | ||
| 155 | $inverseJoinColumn->referencedColumnName = trim($inverseJoinColumn->referencedColumnName, '`'); | ||
| 156 | $inverseJoinColumn->quoted = true; | ||
| 157 | } | ||
| 158 | |||
| 159 | if (isset($inverseJoinColumn->onDelete) && strtolower($inverseJoinColumn->onDelete) === 'cascade') { | ||
| 160 | $mapping->isOnDeleteCascade = true; | ||
| 161 | } | ||
| 162 | |||
| 163 | $mapping->relationToTargetKeyColumns[$inverseJoinColumn->name] = $inverseJoinColumn->referencedColumnName; | ||
| 164 | $mapping->joinTableColumns[] = $inverseJoinColumn->name; | ||
| 165 | } | ||
| 166 | |||
| 167 | return $mapping; | ||
| 168 | } | ||
| 169 | |||
| 170 | /** @return list<string> */ | ||
| 171 | public function __sleep(): array | ||
| 172 | { | ||
| 173 | $serialized = parent::__sleep(); | ||
| 174 | $serialized[] = 'joinTable'; | ||
| 175 | $serialized[] = 'joinTableColumns'; | ||
| 176 | |||
| 177 | foreach (['relationToSourceKeyColumns', 'relationToTargetKeyColumns'] as $arrayKey) { | ||
| 178 | if ($this->$arrayKey !== null) { | ||
| 179 | $serialized[] = $arrayKey; | ||
| 180 | } | ||
| 181 | } | ||
| 182 | |||
| 183 | return $serialized; | ||
| 184 | } | ||
| 185 | } | ||
diff --git a/vendor/doctrine/orm/src/Mapping/ManyToOne.php b/vendor/doctrine/orm/src/Mapping/ManyToOne.php new file mode 100644 index 0000000..8fccff3 --- /dev/null +++ b/vendor/doctrine/orm/src/Mapping/ManyToOne.php | |||
| @@ -0,0 +1,24 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Mapping; | ||
| 6 | |||
| 7 | use Attribute; | ||
| 8 | |||
| 9 | #[Attribute(Attribute::TARGET_PROPERTY)] | ||
| 10 | final class ManyToOne implements MappingAttribute | ||
| 11 | { | ||
| 12 | /** | ||
| 13 | * @param class-string|null $targetEntity | ||
| 14 | * @param string[]|null $cascade | ||
| 15 | * @psalm-param 'LAZY'|'EAGER'|'EXTRA_LAZY' $fetch | ||
| 16 | */ | ||
| 17 | public function __construct( | ||
| 18 | public readonly string|null $targetEntity = null, | ||
| 19 | public readonly array|null $cascade = null, | ||
| 20 | public readonly string $fetch = 'LAZY', | ||
| 21 | public readonly string|null $inversedBy = null, | ||
| 22 | ) { | ||
| 23 | } | ||
| 24 | } | ||
diff --git a/vendor/doctrine/orm/src/Mapping/ManyToOneAssociationMapping.php b/vendor/doctrine/orm/src/Mapping/ManyToOneAssociationMapping.php new file mode 100644 index 0000000..3d1bf90 --- /dev/null +++ b/vendor/doctrine/orm/src/Mapping/ManyToOneAssociationMapping.php | |||
| @@ -0,0 +1,12 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Mapping; | ||
| 6 | |||
| 7 | /** | ||
| 8 | * The "many" side of a many-to-one association mapping is always the owning side. | ||
| 9 | */ | ||
| 10 | final class ManyToOneAssociationMapping extends ToOneOwningSideMapping | ||
| 11 | { | ||
| 12 | } | ||
diff --git a/vendor/doctrine/orm/src/Mapping/MappedSuperclass.php b/vendor/doctrine/orm/src/Mapping/MappedSuperclass.php new file mode 100644 index 0000000..29475a2 --- /dev/null +++ b/vendor/doctrine/orm/src/Mapping/MappedSuperclass.php | |||
| @@ -0,0 +1,18 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Mapping; | ||
| 6 | |||
| 7 | use Attribute; | ||
| 8 | use Doctrine\ORM\EntityRepository; | ||
| 9 | |||
| 10 | #[Attribute(Attribute::TARGET_CLASS)] | ||
| 11 | final class MappedSuperclass implements MappingAttribute | ||
| 12 | { | ||
| 13 | /** @psalm-param class-string<EntityRepository>|null $repositoryClass */ | ||
| 14 | public function __construct( | ||
| 15 | public readonly string|null $repositoryClass = null, | ||
| 16 | ) { | ||
| 17 | } | ||
| 18 | } | ||
diff --git a/vendor/doctrine/orm/src/Mapping/MappingAttribute.php b/vendor/doctrine/orm/src/Mapping/MappingAttribute.php new file mode 100644 index 0000000..61091a9 --- /dev/null +++ b/vendor/doctrine/orm/src/Mapping/MappingAttribute.php | |||
| @@ -0,0 +1,10 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Mapping; | ||
| 6 | |||
| 7 | /** A marker interface for mapping attributes. */ | ||
| 8 | interface MappingAttribute | ||
| 9 | { | ||
| 10 | } | ||
diff --git a/vendor/doctrine/orm/src/Mapping/MappingException.php b/vendor/doctrine/orm/src/Mapping/MappingException.php new file mode 100644 index 0000000..9b73242 --- /dev/null +++ b/vendor/doctrine/orm/src/Mapping/MappingException.php | |||
| @@ -0,0 +1,691 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Mapping; | ||
| 6 | |||
| 7 | use BackedEnum; | ||
| 8 | use Doctrine\ORM\Exception\ORMException; | ||
| 9 | use Doctrine\Persistence\Mapping\MappingException as PersistenceMappingException; | ||
| 10 | use LibXMLError; | ||
| 11 | use ReflectionException; | ||
| 12 | use ValueError; | ||
| 13 | |||
| 14 | use function array_keys; | ||
| 15 | use function array_map; | ||
| 16 | use function array_values; | ||
| 17 | use function get_debug_type; | ||
| 18 | use function get_parent_class; | ||
| 19 | use function implode; | ||
| 20 | use function sprintf; | ||
| 21 | |||
| 22 | use const PHP_EOL; | ||
| 23 | |||
| 24 | /** | ||
| 25 | * A MappingException indicates that something is wrong with the mapping setup. | ||
| 26 | */ | ||
| 27 | class MappingException extends PersistenceMappingException implements ORMException | ||
| 28 | { | ||
| 29 | /** @param class-string $entityName */ | ||
| 30 | public static function identifierRequired(string $entityName): self | ||
| 31 | { | ||
| 32 | $parent = get_parent_class($entityName); | ||
| 33 | if ($parent !== false) { | ||
| 34 | return new self(sprintf( | ||
| 35 | 'No identifier/primary key specified for Entity "%s" sub class of "%s". Every Entity must have an identifier/primary key.', | ||
| 36 | $entityName, | ||
| 37 | $parent, | ||
| 38 | )); | ||
| 39 | } | ||
| 40 | |||
| 41 | return new self(sprintf( | ||
| 42 | 'No identifier/primary key specified for Entity "%s". Every Entity must have an identifier/primary key.', | ||
| 43 | $entityName, | ||
| 44 | )); | ||
| 45 | } | ||
| 46 | |||
| 47 | public static function invalidAssociationType(string $entityName, string $fieldName, int $type): self | ||
| 48 | { | ||
| 49 | return new self(sprintf( | ||
| 50 | 'The association "%s#%s" must be of type "ClassMetadata::ONE_TO_MANY", "ClassMetadata::MANY_TO_MANY" or "ClassMetadata::MANY_TO_ONE", "%d" given.', | ||
| 51 | $entityName, | ||
| 52 | $fieldName, | ||
| 53 | $type, | ||
| 54 | )); | ||
| 55 | } | ||
| 56 | |||
| 57 | public static function invalidInheritanceType(string $entityName, int $type): self | ||
| 58 | { | ||
| 59 | return new self(sprintf("The inheritance type '%s' specified for '%s' does not exist.", $type, $entityName)); | ||
| 60 | } | ||
| 61 | |||
| 62 | public static function generatorNotAllowedWithCompositeId(): self | ||
| 63 | { | ||
| 64 | return new self("Id generators can't be used with a composite id."); | ||
| 65 | } | ||
| 66 | |||
| 67 | public static function missingFieldName(string $entity): self | ||
| 68 | { | ||
| 69 | return new self(sprintf( | ||
| 70 | "The field or association mapping misses the 'fieldName' attribute in entity '%s'.", | ||
| 71 | $entity, | ||
| 72 | )); | ||
| 73 | } | ||
| 74 | |||
| 75 | public static function missingTargetEntity(string $fieldName): self | ||
| 76 | { | ||
| 77 | return new self(sprintf("The association mapping '%s' misses the 'targetEntity' attribute.", $fieldName)); | ||
| 78 | } | ||
| 79 | |||
| 80 | public static function missingSourceEntity(string $fieldName): self | ||
| 81 | { | ||
| 82 | return new self(sprintf("The association mapping '%s' misses the 'sourceEntity' attribute.", $fieldName)); | ||
| 83 | } | ||
| 84 | |||
| 85 | public static function missingEmbeddedClass(string $fieldName): self | ||
| 86 | { | ||
| 87 | return new self(sprintf("The embed mapping '%s' misses the 'class' attribute.", $fieldName)); | ||
| 88 | } | ||
| 89 | |||
| 90 | public static function mappingFileNotFound(string $entityName, string $fileName): self | ||
| 91 | { | ||
| 92 | return new self(sprintf("No mapping file found named '%s' for class '%s'.", $fileName, $entityName)); | ||
| 93 | } | ||
| 94 | |||
| 95 | /** | ||
| 96 | * Exception for invalid property name override. | ||
| 97 | * | ||
| 98 | * @param string $className The entity's name. | ||
| 99 | */ | ||
| 100 | public static function invalidOverrideFieldName(string $className, string $fieldName): self | ||
| 101 | { | ||
| 102 | return new self(sprintf("Invalid field override named '%s' for class '%s'.", $fieldName, $className)); | ||
| 103 | } | ||
| 104 | |||
| 105 | /** | ||
| 106 | * Exception for invalid property type override. | ||
| 107 | * | ||
| 108 | * @param string $className The entity's name. | ||
| 109 | */ | ||
| 110 | public static function invalidOverrideFieldType(string $className, string $fieldName): self | ||
| 111 | { | ||
| 112 | return new self(sprintf( | ||
| 113 | "The column type of attribute '%s' on class '%s' could not be changed.", | ||
| 114 | $fieldName, | ||
| 115 | $className, | ||
| 116 | )); | ||
| 117 | } | ||
| 118 | |||
| 119 | public static function mappingNotFound(string $className, string $fieldName): self | ||
| 120 | { | ||
| 121 | return new self(sprintf("No mapping found for field '%s' on class '%s'.", $fieldName, $className)); | ||
| 122 | } | ||
| 123 | |||
| 124 | public static function queryNotFound(string $className, string $queryName): self | ||
| 125 | { | ||
| 126 | return new self(sprintf("No query found named '%s' on class '%s'.", $queryName, $className)); | ||
| 127 | } | ||
| 128 | |||
| 129 | public static function resultMappingNotFound(string $className, string $resultName): self | ||
| 130 | { | ||
| 131 | return new self(sprintf("No result set mapping found named '%s' on class '%s'.", $resultName, $className)); | ||
| 132 | } | ||
| 133 | |||
| 134 | public static function emptyQueryMapping(string $entity, string $queryName): self | ||
| 135 | { | ||
| 136 | return new self(sprintf('Query named "%s" in "%s" could not be empty.', $queryName, $entity)); | ||
| 137 | } | ||
| 138 | |||
| 139 | public static function nameIsMandatoryForQueryMapping(string $className): self | ||
| 140 | { | ||
| 141 | return new self(sprintf("Query name on entity class '%s' is not defined.", $className)); | ||
| 142 | } | ||
| 143 | |||
| 144 | public static function missingQueryMapping(string $entity, string $queryName): self | ||
| 145 | { | ||
| 146 | return new self(sprintf( | ||
| 147 | 'Query named "%s" in "%s requires a result class or result set mapping.', | ||
| 148 | $queryName, | ||
| 149 | $entity, | ||
| 150 | )); | ||
| 151 | } | ||
| 152 | |||
| 153 | public static function missingResultSetMappingEntity(string $entity, string $resultName): self | ||
| 154 | { | ||
| 155 | return new self(sprintf( | ||
| 156 | 'Result set mapping named "%s" in "%s requires a entity class name.', | ||
| 157 | $resultName, | ||
| 158 | $entity, | ||
| 159 | )); | ||
| 160 | } | ||
| 161 | |||
| 162 | public static function missingResultSetMappingFieldName(string $entity, string $resultName): self | ||
| 163 | { | ||
| 164 | return new self(sprintf( | ||
| 165 | 'Result set mapping named "%s" in "%s requires a field name.', | ||
| 166 | $resultName, | ||
| 167 | $entity, | ||
| 168 | )); | ||
| 169 | } | ||
| 170 | |||
| 171 | public static function oneToManyRequiresMappedBy(string $entityName, string $fieldName): MappingException | ||
| 172 | { | ||
| 173 | return new self(sprintf( | ||
| 174 | "OneToMany mapping on entity '%s' field '%s' requires the 'mappedBy' attribute.", | ||
| 175 | $entityName, | ||
| 176 | $fieldName, | ||
| 177 | )); | ||
| 178 | } | ||
| 179 | |||
| 180 | public static function joinTableRequired(string $fieldName): self | ||
| 181 | { | ||
| 182 | return new self(sprintf("The mapping of field '%s' requires an the 'joinTable' attribute.", $fieldName)); | ||
| 183 | } | ||
| 184 | |||
| 185 | /** | ||
| 186 | * Called if a required option was not found but is required | ||
| 187 | * | ||
| 188 | * @param string $field Which field cannot be processed? | ||
| 189 | * @param string $expectedOption Which option is required | ||
| 190 | * @param string $hint Can optionally be used to supply a tip for common mistakes, | ||
| 191 | * e.g. "Did you think of the plural s?" | ||
| 192 | */ | ||
| 193 | public static function missingRequiredOption(string $field, string $expectedOption, string $hint = ''): self | ||
| 194 | { | ||
| 195 | $message = "The mapping of field '" . $field . "' is invalid: The option '" . $expectedOption . "' is required."; | ||
| 196 | |||
| 197 | if (! empty($hint)) { | ||
| 198 | $message .= ' (Hint: ' . $hint . ')'; | ||
| 199 | } | ||
| 200 | |||
| 201 | return new self($message); | ||
| 202 | } | ||
| 203 | |||
| 204 | /** | ||
| 205 | * Generic exception for invalid mappings. | ||
| 206 | */ | ||
| 207 | public static function invalidMapping(string $fieldName): self | ||
| 208 | { | ||
| 209 | return new self(sprintf("The mapping of field '%s' is invalid.", $fieldName)); | ||
| 210 | } | ||
| 211 | |||
| 212 | /** | ||
| 213 | * Exception for reflection exceptions - adds the entity name, | ||
| 214 | * because there might be long classnames that will be shortened | ||
| 215 | * within the stacktrace | ||
| 216 | * | ||
| 217 | * @param string $entity The entity's name | ||
| 218 | */ | ||
| 219 | public static function reflectionFailure(string $entity, ReflectionException $previousException): self | ||
| 220 | { | ||
| 221 | return new self(sprintf('An error occurred in %s', $entity), 0, $previousException); | ||
| 222 | } | ||
| 223 | |||
| 224 | public static function joinColumnMustPointToMappedField(string $className, string $joinColumn): self | ||
| 225 | { | ||
| 226 | return new self(sprintf( | ||
| 227 | 'The column %s must be mapped to a field in class %s since it is referenced by a join column of another class.', | ||
| 228 | $joinColumn, | ||
| 229 | $className, | ||
| 230 | )); | ||
| 231 | } | ||
| 232 | |||
| 233 | public static function joinColumnNotAllowedOnOneToOneInverseSide(string $className, string $fieldName): self | ||
| 234 | { | ||
| 235 | return new self(sprintf( | ||
| 236 | '%s#%s is a OneToOne inverse side, which does not allow join columns.', | ||
| 237 | $className, | ||
| 238 | $fieldName, | ||
| 239 | )); | ||
| 240 | } | ||
| 241 | |||
| 242 | /** @param class-string $className */ | ||
| 243 | public static function classIsNotAValidEntityOrMappedSuperClass(string $className): self | ||
| 244 | { | ||
| 245 | $parent = get_parent_class($className); | ||
| 246 | if ($parent !== false) { | ||
| 247 | return new self(sprintf( | ||
| 248 | 'Class "%s" sub class of "%s" is not a valid entity or mapped super class.', | ||
| 249 | $className, | ||
| 250 | $parent, | ||
| 251 | )); | ||
| 252 | } | ||
| 253 | |||
| 254 | return new self(sprintf( | ||
| 255 | 'Class "%s" is not a valid entity or mapped super class.', | ||
| 256 | $className, | ||
| 257 | )); | ||
| 258 | } | ||
| 259 | |||
| 260 | /** | ||
| 261 | * @param string $entity The entity's name. | ||
| 262 | * @param string $fieldName The name of the field that was already declared. | ||
| 263 | */ | ||
| 264 | public static function duplicateFieldMapping(string $entity, string $fieldName): self | ||
| 265 | { | ||
| 266 | return new self(sprintf( | ||
| 267 | 'Property "%s" in "%s" was already declared, but it must be declared only once', | ||
| 268 | $fieldName, | ||
| 269 | $entity, | ||
| 270 | )); | ||
| 271 | } | ||
| 272 | |||
| 273 | public static function duplicateAssociationMapping(string $entity, string $fieldName): self | ||
| 274 | { | ||
| 275 | return new self(sprintf( | ||
| 276 | 'Property "%s" in "%s" was already declared, but it must be declared only once', | ||
| 277 | $fieldName, | ||
| 278 | $entity, | ||
| 279 | )); | ||
| 280 | } | ||
| 281 | |||
| 282 | public static function duplicateQueryMapping(string $entity, string $queryName): self | ||
| 283 | { | ||
| 284 | return new self(sprintf( | ||
| 285 | 'Query named "%s" in "%s" was already declared, but it must be declared only once', | ||
| 286 | $queryName, | ||
| 287 | $entity, | ||
| 288 | )); | ||
| 289 | } | ||
| 290 | |||
| 291 | public static function duplicateResultSetMapping(string $entity, string $resultName): self | ||
| 292 | { | ||
| 293 | return new self(sprintf( | ||
| 294 | 'Result set mapping named "%s" in "%s" was already declared, but it must be declared only once', | ||
| 295 | $resultName, | ||
| 296 | $entity, | ||
| 297 | )); | ||
| 298 | } | ||
| 299 | |||
| 300 | public static function singleIdNotAllowedOnCompositePrimaryKey(string $entity): self | ||
| 301 | { | ||
| 302 | return new self('Single id is not allowed on composite primary key in entity ' . $entity); | ||
| 303 | } | ||
| 304 | |||
| 305 | public static function noIdDefined(string $entity): self | ||
| 306 | { | ||
| 307 | return new self('No ID defined for entity ' . $entity); | ||
| 308 | } | ||
| 309 | |||
| 310 | public static function unsupportedOptimisticLockingType(string $entity, string $fieldName, string $unsupportedType): self | ||
| 311 | { | ||
| 312 | return new self(sprintf( | ||
| 313 | 'Locking type "%s" (specified in "%s", field "%s") is not supported by Doctrine.', | ||
| 314 | $unsupportedType, | ||
| 315 | $entity, | ||
| 316 | $fieldName, | ||
| 317 | )); | ||
| 318 | } | ||
| 319 | |||
| 320 | public static function fileMappingDriversRequireConfiguredDirectoryPath(string|null $path = null): self | ||
| 321 | { | ||
| 322 | if (! empty($path)) { | ||
| 323 | $path = '[' . $path . ']'; | ||
| 324 | } | ||
| 325 | |||
| 326 | return new self( | ||
| 327 | 'File mapping drivers must have a valid directory path, ' . | ||
| 328 | 'however the given path ' . $path . ' seems to be incorrect!', | ||
| 329 | ); | ||
| 330 | } | ||
| 331 | |||
| 332 | /** | ||
| 333 | * Returns an exception that indicates that a class used in a discriminator map does not exist. | ||
| 334 | * An example would be an outdated (maybe renamed) classname. | ||
| 335 | * | ||
| 336 | * @param string $className The class that could not be found | ||
| 337 | * @param string $owningClass The class that declares the discriminator map. | ||
| 338 | */ | ||
| 339 | public static function invalidClassInDiscriminatorMap(string $className, string $owningClass): self | ||
| 340 | { | ||
| 341 | return new self(sprintf( | ||
| 342 | "Entity class '%s' used in the discriminator map of class '%s' " . | ||
| 343 | 'does not exist.', | ||
| 344 | $className, | ||
| 345 | $owningClass, | ||
| 346 | )); | ||
| 347 | } | ||
| 348 | |||
| 349 | /** | ||
| 350 | * @param string[] $entries | ||
| 351 | * @param array<string,string> $map | ||
| 352 | */ | ||
| 353 | public static function duplicateDiscriminatorEntry(string $className, array $entries, array $map): self | ||
| 354 | { | ||
| 355 | return new self( | ||
| 356 | 'The entries ' . implode(', ', $entries) . " in discriminator map of class '" . $className . "' is duplicated. " . | ||
| 357 | 'If the discriminator map is automatically generated you have to convert it to an explicit discriminator map now. ' . | ||
| 358 | 'The entries of the current map are: @DiscriminatorMap({' . implode(', ', array_map( | ||
| 359 | static fn ($a, $b) => sprintf("'%s': '%s'", $a, $b), | ||
| 360 | array_keys($map), | ||
| 361 | array_values($map), | ||
| 362 | )) . '})', | ||
| 363 | ); | ||
| 364 | } | ||
| 365 | |||
| 366 | /** | ||
| 367 | * @param class-string $rootEntityClass | ||
| 368 | * @param class-string $childEntityClass | ||
| 369 | */ | ||
| 370 | public static function missingInheritanceTypeDeclaration(string $rootEntityClass, string $childEntityClass): self | ||
| 371 | { | ||
| 372 | return new self(sprintf( | ||
| 373 | "Entity class '%s' is a subclass of the root entity class '%s', but no inheritance mapping type was declared.", | ||
| 374 | $childEntityClass, | ||
| 375 | $rootEntityClass, | ||
| 376 | )); | ||
| 377 | } | ||
| 378 | |||
| 379 | public static function missingDiscriminatorMap(string $className): self | ||
| 380 | { | ||
| 381 | return new self(sprintf( | ||
| 382 | "Entity class '%s' is using inheritance but no discriminator map was defined.", | ||
| 383 | $className, | ||
| 384 | )); | ||
| 385 | } | ||
| 386 | |||
| 387 | public static function missingDiscriminatorColumn(string $className): self | ||
| 388 | { | ||
| 389 | return new self(sprintf( | ||
| 390 | "Entity class '%s' is using inheritance but no discriminator column was defined.", | ||
| 391 | $className, | ||
| 392 | )); | ||
| 393 | } | ||
| 394 | |||
| 395 | public static function invalidDiscriminatorColumnType(string $className, string $type): self | ||
| 396 | { | ||
| 397 | return new self(sprintf( | ||
| 398 | "Discriminator column type on entity class '%s' is not allowed to be '%s'. 'string' or 'integer' type variables are suggested!", | ||
| 399 | $className, | ||
| 400 | $type, | ||
| 401 | )); | ||
| 402 | } | ||
| 403 | |||
| 404 | public static function nameIsMandatoryForDiscriminatorColumns(string $className): self | ||
| 405 | { | ||
| 406 | return new self(sprintf("Discriminator column name on entity class '%s' is not defined.", $className)); | ||
| 407 | } | ||
| 408 | |||
| 409 | public static function cannotVersionIdField(string $className, string $fieldName): self | ||
| 410 | { | ||
| 411 | return new self(sprintf( | ||
| 412 | "Setting Id field '%s' as versionable in entity class '%s' is not supported.", | ||
| 413 | $fieldName, | ||
| 414 | $className, | ||
| 415 | )); | ||
| 416 | } | ||
| 417 | |||
| 418 | public static function duplicateColumnName(string $className, string $columnName): self | ||
| 419 | { | ||
| 420 | return new self("Duplicate definition of column '" . $columnName . "' on entity '" . $className . "' in a field or discriminator column mapping."); | ||
| 421 | } | ||
| 422 | |||
| 423 | public static function illegalToManyAssociationOnMappedSuperclass(string $className, string $field): self | ||
| 424 | { | ||
| 425 | return new self("It is illegal to put an inverse side one-to-many or many-to-many association on mapped superclass '" . $className . '#' . $field . "'."); | ||
| 426 | } | ||
| 427 | |||
| 428 | public static function cannotMapCompositePrimaryKeyEntitiesAsForeignId(string $className, string $targetEntity, string $targetField): self | ||
| 429 | { | ||
| 430 | return new self("It is not possible to map entity '" . $className . "' with a composite primary key " . | ||
| 431 | "as part of the primary key of another entity '" . $targetEntity . '#' . $targetField . "'."); | ||
| 432 | } | ||
| 433 | |||
| 434 | public static function noSingleAssociationJoinColumnFound(string $className, string $field): self | ||
| 435 | { | ||
| 436 | return new self(sprintf("'%s#%s' is not an association with a single join column.", $className, $field)); | ||
| 437 | } | ||
| 438 | |||
| 439 | public static function noFieldNameFoundForColumn(string $className, string $column): self | ||
| 440 | { | ||
| 441 | return new self(sprintf( | ||
| 442 | "Cannot find a field on '%s' that is mapped to column '%s'. Either the " . | ||
| 443 | 'field does not exist or an association exists but it has multiple join columns.', | ||
| 444 | $className, | ||
| 445 | $column, | ||
| 446 | )); | ||
| 447 | } | ||
| 448 | |||
| 449 | public static function illegalOrphanRemovalOnIdentifierAssociation(string $className, string $field): self | ||
| 450 | { | ||
| 451 | return new self(sprintf( | ||
| 452 | "The orphan removal option is not allowed on an association that is part of the identifier in '%s#%s'.", | ||
| 453 | $className, | ||
| 454 | $field, | ||
| 455 | )); | ||
| 456 | } | ||
| 457 | |||
| 458 | public static function illegalOrphanRemoval(string $className, string $field): self | ||
| 459 | { | ||
| 460 | return new self('Orphan removal is only allowed on one-to-one and one-to-many ' . | ||
| 461 | 'associations, but ' . $className . '#' . $field . ' is not.'); | ||
| 462 | } | ||
| 463 | |||
| 464 | public static function illegalInverseIdentifierAssociation(string $className, string $field): self | ||
| 465 | { | ||
| 466 | return new self(sprintf( | ||
| 467 | "An inverse association is not allowed to be identifier in '%s#%s'.", | ||
| 468 | $className, | ||
| 469 | $field, | ||
| 470 | )); | ||
| 471 | } | ||
| 472 | |||
| 473 | public static function illegalToManyIdentifierAssociation(string $className, string $field): self | ||
| 474 | { | ||
| 475 | return new self(sprintf( | ||
| 476 | "Many-to-many or one-to-many associations are not allowed to be identifier in '%s#%s'.", | ||
| 477 | $className, | ||
| 478 | $field, | ||
| 479 | )); | ||
| 480 | } | ||
| 481 | |||
| 482 | public static function noInheritanceOnMappedSuperClass(string $className): self | ||
| 483 | { | ||
| 484 | return new self("It is not supported to define inheritance information on a mapped superclass '" . $className . "'."); | ||
| 485 | } | ||
| 486 | |||
| 487 | public static function mappedClassNotPartOfDiscriminatorMap(string $className, string $rootClassName): self | ||
| 488 | { | ||
| 489 | return new self( | ||
| 490 | "Entity '" . $className . "' has to be part of the discriminator map of '" . $rootClassName . "' " . | ||
| 491 | "to be properly mapped in the inheritance hierarchy. Alternatively you can make '" . $className . "' an abstract class " . | ||
| 492 | 'to avoid this exception from occurring.', | ||
| 493 | ); | ||
| 494 | } | ||
| 495 | |||
| 496 | public static function lifecycleCallbackMethodNotFound(string $className, string $methodName): self | ||
| 497 | { | ||
| 498 | return new self("Entity '" . $className . "' has no method '" . $methodName . "' to be registered as lifecycle callback."); | ||
| 499 | } | ||
| 500 | |||
| 501 | /** @param class-string $className */ | ||
| 502 | public static function illegalLifecycleCallbackOnEmbeddedClass(string $event, string $className): self | ||
| 503 | { | ||
| 504 | return new self(sprintf( | ||
| 505 | <<<'EXCEPTION' | ||
| 506 | Context: Attempt to register lifecycle callback "%s" on embedded class "%s". | ||
| 507 | Problem: Registering lifecycle callbacks on embedded classes is not allowed. | ||
| 508 | EXCEPTION, | ||
| 509 | $event, | ||
| 510 | $className, | ||
| 511 | )); | ||
| 512 | } | ||
| 513 | |||
| 514 | public static function entityListenerClassNotFound(string $listenerName, string $className): self | ||
| 515 | { | ||
| 516 | return new self(sprintf('Entity Listener "%s" declared on "%s" not found.', $listenerName, $className)); | ||
| 517 | } | ||
| 518 | |||
| 519 | public static function entityListenerMethodNotFound(string $listenerName, string $methodName, string $className): self | ||
| 520 | { | ||
| 521 | return new self(sprintf('Entity Listener "%s" declared on "%s" has no method "%s".', $listenerName, $className, $methodName)); | ||
| 522 | } | ||
| 523 | |||
| 524 | public static function duplicateEntityListener(string $listenerName, string $methodName, string $className): self | ||
| 525 | { | ||
| 526 | return new self(sprintf('Entity Listener "%s#%s()" in "%s" was already declared, but it must be declared only once.', $listenerName, $methodName, $className)); | ||
| 527 | } | ||
| 528 | |||
| 529 | /** @param class-string $className */ | ||
| 530 | public static function invalidFetchMode(string $className, string $fetchMode): self | ||
| 531 | { | ||
| 532 | return new self("Entity '" . $className . "' has a mapping with invalid fetch mode '" . $fetchMode . "'"); | ||
| 533 | } | ||
| 534 | |||
| 535 | public static function invalidGeneratedMode(int|string $generatedMode): self | ||
| 536 | { | ||
| 537 | return new self("Invalid generated mode '" . $generatedMode . "'"); | ||
| 538 | } | ||
| 539 | |||
| 540 | public static function compositeKeyAssignedIdGeneratorRequired(string $className): self | ||
| 541 | { | ||
| 542 | return new self("Entity '" . $className . "' has a composite identifier but uses an ID generator other than manually assigning (Identity, Sequence). This is not supported."); | ||
| 543 | } | ||
| 544 | |||
| 545 | public static function invalidTargetEntityClass(string $targetEntity, string $sourceEntity, string $associationName): self | ||
| 546 | { | ||
| 547 | return new self('The target-entity ' . $targetEntity . " cannot be found in '" . $sourceEntity . '#' . $associationName . "'."); | ||
| 548 | } | ||
| 549 | |||
| 550 | /** @param string[] $cascades */ | ||
| 551 | public static function invalidCascadeOption(array $cascades, string $className, string $propertyName): self | ||
| 552 | { | ||
| 553 | $cascades = implode(', ', array_map(static fn (string $e): string => "'" . $e . "'", $cascades)); | ||
| 554 | |||
| 555 | return new self(sprintf( | ||
| 556 | "You have specified invalid cascade options for %s::$%s: %s; available options: 'remove', 'persist', 'refresh', and 'detach'", | ||
| 557 | $className, | ||
| 558 | $propertyName, | ||
| 559 | $cascades, | ||
| 560 | )); | ||
| 561 | } | ||
| 562 | |||
| 563 | public static function missingSequenceName(string $className): self | ||
| 564 | { | ||
| 565 | return new self( | ||
| 566 | sprintf('Missing "sequenceName" attribute for sequence id generator definition on class "%s".', $className), | ||
| 567 | ); | ||
| 568 | } | ||
| 569 | |||
| 570 | public static function infiniteEmbeddableNesting(string $className, string $propertyName): self | ||
| 571 | { | ||
| 572 | return new self( | ||
| 573 | sprintf( | ||
| 574 | 'Infinite nesting detected for embedded property %s::%s. ' . | ||
| 575 | 'You cannot embed an embeddable from the same type inside an embeddable.', | ||
| 576 | $className, | ||
| 577 | $propertyName, | ||
| 578 | ), | ||
| 579 | ); | ||
| 580 | } | ||
| 581 | |||
| 582 | public static function illegalOverrideOfInheritedProperty(string $className, string $propertyName, string $inheritFromClass): self | ||
| 583 | { | ||
| 584 | return new self( | ||
| 585 | sprintf( | ||
| 586 | 'Overrides are only allowed for fields or associations declared in mapped superclasses or traits. This is not the case for %s::%s, which was inherited from %s.', | ||
| 587 | $className, | ||
| 588 | $propertyName, | ||
| 589 | $inheritFromClass, | ||
| 590 | ), | ||
| 591 | ); | ||
| 592 | } | ||
| 593 | |||
| 594 | public static function invalidIndexConfiguration(string $className, string $indexName): self | ||
| 595 | { | ||
| 596 | return new self( | ||
| 597 | sprintf( | ||
| 598 | 'Index %s for entity %s should contain columns or fields values, but not both.', | ||
| 599 | $indexName, | ||
| 600 | $className, | ||
| 601 | ), | ||
| 602 | ); | ||
| 603 | } | ||
| 604 | |||
| 605 | public static function invalidUniqueConstraintConfiguration(string $className, string $indexName): self | ||
| 606 | { | ||
| 607 | return new self( | ||
| 608 | sprintf( | ||
| 609 | 'Unique constraint %s for entity %s should contain columns or fields values, but not both.', | ||
| 610 | $indexName, | ||
| 611 | $className, | ||
| 612 | ), | ||
| 613 | ); | ||
| 614 | } | ||
| 615 | |||
| 616 | public static function invalidOverrideType(string $expectdType, mixed $givenValue): self | ||
| 617 | { | ||
| 618 | return new self(sprintf( | ||
| 619 | 'Expected %s, but %s was given.', | ||
| 620 | $expectdType, | ||
| 621 | get_debug_type($givenValue), | ||
| 622 | )); | ||
| 623 | } | ||
| 624 | |||
| 625 | public static function backedEnumTypeRequired(string $className, string $fieldName, string $enumType): self | ||
| 626 | { | ||
| 627 | return new self(sprintf( | ||
| 628 | 'Attempting to map a non-backed enum type %s in entity %s::$%s. Please use backed enums only', | ||
| 629 | $enumType, | ||
| 630 | $className, | ||
| 631 | $fieldName, | ||
| 632 | )); | ||
| 633 | } | ||
| 634 | |||
| 635 | public static function nonEnumTypeMapped(string $className, string $fieldName, string $enumType): self | ||
| 636 | { | ||
| 637 | return new self(sprintf( | ||
| 638 | 'Attempting to map non-enum type %s as enum in entity %s::$%s', | ||
| 639 | $enumType, | ||
| 640 | $className, | ||
| 641 | $fieldName, | ||
| 642 | )); | ||
| 643 | } | ||
| 644 | |||
| 645 | /** | ||
| 646 | * @param class-string $className | ||
| 647 | * @param class-string<BackedEnum> $enumType | ||
| 648 | */ | ||
| 649 | public static function invalidEnumValue( | ||
| 650 | string $className, | ||
| 651 | string $fieldName, | ||
| 652 | string $value, | ||
| 653 | string $enumType, | ||
| 654 | ValueError $previous, | ||
| 655 | ): self { | ||
| 656 | return new self(sprintf( | ||
| 657 | <<<'EXCEPTION' | ||
| 658 | Context: Trying to hydrate enum property "%s::$%s" | ||
| 659 | Problem: Case "%s" is not listed in enum "%s" | ||
| 660 | Solution: Either add the case to the enum type or migrate the database column to use another case of the enum | ||
| 661 | EXCEPTION | ||
| 662 | , | ||
| 663 | $className, | ||
| 664 | $fieldName, | ||
| 665 | $value, | ||
| 666 | $enumType, | ||
| 667 | ), 0, $previous); | ||
| 668 | } | ||
| 669 | |||
| 670 | /** @param LibXMLError[] $errors */ | ||
| 671 | public static function fromLibXmlErrors(array $errors): self | ||
| 672 | { | ||
| 673 | $formatter = static fn (LibXMLError $error): string => sprintf( | ||
| 674 | 'libxml error: %s in %s at line %d', | ||
| 675 | $error->message, | ||
| 676 | $error->file, | ||
| 677 | $error->line, | ||
| 678 | ); | ||
| 679 | |||
| 680 | return new self(implode(PHP_EOL, array_map($formatter, $errors))); | ||
| 681 | } | ||
| 682 | |||
| 683 | public static function invalidAttributeOnEmbeddable(string $entityName, string $attributeName): self | ||
| 684 | { | ||
| 685 | return new self(sprintf( | ||
| 686 | 'Attribute "%s" on embeddable "%s" is not allowed.', | ||
| 687 | $attributeName, | ||
| 688 | $entityName, | ||
| 689 | )); | ||
| 690 | } | ||
| 691 | } | ||
diff --git a/vendor/doctrine/orm/src/Mapping/NamingStrategy.php b/vendor/doctrine/orm/src/Mapping/NamingStrategy.php new file mode 100644 index 0000000..afedebe --- /dev/null +++ b/vendor/doctrine/orm/src/Mapping/NamingStrategy.php | |||
| @@ -0,0 +1,71 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Mapping; | ||
| 6 | |||
| 7 | /** | ||
| 8 | * A set of rules for determining the physical column and table names | ||
| 9 | * | ||
| 10 | * @link www.doctrine-project.org | ||
| 11 | */ | ||
| 12 | interface NamingStrategy | ||
| 13 | { | ||
| 14 | /** | ||
| 15 | * Returns a table name for an entity class. | ||
| 16 | * | ||
| 17 | * @param class-string $className | ||
| 18 | */ | ||
| 19 | public function classToTableName(string $className): string; | ||
| 20 | |||
| 21 | /** | ||
| 22 | * Returns a column name for a property. | ||
| 23 | * | ||
| 24 | * @param class-string $className | ||
| 25 | */ | ||
| 26 | public function propertyToColumnName(string $propertyName, string $className): string; | ||
| 27 | |||
| 28 | /** | ||
| 29 | * Returns a column name for an embedded property. | ||
| 30 | * | ||
| 31 | * @param class-string $className | ||
| 32 | * @param class-string $embeddedClassName | ||
| 33 | */ | ||
| 34 | public function embeddedFieldToColumnName( | ||
| 35 | string $propertyName, | ||
| 36 | string $embeddedColumnName, | ||
| 37 | string $className, | ||
| 38 | string $embeddedClassName, | ||
| 39 | ): string; | ||
| 40 | |||
| 41 | /** | ||
| 42 | * Returns the default reference column name. | ||
| 43 | */ | ||
| 44 | public function referenceColumnName(): string; | ||
| 45 | |||
| 46 | /** | ||
| 47 | * Returns a join column name for a property. | ||
| 48 | * | ||
| 49 | * @param class-string $className | ||
| 50 | */ | ||
| 51 | public function joinColumnName(string $propertyName, string $className): string; | ||
| 52 | |||
| 53 | /** | ||
| 54 | * Returns a join table name. | ||
| 55 | * | ||
| 56 | * @param class-string $sourceEntity | ||
| 57 | * @param class-string $targetEntity | ||
| 58 | */ | ||
| 59 | public function joinTableName(string $sourceEntity, string $targetEntity, string $propertyName): string; | ||
| 60 | |||
| 61 | /** | ||
| 62 | * Returns the foreign key column name for the given parameters. | ||
| 63 | * | ||
| 64 | * @param class-string $entityName An entity. | ||
| 65 | * @param string|null $referencedColumnName A property name or null in | ||
| 66 | * case of a self-referencing | ||
| 67 | * entity with join columns | ||
| 68 | * defined in the mapping | ||
| 69 | */ | ||
| 70 | public function joinKeyColumnName(string $entityName, string|null $referencedColumnName): string; | ||
| 71 | } | ||
diff --git a/vendor/doctrine/orm/src/Mapping/OneToMany.php b/vendor/doctrine/orm/src/Mapping/OneToMany.php new file mode 100644 index 0000000..d71c4f9 --- /dev/null +++ b/vendor/doctrine/orm/src/Mapping/OneToMany.php | |||
| @@ -0,0 +1,26 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Mapping; | ||
| 6 | |||
| 7 | use Attribute; | ||
| 8 | |||
| 9 | #[Attribute(Attribute::TARGET_PROPERTY)] | ||
| 10 | final class OneToMany implements MappingAttribute | ||
| 11 | { | ||
| 12 | /** | ||
| 13 | * @param class-string|null $targetEntity | ||
| 14 | * @param string[]|null $cascade | ||
| 15 | * @psalm-param 'LAZY'|'EAGER'|'EXTRA_LAZY' $fetch | ||
| 16 | */ | ||
| 17 | public function __construct( | ||
| 18 | public readonly string|null $targetEntity = null, | ||
| 19 | public readonly string|null $mappedBy = null, | ||
| 20 | public readonly array|null $cascade = null, | ||
| 21 | public readonly string $fetch = 'LAZY', | ||
| 22 | public readonly bool $orphanRemoval = false, | ||
| 23 | public readonly string|null $indexBy = null, | ||
| 24 | ) { | ||
| 25 | } | ||
| 26 | } | ||
diff --git a/vendor/doctrine/orm/src/Mapping/OneToManyAssociationMapping.php b/vendor/doctrine/orm/src/Mapping/OneToManyAssociationMapping.php new file mode 100644 index 0000000..786e981 --- /dev/null +++ b/vendor/doctrine/orm/src/Mapping/OneToManyAssociationMapping.php | |||
| @@ -0,0 +1,75 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Mapping; | ||
| 6 | |||
| 7 | final class OneToManyAssociationMapping extends ToManyInverseSideMapping | ||
| 8 | { | ||
| 9 | /** | ||
| 10 | * @param mixed[] $mappingArray | ||
| 11 | * @psalm-param array{ | ||
| 12 | * fieldName: string, | ||
| 13 | * sourceEntity: class-string, | ||
| 14 | * targetEntity: class-string, | ||
| 15 | * cascade?: list<'persist'|'remove'|'detach'|'refresh'|'all'>, | ||
| 16 | * fetch?: ClassMetadata::FETCH_*|null, | ||
| 17 | * inherited?: class-string|null, | ||
| 18 | * declared?: class-string|null, | ||
| 19 | * cache?: array<mixed>|null, | ||
| 20 | * id?: bool|null, | ||
| 21 | * isOnDeleteCascade?: bool|null, | ||
| 22 | * originalClass?: class-string|null, | ||
| 23 | * originalField?: string|null, | ||
| 24 | * orphanRemoval?: bool, | ||
| 25 | * unique?: bool|null, | ||
| 26 | * joinTable?: mixed[]|null, | ||
| 27 | * type?: int, | ||
| 28 | * isOwningSide: bool, | ||
| 29 | * } $mappingArray | ||
| 30 | */ | ||
| 31 | public static function fromMappingArray(array $mappingArray): static | ||
| 32 | { | ||
| 33 | $mapping = parent::fromMappingArray($mappingArray); | ||
| 34 | |||
| 35 | if ($mapping->orphanRemoval && ! $mapping->isCascadeRemove()) { | ||
| 36 | $mapping->cascade[] = 'remove'; | ||
| 37 | } | ||
| 38 | |||
| 39 | return $mapping; | ||
| 40 | } | ||
| 41 | |||
| 42 | /** | ||
| 43 | * @param mixed[] $mappingArray | ||
| 44 | * @psalm-param array{ | ||
| 45 | * fieldName: string, | ||
| 46 | * sourceEntity: class-string, | ||
| 47 | * targetEntity: class-string, | ||
| 48 | * cascade?: list<'persist'|'remove'|'detach'|'refresh'|'all'>, | ||
| 49 | * fetch?: ClassMetadata::FETCH_*|null, | ||
| 50 | * inherited?: class-string|null, | ||
| 51 | * declared?: class-string|null, | ||
| 52 | * cache?: array<mixed>|null, | ||
| 53 | * id?: bool|null, | ||
| 54 | * isOnDeleteCascade?: bool|null, | ||
| 55 | * originalClass?: class-string|null, | ||
| 56 | * originalField?: string|null, | ||
| 57 | * orphanRemoval?: bool, | ||
| 58 | * unique?: bool|null, | ||
| 59 | * joinTable?: mixed[]|null, | ||
| 60 | * type?: int, | ||
| 61 | * isOwningSide: bool, | ||
| 62 | * } $mappingArray | ||
| 63 | */ | ||
| 64 | public static function fromMappingArrayAndName(array $mappingArray, string $name): static | ||
| 65 | { | ||
| 66 | $mapping = self::fromMappingArray($mappingArray); | ||
| 67 | |||
| 68 | // OneToMany-side MUST be inverse (must have mappedBy) | ||
| 69 | if (! isset($mapping->mappedBy)) { | ||
| 70 | throw MappingException::oneToManyRequiresMappedBy($name, $mapping->fieldName); | ||
| 71 | } | ||
| 72 | |||
| 73 | return $mapping; | ||
| 74 | } | ||
| 75 | } | ||
diff --git a/vendor/doctrine/orm/src/Mapping/OneToOne.php b/vendor/doctrine/orm/src/Mapping/OneToOne.php new file mode 100644 index 0000000..1ddf21c --- /dev/null +++ b/vendor/doctrine/orm/src/Mapping/OneToOne.php | |||
| @@ -0,0 +1,26 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Mapping; | ||
| 6 | |||
| 7 | use Attribute; | ||
| 8 | |||
| 9 | #[Attribute(Attribute::TARGET_PROPERTY)] | ||
| 10 | final class OneToOne implements MappingAttribute | ||
| 11 | { | ||
| 12 | /** | ||
| 13 | * @param class-string|null $targetEntity | ||
| 14 | * @param array<string>|null $cascade | ||
| 15 | * @psalm-param 'LAZY'|'EAGER'|'EXTRA_LAZY' $fetch | ||
| 16 | */ | ||
| 17 | public function __construct( | ||
| 18 | public readonly string|null $targetEntity = null, | ||
| 19 | public readonly string|null $mappedBy = null, | ||
| 20 | public readonly string|null $inversedBy = null, | ||
| 21 | public readonly array|null $cascade = null, | ||
| 22 | public readonly string $fetch = 'LAZY', | ||
| 23 | public readonly bool $orphanRemoval = false, | ||
| 24 | ) { | ||
| 25 | } | ||
| 26 | } | ||
diff --git a/vendor/doctrine/orm/src/Mapping/OneToOneAssociationMapping.php b/vendor/doctrine/orm/src/Mapping/OneToOneAssociationMapping.php new file mode 100644 index 0000000..89c6483 --- /dev/null +++ b/vendor/doctrine/orm/src/Mapping/OneToOneAssociationMapping.php | |||
| @@ -0,0 +1,9 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Mapping; | ||
| 6 | |||
| 7 | interface OneToOneAssociationMapping extends ToOneAssociationMapping | ||
| 8 | { | ||
| 9 | } | ||
diff --git a/vendor/doctrine/orm/src/Mapping/OneToOneInverseSideMapping.php b/vendor/doctrine/orm/src/Mapping/OneToOneInverseSideMapping.php new file mode 100644 index 0000000..85e0f30 --- /dev/null +++ b/vendor/doctrine/orm/src/Mapping/OneToOneInverseSideMapping.php | |||
| @@ -0,0 +1,9 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Mapping; | ||
| 6 | |||
| 7 | final class OneToOneInverseSideMapping extends ToOneInverseSideMapping implements OneToOneAssociationMapping | ||
| 8 | { | ||
| 9 | } | ||
diff --git a/vendor/doctrine/orm/src/Mapping/OneToOneOwningSideMapping.php b/vendor/doctrine/orm/src/Mapping/OneToOneOwningSideMapping.php new file mode 100644 index 0000000..4ad181b --- /dev/null +++ b/vendor/doctrine/orm/src/Mapping/OneToOneOwningSideMapping.php | |||
| @@ -0,0 +1,9 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Mapping; | ||
| 6 | |||
| 7 | final class OneToOneOwningSideMapping extends ToOneOwningSideMapping implements OneToOneAssociationMapping | ||
| 8 | { | ||
| 9 | } | ||
diff --git a/vendor/doctrine/orm/src/Mapping/OrderBy.php b/vendor/doctrine/orm/src/Mapping/OrderBy.php new file mode 100644 index 0000000..5cb2ed9 --- /dev/null +++ b/vendor/doctrine/orm/src/Mapping/OrderBy.php | |||
| @@ -0,0 +1,17 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Mapping; | ||
| 6 | |||
| 7 | use Attribute; | ||
| 8 | |||
| 9 | #[Attribute(Attribute::TARGET_PROPERTY)] | ||
| 10 | final class OrderBy implements MappingAttribute | ||
| 11 | { | ||
| 12 | /** @param array<string> $value */ | ||
| 13 | public function __construct( | ||
| 14 | public readonly array $value, | ||
| 15 | ) { | ||
| 16 | } | ||
| 17 | } | ||
diff --git a/vendor/doctrine/orm/src/Mapping/OwningSideMapping.php b/vendor/doctrine/orm/src/Mapping/OwningSideMapping.php new file mode 100644 index 0000000..ab8b7b2 --- /dev/null +++ b/vendor/doctrine/orm/src/Mapping/OwningSideMapping.php | |||
| @@ -0,0 +1,28 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Mapping; | ||
| 6 | |||
| 7 | abstract class OwningSideMapping extends AssociationMapping | ||
| 8 | { | ||
| 9 | /** | ||
| 10 | * required for bidirectional associations | ||
| 11 | * The name of the field that completes the bidirectional association on | ||
| 12 | * the inverse side. This key must be specified on the owning side of a | ||
| 13 | * bidirectional association. | ||
| 14 | */ | ||
| 15 | public string|null $inversedBy = null; | ||
| 16 | |||
| 17 | /** @return list<string> */ | ||
| 18 | public function __sleep(): array | ||
| 19 | { | ||
| 20 | $serialized = parent::__sleep(); | ||
| 21 | |||
| 22 | if ($this->inversedBy !== null) { | ||
| 23 | $serialized[] = 'inversedBy'; | ||
| 24 | } | ||
| 25 | |||
| 26 | return $serialized; | ||
| 27 | } | ||
| 28 | } | ||
diff --git a/vendor/doctrine/orm/src/Mapping/PostLoad.php b/vendor/doctrine/orm/src/Mapping/PostLoad.php new file mode 100644 index 0000000..9ce1e5c --- /dev/null +++ b/vendor/doctrine/orm/src/Mapping/PostLoad.php | |||
| @@ -0,0 +1,12 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Mapping; | ||
| 6 | |||
| 7 | use Attribute; | ||
| 8 | |||
| 9 | #[Attribute(Attribute::TARGET_METHOD)] | ||
| 10 | final class PostLoad implements MappingAttribute | ||
| 11 | { | ||
| 12 | } | ||
diff --git a/vendor/doctrine/orm/src/Mapping/PostPersist.php b/vendor/doctrine/orm/src/Mapping/PostPersist.php new file mode 100644 index 0000000..f7bf2e1 --- /dev/null +++ b/vendor/doctrine/orm/src/Mapping/PostPersist.php | |||
| @@ -0,0 +1,12 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Mapping; | ||
| 6 | |||
| 7 | use Attribute; | ||
| 8 | |||
| 9 | #[Attribute(Attribute::TARGET_METHOD)] | ||
| 10 | final class PostPersist implements MappingAttribute | ||
| 11 | { | ||
| 12 | } | ||
diff --git a/vendor/doctrine/orm/src/Mapping/PostRemove.php b/vendor/doctrine/orm/src/Mapping/PostRemove.php new file mode 100644 index 0000000..394c175 --- /dev/null +++ b/vendor/doctrine/orm/src/Mapping/PostRemove.php | |||
| @@ -0,0 +1,12 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Mapping; | ||
| 6 | |||
| 7 | use Attribute; | ||
| 8 | |||
| 9 | #[Attribute(Attribute::TARGET_METHOD)] | ||
| 10 | final class PostRemove implements MappingAttribute | ||
| 11 | { | ||
| 12 | } | ||
diff --git a/vendor/doctrine/orm/src/Mapping/PostUpdate.php b/vendor/doctrine/orm/src/Mapping/PostUpdate.php new file mode 100644 index 0000000..7b95675 --- /dev/null +++ b/vendor/doctrine/orm/src/Mapping/PostUpdate.php | |||
| @@ -0,0 +1,12 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Mapping; | ||
| 6 | |||
| 7 | use Attribute; | ||
| 8 | |||
| 9 | #[Attribute(Attribute::TARGET_METHOD)] | ||
| 10 | final class PostUpdate implements MappingAttribute | ||
| 11 | { | ||
| 12 | } | ||
diff --git a/vendor/doctrine/orm/src/Mapping/PreFlush.php b/vendor/doctrine/orm/src/Mapping/PreFlush.php new file mode 100644 index 0000000..f2c09d7 --- /dev/null +++ b/vendor/doctrine/orm/src/Mapping/PreFlush.php | |||
| @@ -0,0 +1,12 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Mapping; | ||
| 6 | |||
| 7 | use Attribute; | ||
| 8 | |||
| 9 | #[Attribute(Attribute::TARGET_METHOD)] | ||
| 10 | final class PreFlush implements MappingAttribute | ||
| 11 | { | ||
| 12 | } | ||
diff --git a/vendor/doctrine/orm/src/Mapping/PrePersist.php b/vendor/doctrine/orm/src/Mapping/PrePersist.php new file mode 100644 index 0000000..1b88a7a --- /dev/null +++ b/vendor/doctrine/orm/src/Mapping/PrePersist.php | |||
| @@ -0,0 +1,12 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Mapping; | ||
| 6 | |||
| 7 | use Attribute; | ||
| 8 | |||
| 9 | #[Attribute(Attribute::TARGET_METHOD)] | ||
| 10 | final class PrePersist implements MappingAttribute | ||
| 11 | { | ||
| 12 | } | ||
diff --git a/vendor/doctrine/orm/src/Mapping/PreRemove.php b/vendor/doctrine/orm/src/Mapping/PreRemove.php new file mode 100644 index 0000000..f63d4e0 --- /dev/null +++ b/vendor/doctrine/orm/src/Mapping/PreRemove.php | |||
| @@ -0,0 +1,12 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Mapping; | ||
| 6 | |||
| 7 | use Attribute; | ||
| 8 | |||
| 9 | #[Attribute(Attribute::TARGET_METHOD)] | ||
| 10 | final class PreRemove implements MappingAttribute | ||
| 11 | { | ||
| 12 | } | ||
diff --git a/vendor/doctrine/orm/src/Mapping/PreUpdate.php b/vendor/doctrine/orm/src/Mapping/PreUpdate.php new file mode 100644 index 0000000..9b73bfd --- /dev/null +++ b/vendor/doctrine/orm/src/Mapping/PreUpdate.php | |||
| @@ -0,0 +1,12 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Mapping; | ||
| 6 | |||
| 7 | use Attribute; | ||
| 8 | |||
| 9 | #[Attribute(Attribute::TARGET_METHOD)] | ||
| 10 | final class PreUpdate implements MappingAttribute | ||
| 11 | { | ||
| 12 | } | ||
diff --git a/vendor/doctrine/orm/src/Mapping/QuoteStrategy.php b/vendor/doctrine/orm/src/Mapping/QuoteStrategy.php new file mode 100644 index 0000000..9eb3e53 --- /dev/null +++ b/vendor/doctrine/orm/src/Mapping/QuoteStrategy.php | |||
| @@ -0,0 +1,68 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Mapping; | ||
| 6 | |||
| 7 | use Doctrine\DBAL\Platforms\AbstractPlatform; | ||
| 8 | |||
| 9 | /** | ||
| 10 | * A set of rules for determining the column, alias and table quotes. | ||
| 11 | */ | ||
| 12 | interface QuoteStrategy | ||
| 13 | { | ||
| 14 | /** | ||
| 15 | * Gets the (possibly quoted) column name for safe use in an SQL statement. | ||
| 16 | */ | ||
| 17 | public function getColumnName(string $fieldName, ClassMetadata $class, AbstractPlatform $platform): string; | ||
| 18 | |||
| 19 | /** | ||
| 20 | * Gets the (possibly quoted) primary table name for safe use in an SQL statement. | ||
| 21 | */ | ||
| 22 | public function getTableName(ClassMetadata $class, AbstractPlatform $platform): string; | ||
| 23 | |||
| 24 | /** | ||
| 25 | * Gets the (possibly quoted) sequence name for safe use in an SQL statement. | ||
| 26 | * | ||
| 27 | * @param mixed[] $definition | ||
| 28 | */ | ||
| 29 | public function getSequenceName(array $definition, ClassMetadata $class, AbstractPlatform $platform): string; | ||
| 30 | |||
| 31 | /** Gets the (possibly quoted) name of the join table. */ | ||
| 32 | public function getJoinTableName( | ||
| 33 | ManyToManyOwningSideMapping $association, | ||
| 34 | ClassMetadata $class, | ||
| 35 | AbstractPlatform $platform, | ||
| 36 | ): string; | ||
| 37 | |||
| 38 | /** | ||
| 39 | * Gets the (possibly quoted) join column name. | ||
| 40 | */ | ||
| 41 | public function getJoinColumnName(JoinColumnMapping $joinColumn, ClassMetadata $class, AbstractPlatform $platform): string; | ||
| 42 | |||
| 43 | /** | ||
| 44 | * Gets the (possibly quoted) join column name. | ||
| 45 | */ | ||
| 46 | public function getReferencedJoinColumnName( | ||
| 47 | JoinColumnMapping $joinColumn, | ||
| 48 | ClassMetadata $class, | ||
| 49 | AbstractPlatform $platform, | ||
| 50 | ): string; | ||
| 51 | |||
| 52 | /** | ||
| 53 | * Gets the (possibly quoted) identifier column names for safe use in an SQL statement. | ||
| 54 | * | ||
| 55 | * @psalm-return list<string> | ||
| 56 | */ | ||
| 57 | public function getIdentifierColumnNames(ClassMetadata $class, AbstractPlatform $platform): array; | ||
| 58 | |||
| 59 | /** | ||
| 60 | * Gets the column alias. | ||
| 61 | */ | ||
| 62 | public function getColumnAlias( | ||
| 63 | string $columnName, | ||
| 64 | int $counter, | ||
| 65 | AbstractPlatform $platform, | ||
| 66 | ClassMetadata|null $class = null, | ||
| 67 | ): string; | ||
| 68 | } | ||
diff --git a/vendor/doctrine/orm/src/Mapping/ReflectionEmbeddedProperty.php b/vendor/doctrine/orm/src/Mapping/ReflectionEmbeddedProperty.php new file mode 100644 index 0000000..da3d097 --- /dev/null +++ b/vendor/doctrine/orm/src/Mapping/ReflectionEmbeddedProperty.php | |||
| @@ -0,0 +1,61 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Mapping; | ||
| 6 | |||
| 7 | use Doctrine\Instantiator\Instantiator; | ||
| 8 | use ReflectionProperty; | ||
| 9 | |||
| 10 | /** | ||
| 11 | * Acts as a proxy to a nested Property structure, making it look like | ||
| 12 | * just a single scalar property. | ||
| 13 | * | ||
| 14 | * This way value objects "just work" without UnitOfWork, Persisters or Hydrators | ||
| 15 | * needing any changes. | ||
| 16 | * | ||
| 17 | * TODO: Move this class into Common\Reflection | ||
| 18 | */ | ||
| 19 | final class ReflectionEmbeddedProperty extends ReflectionProperty | ||
| 20 | { | ||
| 21 | private Instantiator|null $instantiator = null; | ||
| 22 | |||
| 23 | /** | ||
| 24 | * @param ReflectionProperty $parentProperty reflection property of the class where the embedded object has to be put | ||
| 25 | * @param ReflectionProperty $childProperty reflection property of the embedded object | ||
| 26 | * @psalm-param class-string $embeddedClass | ||
| 27 | */ | ||
| 28 | public function __construct( | ||
| 29 | private readonly ReflectionProperty $parentProperty, | ||
| 30 | private readonly ReflectionProperty $childProperty, | ||
| 31 | private readonly string $embeddedClass, | ||
| 32 | ) { | ||
| 33 | parent::__construct($childProperty->getDeclaringClass()->name, $childProperty->getName()); | ||
| 34 | } | ||
| 35 | |||
| 36 | public function getValue(object|null $object = null): mixed | ||
| 37 | { | ||
| 38 | $embeddedObject = $this->parentProperty->getValue($object); | ||
| 39 | |||
| 40 | if ($embeddedObject === null) { | ||
| 41 | return null; | ||
| 42 | } | ||
| 43 | |||
| 44 | return $this->childProperty->getValue($embeddedObject); | ||
| 45 | } | ||
| 46 | |||
| 47 | public function setValue(mixed $object, mixed $value = null): void | ||
| 48 | { | ||
| 49 | $embeddedObject = $this->parentProperty->getValue($object); | ||
| 50 | |||
| 51 | if ($embeddedObject === null) { | ||
| 52 | $this->instantiator ??= new Instantiator(); | ||
| 53 | |||
| 54 | $embeddedObject = $this->instantiator->instantiate($this->embeddedClass); | ||
| 55 | |||
| 56 | $this->parentProperty->setValue($object, $embeddedObject); | ||
| 57 | } | ||
| 58 | |||
| 59 | $this->childProperty->setValue($embeddedObject, $value); | ||
| 60 | } | ||
| 61 | } | ||
diff --git a/vendor/doctrine/orm/src/Mapping/ReflectionEnumProperty.php b/vendor/doctrine/orm/src/Mapping/ReflectionEnumProperty.php new file mode 100644 index 0000000..0ebd978 --- /dev/null +++ b/vendor/doctrine/orm/src/Mapping/ReflectionEnumProperty.php | |||
| @@ -0,0 +1,87 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Mapping; | ||
| 6 | |||
| 7 | use BackedEnum; | ||
| 8 | use ReflectionProperty; | ||
| 9 | use ValueError; | ||
| 10 | |||
| 11 | use function array_map; | ||
| 12 | use function is_array; | ||
| 13 | |||
| 14 | /** @deprecated use Doctrine\Persistence\Reflection\EnumReflectionProperty instead */ | ||
| 15 | final class ReflectionEnumProperty extends ReflectionProperty | ||
| 16 | { | ||
| 17 | /** @param class-string<BackedEnum> $enumType */ | ||
| 18 | public function __construct( | ||
| 19 | private readonly ReflectionProperty $originalReflectionProperty, | ||
| 20 | private readonly string $enumType, | ||
| 21 | ) { | ||
| 22 | parent::__construct( | ||
| 23 | $originalReflectionProperty->class, | ||
| 24 | $originalReflectionProperty->name, | ||
| 25 | ); | ||
| 26 | } | ||
| 27 | |||
| 28 | public function getValue(object|null $object = null): int|string|array|null | ||
| 29 | { | ||
| 30 | if ($object === null) { | ||
| 31 | return null; | ||
| 32 | } | ||
| 33 | |||
| 34 | $enum = $this->originalReflectionProperty->getValue($object); | ||
| 35 | |||
| 36 | if ($enum === null) { | ||
| 37 | return null; | ||
| 38 | } | ||
| 39 | |||
| 40 | if (is_array($enum)) { | ||
| 41 | return array_map( | ||
| 42 | static fn (BackedEnum $item): int|string => $item->value, | ||
| 43 | $enum, | ||
| 44 | ); | ||
| 45 | } | ||
| 46 | |||
| 47 | return $enum->value; | ||
| 48 | } | ||
| 49 | |||
| 50 | /** | ||
| 51 | * @param object $object | ||
| 52 | * @param int|string|int[]|string[]|BackedEnum|BackedEnum[]|null $value | ||
| 53 | */ | ||
| 54 | public function setValue(mixed $object, mixed $value = null): void | ||
| 55 | { | ||
| 56 | if ($value !== null) { | ||
| 57 | if (is_array($value)) { | ||
| 58 | $value = array_map(fn (int|string|BackedEnum $item): BackedEnum => $this->initializeEnumValue($object, $item), $value); | ||
| 59 | } else { | ||
| 60 | $value = $this->initializeEnumValue($object, $value); | ||
| 61 | } | ||
| 62 | } | ||
| 63 | |||
| 64 | $this->originalReflectionProperty->setValue($object, $value); | ||
| 65 | } | ||
| 66 | |||
| 67 | private function initializeEnumValue(object $object, int|string|BackedEnum $value): BackedEnum | ||
| 68 | { | ||
| 69 | if ($value instanceof BackedEnum) { | ||
| 70 | return $value; | ||
| 71 | } | ||
| 72 | |||
| 73 | $enumType = $this->enumType; | ||
| 74 | |||
| 75 | try { | ||
| 76 | return $enumType::from($value); | ||
| 77 | } catch (ValueError $e) { | ||
| 78 | throw MappingException::invalidEnumValue( | ||
| 79 | $object::class, | ||
| 80 | $this->originalReflectionProperty->name, | ||
| 81 | (string) $value, | ||
| 82 | $enumType, | ||
| 83 | $e, | ||
| 84 | ); | ||
| 85 | } | ||
| 86 | } | ||
| 87 | } | ||
diff --git a/vendor/doctrine/orm/src/Mapping/ReflectionReadonlyProperty.php b/vendor/doctrine/orm/src/Mapping/ReflectionReadonlyProperty.php new file mode 100644 index 0000000..13e9f6d --- /dev/null +++ b/vendor/doctrine/orm/src/Mapping/ReflectionReadonlyProperty.php | |||
| @@ -0,0 +1,49 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Mapping; | ||
| 6 | |||
| 7 | use InvalidArgumentException; | ||
| 8 | use LogicException; | ||
| 9 | use ReflectionProperty; | ||
| 10 | |||
| 11 | use function assert; | ||
| 12 | use function func_get_args; | ||
| 13 | use function func_num_args; | ||
| 14 | use function is_object; | ||
| 15 | use function sprintf; | ||
| 16 | |||
| 17 | /** @internal */ | ||
| 18 | final class ReflectionReadonlyProperty extends ReflectionProperty | ||
| 19 | { | ||
| 20 | public function __construct( | ||
| 21 | private readonly ReflectionProperty $wrappedProperty, | ||
| 22 | ) { | ||
| 23 | if (! $wrappedProperty->isReadOnly()) { | ||
| 24 | throw new InvalidArgumentException('Given property is not readonly.'); | ||
| 25 | } | ||
| 26 | |||
| 27 | parent::__construct($wrappedProperty->class, $wrappedProperty->name); | ||
| 28 | } | ||
| 29 | |||
| 30 | public function getValue(object|null $object = null): mixed | ||
| 31 | { | ||
| 32 | return $this->wrappedProperty->getValue(...func_get_args()); | ||
| 33 | } | ||
| 34 | |||
| 35 | public function setValue(mixed $objectOrValue, mixed $value = null): void | ||
| 36 | { | ||
| 37 | if (func_num_args() < 2 || $objectOrValue === null || ! $this->isInitialized($objectOrValue)) { | ||
| 38 | $this->wrappedProperty->setValue(...func_get_args()); | ||
| 39 | |||
| 40 | return; | ||
| 41 | } | ||
| 42 | |||
| 43 | assert(is_object($objectOrValue)); | ||
| 44 | |||
| 45 | if (parent::getValue($objectOrValue) !== $value) { | ||
| 46 | throw new LogicException(sprintf('Attempting to change readonly property %s::$%s.', $this->class, $this->name)); | ||
| 47 | } | ||
| 48 | } | ||
| 49 | } | ||
diff --git a/vendor/doctrine/orm/src/Mapping/SequenceGenerator.php b/vendor/doctrine/orm/src/Mapping/SequenceGenerator.php new file mode 100644 index 0000000..6c06e84 --- /dev/null +++ b/vendor/doctrine/orm/src/Mapping/SequenceGenerator.php | |||
| @@ -0,0 +1,18 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Mapping; | ||
| 6 | |||
| 7 | use Attribute; | ||
| 8 | |||
| 9 | #[Attribute(Attribute::TARGET_PROPERTY)] | ||
| 10 | final class SequenceGenerator implements MappingAttribute | ||
| 11 | { | ||
| 12 | public function __construct( | ||
| 13 | public readonly string|null $sequenceName = null, | ||
| 14 | public readonly int $allocationSize = 1, | ||
| 15 | public readonly int $initialValue = 1, | ||
| 16 | ) { | ||
| 17 | } | ||
| 18 | } | ||
diff --git a/vendor/doctrine/orm/src/Mapping/Table.php b/vendor/doctrine/orm/src/Mapping/Table.php new file mode 100644 index 0000000..ac1e8ed --- /dev/null +++ b/vendor/doctrine/orm/src/Mapping/Table.php | |||
| @@ -0,0 +1,45 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Mapping; | ||
| 6 | |||
| 7 | use Attribute; | ||
| 8 | use Doctrine\Deprecations\Deprecation; | ||
| 9 | |||
| 10 | #[Attribute(Attribute::TARGET_CLASS)] | ||
| 11 | final class Table implements MappingAttribute | ||
| 12 | { | ||
| 13 | /** | ||
| 14 | * @param array<Index>|null $indexes | ||
| 15 | * @param array<UniqueConstraint>|null $uniqueConstraints | ||
| 16 | * @param array<string,mixed> $options | ||
| 17 | */ | ||
| 18 | public function __construct( | ||
| 19 | public readonly string|null $name = null, | ||
| 20 | public readonly string|null $schema = null, | ||
| 21 | public readonly array|null $indexes = null, | ||
| 22 | public readonly array|null $uniqueConstraints = null, | ||
| 23 | public readonly array $options = [], | ||
| 24 | ) { | ||
| 25 | if ($this->indexes !== null) { | ||
| 26 | Deprecation::trigger( | ||
| 27 | 'doctrine/orm', | ||
| 28 | 'https://github.com/doctrine/orm/pull/11357', | ||
| 29 | 'Providing the property $indexes on %s does not have any effect and will be removed in Doctrine ORM 4.0. Please use the %s attribute instead.', | ||
| 30 | self::class, | ||
| 31 | Index::class, | ||
| 32 | ); | ||
| 33 | } | ||
| 34 | |||
| 35 | if ($this->uniqueConstraints !== null) { | ||
| 36 | Deprecation::trigger( | ||
| 37 | 'doctrine/orm', | ||
| 38 | 'https://github.com/doctrine/orm/pull/11357', | ||
| 39 | 'Providing the property $uniqueConstraints on %s does not have any effect and will be removed in Doctrine ORM 4.0. Please use the %s attribute instead.', | ||
| 40 | self::class, | ||
| 41 | UniqueConstraint::class, | ||
| 42 | ); | ||
| 43 | } | ||
| 44 | } | ||
| 45 | } | ||
diff --git a/vendor/doctrine/orm/src/Mapping/ToManyAssociationMapping.php b/vendor/doctrine/orm/src/Mapping/ToManyAssociationMapping.php new file mode 100644 index 0000000..2e4969c --- /dev/null +++ b/vendor/doctrine/orm/src/Mapping/ToManyAssociationMapping.php | |||
| @@ -0,0 +1,16 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Mapping; | ||
| 6 | |||
| 7 | interface ToManyAssociationMapping | ||
| 8 | { | ||
| 9 | /** @psalm-assert-if-true string $this->indexBy() */ | ||
| 10 | public function isIndexed(): bool; | ||
| 11 | |||
| 12 | public function indexBy(): string; | ||
| 13 | |||
| 14 | /** @return array<string, 'asc'|'desc'> */ | ||
| 15 | public function orderBy(): array; | ||
| 16 | } | ||
diff --git a/vendor/doctrine/orm/src/Mapping/ToManyAssociationMappingImplementation.php b/vendor/doctrine/orm/src/Mapping/ToManyAssociationMappingImplementation.php new file mode 100644 index 0000000..306880d --- /dev/null +++ b/vendor/doctrine/orm/src/Mapping/ToManyAssociationMappingImplementation.php | |||
| @@ -0,0 +1,69 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Mapping; | ||
| 6 | |||
| 7 | use LogicException; | ||
| 8 | |||
| 9 | use function sprintf; | ||
| 10 | |||
| 11 | /** @internal */ | ||
| 12 | trait ToManyAssociationMappingImplementation | ||
| 13 | { | ||
| 14 | /** | ||
| 15 | * Specification of a field on target-entity that is used to index the | ||
| 16 | * collection by. This field HAS to be either the primary key or a unique | ||
| 17 | * column. Otherwise the collection does not contain all the entities that | ||
| 18 | * are actually related. | ||
| 19 | */ | ||
| 20 | public string|null $indexBy = null; | ||
| 21 | |||
| 22 | /** | ||
| 23 | * A map of field names (of the target entity) to sorting directions | ||
| 24 | * | ||
| 25 | * @var array<string, 'asc'|'desc'> | ||
| 26 | */ | ||
| 27 | public array $orderBy = []; | ||
| 28 | |||
| 29 | /** @return array<string, 'asc'|'desc'> */ | ||
| 30 | final public function orderBy(): array | ||
| 31 | { | ||
| 32 | return $this->orderBy; | ||
| 33 | } | ||
| 34 | |||
| 35 | /** @psalm-assert-if-true !null $this->indexBy */ | ||
| 36 | final public function isIndexed(): bool | ||
| 37 | { | ||
| 38 | return $this->indexBy !== null; | ||
| 39 | } | ||
| 40 | |||
| 41 | final public function indexBy(): string | ||
| 42 | { | ||
| 43 | if (! $this->isIndexed()) { | ||
| 44 | throw new LogicException(sprintf( | ||
| 45 | 'This mapping is not indexed. Use %s::isIndexed() to check that before calling %s.', | ||
| 46 | self::class, | ||
| 47 | __METHOD__, | ||
| 48 | )); | ||
| 49 | } | ||
| 50 | |||
| 51 | return $this->indexBy; | ||
| 52 | } | ||
| 53 | |||
| 54 | /** @return list<string> */ | ||
| 55 | public function __sleep(): array | ||
| 56 | { | ||
| 57 | $serialized = parent::__sleep(); | ||
| 58 | |||
| 59 | if ($this->indexBy !== null) { | ||
| 60 | $serialized[] = 'indexBy'; | ||
| 61 | } | ||
| 62 | |||
| 63 | if ($this->orderBy !== []) { | ||
| 64 | $serialized[] = 'orderBy'; | ||
| 65 | } | ||
| 66 | |||
| 67 | return $serialized; | ||
| 68 | } | ||
| 69 | } | ||
diff --git a/vendor/doctrine/orm/src/Mapping/ToManyInverseSideMapping.php b/vendor/doctrine/orm/src/Mapping/ToManyInverseSideMapping.php new file mode 100644 index 0000000..a092ebe --- /dev/null +++ b/vendor/doctrine/orm/src/Mapping/ToManyInverseSideMapping.php | |||
| @@ -0,0 +1,10 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Mapping; | ||
| 6 | |||
| 7 | abstract class ToManyInverseSideMapping extends InverseSideMapping implements ToManyAssociationMapping | ||
| 8 | { | ||
| 9 | use ToManyAssociationMappingImplementation; | ||
| 10 | } | ||
diff --git a/vendor/doctrine/orm/src/Mapping/ToManyOwningSideMapping.php b/vendor/doctrine/orm/src/Mapping/ToManyOwningSideMapping.php new file mode 100644 index 0000000..92eca7c --- /dev/null +++ b/vendor/doctrine/orm/src/Mapping/ToManyOwningSideMapping.php | |||
| @@ -0,0 +1,10 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Mapping; | ||
| 6 | |||
| 7 | abstract class ToManyOwningSideMapping extends OwningSideMapping | ||
| 8 | { | ||
| 9 | use ToManyAssociationMappingImplementation; | ||
| 10 | } | ||
diff --git a/vendor/doctrine/orm/src/Mapping/ToOneAssociationMapping.php b/vendor/doctrine/orm/src/Mapping/ToOneAssociationMapping.php new file mode 100644 index 0000000..048055c --- /dev/null +++ b/vendor/doctrine/orm/src/Mapping/ToOneAssociationMapping.php | |||
| @@ -0,0 +1,9 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Mapping; | ||
| 6 | |||
| 7 | interface ToOneAssociationMapping | ||
| 8 | { | ||
| 9 | } | ||
diff --git a/vendor/doctrine/orm/src/Mapping/ToOneInverseSideMapping.php b/vendor/doctrine/orm/src/Mapping/ToOneInverseSideMapping.php new file mode 100644 index 0000000..5be89e6 --- /dev/null +++ b/vendor/doctrine/orm/src/Mapping/ToOneInverseSideMapping.php | |||
| @@ -0,0 +1,52 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Mapping; | ||
| 6 | |||
| 7 | abstract class ToOneInverseSideMapping extends InverseSideMapping | ||
| 8 | { | ||
| 9 | /** | ||
| 10 | * @param mixed[] $mappingArray | ||
| 11 | * @param class-string $name | ||
| 12 | * @psalm-param array{ | ||
| 13 | * fieldName: string, | ||
| 14 | * sourceEntity: class-string, | ||
| 15 | * targetEntity: class-string, | ||
| 16 | * cascade?: list<'persist'|'remove'|'detach'|'refresh'|'all'>, | ||
| 17 | * fetch?: ClassMetadata::FETCH_*|null, | ||
| 18 | * inherited?: class-string|null, | ||
| 19 | * declared?: class-string|null, | ||
| 20 | * cache?: array<mixed>|null, | ||
| 21 | * id?: bool|null, | ||
| 22 | * isOnDeleteCascade?: bool|null, | ||
| 23 | * originalClass?: class-string|null, | ||
| 24 | * originalField?: string|null, | ||
| 25 | * orphanRemoval?: bool, | ||
| 26 | * unique?: bool|null, | ||
| 27 | * joinTable?: mixed[]|null, | ||
| 28 | * type?: int, | ||
| 29 | * isOwningSide: bool, | ||
| 30 | * } $mappingArray | ||
| 31 | */ | ||
| 32 | public static function fromMappingArrayAndName( | ||
| 33 | array $mappingArray, | ||
| 34 | string $name, | ||
| 35 | ): static { | ||
| 36 | $mapping = static::fromMappingArray($mappingArray); | ||
| 37 | |||
| 38 | if (isset($mapping->id) && $mapping->id === true) { | ||
| 39 | throw MappingException::illegalInverseIdentifierAssociation($name, $mapping->fieldName); | ||
| 40 | } | ||
| 41 | |||
| 42 | if ($mapping->orphanRemoval) { | ||
| 43 | if (! $mapping->isCascadeRemove()) { | ||
| 44 | $mapping->cascade[] = 'remove'; | ||
| 45 | } | ||
| 46 | |||
| 47 | $mapping->unique = null; | ||
| 48 | } | ||
| 49 | |||
| 50 | return $mapping; | ||
| 51 | } | ||
| 52 | } | ||
diff --git a/vendor/doctrine/orm/src/Mapping/ToOneOwningSideMapping.php b/vendor/doctrine/orm/src/Mapping/ToOneOwningSideMapping.php new file mode 100644 index 0000000..cb85afb --- /dev/null +++ b/vendor/doctrine/orm/src/Mapping/ToOneOwningSideMapping.php | |||
| @@ -0,0 +1,212 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Mapping; | ||
| 6 | |||
| 7 | use RuntimeException; | ||
| 8 | |||
| 9 | use function array_flip; | ||
| 10 | use function assert; | ||
| 11 | use function count; | ||
| 12 | use function trim; | ||
| 13 | |||
| 14 | abstract class ToOneOwningSideMapping extends OwningSideMapping implements ToOneAssociationMapping | ||
| 15 | { | ||
| 16 | /** @var array<string, string> */ | ||
| 17 | public array $sourceToTargetKeyColumns = []; | ||
| 18 | |||
| 19 | /** @var array<string, string> */ | ||
| 20 | public array $targetToSourceKeyColumns = []; | ||
| 21 | |||
| 22 | /** @var list<JoinColumnMapping> */ | ||
| 23 | public array $joinColumns = []; | ||
| 24 | |||
| 25 | /** @var array<string, string> */ | ||
| 26 | public array $joinColumnFieldNames = []; | ||
| 27 | |||
| 28 | /** | ||
| 29 | * @param array<string, mixed> $mappingArray | ||
| 30 | * @psalm-param array{ | ||
| 31 | * fieldName: string, | ||
| 32 | * sourceEntity: class-string, | ||
| 33 | * targetEntity: class-string, | ||
| 34 | * cascade?: list<'persist'|'remove'|'detach'|'refresh'|'all'>, | ||
| 35 | * fetch?: ClassMetadata::FETCH_*|null, | ||
| 36 | * inherited?: class-string|null, | ||
| 37 | * declared?: class-string|null, | ||
| 38 | * cache?: array<mixed>|null, | ||
| 39 | * id?: bool|null, | ||
| 40 | * isOnDeleteCascade?: bool|null, | ||
| 41 | * originalClass?: class-string|null, | ||
| 42 | * originalField?: string|null, | ||
| 43 | * orphanRemoval?: bool, | ||
| 44 | * unique?: bool|null, | ||
| 45 | * joinTable?: mixed[]|null, | ||
| 46 | * type?: int, | ||
| 47 | * isOwningSide: bool, | ||
| 48 | * joinColumns?: mixed[]|null, | ||
| 49 | * } $mappingArray | ||
| 50 | */ | ||
| 51 | public static function fromMappingArray(array $mappingArray): static | ||
| 52 | { | ||
| 53 | $joinColumns = $mappingArray['joinColumns'] ?? []; | ||
| 54 | unset($mappingArray['joinColumns']); | ||
| 55 | |||
| 56 | $instance = parent::fromMappingArray($mappingArray); | ||
| 57 | assert($instance->isToOneOwningSide()); | ||
| 58 | |||
| 59 | foreach ($joinColumns as $column) { | ||
| 60 | $instance->joinColumns[] = JoinColumnMapping::fromMappingArray($column); | ||
| 61 | } | ||
| 62 | |||
| 63 | if ($instance->orphanRemoval) { | ||
| 64 | if (! $instance->isCascadeRemove()) { | ||
| 65 | $instance->cascade[] = 'remove'; | ||
| 66 | } | ||
| 67 | |||
| 68 | $instance->unique = null; | ||
| 69 | } | ||
| 70 | |||
| 71 | return $instance; | ||
| 72 | } | ||
| 73 | |||
| 74 | /** | ||
| 75 | * @param mixed[] $mappingArray | ||
| 76 | * @param class-string $name | ||
| 77 | * @psalm-param array{ | ||
| 78 | * fieldName: string, | ||
| 79 | * sourceEntity: class-string, | ||
| 80 | * targetEntity: class-string, | ||
| 81 | * cascade?: list<'persist'|'remove'|'detach'|'refresh'|'all'>, | ||
| 82 | * fetch?: ClassMetadata::FETCH_*|null, | ||
| 83 | * inherited?: class-string|null, | ||
| 84 | * declared?: class-string|null, | ||
| 85 | * cache?: array<mixed>|null, | ||
| 86 | * id?: bool|null, | ||
| 87 | * isOnDeleteCascade?: bool|null, | ||
| 88 | * originalClass?: class-string|null, | ||
| 89 | * originalField?: string|null, | ||
| 90 | * orphanRemoval?: bool, | ||
| 91 | * unique?: bool|null, | ||
| 92 | * joinTable?: mixed[]|null, | ||
| 93 | * type?: int, | ||
| 94 | * isOwningSide: bool, | ||
| 95 | * joinColumns?: mixed[]|null, | ||
| 96 | * } $mappingArray | ||
| 97 | */ | ||
| 98 | public static function fromMappingArrayAndName( | ||
| 99 | array $mappingArray, | ||
| 100 | NamingStrategy $namingStrategy, | ||
| 101 | string $name, | ||
| 102 | array|null $table, | ||
| 103 | bool $isInheritanceTypeSingleTable, | ||
| 104 | ): static { | ||
| 105 | if (isset($mappingArray['joinColumns'])) { | ||
| 106 | foreach ($mappingArray['joinColumns'] as $index => $joinColumn) { | ||
| 107 | if (empty($joinColumn['name'])) { | ||
| 108 | $mappingArray['joinColumns'][$index]['name'] = $namingStrategy->joinColumnName($mappingArray['fieldName'], $name); | ||
| 109 | } | ||
| 110 | } | ||
| 111 | } | ||
| 112 | |||
| 113 | $mapping = static::fromMappingArray($mappingArray); | ||
| 114 | |||
| 115 | assert($mapping->isToOneOwningSide()); | ||
| 116 | if (empty($mapping->joinColumns)) { | ||
| 117 | // Apply default join column | ||
| 118 | $mapping->joinColumns = [ | ||
| 119 | JoinColumnMapping::fromMappingArray([ | ||
| 120 | 'name' => $namingStrategy->joinColumnName($mapping->fieldName, $name), | ||
| 121 | 'referencedColumnName' => $namingStrategy->referenceColumnName(), | ||
| 122 | ]), | ||
| 123 | ]; | ||
| 124 | } | ||
| 125 | |||
| 126 | $uniqueConstraintColumns = []; | ||
| 127 | |||
| 128 | foreach ($mapping->joinColumns as $joinColumn) { | ||
| 129 | if ($mapping->isOneToOne() && ! $isInheritanceTypeSingleTable) { | ||
| 130 | if (count($mapping->joinColumns) === 1) { | ||
| 131 | if (empty($mapping->id)) { | ||
| 132 | $joinColumn->unique = true; | ||
| 133 | } | ||
| 134 | } else { | ||
| 135 | $uniqueConstraintColumns[] = $joinColumn->name; | ||
| 136 | } | ||
| 137 | } | ||
| 138 | |||
| 139 | if (empty($joinColumn->referencedColumnName)) { | ||
| 140 | $joinColumn->referencedColumnName = $namingStrategy->referenceColumnName(); | ||
| 141 | } | ||
| 142 | |||
| 143 | if ($joinColumn->name[0] === '`') { | ||
| 144 | $joinColumn->name = trim($joinColumn->name, '`'); | ||
| 145 | $joinColumn->quoted = true; | ||
| 146 | } | ||
| 147 | |||
| 148 | if ($joinColumn->referencedColumnName[0] === '`') { | ||
| 149 | $joinColumn->referencedColumnName = trim($joinColumn->referencedColumnName, '`'); | ||
| 150 | $joinColumn->quoted = true; | ||
| 151 | } | ||
| 152 | |||
| 153 | $mapping->sourceToTargetKeyColumns[$joinColumn->name] = $joinColumn->referencedColumnName; | ||
| 154 | $mapping->joinColumnFieldNames[$joinColumn->name] = $joinColumn->fieldName ?? $joinColumn->name; | ||
| 155 | } | ||
| 156 | |||
| 157 | if ($uniqueConstraintColumns) { | ||
| 158 | if (! $table) { | ||
| 159 | throw new RuntimeException('ClassMetadata::setTable() has to be called before defining a one to one relationship.'); | ||
| 160 | } | ||
| 161 | |||
| 162 | $table['uniqueConstraints'][$mapping->fieldName . '_uniq'] = ['columns' => $uniqueConstraintColumns]; | ||
| 163 | } | ||
| 164 | |||
| 165 | $mapping->targetToSourceKeyColumns = array_flip($mapping->sourceToTargetKeyColumns); | ||
| 166 | |||
| 167 | return $mapping; | ||
| 168 | } | ||
| 169 | |||
| 170 | public function offsetSet(mixed $offset, mixed $value): void | ||
| 171 | { | ||
| 172 | if ($offset === 'joinColumns') { | ||
| 173 | $joinColumns = []; | ||
| 174 | foreach ($value as $column) { | ||
| 175 | $joinColumns[] = JoinColumnMapping::fromMappingArray($column); | ||
| 176 | } | ||
| 177 | |||
| 178 | $this->joinColumns = $joinColumns; | ||
| 179 | |||
| 180 | return; | ||
| 181 | } | ||
| 182 | |||
| 183 | parent::offsetSet($offset, $value); | ||
| 184 | } | ||
| 185 | |||
| 186 | /** @return array<string, mixed> */ | ||
| 187 | public function toArray(): array | ||
| 188 | { | ||
| 189 | $array = parent::toArray(); | ||
| 190 | |||
| 191 | $joinColumns = []; | ||
| 192 | foreach ($array['joinColumns'] as $column) { | ||
| 193 | $joinColumns[] = (array) $column; | ||
| 194 | } | ||
| 195 | |||
| 196 | $array['joinColumns'] = $joinColumns; | ||
| 197 | |||
| 198 | return $array; | ||
| 199 | } | ||
| 200 | |||
| 201 | /** @return list<string> */ | ||
| 202 | public function __sleep(): array | ||
| 203 | { | ||
| 204 | return [ | ||
| 205 | ...parent::__sleep(), | ||
| 206 | 'joinColumns', | ||
| 207 | 'joinColumnFieldNames', | ||
| 208 | 'sourceToTargetKeyColumns', | ||
| 209 | 'targetToSourceKeyColumns', | ||
| 210 | ]; | ||
| 211 | } | ||
| 212 | } | ||
diff --git a/vendor/doctrine/orm/src/Mapping/TypedFieldMapper.php b/vendor/doctrine/orm/src/Mapping/TypedFieldMapper.php new file mode 100644 index 0000000..2db9e90 --- /dev/null +++ b/vendor/doctrine/orm/src/Mapping/TypedFieldMapper.php | |||
| @@ -0,0 +1,20 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Mapping; | ||
| 6 | |||
| 7 | use BackedEnum; | ||
| 8 | use ReflectionProperty; | ||
| 9 | |||
| 10 | interface TypedFieldMapper | ||
| 11 | { | ||
| 12 | /** | ||
| 13 | * Validates & completes the given field mapping based on typed property. | ||
| 14 | * | ||
| 15 | * @param array{fieldName: string, enumType?: class-string<BackedEnum>, type?: string} $mapping The field mapping to validate & complete. | ||
| 16 | * | ||
| 17 | * @return array{fieldName: string, enumType?: class-string<BackedEnum>, type?: string} The updated mapping. | ||
| 18 | */ | ||
| 19 | public function validateAndComplete(array $mapping, ReflectionProperty $field): array; | ||
| 20 | } | ||
diff --git a/vendor/doctrine/orm/src/Mapping/UnderscoreNamingStrategy.php b/vendor/doctrine/orm/src/Mapping/UnderscoreNamingStrategy.php new file mode 100644 index 0000000..cedc150 --- /dev/null +++ b/vendor/doctrine/orm/src/Mapping/UnderscoreNamingStrategy.php | |||
| @@ -0,0 +1,108 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Mapping; | ||
| 6 | |||
| 7 | use function preg_replace; | ||
| 8 | use function str_contains; | ||
| 9 | use function strrpos; | ||
| 10 | use function strtolower; | ||
| 11 | use function strtoupper; | ||
| 12 | use function substr; | ||
| 13 | |||
| 14 | use const CASE_LOWER; | ||
| 15 | use const CASE_UPPER; | ||
| 16 | |||
| 17 | /** | ||
| 18 | * Naming strategy implementing the underscore naming convention. | ||
| 19 | * Converts 'MyEntity' to 'my_entity' or 'MY_ENTITY'. | ||
| 20 | * | ||
| 21 | * @link www.doctrine-project.org | ||
| 22 | */ | ||
| 23 | class UnderscoreNamingStrategy implements NamingStrategy | ||
| 24 | { | ||
| 25 | /** | ||
| 26 | * Underscore naming strategy construct. | ||
| 27 | * | ||
| 28 | * @param int $case CASE_LOWER | CASE_UPPER | ||
| 29 | */ | ||
| 30 | public function __construct(private int $case = CASE_LOWER) | ||
| 31 | { | ||
| 32 | } | ||
| 33 | |||
| 34 | /** @return int CASE_LOWER | CASE_UPPER */ | ||
| 35 | public function getCase(): int | ||
| 36 | { | ||
| 37 | return $this->case; | ||
| 38 | } | ||
| 39 | |||
| 40 | /** | ||
| 41 | * Sets string case CASE_LOWER | CASE_UPPER. | ||
| 42 | * Alphabetic characters converted to lowercase or uppercase. | ||
| 43 | */ | ||
| 44 | public function setCase(int $case): void | ||
| 45 | { | ||
| 46 | $this->case = $case; | ||
| 47 | } | ||
| 48 | |||
| 49 | public function classToTableName(string $className): string | ||
| 50 | { | ||
| 51 | if (str_contains($className, '\\')) { | ||
| 52 | $className = substr($className, strrpos($className, '\\') + 1); | ||
| 53 | } | ||
| 54 | |||
| 55 | return $this->underscore($className); | ||
| 56 | } | ||
| 57 | |||
| 58 | public function propertyToColumnName(string $propertyName, string $className): string | ||
| 59 | { | ||
| 60 | return $this->underscore($propertyName); | ||
| 61 | } | ||
| 62 | |||
| 63 | public function embeddedFieldToColumnName( | ||
| 64 | string $propertyName, | ||
| 65 | string $embeddedColumnName, | ||
| 66 | string $className, | ||
| 67 | string $embeddedClassName, | ||
| 68 | ): string { | ||
| 69 | return $this->underscore($propertyName) . '_' . $embeddedColumnName; | ||
| 70 | } | ||
| 71 | |||
| 72 | public function referenceColumnName(): string | ||
| 73 | { | ||
| 74 | return $this->case === CASE_UPPER ? 'ID' : 'id'; | ||
| 75 | } | ||
| 76 | |||
| 77 | public function joinColumnName(string $propertyName, string $className): string | ||
| 78 | { | ||
| 79 | return $this->underscore($propertyName) . '_' . $this->referenceColumnName(); | ||
| 80 | } | ||
| 81 | |||
| 82 | public function joinTableName( | ||
| 83 | string $sourceEntity, | ||
| 84 | string $targetEntity, | ||
| 85 | string $propertyName, | ||
| 86 | ): string { | ||
| 87 | return $this->classToTableName($sourceEntity) . '_' . $this->classToTableName($targetEntity); | ||
| 88 | } | ||
| 89 | |||
| 90 | public function joinKeyColumnName( | ||
| 91 | string $entityName, | ||
| 92 | string|null $referencedColumnName, | ||
| 93 | ): string { | ||
| 94 | return $this->classToTableName($entityName) . '_' . | ||
| 95 | ($referencedColumnName ?: $this->referenceColumnName()); | ||
| 96 | } | ||
| 97 | |||
| 98 | private function underscore(string $string): string | ||
| 99 | { | ||
| 100 | $string = preg_replace('/(?<=[a-z0-9])([A-Z])/', '_$1', $string); | ||
| 101 | |||
| 102 | if ($this->case === CASE_UPPER) { | ||
| 103 | return strtoupper($string); | ||
| 104 | } | ||
| 105 | |||
| 106 | return strtolower($string); | ||
| 107 | } | ||
| 108 | } | ||
diff --git a/vendor/doctrine/orm/src/Mapping/UniqueConstraint.php b/vendor/doctrine/orm/src/Mapping/UniqueConstraint.php new file mode 100644 index 0000000..3180be0 --- /dev/null +++ b/vendor/doctrine/orm/src/Mapping/UniqueConstraint.php | |||
| @@ -0,0 +1,24 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Mapping; | ||
| 6 | |||
| 7 | use Attribute; | ||
| 8 | |||
| 9 | #[Attribute(Attribute::TARGET_CLASS | Attribute::IS_REPEATABLE)] | ||
| 10 | final class UniqueConstraint implements MappingAttribute | ||
| 11 | { | ||
| 12 | /** | ||
| 13 | * @param array<string>|null $columns | ||
| 14 | * @param array<string>|null $fields | ||
| 15 | * @param array<string,mixed>|null $options | ||
| 16 | */ | ||
| 17 | public function __construct( | ||
| 18 | public readonly string|null $name = null, | ||
| 19 | public readonly array|null $columns = null, | ||
| 20 | public readonly array|null $fields = null, | ||
| 21 | public readonly array|null $options = null, | ||
| 22 | ) { | ||
| 23 | } | ||
| 24 | } | ||
diff --git a/vendor/doctrine/orm/src/Mapping/Version.php b/vendor/doctrine/orm/src/Mapping/Version.php new file mode 100644 index 0000000..7252e05 --- /dev/null +++ b/vendor/doctrine/orm/src/Mapping/Version.php | |||
| @@ -0,0 +1,12 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Mapping; | ||
| 6 | |||
| 7 | use Attribute; | ||
| 8 | |||
| 9 | #[Attribute(Attribute::TARGET_PROPERTY)] | ||
| 10 | final class Version implements MappingAttribute | ||
| 11 | { | ||
| 12 | } | ||
diff --git a/vendor/doctrine/orm/src/NativeQuery.php b/vendor/doctrine/orm/src/NativeQuery.php new file mode 100644 index 0000000..6cee0e8 --- /dev/null +++ b/vendor/doctrine/orm/src/NativeQuery.php | |||
| @@ -0,0 +1,68 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM; | ||
| 6 | |||
| 7 | use Doctrine\DBAL\Result; | ||
| 8 | use Doctrine\ORM\Query\ParameterTypeInferer; | ||
| 9 | |||
| 10 | use function array_values; | ||
| 11 | use function is_int; | ||
| 12 | use function key; | ||
| 13 | use function ksort; | ||
| 14 | |||
| 15 | /** | ||
| 16 | * Represents a native SQL query. | ||
| 17 | * | ||
| 18 | * @final | ||
| 19 | */ | ||
| 20 | class NativeQuery extends AbstractQuery | ||
| 21 | { | ||
| 22 | private string $sql; | ||
| 23 | |||
| 24 | /** @return $this */ | ||
| 25 | public function setSQL(string $sql): self | ||
| 26 | { | ||
| 27 | $this->sql = $sql; | ||
| 28 | |||
| 29 | return $this; | ||
| 30 | } | ||
| 31 | |||
| 32 | public function getSQL(): string | ||
| 33 | { | ||
| 34 | return $this->sql; | ||
| 35 | } | ||
| 36 | |||
| 37 | protected function _doExecute(): Result|int | ||
| 38 | { | ||
| 39 | $parameters = []; | ||
| 40 | $types = []; | ||
| 41 | |||
| 42 | foreach ($this->getParameters() as $parameter) { | ||
| 43 | $name = $parameter->getName(); | ||
| 44 | $value = $this->processParameterValue($parameter->getValue()); | ||
| 45 | $type = $parameter->getValue() === $value | ||
| 46 | ? $parameter->getType() | ||
| 47 | : ParameterTypeInferer::inferType($value); | ||
| 48 | |||
| 49 | $parameters[$name] = $value; | ||
| 50 | $types[$name] = $type; | ||
| 51 | } | ||
| 52 | |||
| 53 | if ($parameters && is_int(key($parameters))) { | ||
| 54 | ksort($parameters); | ||
| 55 | ksort($types); | ||
| 56 | |||
| 57 | $parameters = array_values($parameters); | ||
| 58 | $types = array_values($types); | ||
| 59 | } | ||
| 60 | |||
| 61 | return $this->em->getConnection()->executeQuery( | ||
| 62 | $this->sql, | ||
| 63 | $parameters, | ||
| 64 | $types, | ||
| 65 | $this->queryCacheProfile, | ||
| 66 | ); | ||
| 67 | } | ||
| 68 | } | ||
diff --git a/vendor/doctrine/orm/src/NoResultException.php b/vendor/doctrine/orm/src/NoResultException.php new file mode 100644 index 0000000..6ecabae --- /dev/null +++ b/vendor/doctrine/orm/src/NoResultException.php | |||
| @@ -0,0 +1,16 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM; | ||
| 6 | |||
| 7 | /** | ||
| 8 | * Exception thrown when an ORM query unexpectedly does not return any results. | ||
| 9 | */ | ||
| 10 | class NoResultException extends UnexpectedResultException | ||
| 11 | { | ||
| 12 | public function __construct() | ||
| 13 | { | ||
| 14 | parent::__construct('No result was found for query although at least one row was expected.'); | ||
| 15 | } | ||
| 16 | } | ||
diff --git a/vendor/doctrine/orm/src/NonUniqueResultException.php b/vendor/doctrine/orm/src/NonUniqueResultException.php new file mode 100644 index 0000000..8f56e44 --- /dev/null +++ b/vendor/doctrine/orm/src/NonUniqueResultException.php | |||
| @@ -0,0 +1,18 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM; | ||
| 6 | |||
| 7 | /** | ||
| 8 | * Exception thrown when an ORM query unexpectedly returns more than one result. | ||
| 9 | */ | ||
| 10 | class NonUniqueResultException extends UnexpectedResultException | ||
| 11 | { | ||
| 12 | public const DEFAULT_MESSAGE = 'More than one result was found for query although one row or none was expected.'; | ||
| 13 | |||
| 14 | public function __construct(string|null $message = null) | ||
| 15 | { | ||
| 16 | parent::__construct($message ?? self::DEFAULT_MESSAGE); | ||
| 17 | } | ||
| 18 | } | ||
diff --git a/vendor/doctrine/orm/src/ORMInvalidArgumentException.php b/vendor/doctrine/orm/src/ORMInvalidArgumentException.php new file mode 100644 index 0000000..fe07a2a --- /dev/null +++ b/vendor/doctrine/orm/src/ORMInvalidArgumentException.php | |||
| @@ -0,0 +1,195 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM; | ||
| 6 | |||
| 7 | use Doctrine\ORM\Mapping\AssociationMapping; | ||
| 8 | use Doctrine\ORM\Mapping\ClassMetadata; | ||
| 9 | use InvalidArgumentException; | ||
| 10 | use Stringable; | ||
| 11 | |||
| 12 | use function array_map; | ||
| 13 | use function count; | ||
| 14 | use function get_debug_type; | ||
| 15 | use function gettype; | ||
| 16 | use function implode; | ||
| 17 | use function is_scalar; | ||
| 18 | use function reset; | ||
| 19 | use function spl_object_id; | ||
| 20 | use function sprintf; | ||
| 21 | |||
| 22 | /** | ||
| 23 | * Contains exception messages for all invalid lifecycle state exceptions inside UnitOfWork | ||
| 24 | */ | ||
| 25 | class ORMInvalidArgumentException extends InvalidArgumentException | ||
| 26 | { | ||
| 27 | public static function scheduleInsertForManagedEntity(object $entity): self | ||
| 28 | { | ||
| 29 | return new self('A managed+dirty entity ' . self::objToStr($entity) . ' can not be scheduled for insertion.'); | ||
| 30 | } | ||
| 31 | |||
| 32 | public static function scheduleInsertForRemovedEntity(object $entity): self | ||
| 33 | { | ||
| 34 | return new self('Removed entity ' . self::objToStr($entity) . ' can not be scheduled for insertion.'); | ||
| 35 | } | ||
| 36 | |||
| 37 | public static function scheduleInsertTwice(object $entity): self | ||
| 38 | { | ||
| 39 | return new self('Entity ' . self::objToStr($entity) . ' can not be scheduled for insertion twice.'); | ||
| 40 | } | ||
| 41 | |||
| 42 | public static function entityWithoutIdentity(string $className, object $entity): self | ||
| 43 | { | ||
| 44 | return new self( | ||
| 45 | "The given entity of type '" . $className . "' (" . self::objToStr($entity) . ') has no identity/no ' . | ||
| 46 | 'id values set. It cannot be added to the identity map.', | ||
| 47 | ); | ||
| 48 | } | ||
| 49 | |||
| 50 | public static function readOnlyRequiresManagedEntity(object $entity): self | ||
| 51 | { | ||
| 52 | return new self('Only managed entities can be marked or checked as read only. But ' . self::objToStr($entity) . ' is not'); | ||
| 53 | } | ||
| 54 | |||
| 55 | /** @param non-empty-list<array{AssociationMapping, object}> $newEntitiesWithAssociations */ | ||
| 56 | public static function newEntitiesFoundThroughRelationships(array $newEntitiesWithAssociations): self | ||
| 57 | { | ||
| 58 | $errorMessages = array_map( | ||
| 59 | static function (array $newEntityWithAssociation): string { | ||
| 60 | [$associationMapping, $entity] = $newEntityWithAssociation; | ||
| 61 | |||
| 62 | return self::newEntityFoundThroughRelationshipMessage($associationMapping, $entity); | ||
| 63 | }, | ||
| 64 | $newEntitiesWithAssociations, | ||
| 65 | ); | ||
| 66 | |||
| 67 | if (count($errorMessages) === 1) { | ||
| 68 | return new self(reset($errorMessages)); | ||
| 69 | } | ||
| 70 | |||
| 71 | return new self( | ||
| 72 | 'Multiple non-persisted new entities were found through the given association graph:' | ||
| 73 | . "\n\n * " | ||
| 74 | . implode("\n * ", $errorMessages), | ||
| 75 | ); | ||
| 76 | } | ||
| 77 | |||
| 78 | public static function newEntityFoundThroughRelationship(AssociationMapping $associationMapping, object $entry): self | ||
| 79 | { | ||
| 80 | return new self(self::newEntityFoundThroughRelationshipMessage($associationMapping, $entry)); | ||
| 81 | } | ||
| 82 | |||
| 83 | public static function detachedEntityFoundThroughRelationship(AssociationMapping $assoc, object $entry): self | ||
| 84 | { | ||
| 85 | return new self('A detached entity of type ' . $assoc->targetEntity . ' (' . self::objToStr($entry) . ') ' | ||
| 86 | . " was found through the relationship '" . $assoc->sourceEntity . '#' . $assoc->fieldName . "' " | ||
| 87 | . 'during cascading a persist operation.'); | ||
| 88 | } | ||
| 89 | |||
| 90 | public static function entityNotManaged(object $entity): self | ||
| 91 | { | ||
| 92 | return new self('Entity ' . self::objToStr($entity) . ' is not managed. An entity is managed if its fetched ' . | ||
| 93 | 'from the database or registered as new through EntityManager#persist'); | ||
| 94 | } | ||
| 95 | |||
| 96 | public static function entityHasNoIdentity(object $entity, string $operation): self | ||
| 97 | { | ||
| 98 | return new self('Entity has no identity, therefore ' . $operation . ' cannot be performed. ' . self::objToStr($entity)); | ||
| 99 | } | ||
| 100 | |||
| 101 | public static function entityIsRemoved(object $entity, string $operation): self | ||
| 102 | { | ||
| 103 | return new self('Entity is removed, therefore ' . $operation . ' cannot be performed. ' . self::objToStr($entity)); | ||
| 104 | } | ||
| 105 | |||
| 106 | public static function detachedEntityCannot(object $entity, string $operation): self | ||
| 107 | { | ||
| 108 | return new self('Detached entity ' . self::objToStr($entity) . ' cannot be ' . $operation); | ||
| 109 | } | ||
| 110 | |||
| 111 | public static function invalidObject(string $context, mixed $given, int $parameterIndex = 1): self | ||
| 112 | { | ||
| 113 | return new self($context . ' expects parameter ' . $parameterIndex . | ||
| 114 | ' to be an entity object, ' . gettype($given) . ' given.'); | ||
| 115 | } | ||
| 116 | |||
| 117 | public static function invalidCompositeIdentifier(): self | ||
| 118 | { | ||
| 119 | return new self('Binding an entity with a composite primary key to a query is not supported. ' . | ||
| 120 | 'You should split the parameter into the explicit fields and bind them separately.'); | ||
| 121 | } | ||
| 122 | |||
| 123 | public static function invalidIdentifierBindingEntity(string $class): self | ||
| 124 | { | ||
| 125 | return new self(sprintf( | ||
| 126 | <<<'EXCEPTION' | ||
| 127 | Binding entities to query parameters only allowed for entities that have an identifier. | ||
| 128 | Class "%s" does not have an identifier. | ||
| 129 | EXCEPTION | ||
| 130 | , | ||
| 131 | $class, | ||
| 132 | )); | ||
| 133 | } | ||
| 134 | |||
| 135 | public static function invalidAssociation(ClassMetadata $targetClass, AssociationMapping $assoc, mixed $actualValue): self | ||
| 136 | { | ||
| 137 | $expectedType = $targetClass->getName(); | ||
| 138 | |||
| 139 | return new self(sprintf( | ||
| 140 | 'Expected value of type "%s" for association field "%s#$%s", got "%s" instead.', | ||
| 141 | $expectedType, | ||
| 142 | $assoc->sourceEntity, | ||
| 143 | $assoc->fieldName, | ||
| 144 | get_debug_type($actualValue), | ||
| 145 | )); | ||
| 146 | } | ||
| 147 | |||
| 148 | public static function invalidAutoGenerateMode(mixed $value): self | ||
| 149 | { | ||
| 150 | return new self(sprintf('Invalid auto generate mode "%s" given.', is_scalar($value) ? (string) $value : get_debug_type($value))); | ||
| 151 | } | ||
| 152 | |||
| 153 | public static function missingPrimaryKeyValue(string $className, string $idField): self | ||
| 154 | { | ||
| 155 | return new self(sprintf('Missing value for primary key %s on %s', $idField, $className)); | ||
| 156 | } | ||
| 157 | |||
| 158 | public static function proxyDirectoryRequired(): self | ||
| 159 | { | ||
| 160 | return new self('You must configure a proxy directory. See docs for details'); | ||
| 161 | } | ||
| 162 | |||
| 163 | public static function proxyNamespaceRequired(): self | ||
| 164 | { | ||
| 165 | return new self('You must configure a proxy namespace'); | ||
| 166 | } | ||
| 167 | |||
| 168 | public static function proxyDirectoryNotWritable(string $proxyDirectory): self | ||
| 169 | { | ||
| 170 | return new self(sprintf('Your proxy directory "%s" must be writable', $proxyDirectory)); | ||
| 171 | } | ||
| 172 | |||
| 173 | /** | ||
| 174 | * Helper method to show an object as string. | ||
| 175 | */ | ||
| 176 | private static function objToStr(object $obj): string | ||
| 177 | { | ||
| 178 | return $obj instanceof Stringable ? (string) $obj : get_debug_type($obj) . '@' . spl_object_id($obj); | ||
| 179 | } | ||
| 180 | |||
| 181 | private static function newEntityFoundThroughRelationshipMessage(AssociationMapping $associationMapping, object $entity): string | ||
| 182 | { | ||
| 183 | return 'A new entity was found through the relationship \'' | ||
| 184 | . $associationMapping->sourceEntity . '#' . $associationMapping->fieldName . '\' that was not' | ||
| 185 | . ' configured to cascade persist operations for entity: ' . self::objToStr($entity) . '.' | ||
| 186 | . ' To solve this issue: Either explicitly call EntityManager#persist()' | ||
| 187 | . ' on this unknown entity or configure cascade persist' | ||
| 188 | . ' this association in the mapping for example @ManyToOne(..,cascade={"persist"}).' | ||
| 189 | . ($entity instanceof Stringable | ||
| 190 | ? '' | ||
| 191 | : ' If you cannot find out which entity causes the problem implement \'' | ||
| 192 | . $associationMapping->targetEntity . '#__toString()\' to get a clue.' | ||
| 193 | ); | ||
| 194 | } | ||
| 195 | } | ||
diff --git a/vendor/doctrine/orm/src/ORMSetup.php b/vendor/doctrine/orm/src/ORMSetup.php new file mode 100644 index 0000000..7354c71 --- /dev/null +++ b/vendor/doctrine/orm/src/ORMSetup.php | |||
| @@ -0,0 +1,127 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM; | ||
| 6 | |||
| 7 | use Doctrine\ORM\Mapping\Driver\AttributeDriver; | ||
| 8 | use Doctrine\ORM\Mapping\Driver\XmlDriver; | ||
| 9 | use Psr\Cache\CacheItemPoolInterface; | ||
| 10 | use Redis; | ||
| 11 | use RuntimeException; | ||
| 12 | use Symfony\Component\Cache\Adapter\ApcuAdapter; | ||
| 13 | use Symfony\Component\Cache\Adapter\ArrayAdapter; | ||
| 14 | use Symfony\Component\Cache\Adapter\MemcachedAdapter; | ||
| 15 | use Symfony\Component\Cache\Adapter\RedisAdapter; | ||
| 16 | |||
| 17 | use function apcu_enabled; | ||
| 18 | use function class_exists; | ||
| 19 | use function extension_loaded; | ||
| 20 | use function md5; | ||
| 21 | use function sys_get_temp_dir; | ||
| 22 | |||
| 23 | final class ORMSetup | ||
| 24 | { | ||
| 25 | /** | ||
| 26 | * Creates a configuration with an attribute metadata driver. | ||
| 27 | * | ||
| 28 | * @param string[] $paths | ||
| 29 | */ | ||
| 30 | public static function createAttributeMetadataConfiguration( | ||
| 31 | array $paths, | ||
| 32 | bool $isDevMode = false, | ||
| 33 | string|null $proxyDir = null, | ||
| 34 | CacheItemPoolInterface|null $cache = null, | ||
| 35 | ): Configuration { | ||
| 36 | $config = self::createConfiguration($isDevMode, $proxyDir, $cache); | ||
| 37 | $config->setMetadataDriverImpl(new AttributeDriver($paths)); | ||
| 38 | |||
| 39 | return $config; | ||
| 40 | } | ||
| 41 | |||
| 42 | /** | ||
| 43 | * Creates a configuration with an XML metadata driver. | ||
| 44 | * | ||
| 45 | * @param string[] $paths | ||
| 46 | */ | ||
| 47 | public static function createXMLMetadataConfiguration( | ||
| 48 | array $paths, | ||
| 49 | bool $isDevMode = false, | ||
| 50 | string|null $proxyDir = null, | ||
| 51 | CacheItemPoolInterface|null $cache = null, | ||
| 52 | bool $isXsdValidationEnabled = true, | ||
| 53 | ): Configuration { | ||
| 54 | $config = self::createConfiguration($isDevMode, $proxyDir, $cache); | ||
| 55 | $config->setMetadataDriverImpl(new XmlDriver($paths, XmlDriver::DEFAULT_FILE_EXTENSION, $isXsdValidationEnabled)); | ||
| 56 | |||
| 57 | return $config; | ||
| 58 | } | ||
| 59 | |||
| 60 | /** | ||
| 61 | * Creates a configuration without a metadata driver. | ||
| 62 | */ | ||
| 63 | public static function createConfiguration( | ||
| 64 | bool $isDevMode = false, | ||
| 65 | string|null $proxyDir = null, | ||
| 66 | CacheItemPoolInterface|null $cache = null, | ||
| 67 | ): Configuration { | ||
| 68 | $proxyDir = $proxyDir ?: sys_get_temp_dir(); | ||
| 69 | |||
| 70 | $cache = self::createCacheInstance($isDevMode, $proxyDir, $cache); | ||
| 71 | |||
| 72 | $config = new Configuration(); | ||
| 73 | |||
| 74 | $config->setMetadataCache($cache); | ||
| 75 | $config->setQueryCache($cache); | ||
| 76 | $config->setResultCache($cache); | ||
| 77 | $config->setProxyDir($proxyDir); | ||
| 78 | $config->setProxyNamespace('DoctrineProxies'); | ||
| 79 | $config->setAutoGenerateProxyClasses($isDevMode); | ||
| 80 | |||
| 81 | return $config; | ||
| 82 | } | ||
| 83 | |||
| 84 | private static function createCacheInstance( | ||
| 85 | bool $isDevMode, | ||
| 86 | string $proxyDir, | ||
| 87 | CacheItemPoolInterface|null $cache, | ||
| 88 | ): CacheItemPoolInterface { | ||
| 89 | if ($cache !== null) { | ||
| 90 | return $cache; | ||
| 91 | } | ||
| 92 | |||
| 93 | if (! class_exists(ArrayAdapter::class)) { | ||
| 94 | throw new RuntimeException( | ||
| 95 | 'The Doctrine setup tool cannot configure caches without symfony/cache.' | ||
| 96 | . ' Please add symfony/cache as explicit dependency or pass your own cache implementation.', | ||
| 97 | ); | ||
| 98 | } | ||
| 99 | |||
| 100 | if ($isDevMode) { | ||
| 101 | return new ArrayAdapter(); | ||
| 102 | } | ||
| 103 | |||
| 104 | $namespace = 'dc2_' . md5($proxyDir); | ||
| 105 | |||
| 106 | if (extension_loaded('apcu') && apcu_enabled()) { | ||
| 107 | return new ApcuAdapter($namespace); | ||
| 108 | } | ||
| 109 | |||
| 110 | if (MemcachedAdapter::isSupported()) { | ||
| 111 | return new MemcachedAdapter(MemcachedAdapter::createConnection('memcached://127.0.0.1'), $namespace); | ||
| 112 | } | ||
| 113 | |||
| 114 | if (extension_loaded('redis')) { | ||
| 115 | $redis = new Redis(); | ||
| 116 | $redis->connect('127.0.0.1'); | ||
| 117 | |||
| 118 | return new RedisAdapter($redis, $namespace); | ||
| 119 | } | ||
| 120 | |||
| 121 | return new ArrayAdapter(); | ||
| 122 | } | ||
| 123 | |||
| 124 | private function __construct() | ||
| 125 | { | ||
| 126 | } | ||
| 127 | } | ||
diff --git a/vendor/doctrine/orm/src/OptimisticLockException.php b/vendor/doctrine/orm/src/OptimisticLockException.php new file mode 100644 index 0000000..f84e134 --- /dev/null +++ b/vendor/doctrine/orm/src/OptimisticLockException.php | |||
| @@ -0,0 +1,55 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM; | ||
| 6 | |||
| 7 | use DateTimeInterface; | ||
| 8 | use Doctrine\ORM\Exception\ORMException; | ||
| 9 | use Exception; | ||
| 10 | use Throwable; | ||
| 11 | |||
| 12 | /** | ||
| 13 | * An OptimisticLockException is thrown when a version check on an object | ||
| 14 | * that uses optimistic locking through a version field fails. | ||
| 15 | */ | ||
| 16 | class OptimisticLockException extends Exception implements ORMException | ||
| 17 | { | ||
| 18 | public function __construct( | ||
| 19 | string $msg, | ||
| 20 | private readonly object|string|null $entity, | ||
| 21 | Throwable|null $previous = null, | ||
| 22 | ) { | ||
| 23 | parent::__construct($msg, 0, $previous); | ||
| 24 | } | ||
| 25 | |||
| 26 | /** | ||
| 27 | * Gets the entity that caused the exception. | ||
| 28 | */ | ||
| 29 | public function getEntity(): object|string|null | ||
| 30 | { | ||
| 31 | return $this->entity; | ||
| 32 | } | ||
| 33 | |||
| 34 | /** @param object|class-string $entity */ | ||
| 35 | public static function lockFailed(object|string $entity): self | ||
| 36 | { | ||
| 37 | return new self('The optimistic lock on an entity failed.', $entity); | ||
| 38 | } | ||
| 39 | |||
| 40 | public static function lockFailedVersionMismatch( | ||
| 41 | object $entity, | ||
| 42 | int|string|DateTimeInterface $expectedLockVersion, | ||
| 43 | int|string|DateTimeInterface $actualLockVersion, | ||
| 44 | ): self { | ||
| 45 | $expectedLockVersion = $expectedLockVersion instanceof DateTimeInterface ? $expectedLockVersion->getTimestamp() : $expectedLockVersion; | ||
| 46 | $actualLockVersion = $actualLockVersion instanceof DateTimeInterface ? $actualLockVersion->getTimestamp() : $actualLockVersion; | ||
| 47 | |||
| 48 | return new self('The optimistic lock failed, version ' . $expectedLockVersion . ' was expected, but is actually ' . $actualLockVersion, $entity); | ||
| 49 | } | ||
| 50 | |||
| 51 | public static function notVersioned(string $entityName): self | ||
| 52 | { | ||
| 53 | return new self('Cannot obtain optimistic lock on unversioned entity ' . $entityName, null); | ||
| 54 | } | ||
| 55 | } | ||
diff --git a/vendor/doctrine/orm/src/PersistentCollection.php b/vendor/doctrine/orm/src/PersistentCollection.php new file mode 100644 index 0000000..d54d3d1 --- /dev/null +++ b/vendor/doctrine/orm/src/PersistentCollection.php | |||
| @@ -0,0 +1,652 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM; | ||
| 6 | |||
| 7 | use Doctrine\Common\Collections\AbstractLazyCollection; | ||
| 8 | use Doctrine\Common\Collections\ArrayCollection; | ||
| 9 | use Doctrine\Common\Collections\Collection; | ||
| 10 | use Doctrine\Common\Collections\Criteria; | ||
| 11 | use Doctrine\Common\Collections\Order; | ||
| 12 | use Doctrine\Common\Collections\Selectable; | ||
| 13 | use Doctrine\ORM\Mapping\AssociationMapping; | ||
| 14 | use Doctrine\ORM\Mapping\ClassMetadata; | ||
| 15 | use Doctrine\ORM\Mapping\ToManyAssociationMapping; | ||
| 16 | use RuntimeException; | ||
| 17 | use UnexpectedValueException; | ||
| 18 | |||
| 19 | use function array_combine; | ||
| 20 | use function array_diff_key; | ||
| 21 | use function array_map; | ||
| 22 | use function array_values; | ||
| 23 | use function array_walk; | ||
| 24 | use function assert; | ||
| 25 | use function is_object; | ||
| 26 | use function spl_object_id; | ||
| 27 | use function strtoupper; | ||
| 28 | |||
| 29 | /** | ||
| 30 | * A PersistentCollection represents a collection of elements that have persistent state. | ||
| 31 | * | ||
| 32 | * Collections of entities represent only the associations (links) to those entities. | ||
| 33 | * That means, if the collection is part of a many-many mapping and you remove | ||
| 34 | * entities from the collection, only the links in the relation table are removed (on flush). | ||
| 35 | * Similarly, if you remove entities from a collection that is part of a one-many | ||
| 36 | * mapping this will only result in the nulling out of the foreign keys on flush. | ||
| 37 | * | ||
| 38 | * @psalm-template TKey of array-key | ||
| 39 | * @psalm-template T | ||
| 40 | * @template-extends AbstractLazyCollection<TKey,T> | ||
| 41 | * @template-implements Selectable<TKey,T> | ||
| 42 | */ | ||
| 43 | final class PersistentCollection extends AbstractLazyCollection implements Selectable | ||
| 44 | { | ||
| 45 | /** | ||
| 46 | * A snapshot of the collection at the moment it was fetched from the database. | ||
| 47 | * This is used to create a diff of the collection at commit time. | ||
| 48 | * | ||
| 49 | * @psalm-var array<string|int, mixed> | ||
| 50 | */ | ||
| 51 | private array $snapshot = []; | ||
| 52 | |||
| 53 | /** | ||
| 54 | * The entity that owns this collection. | ||
| 55 | */ | ||
| 56 | private object|null $owner = null; | ||
| 57 | |||
| 58 | /** | ||
| 59 | * The association mapping the collection belongs to. | ||
| 60 | * This is currently either a OneToManyMapping or a ManyToManyMapping. | ||
| 61 | * | ||
| 62 | * @var (AssociationMapping&ToManyAssociationMapping)|null | ||
| 63 | */ | ||
| 64 | private AssociationMapping|null $association = null; | ||
| 65 | |||
| 66 | /** | ||
| 67 | * The name of the field on the target entities that points to the owner | ||
| 68 | * of the collection. This is only set if the association is bi-directional. | ||
| 69 | */ | ||
| 70 | private string|null $backRefFieldName = null; | ||
| 71 | |||
| 72 | /** | ||
| 73 | * Whether the collection is dirty and needs to be synchronized with the database | ||
| 74 | * when the UnitOfWork that manages its persistent state commits. | ||
| 75 | */ | ||
| 76 | private bool $isDirty = false; | ||
| 77 | |||
| 78 | /** | ||
| 79 | * Creates a new persistent collection. | ||
| 80 | * | ||
| 81 | * @param EntityManagerInterface $em The EntityManager the collection will be associated with. | ||
| 82 | * @param ClassMetadata $typeClass The class descriptor of the entity type of this collection. | ||
| 83 | * @psalm-param Collection<TKey, T>&Selectable<TKey, T> $collection The collection elements. | ||
| 84 | */ | ||
| 85 | public function __construct( | ||
| 86 | private EntityManagerInterface|null $em, | ||
| 87 | private readonly ClassMetadata|null $typeClass, | ||
| 88 | Collection $collection, | ||
| 89 | ) { | ||
| 90 | $this->collection = $collection; | ||
| 91 | $this->initialized = true; | ||
| 92 | } | ||
| 93 | |||
| 94 | /** | ||
| 95 | * INTERNAL: | ||
| 96 | * Sets the collection's owning entity together with the AssociationMapping that | ||
| 97 | * describes the association between the owner and the elements of the collection. | ||
| 98 | */ | ||
| 99 | public function setOwner(object $entity, AssociationMapping&ToManyAssociationMapping $assoc): void | ||
| 100 | { | ||
| 101 | $this->owner = $entity; | ||
| 102 | $this->association = $assoc; | ||
| 103 | $this->backRefFieldName = $assoc->isOwningSide() ? $assoc->inversedBy : $assoc->mappedBy; | ||
| 104 | } | ||
| 105 | |||
| 106 | /** | ||
| 107 | * INTERNAL: | ||
| 108 | * Gets the collection owner. | ||
| 109 | */ | ||
| 110 | public function getOwner(): object|null | ||
| 111 | { | ||
| 112 | return $this->owner; | ||
| 113 | } | ||
| 114 | |||
| 115 | public function getTypeClass(): ClassMetadata | ||
| 116 | { | ||
| 117 | assert($this->typeClass !== null); | ||
| 118 | |||
| 119 | return $this->typeClass; | ||
| 120 | } | ||
| 121 | |||
| 122 | private function getUnitOfWork(): UnitOfWork | ||
| 123 | { | ||
| 124 | assert($this->em !== null); | ||
| 125 | |||
| 126 | return $this->em->getUnitOfWork(); | ||
| 127 | } | ||
| 128 | |||
| 129 | /** | ||
| 130 | * INTERNAL: | ||
| 131 | * Adds an element to a collection during hydration. This will automatically | ||
| 132 | * complete bidirectional associations in the case of a one-to-many association. | ||
| 133 | */ | ||
| 134 | public function hydrateAdd(mixed $element): void | ||
| 135 | { | ||
| 136 | $this->unwrap()->add($element); | ||
| 137 | |||
| 138 | // If _backRefFieldName is set and its a one-to-many association, | ||
| 139 | // we need to set the back reference. | ||
| 140 | if ($this->backRefFieldName && $this->getMapping()->isOneToMany()) { | ||
| 141 | assert($this->typeClass !== null); | ||
| 142 | // Set back reference to owner | ||
| 143 | $this->typeClass->reflFields[$this->backRefFieldName]->setValue( | ||
| 144 | $element, | ||
| 145 | $this->owner, | ||
| 146 | ); | ||
| 147 | |||
| 148 | $this->getUnitOfWork()->setOriginalEntityProperty( | ||
| 149 | spl_object_id($element), | ||
| 150 | $this->backRefFieldName, | ||
| 151 | $this->owner, | ||
| 152 | ); | ||
| 153 | } | ||
| 154 | } | ||
| 155 | |||
| 156 | /** | ||
| 157 | * INTERNAL: | ||
| 158 | * Sets a keyed element in the collection during hydration. | ||
| 159 | */ | ||
| 160 | public function hydrateSet(mixed $key, mixed $element): void | ||
| 161 | { | ||
| 162 | $this->unwrap()->set($key, $element); | ||
| 163 | |||
| 164 | // If _backRefFieldName is set, then the association is bidirectional | ||
| 165 | // and we need to set the back reference. | ||
| 166 | if ($this->backRefFieldName && $this->getMapping()->isOneToMany()) { | ||
| 167 | assert($this->typeClass !== null); | ||
| 168 | // Set back reference to owner | ||
| 169 | $this->typeClass->reflFields[$this->backRefFieldName]->setValue( | ||
| 170 | $element, | ||
| 171 | $this->owner, | ||
| 172 | ); | ||
| 173 | } | ||
| 174 | } | ||
| 175 | |||
| 176 | /** | ||
| 177 | * Initializes the collection by loading its contents from the database | ||
| 178 | * if the collection is not yet initialized. | ||
| 179 | */ | ||
| 180 | public function initialize(): void | ||
| 181 | { | ||
| 182 | if ($this->initialized || ! $this->association) { | ||
| 183 | return; | ||
| 184 | } | ||
| 185 | |||
| 186 | $this->doInitialize(); | ||
| 187 | |||
| 188 | $this->initialized = true; | ||
| 189 | } | ||
| 190 | |||
| 191 | /** | ||
| 192 | * INTERNAL: | ||
| 193 | * Tells this collection to take a snapshot of its current state. | ||
| 194 | */ | ||
| 195 | public function takeSnapshot(): void | ||
| 196 | { | ||
| 197 | $this->snapshot = $this->unwrap()->toArray(); | ||
| 198 | $this->isDirty = false; | ||
| 199 | } | ||
| 200 | |||
| 201 | /** | ||
| 202 | * INTERNAL: | ||
| 203 | * Returns the last snapshot of the elements in the collection. | ||
| 204 | * | ||
| 205 | * @psalm-return array<string|int, mixed> The last snapshot of the elements. | ||
| 206 | */ | ||
| 207 | public function getSnapshot(): array | ||
| 208 | { | ||
| 209 | return $this->snapshot; | ||
| 210 | } | ||
| 211 | |||
| 212 | /** | ||
| 213 | * INTERNAL: | ||
| 214 | * getDeleteDiff | ||
| 215 | * | ||
| 216 | * @return mixed[] | ||
| 217 | */ | ||
| 218 | public function getDeleteDiff(): array | ||
| 219 | { | ||
| 220 | $collectionItems = $this->unwrap()->toArray(); | ||
| 221 | |||
| 222 | return array_values(array_diff_key( | ||
| 223 | array_combine(array_map('spl_object_id', $this->snapshot), $this->snapshot), | ||
| 224 | array_combine(array_map('spl_object_id', $collectionItems), $collectionItems), | ||
| 225 | )); | ||
| 226 | } | ||
| 227 | |||
| 228 | /** | ||
| 229 | * INTERNAL: | ||
| 230 | * getInsertDiff | ||
| 231 | * | ||
| 232 | * @return mixed[] | ||
| 233 | */ | ||
| 234 | public function getInsertDiff(): array | ||
| 235 | { | ||
| 236 | $collectionItems = $this->unwrap()->toArray(); | ||
| 237 | |||
| 238 | return array_values(array_diff_key( | ||
| 239 | array_combine(array_map('spl_object_id', $collectionItems), $collectionItems), | ||
| 240 | array_combine(array_map('spl_object_id', $this->snapshot), $this->snapshot), | ||
| 241 | )); | ||
| 242 | } | ||
| 243 | |||
| 244 | /** INTERNAL: Gets the association mapping of the collection. */ | ||
| 245 | public function getMapping(): AssociationMapping&ToManyAssociationMapping | ||
| 246 | { | ||
| 247 | if ($this->association === null) { | ||
| 248 | throw new UnexpectedValueException('The underlying association mapping is null although it should not be'); | ||
| 249 | } | ||
| 250 | |||
| 251 | return $this->association; | ||
| 252 | } | ||
| 253 | |||
| 254 | /** | ||
| 255 | * Marks this collection as changed/dirty. | ||
| 256 | */ | ||
| 257 | private function changed(): void | ||
| 258 | { | ||
| 259 | if ($this->isDirty) { | ||
| 260 | return; | ||
| 261 | } | ||
| 262 | |||
| 263 | $this->isDirty = true; | ||
| 264 | } | ||
| 265 | |||
| 266 | /** | ||
| 267 | * Gets a boolean flag indicating whether this collection is dirty which means | ||
| 268 | * its state needs to be synchronized with the database. | ||
| 269 | */ | ||
| 270 | public function isDirty(): bool | ||
| 271 | { | ||
| 272 | return $this->isDirty; | ||
| 273 | } | ||
| 274 | |||
| 275 | /** | ||
| 276 | * Sets a boolean flag, indicating whether this collection is dirty. | ||
| 277 | */ | ||
| 278 | public function setDirty(bool $dirty): void | ||
| 279 | { | ||
| 280 | $this->isDirty = $dirty; | ||
| 281 | } | ||
| 282 | |||
| 283 | /** | ||
| 284 | * Sets the initialized flag of the collection, forcing it into that state. | ||
| 285 | */ | ||
| 286 | public function setInitialized(bool $bool): void | ||
| 287 | { | ||
| 288 | $this->initialized = $bool; | ||
| 289 | } | ||
| 290 | |||
| 291 | public function remove(string|int $key): mixed | ||
| 292 | { | ||
| 293 | // TODO: If the keys are persistent as well (not yet implemented) | ||
| 294 | // and the collection is not initialized and orphanRemoval is | ||
| 295 | // not used we can issue a straight SQL delete/update on the | ||
| 296 | // association (table). Without initializing the collection. | ||
| 297 | $removed = parent::remove($key); | ||
| 298 | |||
| 299 | if (! $removed) { | ||
| 300 | return $removed; | ||
| 301 | } | ||
| 302 | |||
| 303 | $this->changed(); | ||
| 304 | |||
| 305 | if ( | ||
| 306 | $this->association !== null && | ||
| 307 | $this->association->isToMany() && | ||
| 308 | $this->owner && | ||
| 309 | $this->getMapping()->orphanRemoval | ||
| 310 | ) { | ||
| 311 | $this->getUnitOfWork()->scheduleOrphanRemoval($removed); | ||
| 312 | } | ||
| 313 | |||
| 314 | return $removed; | ||
| 315 | } | ||
| 316 | |||
| 317 | public function removeElement(mixed $element): bool | ||
| 318 | { | ||
| 319 | $removed = parent::removeElement($element); | ||
| 320 | |||
| 321 | if (! $removed) { | ||
| 322 | return $removed; | ||
| 323 | } | ||
| 324 | |||
| 325 | $this->changed(); | ||
| 326 | |||
| 327 | if ( | ||
| 328 | $this->association !== null && | ||
| 329 | $this->association->isToMany() && | ||
| 330 | $this->owner && | ||
| 331 | $this->getMapping()->orphanRemoval | ||
| 332 | ) { | ||
| 333 | $this->getUnitOfWork()->scheduleOrphanRemoval($element); | ||
| 334 | } | ||
| 335 | |||
| 336 | return $removed; | ||
| 337 | } | ||
| 338 | |||
| 339 | public function containsKey(mixed $key): bool | ||
| 340 | { | ||
| 341 | if ( | ||
| 342 | ! $this->initialized && $this->getMapping()->fetch === ClassMetadata::FETCH_EXTRA_LAZY | ||
| 343 | && isset($this->getMapping()->indexBy) | ||
| 344 | ) { | ||
| 345 | $persister = $this->getUnitOfWork()->getCollectionPersister($this->getMapping()); | ||
| 346 | |||
| 347 | return $this->unwrap()->containsKey($key) || $persister->containsKey($this, $key); | ||
| 348 | } | ||
| 349 | |||
| 350 | return parent::containsKey($key); | ||
| 351 | } | ||
| 352 | |||
| 353 | public function contains(mixed $element): bool | ||
| 354 | { | ||
| 355 | if (! $this->initialized && $this->getMapping()->fetch === ClassMetadata::FETCH_EXTRA_LAZY) { | ||
| 356 | $persister = $this->getUnitOfWork()->getCollectionPersister($this->getMapping()); | ||
| 357 | |||
| 358 | return $this->unwrap()->contains($element) || $persister->contains($this, $element); | ||
| 359 | } | ||
| 360 | |||
| 361 | return parent::contains($element); | ||
| 362 | } | ||
| 363 | |||
| 364 | public function get(string|int $key): mixed | ||
| 365 | { | ||
| 366 | if ( | ||
| 367 | ! $this->initialized | ||
| 368 | && $this->getMapping()->fetch === ClassMetadata::FETCH_EXTRA_LAZY | ||
| 369 | && isset($this->getMapping()->indexBy) | ||
| 370 | ) { | ||
| 371 | assert($this->em !== null); | ||
| 372 | assert($this->typeClass !== null); | ||
| 373 | if (! $this->typeClass->isIdentifierComposite && $this->typeClass->isIdentifier($this->getMapping()->indexBy)) { | ||
| 374 | return $this->em->find($this->typeClass->name, $key); | ||
| 375 | } | ||
| 376 | |||
| 377 | return $this->getUnitOfWork()->getCollectionPersister($this->getMapping())->get($this, $key); | ||
| 378 | } | ||
| 379 | |||
| 380 | return parent::get($key); | ||
| 381 | } | ||
| 382 | |||
| 383 | public function count(): int | ||
| 384 | { | ||
| 385 | if (! $this->initialized && $this->association !== null && $this->getMapping()->fetch === ClassMetadata::FETCH_EXTRA_LAZY) { | ||
| 386 | $persister = $this->getUnitOfWork()->getCollectionPersister($this->association); | ||
| 387 | |||
| 388 | return $persister->count($this) + ($this->isDirty ? $this->unwrap()->count() : 0); | ||
| 389 | } | ||
| 390 | |||
| 391 | return parent::count(); | ||
| 392 | } | ||
| 393 | |||
| 394 | public function set(string|int $key, mixed $value): void | ||
| 395 | { | ||
| 396 | parent::set($key, $value); | ||
| 397 | |||
| 398 | $this->changed(); | ||
| 399 | |||
| 400 | if (is_object($value) && $this->em) { | ||
| 401 | $this->getUnitOfWork()->cancelOrphanRemoval($value); | ||
| 402 | } | ||
| 403 | } | ||
| 404 | |||
| 405 | public function add(mixed $value): bool | ||
| 406 | { | ||
| 407 | $this->unwrap()->add($value); | ||
| 408 | |||
| 409 | $this->changed(); | ||
| 410 | |||
| 411 | if (is_object($value) && $this->em) { | ||
| 412 | $this->getUnitOfWork()->cancelOrphanRemoval($value); | ||
| 413 | } | ||
| 414 | |||
| 415 | return true; | ||
| 416 | } | ||
| 417 | |||
| 418 | public function offsetExists(mixed $offset): bool | ||
| 419 | { | ||
| 420 | return $this->containsKey($offset); | ||
| 421 | } | ||
| 422 | |||
| 423 | public function offsetGet(mixed $offset): mixed | ||
| 424 | { | ||
| 425 | return $this->get($offset); | ||
| 426 | } | ||
| 427 | |||
| 428 | public function offsetSet(mixed $offset, mixed $value): void | ||
| 429 | { | ||
| 430 | if (! isset($offset)) { | ||
| 431 | $this->add($value); | ||
| 432 | |||
| 433 | return; | ||
| 434 | } | ||
| 435 | |||
| 436 | $this->set($offset, $value); | ||
| 437 | } | ||
| 438 | |||
| 439 | public function offsetUnset(mixed $offset): void | ||
| 440 | { | ||
| 441 | $this->remove($offset); | ||
| 442 | } | ||
| 443 | |||
| 444 | public function isEmpty(): bool | ||
| 445 | { | ||
| 446 | return $this->unwrap()->isEmpty() && $this->count() === 0; | ||
| 447 | } | ||
| 448 | |||
| 449 | public function clear(): void | ||
| 450 | { | ||
| 451 | if ($this->initialized && $this->isEmpty()) { | ||
| 452 | $this->unwrap()->clear(); | ||
| 453 | |||
| 454 | return; | ||
| 455 | } | ||
| 456 | |||
| 457 | $uow = $this->getUnitOfWork(); | ||
| 458 | $association = $this->getMapping(); | ||
| 459 | |||
| 460 | if ( | ||
| 461 | $association->isToMany() && | ||
| 462 | $association->orphanRemoval && | ||
| 463 | $this->owner | ||
| 464 | ) { | ||
| 465 | // we need to initialize here, as orphan removal acts like implicit cascadeRemove, | ||
| 466 | // hence for event listeners we need the objects in memory. | ||
| 467 | $this->initialize(); | ||
| 468 | |||
| 469 | foreach ($this->unwrap() as $element) { | ||
| 470 | $uow->scheduleOrphanRemoval($element); | ||
| 471 | } | ||
| 472 | } | ||
| 473 | |||
| 474 | $this->unwrap()->clear(); | ||
| 475 | |||
| 476 | $this->initialized = true; // direct call, {@link initialize()} is too expensive | ||
| 477 | |||
| 478 | if ($association->isOwningSide() && $this->owner) { | ||
| 479 | $this->changed(); | ||
| 480 | |||
| 481 | $uow->scheduleCollectionDeletion($this); | ||
| 482 | |||
| 483 | $this->takeSnapshot(); | ||
| 484 | } | ||
| 485 | } | ||
| 486 | |||
| 487 | /** | ||
| 488 | * Called by PHP when this collection is serialized. Ensures that only the | ||
| 489 | * elements are properly serialized. | ||
| 490 | * | ||
| 491 | * Internal note: Tried to implement Serializable first but that did not work well | ||
| 492 | * with circular references. This solution seems simpler and works well. | ||
| 493 | * | ||
| 494 | * @return string[] | ||
| 495 | * @psalm-return array{0: string, 1: string} | ||
| 496 | */ | ||
| 497 | public function __sleep(): array | ||
| 498 | { | ||
| 499 | return ['collection', 'initialized']; | ||
| 500 | } | ||
| 501 | |||
| 502 | public function __wakeup(): void | ||
| 503 | { | ||
| 504 | $this->em = null; | ||
| 505 | } | ||
| 506 | |||
| 507 | /** | ||
| 508 | * Extracts a slice of $length elements starting at position $offset from the Collection. | ||
| 509 | * | ||
| 510 | * If $length is null it returns all elements from $offset to the end of the Collection. | ||
| 511 | * Keys have to be preserved by this method. Calling this method will only return the | ||
| 512 | * selected slice and NOT change the elements contained in the collection slice is called on. | ||
| 513 | * | ||
| 514 | * @return mixed[] | ||
| 515 | * @psalm-return array<TKey,T> | ||
| 516 | */ | ||
| 517 | public function slice(int $offset, int|null $length = null): array | ||
| 518 | { | ||
| 519 | if (! $this->initialized && ! $this->isDirty && $this->getMapping()->fetch === ClassMetadata::FETCH_EXTRA_LAZY) { | ||
| 520 | $persister = $this->getUnitOfWork()->getCollectionPersister($this->getMapping()); | ||
| 521 | |||
| 522 | return $persister->slice($this, $offset, $length); | ||
| 523 | } | ||
| 524 | |||
| 525 | return parent::slice($offset, $length); | ||
| 526 | } | ||
| 527 | |||
| 528 | /** | ||
| 529 | * Cleans up internal state of cloned persistent collection. | ||
| 530 | * | ||
| 531 | * The following problems have to be prevented: | ||
| 532 | * 1. Added entities are added to old PC | ||
| 533 | * 2. New collection is not dirty, if reused on other entity nothing | ||
| 534 | * changes. | ||
| 535 | * 3. Snapshot leads to invalid diffs being generated. | ||
| 536 | * 4. Lazy loading grabs entities from old owner object. | ||
| 537 | * 5. New collection is connected to old owner and leads to duplicate keys. | ||
| 538 | */ | ||
| 539 | public function __clone() | ||
| 540 | { | ||
| 541 | if (is_object($this->collection)) { | ||
| 542 | $this->collection = clone $this->collection; | ||
| 543 | } | ||
| 544 | |||
| 545 | $this->initialize(); | ||
| 546 | |||
| 547 | $this->owner = null; | ||
| 548 | $this->snapshot = []; | ||
| 549 | |||
| 550 | $this->changed(); | ||
| 551 | } | ||
| 552 | |||
| 553 | /** | ||
| 554 | * Selects all elements from a selectable that match the expression and | ||
| 555 | * return a new collection containing these elements. | ||
| 556 | * | ||
| 557 | * @psalm-return Collection<TKey, T> | ||
| 558 | * | ||
| 559 | * @throws RuntimeException | ||
| 560 | */ | ||
| 561 | public function matching(Criteria $criteria): Collection | ||
| 562 | { | ||
| 563 | if ($this->isDirty) { | ||
| 564 | $this->initialize(); | ||
| 565 | } | ||
| 566 | |||
| 567 | if ($this->initialized) { | ||
| 568 | return $this->unwrap()->matching($criteria); | ||
| 569 | } | ||
| 570 | |||
| 571 | $association = $this->getMapping(); | ||
| 572 | if ($association->isManyToMany()) { | ||
| 573 | $persister = $this->getUnitOfWork()->getCollectionPersister($association); | ||
| 574 | |||
| 575 | return new ArrayCollection($persister->loadCriteria($this, $criteria)); | ||
| 576 | } | ||
| 577 | |||
| 578 | $builder = Criteria::expr(); | ||
| 579 | $ownerExpression = $builder->eq($this->backRefFieldName, $this->owner); | ||
| 580 | $expression = $criteria->getWhereExpression(); | ||
| 581 | $expression = $expression ? $builder->andX($expression, $ownerExpression) : $ownerExpression; | ||
| 582 | |||
| 583 | $criteria = clone $criteria; | ||
| 584 | $criteria->where($expression); | ||
| 585 | $criteria->orderBy( | ||
| 586 | $criteria->orderings() ?: array_map( | ||
| 587 | static fn (string $order): Order => Order::from(strtoupper($order)), | ||
| 588 | $association->orderBy(), | ||
| 589 | ), | ||
| 590 | ); | ||
| 591 | |||
| 592 | $persister = $this->getUnitOfWork()->getEntityPersister($association->targetEntity); | ||
| 593 | |||
| 594 | return $association->fetch === ClassMetadata::FETCH_EXTRA_LAZY | ||
| 595 | ? new LazyCriteriaCollection($persister, $criteria) | ||
| 596 | : new ArrayCollection($persister->loadCriteria($criteria)); | ||
| 597 | } | ||
| 598 | |||
| 599 | /** | ||
| 600 | * Retrieves the wrapped Collection instance. | ||
| 601 | * | ||
| 602 | * @return Collection<TKey, T>&Selectable<TKey, T> | ||
| 603 | */ | ||
| 604 | public function unwrap(): Selectable&Collection | ||
| 605 | { | ||
| 606 | assert($this->collection instanceof Collection); | ||
| 607 | assert($this->collection instanceof Selectable); | ||
| 608 | |||
| 609 | return $this->collection; | ||
| 610 | } | ||
| 611 | |||
| 612 | protected function doInitialize(): void | ||
| 613 | { | ||
| 614 | // Has NEW objects added through add(). Remember them. | ||
| 615 | $newlyAddedDirtyObjects = []; | ||
| 616 | |||
| 617 | if ($this->isDirty) { | ||
| 618 | $newlyAddedDirtyObjects = $this->unwrap()->toArray(); | ||
| 619 | } | ||
| 620 | |||
| 621 | $this->unwrap()->clear(); | ||
| 622 | $this->getUnitOfWork()->loadCollection($this); | ||
| 623 | $this->takeSnapshot(); | ||
| 624 | |||
| 625 | if ($newlyAddedDirtyObjects) { | ||
| 626 | $this->restoreNewObjectsInDirtyCollection($newlyAddedDirtyObjects); | ||
| 627 | } | ||
| 628 | } | ||
| 629 | |||
| 630 | /** | ||
| 631 | * @param object[] $newObjects | ||
| 632 | * | ||
| 633 | * Note: the only reason why this entire looping/complexity is performed via `spl_object_id` | ||
| 634 | * is because we want to prevent using `array_udiff()`, which is likely to cause very | ||
| 635 | * high overhead (complexity of O(n^2)). `array_diff_key()` performs the operation in | ||
| 636 | * core, which is faster than using a callback for comparisons | ||
| 637 | */ | ||
| 638 | private function restoreNewObjectsInDirtyCollection(array $newObjects): void | ||
| 639 | { | ||
| 640 | $loadedObjects = $this->unwrap()->toArray(); | ||
| 641 | $newObjectsByOid = array_combine(array_map('spl_object_id', $newObjects), $newObjects); | ||
| 642 | $loadedObjectsByOid = array_combine(array_map('spl_object_id', $loadedObjects), $loadedObjects); | ||
| 643 | $newObjectsThatWereNotLoaded = array_diff_key($newObjectsByOid, $loadedObjectsByOid); | ||
| 644 | |||
| 645 | if ($newObjectsThatWereNotLoaded) { | ||
| 646 | // Reattach NEW objects added through add(), if any. | ||
| 647 | array_walk($newObjectsThatWereNotLoaded, [$this->unwrap(), 'add']); | ||
| 648 | |||
| 649 | $this->isDirty = true; | ||
| 650 | } | ||
| 651 | } | ||
| 652 | } | ||
diff --git a/vendor/doctrine/orm/src/Persisters/Collection/AbstractCollectionPersister.php b/vendor/doctrine/orm/src/Persisters/Collection/AbstractCollectionPersister.php new file mode 100644 index 0000000..26f0b9e --- /dev/null +++ b/vendor/doctrine/orm/src/Persisters/Collection/AbstractCollectionPersister.php | |||
| @@ -0,0 +1,50 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Persisters\Collection; | ||
| 6 | |||
| 7 | use Doctrine\DBAL\Connection; | ||
| 8 | use Doctrine\DBAL\Platforms\AbstractPlatform; | ||
| 9 | use Doctrine\ORM\EntityManagerInterface; | ||
| 10 | use Doctrine\ORM\Mapping\QuoteStrategy; | ||
| 11 | use Doctrine\ORM\UnitOfWork; | ||
| 12 | |||
| 13 | /** | ||
| 14 | * Base class for all collection persisters. | ||
| 15 | */ | ||
| 16 | abstract class AbstractCollectionPersister implements CollectionPersister | ||
| 17 | { | ||
| 18 | protected Connection $conn; | ||
| 19 | protected UnitOfWork $uow; | ||
| 20 | protected AbstractPlatform $platform; | ||
| 21 | protected QuoteStrategy $quoteStrategy; | ||
| 22 | |||
| 23 | /** | ||
| 24 | * Initializes a new instance of a class derived from AbstractCollectionPersister. | ||
| 25 | */ | ||
| 26 | public function __construct( | ||
| 27 | protected EntityManagerInterface $em, | ||
| 28 | ) { | ||
| 29 | $this->uow = $em->getUnitOfWork(); | ||
| 30 | $this->conn = $em->getConnection(); | ||
| 31 | $this->platform = $this->conn->getDatabasePlatform(); | ||
| 32 | $this->quoteStrategy = $em->getConfiguration()->getQuoteStrategy(); | ||
| 33 | } | ||
| 34 | |||
| 35 | /** | ||
| 36 | * Check if entity is in a valid state for operations. | ||
| 37 | */ | ||
| 38 | protected function isValidEntityState(object $entity): bool | ||
| 39 | { | ||
| 40 | $entityState = $this->uow->getEntityState($entity, UnitOfWork::STATE_NEW); | ||
| 41 | |||
| 42 | if ($entityState === UnitOfWork::STATE_NEW) { | ||
| 43 | return false; | ||
| 44 | } | ||
| 45 | |||
| 46 | // If Entity is scheduled for inclusion, it is not in this collection. | ||
| 47 | // We can assure that because it would have return true before on array check | ||
| 48 | return ! ($entityState === UnitOfWork::STATE_MANAGED && $this->uow->isScheduledForInsert($entity)); | ||
| 49 | } | ||
| 50 | } | ||
diff --git a/vendor/doctrine/orm/src/Persisters/Collection/CollectionPersister.php b/vendor/doctrine/orm/src/Persisters/Collection/CollectionPersister.php new file mode 100644 index 0000000..07c4eaf --- /dev/null +++ b/vendor/doctrine/orm/src/Persisters/Collection/CollectionPersister.php | |||
| @@ -0,0 +1,59 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Persisters\Collection; | ||
| 6 | |||
| 7 | use Doctrine\Common\Collections\Criteria; | ||
| 8 | use Doctrine\ORM\PersistentCollection; | ||
| 9 | |||
| 10 | /** | ||
| 11 | * Define the behavior that should be implemented by all collection persisters. | ||
| 12 | */ | ||
| 13 | interface CollectionPersister | ||
| 14 | { | ||
| 15 | /** | ||
| 16 | * Deletes the persistent state represented by the given collection. | ||
| 17 | */ | ||
| 18 | public function delete(PersistentCollection $collection): void; | ||
| 19 | |||
| 20 | /** | ||
| 21 | * Updates the given collection, synchronizing its state with the database | ||
| 22 | * by inserting, updating and deleting individual elements. | ||
| 23 | */ | ||
| 24 | public function update(PersistentCollection $collection): void; | ||
| 25 | |||
| 26 | /** | ||
| 27 | * Counts the size of this persistent collection. | ||
| 28 | */ | ||
| 29 | public function count(PersistentCollection $collection): int; | ||
| 30 | |||
| 31 | /** | ||
| 32 | * Slices elements. | ||
| 33 | * | ||
| 34 | * @return mixed[] | ||
| 35 | */ | ||
| 36 | public function slice(PersistentCollection $collection, int $offset, int|null $length = null): array; | ||
| 37 | |||
| 38 | /** | ||
| 39 | * Checks for existence of an element. | ||
| 40 | */ | ||
| 41 | public function contains(PersistentCollection $collection, object $element): bool; | ||
| 42 | |||
| 43 | /** | ||
| 44 | * Checks for existence of a key. | ||
| 45 | */ | ||
| 46 | public function containsKey(PersistentCollection $collection, mixed $key): bool; | ||
| 47 | |||
| 48 | /** | ||
| 49 | * Gets an element by key. | ||
| 50 | */ | ||
| 51 | public function get(PersistentCollection $collection, mixed $index): mixed; | ||
| 52 | |||
| 53 | /** | ||
| 54 | * Loads association entities matching the given Criteria object. | ||
| 55 | * | ||
| 56 | * @return mixed[] | ||
| 57 | */ | ||
| 58 | public function loadCriteria(PersistentCollection $collection, Criteria $criteria): array; | ||
| 59 | } | ||
diff --git a/vendor/doctrine/orm/src/Persisters/Collection/ManyToManyPersister.php b/vendor/doctrine/orm/src/Persisters/Collection/ManyToManyPersister.php new file mode 100644 index 0000000..7cf993d --- /dev/null +++ b/vendor/doctrine/orm/src/Persisters/Collection/ManyToManyPersister.php | |||
| @@ -0,0 +1,770 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Persisters\Collection; | ||
| 6 | |||
| 7 | use BadMethodCallException; | ||
| 8 | use Doctrine\Common\Collections\Criteria; | ||
| 9 | use Doctrine\Common\Collections\Expr\Comparison; | ||
| 10 | use Doctrine\DBAL\Exception as DBALException; | ||
| 11 | use Doctrine\DBAL\LockMode; | ||
| 12 | use Doctrine\ORM\Mapping\AssociationMapping; | ||
| 13 | use Doctrine\ORM\Mapping\ClassMetadata; | ||
| 14 | use Doctrine\ORM\Mapping\InverseSideMapping; | ||
| 15 | use Doctrine\ORM\Mapping\ManyToManyAssociationMapping; | ||
| 16 | use Doctrine\ORM\PersistentCollection; | ||
| 17 | use Doctrine\ORM\Persisters\SqlValueVisitor; | ||
| 18 | use Doctrine\ORM\Query; | ||
| 19 | use Doctrine\ORM\Utility\PersisterHelper; | ||
| 20 | |||
| 21 | use function array_fill; | ||
| 22 | use function array_pop; | ||
| 23 | use function assert; | ||
| 24 | use function count; | ||
| 25 | use function implode; | ||
| 26 | use function in_array; | ||
| 27 | use function reset; | ||
| 28 | use function sprintf; | ||
| 29 | |||
| 30 | /** | ||
| 31 | * Persister for many-to-many collections. | ||
| 32 | */ | ||
| 33 | class ManyToManyPersister extends AbstractCollectionPersister | ||
| 34 | { | ||
| 35 | public function delete(PersistentCollection $collection): void | ||
| 36 | { | ||
| 37 | $mapping = $this->getMapping($collection); | ||
| 38 | |||
| 39 | if (! $mapping->isOwningSide()) { | ||
| 40 | return; // ignore inverse side | ||
| 41 | } | ||
| 42 | |||
| 43 | assert($mapping->isManyToManyOwningSide()); | ||
| 44 | |||
| 45 | $types = []; | ||
| 46 | $class = $this->em->getClassMetadata($mapping->sourceEntity); | ||
| 47 | |||
| 48 | foreach ($mapping->joinTable->joinColumns as $joinColumn) { | ||
| 49 | $types[] = PersisterHelper::getTypeOfColumn($joinColumn->referencedColumnName, $class, $this->em); | ||
| 50 | } | ||
| 51 | |||
| 52 | $this->conn->executeStatement($this->getDeleteSQL($collection), $this->getDeleteSQLParameters($collection), $types); | ||
| 53 | } | ||
| 54 | |||
| 55 | public function update(PersistentCollection $collection): void | ||
| 56 | { | ||
| 57 | $mapping = $this->getMapping($collection); | ||
| 58 | |||
| 59 | if (! $mapping->isOwningSide()) { | ||
| 60 | return; // ignore inverse side | ||
| 61 | } | ||
| 62 | |||
| 63 | [$deleteSql, $deleteTypes] = $this->getDeleteRowSQL($collection); | ||
| 64 | [$insertSql, $insertTypes] = $this->getInsertRowSQL($collection); | ||
| 65 | |||
| 66 | foreach ($collection->getDeleteDiff() as $element) { | ||
| 67 | $this->conn->executeStatement( | ||
| 68 | $deleteSql, | ||
| 69 | $this->getDeleteRowSQLParameters($collection, $element), | ||
| 70 | $deleteTypes, | ||
| 71 | ); | ||
| 72 | } | ||
| 73 | |||
| 74 | foreach ($collection->getInsertDiff() as $element) { | ||
| 75 | $this->conn->executeStatement( | ||
| 76 | $insertSql, | ||
| 77 | $this->getInsertRowSQLParameters($collection, $element), | ||
| 78 | $insertTypes, | ||
| 79 | ); | ||
| 80 | } | ||
| 81 | } | ||
| 82 | |||
| 83 | public function get(PersistentCollection $collection, mixed $index): object|null | ||
| 84 | { | ||
| 85 | $mapping = $this->getMapping($collection); | ||
| 86 | |||
| 87 | if (! $mapping->isIndexed()) { | ||
| 88 | throw new BadMethodCallException('Selecting a collection by index is only supported on indexed collections.'); | ||
| 89 | } | ||
| 90 | |||
| 91 | $persister = $this->uow->getEntityPersister($mapping->targetEntity); | ||
| 92 | $mappedKey = $mapping->isOwningSide() | ||
| 93 | ? $mapping->inversedBy | ||
| 94 | : $mapping->mappedBy; | ||
| 95 | |||
| 96 | assert($mappedKey !== null); | ||
| 97 | |||
| 98 | return $persister->load( | ||
| 99 | [$mappedKey => $collection->getOwner(), $mapping->indexBy() => $index], | ||
| 100 | null, | ||
| 101 | $mapping, | ||
| 102 | [], | ||
| 103 | LockMode::NONE, | ||
| 104 | 1, | ||
| 105 | ); | ||
| 106 | } | ||
| 107 | |||
| 108 | public function count(PersistentCollection $collection): int | ||
| 109 | { | ||
| 110 | $conditions = []; | ||
| 111 | $params = []; | ||
| 112 | $types = []; | ||
| 113 | $mapping = $this->getMapping($collection); | ||
| 114 | $id = $this->uow->getEntityIdentifier($collection->getOwner()); | ||
| 115 | $sourceClass = $this->em->getClassMetadata($mapping->sourceEntity); | ||
| 116 | $association = $this->em->getMetadataFactory()->getOwningSide($mapping); | ||
| 117 | |||
| 118 | $joinTableName = $this->quoteStrategy->getJoinTableName($association, $sourceClass, $this->platform); | ||
| 119 | $joinColumns = ! $mapping->isOwningSide() | ||
| 120 | ? $association->joinTable->inverseJoinColumns | ||
| 121 | : $association->joinTable->joinColumns; | ||
| 122 | |||
| 123 | foreach ($joinColumns as $joinColumn) { | ||
| 124 | $columnName = $this->quoteStrategy->getJoinColumnName($joinColumn, $sourceClass, $this->platform); | ||
| 125 | $referencedName = $joinColumn->referencedColumnName; | ||
| 126 | $conditions[] = 't.' . $columnName . ' = ?'; | ||
| 127 | $params[] = $id[$sourceClass->getFieldForColumn($referencedName)]; | ||
| 128 | $types[] = PersisterHelper::getTypeOfColumn($referencedName, $sourceClass, $this->em); | ||
| 129 | } | ||
| 130 | |||
| 131 | [$joinTargetEntitySQL, $filterSql] = $this->getFilterSql($mapping); | ||
| 132 | |||
| 133 | if ($filterSql) { | ||
| 134 | $conditions[] = $filterSql; | ||
| 135 | } | ||
| 136 | |||
| 137 | // If there is a provided criteria, make part of conditions | ||
| 138 | // @todo Fix this. Current SQL returns something like: | ||
| 139 | /*if ($criteria && ($expression = $criteria->getWhereExpression()) !== null) { | ||
| 140 | // A join is needed on the target entity | ||
| 141 | $targetTableName = $this->quoteStrategy->getTableName($targetClass, $this->platform); | ||
| 142 | $targetJoinSql = ' JOIN ' . $targetTableName . ' te' | ||
| 143 | . ' ON' . implode(' AND ', $this->getOnConditionSQL($association)); | ||
| 144 | |||
| 145 | // And criteria conditions needs to be added | ||
| 146 | $persister = $this->uow->getEntityPersister($targetClass->name); | ||
| 147 | $visitor = new SqlExpressionVisitor($persister, $targetClass); | ||
| 148 | $conditions[] = $visitor->dispatch($expression); | ||
| 149 | |||
| 150 | $joinTargetEntitySQL = $targetJoinSql . $joinTargetEntitySQL; | ||
| 151 | }*/ | ||
| 152 | |||
| 153 | $sql = 'SELECT COUNT(*)' | ||
| 154 | . ' FROM ' . $joinTableName . ' t' | ||
| 155 | . $joinTargetEntitySQL | ||
| 156 | . ' WHERE ' . implode(' AND ', $conditions); | ||
| 157 | |||
| 158 | return (int) $this->conn->fetchOne($sql, $params, $types); | ||
| 159 | } | ||
| 160 | |||
| 161 | /** | ||
| 162 | * {@inheritDoc} | ||
| 163 | */ | ||
| 164 | public function slice(PersistentCollection $collection, int $offset, int|null $length = null): array | ||
| 165 | { | ||
| 166 | $mapping = $this->getMapping($collection); | ||
| 167 | $persister = $this->uow->getEntityPersister($mapping->targetEntity); | ||
| 168 | |||
| 169 | return $persister->getManyToManyCollection($mapping, $collection->getOwner(), $offset, $length); | ||
| 170 | } | ||
| 171 | |||
| 172 | public function containsKey(PersistentCollection $collection, mixed $key): bool | ||
| 173 | { | ||
| 174 | $mapping = $this->getMapping($collection); | ||
| 175 | |||
| 176 | if (! $mapping->isIndexed()) { | ||
| 177 | throw new BadMethodCallException('Selecting a collection by index is only supported on indexed collections.'); | ||
| 178 | } | ||
| 179 | |||
| 180 | [$quotedJoinTable, $whereClauses, $params, $types] = $this->getJoinTableRestrictionsWithKey( | ||
| 181 | $collection, | ||
| 182 | (string) $key, | ||
| 183 | true, | ||
| 184 | ); | ||
| 185 | |||
| 186 | $sql = 'SELECT 1 FROM ' . $quotedJoinTable . ' WHERE ' . implode(' AND ', $whereClauses); | ||
| 187 | |||
| 188 | return (bool) $this->conn->fetchOne($sql, $params, $types); | ||
| 189 | } | ||
| 190 | |||
| 191 | public function contains(PersistentCollection $collection, object $element): bool | ||
| 192 | { | ||
| 193 | if (! $this->isValidEntityState($element)) { | ||
| 194 | return false; | ||
| 195 | } | ||
| 196 | |||
| 197 | [$quotedJoinTable, $whereClauses, $params, $types] = $this->getJoinTableRestrictions( | ||
| 198 | $collection, | ||
| 199 | $element, | ||
| 200 | true, | ||
| 201 | ); | ||
| 202 | |||
| 203 | $sql = 'SELECT 1 FROM ' . $quotedJoinTable . ' WHERE ' . implode(' AND ', $whereClauses); | ||
| 204 | |||
| 205 | return (bool) $this->conn->fetchOne($sql, $params, $types); | ||
| 206 | } | ||
| 207 | |||
| 208 | /** | ||
| 209 | * {@inheritDoc} | ||
| 210 | */ | ||
| 211 | public function loadCriteria(PersistentCollection $collection, Criteria $criteria): array | ||
| 212 | { | ||
| 213 | $mapping = $this->getMapping($collection); | ||
| 214 | $owner = $collection->getOwner(); | ||
| 215 | $ownerMetadata = $this->em->getClassMetadata($owner::class); | ||
| 216 | $id = $this->uow->getEntityIdentifier($owner); | ||
| 217 | $targetClass = $this->em->getClassMetadata($mapping->targetEntity); | ||
| 218 | $onConditions = $this->getOnConditionSQL($mapping); | ||
| 219 | $whereClauses = $params = []; | ||
| 220 | $paramTypes = []; | ||
| 221 | |||
| 222 | if (! $mapping->isOwningSide()) { | ||
| 223 | assert($mapping instanceof InverseSideMapping); | ||
| 224 | $associationSourceClass = $targetClass; | ||
| 225 | $sourceRelationMode = 'relationToTargetKeyColumns'; | ||
| 226 | } else { | ||
| 227 | $associationSourceClass = $ownerMetadata; | ||
| 228 | $sourceRelationMode = 'relationToSourceKeyColumns'; | ||
| 229 | } | ||
| 230 | |||
| 231 | $mapping = $this->em->getMetadataFactory()->getOwningSide($mapping); | ||
| 232 | |||
| 233 | foreach ($mapping->$sourceRelationMode as $key => $value) { | ||
| 234 | $whereClauses[] = sprintf('t.%s = ?', $key); | ||
| 235 | $params[] = $ownerMetadata->containsForeignIdentifier | ||
| 236 | ? $id[$ownerMetadata->getFieldForColumn($value)] | ||
| 237 | : $id[$ownerMetadata->fieldNames[$value]]; | ||
| 238 | $paramTypes[] = PersisterHelper::getTypeOfColumn($value, $ownerMetadata, $this->em); | ||
| 239 | } | ||
| 240 | |||
| 241 | $parameters = $this->expandCriteriaParameters($criteria); | ||
| 242 | |||
| 243 | foreach ($parameters as $parameter) { | ||
| 244 | [$name, $value, $operator] = $parameter; | ||
| 245 | |||
| 246 | $field = $this->quoteStrategy->getColumnName($name, $targetClass, $this->platform); | ||
| 247 | |||
| 248 | if ($value === null && ($operator === Comparison::EQ || $operator === Comparison::NEQ)) { | ||
| 249 | $whereClauses[] = sprintf('te.%s %s NULL', $field, $operator === Comparison::EQ ? 'IS' : 'IS NOT'); | ||
| 250 | } else { | ||
| 251 | $whereClauses[] = sprintf('te.%s %s ?', $field, $operator); | ||
| 252 | $params[] = $value; | ||
| 253 | $paramTypes[] = PersisterHelper::getTypeOfField($name, $targetClass, $this->em)[0]; | ||
| 254 | } | ||
| 255 | } | ||
| 256 | |||
| 257 | $tableName = $this->quoteStrategy->getTableName($targetClass, $this->platform); | ||
| 258 | $joinTable = $this->quoteStrategy->getJoinTableName($mapping, $associationSourceClass, $this->platform); | ||
| 259 | |||
| 260 | $rsm = new Query\ResultSetMappingBuilder($this->em); | ||
| 261 | $rsm->addRootEntityFromClassMetadata($targetClass->name, 'te'); | ||
| 262 | |||
| 263 | $sql = 'SELECT ' . $rsm->generateSelectClause() | ||
| 264 | . ' FROM ' . $tableName . ' te' | ||
| 265 | . ' JOIN ' . $joinTable . ' t ON' | ||
| 266 | . implode(' AND ', $onConditions) | ||
| 267 | . ' WHERE ' . implode(' AND ', $whereClauses); | ||
| 268 | |||
| 269 | $sql .= $this->getOrderingSql($criteria, $targetClass); | ||
| 270 | |||
| 271 | $sql .= $this->getLimitSql($criteria); | ||
| 272 | |||
| 273 | $stmt = $this->conn->executeQuery($sql, $params, $paramTypes); | ||
| 274 | |||
| 275 | return $this | ||
| 276 | ->em | ||
| 277 | ->newHydrator(Query::HYDRATE_OBJECT) | ||
| 278 | ->hydrateAll($stmt, $rsm); | ||
| 279 | } | ||
| 280 | |||
| 281 | /** | ||
| 282 | * Generates the filter SQL for a given mapping. | ||
| 283 | * | ||
| 284 | * This method is not used for actually grabbing the related entities | ||
| 285 | * but when the extra-lazy collection methods are called on a filtered | ||
| 286 | * association. This is why besides the many to many table we also | ||
| 287 | * have to join in the actual entities table leading to additional | ||
| 288 | * JOIN. | ||
| 289 | * | ||
| 290 | * @param AssociationMapping $mapping Array containing mapping information. | ||
| 291 | * | ||
| 292 | * @return string[] ordered tuple: | ||
| 293 | * - JOIN condition to add to the SQL | ||
| 294 | * - WHERE condition to add to the SQL | ||
| 295 | * @psalm-return array{0: string, 1: string} | ||
| 296 | */ | ||
| 297 | public function getFilterSql(AssociationMapping $mapping): array | ||
| 298 | { | ||
| 299 | $targetClass = $this->em->getClassMetadata($mapping->targetEntity); | ||
| 300 | $rootClass = $this->em->getClassMetadata($targetClass->rootEntityName); | ||
| 301 | $filterSql = $this->generateFilterConditionSQL($rootClass, 'te'); | ||
| 302 | |||
| 303 | if ($filterSql === '') { | ||
| 304 | return ['', '']; | ||
| 305 | } | ||
| 306 | |||
| 307 | // A join is needed if there is filtering on the target entity | ||
| 308 | $tableName = $this->quoteStrategy->getTableName($rootClass, $this->platform); | ||
| 309 | $joinSql = ' JOIN ' . $tableName . ' te' | ||
| 310 | . ' ON' . implode(' AND ', $this->getOnConditionSQL($mapping)); | ||
| 311 | |||
| 312 | return [$joinSql, $filterSql]; | ||
| 313 | } | ||
| 314 | |||
| 315 | /** | ||
| 316 | * Generates the filter SQL for a given entity and table alias. | ||
| 317 | * | ||
| 318 | * @param ClassMetadata $targetEntity Metadata of the target entity. | ||
| 319 | * @param string $targetTableAlias The table alias of the joined/selected table. | ||
| 320 | * | ||
| 321 | * @return string The SQL query part to add to a query. | ||
| 322 | */ | ||
| 323 | protected function generateFilterConditionSQL(ClassMetadata $targetEntity, string $targetTableAlias): string | ||
| 324 | { | ||
| 325 | $filterClauses = []; | ||
| 326 | |||
| 327 | foreach ($this->em->getFilters()->getEnabledFilters() as $filter) { | ||
| 328 | $filterExpr = $filter->addFilterConstraint($targetEntity, $targetTableAlias); | ||
| 329 | if ($filterExpr) { | ||
| 330 | $filterClauses[] = '(' . $filterExpr . ')'; | ||
| 331 | } | ||
| 332 | } | ||
| 333 | |||
| 334 | return $filterClauses | ||
| 335 | ? '(' . implode(' AND ', $filterClauses) . ')' | ||
| 336 | : ''; | ||
| 337 | } | ||
| 338 | |||
| 339 | /** | ||
| 340 | * Generate ON condition | ||
| 341 | * | ||
| 342 | * @return string[] | ||
| 343 | * @psalm-return list<string> | ||
| 344 | */ | ||
| 345 | protected function getOnConditionSQL(AssociationMapping $mapping): array | ||
| 346 | { | ||
| 347 | $association = $this->em->getMetadataFactory()->getOwningSide($mapping); | ||
| 348 | $joinColumns = $mapping->isOwningSide() | ||
| 349 | ? $association->joinTable->inverseJoinColumns | ||
| 350 | : $association->joinTable->joinColumns; | ||
| 351 | |||
| 352 | $conditions = []; | ||
| 353 | |||
| 354 | $targetClass = $this->em->getClassMetadata($mapping->targetEntity); | ||
| 355 | foreach ($joinColumns as $joinColumn) { | ||
| 356 | $joinColumnName = $this->quoteStrategy->getJoinColumnName($joinColumn, $targetClass, $this->platform); | ||
| 357 | $refColumnName = $this->quoteStrategy->getReferencedJoinColumnName($joinColumn, $targetClass, $this->platform); | ||
| 358 | |||
| 359 | $conditions[] = ' t.' . $joinColumnName . ' = te.' . $refColumnName; | ||
| 360 | } | ||
| 361 | |||
| 362 | return $conditions; | ||
| 363 | } | ||
| 364 | |||
| 365 | protected function getDeleteSQL(PersistentCollection $collection): string | ||
| 366 | { | ||
| 367 | $columns = []; | ||
| 368 | $mapping = $this->getMapping($collection); | ||
| 369 | assert($mapping->isManyToManyOwningSide()); | ||
| 370 | $class = $this->em->getClassMetadata($collection->getOwner()::class); | ||
| 371 | $joinTable = $this->quoteStrategy->getJoinTableName($mapping, $class, $this->platform); | ||
| 372 | |||
| 373 | foreach ($mapping->joinTable->joinColumns as $joinColumn) { | ||
| 374 | $columns[] = $this->quoteStrategy->getJoinColumnName($joinColumn, $class, $this->platform); | ||
| 375 | } | ||
| 376 | |||
| 377 | return 'DELETE FROM ' . $joinTable | ||
| 378 | . ' WHERE ' . implode(' = ? AND ', $columns) . ' = ?'; | ||
| 379 | } | ||
| 380 | |||
| 381 | /** | ||
| 382 | * Internal note: Order of the parameters must be the same as the order of the columns in getDeleteSql. | ||
| 383 | * | ||
| 384 | * @return list<mixed> | ||
| 385 | */ | ||
| 386 | protected function getDeleteSQLParameters(PersistentCollection $collection): array | ||
| 387 | { | ||
| 388 | $mapping = $this->getMapping($collection); | ||
| 389 | assert($mapping->isManyToManyOwningSide()); | ||
| 390 | $identifier = $this->uow->getEntityIdentifier($collection->getOwner()); | ||
| 391 | |||
| 392 | // Optimization for single column identifier | ||
| 393 | if (count($mapping->relationToSourceKeyColumns) === 1) { | ||
| 394 | return [reset($identifier)]; | ||
| 395 | } | ||
| 396 | |||
| 397 | // Composite identifier | ||
| 398 | $sourceClass = $this->em->getClassMetadata($mapping->sourceEntity); | ||
| 399 | $params = []; | ||
| 400 | |||
| 401 | foreach ($mapping->relationToSourceKeyColumns as $columnName => $refColumnName) { | ||
| 402 | $params[] = isset($sourceClass->fieldNames[$refColumnName]) | ||
| 403 | ? $identifier[$sourceClass->fieldNames[$refColumnName]] | ||
| 404 | : $identifier[$sourceClass->getFieldForColumn($refColumnName)]; | ||
| 405 | } | ||
| 406 | |||
| 407 | return $params; | ||
| 408 | } | ||
| 409 | |||
| 410 | /** | ||
| 411 | * Gets the SQL statement used for deleting a row from the collection. | ||
| 412 | * | ||
| 413 | * @return string[]|string[][] ordered tuple containing the SQL to be executed and an array | ||
| 414 | * of types for bound parameters | ||
| 415 | * @psalm-return array{0: string, 1: list<string>} | ||
| 416 | */ | ||
| 417 | protected function getDeleteRowSQL(PersistentCollection $collection): array | ||
| 418 | { | ||
| 419 | $mapping = $this->getMapping($collection); | ||
| 420 | assert($mapping->isManyToManyOwningSide()); | ||
| 421 | $class = $this->em->getClassMetadata($mapping->sourceEntity); | ||
| 422 | $targetClass = $this->em->getClassMetadata($mapping->targetEntity); | ||
| 423 | $columns = []; | ||
| 424 | $types = []; | ||
| 425 | |||
| 426 | foreach ($mapping->joinTable->joinColumns as $joinColumn) { | ||
| 427 | $columns[] = $this->quoteStrategy->getJoinColumnName($joinColumn, $class, $this->platform); | ||
| 428 | $types[] = PersisterHelper::getTypeOfColumn($joinColumn->referencedColumnName, $class, $this->em); | ||
| 429 | } | ||
| 430 | |||
| 431 | foreach ($mapping->joinTable->inverseJoinColumns as $joinColumn) { | ||
| 432 | $columns[] = $this->quoteStrategy->getJoinColumnName($joinColumn, $targetClass, $this->platform); | ||
| 433 | $types[] = PersisterHelper::getTypeOfColumn($joinColumn->referencedColumnName, $targetClass, $this->em); | ||
| 434 | } | ||
| 435 | |||
| 436 | return [ | ||
| 437 | 'DELETE FROM ' . $this->quoteStrategy->getJoinTableName($mapping, $class, $this->platform) | ||
| 438 | . ' WHERE ' . implode(' = ? AND ', $columns) . ' = ?', | ||
| 439 | $types, | ||
| 440 | ]; | ||
| 441 | } | ||
| 442 | |||
| 443 | /** | ||
| 444 | * Gets the SQL parameters for the corresponding SQL statement to delete the given | ||
| 445 | * element from the given collection. | ||
| 446 | * | ||
| 447 | * Internal note: Order of the parameters must be the same as the order of the columns in getDeleteRowSql. | ||
| 448 | * | ||
| 449 | * @return mixed[] | ||
| 450 | * @psalm-return list<mixed> | ||
| 451 | */ | ||
| 452 | protected function getDeleteRowSQLParameters(PersistentCollection $collection, object $element): array | ||
| 453 | { | ||
| 454 | return $this->collectJoinTableColumnParameters($collection, $element); | ||
| 455 | } | ||
| 456 | |||
| 457 | /** | ||
| 458 | * Gets the SQL statement used for inserting a row in the collection. | ||
| 459 | * | ||
| 460 | * @return string[]|string[][] ordered tuple containing the SQL to be executed and an array | ||
| 461 | * of types for bound parameters | ||
| 462 | * @psalm-return array{0: string, 1: list<string>} | ||
| 463 | */ | ||
| 464 | protected function getInsertRowSQL(PersistentCollection $collection): array | ||
| 465 | { | ||
| 466 | $columns = []; | ||
| 467 | $types = []; | ||
| 468 | $mapping = $this->getMapping($collection); | ||
| 469 | assert($mapping->isManyToManyOwningSide()); | ||
| 470 | $class = $this->em->getClassMetadata($mapping->sourceEntity); | ||
| 471 | $targetClass = $this->em->getClassMetadata($mapping->targetEntity); | ||
| 472 | |||
| 473 | foreach ($mapping->joinTable->joinColumns as $joinColumn) { | ||
| 474 | $columns[] = $this->quoteStrategy->getJoinColumnName($joinColumn, $targetClass, $this->platform); | ||
| 475 | $types[] = PersisterHelper::getTypeOfColumn($joinColumn->referencedColumnName, $class, $this->em); | ||
| 476 | } | ||
| 477 | |||
| 478 | foreach ($mapping->joinTable->inverseJoinColumns as $joinColumn) { | ||
| 479 | $columns[] = $this->quoteStrategy->getJoinColumnName($joinColumn, $targetClass, $this->platform); | ||
| 480 | $types[] = PersisterHelper::getTypeOfColumn($joinColumn->referencedColumnName, $targetClass, $this->em); | ||
| 481 | } | ||
| 482 | |||
| 483 | return [ | ||
| 484 | 'INSERT INTO ' . $this->quoteStrategy->getJoinTableName($mapping, $class, $this->platform) | ||
| 485 | . ' (' . implode(', ', $columns) . ')' | ||
| 486 | . ' VALUES' | ||
| 487 | . ' (' . implode(', ', array_fill(0, count($columns), '?')) . ')', | ||
| 488 | $types, | ||
| 489 | ]; | ||
| 490 | } | ||
| 491 | |||
| 492 | /** | ||
| 493 | * Gets the SQL parameters for the corresponding SQL statement to insert the given | ||
| 494 | * element of the given collection into the database. | ||
| 495 | * | ||
| 496 | * Internal note: Order of the parameters must be the same as the order of the columns in getInsertRowSql. | ||
| 497 | * | ||
| 498 | * @return mixed[] | ||
| 499 | * @psalm-return list<mixed> | ||
| 500 | */ | ||
| 501 | protected function getInsertRowSQLParameters(PersistentCollection $collection, object $element): array | ||
| 502 | { | ||
| 503 | return $this->collectJoinTableColumnParameters($collection, $element); | ||
| 504 | } | ||
| 505 | |||
| 506 | /** | ||
| 507 | * Collects the parameters for inserting/deleting on the join table in the order | ||
| 508 | * of the join table columns as specified in ManyToManyMapping#joinTableColumns. | ||
| 509 | * | ||
| 510 | * @return mixed[] | ||
| 511 | * @psalm-return list<mixed> | ||
| 512 | */ | ||
| 513 | private function collectJoinTableColumnParameters( | ||
| 514 | PersistentCollection $collection, | ||
| 515 | object $element, | ||
| 516 | ): array { | ||
| 517 | $params = []; | ||
| 518 | $mapping = $this->getMapping($collection); | ||
| 519 | assert($mapping->isManyToManyOwningSide()); | ||
| 520 | $isComposite = count($mapping->joinTableColumns) > 2; | ||
| 521 | |||
| 522 | $identifier1 = $this->uow->getEntityIdentifier($collection->getOwner()); | ||
| 523 | $identifier2 = $this->uow->getEntityIdentifier($element); | ||
| 524 | |||
| 525 | $class1 = $class2 = null; | ||
| 526 | if ($isComposite) { | ||
| 527 | $class1 = $this->em->getClassMetadata($collection->getOwner()::class); | ||
| 528 | $class2 = $collection->getTypeClass(); | ||
| 529 | } | ||
| 530 | |||
| 531 | foreach ($mapping->joinTableColumns as $joinTableColumn) { | ||
| 532 | $isRelationToSource = isset($mapping->relationToSourceKeyColumns[$joinTableColumn]); | ||
| 533 | |||
| 534 | if (! $isComposite) { | ||
| 535 | $params[] = $isRelationToSource ? array_pop($identifier1) : array_pop($identifier2); | ||
| 536 | |||
| 537 | continue; | ||
| 538 | } | ||
| 539 | |||
| 540 | if ($isRelationToSource) { | ||
| 541 | $params[] = $identifier1[$class1->getFieldForColumn($mapping->relationToSourceKeyColumns[$joinTableColumn])]; | ||
| 542 | |||
| 543 | continue; | ||
| 544 | } | ||
| 545 | |||
| 546 | $params[] = $identifier2[$class2->getFieldForColumn($mapping->relationToTargetKeyColumns[$joinTableColumn])]; | ||
| 547 | } | ||
| 548 | |||
| 549 | return $params; | ||
| 550 | } | ||
| 551 | |||
| 552 | /** | ||
| 553 | * @param bool $addFilters Whether the filter SQL should be included or not. | ||
| 554 | * | ||
| 555 | * @return mixed[] ordered vector: | ||
| 556 | * - quoted join table name | ||
| 557 | * - where clauses to be added for filtering | ||
| 558 | * - parameters to be bound for filtering | ||
| 559 | * - types of the parameters to be bound for filtering | ||
| 560 | * @psalm-return array{0: string, 1: list<string>, 2: list<mixed>, 3: list<string>} | ||
| 561 | */ | ||
| 562 | private function getJoinTableRestrictionsWithKey( | ||
| 563 | PersistentCollection $collection, | ||
| 564 | string $key, | ||
| 565 | bool $addFilters, | ||
| 566 | ): array { | ||
| 567 | $filterMapping = $this->getMapping($collection); | ||
| 568 | $mapping = $filterMapping; | ||
| 569 | $indexBy = $mapping->indexBy(); | ||
| 570 | $id = $this->uow->getEntityIdentifier($collection->getOwner()); | ||
| 571 | $sourceClass = $this->em->getClassMetadata($mapping->sourceEntity); | ||
| 572 | $targetClass = $this->em->getClassMetadata($mapping->targetEntity); | ||
| 573 | |||
| 574 | if (! $mapping->isOwningSide()) { | ||
| 575 | assert($mapping instanceof InverseSideMapping); | ||
| 576 | $associationSourceClass = $this->em->getClassMetadata($mapping->targetEntity); | ||
| 577 | $mapping = $associationSourceClass->associationMappings[$mapping->mappedBy]; | ||
| 578 | assert($mapping->isManyToManyOwningSide()); | ||
| 579 | $joinColumns = $mapping->joinTable->joinColumns; | ||
| 580 | $sourceRelationMode = 'relationToTargetKeyColumns'; | ||
| 581 | $targetRelationMode = 'relationToSourceKeyColumns'; | ||
| 582 | } else { | ||
| 583 | assert($mapping->isManyToManyOwningSide()); | ||
| 584 | $associationSourceClass = $this->em->getClassMetadata($mapping->sourceEntity); | ||
| 585 | $joinColumns = $mapping->joinTable->inverseJoinColumns; | ||
| 586 | $sourceRelationMode = 'relationToSourceKeyColumns'; | ||
| 587 | $targetRelationMode = 'relationToTargetKeyColumns'; | ||
| 588 | } | ||
| 589 | |||
| 590 | $quotedJoinTable = $this->quoteStrategy->getJoinTableName($mapping, $associationSourceClass, $this->platform) . ' t'; | ||
| 591 | $whereClauses = []; | ||
| 592 | $params = []; | ||
| 593 | $types = []; | ||
| 594 | |||
| 595 | $joinNeeded = ! in_array($indexBy, $targetClass->identifier, true); | ||
| 596 | |||
| 597 | if ($joinNeeded) { // extra join needed if indexBy is not a @id | ||
| 598 | $joinConditions = []; | ||
| 599 | |||
| 600 | foreach ($joinColumns as $joinTableColumn) { | ||
| 601 | $joinConditions[] = 't.' . $joinTableColumn->name . ' = tr.' . $joinTableColumn->referencedColumnName; | ||
| 602 | } | ||
| 603 | |||
| 604 | $tableName = $this->quoteStrategy->getTableName($targetClass, $this->platform); | ||
| 605 | $quotedJoinTable .= ' JOIN ' . $tableName . ' tr ON ' . implode(' AND ', $joinConditions); | ||
| 606 | $columnName = $targetClass->getColumnName($indexBy); | ||
| 607 | |||
| 608 | $whereClauses[] = 'tr.' . $columnName . ' = ?'; | ||
| 609 | $params[] = $key; | ||
| 610 | $types[] = PersisterHelper::getTypeOfColumn($columnName, $targetClass, $this->em); | ||
| 611 | } | ||
| 612 | |||
| 613 | foreach ($mapping->joinTableColumns as $joinTableColumn) { | ||
| 614 | if (isset($mapping->{$sourceRelationMode}[$joinTableColumn])) { | ||
| 615 | $column = $mapping->{$sourceRelationMode}[$joinTableColumn]; | ||
| 616 | $whereClauses[] = 't.' . $joinTableColumn . ' = ?'; | ||
| 617 | $params[] = $sourceClass->containsForeignIdentifier | ||
| 618 | ? $id[$sourceClass->getFieldForColumn($column)] | ||
| 619 | : $id[$sourceClass->fieldNames[$column]]; | ||
| 620 | $types[] = PersisterHelper::getTypeOfColumn($column, $sourceClass, $this->em); | ||
| 621 | } elseif (! $joinNeeded) { | ||
| 622 | $column = $mapping->{$targetRelationMode}[$joinTableColumn]; | ||
| 623 | |||
| 624 | $whereClauses[] = 't.' . $joinTableColumn . ' = ?'; | ||
| 625 | $params[] = $key; | ||
| 626 | $types[] = PersisterHelper::getTypeOfColumn($column, $targetClass, $this->em); | ||
| 627 | } | ||
| 628 | } | ||
| 629 | |||
| 630 | if ($addFilters) { | ||
| 631 | [$joinTargetEntitySQL, $filterSql] = $this->getFilterSql($filterMapping); | ||
| 632 | |||
| 633 | if ($filterSql) { | ||
| 634 | $quotedJoinTable .= ' ' . $joinTargetEntitySQL; | ||
| 635 | $whereClauses[] = $filterSql; | ||
| 636 | } | ||
| 637 | } | ||
| 638 | |||
| 639 | return [$quotedJoinTable, $whereClauses, $params, $types]; | ||
| 640 | } | ||
| 641 | |||
| 642 | /** | ||
| 643 | * @param bool $addFilters Whether the filter SQL should be included or not. | ||
| 644 | * | ||
| 645 | * @return mixed[] ordered vector: | ||
| 646 | * - quoted join table name | ||
| 647 | * - where clauses to be added for filtering | ||
| 648 | * - parameters to be bound for filtering | ||
| 649 | * - types of the parameters to be bound for filtering | ||
| 650 | * @psalm-return array{0: string, 1: list<string>, 2: list<mixed>, 3: list<string>} | ||
| 651 | */ | ||
| 652 | private function getJoinTableRestrictions( | ||
| 653 | PersistentCollection $collection, | ||
| 654 | object $element, | ||
| 655 | bool $addFilters, | ||
| 656 | ): array { | ||
| 657 | $filterMapping = $this->getMapping($collection); | ||
| 658 | $mapping = $filterMapping; | ||
| 659 | |||
| 660 | if (! $mapping->isOwningSide()) { | ||
| 661 | $sourceClass = $this->em->getClassMetadata($mapping->targetEntity); | ||
| 662 | $targetClass = $this->em->getClassMetadata($mapping->sourceEntity); | ||
| 663 | $sourceId = $this->uow->getEntityIdentifier($element); | ||
| 664 | $targetId = $this->uow->getEntityIdentifier($collection->getOwner()); | ||
| 665 | } else { | ||
| 666 | $sourceClass = $this->em->getClassMetadata($mapping->sourceEntity); | ||
| 667 | $targetClass = $this->em->getClassMetadata($mapping->targetEntity); | ||
| 668 | $sourceId = $this->uow->getEntityIdentifier($collection->getOwner()); | ||
| 669 | $targetId = $this->uow->getEntityIdentifier($element); | ||
| 670 | } | ||
| 671 | |||
| 672 | $mapping = $this->em->getMetadataFactory()->getOwningSide($mapping); | ||
| 673 | |||
| 674 | $quotedJoinTable = $this->quoteStrategy->getJoinTableName($mapping, $sourceClass, $this->platform); | ||
| 675 | $whereClauses = []; | ||
| 676 | $params = []; | ||
| 677 | $types = []; | ||
| 678 | |||
| 679 | foreach ($mapping->joinTableColumns as $joinTableColumn) { | ||
| 680 | $whereClauses[] = ($addFilters ? 't.' : '') . $joinTableColumn . ' = ?'; | ||
| 681 | |||
| 682 | if (isset($mapping->relationToTargetKeyColumns[$joinTableColumn])) { | ||
| 683 | $targetColumn = $mapping->relationToTargetKeyColumns[$joinTableColumn]; | ||
| 684 | $params[] = $targetId[$targetClass->getFieldForColumn($targetColumn)]; | ||
| 685 | $types[] = PersisterHelper::getTypeOfColumn($targetColumn, $targetClass, $this->em); | ||
| 686 | |||
| 687 | continue; | ||
| 688 | } | ||
| 689 | |||
| 690 | // relationToSourceKeyColumns | ||
| 691 | $targetColumn = $mapping->relationToSourceKeyColumns[$joinTableColumn]; | ||
| 692 | $params[] = $sourceId[$sourceClass->getFieldForColumn($targetColumn)]; | ||
| 693 | $types[] = PersisterHelper::getTypeOfColumn($targetColumn, $sourceClass, $this->em); | ||
| 694 | } | ||
| 695 | |||
| 696 | if ($addFilters) { | ||
| 697 | $quotedJoinTable .= ' t'; | ||
| 698 | |||
| 699 | [$joinTargetEntitySQL, $filterSql] = $this->getFilterSql($filterMapping); | ||
| 700 | |||
| 701 | if ($filterSql) { | ||
| 702 | $quotedJoinTable .= ' ' . $joinTargetEntitySQL; | ||
| 703 | $whereClauses[] = $filterSql; | ||
| 704 | } | ||
| 705 | } | ||
| 706 | |||
| 707 | return [$quotedJoinTable, $whereClauses, $params, $types]; | ||
| 708 | } | ||
| 709 | |||
| 710 | /** | ||
| 711 | * Expands Criteria Parameters by walking the expressions and grabbing all | ||
| 712 | * parameters and types from it. | ||
| 713 | * | ||
| 714 | * @return mixed[][] | ||
| 715 | */ | ||
| 716 | private function expandCriteriaParameters(Criteria $criteria): array | ||
| 717 | { | ||
| 718 | $expression = $criteria->getWhereExpression(); | ||
| 719 | |||
| 720 | if ($expression === null) { | ||
| 721 | return []; | ||
| 722 | } | ||
| 723 | |||
| 724 | $valueVisitor = new SqlValueVisitor(); | ||
| 725 | |||
| 726 | $valueVisitor->dispatch($expression); | ||
| 727 | |||
| 728 | [, $types] = $valueVisitor->getParamsAndTypes(); | ||
| 729 | |||
| 730 | return $types; | ||
| 731 | } | ||
| 732 | |||
| 733 | private function getOrderingSql(Criteria $criteria, ClassMetadata $targetClass): string | ||
| 734 | { | ||
| 735 | $orderings = $criteria->orderings(); | ||
| 736 | if ($orderings) { | ||
| 737 | $orderBy = []; | ||
| 738 | foreach ($orderings as $name => $direction) { | ||
| 739 | $field = $this->quoteStrategy->getColumnName( | ||
| 740 | $name, | ||
| 741 | $targetClass, | ||
| 742 | $this->platform, | ||
| 743 | ); | ||
| 744 | $orderBy[] = $field . ' ' . $direction->value; | ||
| 745 | } | ||
| 746 | |||
| 747 | return ' ORDER BY ' . implode(', ', $orderBy); | ||
| 748 | } | ||
| 749 | |||
| 750 | return ''; | ||
| 751 | } | ||
| 752 | |||
| 753 | /** @throws DBALException */ | ||
| 754 | private function getLimitSql(Criteria $criteria): string | ||
| 755 | { | ||
| 756 | $limit = $criteria->getMaxResults(); | ||
| 757 | $offset = $criteria->getFirstResult(); | ||
| 758 | |||
| 759 | return $this->platform->modifyLimitQuery('', $limit, $offset ?? 0); | ||
| 760 | } | ||
| 761 | |||
| 762 | private function getMapping(PersistentCollection $collection): AssociationMapping&ManyToManyAssociationMapping | ||
| 763 | { | ||
| 764 | $mapping = $collection->getMapping(); | ||
| 765 | |||
| 766 | assert($mapping instanceof ManyToManyAssociationMapping); | ||
| 767 | |||
| 768 | return $mapping; | ||
| 769 | } | ||
| 770 | } | ||
diff --git a/vendor/doctrine/orm/src/Persisters/Collection/OneToManyPersister.php b/vendor/doctrine/orm/src/Persisters/Collection/OneToManyPersister.php new file mode 100644 index 0000000..0727b1f --- /dev/null +++ b/vendor/doctrine/orm/src/Persisters/Collection/OneToManyPersister.php | |||
| @@ -0,0 +1,264 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Persisters\Collection; | ||
| 6 | |||
| 7 | use BadMethodCallException; | ||
| 8 | use Doctrine\Common\Collections\Criteria; | ||
| 9 | use Doctrine\DBAL\Exception as DBALException; | ||
| 10 | use Doctrine\DBAL\Types\Type; | ||
| 11 | use Doctrine\ORM\EntityNotFoundException; | ||
| 12 | use Doctrine\ORM\Mapping\MappingException; | ||
| 13 | use Doctrine\ORM\Mapping\OneToManyAssociationMapping; | ||
| 14 | use Doctrine\ORM\PersistentCollection; | ||
| 15 | use Doctrine\ORM\Utility\PersisterHelper; | ||
| 16 | |||
| 17 | use function array_reverse; | ||
| 18 | use function array_values; | ||
| 19 | use function assert; | ||
| 20 | use function implode; | ||
| 21 | use function is_int; | ||
| 22 | use function is_string; | ||
| 23 | |||
| 24 | /** | ||
| 25 | * Persister for one-to-many collections. | ||
| 26 | */ | ||
| 27 | class OneToManyPersister extends AbstractCollectionPersister | ||
| 28 | { | ||
| 29 | public function delete(PersistentCollection $collection): void | ||
| 30 | { | ||
| 31 | // The only valid case here is when you have weak entities. In this | ||
| 32 | // scenario, you have @OneToMany with orphanRemoval=true, and replacing | ||
| 33 | // the entire collection with a new would trigger this operation. | ||
| 34 | $mapping = $this->getMapping($collection); | ||
| 35 | |||
| 36 | if (! $mapping->orphanRemoval) { | ||
| 37 | // Handling non-orphan removal should never happen, as @OneToMany | ||
| 38 | // can only be inverse side. For owning side one to many, it is | ||
| 39 | // required to have a join table, which would classify as a ManyToManyPersister. | ||
| 40 | return; | ||
| 41 | } | ||
| 42 | |||
| 43 | $targetClass = $this->em->getClassMetadata($mapping->targetEntity); | ||
| 44 | |||
| 45 | $targetClass->isInheritanceTypeJoined() | ||
| 46 | ? $this->deleteJoinedEntityCollection($collection) | ||
| 47 | : $this->deleteEntityCollection($collection); | ||
| 48 | } | ||
| 49 | |||
| 50 | public function update(PersistentCollection $collection): void | ||
| 51 | { | ||
| 52 | // This can never happen. One to many can only be inverse side. | ||
| 53 | // For owning side one to many, it is required to have a join table, | ||
| 54 | // then classifying it as a ManyToManyPersister. | ||
| 55 | return; | ||
| 56 | } | ||
| 57 | |||
| 58 | public function get(PersistentCollection $collection, mixed $index): object|null | ||
| 59 | { | ||
| 60 | $mapping = $this->getMapping($collection); | ||
| 61 | |||
| 62 | if (! $mapping->isIndexed()) { | ||
| 63 | throw new BadMethodCallException('Selecting a collection by index is only supported on indexed collections.'); | ||
| 64 | } | ||
| 65 | |||
| 66 | $persister = $this->uow->getEntityPersister($mapping->targetEntity); | ||
| 67 | |||
| 68 | return $persister->load( | ||
| 69 | [ | ||
| 70 | $mapping->mappedBy => $collection->getOwner(), | ||
| 71 | $mapping->indexBy() => $index, | ||
| 72 | ], | ||
| 73 | null, | ||
| 74 | $mapping, | ||
| 75 | [], | ||
| 76 | null, | ||
| 77 | 1, | ||
| 78 | ); | ||
| 79 | } | ||
| 80 | |||
| 81 | public function count(PersistentCollection $collection): int | ||
| 82 | { | ||
| 83 | $mapping = $this->getMapping($collection); | ||
| 84 | $persister = $this->uow->getEntityPersister($mapping->targetEntity); | ||
| 85 | |||
| 86 | // only works with single id identifier entities. Will throw an | ||
| 87 | // exception in Entity Persisters if that is not the case for the | ||
| 88 | // 'mappedBy' field. | ||
| 89 | $criteria = new Criteria(Criteria::expr()->eq($mapping->mappedBy, $collection->getOwner())); | ||
| 90 | |||
| 91 | return $persister->count($criteria); | ||
| 92 | } | ||
| 93 | |||
| 94 | /** | ||
| 95 | * {@inheritDoc} | ||
| 96 | */ | ||
| 97 | public function slice(PersistentCollection $collection, int $offset, int|null $length = null): array | ||
| 98 | { | ||
| 99 | $mapping = $this->getMapping($collection); | ||
| 100 | $persister = $this->uow->getEntityPersister($mapping->targetEntity); | ||
| 101 | |||
| 102 | return $persister->getOneToManyCollection($mapping, $collection->getOwner(), $offset, $length); | ||
| 103 | } | ||
| 104 | |||
| 105 | public function containsKey(PersistentCollection $collection, mixed $key): bool | ||
| 106 | { | ||
| 107 | $mapping = $this->getMapping($collection); | ||
| 108 | |||
| 109 | if (! $mapping->isIndexed()) { | ||
| 110 | throw new BadMethodCallException('Selecting a collection by index is only supported on indexed collections.'); | ||
| 111 | } | ||
| 112 | |||
| 113 | $persister = $this->uow->getEntityPersister($mapping->targetEntity); | ||
| 114 | |||
| 115 | // only works with single id identifier entities. Will throw an | ||
| 116 | // exception in Entity Persisters if that is not the case for the | ||
| 117 | // 'mappedBy' field. | ||
| 118 | $criteria = new Criteria(); | ||
| 119 | |||
| 120 | $criteria->andWhere(Criteria::expr()->eq($mapping->mappedBy, $collection->getOwner())); | ||
| 121 | $criteria->andWhere(Criteria::expr()->eq($mapping->indexBy(), $key)); | ||
| 122 | |||
| 123 | return (bool) $persister->count($criteria); | ||
| 124 | } | ||
| 125 | |||
| 126 | public function contains(PersistentCollection $collection, object $element): bool | ||
| 127 | { | ||
| 128 | if (! $this->isValidEntityState($element)) { | ||
| 129 | return false; | ||
| 130 | } | ||
| 131 | |||
| 132 | $mapping = $this->getMapping($collection); | ||
| 133 | $persister = $this->uow->getEntityPersister($mapping->targetEntity); | ||
| 134 | |||
| 135 | // only works with single id identifier entities. Will throw an | ||
| 136 | // exception in Entity Persisters if that is not the case for the | ||
| 137 | // 'mappedBy' field. | ||
| 138 | $criteria = new Criteria(Criteria::expr()->eq($mapping->mappedBy, $collection->getOwner())); | ||
| 139 | |||
| 140 | return $persister->exists($element, $criteria); | ||
| 141 | } | ||
| 142 | |||
| 143 | /** | ||
| 144 | * {@inheritDoc} | ||
| 145 | */ | ||
| 146 | public function loadCriteria(PersistentCollection $collection, Criteria $criteria): array | ||
| 147 | { | ||
| 148 | throw new BadMethodCallException('Filtering a collection by Criteria is not supported by this CollectionPersister.'); | ||
| 149 | } | ||
| 150 | |||
| 151 | /** | ||
| 152 | * @throws DBALException | ||
| 153 | * @throws EntityNotFoundException | ||
| 154 | * @throws MappingException | ||
| 155 | */ | ||
| 156 | private function deleteEntityCollection(PersistentCollection $collection): int | ||
| 157 | { | ||
| 158 | $mapping = $this->getMapping($collection); | ||
| 159 | $identifier = $this->uow->getEntityIdentifier($collection->getOwner()); | ||
| 160 | $sourceClass = $this->em->getClassMetadata($mapping->sourceEntity); | ||
| 161 | $targetClass = $this->em->getClassMetadata($mapping->targetEntity); | ||
| 162 | $columns = []; | ||
| 163 | $parameters = []; | ||
| 164 | $types = []; | ||
| 165 | |||
| 166 | foreach ($this->em->getMetadataFactory()->getOwningSide($mapping)->joinColumns as $joinColumn) { | ||
| 167 | $columns[] = $this->quoteStrategy->getJoinColumnName($joinColumn, $targetClass, $this->platform); | ||
| 168 | $parameters[] = $identifier[$sourceClass->getFieldForColumn($joinColumn->referencedColumnName)]; | ||
| 169 | $types[] = PersisterHelper::getTypeOfColumn($joinColumn->referencedColumnName, $sourceClass, $this->em); | ||
| 170 | } | ||
| 171 | |||
| 172 | $statement = 'DELETE FROM ' . $this->quoteStrategy->getTableName($targetClass, $this->platform) | ||
| 173 | . ' WHERE ' . implode(' = ? AND ', $columns) . ' = ?'; | ||
| 174 | |||
| 175 | if ($targetClass->isInheritanceTypeSingleTable()) { | ||
| 176 | $discriminatorColumn = $targetClass->getDiscriminatorColumn(); | ||
| 177 | $statement .= ' AND ' . $discriminatorColumn->name . ' = ?'; | ||
| 178 | $parameters[] = $targetClass->discriminatorValue; | ||
| 179 | $types[] = $discriminatorColumn->type; | ||
| 180 | } | ||
| 181 | |||
| 182 | $numAffected = $this->conn->executeStatement($statement, $parameters, $types); | ||
| 183 | |||
| 184 | assert(is_int($numAffected)); | ||
| 185 | |||
| 186 | return $numAffected; | ||
| 187 | } | ||
| 188 | |||
| 189 | /** | ||
| 190 | * Delete Class Table Inheritance entities. | ||
| 191 | * A temporary table is needed to keep IDs to be deleted in both parent and child class' tables. | ||
| 192 | * | ||
| 193 | * Thanks Steve Ebersole (Hibernate) for idea on how to tackle reliably this scenario, we owe him a beer! =) | ||
| 194 | * | ||
| 195 | * @throws DBALException | ||
| 196 | */ | ||
| 197 | private function deleteJoinedEntityCollection(PersistentCollection $collection): int | ||
| 198 | { | ||
| 199 | $mapping = $this->getMapping($collection); | ||
| 200 | $sourceClass = $this->em->getClassMetadata($mapping->sourceEntity); | ||
| 201 | $targetClass = $this->em->getClassMetadata($mapping->targetEntity); | ||
| 202 | $rootClass = $this->em->getClassMetadata($targetClass->rootEntityName); | ||
| 203 | |||
| 204 | // 1) Build temporary table DDL | ||
| 205 | $tempTable = $this->platform->getTemporaryTableName($rootClass->getTemporaryIdTableName()); | ||
| 206 | $idColumnNames = $rootClass->getIdentifierColumnNames(); | ||
| 207 | $idColumnList = implode(', ', $idColumnNames); | ||
| 208 | $columnDefinitions = []; | ||
| 209 | |||
| 210 | foreach ($idColumnNames as $idColumnName) { | ||
| 211 | $columnDefinitions[$idColumnName] = [ | ||
| 212 | 'name' => $idColumnName, | ||
| 213 | 'notnull' => true, | ||
| 214 | 'type' => Type::getType(PersisterHelper::getTypeOfColumn($idColumnName, $rootClass, $this->em)), | ||
| 215 | ]; | ||
| 216 | } | ||
| 217 | |||
| 218 | $statement = $this->platform->getCreateTemporaryTableSnippetSQL() . ' ' . $tempTable | ||
| 219 | . ' (' . $this->platform->getColumnDeclarationListSQL($columnDefinitions) . ')'; | ||
| 220 | |||
| 221 | $this->conn->executeStatement($statement); | ||
| 222 | |||
| 223 | // 2) Build insert table records into temporary table | ||
| 224 | $query = $this->em->createQuery( | ||
| 225 | ' SELECT t0.' . implode(', t0.', $rootClass->getIdentifierFieldNames()) | ||
| 226 | . ' FROM ' . $targetClass->name . ' t0 WHERE t0.' . $mapping->mappedBy . ' = :owner', | ||
| 227 | )->setParameter('owner', $collection->getOwner()); | ||
| 228 | |||
| 229 | $sql = $query->getSQL(); | ||
| 230 | assert(is_string($sql)); | ||
| 231 | $statement = 'INSERT INTO ' . $tempTable . ' (' . $idColumnList . ') ' . $sql; | ||
| 232 | $parameters = array_values($sourceClass->getIdentifierValues($collection->getOwner())); | ||
| 233 | $numDeleted = $this->conn->executeStatement($statement, $parameters); | ||
| 234 | |||
| 235 | // 3) Delete records on each table in the hierarchy | ||
| 236 | $classNames = [...$targetClass->parentClasses, ...[$targetClass->name], ...$targetClass->subClasses]; | ||
| 237 | |||
| 238 | foreach (array_reverse($classNames) as $className) { | ||
| 239 | $tableName = $this->quoteStrategy->getTableName($this->em->getClassMetadata($className), $this->platform); | ||
| 240 | $statement = 'DELETE FROM ' . $tableName . ' WHERE (' . $idColumnList . ')' | ||
| 241 | . ' IN (SELECT ' . $idColumnList . ' FROM ' . $tempTable . ')'; | ||
| 242 | |||
| 243 | $this->conn->executeStatement($statement); | ||
| 244 | } | ||
| 245 | |||
| 246 | // 4) Drop temporary table | ||
| 247 | $statement = $this->platform->getDropTemporaryTableSQL($tempTable); | ||
| 248 | |||
| 249 | $this->conn->executeStatement($statement); | ||
| 250 | |||
| 251 | assert(is_int($numDeleted)); | ||
| 252 | |||
| 253 | return $numDeleted; | ||
| 254 | } | ||
| 255 | |||
| 256 | private function getMapping(PersistentCollection $collection): OneToManyAssociationMapping | ||
| 257 | { | ||
| 258 | $mapping = $collection->getMapping(); | ||
| 259 | |||
| 260 | assert($mapping->isOneToMany()); | ||
| 261 | |||
| 262 | return $mapping; | ||
| 263 | } | ||
| 264 | } | ||
diff --git a/vendor/doctrine/orm/src/Persisters/Entity/AbstractEntityInheritancePersister.php b/vendor/doctrine/orm/src/Persisters/Entity/AbstractEntityInheritancePersister.php new file mode 100644 index 0000000..cf8a74e --- /dev/null +++ b/vendor/doctrine/orm/src/Persisters/Entity/AbstractEntityInheritancePersister.php | |||
| @@ -0,0 +1,66 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Persisters\Entity; | ||
| 6 | |||
| 7 | use Doctrine\DBAL\Types\Type; | ||
| 8 | use Doctrine\ORM\Mapping\ClassMetadata; | ||
| 9 | |||
| 10 | use function sprintf; | ||
| 11 | |||
| 12 | /** | ||
| 13 | * Base class for entity persisters that implement a certain inheritance mapping strategy. | ||
| 14 | * All these persisters are assumed to use a discriminator column to discriminate entity | ||
| 15 | * types in the hierarchy. | ||
| 16 | */ | ||
| 17 | abstract class AbstractEntityInheritancePersister extends BasicEntityPersister | ||
| 18 | { | ||
| 19 | /** | ||
| 20 | * {@inheritDoc} | ||
| 21 | */ | ||
| 22 | protected function prepareInsertData(object $entity): array | ||
| 23 | { | ||
| 24 | $data = parent::prepareInsertData($entity); | ||
| 25 | |||
| 26 | // Populate the discriminator column | ||
| 27 | $discColumn = $this->class->getDiscriminatorColumn(); | ||
| 28 | $this->columnTypes[$discColumn->name] = $discColumn->type; | ||
| 29 | $data[$this->getDiscriminatorColumnTableName()][$discColumn->name] = $this->class->discriminatorValue; | ||
| 30 | |||
| 31 | return $data; | ||
| 32 | } | ||
| 33 | |||
| 34 | /** | ||
| 35 | * Gets the name of the table that contains the discriminator column. | ||
| 36 | */ | ||
| 37 | abstract protected function getDiscriminatorColumnTableName(): string; | ||
| 38 | |||
| 39 | protected function getSelectColumnSQL(string $field, ClassMetadata $class, string $alias = 'r'): string | ||
| 40 | { | ||
| 41 | $tableAlias = $alias === 'r' ? '' : $alias; | ||
| 42 | $fieldMapping = $class->fieldMappings[$field]; | ||
| 43 | $columnAlias = $this->getSQLColumnAlias($fieldMapping->columnName); | ||
| 44 | $sql = sprintf( | ||
| 45 | '%s.%s', | ||
| 46 | $this->getSQLTableAlias($class->name, $tableAlias), | ||
| 47 | $this->quoteStrategy->getColumnName($field, $class, $this->platform), | ||
| 48 | ); | ||
| 49 | |||
| 50 | $this->currentPersisterContext->rsm->addFieldResult($alias, $columnAlias, $field, $class->name); | ||
| 51 | |||
| 52 | $type = Type::getType($fieldMapping->type); | ||
| 53 | $sql = $type->convertToPHPValueSQL($sql, $this->platform); | ||
| 54 | |||
| 55 | return $sql . ' AS ' . $columnAlias; | ||
| 56 | } | ||
| 57 | |||
| 58 | protected function getSelectJoinColumnSQL(string $tableAlias, string $joinColumnName, string $quotedColumnName, string $type): string | ||
| 59 | { | ||
| 60 | $columnAlias = $this->getSQLColumnAlias($joinColumnName); | ||
| 61 | |||
| 62 | $this->currentPersisterContext->rsm->addMetaResult('r', $columnAlias, $joinColumnName, false, $type); | ||
| 63 | |||
| 64 | return $tableAlias . '.' . $quotedColumnName . ' AS ' . $columnAlias; | ||
| 65 | } | ||
| 66 | } | ||
diff --git a/vendor/doctrine/orm/src/Persisters/Entity/BasicEntityPersister.php b/vendor/doctrine/orm/src/Persisters/Entity/BasicEntityPersister.php new file mode 100644 index 0000000..377e03c --- /dev/null +++ b/vendor/doctrine/orm/src/Persisters/Entity/BasicEntityPersister.php | |||
| @@ -0,0 +1,2085 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Persisters\Entity; | ||
| 6 | |||
| 7 | use BackedEnum; | ||
| 8 | use Doctrine\Common\Collections\Criteria; | ||
| 9 | use Doctrine\Common\Collections\Expr\Comparison; | ||
| 10 | use Doctrine\Common\Collections\Order; | ||
| 11 | use Doctrine\DBAL\ArrayParameterType; | ||
| 12 | use Doctrine\DBAL\Connection; | ||
| 13 | use Doctrine\DBAL\LockMode; | ||
| 14 | use Doctrine\DBAL\ParameterType; | ||
| 15 | use Doctrine\DBAL\Platforms\AbstractPlatform; | ||
| 16 | use Doctrine\DBAL\Result; | ||
| 17 | use Doctrine\DBAL\Types\Type; | ||
| 18 | use Doctrine\DBAL\Types\Types; | ||
| 19 | use Doctrine\ORM\EntityManagerInterface; | ||
| 20 | use Doctrine\ORM\Mapping\AssociationMapping; | ||
| 21 | use Doctrine\ORM\Mapping\ClassMetadata; | ||
| 22 | use Doctrine\ORM\Mapping\JoinColumnMapping; | ||
| 23 | use Doctrine\ORM\Mapping\ManyToManyAssociationMapping; | ||
| 24 | use Doctrine\ORM\Mapping\MappingException; | ||
| 25 | use Doctrine\ORM\Mapping\OneToManyAssociationMapping; | ||
| 26 | use Doctrine\ORM\Mapping\QuoteStrategy; | ||
| 27 | use Doctrine\ORM\OptimisticLockException; | ||
| 28 | use Doctrine\ORM\PersistentCollection; | ||
| 29 | use Doctrine\ORM\Persisters\Exception\CantUseInOperatorOnCompositeKeys; | ||
| 30 | use Doctrine\ORM\Persisters\Exception\InvalidOrientation; | ||
| 31 | use Doctrine\ORM\Persisters\Exception\UnrecognizedField; | ||
| 32 | use Doctrine\ORM\Persisters\SqlExpressionVisitor; | ||
| 33 | use Doctrine\ORM\Persisters\SqlValueVisitor; | ||
| 34 | use Doctrine\ORM\Proxy\DefaultProxyClassNameResolver; | ||
| 35 | use Doctrine\ORM\Query; | ||
| 36 | use Doctrine\ORM\Query\QueryException; | ||
| 37 | use Doctrine\ORM\Query\ResultSetMapping; | ||
| 38 | use Doctrine\ORM\Repository\Exception\InvalidFindByCall; | ||
| 39 | use Doctrine\ORM\UnitOfWork; | ||
| 40 | use Doctrine\ORM\Utility\IdentifierFlattener; | ||
| 41 | use Doctrine\ORM\Utility\LockSqlHelper; | ||
| 42 | use Doctrine\ORM\Utility\PersisterHelper; | ||
| 43 | use LengthException; | ||
| 44 | |||
| 45 | use function array_combine; | ||
| 46 | use function array_keys; | ||
| 47 | use function array_map; | ||
| 48 | use function array_merge; | ||
| 49 | use function array_search; | ||
| 50 | use function array_unique; | ||
| 51 | use function array_values; | ||
| 52 | use function assert; | ||
| 53 | use function count; | ||
| 54 | use function implode; | ||
| 55 | use function is_array; | ||
| 56 | use function is_object; | ||
| 57 | use function reset; | ||
| 58 | use function spl_object_id; | ||
| 59 | use function sprintf; | ||
| 60 | use function str_contains; | ||
| 61 | use function strtoupper; | ||
| 62 | use function trim; | ||
| 63 | |||
| 64 | /** | ||
| 65 | * A BasicEntityPersister maps an entity to a single table in a relational database. | ||
| 66 | * | ||
| 67 | * A persister is always responsible for a single entity type. | ||
| 68 | * | ||
| 69 | * EntityPersisters are used during a UnitOfWork to apply any changes to the persistent | ||
| 70 | * state of entities onto a relational database when the UnitOfWork is committed, | ||
| 71 | * as well as for basic querying of entities and their associations (not DQL). | ||
| 72 | * | ||
| 73 | * The persisting operations that are invoked during a commit of a UnitOfWork to | ||
| 74 | * persist the persistent entity state are: | ||
| 75 | * | ||
| 76 | * - {@link addInsert} : To schedule an entity for insertion. | ||
| 77 | * - {@link executeInserts} : To execute all scheduled insertions. | ||
| 78 | * - {@link update} : To update the persistent state of an entity. | ||
| 79 | * - {@link delete} : To delete the persistent state of an entity. | ||
| 80 | * | ||
| 81 | * As can be seen from the above list, insertions are batched and executed all at once | ||
| 82 | * for increased efficiency. | ||
| 83 | * | ||
| 84 | * The querying operations invoked during a UnitOfWork, either through direct find | ||
| 85 | * requests or lazy-loading, are the following: | ||
| 86 | * | ||
| 87 | * - {@link load} : Loads (the state of) a single, managed entity. | ||
| 88 | * - {@link loadAll} : Loads multiple, managed entities. | ||
| 89 | * - {@link loadOneToOneEntity} : Loads a one/many-to-one entity association (lazy-loading). | ||
| 90 | * - {@link loadOneToManyCollection} : Loads a one-to-many entity association (lazy-loading). | ||
| 91 | * - {@link loadManyToManyCollection} : Loads a many-to-many entity association (lazy-loading). | ||
| 92 | * | ||
| 93 | * The BasicEntityPersister implementation provides the default behavior for | ||
| 94 | * persisting and querying entities that are mapped to a single database table. | ||
| 95 | * | ||
| 96 | * Subclasses can be created to provide custom persisting and querying strategies, | ||
| 97 | * i.e. spanning multiple tables. | ||
| 98 | */ | ||
| 99 | class BasicEntityPersister implements EntityPersister | ||
| 100 | { | ||
| 101 | use LockSqlHelper; | ||
| 102 | |||
| 103 | /** @var array<string,string> */ | ||
| 104 | private static array $comparisonMap = [ | ||
| 105 | Comparison::EQ => '= %s', | ||
| 106 | Comparison::NEQ => '!= %s', | ||
| 107 | Comparison::GT => '> %s', | ||
| 108 | Comparison::GTE => '>= %s', | ||
| 109 | Comparison::LT => '< %s', | ||
| 110 | Comparison::LTE => '<= %s', | ||
| 111 | Comparison::IN => 'IN (%s)', | ||
| 112 | Comparison::NIN => 'NOT IN (%s)', | ||
| 113 | Comparison::CONTAINS => 'LIKE %s', | ||
| 114 | Comparison::STARTS_WITH => 'LIKE %s', | ||
| 115 | Comparison::ENDS_WITH => 'LIKE %s', | ||
| 116 | ]; | ||
| 117 | |||
| 118 | /** | ||
| 119 | * The underlying DBAL Connection of the used EntityManager. | ||
| 120 | */ | ||
| 121 | protected Connection $conn; | ||
| 122 | |||
| 123 | /** | ||
| 124 | * The database platform. | ||
| 125 | */ | ||
| 126 | protected AbstractPlatform $platform; | ||
| 127 | |||
| 128 | /** | ||
| 129 | * Queued inserts. | ||
| 130 | * | ||
| 131 | * @psalm-var array<int, object> | ||
| 132 | */ | ||
| 133 | protected array $queuedInserts = []; | ||
| 134 | |||
| 135 | /** | ||
| 136 | * The map of column names to DBAL mapping types of all prepared columns used | ||
| 137 | * when INSERTing or UPDATEing an entity. | ||
| 138 | * | ||
| 139 | * @see prepareInsertData($entity) | ||
| 140 | * @see prepareUpdateData($entity) | ||
| 141 | * | ||
| 142 | * @var mixed[] | ||
| 143 | */ | ||
| 144 | protected array $columnTypes = []; | ||
| 145 | |||
| 146 | /** | ||
| 147 | * The map of quoted column names. | ||
| 148 | * | ||
| 149 | * @see prepareInsertData($entity) | ||
| 150 | * @see prepareUpdateData($entity) | ||
| 151 | * | ||
| 152 | * @var mixed[] | ||
| 153 | */ | ||
| 154 | protected array $quotedColumns = []; | ||
| 155 | |||
| 156 | /** | ||
| 157 | * The INSERT SQL statement used for entities handled by this persister. | ||
| 158 | * This SQL is only generated once per request, if at all. | ||
| 159 | */ | ||
| 160 | private string|null $insertSql = null; | ||
| 161 | |||
| 162 | /** | ||
| 163 | * The quote strategy. | ||
| 164 | */ | ||
| 165 | protected QuoteStrategy $quoteStrategy; | ||
| 166 | |||
| 167 | /** | ||
| 168 | * The IdentifierFlattener used for manipulating identifiers | ||
| 169 | */ | ||
| 170 | protected readonly IdentifierFlattener $identifierFlattener; | ||
| 171 | |||
| 172 | protected CachedPersisterContext $currentPersisterContext; | ||
| 173 | private readonly CachedPersisterContext $limitsHandlingContext; | ||
| 174 | private readonly CachedPersisterContext $noLimitsContext; | ||
| 175 | |||
| 176 | /** | ||
| 177 | * Initializes a new <tt>BasicEntityPersister</tt> that uses the given EntityManager | ||
| 178 | * and persists instances of the class described by the given ClassMetadata descriptor. | ||
| 179 | * | ||
| 180 | * @param ClassMetadata $class Metadata object that describes the mapping of the mapped entity class. | ||
| 181 | */ | ||
| 182 | public function __construct( | ||
| 183 | protected EntityManagerInterface $em, | ||
| 184 | protected ClassMetadata $class, | ||
| 185 | ) { | ||
| 186 | $this->conn = $em->getConnection(); | ||
| 187 | $this->platform = $this->conn->getDatabasePlatform(); | ||
| 188 | $this->quoteStrategy = $em->getConfiguration()->getQuoteStrategy(); | ||
| 189 | $this->identifierFlattener = new IdentifierFlattener($em->getUnitOfWork(), $em->getMetadataFactory()); | ||
| 190 | $this->noLimitsContext = $this->currentPersisterContext = new CachedPersisterContext( | ||
| 191 | $class, | ||
| 192 | new Query\ResultSetMapping(), | ||
| 193 | false, | ||
| 194 | ); | ||
| 195 | $this->limitsHandlingContext = new CachedPersisterContext( | ||
| 196 | $class, | ||
| 197 | new Query\ResultSetMapping(), | ||
| 198 | true, | ||
| 199 | ); | ||
| 200 | } | ||
| 201 | |||
| 202 | public function getClassMetadata(): ClassMetadata | ||
| 203 | { | ||
| 204 | return $this->class; | ||
| 205 | } | ||
| 206 | |||
| 207 | public function getResultSetMapping(): ResultSetMapping | ||
| 208 | { | ||
| 209 | return $this->currentPersisterContext->rsm; | ||
| 210 | } | ||
| 211 | |||
| 212 | public function addInsert(object $entity): void | ||
| 213 | { | ||
| 214 | $this->queuedInserts[spl_object_id($entity)] = $entity; | ||
| 215 | } | ||
| 216 | |||
| 217 | /** | ||
| 218 | * {@inheritDoc} | ||
| 219 | */ | ||
| 220 | public function getInserts(): array | ||
| 221 | { | ||
| 222 | return $this->queuedInserts; | ||
| 223 | } | ||
| 224 | |||
| 225 | public function executeInserts(): void | ||
| 226 | { | ||
| 227 | if (! $this->queuedInserts) { | ||
| 228 | return; | ||
| 229 | } | ||
| 230 | |||
| 231 | $uow = $this->em->getUnitOfWork(); | ||
| 232 | $idGenerator = $this->class->idGenerator; | ||
| 233 | $isPostInsertId = $idGenerator->isPostInsertGenerator(); | ||
| 234 | |||
| 235 | $stmt = $this->conn->prepare($this->getInsertSQL()); | ||
| 236 | $tableName = $this->class->getTableName(); | ||
| 237 | |||
| 238 | foreach ($this->queuedInserts as $key => $entity) { | ||
| 239 | $insertData = $this->prepareInsertData($entity); | ||
| 240 | |||
| 241 | if (isset($insertData[$tableName])) { | ||
| 242 | $paramIndex = 1; | ||
| 243 | |||
| 244 | foreach ($insertData[$tableName] as $column => $value) { | ||
| 245 | $stmt->bindValue($paramIndex++, $value, $this->columnTypes[$column]); | ||
| 246 | } | ||
| 247 | } | ||
| 248 | |||
| 249 | $stmt->executeStatement(); | ||
| 250 | |||
| 251 | if ($isPostInsertId) { | ||
| 252 | $generatedId = $idGenerator->generateId($this->em, $entity); | ||
| 253 | $id = [$this->class->identifier[0] => $generatedId]; | ||
| 254 | |||
| 255 | $uow->assignPostInsertId($entity, $generatedId); | ||
| 256 | } else { | ||
| 257 | $id = $this->class->getIdentifierValues($entity); | ||
| 258 | } | ||
| 259 | |||
| 260 | if ($this->class->requiresFetchAfterChange) { | ||
| 261 | $this->assignDefaultVersionAndUpsertableValues($entity, $id); | ||
| 262 | } | ||
| 263 | |||
| 264 | // Unset this queued insert, so that the prepareUpdateData() method knows right away | ||
| 265 | // (for the next entity already) that the current entity has been written to the database | ||
| 266 | // and no extra updates need to be scheduled to refer to it. | ||
| 267 | // | ||
| 268 | // In \Doctrine\ORM\UnitOfWork::executeInserts(), the UoW already removed entities | ||
| 269 | // from its own list (\Doctrine\ORM\UnitOfWork::$entityInsertions) right after they | ||
| 270 | // were given to our addInsert() method. | ||
| 271 | unset($this->queuedInserts[$key]); | ||
| 272 | } | ||
| 273 | } | ||
| 274 | |||
| 275 | /** | ||
| 276 | * Retrieves the default version value which was created | ||
| 277 | * by the preceding INSERT statement and assigns it back in to the | ||
| 278 | * entities version field if the given entity is versioned. | ||
| 279 | * Also retrieves values of columns marked as 'non insertable' and / or | ||
| 280 | * 'not updatable' and assigns them back to the entities corresponding fields. | ||
| 281 | * | ||
| 282 | * @param mixed[] $id | ||
| 283 | */ | ||
| 284 | protected function assignDefaultVersionAndUpsertableValues(object $entity, array $id): void | ||
| 285 | { | ||
| 286 | $values = $this->fetchVersionAndNotUpsertableValues($this->class, $id); | ||
| 287 | |||
| 288 | foreach ($values as $field => $value) { | ||
| 289 | $value = Type::getType($this->class->fieldMappings[$field]->type)->convertToPHPValue($value, $this->platform); | ||
| 290 | |||
| 291 | $this->class->setFieldValue($entity, $field, $value); | ||
| 292 | } | ||
| 293 | } | ||
| 294 | |||
| 295 | /** | ||
| 296 | * Fetches the current version value of a versioned entity and / or the values of fields | ||
| 297 | * marked as 'not insertable' and / or 'not updatable'. | ||
| 298 | * | ||
| 299 | * @param mixed[] $id | ||
| 300 | */ | ||
| 301 | protected function fetchVersionAndNotUpsertableValues(ClassMetadata $versionedClass, array $id): mixed | ||
| 302 | { | ||
| 303 | $columnNames = []; | ||
| 304 | foreach ($this->class->fieldMappings as $key => $column) { | ||
| 305 | if (isset($column->generated) || ($this->class->isVersioned && $key === $versionedClass->versionField)) { | ||
| 306 | $columnNames[$key] = $this->quoteStrategy->getColumnName($key, $versionedClass, $this->platform); | ||
| 307 | } | ||
| 308 | } | ||
| 309 | |||
| 310 | $tableName = $this->quoteStrategy->getTableName($versionedClass, $this->platform); | ||
| 311 | $identifier = $this->quoteStrategy->getIdentifierColumnNames($versionedClass, $this->platform); | ||
| 312 | |||
| 313 | // FIXME: Order with composite keys might not be correct | ||
| 314 | $sql = 'SELECT ' . implode(', ', $columnNames) | ||
| 315 | . ' FROM ' . $tableName | ||
| 316 | . ' WHERE ' . implode(' = ? AND ', $identifier) . ' = ?'; | ||
| 317 | |||
| 318 | $flatId = $this->identifierFlattener->flattenIdentifier($versionedClass, $id); | ||
| 319 | |||
| 320 | $values = $this->conn->fetchNumeric( | ||
| 321 | $sql, | ||
| 322 | array_values($flatId), | ||
| 323 | $this->extractIdentifierTypes($id, $versionedClass), | ||
| 324 | ); | ||
| 325 | |||
| 326 | if ($values === false) { | ||
| 327 | throw new LengthException('Unexpected empty result for database query.'); | ||
| 328 | } | ||
| 329 | |||
| 330 | $values = array_combine(array_keys($columnNames), $values); | ||
| 331 | |||
| 332 | if (! $values) { | ||
| 333 | throw new LengthException('Unexpected number of database columns.'); | ||
| 334 | } | ||
| 335 | |||
| 336 | return $values; | ||
| 337 | } | ||
| 338 | |||
| 339 | /** | ||
| 340 | * @param mixed[] $id | ||
| 341 | * | ||
| 342 | * @return list<ParameterType|int|string> | ||
| 343 | * @psalm-return list<ParameterType::*|ArrayParameterType::*|string> | ||
| 344 | */ | ||
| 345 | final protected function extractIdentifierTypes(array $id, ClassMetadata $versionedClass): array | ||
| 346 | { | ||
| 347 | $types = []; | ||
| 348 | |||
| 349 | foreach ($id as $field => $value) { | ||
| 350 | $types = [...$types, ...$this->getTypes($field, $value, $versionedClass)]; | ||
| 351 | } | ||
| 352 | |||
| 353 | return $types; | ||
| 354 | } | ||
| 355 | |||
| 356 | public function update(object $entity): void | ||
| 357 | { | ||
| 358 | $tableName = $this->class->getTableName(); | ||
| 359 | $updateData = $this->prepareUpdateData($entity); | ||
| 360 | |||
| 361 | if (! isset($updateData[$tableName])) { | ||
| 362 | return; | ||
| 363 | } | ||
| 364 | |||
| 365 | $data = $updateData[$tableName]; | ||
| 366 | |||
| 367 | if (! $data) { | ||
| 368 | return; | ||
| 369 | } | ||
| 370 | |||
| 371 | $isVersioned = $this->class->isVersioned; | ||
| 372 | $quotedTableName = $this->quoteStrategy->getTableName($this->class, $this->platform); | ||
| 373 | |||
| 374 | $this->updateTable($entity, $quotedTableName, $data, $isVersioned); | ||
| 375 | |||
| 376 | if ($this->class->requiresFetchAfterChange) { | ||
| 377 | $id = $this->class->getIdentifierValues($entity); | ||
| 378 | |||
| 379 | $this->assignDefaultVersionAndUpsertableValues($entity, $id); | ||
| 380 | } | ||
| 381 | } | ||
| 382 | |||
| 383 | /** | ||
| 384 | * Performs an UPDATE statement for an entity on a specific table. | ||
| 385 | * The UPDATE can optionally be versioned, which requires the entity to have a version field. | ||
| 386 | * | ||
| 387 | * @param object $entity The entity object being updated. | ||
| 388 | * @param string $quotedTableName The quoted name of the table to apply the UPDATE on. | ||
| 389 | * @param mixed[] $updateData The map of columns to update (column => value). | ||
| 390 | * @param bool $versioned Whether the UPDATE should be versioned. | ||
| 391 | * | ||
| 392 | * @throws UnrecognizedField | ||
| 393 | * @throws OptimisticLockException | ||
| 394 | */ | ||
| 395 | final protected function updateTable( | ||
| 396 | object $entity, | ||
| 397 | string $quotedTableName, | ||
| 398 | array $updateData, | ||
| 399 | bool $versioned = false, | ||
| 400 | ): void { | ||
| 401 | $set = []; | ||
| 402 | $types = []; | ||
| 403 | $params = []; | ||
| 404 | |||
| 405 | foreach ($updateData as $columnName => $value) { | ||
| 406 | $placeholder = '?'; | ||
| 407 | $column = $columnName; | ||
| 408 | |||
| 409 | switch (true) { | ||
| 410 | case isset($this->class->fieldNames[$columnName]): | ||
| 411 | $fieldName = $this->class->fieldNames[$columnName]; | ||
| 412 | $column = $this->quoteStrategy->getColumnName($fieldName, $this->class, $this->platform); | ||
| 413 | |||
| 414 | if (isset($this->class->fieldMappings[$fieldName])) { | ||
| 415 | $type = Type::getType($this->columnTypes[$columnName]); | ||
| 416 | $placeholder = $type->convertToDatabaseValueSQL('?', $this->platform); | ||
| 417 | } | ||
| 418 | |||
| 419 | break; | ||
| 420 | |||
| 421 | case isset($this->quotedColumns[$columnName]): | ||
| 422 | $column = $this->quotedColumns[$columnName]; | ||
| 423 | |||
| 424 | break; | ||
| 425 | } | ||
| 426 | |||
| 427 | $params[] = $value; | ||
| 428 | $set[] = $column . ' = ' . $placeholder; | ||
| 429 | $types[] = $this->columnTypes[$columnName]; | ||
| 430 | } | ||
| 431 | |||
| 432 | $where = []; | ||
| 433 | $identifier = $this->em->getUnitOfWork()->getEntityIdentifier($entity); | ||
| 434 | |||
| 435 | foreach ($this->class->identifier as $idField) { | ||
| 436 | if (! isset($this->class->associationMappings[$idField])) { | ||
| 437 | $params[] = $identifier[$idField]; | ||
| 438 | $types[] = $this->class->fieldMappings[$idField]->type; | ||
| 439 | $where[] = $this->quoteStrategy->getColumnName($idField, $this->class, $this->platform); | ||
| 440 | |||
| 441 | continue; | ||
| 442 | } | ||
| 443 | |||
| 444 | assert($this->class->associationMappings[$idField]->isToOneOwningSide()); | ||
| 445 | |||
| 446 | $params[] = $identifier[$idField]; | ||
| 447 | $where[] = $this->quoteStrategy->getJoinColumnName( | ||
| 448 | $this->class->associationMappings[$idField]->joinColumns[0], | ||
| 449 | $this->class, | ||
| 450 | $this->platform, | ||
| 451 | ); | ||
| 452 | |||
| 453 | $targetMapping = $this->em->getClassMetadata($this->class->associationMappings[$idField]->targetEntity); | ||
| 454 | $targetType = PersisterHelper::getTypeOfField($targetMapping->identifier[0], $targetMapping, $this->em); | ||
| 455 | |||
| 456 | if ($targetType === []) { | ||
| 457 | throw UnrecognizedField::byFullyQualifiedName($this->class->name, $targetMapping->identifier[0]); | ||
| 458 | } | ||
| 459 | |||
| 460 | $types[] = reset($targetType); | ||
| 461 | } | ||
| 462 | |||
| 463 | if ($versioned) { | ||
| 464 | $versionField = $this->class->versionField; | ||
| 465 | assert($versionField !== null); | ||
| 466 | $versionFieldType = $this->class->fieldMappings[$versionField]->type; | ||
| 467 | $versionColumn = $this->quoteStrategy->getColumnName($versionField, $this->class, $this->platform); | ||
| 468 | |||
| 469 | $where[] = $versionColumn; | ||
| 470 | $types[] = $this->class->fieldMappings[$versionField]->type; | ||
| 471 | $params[] = $this->class->reflFields[$versionField]->getValue($entity); | ||
| 472 | |||
| 473 | switch ($versionFieldType) { | ||
| 474 | case Types::SMALLINT: | ||
| 475 | case Types::INTEGER: | ||
| 476 | case Types::BIGINT: | ||
| 477 | $set[] = $versionColumn . ' = ' . $versionColumn . ' + 1'; | ||
| 478 | break; | ||
| 479 | |||
| 480 | case Types::DATETIME_MUTABLE: | ||
| 481 | $set[] = $versionColumn . ' = CURRENT_TIMESTAMP'; | ||
| 482 | break; | ||
| 483 | } | ||
| 484 | } | ||
| 485 | |||
| 486 | $sql = 'UPDATE ' . $quotedTableName | ||
| 487 | . ' SET ' . implode(', ', $set) | ||
| 488 | . ' WHERE ' . implode(' = ? AND ', $where) . ' = ?'; | ||
| 489 | |||
| 490 | $result = $this->conn->executeStatement($sql, $params, $types); | ||
| 491 | |||
| 492 | if ($versioned && ! $result) { | ||
| 493 | throw OptimisticLockException::lockFailed($entity); | ||
| 494 | } | ||
| 495 | } | ||
| 496 | |||
| 497 | /** | ||
| 498 | * @param array<mixed> $identifier | ||
| 499 | * @param string[] $types | ||
| 500 | * | ||
| 501 | * @todo Add check for platform if it supports foreign keys/cascading. | ||
| 502 | */ | ||
| 503 | protected function deleteJoinTableRecords(array $identifier, array $types): void | ||
| 504 | { | ||
| 505 | foreach ($this->class->associationMappings as $mapping) { | ||
| 506 | if (! $mapping->isManyToMany() || $mapping->isOnDeleteCascade) { | ||
| 507 | continue; | ||
| 508 | } | ||
| 509 | |||
| 510 | // @Todo this only covers scenarios with no inheritance or of the same level. Is there something | ||
| 511 | // like self-referential relationship between different levels of an inheritance hierarchy? I hope not! | ||
| 512 | $selfReferential = ($mapping->targetEntity === $mapping->sourceEntity); | ||
| 513 | $class = $this->class; | ||
| 514 | $association = $mapping; | ||
| 515 | $otherColumns = []; | ||
| 516 | $otherKeys = []; | ||
| 517 | $keys = []; | ||
| 518 | |||
| 519 | if (! $mapping->isOwningSide()) { | ||
| 520 | $class = $this->em->getClassMetadata($mapping->targetEntity); | ||
| 521 | } | ||
| 522 | |||
| 523 | $association = $this->em->getMetadataFactory()->getOwningSide($association); | ||
| 524 | $joinColumns = $mapping->isOwningSide() | ||
| 525 | ? $association->joinTable->joinColumns | ||
| 526 | : $association->joinTable->inverseJoinColumns; | ||
| 527 | |||
| 528 | if ($selfReferential) { | ||
| 529 | $otherColumns = ! $mapping->isOwningSide() | ||
| 530 | ? $association->joinTable->joinColumns | ||
| 531 | : $association->joinTable->inverseJoinColumns; | ||
| 532 | } | ||
| 533 | |||
| 534 | foreach ($joinColumns as $joinColumn) { | ||
| 535 | $keys[] = $this->quoteStrategy->getJoinColumnName($joinColumn, $class, $this->platform); | ||
| 536 | } | ||
| 537 | |||
| 538 | foreach ($otherColumns as $joinColumn) { | ||
| 539 | $otherKeys[] = $this->quoteStrategy->getJoinColumnName($joinColumn, $class, $this->platform); | ||
| 540 | } | ||
| 541 | |||
| 542 | $joinTableName = $this->quoteStrategy->getJoinTableName($association, $this->class, $this->platform); | ||
| 543 | |||
| 544 | $this->conn->delete($joinTableName, array_combine($keys, $identifier), $types); | ||
| 545 | |||
| 546 | if ($selfReferential) { | ||
| 547 | $this->conn->delete($joinTableName, array_combine($otherKeys, $identifier), $types); | ||
| 548 | } | ||
| 549 | } | ||
| 550 | } | ||
| 551 | |||
| 552 | public function delete(object $entity): bool | ||
| 553 | { | ||
| 554 | $class = $this->class; | ||
| 555 | $identifier = $this->em->getUnitOfWork()->getEntityIdentifier($entity); | ||
| 556 | $tableName = $this->quoteStrategy->getTableName($class, $this->platform); | ||
| 557 | $idColumns = $this->quoteStrategy->getIdentifierColumnNames($class, $this->platform); | ||
| 558 | $id = array_combine($idColumns, $identifier); | ||
| 559 | $types = $this->getClassIdentifiersTypes($class); | ||
| 560 | |||
| 561 | $this->deleteJoinTableRecords($identifier, $types); | ||
| 562 | |||
| 563 | return (bool) $this->conn->delete($tableName, $id, $types); | ||
| 564 | } | ||
| 565 | |||
| 566 | /** | ||
| 567 | * Prepares the changeset of an entity for database insertion (UPDATE). | ||
| 568 | * | ||
| 569 | * The changeset is obtained from the currently running UnitOfWork. | ||
| 570 | * | ||
| 571 | * During this preparation the array that is passed as the second parameter is filled with | ||
| 572 | * <columnName> => <value> pairs, grouped by table name. | ||
| 573 | * | ||
| 574 | * Example: | ||
| 575 | * <code> | ||
| 576 | * array( | ||
| 577 | * 'foo_table' => array('column1' => 'value1', 'column2' => 'value2', ...), | ||
| 578 | * 'bar_table' => array('columnX' => 'valueX', 'columnY' => 'valueY', ...), | ||
| 579 | * ... | ||
| 580 | * ) | ||
| 581 | * </code> | ||
| 582 | * | ||
| 583 | * @param object $entity The entity for which to prepare the data. | ||
| 584 | * @param bool $isInsert Whether the data to be prepared refers to an insert statement. | ||
| 585 | * | ||
| 586 | * @return mixed[][] The prepared data. | ||
| 587 | * @psalm-return array<string, array<array-key, mixed|null>> | ||
| 588 | */ | ||
| 589 | protected function prepareUpdateData(object $entity, bool $isInsert = false): array | ||
| 590 | { | ||
| 591 | $versionField = null; | ||
| 592 | $result = []; | ||
| 593 | $uow = $this->em->getUnitOfWork(); | ||
| 594 | |||
| 595 | $versioned = $this->class->isVersioned; | ||
| 596 | if ($versioned !== false) { | ||
| 597 | $versionField = $this->class->versionField; | ||
| 598 | } | ||
| 599 | |||
| 600 | foreach ($uow->getEntityChangeSet($entity) as $field => $change) { | ||
| 601 | if (isset($versionField) && $versionField === $field) { | ||
| 602 | continue; | ||
| 603 | } | ||
| 604 | |||
| 605 | if (isset($this->class->embeddedClasses[$field])) { | ||
| 606 | continue; | ||
| 607 | } | ||
| 608 | |||
| 609 | $newVal = $change[1]; | ||
| 610 | |||
| 611 | if (! isset($this->class->associationMappings[$field])) { | ||
| 612 | $fieldMapping = $this->class->fieldMappings[$field]; | ||
| 613 | $columnName = $fieldMapping->columnName; | ||
| 614 | |||
| 615 | if (! $isInsert && isset($fieldMapping->notUpdatable)) { | ||
| 616 | continue; | ||
| 617 | } | ||
| 618 | |||
| 619 | if ($isInsert && isset($fieldMapping->notInsertable)) { | ||
| 620 | continue; | ||
| 621 | } | ||
| 622 | |||
| 623 | $this->columnTypes[$columnName] = $fieldMapping->type; | ||
| 624 | |||
| 625 | $result[$this->getOwningTable($field)][$columnName] = $newVal; | ||
| 626 | |||
| 627 | continue; | ||
| 628 | } | ||
| 629 | |||
| 630 | $assoc = $this->class->associationMappings[$field]; | ||
| 631 | |||
| 632 | // Only owning side of x-1 associations can have a FK column. | ||
| 633 | if (! $assoc->isToOneOwningSide()) { | ||
| 634 | continue; | ||
| 635 | } | ||
| 636 | |||
| 637 | if ($newVal !== null) { | ||
| 638 | $oid = spl_object_id($newVal); | ||
| 639 | |||
| 640 | // If the associated entity $newVal is not yet persisted and/or does not yet have | ||
| 641 | // an ID assigned, we must set $newVal = null. This will insert a null value and | ||
| 642 | // schedule an extra update on the UnitOfWork. | ||
| 643 | // | ||
| 644 | // This gives us extra time to a) possibly obtain a database-generated identifier | ||
| 645 | // value for $newVal, and b) insert $newVal into the database before the foreign | ||
| 646 | // key reference is being made. | ||
| 647 | // | ||
| 648 | // When looking at $this->queuedInserts and $uow->isScheduledForInsert, be aware | ||
| 649 | // of the implementation details that our own executeInserts() method will remove | ||
| 650 | // entities from the former as soon as the insert statement has been executed and | ||
| 651 | // a post-insert ID has been assigned (if necessary), and that the UnitOfWork has | ||
| 652 | // already removed entities from its own list at the time they were passed to our | ||
| 653 | // addInsert() method. | ||
| 654 | // | ||
| 655 | // Then, there is one extra exception we can make: An entity that references back to itself | ||
| 656 | // _and_ uses an application-provided ID (the "NONE" generator strategy) also does not | ||
| 657 | // need the extra update, although it is still in the list of insertions itself. | ||
| 658 | // This looks like a minor optimization at first, but is the capstone for being able to | ||
| 659 | // use non-NULLable, self-referencing associations in applications that provide IDs (like UUIDs). | ||
| 660 | if ( | ||
| 661 | (isset($this->queuedInserts[$oid]) || $uow->isScheduledForInsert($newVal)) | ||
| 662 | && ! ($newVal === $entity && $this->class->isIdentifierNatural()) | ||
| 663 | ) { | ||
| 664 | $uow->scheduleExtraUpdate($entity, [$field => [null, $newVal]]); | ||
| 665 | |||
| 666 | $newVal = null; | ||
| 667 | } | ||
| 668 | } | ||
| 669 | |||
| 670 | $newValId = null; | ||
| 671 | |||
| 672 | if ($newVal !== null) { | ||
| 673 | $newValId = $uow->getEntityIdentifier($newVal); | ||
| 674 | } | ||
| 675 | |||
| 676 | $targetClass = $this->em->getClassMetadata($assoc->targetEntity); | ||
| 677 | $owningTable = $this->getOwningTable($field); | ||
| 678 | |||
| 679 | foreach ($assoc->joinColumns as $joinColumn) { | ||
| 680 | $sourceColumn = $joinColumn->name; | ||
| 681 | $targetColumn = $joinColumn->referencedColumnName; | ||
| 682 | $quotedColumn = $this->quoteStrategy->getJoinColumnName($joinColumn, $this->class, $this->platform); | ||
| 683 | |||
| 684 | $this->quotedColumns[$sourceColumn] = $quotedColumn; | ||
| 685 | $this->columnTypes[$sourceColumn] = PersisterHelper::getTypeOfColumn($targetColumn, $targetClass, $this->em); | ||
| 686 | $result[$owningTable][$sourceColumn] = $newValId | ||
| 687 | ? $newValId[$targetClass->getFieldForColumn($targetColumn)] | ||
| 688 | : null; | ||
| 689 | } | ||
| 690 | } | ||
| 691 | |||
| 692 | return $result; | ||
| 693 | } | ||
| 694 | |||
| 695 | /** | ||
| 696 | * Prepares the data changeset of a managed entity for database insertion (initial INSERT). | ||
| 697 | * The changeset of the entity is obtained from the currently running UnitOfWork. | ||
| 698 | * | ||
| 699 | * The default insert data preparation is the same as for updates. | ||
| 700 | * | ||
| 701 | * @see prepareUpdateData | ||
| 702 | * | ||
| 703 | * @param object $entity The entity for which to prepare the data. | ||
| 704 | * | ||
| 705 | * @return mixed[][] The prepared data for the tables to update. | ||
| 706 | * @psalm-return array<string, mixed[]> | ||
| 707 | */ | ||
| 708 | protected function prepareInsertData(object $entity): array | ||
| 709 | { | ||
| 710 | return $this->prepareUpdateData($entity, true); | ||
| 711 | } | ||
| 712 | |||
| 713 | public function getOwningTable(string $fieldName): string | ||
| 714 | { | ||
| 715 | return $this->class->getTableName(); | ||
| 716 | } | ||
| 717 | |||
| 718 | /** | ||
| 719 | * {@inheritDoc} | ||
| 720 | */ | ||
| 721 | public function load( | ||
| 722 | array $criteria, | ||
| 723 | object|null $entity = null, | ||
| 724 | AssociationMapping|null $assoc = null, | ||
| 725 | array $hints = [], | ||
| 726 | LockMode|int|null $lockMode = null, | ||
| 727 | int|null $limit = null, | ||
| 728 | array|null $orderBy = null, | ||
| 729 | ): object|null { | ||
| 730 | $this->switchPersisterContext(null, $limit); | ||
| 731 | |||
| 732 | $sql = $this->getSelectSQL($criteria, $assoc, $lockMode, $limit, null, $orderBy); | ||
| 733 | [$params, $types] = $this->expandParameters($criteria); | ||
| 734 | $stmt = $this->conn->executeQuery($sql, $params, $types); | ||
| 735 | |||
| 736 | if ($entity !== null) { | ||
| 737 | $hints[Query::HINT_REFRESH] = true; | ||
| 738 | $hints[Query::HINT_REFRESH_ENTITY] = $entity; | ||
| 739 | } | ||
| 740 | |||
| 741 | $hydrator = $this->em->newHydrator($this->currentPersisterContext->selectJoinSql ? Query::HYDRATE_OBJECT : Query::HYDRATE_SIMPLEOBJECT); | ||
| 742 | $entities = $hydrator->hydrateAll($stmt, $this->currentPersisterContext->rsm, $hints); | ||
| 743 | |||
| 744 | return $entities ? $entities[0] : null; | ||
| 745 | } | ||
| 746 | |||
| 747 | /** | ||
| 748 | * {@inheritDoc} | ||
| 749 | */ | ||
| 750 | public function loadById(array $identifier, object|null $entity = null): object|null | ||
| 751 | { | ||
| 752 | return $this->load($identifier, $entity); | ||
| 753 | } | ||
| 754 | |||
| 755 | /** | ||
| 756 | * {@inheritDoc} | ||
| 757 | */ | ||
| 758 | public function loadOneToOneEntity(AssociationMapping $assoc, object $sourceEntity, array $identifier = []): object|null | ||
| 759 | { | ||
| 760 | $foundEntity = $this->em->getUnitOfWork()->tryGetById($identifier, $assoc->targetEntity); | ||
| 761 | if ($foundEntity !== false) { | ||
| 762 | return $foundEntity; | ||
| 763 | } | ||
| 764 | |||
| 765 | $targetClass = $this->em->getClassMetadata($assoc->targetEntity); | ||
| 766 | |||
| 767 | if ($assoc->isOwningSide()) { | ||
| 768 | $isInverseSingleValued = $assoc->inversedBy !== null && ! $targetClass->isCollectionValuedAssociation($assoc->inversedBy); | ||
| 769 | |||
| 770 | // Mark inverse side as fetched in the hints, otherwise the UoW would | ||
| 771 | // try to load it in a separate query (remember: to-one inverse sides can not be lazy). | ||
| 772 | $hints = []; | ||
| 773 | |||
| 774 | if ($isInverseSingleValued) { | ||
| 775 | $hints['fetched']['r'][$assoc->inversedBy] = true; | ||
| 776 | } | ||
| 777 | |||
| 778 | $targetEntity = $this->load($identifier, null, $assoc, $hints); | ||
| 779 | |||
| 780 | // Complete bidirectional association, if necessary | ||
| 781 | if ($targetEntity !== null && $isInverseSingleValued) { | ||
| 782 | $targetClass->reflFields[$assoc->inversedBy]->setValue($targetEntity, $sourceEntity); | ||
| 783 | } | ||
| 784 | |||
| 785 | return $targetEntity; | ||
| 786 | } | ||
| 787 | |||
| 788 | assert(isset($assoc->mappedBy)); | ||
| 789 | $sourceClass = $this->em->getClassMetadata($assoc->sourceEntity); | ||
| 790 | $owningAssoc = $targetClass->getAssociationMapping($assoc->mappedBy); | ||
| 791 | assert($owningAssoc->isOneToOneOwningSide()); | ||
| 792 | |||
| 793 | $computedIdentifier = []; | ||
| 794 | |||
| 795 | // TRICKY: since the association is specular source and target are flipped | ||
| 796 | foreach ($owningAssoc->targetToSourceKeyColumns as $sourceKeyColumn => $targetKeyColumn) { | ||
| 797 | if (! isset($sourceClass->fieldNames[$sourceKeyColumn])) { | ||
| 798 | throw MappingException::joinColumnMustPointToMappedField( | ||
| 799 | $sourceClass->name, | ||
| 800 | $sourceKeyColumn, | ||
| 801 | ); | ||
| 802 | } | ||
| 803 | |||
| 804 | $computedIdentifier[$targetClass->getFieldForColumn($targetKeyColumn)] = | ||
| 805 | $sourceClass->reflFields[$sourceClass->fieldNames[$sourceKeyColumn]]->getValue($sourceEntity); | ||
| 806 | } | ||
| 807 | |||
| 808 | $targetEntity = $this->load($computedIdentifier, null, $assoc); | ||
| 809 | |||
| 810 | if ($targetEntity !== null) { | ||
| 811 | $targetClass->setFieldValue($targetEntity, $assoc->mappedBy, $sourceEntity); | ||
| 812 | } | ||
| 813 | |||
| 814 | return $targetEntity; | ||
| 815 | } | ||
| 816 | |||
| 817 | /** | ||
| 818 | * {@inheritDoc} | ||
| 819 | */ | ||
| 820 | public function refresh(array $id, object $entity, LockMode|int|null $lockMode = null): void | ||
| 821 | { | ||
| 822 | $sql = $this->getSelectSQL($id, null, $lockMode); | ||
| 823 | [$params, $types] = $this->expandParameters($id); | ||
| 824 | $stmt = $this->conn->executeQuery($sql, $params, $types); | ||
| 825 | |||
| 826 | $hydrator = $this->em->newHydrator(Query::HYDRATE_OBJECT); | ||
| 827 | $hydrator->hydrateAll($stmt, $this->currentPersisterContext->rsm, [Query::HINT_REFRESH => true]); | ||
| 828 | } | ||
| 829 | |||
| 830 | public function count(array|Criteria $criteria = []): int | ||
| 831 | { | ||
| 832 | $sql = $this->getCountSQL($criteria); | ||
| 833 | |||
| 834 | [$params, $types] = $criteria instanceof Criteria | ||
| 835 | ? $this->expandCriteriaParameters($criteria) | ||
| 836 | : $this->expandParameters($criteria); | ||
| 837 | |||
| 838 | return (int) $this->conn->executeQuery($sql, $params, $types)->fetchOne(); | ||
| 839 | } | ||
| 840 | |||
| 841 | /** | ||
| 842 | * {@inheritDoc} | ||
| 843 | */ | ||
| 844 | public function loadCriteria(Criteria $criteria): array | ||
| 845 | { | ||
| 846 | $orderBy = array_map( | ||
| 847 | static fn (Order $order): string => $order->value, | ||
| 848 | $criteria->orderings(), | ||
| 849 | ); | ||
| 850 | $limit = $criteria->getMaxResults(); | ||
| 851 | $offset = $criteria->getFirstResult(); | ||
| 852 | $query = $this->getSelectSQL($criteria, null, null, $limit, $offset, $orderBy); | ||
| 853 | |||
| 854 | [$params, $types] = $this->expandCriteriaParameters($criteria); | ||
| 855 | |||
| 856 | $stmt = $this->conn->executeQuery($query, $params, $types); | ||
| 857 | $hydrator = $this->em->newHydrator($this->currentPersisterContext->selectJoinSql ? Query::HYDRATE_OBJECT : Query::HYDRATE_SIMPLEOBJECT); | ||
| 858 | |||
| 859 | return $hydrator->hydrateAll($stmt, $this->currentPersisterContext->rsm, [UnitOfWork::HINT_DEFEREAGERLOAD => true]); | ||
| 860 | } | ||
| 861 | |||
| 862 | /** | ||
| 863 | * {@inheritDoc} | ||
| 864 | */ | ||
| 865 | public function expandCriteriaParameters(Criteria $criteria): array | ||
| 866 | { | ||
| 867 | $expression = $criteria->getWhereExpression(); | ||
| 868 | $sqlParams = []; | ||
| 869 | $sqlTypes = []; | ||
| 870 | |||
| 871 | if ($expression === null) { | ||
| 872 | return [$sqlParams, $sqlTypes]; | ||
| 873 | } | ||
| 874 | |||
| 875 | $valueVisitor = new SqlValueVisitor(); | ||
| 876 | |||
| 877 | $valueVisitor->dispatch($expression); | ||
| 878 | |||
| 879 | [, $types] = $valueVisitor->getParamsAndTypes(); | ||
| 880 | |||
| 881 | foreach ($types as $type) { | ||
| 882 | [$field, $value, $operator] = $type; | ||
| 883 | |||
| 884 | if ($value === null && ($operator === Comparison::EQ || $operator === Comparison::NEQ)) { | ||
| 885 | continue; | ||
| 886 | } | ||
| 887 | |||
| 888 | $sqlParams = [...$sqlParams, ...$this->getValues($value)]; | ||
| 889 | $sqlTypes = [...$sqlTypes, ...$this->getTypes($field, $value, $this->class)]; | ||
| 890 | } | ||
| 891 | |||
| 892 | return [$sqlParams, $sqlTypes]; | ||
| 893 | } | ||
| 894 | |||
| 895 | /** | ||
| 896 | * {@inheritDoc} | ||
| 897 | */ | ||
| 898 | public function loadAll( | ||
| 899 | array $criteria = [], | ||
| 900 | array|null $orderBy = null, | ||
| 901 | int|null $limit = null, | ||
| 902 | int|null $offset = null, | ||
| 903 | ): array { | ||
| 904 | $this->switchPersisterContext($offset, $limit); | ||
| 905 | |||
| 906 | $sql = $this->getSelectSQL($criteria, null, null, $limit, $offset, $orderBy); | ||
| 907 | [$params, $types] = $this->expandParameters($criteria); | ||
| 908 | $stmt = $this->conn->executeQuery($sql, $params, $types); | ||
| 909 | |||
| 910 | $hydrator = $this->em->newHydrator($this->currentPersisterContext->selectJoinSql ? Query::HYDRATE_OBJECT : Query::HYDRATE_SIMPLEOBJECT); | ||
| 911 | |||
| 912 | return $hydrator->hydrateAll($stmt, $this->currentPersisterContext->rsm, [UnitOfWork::HINT_DEFEREAGERLOAD => true]); | ||
| 913 | } | ||
| 914 | |||
| 915 | /** | ||
| 916 | * {@inheritDoc} | ||
| 917 | */ | ||
| 918 | public function getManyToManyCollection( | ||
| 919 | AssociationMapping $assoc, | ||
| 920 | object $sourceEntity, | ||
| 921 | int|null $offset = null, | ||
| 922 | int|null $limit = null, | ||
| 923 | ): array { | ||
| 924 | assert($assoc->isManyToMany()); | ||
| 925 | $this->switchPersisterContext($offset, $limit); | ||
| 926 | |||
| 927 | $stmt = $this->getManyToManyStatement($assoc, $sourceEntity, $offset, $limit); | ||
| 928 | |||
| 929 | return $this->loadArrayFromResult($assoc, $stmt); | ||
| 930 | } | ||
| 931 | |||
| 932 | /** | ||
| 933 | * Loads an array of entities from a given DBAL statement. | ||
| 934 | * | ||
| 935 | * @return mixed[] | ||
| 936 | */ | ||
| 937 | private function loadArrayFromResult(AssociationMapping $assoc, Result $stmt): array | ||
| 938 | { | ||
| 939 | $rsm = $this->currentPersisterContext->rsm; | ||
| 940 | $hints = [UnitOfWork::HINT_DEFEREAGERLOAD => true]; | ||
| 941 | |||
| 942 | if ($assoc->isIndexed()) { | ||
| 943 | $rsm = clone $this->currentPersisterContext->rsm; // this is necessary because the "default rsm" should be changed. | ||
| 944 | $rsm->addIndexBy('r', $assoc->indexBy()); | ||
| 945 | } | ||
| 946 | |||
| 947 | return $this->em->newHydrator(Query::HYDRATE_OBJECT)->hydrateAll($stmt, $rsm, $hints); | ||
| 948 | } | ||
| 949 | |||
| 950 | /** | ||
| 951 | * Hydrates a collection from a given DBAL statement. | ||
| 952 | * | ||
| 953 | * @return mixed[] | ||
| 954 | */ | ||
| 955 | private function loadCollectionFromStatement( | ||
| 956 | AssociationMapping $assoc, | ||
| 957 | Result $stmt, | ||
| 958 | PersistentCollection $coll, | ||
| 959 | ): array { | ||
| 960 | $rsm = $this->currentPersisterContext->rsm; | ||
| 961 | $hints = [ | ||
| 962 | UnitOfWork::HINT_DEFEREAGERLOAD => true, | ||
| 963 | 'collection' => $coll, | ||
| 964 | ]; | ||
| 965 | |||
| 966 | if ($assoc->isIndexed()) { | ||
| 967 | $rsm = clone $this->currentPersisterContext->rsm; // this is necessary because the "default rsm" should be changed. | ||
| 968 | $rsm->addIndexBy('r', $assoc->indexBy()); | ||
| 969 | } | ||
| 970 | |||
| 971 | return $this->em->newHydrator(Query::HYDRATE_OBJECT)->hydrateAll($stmt, $rsm, $hints); | ||
| 972 | } | ||
| 973 | |||
| 974 | /** | ||
| 975 | * {@inheritDoc} | ||
| 976 | */ | ||
| 977 | public function loadManyToManyCollection(AssociationMapping $assoc, object $sourceEntity, PersistentCollection $collection): array | ||
| 978 | { | ||
| 979 | assert($assoc->isManyToMany()); | ||
| 980 | $stmt = $this->getManyToManyStatement($assoc, $sourceEntity); | ||
| 981 | |||
| 982 | return $this->loadCollectionFromStatement($assoc, $stmt, $collection); | ||
| 983 | } | ||
| 984 | |||
| 985 | /** @throws MappingException */ | ||
| 986 | private function getManyToManyStatement( | ||
| 987 | AssociationMapping&ManyToManyAssociationMapping $assoc, | ||
| 988 | object $sourceEntity, | ||
| 989 | int|null $offset = null, | ||
| 990 | int|null $limit = null, | ||
| 991 | ): Result { | ||
| 992 | $this->switchPersisterContext($offset, $limit); | ||
| 993 | |||
| 994 | $sourceClass = $this->em->getClassMetadata($assoc->sourceEntity); | ||
| 995 | $class = $sourceClass; | ||
| 996 | $association = $assoc; | ||
| 997 | $criteria = []; | ||
| 998 | $parameters = []; | ||
| 999 | |||
| 1000 | if (! $assoc->isOwningSide()) { | ||
| 1001 | $class = $this->em->getClassMetadata($assoc->targetEntity); | ||
| 1002 | } | ||
| 1003 | |||
| 1004 | $association = $this->em->getMetadataFactory()->getOwningSide($assoc); | ||
| 1005 | $joinColumns = $assoc->isOwningSide() | ||
| 1006 | ? $association->joinTable->joinColumns | ||
| 1007 | : $association->joinTable->inverseJoinColumns; | ||
| 1008 | |||
| 1009 | $quotedJoinTable = $this->quoteStrategy->getJoinTableName($association, $class, $this->platform); | ||
| 1010 | |||
| 1011 | foreach ($joinColumns as $joinColumn) { | ||
| 1012 | $sourceKeyColumn = $joinColumn->referencedColumnName; | ||
| 1013 | $quotedKeyColumn = $this->quoteStrategy->getJoinColumnName($joinColumn, $class, $this->platform); | ||
| 1014 | |||
| 1015 | switch (true) { | ||
| 1016 | case $sourceClass->containsForeignIdentifier: | ||
| 1017 | $field = $sourceClass->getFieldForColumn($sourceKeyColumn); | ||
| 1018 | $value = $sourceClass->reflFields[$field]->getValue($sourceEntity); | ||
| 1019 | |||
| 1020 | if (isset($sourceClass->associationMappings[$field])) { | ||
| 1021 | $value = $this->em->getUnitOfWork()->getEntityIdentifier($value); | ||
| 1022 | $value = $value[$this->em->getClassMetadata($sourceClass->associationMappings[$field]->targetEntity)->identifier[0]]; | ||
| 1023 | } | ||
| 1024 | |||
| 1025 | break; | ||
| 1026 | |||
| 1027 | case isset($sourceClass->fieldNames[$sourceKeyColumn]): | ||
| 1028 | $field = $sourceClass->fieldNames[$sourceKeyColumn]; | ||
| 1029 | $value = $sourceClass->reflFields[$field]->getValue($sourceEntity); | ||
| 1030 | |||
| 1031 | break; | ||
| 1032 | |||
| 1033 | default: | ||
| 1034 | throw MappingException::joinColumnMustPointToMappedField( | ||
| 1035 | $sourceClass->name, | ||
| 1036 | $sourceKeyColumn, | ||
| 1037 | ); | ||
| 1038 | } | ||
| 1039 | |||
| 1040 | $criteria[$quotedJoinTable . '.' . $quotedKeyColumn] = $value; | ||
| 1041 | $parameters[] = [ | ||
| 1042 | 'value' => $value, | ||
| 1043 | 'field' => $field, | ||
| 1044 | 'class' => $sourceClass, | ||
| 1045 | ]; | ||
| 1046 | } | ||
| 1047 | |||
| 1048 | $sql = $this->getSelectSQL($criteria, $assoc, null, $limit, $offset); | ||
| 1049 | [$params, $types] = $this->expandToManyParameters($parameters); | ||
| 1050 | |||
| 1051 | return $this->conn->executeQuery($sql, $params, $types); | ||
| 1052 | } | ||
| 1053 | |||
| 1054 | public function getSelectSQL( | ||
| 1055 | array|Criteria $criteria, | ||
| 1056 | AssociationMapping|null $assoc = null, | ||
| 1057 | LockMode|int|null $lockMode = null, | ||
| 1058 | int|null $limit = null, | ||
| 1059 | int|null $offset = null, | ||
| 1060 | array|null $orderBy = null, | ||
| 1061 | ): string { | ||
| 1062 | $this->switchPersisterContext($offset, $limit); | ||
| 1063 | |||
| 1064 | $joinSql = ''; | ||
| 1065 | $orderBySql = ''; | ||
| 1066 | |||
| 1067 | if ($assoc !== null && $assoc->isManyToMany()) { | ||
| 1068 | $joinSql = $this->getSelectManyToManyJoinSQL($assoc); | ||
| 1069 | } | ||
| 1070 | |||
| 1071 | if ($assoc !== null && $assoc->isOrdered()) { | ||
| 1072 | $orderBy = $assoc->orderBy(); | ||
| 1073 | } | ||
| 1074 | |||
| 1075 | if ($orderBy) { | ||
| 1076 | $orderBySql = $this->getOrderBySQL($orderBy, $this->getSQLTableAlias($this->class->name)); | ||
| 1077 | } | ||
| 1078 | |||
| 1079 | $conditionSql = $criteria instanceof Criteria | ||
| 1080 | ? $this->getSelectConditionCriteriaSQL($criteria) | ||
| 1081 | : $this->getSelectConditionSQL($criteria, $assoc); | ||
| 1082 | |||
| 1083 | $lockSql = match ($lockMode) { | ||
| 1084 | LockMode::PESSIMISTIC_READ => ' ' . $this->getReadLockSQL($this->platform), | ||
| 1085 | LockMode::PESSIMISTIC_WRITE => ' ' . $this->getWriteLockSQL($this->platform), | ||
| 1086 | default => '', | ||
| 1087 | }; | ||
| 1088 | |||
| 1089 | $columnList = $this->getSelectColumnsSQL(); | ||
| 1090 | $tableAlias = $this->getSQLTableAlias($this->class->name); | ||
| 1091 | $filterSql = $this->generateFilterConditionSQL($this->class, $tableAlias); | ||
| 1092 | $tableName = $this->quoteStrategy->getTableName($this->class, $this->platform); | ||
| 1093 | |||
| 1094 | if ($filterSql !== '') { | ||
| 1095 | $conditionSql = $conditionSql | ||
| 1096 | ? $conditionSql . ' AND ' . $filterSql | ||
| 1097 | : $filterSql; | ||
| 1098 | } | ||
| 1099 | |||
| 1100 | $select = 'SELECT ' . $columnList; | ||
| 1101 | $from = ' FROM ' . $tableName . ' ' . $tableAlias; | ||
| 1102 | $join = $this->currentPersisterContext->selectJoinSql . $joinSql; | ||
| 1103 | $where = ($conditionSql ? ' WHERE ' . $conditionSql : ''); | ||
| 1104 | $lock = $this->platform->appendLockHint($from, $lockMode ?? LockMode::NONE); | ||
| 1105 | $query = $select | ||
| 1106 | . $lock | ||
| 1107 | . $join | ||
| 1108 | . $where | ||
| 1109 | . $orderBySql; | ||
| 1110 | |||
| 1111 | return $this->platform->modifyLimitQuery($query, $limit, $offset ?? 0) . $lockSql; | ||
| 1112 | } | ||
| 1113 | |||
| 1114 | public function getCountSQL(array|Criteria $criteria = []): string | ||
| 1115 | { | ||
| 1116 | $tableName = $this->quoteStrategy->getTableName($this->class, $this->platform); | ||
| 1117 | $tableAlias = $this->getSQLTableAlias($this->class->name); | ||
| 1118 | |||
| 1119 | $conditionSql = $criteria instanceof Criteria | ||
| 1120 | ? $this->getSelectConditionCriteriaSQL($criteria) | ||
| 1121 | : $this->getSelectConditionSQL($criteria); | ||
| 1122 | |||
| 1123 | $filterSql = $this->generateFilterConditionSQL($this->class, $tableAlias); | ||
| 1124 | |||
| 1125 | if ($filterSql !== '') { | ||
| 1126 | $conditionSql = $conditionSql | ||
| 1127 | ? $conditionSql . ' AND ' . $filterSql | ||
| 1128 | : $filterSql; | ||
| 1129 | } | ||
| 1130 | |||
| 1131 | return 'SELECT COUNT(*) ' | ||
| 1132 | . 'FROM ' . $tableName . ' ' . $tableAlias | ||
| 1133 | . (empty($conditionSql) ? '' : ' WHERE ' . $conditionSql); | ||
| 1134 | } | ||
| 1135 | |||
| 1136 | /** | ||
| 1137 | * Gets the ORDER BY SQL snippet for ordered collections. | ||
| 1138 | * | ||
| 1139 | * @psalm-param array<string, string> $orderBy | ||
| 1140 | * | ||
| 1141 | * @throws InvalidOrientation | ||
| 1142 | * @throws InvalidFindByCall | ||
| 1143 | * @throws UnrecognizedField | ||
| 1144 | */ | ||
| 1145 | final protected function getOrderBySQL(array $orderBy, string $baseTableAlias): string | ||
| 1146 | { | ||
| 1147 | $orderByList = []; | ||
| 1148 | |||
| 1149 | foreach ($orderBy as $fieldName => $orientation) { | ||
| 1150 | $orientation = strtoupper(trim($orientation)); | ||
| 1151 | |||
| 1152 | if ($orientation !== 'ASC' && $orientation !== 'DESC') { | ||
| 1153 | throw InvalidOrientation::fromClassNameAndField($this->class->name, $fieldName); | ||
| 1154 | } | ||
| 1155 | |||
| 1156 | if (isset($this->class->fieldMappings[$fieldName])) { | ||
| 1157 | $tableAlias = isset($this->class->fieldMappings[$fieldName]->inherited) | ||
| 1158 | ? $this->getSQLTableAlias($this->class->fieldMappings[$fieldName]->inherited) | ||
| 1159 | : $baseTableAlias; | ||
| 1160 | |||
| 1161 | $columnName = $this->quoteStrategy->getColumnName($fieldName, $this->class, $this->platform); | ||
| 1162 | $orderByList[] = $tableAlias . '.' . $columnName . ' ' . $orientation; | ||
| 1163 | |||
| 1164 | continue; | ||
| 1165 | } | ||
| 1166 | |||
| 1167 | if (isset($this->class->associationMappings[$fieldName])) { | ||
| 1168 | $association = $this->class->associationMappings[$fieldName]; | ||
| 1169 | if (! $association->isOwningSide()) { | ||
| 1170 | throw InvalidFindByCall::fromInverseSideUsage($this->class->name, $fieldName); | ||
| 1171 | } | ||
| 1172 | |||
| 1173 | assert($association->isToOneOwningSide()); | ||
| 1174 | |||
| 1175 | $tableAlias = isset($association->inherited) | ||
| 1176 | ? $this->getSQLTableAlias($association->inherited) | ||
| 1177 | : $baseTableAlias; | ||
| 1178 | |||
| 1179 | foreach ($association->joinColumns as $joinColumn) { | ||
| 1180 | $columnName = $this->quoteStrategy->getJoinColumnName($joinColumn, $this->class, $this->platform); | ||
| 1181 | $orderByList[] = $tableAlias . '.' . $columnName . ' ' . $orientation; | ||
| 1182 | } | ||
| 1183 | |||
| 1184 | continue; | ||
| 1185 | } | ||
| 1186 | |||
| 1187 | throw UnrecognizedField::byFullyQualifiedName($this->class->name, $fieldName); | ||
| 1188 | } | ||
| 1189 | |||
| 1190 | return ' ORDER BY ' . implode(', ', $orderByList); | ||
| 1191 | } | ||
| 1192 | |||
| 1193 | /** | ||
| 1194 | * Gets the SQL fragment with the list of columns to select when querying for | ||
| 1195 | * an entity in this persister. | ||
| 1196 | * | ||
| 1197 | * Subclasses should override this method to alter or change the select column | ||
| 1198 | * list SQL fragment. Note that in the implementation of BasicEntityPersister | ||
| 1199 | * the resulting SQL fragment is generated only once and cached in {@link selectColumnListSql}. | ||
| 1200 | * Subclasses may or may not do the same. | ||
| 1201 | */ | ||
| 1202 | protected function getSelectColumnsSQL(): string | ||
| 1203 | { | ||
| 1204 | if ($this->currentPersisterContext->selectColumnListSql !== null) { | ||
| 1205 | return $this->currentPersisterContext->selectColumnListSql; | ||
| 1206 | } | ||
| 1207 | |||
| 1208 | $columnList = []; | ||
| 1209 | $this->currentPersisterContext->rsm->addEntityResult($this->class->name, 'r'); // r for root | ||
| 1210 | |||
| 1211 | // Add regular columns to select list | ||
| 1212 | foreach ($this->class->fieldNames as $field) { | ||
| 1213 | $columnList[] = $this->getSelectColumnSQL($field, $this->class); | ||
| 1214 | } | ||
| 1215 | |||
| 1216 | $this->currentPersisterContext->selectJoinSql = ''; | ||
| 1217 | $eagerAliasCounter = 0; | ||
| 1218 | |||
| 1219 | foreach ($this->class->associationMappings as $assocField => $assoc) { | ||
| 1220 | $assocColumnSQL = $this->getSelectColumnAssociationSQL($assocField, $assoc, $this->class); | ||
| 1221 | |||
| 1222 | if ($assocColumnSQL) { | ||
| 1223 | $columnList[] = $assocColumnSQL; | ||
| 1224 | } | ||
| 1225 | |||
| 1226 | $isAssocToOneInverseSide = $assoc->isToOne() && ! $assoc->isOwningSide(); | ||
| 1227 | $isAssocFromOneEager = $assoc->isToOne() && $assoc->fetch === ClassMetadata::FETCH_EAGER; | ||
| 1228 | |||
| 1229 | if (! ($isAssocFromOneEager || $isAssocToOneInverseSide)) { | ||
| 1230 | continue; | ||
| 1231 | } | ||
| 1232 | |||
| 1233 | if ($assoc->isToMany() && $this->currentPersisterContext->handlesLimits) { | ||
| 1234 | continue; | ||
| 1235 | } | ||
| 1236 | |||
| 1237 | $eagerEntity = $this->em->getClassMetadata($assoc->targetEntity); | ||
| 1238 | |||
| 1239 | if ($eagerEntity->inheritanceType !== ClassMetadata::INHERITANCE_TYPE_NONE) { | ||
| 1240 | continue; // now this is why you shouldn't use inheritance | ||
| 1241 | } | ||
| 1242 | |||
| 1243 | $assocAlias = 'e' . ($eagerAliasCounter++); | ||
| 1244 | $this->currentPersisterContext->rsm->addJoinedEntityResult($assoc->targetEntity, $assocAlias, 'r', $assocField); | ||
| 1245 | |||
| 1246 | foreach ($eagerEntity->fieldNames as $field) { | ||
| 1247 | $columnList[] = $this->getSelectColumnSQL($field, $eagerEntity, $assocAlias); | ||
| 1248 | } | ||
| 1249 | |||
| 1250 | foreach ($eagerEntity->associationMappings as $eagerAssocField => $eagerAssoc) { | ||
| 1251 | $eagerAssocColumnSQL = $this->getSelectColumnAssociationSQL( | ||
| 1252 | $eagerAssocField, | ||
| 1253 | $eagerAssoc, | ||
| 1254 | $eagerEntity, | ||
| 1255 | $assocAlias, | ||
| 1256 | ); | ||
| 1257 | |||
| 1258 | if ($eagerAssocColumnSQL) { | ||
| 1259 | $columnList[] = $eagerAssocColumnSQL; | ||
| 1260 | } | ||
| 1261 | } | ||
| 1262 | |||
| 1263 | $association = $assoc; | ||
| 1264 | $joinCondition = []; | ||
| 1265 | |||
| 1266 | if ($assoc->isIndexed()) { | ||
| 1267 | assert($assoc->isToMany()); | ||
| 1268 | $this->currentPersisterContext->rsm->addIndexBy($assocAlias, $assoc->indexBy()); | ||
| 1269 | } | ||
| 1270 | |||
| 1271 | if (! $assoc->isOwningSide()) { | ||
| 1272 | $eagerEntity = $this->em->getClassMetadata($assoc->targetEntity); | ||
| 1273 | $association = $eagerEntity->getAssociationMapping($assoc->mappedBy); | ||
| 1274 | } | ||
| 1275 | |||
| 1276 | assert($association->isToOneOwningSide()); | ||
| 1277 | |||
| 1278 | $joinTableAlias = $this->getSQLTableAlias($eagerEntity->name, $assocAlias); | ||
| 1279 | $joinTableName = $this->quoteStrategy->getTableName($eagerEntity, $this->platform); | ||
| 1280 | |||
| 1281 | if ($assoc->isOwningSide()) { | ||
| 1282 | $tableAlias = $this->getSQLTableAlias($association->targetEntity, $assocAlias); | ||
| 1283 | $this->currentPersisterContext->selectJoinSql .= ' ' . $this->getJoinSQLForJoinColumns($association->joinColumns); | ||
| 1284 | |||
| 1285 | foreach ($association->joinColumns as $joinColumn) { | ||
| 1286 | $sourceCol = $this->quoteStrategy->getJoinColumnName($joinColumn, $this->class, $this->platform); | ||
| 1287 | $targetCol = $this->quoteStrategy->getReferencedJoinColumnName($joinColumn, $this->class, $this->platform); | ||
| 1288 | $joinCondition[] = $this->getSQLTableAlias($association->sourceEntity) | ||
| 1289 | . '.' . $sourceCol . ' = ' . $tableAlias . '.' . $targetCol; | ||
| 1290 | } | ||
| 1291 | |||
| 1292 | // Add filter SQL | ||
| 1293 | $filterSql = $this->generateFilterConditionSQL($eagerEntity, $tableAlias); | ||
| 1294 | if ($filterSql) { | ||
| 1295 | $joinCondition[] = $filterSql; | ||
| 1296 | } | ||
| 1297 | } else { | ||
| 1298 | $this->currentPersisterContext->selectJoinSql .= ' LEFT JOIN'; | ||
| 1299 | |||
| 1300 | foreach ($association->joinColumns as $joinColumn) { | ||
| 1301 | $sourceCol = $this->quoteStrategy->getJoinColumnName($joinColumn, $this->class, $this->platform); | ||
| 1302 | $targetCol = $this->quoteStrategy->getReferencedJoinColumnName($joinColumn, $this->class, $this->platform); | ||
| 1303 | |||
| 1304 | $joinCondition[] = $this->getSQLTableAlias($association->sourceEntity, $assocAlias) . '.' . $sourceCol . ' = ' | ||
| 1305 | . $this->getSQLTableAlias($association->targetEntity) . '.' . $targetCol; | ||
| 1306 | } | ||
| 1307 | } | ||
| 1308 | |||
| 1309 | $this->currentPersisterContext->selectJoinSql .= ' ' . $joinTableName . ' ' . $joinTableAlias . ' ON '; | ||
| 1310 | $this->currentPersisterContext->selectJoinSql .= implode(' AND ', $joinCondition); | ||
| 1311 | } | ||
| 1312 | |||
| 1313 | $this->currentPersisterContext->selectColumnListSql = implode(', ', $columnList); | ||
| 1314 | |||
| 1315 | return $this->currentPersisterContext->selectColumnListSql; | ||
| 1316 | } | ||
| 1317 | |||
| 1318 | /** Gets the SQL join fragment used when selecting entities from an association. */ | ||
| 1319 | protected function getSelectColumnAssociationSQL( | ||
| 1320 | string $field, | ||
| 1321 | AssociationMapping $assoc, | ||
| 1322 | ClassMetadata $class, | ||
| 1323 | string $alias = 'r', | ||
| 1324 | ): string { | ||
| 1325 | if (! $assoc->isToOneOwningSide()) { | ||
| 1326 | return ''; | ||
| 1327 | } | ||
| 1328 | |||
| 1329 | $columnList = []; | ||
| 1330 | $targetClass = $this->em->getClassMetadata($assoc->targetEntity); | ||
| 1331 | $isIdentifier = isset($assoc->id) && $assoc->id === true; | ||
| 1332 | $sqlTableAlias = $this->getSQLTableAlias($class->name, ($alias === 'r' ? '' : $alias)); | ||
| 1333 | |||
| 1334 | foreach ($assoc->joinColumns as $joinColumn) { | ||
| 1335 | $quotedColumn = $this->quoteStrategy->getJoinColumnName($joinColumn, $this->class, $this->platform); | ||
| 1336 | $resultColumnName = $this->getSQLColumnAlias($joinColumn->name); | ||
| 1337 | $type = PersisterHelper::getTypeOfColumn($joinColumn->referencedColumnName, $targetClass, $this->em); | ||
| 1338 | |||
| 1339 | $this->currentPersisterContext->rsm->addMetaResult($alias, $resultColumnName, $joinColumn->name, $isIdentifier, $type); | ||
| 1340 | |||
| 1341 | $columnList[] = sprintf('%s.%s AS %s', $sqlTableAlias, $quotedColumn, $resultColumnName); | ||
| 1342 | } | ||
| 1343 | |||
| 1344 | return implode(', ', $columnList); | ||
| 1345 | } | ||
| 1346 | |||
| 1347 | /** | ||
| 1348 | * Gets the SQL join fragment used when selecting entities from a | ||
| 1349 | * many-to-many association. | ||
| 1350 | */ | ||
| 1351 | protected function getSelectManyToManyJoinSQL(AssociationMapping&ManyToManyAssociationMapping $manyToMany): string | ||
| 1352 | { | ||
| 1353 | $conditions = []; | ||
| 1354 | $association = $manyToMany; | ||
| 1355 | $sourceTableAlias = $this->getSQLTableAlias($this->class->name); | ||
| 1356 | |||
| 1357 | $association = $this->em->getMetadataFactory()->getOwningSide($manyToMany); | ||
| 1358 | $joinTableName = $this->quoteStrategy->getJoinTableName($association, $this->class, $this->platform); | ||
| 1359 | $joinColumns = $manyToMany->isOwningSide() | ||
| 1360 | ? $association->joinTable->inverseJoinColumns | ||
| 1361 | : $association->joinTable->joinColumns; | ||
| 1362 | |||
| 1363 | foreach ($joinColumns as $joinColumn) { | ||
| 1364 | $quotedSourceColumn = $this->quoteStrategy->getJoinColumnName($joinColumn, $this->class, $this->platform); | ||
| 1365 | $quotedTargetColumn = $this->quoteStrategy->getReferencedJoinColumnName($joinColumn, $this->class, $this->platform); | ||
| 1366 | $conditions[] = $sourceTableAlias . '.' . $quotedTargetColumn . ' = ' . $joinTableName . '.' . $quotedSourceColumn; | ||
| 1367 | } | ||
| 1368 | |||
| 1369 | return ' INNER JOIN ' . $joinTableName . ' ON ' . implode(' AND ', $conditions); | ||
| 1370 | } | ||
| 1371 | |||
| 1372 | public function getInsertSQL(): string | ||
| 1373 | { | ||
| 1374 | if ($this->insertSql !== null) { | ||
| 1375 | return $this->insertSql; | ||
| 1376 | } | ||
| 1377 | |||
| 1378 | $columns = $this->getInsertColumnList(); | ||
| 1379 | $tableName = $this->quoteStrategy->getTableName($this->class, $this->platform); | ||
| 1380 | |||
| 1381 | if (empty($columns)) { | ||
| 1382 | $identityColumn = $this->quoteStrategy->getColumnName($this->class->identifier[0], $this->class, $this->platform); | ||
| 1383 | $this->insertSql = $this->platform->getEmptyIdentityInsertSQL($tableName, $identityColumn); | ||
| 1384 | |||
| 1385 | return $this->insertSql; | ||
| 1386 | } | ||
| 1387 | |||
| 1388 | $values = []; | ||
| 1389 | $columns = array_unique($columns); | ||
| 1390 | |||
| 1391 | foreach ($columns as $column) { | ||
| 1392 | $placeholder = '?'; | ||
| 1393 | |||
| 1394 | if ( | ||
| 1395 | isset($this->class->fieldNames[$column]) | ||
| 1396 | && isset($this->columnTypes[$this->class->fieldNames[$column]]) | ||
| 1397 | && isset($this->class->fieldMappings[$this->class->fieldNames[$column]]) | ||
| 1398 | ) { | ||
| 1399 | $type = Type::getType($this->columnTypes[$this->class->fieldNames[$column]]); | ||
| 1400 | $placeholder = $type->convertToDatabaseValueSQL('?', $this->platform); | ||
| 1401 | } | ||
| 1402 | |||
| 1403 | $values[] = $placeholder; | ||
| 1404 | } | ||
| 1405 | |||
| 1406 | $columns = implode(', ', $columns); | ||
| 1407 | $values = implode(', ', $values); | ||
| 1408 | |||
| 1409 | $this->insertSql = sprintf('INSERT INTO %s (%s) VALUES (%s)', $tableName, $columns, $values); | ||
| 1410 | |||
| 1411 | return $this->insertSql; | ||
| 1412 | } | ||
| 1413 | |||
| 1414 | /** | ||
| 1415 | * Gets the list of columns to put in the INSERT SQL statement. | ||
| 1416 | * | ||
| 1417 | * Subclasses should override this method to alter or change the list of | ||
| 1418 | * columns placed in the INSERT statements used by the persister. | ||
| 1419 | * | ||
| 1420 | * @psalm-return list<string> | ||
| 1421 | */ | ||
| 1422 | protected function getInsertColumnList(): array | ||
| 1423 | { | ||
| 1424 | $columns = []; | ||
| 1425 | |||
| 1426 | foreach ($this->class->reflFields as $name => $field) { | ||
| 1427 | if ($this->class->isVersioned && $this->class->versionField === $name) { | ||
| 1428 | continue; | ||
| 1429 | } | ||
| 1430 | |||
| 1431 | if (isset($this->class->embeddedClasses[$name])) { | ||
| 1432 | continue; | ||
| 1433 | } | ||
| 1434 | |||
| 1435 | if (isset($this->class->associationMappings[$name])) { | ||
| 1436 | $assoc = $this->class->associationMappings[$name]; | ||
| 1437 | |||
| 1438 | if ($assoc->isToOneOwningSide()) { | ||
| 1439 | foreach ($assoc->joinColumns as $joinColumn) { | ||
| 1440 | $columns[] = $this->quoteStrategy->getJoinColumnName($joinColumn, $this->class, $this->platform); | ||
| 1441 | } | ||
| 1442 | } | ||
| 1443 | |||
| 1444 | continue; | ||
| 1445 | } | ||
| 1446 | |||
| 1447 | if (! $this->class->isIdGeneratorIdentity() || $this->class->identifier[0] !== $name) { | ||
| 1448 | if (isset($this->class->fieldMappings[$name]->notInsertable)) { | ||
| 1449 | continue; | ||
| 1450 | } | ||
| 1451 | |||
| 1452 | $columns[] = $this->quoteStrategy->getColumnName($name, $this->class, $this->platform); | ||
| 1453 | $this->columnTypes[$name] = $this->class->fieldMappings[$name]->type; | ||
| 1454 | } | ||
| 1455 | } | ||
| 1456 | |||
| 1457 | return $columns; | ||
| 1458 | } | ||
| 1459 | |||
| 1460 | /** | ||
| 1461 | * Gets the SQL snippet of a qualified column name for the given field name. | ||
| 1462 | * | ||
| 1463 | * @param ClassMetadata $class The class that declares this field. The table this class is | ||
| 1464 | * mapped to must own the column for the given field. | ||
| 1465 | */ | ||
| 1466 | protected function getSelectColumnSQL(string $field, ClassMetadata $class, string $alias = 'r'): string | ||
| 1467 | { | ||
| 1468 | $root = $alias === 'r' ? '' : $alias; | ||
| 1469 | $tableAlias = $this->getSQLTableAlias($class->name, $root); | ||
| 1470 | $fieldMapping = $class->fieldMappings[$field]; | ||
| 1471 | $sql = sprintf('%s.%s', $tableAlias, $this->quoteStrategy->getColumnName($field, $class, $this->platform)); | ||
| 1472 | $columnAlias = $this->getSQLColumnAlias($fieldMapping->columnName); | ||
| 1473 | |||
| 1474 | $this->currentPersisterContext->rsm->addFieldResult($alias, $columnAlias, $field); | ||
| 1475 | if (! empty($fieldMapping->enumType)) { | ||
| 1476 | $this->currentPersisterContext->rsm->addEnumResult($columnAlias, $fieldMapping->enumType); | ||
| 1477 | } | ||
| 1478 | |||
| 1479 | $type = Type::getType($fieldMapping->type); | ||
| 1480 | $sql = $type->convertToPHPValueSQL($sql, $this->platform); | ||
| 1481 | |||
| 1482 | return $sql . ' AS ' . $columnAlias; | ||
| 1483 | } | ||
| 1484 | |||
| 1485 | /** | ||
| 1486 | * Gets the SQL table alias for the given class name. | ||
| 1487 | * | ||
| 1488 | * @todo Reconsider. Binding table aliases to class names is not such a good idea. | ||
| 1489 | */ | ||
| 1490 | protected function getSQLTableAlias(string $className, string $assocName = ''): string | ||
| 1491 | { | ||
| 1492 | if ($assocName) { | ||
| 1493 | $className .= '#' . $assocName; | ||
| 1494 | } | ||
| 1495 | |||
| 1496 | if (isset($this->currentPersisterContext->sqlTableAliases[$className])) { | ||
| 1497 | return $this->currentPersisterContext->sqlTableAliases[$className]; | ||
| 1498 | } | ||
| 1499 | |||
| 1500 | $tableAlias = 't' . $this->currentPersisterContext->sqlAliasCounter++; | ||
| 1501 | |||
| 1502 | $this->currentPersisterContext->sqlTableAliases[$className] = $tableAlias; | ||
| 1503 | |||
| 1504 | return $tableAlias; | ||
| 1505 | } | ||
| 1506 | |||
| 1507 | /** | ||
| 1508 | * {@inheritDoc} | ||
| 1509 | */ | ||
| 1510 | public function lock(array $criteria, LockMode|int $lockMode): void | ||
| 1511 | { | ||
| 1512 | $conditionSql = $this->getSelectConditionSQL($criteria); | ||
| 1513 | |||
| 1514 | $lockSql = match ($lockMode) { | ||
| 1515 | LockMode::PESSIMISTIC_READ => $this->getReadLockSQL($this->platform), | ||
| 1516 | LockMode::PESSIMISTIC_WRITE => $this->getWriteLockSQL($this->platform), | ||
| 1517 | default => '', | ||
| 1518 | }; | ||
| 1519 | |||
| 1520 | $lock = $this->getLockTablesSql($lockMode); | ||
| 1521 | $where = ($conditionSql ? ' WHERE ' . $conditionSql : '') . ' '; | ||
| 1522 | $sql = 'SELECT 1 ' | ||
| 1523 | . $lock | ||
| 1524 | . $where | ||
| 1525 | . $lockSql; | ||
| 1526 | |||
| 1527 | [$params, $types] = $this->expandParameters($criteria); | ||
| 1528 | |||
| 1529 | $this->conn->executeQuery($sql, $params, $types); | ||
| 1530 | } | ||
| 1531 | |||
| 1532 | /** | ||
| 1533 | * Gets the FROM and optionally JOIN conditions to lock the entity managed by this persister. | ||
| 1534 | * | ||
| 1535 | * @psalm-param LockMode::* $lockMode | ||
| 1536 | */ | ||
| 1537 | protected function getLockTablesSql(LockMode|int $lockMode): string | ||
| 1538 | { | ||
| 1539 | return $this->platform->appendLockHint( | ||
| 1540 | 'FROM ' | ||
| 1541 | . $this->quoteStrategy->getTableName($this->class, $this->platform) . ' ' | ||
| 1542 | . $this->getSQLTableAlias($this->class->name), | ||
| 1543 | $lockMode, | ||
| 1544 | ); | ||
| 1545 | } | ||
| 1546 | |||
| 1547 | /** | ||
| 1548 | * Gets the Select Where Condition from a Criteria object. | ||
| 1549 | */ | ||
| 1550 | protected function getSelectConditionCriteriaSQL(Criteria $criteria): string | ||
| 1551 | { | ||
| 1552 | $expression = $criteria->getWhereExpression(); | ||
| 1553 | |||
| 1554 | if ($expression === null) { | ||
| 1555 | return ''; | ||
| 1556 | } | ||
| 1557 | |||
| 1558 | $visitor = new SqlExpressionVisitor($this, $this->class); | ||
| 1559 | |||
| 1560 | return $visitor->dispatch($expression); | ||
| 1561 | } | ||
| 1562 | |||
| 1563 | public function getSelectConditionStatementSQL( | ||
| 1564 | string $field, | ||
| 1565 | mixed $value, | ||
| 1566 | AssociationMapping|null $assoc = null, | ||
| 1567 | string|null $comparison = null, | ||
| 1568 | ): string { | ||
| 1569 | $selectedColumns = []; | ||
| 1570 | $columns = $this->getSelectConditionStatementColumnSQL($field, $assoc); | ||
| 1571 | |||
| 1572 | if (count($columns) > 1 && $comparison === Comparison::IN) { | ||
| 1573 | /* | ||
| 1574 | * @todo try to support multi-column IN expressions. | ||
| 1575 | * Example: (col1, col2) IN (('val1A', 'val2A'), ('val1B', 'val2B')) | ||
| 1576 | */ | ||
| 1577 | throw CantUseInOperatorOnCompositeKeys::create(); | ||
| 1578 | } | ||
| 1579 | |||
| 1580 | foreach ($columns as $column) { | ||
| 1581 | $placeholder = '?'; | ||
| 1582 | |||
| 1583 | if (isset($this->class->fieldMappings[$field])) { | ||
| 1584 | $type = Type::getType($this->class->fieldMappings[$field]->type); | ||
| 1585 | $placeholder = $type->convertToDatabaseValueSQL($placeholder, $this->platform); | ||
| 1586 | } | ||
| 1587 | |||
| 1588 | if ($comparison !== null) { | ||
| 1589 | // special case null value handling | ||
| 1590 | if (($comparison === Comparison::EQ || $comparison === Comparison::IS) && $value === null) { | ||
| 1591 | $selectedColumns[] = $column . ' IS NULL'; | ||
| 1592 | |||
| 1593 | continue; | ||
| 1594 | } | ||
| 1595 | |||
| 1596 | if ($comparison === Comparison::NEQ && $value === null) { | ||
| 1597 | $selectedColumns[] = $column . ' IS NOT NULL'; | ||
| 1598 | |||
| 1599 | continue; | ||
| 1600 | } | ||
| 1601 | |||
| 1602 | $selectedColumns[] = $column . ' ' . sprintf(self::$comparisonMap[$comparison], $placeholder); | ||
| 1603 | |||
| 1604 | continue; | ||
| 1605 | } | ||
| 1606 | |||
| 1607 | if (is_array($value)) { | ||
| 1608 | $in = sprintf('%s IN (%s)', $column, $placeholder); | ||
| 1609 | |||
| 1610 | if (array_search(null, $value, true) !== false) { | ||
| 1611 | $selectedColumns[] = sprintf('(%s OR %s IS NULL)', $in, $column); | ||
| 1612 | |||
| 1613 | continue; | ||
| 1614 | } | ||
| 1615 | |||
| 1616 | $selectedColumns[] = $in; | ||
| 1617 | |||
| 1618 | continue; | ||
| 1619 | } | ||
| 1620 | |||
| 1621 | if ($value === null) { | ||
| 1622 | $selectedColumns[] = sprintf('%s IS NULL', $column); | ||
| 1623 | |||
| 1624 | continue; | ||
| 1625 | } | ||
| 1626 | |||
| 1627 | $selectedColumns[] = sprintf('%s = %s', $column, $placeholder); | ||
| 1628 | } | ||
| 1629 | |||
| 1630 | return implode(' AND ', $selectedColumns); | ||
| 1631 | } | ||
| 1632 | |||
| 1633 | /** | ||
| 1634 | * Builds the left-hand-side of a where condition statement. | ||
| 1635 | * | ||
| 1636 | * @return string[] | ||
| 1637 | * @psalm-return list<string> | ||
| 1638 | * | ||
| 1639 | * @throws InvalidFindByCall | ||
| 1640 | * @throws UnrecognizedField | ||
| 1641 | */ | ||
| 1642 | private function getSelectConditionStatementColumnSQL( | ||
| 1643 | string $field, | ||
| 1644 | AssociationMapping|null $assoc = null, | ||
| 1645 | ): array { | ||
| 1646 | if (isset($this->class->fieldMappings[$field])) { | ||
| 1647 | $className = $this->class->fieldMappings[$field]->inherited ?? $this->class->name; | ||
| 1648 | |||
| 1649 | return [$this->getSQLTableAlias($className) . '.' . $this->quoteStrategy->getColumnName($field, $this->class, $this->platform)]; | ||
| 1650 | } | ||
| 1651 | |||
| 1652 | if (isset($this->class->associationMappings[$field])) { | ||
| 1653 | $association = $this->class->associationMappings[$field]; | ||
| 1654 | // Many-To-Many requires join table check for joinColumn | ||
| 1655 | $columns = []; | ||
| 1656 | $class = $this->class; | ||
| 1657 | |||
| 1658 | if ($association->isManyToMany()) { | ||
| 1659 | assert($assoc !== null); | ||
| 1660 | if (! $association->isOwningSide()) { | ||
| 1661 | $association = $assoc; | ||
| 1662 | } | ||
| 1663 | |||
| 1664 | assert($association->isManyToManyOwningSide()); | ||
| 1665 | |||
| 1666 | $joinTableName = $this->quoteStrategy->getJoinTableName($association, $class, $this->platform); | ||
| 1667 | $joinColumns = $assoc->isOwningSide() | ||
| 1668 | ? $association->joinTable->joinColumns | ||
| 1669 | : $association->joinTable->inverseJoinColumns; | ||
| 1670 | |||
| 1671 | foreach ($joinColumns as $joinColumn) { | ||
| 1672 | $columns[] = $joinTableName . '.' . $this->quoteStrategy->getJoinColumnName($joinColumn, $class, $this->platform); | ||
| 1673 | } | ||
| 1674 | } else { | ||
| 1675 | if (! $association->isOwningSide()) { | ||
| 1676 | throw InvalidFindByCall::fromInverseSideUsage( | ||
| 1677 | $this->class->name, | ||
| 1678 | $field, | ||
| 1679 | ); | ||
| 1680 | } | ||
| 1681 | |||
| 1682 | assert($association->isToOneOwningSide()); | ||
| 1683 | |||
| 1684 | $className = $association->inherited ?? $this->class->name; | ||
| 1685 | |||
| 1686 | foreach ($association->joinColumns as $joinColumn) { | ||
| 1687 | $columns[] = $this->getSQLTableAlias($className) . '.' . $this->quoteStrategy->getJoinColumnName($joinColumn, $this->class, $this->platform); | ||
| 1688 | } | ||
| 1689 | } | ||
| 1690 | |||
| 1691 | return $columns; | ||
| 1692 | } | ||
| 1693 | |||
| 1694 | if ($assoc !== null && ! str_contains($field, ' ') && ! str_contains($field, '(')) { | ||
| 1695 | // very careless developers could potentially open up this normally hidden api for userland attacks, | ||
| 1696 | // therefore checking for spaces and function calls which are not allowed. | ||
| 1697 | |||
| 1698 | // found a join column condition, not really a "field" | ||
| 1699 | return [$field]; | ||
| 1700 | } | ||
| 1701 | |||
| 1702 | throw UnrecognizedField::byFullyQualifiedName($this->class->name, $field); | ||
| 1703 | } | ||
| 1704 | |||
| 1705 | /** | ||
| 1706 | * Gets the conditional SQL fragment used in the WHERE clause when selecting | ||
| 1707 | * entities in this persister. | ||
| 1708 | * | ||
| 1709 | * Subclasses are supposed to override this method if they intend to change | ||
| 1710 | * or alter the criteria by which entities are selected. | ||
| 1711 | * | ||
| 1712 | * @psalm-param array<string, mixed> $criteria | ||
| 1713 | */ | ||
| 1714 | protected function getSelectConditionSQL(array $criteria, AssociationMapping|null $assoc = null): string | ||
| 1715 | { | ||
| 1716 | $conditions = []; | ||
| 1717 | |||
| 1718 | foreach ($criteria as $field => $value) { | ||
| 1719 | $conditions[] = $this->getSelectConditionStatementSQL($field, $value, $assoc); | ||
| 1720 | } | ||
| 1721 | |||
| 1722 | return implode(' AND ', $conditions); | ||
| 1723 | } | ||
| 1724 | |||
| 1725 | /** | ||
| 1726 | * {@inheritDoc} | ||
| 1727 | */ | ||
| 1728 | public function getOneToManyCollection( | ||
| 1729 | AssociationMapping $assoc, | ||
| 1730 | object $sourceEntity, | ||
| 1731 | int|null $offset = null, | ||
| 1732 | int|null $limit = null, | ||
| 1733 | ): array { | ||
| 1734 | assert($assoc instanceof OneToManyAssociationMapping); | ||
| 1735 | $this->switchPersisterContext($offset, $limit); | ||
| 1736 | |||
| 1737 | $stmt = $this->getOneToManyStatement($assoc, $sourceEntity, $offset, $limit); | ||
| 1738 | |||
| 1739 | return $this->loadArrayFromResult($assoc, $stmt); | ||
| 1740 | } | ||
| 1741 | |||
| 1742 | public function loadOneToManyCollection( | ||
| 1743 | AssociationMapping $assoc, | ||
| 1744 | object $sourceEntity, | ||
| 1745 | PersistentCollection $collection, | ||
| 1746 | ): mixed { | ||
| 1747 | assert($assoc instanceof OneToManyAssociationMapping); | ||
| 1748 | $stmt = $this->getOneToManyStatement($assoc, $sourceEntity); | ||
| 1749 | |||
| 1750 | return $this->loadCollectionFromStatement($assoc, $stmt, $collection); | ||
| 1751 | } | ||
| 1752 | |||
| 1753 | /** Builds criteria and execute SQL statement to fetch the one to many entities from. */ | ||
| 1754 | private function getOneToManyStatement( | ||
| 1755 | OneToManyAssociationMapping $assoc, | ||
| 1756 | object $sourceEntity, | ||
| 1757 | int|null $offset = null, | ||
| 1758 | int|null $limit = null, | ||
| 1759 | ): Result { | ||
| 1760 | $this->switchPersisterContext($offset, $limit); | ||
| 1761 | |||
| 1762 | $criteria = []; | ||
| 1763 | $parameters = []; | ||
| 1764 | $owningAssoc = $this->class->associationMappings[$assoc->mappedBy]; | ||
| 1765 | $sourceClass = $this->em->getClassMetadata($assoc->sourceEntity); | ||
| 1766 | $tableAlias = $this->getSQLTableAlias($owningAssoc->inherited ?? $this->class->name); | ||
| 1767 | assert($owningAssoc->isManyToOne()); | ||
| 1768 | |||
| 1769 | foreach ($owningAssoc->targetToSourceKeyColumns as $sourceKeyColumn => $targetKeyColumn) { | ||
| 1770 | if ($sourceClass->containsForeignIdentifier) { | ||
| 1771 | $field = $sourceClass->getFieldForColumn($sourceKeyColumn); | ||
| 1772 | $value = $sourceClass->reflFields[$field]->getValue($sourceEntity); | ||
| 1773 | |||
| 1774 | if (isset($sourceClass->associationMappings[$field])) { | ||
| 1775 | $value = $this->em->getUnitOfWork()->getEntityIdentifier($value); | ||
| 1776 | $value = $value[$this->em->getClassMetadata($sourceClass->associationMappings[$field]->targetEntity)->identifier[0]]; | ||
| 1777 | } | ||
| 1778 | |||
| 1779 | $criteria[$tableAlias . '.' . $targetKeyColumn] = $value; | ||
| 1780 | $parameters[] = [ | ||
| 1781 | 'value' => $value, | ||
| 1782 | 'field' => $field, | ||
| 1783 | 'class' => $sourceClass, | ||
| 1784 | ]; | ||
| 1785 | |||
| 1786 | continue; | ||
| 1787 | } | ||
| 1788 | |||
| 1789 | $field = $sourceClass->fieldNames[$sourceKeyColumn]; | ||
| 1790 | $value = $sourceClass->reflFields[$field]->getValue($sourceEntity); | ||
| 1791 | |||
| 1792 | $criteria[$tableAlias . '.' . $targetKeyColumn] = $value; | ||
| 1793 | $parameters[] = [ | ||
| 1794 | 'value' => $value, | ||
| 1795 | 'field' => $field, | ||
| 1796 | 'class' => $sourceClass, | ||
| 1797 | ]; | ||
| 1798 | } | ||
| 1799 | |||
| 1800 | $sql = $this->getSelectSQL($criteria, $assoc, null, $limit, $offset); | ||
| 1801 | [$params, $types] = $this->expandToManyParameters($parameters); | ||
| 1802 | |||
| 1803 | return $this->conn->executeQuery($sql, $params, $types); | ||
| 1804 | } | ||
| 1805 | |||
| 1806 | /** | ||
| 1807 | * {@inheritDoc} | ||
| 1808 | */ | ||
| 1809 | public function expandParameters(array $criteria): array | ||
| 1810 | { | ||
| 1811 | $params = []; | ||
| 1812 | $types = []; | ||
| 1813 | |||
| 1814 | foreach ($criteria as $field => $value) { | ||
| 1815 | if ($value === null) { | ||
| 1816 | continue; // skip null values. | ||
| 1817 | } | ||
| 1818 | |||
| 1819 | $types = [...$types, ...$this->getTypes($field, $value, $this->class)]; | ||
| 1820 | $params = array_merge($params, $this->getValues($value)); | ||
| 1821 | } | ||
| 1822 | |||
| 1823 | return [$params, $types]; | ||
| 1824 | } | ||
| 1825 | |||
| 1826 | /** | ||
| 1827 | * Expands the parameters from the given criteria and use the correct binding types if found, | ||
| 1828 | * specialized for OneToMany or ManyToMany associations. | ||
| 1829 | * | ||
| 1830 | * @param mixed[][] $criteria an array of arrays containing following: | ||
| 1831 | * - field to which each criterion will be bound | ||
| 1832 | * - value to be bound | ||
| 1833 | * - class to which the field belongs to | ||
| 1834 | * | ||
| 1835 | * @return mixed[][] | ||
| 1836 | * @psalm-return array{0: array, 1: list<ParameterType::*|ArrayParameterType::*|string>} | ||
| 1837 | */ | ||
| 1838 | private function expandToManyParameters(array $criteria): array | ||
| 1839 | { | ||
| 1840 | $params = []; | ||
| 1841 | $types = []; | ||
| 1842 | |||
| 1843 | foreach ($criteria as $criterion) { | ||
| 1844 | if ($criterion['value'] === null) { | ||
| 1845 | continue; // skip null values. | ||
| 1846 | } | ||
| 1847 | |||
| 1848 | $types = [...$types, ...$this->getTypes($criterion['field'], $criterion['value'], $criterion['class'])]; | ||
| 1849 | $params = array_merge($params, $this->getValues($criterion['value'])); | ||
| 1850 | } | ||
| 1851 | |||
| 1852 | return [$params, $types]; | ||
| 1853 | } | ||
| 1854 | |||
| 1855 | /** | ||
| 1856 | * Infers field types to be used by parameter type casting. | ||
| 1857 | * | ||
| 1858 | * @return list<ParameterType|ArrayParameterType|int|string> | ||
| 1859 | * @psalm-return list<ParameterType::*|ArrayParameterType::*|string> | ||
| 1860 | * | ||
| 1861 | * @throws QueryException | ||
| 1862 | */ | ||
| 1863 | private function getTypes(string $field, mixed $value, ClassMetadata $class): array | ||
| 1864 | { | ||
| 1865 | $types = []; | ||
| 1866 | |||
| 1867 | switch (true) { | ||
| 1868 | case isset($class->fieldMappings[$field]): | ||
| 1869 | $types = array_merge($types, [$class->fieldMappings[$field]->type]); | ||
| 1870 | break; | ||
| 1871 | |||
| 1872 | case isset($class->associationMappings[$field]): | ||
| 1873 | $assoc = $this->em->getMetadataFactory()->getOwningSide($class->associationMappings[$field]); | ||
| 1874 | $class = $this->em->getClassMetadata($assoc->targetEntity); | ||
| 1875 | |||
| 1876 | if ($assoc->isManyToManyOwningSide()) { | ||
| 1877 | $columns = $assoc->relationToTargetKeyColumns; | ||
| 1878 | } else { | ||
| 1879 | assert($assoc->isToOneOwningSide()); | ||
| 1880 | $columns = $assoc->sourceToTargetKeyColumns; | ||
| 1881 | } | ||
| 1882 | |||
| 1883 | foreach ($columns as $column) { | ||
| 1884 | $types[] = PersisterHelper::getTypeOfColumn($column, $class, $this->em); | ||
| 1885 | } | ||
| 1886 | |||
| 1887 | break; | ||
| 1888 | |||
| 1889 | default: | ||
| 1890 | $types[] = ParameterType::STRING; | ||
| 1891 | break; | ||
| 1892 | } | ||
| 1893 | |||
| 1894 | if (is_array($value)) { | ||
| 1895 | return array_map($this->getArrayBindingType(...), $types); | ||
| 1896 | } | ||
| 1897 | |||
| 1898 | return $types; | ||
| 1899 | } | ||
| 1900 | |||
| 1901 | /** @psalm-return ArrayParameterType::* */ | ||
| 1902 | private function getArrayBindingType(ParameterType|int|string $type): ArrayParameterType|int | ||
| 1903 | { | ||
| 1904 | if (! $type instanceof ParameterType) { | ||
| 1905 | $type = Type::getType((string) $type)->getBindingType(); | ||
| 1906 | } | ||
| 1907 | |||
| 1908 | return match ($type) { | ||
| 1909 | ParameterType::STRING => ArrayParameterType::STRING, | ||
| 1910 | ParameterType::INTEGER => ArrayParameterType::INTEGER, | ||
| 1911 | ParameterType::ASCII => ArrayParameterType::ASCII, | ||
| 1912 | }; | ||
| 1913 | } | ||
| 1914 | |||
| 1915 | /** | ||
| 1916 | * Retrieves the parameters that identifies a value. | ||
| 1917 | * | ||
| 1918 | * @return mixed[] | ||
| 1919 | */ | ||
| 1920 | private function getValues(mixed $value): array | ||
| 1921 | { | ||
| 1922 | if (is_array($value)) { | ||
| 1923 | $newValue = []; | ||
| 1924 | |||
| 1925 | foreach ($value as $itemValue) { | ||
| 1926 | $newValue = array_merge($newValue, $this->getValues($itemValue)); | ||
| 1927 | } | ||
| 1928 | |||
| 1929 | return [$newValue]; | ||
| 1930 | } | ||
| 1931 | |||
| 1932 | return $this->getIndividualValue($value); | ||
| 1933 | } | ||
| 1934 | |||
| 1935 | /** | ||
| 1936 | * Retrieves an individual parameter value. | ||
| 1937 | * | ||
| 1938 | * @psalm-return list<mixed> | ||
| 1939 | */ | ||
| 1940 | private function getIndividualValue(mixed $value): array | ||
| 1941 | { | ||
| 1942 | if (! is_object($value)) { | ||
| 1943 | return [$value]; | ||
| 1944 | } | ||
| 1945 | |||
| 1946 | if ($value instanceof BackedEnum) { | ||
| 1947 | return [$value->value]; | ||
| 1948 | } | ||
| 1949 | |||
| 1950 | $valueClass = DefaultProxyClassNameResolver::getClass($value); | ||
| 1951 | |||
| 1952 | if ($this->em->getMetadataFactory()->isTransient($valueClass)) { | ||
| 1953 | return [$value]; | ||
| 1954 | } | ||
| 1955 | |||
| 1956 | $class = $this->em->getClassMetadata($valueClass); | ||
| 1957 | |||
| 1958 | if ($class->isIdentifierComposite) { | ||
| 1959 | $newValue = []; | ||
| 1960 | |||
| 1961 | foreach ($class->getIdentifierValues($value) as $innerValue) { | ||
| 1962 | $newValue = array_merge($newValue, $this->getValues($innerValue)); | ||
| 1963 | } | ||
| 1964 | |||
| 1965 | return $newValue; | ||
| 1966 | } | ||
| 1967 | |||
| 1968 | return [$this->em->getUnitOfWork()->getSingleIdentifierValue($value)]; | ||
| 1969 | } | ||
| 1970 | |||
| 1971 | public function exists(object $entity, Criteria|null $extraConditions = null): bool | ||
| 1972 | { | ||
| 1973 | $criteria = $this->class->getIdentifierValues($entity); | ||
| 1974 | |||
| 1975 | if (! $criteria) { | ||
| 1976 | return false; | ||
| 1977 | } | ||
| 1978 | |||
| 1979 | $alias = $this->getSQLTableAlias($this->class->name); | ||
| 1980 | |||
| 1981 | $sql = 'SELECT 1 ' | ||
| 1982 | . $this->getLockTablesSql(LockMode::NONE) | ||
| 1983 | . ' WHERE ' . $this->getSelectConditionSQL($criteria); | ||
| 1984 | |||
| 1985 | [$params, $types] = $this->expandParameters($criteria); | ||
| 1986 | |||
| 1987 | if ($extraConditions !== null) { | ||
| 1988 | $sql .= ' AND ' . $this->getSelectConditionCriteriaSQL($extraConditions); | ||
| 1989 | [$criteriaParams, $criteriaTypes] = $this->expandCriteriaParameters($extraConditions); | ||
| 1990 | |||
| 1991 | $params = [...$params, ...$criteriaParams]; | ||
| 1992 | $types = [...$types, ...$criteriaTypes]; | ||
| 1993 | } | ||
| 1994 | |||
| 1995 | $filterSql = $this->generateFilterConditionSQL($this->class, $alias); | ||
| 1996 | if ($filterSql) { | ||
| 1997 | $sql .= ' AND ' . $filterSql; | ||
| 1998 | } | ||
| 1999 | |||
| 2000 | return (bool) $this->conn->fetchOne($sql, $params, $types); | ||
| 2001 | } | ||
| 2002 | |||
| 2003 | /** | ||
| 2004 | * Generates the appropriate join SQL for the given join column. | ||
| 2005 | * | ||
| 2006 | * @param list<JoinColumnMapping> $joinColumns The join columns definition of an association. | ||
| 2007 | * | ||
| 2008 | * @return string LEFT JOIN if one of the columns is nullable, INNER JOIN otherwise. | ||
| 2009 | */ | ||
| 2010 | protected function getJoinSQLForJoinColumns(array $joinColumns): string | ||
| 2011 | { | ||
| 2012 | // if one of the join columns is nullable, return left join | ||
| 2013 | foreach ($joinColumns as $joinColumn) { | ||
| 2014 | if (! isset($joinColumn->nullable) || $joinColumn->nullable) { | ||
| 2015 | return 'LEFT JOIN'; | ||
| 2016 | } | ||
| 2017 | } | ||
| 2018 | |||
| 2019 | return 'INNER JOIN'; | ||
| 2020 | } | ||
| 2021 | |||
| 2022 | public function getSQLColumnAlias(string $columnName): string | ||
| 2023 | { | ||
| 2024 | return $this->quoteStrategy->getColumnAlias($columnName, $this->currentPersisterContext->sqlAliasCounter++, $this->platform); | ||
| 2025 | } | ||
| 2026 | |||
| 2027 | /** | ||
| 2028 | * Generates the filter SQL for a given entity and table alias. | ||
| 2029 | * | ||
| 2030 | * @param ClassMetadata $targetEntity Metadata of the target entity. | ||
| 2031 | * @param string $targetTableAlias The table alias of the joined/selected table. | ||
| 2032 | * | ||
| 2033 | * @return string The SQL query part to add to a query. | ||
| 2034 | */ | ||
| 2035 | protected function generateFilterConditionSQL(ClassMetadata $targetEntity, string $targetTableAlias): string | ||
| 2036 | { | ||
| 2037 | $filterClauses = []; | ||
| 2038 | |||
| 2039 | foreach ($this->em->getFilters()->getEnabledFilters() as $filter) { | ||
| 2040 | $filterExpr = $filter->addFilterConstraint($targetEntity, $targetTableAlias); | ||
| 2041 | if ($filterExpr !== '') { | ||
| 2042 | $filterClauses[] = '(' . $filterExpr . ')'; | ||
| 2043 | } | ||
| 2044 | } | ||
| 2045 | |||
| 2046 | $sql = implode(' AND ', $filterClauses); | ||
| 2047 | |||
| 2048 | return $sql ? '(' . $sql . ')' : ''; // Wrap again to avoid "X or Y and FilterConditionSQL" | ||
| 2049 | } | ||
| 2050 | |||
| 2051 | /** | ||
| 2052 | * Switches persister context according to current query offset/limits | ||
| 2053 | * | ||
| 2054 | * This is due to the fact that to-many associations cannot be fetch-joined when a limit is involved | ||
| 2055 | */ | ||
| 2056 | protected function switchPersisterContext(int|null $offset, int|null $limit): void | ||
| 2057 | { | ||
| 2058 | if ($offset === null && $limit === null) { | ||
| 2059 | $this->currentPersisterContext = $this->noLimitsContext; | ||
| 2060 | |||
| 2061 | return; | ||
| 2062 | } | ||
| 2063 | |||
| 2064 | $this->currentPersisterContext = $this->limitsHandlingContext; | ||
| 2065 | } | ||
| 2066 | |||
| 2067 | /** | ||
| 2068 | * @return string[] | ||
| 2069 | * @psalm-return list<string> | ||
| 2070 | */ | ||
| 2071 | protected function getClassIdentifiersTypes(ClassMetadata $class): array | ||
| 2072 | { | ||
| 2073 | $entityManager = $this->em; | ||
| 2074 | |||
| 2075 | return array_map( | ||
| 2076 | static function ($fieldName) use ($class, $entityManager): string { | ||
| 2077 | $types = PersisterHelper::getTypeOfField($fieldName, $class, $entityManager); | ||
| 2078 | assert(isset($types[0])); | ||
| 2079 | |||
| 2080 | return $types[0]; | ||
| 2081 | }, | ||
| 2082 | $class->identifier, | ||
| 2083 | ); | ||
| 2084 | } | ||
| 2085 | } | ||
diff --git a/vendor/doctrine/orm/src/Persisters/Entity/CachedPersisterContext.php b/vendor/doctrine/orm/src/Persisters/Entity/CachedPersisterContext.php new file mode 100644 index 0000000..03d053b --- /dev/null +++ b/vendor/doctrine/orm/src/Persisters/Entity/CachedPersisterContext.php | |||
| @@ -0,0 +1,60 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Persisters\Entity; | ||
| 6 | |||
| 7 | use Doctrine\ORM\Query\ResultSetMapping; | ||
| 8 | use Doctrine\Persistence\Mapping\ClassMetadata; | ||
| 9 | |||
| 10 | /** | ||
| 11 | * A swappable persister context to use as a container for the current | ||
| 12 | * generated query/resultSetMapping/type binding information. | ||
| 13 | * | ||
| 14 | * This class is a utility class to be used only by the persister API | ||
| 15 | * | ||
| 16 | * This object is highly mutable due to performance reasons. Same reasoning | ||
| 17 | * behind its properties being public. | ||
| 18 | */ | ||
| 19 | class CachedPersisterContext | ||
| 20 | { | ||
| 21 | /** | ||
| 22 | * The SELECT column list SQL fragment used for querying entities by this persister. | ||
| 23 | * This SQL fragment is only generated once per request, if at all. | ||
| 24 | */ | ||
| 25 | public string|null $selectColumnListSql = null; | ||
| 26 | |||
| 27 | /** | ||
| 28 | * The JOIN SQL fragment used to eagerly load all many-to-one and one-to-one | ||
| 29 | * associations configured as FETCH_EAGER, as well as all inverse one-to-one associations. | ||
| 30 | */ | ||
| 31 | public string|null $selectJoinSql = null; | ||
| 32 | |||
| 33 | /** | ||
| 34 | * Counter for creating unique SQL table and column aliases. | ||
| 35 | */ | ||
| 36 | public int $sqlAliasCounter = 0; | ||
| 37 | |||
| 38 | /** | ||
| 39 | * Map from class names (FQCN) to the corresponding generated SQL table aliases. | ||
| 40 | * | ||
| 41 | * @var array<class-string, string> | ||
| 42 | */ | ||
| 43 | public array $sqlTableAliases = []; | ||
| 44 | |||
| 45 | public function __construct( | ||
| 46 | /** | ||
| 47 | * Metadata object that describes the mapping of the mapped entity class. | ||
| 48 | */ | ||
| 49 | public ClassMetadata $class, | ||
| 50 | /** | ||
| 51 | * ResultSetMapping that is used for all queries. Is generated lazily once per request. | ||
| 52 | */ | ||
| 53 | public ResultSetMapping $rsm, | ||
| 54 | /** | ||
| 55 | * Whether this persistent context is considering limit operations applied to the selection queries | ||
| 56 | */ | ||
| 57 | public bool $handlesLimits, | ||
| 58 | ) { | ||
| 59 | } | ||
| 60 | } | ||
diff --git a/vendor/doctrine/orm/src/Persisters/Entity/EntityPersister.php b/vendor/doctrine/orm/src/Persisters/Entity/EntityPersister.php new file mode 100644 index 0000000..6b278a7 --- /dev/null +++ b/vendor/doctrine/orm/src/Persisters/Entity/EntityPersister.php | |||
| @@ -0,0 +1,298 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Persisters\Entity; | ||
| 6 | |||
| 7 | use Doctrine\Common\Collections\Criteria; | ||
| 8 | use Doctrine\DBAL\ArrayParameterType; | ||
| 9 | use Doctrine\DBAL\LockMode; | ||
| 10 | use Doctrine\DBAL\ParameterType; | ||
| 11 | use Doctrine\ORM\Mapping\AssociationMapping; | ||
| 12 | use Doctrine\ORM\Mapping\ClassMetadata; | ||
| 13 | use Doctrine\ORM\Mapping\MappingException; | ||
| 14 | use Doctrine\ORM\PersistentCollection; | ||
| 15 | use Doctrine\ORM\Query\ResultSetMapping; | ||
| 16 | |||
| 17 | /** | ||
| 18 | * Entity persister interface | ||
| 19 | * Define the behavior that should be implemented by all entity persisters. | ||
| 20 | */ | ||
| 21 | interface EntityPersister | ||
| 22 | { | ||
| 23 | public function getClassMetadata(): ClassMetadata; | ||
| 24 | |||
| 25 | /** | ||
| 26 | * Gets the ResultSetMapping used for hydration. | ||
| 27 | */ | ||
| 28 | public function getResultSetMapping(): ResultSetMapping; | ||
| 29 | |||
| 30 | /** | ||
| 31 | * Get all queued inserts. | ||
| 32 | * | ||
| 33 | * @return object[] | ||
| 34 | */ | ||
| 35 | public function getInserts(): array; | ||
| 36 | |||
| 37 | /** | ||
| 38 | * Gets the INSERT SQL used by the persister to persist a new entity. | ||
| 39 | * | ||
| 40 | * @TODO It should not be here. | ||
| 41 | * But its necessary since JoinedSubclassPersister#executeInserts invoke the root persister. | ||
| 42 | */ | ||
| 43 | public function getInsertSQL(): string; | ||
| 44 | |||
| 45 | /** | ||
| 46 | * Gets the SELECT SQL to select one or more entities by a set of field criteria. | ||
| 47 | * | ||
| 48 | * @param mixed[]|Criteria $criteria | ||
| 49 | * @param mixed[]|null $orderBy | ||
| 50 | * @psalm-param AssociationMapping|null $assoc | ||
| 51 | * @psalm-param LockMode::*|null $lockMode | ||
| 52 | */ | ||
| 53 | public function getSelectSQL( | ||
| 54 | array|Criteria $criteria, | ||
| 55 | AssociationMapping|null $assoc = null, | ||
| 56 | LockMode|int|null $lockMode = null, | ||
| 57 | int|null $limit = null, | ||
| 58 | int|null $offset = null, | ||
| 59 | array|null $orderBy = null, | ||
| 60 | ): string; | ||
| 61 | |||
| 62 | /** | ||
| 63 | * Get the COUNT SQL to count entities (optionally based on a criteria) | ||
| 64 | * | ||
| 65 | * @param mixed[]|Criteria $criteria | ||
| 66 | */ | ||
| 67 | public function getCountSQL(array|Criteria $criteria = []): string; | ||
| 68 | |||
| 69 | /** | ||
| 70 | * Expands the parameters from the given criteria and use the correct binding types if found. | ||
| 71 | * | ||
| 72 | * @param string[] $criteria | ||
| 73 | * | ||
| 74 | * @psalm-return array{list<mixed>, list<ParameterType::*|ArrayParameterType::*|string>} | ||
| 75 | */ | ||
| 76 | public function expandParameters(array $criteria): array; | ||
| 77 | |||
| 78 | /** | ||
| 79 | * Expands Criteria Parameters by walking the expressions and grabbing all parameters and types from it. | ||
| 80 | * | ||
| 81 | * @psalm-return array{list<mixed>, list<ParameterType::*|ArrayParameterType::*|string>} | ||
| 82 | */ | ||
| 83 | public function expandCriteriaParameters(Criteria $criteria): array; | ||
| 84 | |||
| 85 | /** Gets the SQL WHERE condition for matching a field with a given value. */ | ||
| 86 | public function getSelectConditionStatementSQL( | ||
| 87 | string $field, | ||
| 88 | mixed $value, | ||
| 89 | AssociationMapping|null $assoc = null, | ||
| 90 | string|null $comparison = null, | ||
| 91 | ): string; | ||
| 92 | |||
| 93 | /** | ||
| 94 | * Adds an entity to the queued insertions. | ||
| 95 | * The entity remains queued until {@link executeInserts} is invoked. | ||
| 96 | */ | ||
| 97 | public function addInsert(object $entity): void; | ||
| 98 | |||
| 99 | /** | ||
| 100 | * Executes all queued entity insertions. | ||
| 101 | * | ||
| 102 | * If no inserts are queued, invoking this method is a NOOP. | ||
| 103 | */ | ||
| 104 | public function executeInserts(): void; | ||
| 105 | |||
| 106 | /** | ||
| 107 | * Updates a managed entity. The entity is updated according to its current changeset | ||
| 108 | * in the running UnitOfWork. If there is no changeset, nothing is updated. | ||
| 109 | */ | ||
| 110 | public function update(object $entity): void; | ||
| 111 | |||
| 112 | /** | ||
| 113 | * Deletes a managed entity. | ||
| 114 | * | ||
| 115 | * The entity to delete must be managed and have a persistent identifier. | ||
| 116 | * The deletion happens instantaneously. | ||
| 117 | * | ||
| 118 | * Subclasses may override this method to customize the semantics of entity deletion. | ||
| 119 | * | ||
| 120 | * @return bool TRUE if the entity got deleted in the database, FALSE otherwise. | ||
| 121 | */ | ||
| 122 | public function delete(object $entity): bool; | ||
| 123 | |||
| 124 | /** | ||
| 125 | * Count entities (optionally filtered by a criteria) | ||
| 126 | * | ||
| 127 | * @param mixed[]|Criteria $criteria | ||
| 128 | */ | ||
| 129 | public function count(array|Criteria $criteria = []): int; | ||
| 130 | |||
| 131 | /** | ||
| 132 | * Gets the name of the table that owns the column the given field is mapped to. | ||
| 133 | * | ||
| 134 | * The default implementation in BasicEntityPersister always returns the name | ||
| 135 | * of the table the entity type of this persister is mapped to, since an entity | ||
| 136 | * is always persisted to a single table with a BasicEntityPersister. | ||
| 137 | */ | ||
| 138 | public function getOwningTable(string $fieldName): string; | ||
| 139 | |||
| 140 | /** | ||
| 141 | * Loads an entity by a list of field criteria. | ||
| 142 | * | ||
| 143 | * @param mixed[] $criteria The criteria by which to load the entity. | ||
| 144 | * @param object|null $entity The entity to load the data into. If not specified, | ||
| 145 | * a new entity is created. | ||
| 146 | * @param AssociationMapping|null $assoc The association that connects the entity | ||
| 147 | * to load to another entity, if any. | ||
| 148 | * @param mixed[] $hints Hints for entity creation. | ||
| 149 | * @param LockMode|int|null $lockMode One of the \Doctrine\DBAL\LockMode::* constants | ||
| 150 | * or NULL if no specific lock mode should be used | ||
| 151 | * for loading the entity. | ||
| 152 | * @param int|null $limit Limit number of results. | ||
| 153 | * @param string[]|null $orderBy Criteria to order by. | ||
| 154 | * @psalm-param array<string, mixed> $criteria | ||
| 155 | * @psalm-param array<string, mixed> $hints | ||
| 156 | * @psalm-param LockMode::*|null $lockMode | ||
| 157 | * @psalm-param array<string, string>|null $orderBy | ||
| 158 | * | ||
| 159 | * @return object|null The loaded and managed entity instance or NULL if the entity can not be found. | ||
| 160 | * | ||
| 161 | * @todo Check identity map? loadById method? Try to guess whether $criteria is the id? | ||
| 162 | */ | ||
| 163 | public function load( | ||
| 164 | array $criteria, | ||
| 165 | object|null $entity = null, | ||
| 166 | AssociationMapping|null $assoc = null, | ||
| 167 | array $hints = [], | ||
| 168 | LockMode|int|null $lockMode = null, | ||
| 169 | int|null $limit = null, | ||
| 170 | array|null $orderBy = null, | ||
| 171 | ): object|null; | ||
| 172 | |||
| 173 | /** | ||
| 174 | * Loads an entity by identifier. | ||
| 175 | * | ||
| 176 | * @param object|null $entity The entity to load the data into. If not specified, a new entity is created. | ||
| 177 | * @psalm-param array<string, mixed> $identifier The entity identifier. | ||
| 178 | * | ||
| 179 | * @return object|null The loaded and managed entity instance or NULL if the entity can not be found. | ||
| 180 | * | ||
| 181 | * @todo Check parameters | ||
| 182 | */ | ||
| 183 | public function loadById(array $identifier, object|null $entity = null): object|null; | ||
| 184 | |||
| 185 | /** | ||
| 186 | * Loads an entity of this persister's mapped class as part of a single-valued | ||
| 187 | * association from another entity. | ||
| 188 | * | ||
| 189 | * @param AssociationMapping $assoc The association to load. | ||
| 190 | * @param object $sourceEntity The entity that owns the association (not necessarily the "owning side"). | ||
| 191 | * @psalm-param array<string, mixed> $identifier The identifier of the entity to load. Must be provided if | ||
| 192 | * the association to load represents the owning side, otherwise | ||
| 193 | * the identifier is derived from the $sourceEntity. | ||
| 194 | * | ||
| 195 | * @return object|null The loaded and managed entity instance or NULL if the entity can not be found. | ||
| 196 | * | ||
| 197 | * @throws MappingException | ||
| 198 | */ | ||
| 199 | public function loadOneToOneEntity(AssociationMapping $assoc, object $sourceEntity, array $identifier = []): object|null; | ||
| 200 | |||
| 201 | /** | ||
| 202 | * Refreshes a managed entity. | ||
| 203 | * | ||
| 204 | * @param LockMode|int|null $lockMode One of the \Doctrine\DBAL\LockMode::* constants | ||
| 205 | * or NULL if no specific lock mode should be used | ||
| 206 | * for refreshing the managed entity. | ||
| 207 | * @psalm-param array<string, mixed> $id The identifier of the entity as an | ||
| 208 | * associative array from column or | ||
| 209 | * field names to values. | ||
| 210 | * @psalm-param LockMode::*|null $lockMode | ||
| 211 | */ | ||
| 212 | public function refresh(array $id, object $entity, LockMode|int|null $lockMode = null): void; | ||
| 213 | |||
| 214 | /** | ||
| 215 | * Loads Entities matching the given Criteria object. | ||
| 216 | * | ||
| 217 | * @return mixed[] | ||
| 218 | */ | ||
| 219 | public function loadCriteria(Criteria $criteria): array; | ||
| 220 | |||
| 221 | /** | ||
| 222 | * Loads a list of entities by a list of field criteria. | ||
| 223 | * | ||
| 224 | * @psalm-param array<string, string>|null $orderBy | ||
| 225 | * @psalm-param array<string, mixed> $criteria | ||
| 226 | * | ||
| 227 | * @return mixed[] | ||
| 228 | */ | ||
| 229 | public function loadAll( | ||
| 230 | array $criteria = [], | ||
| 231 | array|null $orderBy = null, | ||
| 232 | int|null $limit = null, | ||
| 233 | int|null $offset = null, | ||
| 234 | ): array; | ||
| 235 | |||
| 236 | /** | ||
| 237 | * Gets (sliced or full) elements of the given collection. | ||
| 238 | * | ||
| 239 | * @return mixed[] | ||
| 240 | */ | ||
| 241 | public function getManyToManyCollection( | ||
| 242 | AssociationMapping $assoc, | ||
| 243 | object $sourceEntity, | ||
| 244 | int|null $offset = null, | ||
| 245 | int|null $limit = null, | ||
| 246 | ): array; | ||
| 247 | |||
| 248 | /** | ||
| 249 | * Loads a collection of entities of a many-to-many association. | ||
| 250 | * | ||
| 251 | * @param AssociationMapping $assoc The association mapping of the association being loaded. | ||
| 252 | * @param object $sourceEntity The entity that owns the collection. | ||
| 253 | * @param PersistentCollection $collection The collection to fill. | ||
| 254 | * | ||
| 255 | * @return mixed[] | ||
| 256 | */ | ||
| 257 | public function loadManyToManyCollection( | ||
| 258 | AssociationMapping $assoc, | ||
| 259 | object $sourceEntity, | ||
| 260 | PersistentCollection $collection, | ||
| 261 | ): array; | ||
| 262 | |||
| 263 | /** | ||
| 264 | * Loads a collection of entities in a one-to-many association. | ||
| 265 | * | ||
| 266 | * @param PersistentCollection $collection The collection to load/fill. | ||
| 267 | */ | ||
| 268 | public function loadOneToManyCollection( | ||
| 269 | AssociationMapping $assoc, | ||
| 270 | object $sourceEntity, | ||
| 271 | PersistentCollection $collection, | ||
| 272 | ): mixed; | ||
| 273 | |||
| 274 | /** | ||
| 275 | * Locks all rows of this entity matching the given criteria with the specified pessimistic lock mode. | ||
| 276 | * | ||
| 277 | * @psalm-param array<string, mixed> $criteria | ||
| 278 | * @psalm-param LockMode::* $lockMode | ||
| 279 | */ | ||
| 280 | public function lock(array $criteria, LockMode|int $lockMode): void; | ||
| 281 | |||
| 282 | /** | ||
| 283 | * Returns an array with (sliced or full list) of elements in the specified collection. | ||
| 284 | * | ||
| 285 | * @return mixed[] | ||
| 286 | */ | ||
| 287 | public function getOneToManyCollection( | ||
| 288 | AssociationMapping $assoc, | ||
| 289 | object $sourceEntity, | ||
| 290 | int|null $offset = null, | ||
| 291 | int|null $limit = null, | ||
| 292 | ): array; | ||
| 293 | |||
| 294 | /** | ||
| 295 | * Checks whether the given managed entity exists in the database. | ||
| 296 | */ | ||
| 297 | public function exists(object $entity, Criteria|null $extraConditions = null): bool; | ||
| 298 | } | ||
diff --git a/vendor/doctrine/orm/src/Persisters/Entity/JoinedSubclassPersister.php b/vendor/doctrine/orm/src/Persisters/Entity/JoinedSubclassPersister.php new file mode 100644 index 0000000..76719a2 --- /dev/null +++ b/vendor/doctrine/orm/src/Persisters/Entity/JoinedSubclassPersister.php | |||
| @@ -0,0 +1,601 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Persisters\Entity; | ||
| 6 | |||
| 7 | use Doctrine\Common\Collections\Criteria; | ||
| 8 | use Doctrine\DBAL\LockMode; | ||
| 9 | use Doctrine\DBAL\Types\Type; | ||
| 10 | use Doctrine\DBAL\Types\Types; | ||
| 11 | use Doctrine\ORM\Internal\SQLResultCasing; | ||
| 12 | use Doctrine\ORM\Mapping\AssociationMapping; | ||
| 13 | use Doctrine\ORM\Mapping\ClassMetadata; | ||
| 14 | use Doctrine\ORM\Utility\LockSqlHelper; | ||
| 15 | use Doctrine\ORM\Utility\PersisterHelper; | ||
| 16 | use LengthException; | ||
| 17 | |||
| 18 | use function array_combine; | ||
| 19 | use function array_keys; | ||
| 20 | use function array_values; | ||
| 21 | use function implode; | ||
| 22 | |||
| 23 | /** | ||
| 24 | * The joined subclass persister maps a single entity instance to several tables in the | ||
| 25 | * database as it is defined by the <tt>Class Table Inheritance</tt> strategy. | ||
| 26 | * | ||
| 27 | * @see https://martinfowler.com/eaaCatalog/classTableInheritance.html | ||
| 28 | */ | ||
| 29 | class JoinedSubclassPersister extends AbstractEntityInheritancePersister | ||
| 30 | { | ||
| 31 | use LockSqlHelper; | ||
| 32 | use SQLResultCasing; | ||
| 33 | |||
| 34 | /** | ||
| 35 | * Map that maps column names to the table names that own them. | ||
| 36 | * This is mainly a temporary cache, used during a single request. | ||
| 37 | * | ||
| 38 | * @psalm-var array<string, string> | ||
| 39 | */ | ||
| 40 | private array $owningTableMap = []; | ||
| 41 | |||
| 42 | /** | ||
| 43 | * Map of table to quoted table names. | ||
| 44 | * | ||
| 45 | * @psalm-var array<string, string> | ||
| 46 | */ | ||
| 47 | private array $quotedTableMap = []; | ||
| 48 | |||
| 49 | protected function getDiscriminatorColumnTableName(): string | ||
| 50 | { | ||
| 51 | $class = $this->class->name !== $this->class->rootEntityName | ||
| 52 | ? $this->em->getClassMetadata($this->class->rootEntityName) | ||
| 53 | : $this->class; | ||
| 54 | |||
| 55 | return $class->getTableName(); | ||
| 56 | } | ||
| 57 | |||
| 58 | /** | ||
| 59 | * This function finds the ClassMetadata instance in an inheritance hierarchy | ||
| 60 | * that is responsible for enabling versioning. | ||
| 61 | */ | ||
| 62 | private function getVersionedClassMetadata(): ClassMetadata | ||
| 63 | { | ||
| 64 | if (isset($this->class->fieldMappings[$this->class->versionField]->inherited)) { | ||
| 65 | $definingClassName = $this->class->fieldMappings[$this->class->versionField]->inherited; | ||
| 66 | |||
| 67 | return $this->em->getClassMetadata($definingClassName); | ||
| 68 | } | ||
| 69 | |||
| 70 | return $this->class; | ||
| 71 | } | ||
| 72 | |||
| 73 | /** | ||
| 74 | * Gets the name of the table that owns the column the given field is mapped to. | ||
| 75 | */ | ||
| 76 | public function getOwningTable(string $fieldName): string | ||
| 77 | { | ||
| 78 | if (isset($this->owningTableMap[$fieldName])) { | ||
| 79 | return $this->owningTableMap[$fieldName]; | ||
| 80 | } | ||
| 81 | |||
| 82 | $cm = match (true) { | ||
| 83 | isset($this->class->associationMappings[$fieldName]->inherited) | ||
| 84 | => $this->em->getClassMetadata($this->class->associationMappings[$fieldName]->inherited), | ||
| 85 | isset($this->class->fieldMappings[$fieldName]->inherited) | ||
| 86 | => $this->em->getClassMetadata($this->class->fieldMappings[$fieldName]->inherited), | ||
| 87 | default => $this->class, | ||
| 88 | }; | ||
| 89 | |||
| 90 | $tableName = $cm->getTableName(); | ||
| 91 | $quotedTableName = $this->quoteStrategy->getTableName($cm, $this->platform); | ||
| 92 | |||
| 93 | $this->owningTableMap[$fieldName] = $tableName; | ||
| 94 | $this->quotedTableMap[$tableName] = $quotedTableName; | ||
| 95 | |||
| 96 | return $tableName; | ||
| 97 | } | ||
| 98 | |||
| 99 | public function executeInserts(): void | ||
| 100 | { | ||
| 101 | if (! $this->queuedInserts) { | ||
| 102 | return; | ||
| 103 | } | ||
| 104 | |||
| 105 | $uow = $this->em->getUnitOfWork(); | ||
| 106 | $idGenerator = $this->class->idGenerator; | ||
| 107 | $isPostInsertId = $idGenerator->isPostInsertGenerator(); | ||
| 108 | $rootClass = $this->class->name !== $this->class->rootEntityName | ||
| 109 | ? $this->em->getClassMetadata($this->class->rootEntityName) | ||
| 110 | : $this->class; | ||
| 111 | |||
| 112 | // Prepare statement for the root table | ||
| 113 | $rootPersister = $this->em->getUnitOfWork()->getEntityPersister($rootClass->name); | ||
| 114 | $rootTableName = $rootClass->getTableName(); | ||
| 115 | $rootTableStmt = $this->conn->prepare($rootPersister->getInsertSQL()); | ||
| 116 | |||
| 117 | // Prepare statements for sub tables. | ||
| 118 | $subTableStmts = []; | ||
| 119 | |||
| 120 | if ($rootClass !== $this->class) { | ||
| 121 | $subTableStmts[$this->class->getTableName()] = $this->conn->prepare($this->getInsertSQL()); | ||
| 122 | } | ||
| 123 | |||
| 124 | foreach ($this->class->parentClasses as $parentClassName) { | ||
| 125 | $parentClass = $this->em->getClassMetadata($parentClassName); | ||
| 126 | $parentTableName = $parentClass->getTableName(); | ||
| 127 | |||
| 128 | if ($parentClass !== $rootClass) { | ||
| 129 | $parentPersister = $this->em->getUnitOfWork()->getEntityPersister($parentClassName); | ||
| 130 | $subTableStmts[$parentTableName] = $this->conn->prepare($parentPersister->getInsertSQL()); | ||
| 131 | } | ||
| 132 | } | ||
| 133 | |||
| 134 | // Execute all inserts. For each entity: | ||
| 135 | // 1) Insert on root table | ||
| 136 | // 2) Insert on sub tables | ||
| 137 | foreach ($this->queuedInserts as $entity) { | ||
| 138 | $insertData = $this->prepareInsertData($entity); | ||
| 139 | |||
| 140 | // Execute insert on root table | ||
| 141 | $paramIndex = 1; | ||
| 142 | |||
| 143 | foreach ($insertData[$rootTableName] as $columnName => $value) { | ||
| 144 | $rootTableStmt->bindValue($paramIndex++, $value, $this->columnTypes[$columnName]); | ||
| 145 | } | ||
| 146 | |||
| 147 | $rootTableStmt->executeStatement(); | ||
| 148 | |||
| 149 | if ($isPostInsertId) { | ||
| 150 | $generatedId = $idGenerator->generateId($this->em, $entity); | ||
| 151 | $id = [$this->class->identifier[0] => $generatedId]; | ||
| 152 | |||
| 153 | $uow->assignPostInsertId($entity, $generatedId); | ||
| 154 | } else { | ||
| 155 | $id = $this->em->getUnitOfWork()->getEntityIdentifier($entity); | ||
| 156 | } | ||
| 157 | |||
| 158 | // Execute inserts on subtables. | ||
| 159 | // The order doesn't matter because all child tables link to the root table via FK. | ||
| 160 | foreach ($subTableStmts as $tableName => $stmt) { | ||
| 161 | $paramIndex = 1; | ||
| 162 | $data = $insertData[$tableName] ?? []; | ||
| 163 | |||
| 164 | foreach ($id as $idName => $idVal) { | ||
| 165 | $type = $this->columnTypes[$idName] ?? Types::STRING; | ||
| 166 | |||
| 167 | $stmt->bindValue($paramIndex++, $idVal, $type); | ||
| 168 | } | ||
| 169 | |||
| 170 | foreach ($data as $columnName => $value) { | ||
| 171 | if (! isset($id[$columnName])) { | ||
| 172 | $stmt->bindValue($paramIndex++, $value, $this->columnTypes[$columnName]); | ||
| 173 | } | ||
| 174 | } | ||
| 175 | |||
| 176 | $stmt->executeStatement(); | ||
| 177 | } | ||
| 178 | |||
| 179 | if ($this->class->requiresFetchAfterChange) { | ||
| 180 | $this->assignDefaultVersionAndUpsertableValues($entity, $id); | ||
| 181 | } | ||
| 182 | } | ||
| 183 | |||
| 184 | $this->queuedInserts = []; | ||
| 185 | } | ||
| 186 | |||
| 187 | public function update(object $entity): void | ||
| 188 | { | ||
| 189 | $updateData = $this->prepareUpdateData($entity); | ||
| 190 | |||
| 191 | if (! $updateData) { | ||
| 192 | return; | ||
| 193 | } | ||
| 194 | |||
| 195 | $isVersioned = $this->class->isVersioned; | ||
| 196 | |||
| 197 | $versionedClass = $this->getVersionedClassMetadata(); | ||
| 198 | $versionedTable = $versionedClass->getTableName(); | ||
| 199 | |||
| 200 | foreach ($updateData as $tableName => $data) { | ||
| 201 | $tableName = $this->quotedTableMap[$tableName]; | ||
| 202 | $versioned = $isVersioned && $versionedTable === $tableName; | ||
| 203 | |||
| 204 | $this->updateTable($entity, $tableName, $data, $versioned); | ||
| 205 | } | ||
| 206 | |||
| 207 | if ($this->class->requiresFetchAfterChange) { | ||
| 208 | // Make sure the table with the version column is updated even if no columns on that | ||
| 209 | // table were affected. | ||
| 210 | if ($isVersioned && ! isset($updateData[$versionedTable])) { | ||
| 211 | $tableName = $this->quoteStrategy->getTableName($versionedClass, $this->platform); | ||
| 212 | |||
| 213 | $this->updateTable($entity, $tableName, [], true); | ||
| 214 | } | ||
| 215 | |||
| 216 | $identifiers = $this->em->getUnitOfWork()->getEntityIdentifier($entity); | ||
| 217 | |||
| 218 | $this->assignDefaultVersionAndUpsertableValues($entity, $identifiers); | ||
| 219 | } | ||
| 220 | } | ||
| 221 | |||
| 222 | public function delete(object $entity): bool | ||
| 223 | { | ||
| 224 | $identifier = $this->em->getUnitOfWork()->getEntityIdentifier($entity); | ||
| 225 | $id = array_combine($this->class->getIdentifierColumnNames(), $identifier); | ||
| 226 | $types = $this->getClassIdentifiersTypes($this->class); | ||
| 227 | |||
| 228 | $this->deleteJoinTableRecords($identifier, $types); | ||
| 229 | |||
| 230 | // Delete the row from the root table. Cascades do the rest. | ||
| 231 | $rootClass = $this->em->getClassMetadata($this->class->rootEntityName); | ||
| 232 | $rootTable = $this->quoteStrategy->getTableName($rootClass, $this->platform); | ||
| 233 | $rootTypes = $this->getClassIdentifiersTypes($rootClass); | ||
| 234 | |||
| 235 | return (bool) $this->conn->delete($rootTable, $id, $rootTypes); | ||
| 236 | } | ||
| 237 | |||
| 238 | public function getSelectSQL( | ||
| 239 | array|Criteria $criteria, | ||
| 240 | AssociationMapping|null $assoc = null, | ||
| 241 | LockMode|int|null $lockMode = null, | ||
| 242 | int|null $limit = null, | ||
| 243 | int|null $offset = null, | ||
| 244 | array|null $orderBy = null, | ||
| 245 | ): string { | ||
| 246 | $this->switchPersisterContext($offset, $limit); | ||
| 247 | |||
| 248 | $baseTableAlias = $this->getSQLTableAlias($this->class->name); | ||
| 249 | $joinSql = $this->getJoinSql($baseTableAlias); | ||
| 250 | |||
| 251 | if ($assoc !== null && $assoc->isManyToMany()) { | ||
| 252 | $joinSql .= $this->getSelectManyToManyJoinSQL($assoc); | ||
| 253 | } | ||
| 254 | |||
| 255 | $conditionSql = $criteria instanceof Criteria | ||
| 256 | ? $this->getSelectConditionCriteriaSQL($criteria) | ||
| 257 | : $this->getSelectConditionSQL($criteria, $assoc); | ||
| 258 | |||
| 259 | $filterSql = $this->generateFilterConditionSQL( | ||
| 260 | $this->em->getClassMetadata($this->class->rootEntityName), | ||
| 261 | $this->getSQLTableAlias($this->class->rootEntityName), | ||
| 262 | ); | ||
| 263 | // If the current class in the root entity, add the filters | ||
| 264 | if ($filterSql) { | ||
| 265 | $conditionSql .= $conditionSql | ||
| 266 | ? ' AND ' . $filterSql | ||
| 267 | : $filterSql; | ||
| 268 | } | ||
| 269 | |||
| 270 | $orderBySql = ''; | ||
| 271 | |||
| 272 | if ($assoc !== null && $assoc->isOrdered()) { | ||
| 273 | $orderBy = $assoc->orderBy(); | ||
| 274 | } | ||
| 275 | |||
| 276 | if ($orderBy) { | ||
| 277 | $orderBySql = $this->getOrderBySQL($orderBy, $baseTableAlias); | ||
| 278 | } | ||
| 279 | |||
| 280 | $lockSql = ''; | ||
| 281 | |||
| 282 | switch ($lockMode) { | ||
| 283 | case LockMode::PESSIMISTIC_READ: | ||
| 284 | $lockSql = ' ' . $this->getReadLockSQL($this->platform); | ||
| 285 | |||
| 286 | break; | ||
| 287 | |||
| 288 | case LockMode::PESSIMISTIC_WRITE: | ||
| 289 | $lockSql = ' ' . $this->getWriteLockSQL($this->platform); | ||
| 290 | |||
| 291 | break; | ||
| 292 | } | ||
| 293 | |||
| 294 | $tableName = $this->quoteStrategy->getTableName($this->class, $this->platform); | ||
| 295 | $from = ' FROM ' . $tableName . ' ' . $baseTableAlias; | ||
| 296 | $where = $conditionSql !== '' ? ' WHERE ' . $conditionSql : ''; | ||
| 297 | $lock = $this->platform->appendLockHint($from, $lockMode ?? LockMode::NONE); | ||
| 298 | $columnList = $this->getSelectColumnsSQL(); | ||
| 299 | $query = 'SELECT ' . $columnList | ||
| 300 | . $lock | ||
| 301 | . $joinSql | ||
| 302 | . $where | ||
| 303 | . $orderBySql; | ||
| 304 | |||
| 305 | return $this->platform->modifyLimitQuery($query, $limit, $offset ?? 0) . $lockSql; | ||
| 306 | } | ||
| 307 | |||
| 308 | public function getCountSQL(array|Criteria $criteria = []): string | ||
| 309 | { | ||
| 310 | $tableName = $this->quoteStrategy->getTableName($this->class, $this->platform); | ||
| 311 | $baseTableAlias = $this->getSQLTableAlias($this->class->name); | ||
| 312 | $joinSql = $this->getJoinSql($baseTableAlias); | ||
| 313 | |||
| 314 | $conditionSql = $criteria instanceof Criteria | ||
| 315 | ? $this->getSelectConditionCriteriaSQL($criteria) | ||
| 316 | : $this->getSelectConditionSQL($criteria); | ||
| 317 | |||
| 318 | $filterSql = $this->generateFilterConditionSQL($this->em->getClassMetadata($this->class->rootEntityName), $this->getSQLTableAlias($this->class->rootEntityName)); | ||
| 319 | |||
| 320 | if ($filterSql !== '') { | ||
| 321 | $conditionSql = $conditionSql | ||
| 322 | ? $conditionSql . ' AND ' . $filterSql | ||
| 323 | : $filterSql; | ||
| 324 | } | ||
| 325 | |||
| 326 | return 'SELECT COUNT(*) ' | ||
| 327 | . 'FROM ' . $tableName . ' ' . $baseTableAlias | ||
| 328 | . $joinSql | ||
| 329 | . (empty($conditionSql) ? '' : ' WHERE ' . $conditionSql); | ||
| 330 | } | ||
| 331 | |||
| 332 | protected function getLockTablesSql(LockMode|int $lockMode): string | ||
| 333 | { | ||
| 334 | $joinSql = ''; | ||
| 335 | $identifierColumns = $this->class->getIdentifierColumnNames(); | ||
| 336 | $baseTableAlias = $this->getSQLTableAlias($this->class->name); | ||
| 337 | |||
| 338 | // INNER JOIN parent tables | ||
| 339 | foreach ($this->class->parentClasses as $parentClassName) { | ||
| 340 | $conditions = []; | ||
| 341 | $tableAlias = $this->getSQLTableAlias($parentClassName); | ||
| 342 | $parentClass = $this->em->getClassMetadata($parentClassName); | ||
| 343 | $joinSql .= ' INNER JOIN ' . $this->quoteStrategy->getTableName($parentClass, $this->platform) . ' ' . $tableAlias . ' ON '; | ||
| 344 | |||
| 345 | foreach ($identifierColumns as $idColumn) { | ||
| 346 | $conditions[] = $baseTableAlias . '.' . $idColumn . ' = ' . $tableAlias . '.' . $idColumn; | ||
| 347 | } | ||
| 348 | |||
| 349 | $joinSql .= implode(' AND ', $conditions); | ||
| 350 | } | ||
| 351 | |||
| 352 | return parent::getLockTablesSql($lockMode) . $joinSql; | ||
| 353 | } | ||
| 354 | |||
| 355 | /** | ||
| 356 | * Ensure this method is never called. This persister overrides getSelectEntitiesSQL directly. | ||
| 357 | */ | ||
| 358 | protected function getSelectColumnsSQL(): string | ||
| 359 | { | ||
| 360 | // Create the column list fragment only once | ||
| 361 | if ($this->currentPersisterContext->selectColumnListSql !== null) { | ||
| 362 | return $this->currentPersisterContext->selectColumnListSql; | ||
| 363 | } | ||
| 364 | |||
| 365 | $columnList = []; | ||
| 366 | $discrColumn = $this->class->getDiscriminatorColumn(); | ||
| 367 | $discrColumnName = $discrColumn->name; | ||
| 368 | $discrColumnType = $discrColumn->type; | ||
| 369 | $baseTableAlias = $this->getSQLTableAlias($this->class->name); | ||
| 370 | $resultColumnName = $this->getSQLResultCasing($this->platform, $discrColumnName); | ||
| 371 | |||
| 372 | $this->currentPersisterContext->rsm->addEntityResult($this->class->name, 'r'); | ||
| 373 | $this->currentPersisterContext->rsm->setDiscriminatorColumn('r', $resultColumnName); | ||
| 374 | $this->currentPersisterContext->rsm->addMetaResult('r', $resultColumnName, $discrColumnName, false, $discrColumnType); | ||
| 375 | |||
| 376 | // Add regular columns | ||
| 377 | foreach ($this->class->fieldMappings as $fieldName => $mapping) { | ||
| 378 | $class = isset($mapping->inherited) | ||
| 379 | ? $this->em->getClassMetadata($mapping->inherited) | ||
| 380 | : $this->class; | ||
| 381 | |||
| 382 | $columnList[] = $this->getSelectColumnSQL($fieldName, $class); | ||
| 383 | } | ||
| 384 | |||
| 385 | // Add foreign key columns | ||
| 386 | foreach ($this->class->associationMappings as $mapping) { | ||
| 387 | if (! $mapping->isToOneOwningSide()) { | ||
| 388 | continue; | ||
| 389 | } | ||
| 390 | |||
| 391 | $tableAlias = isset($mapping->inherited) | ||
| 392 | ? $this->getSQLTableAlias($mapping->inherited) | ||
| 393 | : $baseTableAlias; | ||
| 394 | |||
| 395 | $targetClass = $this->em->getClassMetadata($mapping->targetEntity); | ||
| 396 | |||
| 397 | foreach ($mapping->joinColumns as $joinColumn) { | ||
| 398 | $columnList[] = $this->getSelectJoinColumnSQL( | ||
| 399 | $tableAlias, | ||
| 400 | $joinColumn->name, | ||
| 401 | $this->quoteStrategy->getJoinColumnName($joinColumn, $this->class, $this->platform), | ||
| 402 | PersisterHelper::getTypeOfColumn($joinColumn->referencedColumnName, $targetClass, $this->em), | ||
| 403 | ); | ||
| 404 | } | ||
| 405 | } | ||
| 406 | |||
| 407 | // Add discriminator column (DO NOT ALIAS, see AbstractEntityInheritancePersister#processSQLResult). | ||
| 408 | $tableAlias = $this->class->rootEntityName === $this->class->name | ||
| 409 | ? $baseTableAlias | ||
| 410 | : $this->getSQLTableAlias($this->class->rootEntityName); | ||
| 411 | |||
| 412 | $columnList[] = $tableAlias . '.' . $discrColumnName; | ||
| 413 | |||
| 414 | // sub tables | ||
| 415 | foreach ($this->class->subClasses as $subClassName) { | ||
| 416 | $subClass = $this->em->getClassMetadata($subClassName); | ||
| 417 | $tableAlias = $this->getSQLTableAlias($subClassName); | ||
| 418 | |||
| 419 | // Add subclass columns | ||
| 420 | foreach ($subClass->fieldMappings as $fieldName => $mapping) { | ||
| 421 | if (isset($mapping->inherited)) { | ||
| 422 | continue; | ||
| 423 | } | ||
| 424 | |||
| 425 | $columnList[] = $this->getSelectColumnSQL($fieldName, $subClass); | ||
| 426 | } | ||
| 427 | |||
| 428 | // Add join columns (foreign keys) | ||
| 429 | foreach ($subClass->associationMappings as $mapping) { | ||
| 430 | if (! $mapping->isToOneOwningSide() || isset($mapping->inherited)) { | ||
| 431 | continue; | ||
| 432 | } | ||
| 433 | |||
| 434 | $targetClass = $this->em->getClassMetadata($mapping->targetEntity); | ||
| 435 | |||
| 436 | foreach ($mapping->joinColumns as $joinColumn) { | ||
| 437 | $columnList[] = $this->getSelectJoinColumnSQL( | ||
| 438 | $tableAlias, | ||
| 439 | $joinColumn->name, | ||
| 440 | $this->quoteStrategy->getJoinColumnName($joinColumn, $subClass, $this->platform), | ||
| 441 | PersisterHelper::getTypeOfColumn($joinColumn->referencedColumnName, $targetClass, $this->em), | ||
| 442 | ); | ||
| 443 | } | ||
| 444 | } | ||
| 445 | } | ||
| 446 | |||
| 447 | $this->currentPersisterContext->selectColumnListSql = implode(', ', $columnList); | ||
| 448 | |||
| 449 | return $this->currentPersisterContext->selectColumnListSql; | ||
| 450 | } | ||
| 451 | |||
| 452 | /** | ||
| 453 | * {@inheritDoc} | ||
| 454 | */ | ||
| 455 | protected function getInsertColumnList(): array | ||
| 456 | { | ||
| 457 | // Identifier columns must always come first in the column list of subclasses. | ||
| 458 | $columns = $this->class->parentClasses | ||
| 459 | ? $this->class->getIdentifierColumnNames() | ||
| 460 | : []; | ||
| 461 | |||
| 462 | foreach ($this->class->reflFields as $name => $field) { | ||
| 463 | if ( | ||
| 464 | isset($this->class->fieldMappings[$name]->inherited) | ||
| 465 | && ! isset($this->class->fieldMappings[$name]->id) | ||
| 466 | || isset($this->class->associationMappings[$name]->inherited) | ||
| 467 | || ($this->class->isVersioned && $this->class->versionField === $name) | ||
| 468 | || isset($this->class->embeddedClasses[$name]) | ||
| 469 | || isset($this->class->fieldMappings[$name]->notInsertable) | ||
| 470 | ) { | ||
| 471 | continue; | ||
| 472 | } | ||
| 473 | |||
| 474 | if (isset($this->class->associationMappings[$name])) { | ||
| 475 | $assoc = $this->class->associationMappings[$name]; | ||
| 476 | if ($assoc->isToOneOwningSide()) { | ||
| 477 | foreach ($assoc->targetToSourceKeyColumns as $sourceCol) { | ||
| 478 | $columns[] = $sourceCol; | ||
| 479 | } | ||
| 480 | } | ||
| 481 | } elseif ( | ||
| 482 | $this->class->name !== $this->class->rootEntityName || | ||
| 483 | ! $this->class->isIdGeneratorIdentity() || $this->class->identifier[0] !== $name | ||
| 484 | ) { | ||
| 485 | $columns[] = $this->quoteStrategy->getColumnName($name, $this->class, $this->platform); | ||
| 486 | $this->columnTypes[$name] = $this->class->fieldMappings[$name]->type; | ||
| 487 | } | ||
| 488 | } | ||
| 489 | |||
| 490 | // Add discriminator column if it is the topmost class. | ||
| 491 | if ($this->class->name === $this->class->rootEntityName) { | ||
| 492 | $columns[] = $this->class->getDiscriminatorColumn()->name; | ||
| 493 | } | ||
| 494 | |||
| 495 | return $columns; | ||
| 496 | } | ||
| 497 | |||
| 498 | /** | ||
| 499 | * {@inheritDoc} | ||
| 500 | */ | ||
| 501 | protected function assignDefaultVersionAndUpsertableValues(object $entity, array $id): void | ||
| 502 | { | ||
| 503 | $values = $this->fetchVersionAndNotUpsertableValues($this->getVersionedClassMetadata(), $id); | ||
| 504 | |||
| 505 | foreach ($values as $field => $value) { | ||
| 506 | $value = Type::getType($this->class->fieldMappings[$field]->type)->convertToPHPValue($value, $this->platform); | ||
| 507 | |||
| 508 | $this->class->setFieldValue($entity, $field, $value); | ||
| 509 | } | ||
| 510 | } | ||
| 511 | |||
| 512 | /** | ||
| 513 | * {@inheritDoc} | ||
| 514 | */ | ||
| 515 | protected function fetchVersionAndNotUpsertableValues(ClassMetadata $versionedClass, array $id): mixed | ||
| 516 | { | ||
| 517 | $columnNames = []; | ||
| 518 | foreach ($this->class->fieldMappings as $key => $column) { | ||
| 519 | $class = null; | ||
| 520 | if ($this->class->isVersioned && $key === $versionedClass->versionField) { | ||
| 521 | $class = $versionedClass; | ||
| 522 | } elseif (isset($column->generated)) { | ||
| 523 | $class = isset($column->inherited) | ||
| 524 | ? $this->em->getClassMetadata($column->inherited) | ||
| 525 | : $this->class; | ||
| 526 | } else { | ||
| 527 | continue; | ||
| 528 | } | ||
| 529 | |||
| 530 | $columnNames[$key] = $this->getSelectColumnSQL($key, $class); | ||
| 531 | } | ||
| 532 | |||
| 533 | $tableName = $this->quoteStrategy->getTableName($versionedClass, $this->platform); | ||
| 534 | $baseTableAlias = $this->getSQLTableAlias($this->class->name); | ||
| 535 | $joinSql = $this->getJoinSql($baseTableAlias); | ||
| 536 | $identifier = $this->quoteStrategy->getIdentifierColumnNames($versionedClass, $this->platform); | ||
| 537 | foreach ($identifier as $i => $idValue) { | ||
| 538 | $identifier[$i] = $baseTableAlias . '.' . $idValue; | ||
| 539 | } | ||
| 540 | |||
| 541 | $sql = 'SELECT ' . implode(', ', $columnNames) | ||
| 542 | . ' FROM ' . $tableName . ' ' . $baseTableAlias | ||
| 543 | . $joinSql | ||
| 544 | . ' WHERE ' . implode(' = ? AND ', $identifier) . ' = ?'; | ||
| 545 | |||
| 546 | $flatId = $this->identifierFlattener->flattenIdentifier($versionedClass, $id); | ||
| 547 | $values = $this->conn->fetchNumeric( | ||
| 548 | $sql, | ||
| 549 | array_values($flatId), | ||
| 550 | $this->extractIdentifierTypes($id, $versionedClass), | ||
| 551 | ); | ||
| 552 | |||
| 553 | if ($values === false) { | ||
| 554 | throw new LengthException('Unexpected empty result for database query.'); | ||
| 555 | } | ||
| 556 | |||
| 557 | $values = array_combine(array_keys($columnNames), $values); | ||
| 558 | |||
| 559 | if (! $values) { | ||
| 560 | throw new LengthException('Unexpected number of database columns.'); | ||
| 561 | } | ||
| 562 | |||
| 563 | return $values; | ||
| 564 | } | ||
| 565 | |||
| 566 | private function getJoinSql(string $baseTableAlias): string | ||
| 567 | { | ||
| 568 | $joinSql = ''; | ||
| 569 | $identifierColumn = $this->class->getIdentifierColumnNames(); | ||
| 570 | |||
| 571 | // INNER JOIN parent tables | ||
| 572 | foreach ($this->class->parentClasses as $parentClassName) { | ||
| 573 | $conditions = []; | ||
| 574 | $parentClass = $this->em->getClassMetadata($parentClassName); | ||
| 575 | $tableAlias = $this->getSQLTableAlias($parentClassName); | ||
| 576 | $joinSql .= ' INNER JOIN ' . $this->quoteStrategy->getTableName($parentClass, $this->platform) . ' ' . $tableAlias . ' ON '; | ||
| 577 | |||
| 578 | foreach ($identifierColumn as $idColumn) { | ||
| 579 | $conditions[] = $baseTableAlias . '.' . $idColumn . ' = ' . $tableAlias . '.' . $idColumn; | ||
| 580 | } | ||
| 581 | |||
| 582 | $joinSql .= implode(' AND ', $conditions); | ||
| 583 | } | ||
| 584 | |||
| 585 | // OUTER JOIN sub tables | ||
| 586 | foreach ($this->class->subClasses as $subClassName) { | ||
| 587 | $conditions = []; | ||
| 588 | $subClass = $this->em->getClassMetadata($subClassName); | ||
| 589 | $tableAlias = $this->getSQLTableAlias($subClassName); | ||
| 590 | $joinSql .= ' LEFT JOIN ' . $this->quoteStrategy->getTableName($subClass, $this->platform) . ' ' . $tableAlias . ' ON '; | ||
| 591 | |||
| 592 | foreach ($identifierColumn as $idColumn) { | ||
| 593 | $conditions[] = $baseTableAlias . '.' . $idColumn . ' = ' . $tableAlias . '.' . $idColumn; | ||
| 594 | } | ||
| 595 | |||
| 596 | $joinSql .= implode(' AND ', $conditions); | ||
| 597 | } | ||
| 598 | |||
| 599 | return $joinSql; | ||
| 600 | } | ||
| 601 | } | ||
diff --git a/vendor/doctrine/orm/src/Persisters/Entity/SingleTablePersister.php b/vendor/doctrine/orm/src/Persisters/Entity/SingleTablePersister.php new file mode 100644 index 0000000..4a4d999 --- /dev/null +++ b/vendor/doctrine/orm/src/Persisters/Entity/SingleTablePersister.php | |||
| @@ -0,0 +1,166 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Persisters\Entity; | ||
| 6 | |||
| 7 | use Doctrine\Common\Collections\Criteria; | ||
| 8 | use Doctrine\ORM\Internal\SQLResultCasing; | ||
| 9 | use Doctrine\ORM\Mapping\AssociationMapping; | ||
| 10 | use Doctrine\ORM\Mapping\ClassMetadata; | ||
| 11 | use Doctrine\ORM\Utility\PersisterHelper; | ||
| 12 | |||
| 13 | use function array_flip; | ||
| 14 | use function array_intersect; | ||
| 15 | use function array_map; | ||
| 16 | use function array_unshift; | ||
| 17 | use function implode; | ||
| 18 | use function strval; | ||
| 19 | |||
| 20 | /** | ||
| 21 | * Persister for entities that participate in a hierarchy mapped with the | ||
| 22 | * SINGLE_TABLE strategy. | ||
| 23 | * | ||
| 24 | * @link https://martinfowler.com/eaaCatalog/singleTableInheritance.html | ||
| 25 | */ | ||
| 26 | class SingleTablePersister extends AbstractEntityInheritancePersister | ||
| 27 | { | ||
| 28 | use SQLResultCasing; | ||
| 29 | |||
| 30 | protected function getDiscriminatorColumnTableName(): string | ||
| 31 | { | ||
| 32 | return $this->class->getTableName(); | ||
| 33 | } | ||
| 34 | |||
| 35 | protected function getSelectColumnsSQL(): string | ||
| 36 | { | ||
| 37 | $columnList = []; | ||
| 38 | if ($this->currentPersisterContext->selectColumnListSql !== null) { | ||
| 39 | return $this->currentPersisterContext->selectColumnListSql; | ||
| 40 | } | ||
| 41 | |||
| 42 | $columnList[] = parent::getSelectColumnsSQL(); | ||
| 43 | |||
| 44 | $rootClass = $this->em->getClassMetadata($this->class->rootEntityName); | ||
| 45 | $tableAlias = $this->getSQLTableAlias($rootClass->name); | ||
| 46 | |||
| 47 | // Append discriminator column | ||
| 48 | $discrColumn = $this->class->getDiscriminatorColumn(); | ||
| 49 | $discrColumnName = $discrColumn->name; | ||
| 50 | $discrColumnType = $discrColumn->type; | ||
| 51 | |||
| 52 | $columnList[] = $tableAlias . '.' . $discrColumnName; | ||
| 53 | |||
| 54 | $resultColumnName = $this->getSQLResultCasing($this->platform, $discrColumnName); | ||
| 55 | |||
| 56 | $this->currentPersisterContext->rsm->setDiscriminatorColumn('r', $resultColumnName); | ||
| 57 | $this->currentPersisterContext->rsm->addMetaResult('r', $resultColumnName, $discrColumnName, false, $discrColumnType); | ||
| 58 | |||
| 59 | // Append subclass columns | ||
| 60 | foreach ($this->class->subClasses as $subClassName) { | ||
| 61 | $subClass = $this->em->getClassMetadata($subClassName); | ||
| 62 | |||
| 63 | // Regular columns | ||
| 64 | foreach ($subClass->fieldMappings as $fieldName => $mapping) { | ||
| 65 | if (isset($mapping->inherited)) { | ||
| 66 | continue; | ||
| 67 | } | ||
| 68 | |||
| 69 | $columnList[] = $this->getSelectColumnSQL($fieldName, $subClass); | ||
| 70 | } | ||
| 71 | |||
| 72 | // Foreign key columns | ||
| 73 | foreach ($subClass->associationMappings as $assoc) { | ||
| 74 | if (! $assoc->isToOneOwningSide() || isset($assoc->inherited)) { | ||
| 75 | continue; | ||
| 76 | } | ||
| 77 | |||
| 78 | $targetClass = $this->em->getClassMetadata($assoc->targetEntity); | ||
| 79 | |||
| 80 | foreach ($assoc->joinColumns as $joinColumn) { | ||
| 81 | $columnList[] = $this->getSelectJoinColumnSQL( | ||
| 82 | $tableAlias, | ||
| 83 | $joinColumn->name, | ||
| 84 | $this->quoteStrategy->getJoinColumnName($joinColumn, $subClass, $this->platform), | ||
| 85 | PersisterHelper::getTypeOfColumn($joinColumn->referencedColumnName, $targetClass, $this->em), | ||
| 86 | ); | ||
| 87 | } | ||
| 88 | } | ||
| 89 | } | ||
| 90 | |||
| 91 | $this->currentPersisterContext->selectColumnListSql = implode(', ', $columnList); | ||
| 92 | |||
| 93 | return $this->currentPersisterContext->selectColumnListSql; | ||
| 94 | } | ||
| 95 | |||
| 96 | /** | ||
| 97 | * {@inheritDoc} | ||
| 98 | */ | ||
| 99 | protected function getInsertColumnList(): array | ||
| 100 | { | ||
| 101 | $columns = parent::getInsertColumnList(); | ||
| 102 | |||
| 103 | // Add discriminator column to the INSERT SQL | ||
| 104 | $columns[] = $this->class->getDiscriminatorColumn()->name; | ||
| 105 | |||
| 106 | return $columns; | ||
| 107 | } | ||
| 108 | |||
| 109 | protected function getSQLTableAlias(string $className, string $assocName = ''): string | ||
| 110 | { | ||
| 111 | return parent::getSQLTableAlias($this->class->rootEntityName, $assocName); | ||
| 112 | } | ||
| 113 | |||
| 114 | /** | ||
| 115 | * {@inheritDoc} | ||
| 116 | */ | ||
| 117 | protected function getSelectConditionSQL(array $criteria, AssociationMapping|null $assoc = null): string | ||
| 118 | { | ||
| 119 | $conditionSql = parent::getSelectConditionSQL($criteria, $assoc); | ||
| 120 | |||
| 121 | if ($conditionSql) { | ||
| 122 | $conditionSql .= ' AND '; | ||
| 123 | } | ||
| 124 | |||
| 125 | return $conditionSql . $this->getSelectConditionDiscriminatorValueSQL(); | ||
| 126 | } | ||
| 127 | |||
| 128 | protected function getSelectConditionCriteriaSQL(Criteria $criteria): string | ||
| 129 | { | ||
| 130 | $conditionSql = parent::getSelectConditionCriteriaSQL($criteria); | ||
| 131 | |||
| 132 | if ($conditionSql) { | ||
| 133 | $conditionSql .= ' AND '; | ||
| 134 | } | ||
| 135 | |||
| 136 | return $conditionSql . $this->getSelectConditionDiscriminatorValueSQL(); | ||
| 137 | } | ||
| 138 | |||
| 139 | protected function getSelectConditionDiscriminatorValueSQL(): string | ||
| 140 | { | ||
| 141 | $values = array_map($this->conn->quote(...), array_map( | ||
| 142 | strval(...), | ||
| 143 | array_flip(array_intersect($this->class->discriminatorMap, $this->class->subClasses)), | ||
| 144 | )); | ||
| 145 | |||
| 146 | if ($this->class->discriminatorValue !== null) { // discriminators can be 0 | ||
| 147 | array_unshift($values, $this->conn->quote((string) $this->class->discriminatorValue)); | ||
| 148 | } | ||
| 149 | |||
| 150 | $discColumnName = $this->class->getDiscriminatorColumn()->name; | ||
| 151 | |||
| 152 | $values = implode(', ', $values); | ||
| 153 | $tableAlias = $this->getSQLTableAlias($this->class->name); | ||
| 154 | |||
| 155 | return $tableAlias . '.' . $discColumnName . ' IN (' . $values . ')'; | ||
| 156 | } | ||
| 157 | |||
| 158 | protected function generateFilterConditionSQL(ClassMetadata $targetEntity, string $targetTableAlias): string | ||
| 159 | { | ||
| 160 | // Ensure that the filters are applied to the root entity of the inheritance tree | ||
| 161 | $targetEntity = $this->em->getClassMetadata($targetEntity->rootEntityName); | ||
| 162 | // we don't care about the $targetTableAlias, in a STI there is only one table. | ||
| 163 | |||
| 164 | return parent::generateFilterConditionSQL($targetEntity, $targetTableAlias); | ||
| 165 | } | ||
| 166 | } | ||
diff --git a/vendor/doctrine/orm/src/Persisters/Exception/CantUseInOperatorOnCompositeKeys.php b/vendor/doctrine/orm/src/Persisters/Exception/CantUseInOperatorOnCompositeKeys.php new file mode 100644 index 0000000..5c91312 --- /dev/null +++ b/vendor/doctrine/orm/src/Persisters/Exception/CantUseInOperatorOnCompositeKeys.php | |||
| @@ -0,0 +1,15 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Persisters\Exception; | ||
| 6 | |||
| 7 | use Doctrine\ORM\Exception\PersisterException; | ||
| 8 | |||
| 9 | class CantUseInOperatorOnCompositeKeys extends PersisterException | ||
| 10 | { | ||
| 11 | public static function create(): self | ||
| 12 | { | ||
| 13 | return new self("Can't use IN operator on entities that have composite keys."); | ||
| 14 | } | ||
| 15 | } | ||
diff --git a/vendor/doctrine/orm/src/Persisters/Exception/InvalidOrientation.php b/vendor/doctrine/orm/src/Persisters/Exception/InvalidOrientation.php new file mode 100644 index 0000000..7532800 --- /dev/null +++ b/vendor/doctrine/orm/src/Persisters/Exception/InvalidOrientation.php | |||
| @@ -0,0 +1,15 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Persisters\Exception; | ||
| 6 | |||
| 7 | use Doctrine\ORM\Exception\PersisterException; | ||
| 8 | |||
| 9 | class InvalidOrientation extends PersisterException | ||
| 10 | { | ||
| 11 | public static function fromClassNameAndField(string $className, string $field): self | ||
| 12 | { | ||
| 13 | return new self('Invalid order by orientation specified for ' . $className . '#' . $field); | ||
| 14 | } | ||
| 15 | } | ||
diff --git a/vendor/doctrine/orm/src/Persisters/Exception/UnrecognizedField.php b/vendor/doctrine/orm/src/Persisters/Exception/UnrecognizedField.php new file mode 100644 index 0000000..be7303e --- /dev/null +++ b/vendor/doctrine/orm/src/Persisters/Exception/UnrecognizedField.php | |||
| @@ -0,0 +1,24 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Persisters\Exception; | ||
| 6 | |||
| 7 | use Doctrine\ORM\Exception\PersisterException; | ||
| 8 | |||
| 9 | use function sprintf; | ||
| 10 | |||
| 11 | final class UnrecognizedField extends PersisterException | ||
| 12 | { | ||
| 13 | /** @deprecated Use {@see byFullyQualifiedName()} instead. */ | ||
| 14 | public static function byName(string $field): self | ||
| 15 | { | ||
| 16 | return new self(sprintf('Unrecognized field: %s', $field)); | ||
| 17 | } | ||
| 18 | |||
| 19 | /** @param class-string $className */ | ||
| 20 | public static function byFullyQualifiedName(string $className, string $field): self | ||
| 21 | { | ||
| 22 | return new self(sprintf('Unrecognized field: %s::$%s', $className, $field)); | ||
| 23 | } | ||
| 24 | } | ||
diff --git a/vendor/doctrine/orm/src/Persisters/MatchingAssociationFieldRequiresObject.php b/vendor/doctrine/orm/src/Persisters/MatchingAssociationFieldRequiresObject.php new file mode 100644 index 0000000..4e7251e --- /dev/null +++ b/vendor/doctrine/orm/src/Persisters/MatchingAssociationFieldRequiresObject.php | |||
| @@ -0,0 +1,22 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Persisters; | ||
| 6 | |||
| 7 | use Doctrine\ORM\Exception\PersisterException; | ||
| 8 | |||
| 9 | use function sprintf; | ||
| 10 | |||
| 11 | final class MatchingAssociationFieldRequiresObject extends PersisterException | ||
| 12 | { | ||
| 13 | public static function fromClassAndAssociation(string $class, string $associationName): self | ||
| 14 | { | ||
| 15 | return new self(sprintf( | ||
| 16 | 'Cannot match on %s::%s with a non-object value. Matching objects by id is ' . | ||
| 17 | 'not compatible with matching on an in-memory collection, which compares objects by reference.', | ||
| 18 | $class, | ||
| 19 | $associationName, | ||
| 20 | )); | ||
| 21 | } | ||
| 22 | } | ||
diff --git a/vendor/doctrine/orm/src/Persisters/PersisterException.php b/vendor/doctrine/orm/src/Persisters/PersisterException.php new file mode 100644 index 0000000..0016472 --- /dev/null +++ b/vendor/doctrine/orm/src/Persisters/PersisterException.php | |||
| @@ -0,0 +1,23 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Persisters; | ||
| 6 | |||
| 7 | use Doctrine\ORM\Exception\ORMException; | ||
| 8 | use Exception; | ||
| 9 | |||
| 10 | use function sprintf; | ||
| 11 | |||
| 12 | class PersisterException extends Exception implements ORMException | ||
| 13 | { | ||
| 14 | public static function matchingAssocationFieldRequiresObject(string $class, string $associationName): PersisterException | ||
| 15 | { | ||
| 16 | return new self(sprintf( | ||
| 17 | 'Cannot match on %s::%s with a non-object value. Matching objects by id is ' . | ||
| 18 | 'not compatible with matching on an in-memory collection, which compares objects by reference.', | ||
| 19 | $class, | ||
| 20 | $associationName, | ||
| 21 | )); | ||
| 22 | } | ||
| 23 | } | ||
diff --git a/vendor/doctrine/orm/src/Persisters/SqlExpressionVisitor.php b/vendor/doctrine/orm/src/Persisters/SqlExpressionVisitor.php new file mode 100644 index 0000000..df60f6a --- /dev/null +++ b/vendor/doctrine/orm/src/Persisters/SqlExpressionVisitor.php | |||
| @@ -0,0 +1,79 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Persisters; | ||
| 6 | |||
| 7 | use Doctrine\Common\Collections\Expr\Comparison; | ||
| 8 | use Doctrine\Common\Collections\Expr\CompositeExpression; | ||
| 9 | use Doctrine\Common\Collections\Expr\ExpressionVisitor; | ||
| 10 | use Doctrine\Common\Collections\Expr\Value; | ||
| 11 | use Doctrine\ORM\Mapping\ClassMetadata; | ||
| 12 | use Doctrine\ORM\Persisters\Entity\BasicEntityPersister; | ||
| 13 | use RuntimeException; | ||
| 14 | |||
| 15 | use function implode; | ||
| 16 | use function in_array; | ||
| 17 | use function is_object; | ||
| 18 | |||
| 19 | /** | ||
| 20 | * Visit Expressions and generate SQL WHERE conditions from them. | ||
| 21 | */ | ||
| 22 | class SqlExpressionVisitor extends ExpressionVisitor | ||
| 23 | { | ||
| 24 | public function __construct( | ||
| 25 | private readonly BasicEntityPersister $persister, | ||
| 26 | private readonly ClassMetadata $classMetadata, | ||
| 27 | ) { | ||
| 28 | } | ||
| 29 | |||
| 30 | /** Converts a comparison expression into the target query language output. */ | ||
| 31 | public function walkComparison(Comparison $comparison): string | ||
| 32 | { | ||
| 33 | $field = $comparison->getField(); | ||
| 34 | $value = $comparison->getValue()->getValue(); // shortcut for walkValue() | ||
| 35 | |||
| 36 | if ( | ||
| 37 | isset($this->classMetadata->associationMappings[$field]) && | ||
| 38 | $value !== null && | ||
| 39 | ! is_object($value) && | ||
| 40 | ! in_array($comparison->getOperator(), [Comparison::IN, Comparison::NIN], true) | ||
| 41 | ) { | ||
| 42 | throw MatchingAssociationFieldRequiresObject::fromClassAndAssociation( | ||
| 43 | $this->classMetadata->name, | ||
| 44 | $field, | ||
| 45 | ); | ||
| 46 | } | ||
| 47 | |||
| 48 | return $this->persister->getSelectConditionStatementSQL($field, $value, null, $comparison->getOperator()); | ||
| 49 | } | ||
| 50 | |||
| 51 | /** | ||
| 52 | * Converts a composite expression into the target query language output. | ||
| 53 | * | ||
| 54 | * @throws RuntimeException | ||
| 55 | */ | ||
| 56 | public function walkCompositeExpression(CompositeExpression $expr): string | ||
| 57 | { | ||
| 58 | $expressionList = []; | ||
| 59 | |||
| 60 | foreach ($expr->getExpressionList() as $child) { | ||
| 61 | $expressionList[] = $this->dispatch($child); | ||
| 62 | } | ||
| 63 | |||
| 64 | return match ($expr->getType()) { | ||
| 65 | CompositeExpression::TYPE_AND => '(' . implode(' AND ', $expressionList) . ')', | ||
| 66 | CompositeExpression::TYPE_OR => '(' . implode(' OR ', $expressionList) . ')', | ||
| 67 | CompositeExpression::TYPE_NOT => 'NOT (' . $expressionList[0] . ')', | ||
| 68 | default => throw new RuntimeException('Unknown composite ' . $expr->getType()), | ||
| 69 | }; | ||
| 70 | } | ||
| 71 | |||
| 72 | /** | ||
| 73 | * Converts a value expression into the target query language part. | ||
| 74 | */ | ||
| 75 | public function walkValue(Value $value): string | ||
| 76 | { | ||
| 77 | return '?'; | ||
| 78 | } | ||
| 79 | } | ||
diff --git a/vendor/doctrine/orm/src/Persisters/SqlValueVisitor.php b/vendor/doctrine/orm/src/Persisters/SqlValueVisitor.php new file mode 100644 index 0000000..7f987ad --- /dev/null +++ b/vendor/doctrine/orm/src/Persisters/SqlValueVisitor.php | |||
| @@ -0,0 +1,88 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Persisters; | ||
| 6 | |||
| 7 | use Doctrine\Common\Collections\Expr\Comparison; | ||
| 8 | use Doctrine\Common\Collections\Expr\CompositeExpression; | ||
| 9 | use Doctrine\Common\Collections\Expr\ExpressionVisitor; | ||
| 10 | use Doctrine\Common\Collections\Expr\Value; | ||
| 11 | |||
| 12 | /** | ||
| 13 | * Extract the values from a criteria/expression | ||
| 14 | */ | ||
| 15 | class SqlValueVisitor extends ExpressionVisitor | ||
| 16 | { | ||
| 17 | /** @var mixed[] */ | ||
| 18 | private array $values = []; | ||
| 19 | |||
| 20 | /** @var mixed[][] */ | ||
| 21 | private array $types = []; | ||
| 22 | |||
| 23 | /** | ||
| 24 | * Converts a comparison expression into the target query language output. | ||
| 25 | * | ||
| 26 | * {@inheritDoc} | ||
| 27 | */ | ||
| 28 | public function walkComparison(Comparison $comparison) | ||
| 29 | { | ||
| 30 | $value = $this->getValueFromComparison($comparison); | ||
| 31 | |||
| 32 | $this->values[] = $value; | ||
| 33 | $this->types[] = [$comparison->getField(), $value, $comparison->getOperator()]; | ||
| 34 | |||
| 35 | return null; | ||
| 36 | } | ||
| 37 | |||
| 38 | /** | ||
| 39 | * Converts a composite expression into the target query language output. | ||
| 40 | * | ||
| 41 | * {@inheritDoc} | ||
| 42 | */ | ||
| 43 | public function walkCompositeExpression(CompositeExpression $expr) | ||
| 44 | { | ||
| 45 | foreach ($expr->getExpressionList() as $child) { | ||
| 46 | $this->dispatch($child); | ||
| 47 | } | ||
| 48 | |||
| 49 | return null; | ||
| 50 | } | ||
| 51 | |||
| 52 | /** | ||
| 53 | * Converts a value expression into the target query language part. | ||
| 54 | * | ||
| 55 | * {@inheritDoc} | ||
| 56 | */ | ||
| 57 | public function walkValue(Value $value) | ||
| 58 | { | ||
| 59 | return null; | ||
| 60 | } | ||
| 61 | |||
| 62 | /** | ||
| 63 | * Returns the Parameters and Types necessary for matching the last visited expression. | ||
| 64 | * | ||
| 65 | * @return mixed[][] | ||
| 66 | * @psalm-return array{0: array, 1: array<array<mixed>>} | ||
| 67 | */ | ||
| 68 | public function getParamsAndTypes(): array | ||
| 69 | { | ||
| 70 | return [$this->values, $this->types]; | ||
| 71 | } | ||
| 72 | |||
| 73 | /** | ||
| 74 | * Returns the value from a Comparison. In case of a CONTAINS comparison, | ||
| 75 | * the value is wrapped in %-signs, because it will be used in a LIKE clause. | ||
| 76 | */ | ||
| 77 | protected function getValueFromComparison(Comparison $comparison): mixed | ||
| 78 | { | ||
| 79 | $value = $comparison->getValue()->getValue(); | ||
| 80 | |||
| 81 | return match ($comparison->getOperator()) { | ||
| 82 | Comparison::CONTAINS => '%' . $value . '%', | ||
| 83 | Comparison::STARTS_WITH => $value . '%', | ||
| 84 | Comparison::ENDS_WITH => '%' . $value, | ||
| 85 | default => $value, | ||
| 86 | }; | ||
| 87 | } | ||
| 88 | } | ||
diff --git a/vendor/doctrine/orm/src/PessimisticLockException.php b/vendor/doctrine/orm/src/PessimisticLockException.php new file mode 100644 index 0000000..c71560f --- /dev/null +++ b/vendor/doctrine/orm/src/PessimisticLockException.php | |||
| @@ -0,0 +1,16 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM; | ||
| 6 | |||
| 7 | use Doctrine\ORM\Exception\ORMException; | ||
| 8 | use RuntimeException; | ||
| 9 | |||
| 10 | class PessimisticLockException extends RuntimeException implements ORMException | ||
| 11 | { | ||
| 12 | public static function lockFailed(): self | ||
| 13 | { | ||
| 14 | return new self('The pessimistic lock failed.'); | ||
| 15 | } | ||
| 16 | } | ||
diff --git a/vendor/doctrine/orm/src/Proxy/Autoloader.php b/vendor/doctrine/orm/src/Proxy/Autoloader.php new file mode 100644 index 0000000..1013e73 --- /dev/null +++ b/vendor/doctrine/orm/src/Proxy/Autoloader.php | |||
| @@ -0,0 +1,86 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Proxy; | ||
| 6 | |||
| 7 | use Closure; | ||
| 8 | |||
| 9 | use function file_exists; | ||
| 10 | use function ltrim; | ||
| 11 | use function spl_autoload_register; | ||
| 12 | use function str_replace; | ||
| 13 | use function str_starts_with; | ||
| 14 | use function strlen; | ||
| 15 | use function substr; | ||
| 16 | |||
| 17 | use const DIRECTORY_SEPARATOR; | ||
| 18 | |||
| 19 | /** | ||
| 20 | * Special Autoloader for Proxy classes, which are not PSR-0 compliant. | ||
| 21 | */ | ||
| 22 | final class Autoloader | ||
| 23 | { | ||
| 24 | /** | ||
| 25 | * Resolves proxy class name to a filename based on the following pattern. | ||
| 26 | * | ||
| 27 | * 1. Remove Proxy namespace from class name. | ||
| 28 | * 2. Remove namespace separators from remaining class name. | ||
| 29 | * 3. Return PHP filename from proxy-dir with the result from 2. | ||
| 30 | * | ||
| 31 | * @psalm-param class-string $className | ||
| 32 | * | ||
| 33 | * @throws NotAProxyClass | ||
| 34 | */ | ||
| 35 | public static function resolveFile(string $proxyDir, string $proxyNamespace, string $className): string | ||
| 36 | { | ||
| 37 | if (! str_starts_with($className, $proxyNamespace)) { | ||
| 38 | throw new NotAProxyClass($className, $proxyNamespace); | ||
| 39 | } | ||
| 40 | |||
| 41 | // remove proxy namespace from class name | ||
| 42 | $classNameRelativeToProxyNamespace = substr($className, strlen($proxyNamespace)); | ||
| 43 | |||
| 44 | // remove namespace separators from remaining class name | ||
| 45 | $fileName = str_replace('\\', '', $classNameRelativeToProxyNamespace); | ||
| 46 | |||
| 47 | return $proxyDir . DIRECTORY_SEPARATOR . $fileName . '.php'; | ||
| 48 | } | ||
| 49 | |||
| 50 | /** | ||
| 51 | * Registers and returns autoloader callback for the given proxy dir and namespace. | ||
| 52 | * | ||
| 53 | * @param Closure(string, string, class-string): void|null $notFoundCallback Invoked when the proxy file is not found. | ||
| 54 | * | ||
| 55 | * @return Closure(string): void | ||
| 56 | */ | ||
| 57 | public static function register( | ||
| 58 | string $proxyDir, | ||
| 59 | string $proxyNamespace, | ||
| 60 | Closure|null $notFoundCallback = null, | ||
| 61 | ): Closure { | ||
| 62 | $proxyNamespace = ltrim($proxyNamespace, '\\'); | ||
| 63 | |||
| 64 | $autoloader = /** @param class-string $className */ static function (string $className) use ($proxyDir, $proxyNamespace, $notFoundCallback): void { | ||
| 65 | if ($proxyNamespace === '') { | ||
| 66 | return; | ||
| 67 | } | ||
| 68 | |||
| 69 | if (! str_starts_with($className, $proxyNamespace)) { | ||
| 70 | return; | ||
| 71 | } | ||
| 72 | |||
| 73 | $file = Autoloader::resolveFile($proxyDir, $proxyNamespace, $className); | ||
| 74 | |||
| 75 | if ($notFoundCallback && ! file_exists($file)) { | ||
| 76 | $notFoundCallback($proxyDir, $proxyNamespace, $className); | ||
| 77 | } | ||
| 78 | |||
| 79 | require $file; | ||
| 80 | }; | ||
| 81 | |||
| 82 | spl_autoload_register($autoloader); | ||
| 83 | |||
| 84 | return $autoloader; | ||
| 85 | } | ||
| 86 | } | ||
diff --git a/vendor/doctrine/orm/src/Proxy/DefaultProxyClassNameResolver.php b/vendor/doctrine/orm/src/Proxy/DefaultProxyClassNameResolver.php new file mode 100644 index 0000000..1345f2e --- /dev/null +++ b/vendor/doctrine/orm/src/Proxy/DefaultProxyClassNameResolver.php | |||
| @@ -0,0 +1,35 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Proxy; | ||
| 6 | |||
| 7 | use Doctrine\Persistence\Mapping\ProxyClassNameResolver; | ||
| 8 | use Doctrine\Persistence\Proxy; | ||
| 9 | |||
| 10 | use function strrpos; | ||
| 11 | use function substr; | ||
| 12 | |||
| 13 | /** | ||
| 14 | * Class-related functionality for objects that might or not be proxy objects | ||
| 15 | * at the moment. | ||
| 16 | */ | ||
| 17 | final class DefaultProxyClassNameResolver implements ProxyClassNameResolver | ||
| 18 | { | ||
| 19 | public function resolveClassName(string $className): string | ||
| 20 | { | ||
| 21 | $pos = strrpos($className, '\\' . Proxy::MARKER . '\\'); | ||
| 22 | |||
| 23 | if ($pos === false) { | ||
| 24 | return $className; | ||
| 25 | } | ||
| 26 | |||
| 27 | return substr($className, $pos + Proxy::MARKER_LENGTH + 2); | ||
| 28 | } | ||
| 29 | |||
| 30 | /** @return class-string */ | ||
| 31 | public static function getClass(object $object): string | ||
| 32 | { | ||
| 33 | return (new self())->resolveClassName($object::class); | ||
| 34 | } | ||
| 35 | } | ||
diff --git a/vendor/doctrine/orm/src/Proxy/InternalProxy.php b/vendor/doctrine/orm/src/Proxy/InternalProxy.php new file mode 100644 index 0000000..7c1d833 --- /dev/null +++ b/vendor/doctrine/orm/src/Proxy/InternalProxy.php | |||
| @@ -0,0 +1,18 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Proxy; | ||
| 6 | |||
| 7 | use Doctrine\Persistence\Proxy; | ||
| 8 | |||
| 9 | /** | ||
| 10 | * @internal | ||
| 11 | * | ||
| 12 | * @template T of object | ||
| 13 | * @template-extends Proxy<T> | ||
| 14 | */ | ||
| 15 | interface InternalProxy extends Proxy | ||
| 16 | { | ||
| 17 | public function __setInitialized(bool $initialized): void; | ||
| 18 | } | ||
diff --git a/vendor/doctrine/orm/src/Proxy/NotAProxyClass.php b/vendor/doctrine/orm/src/Proxy/NotAProxyClass.php new file mode 100644 index 0000000..689cc3e --- /dev/null +++ b/vendor/doctrine/orm/src/Proxy/NotAProxyClass.php | |||
| @@ -0,0 +1,22 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Proxy; | ||
| 6 | |||
| 7 | use Doctrine\ORM\Exception\ORMException; | ||
| 8 | use InvalidArgumentException; | ||
| 9 | |||
| 10 | use function sprintf; | ||
| 11 | |||
| 12 | final class NotAProxyClass extends InvalidArgumentException implements ORMException | ||
| 13 | { | ||
| 14 | public function __construct(string $className, string $proxyNamespace) | ||
| 15 | { | ||
| 16 | parent::__construct(sprintf( | ||
| 17 | 'The class "%s" is not part of the proxy namespace "%s"', | ||
| 18 | $className, | ||
| 19 | $proxyNamespace, | ||
| 20 | )); | ||
| 21 | } | ||
| 22 | } | ||
diff --git a/vendor/doctrine/orm/src/Proxy/ProxyFactory.php b/vendor/doctrine/orm/src/Proxy/ProxyFactory.php new file mode 100644 index 0000000..b2d114a --- /dev/null +++ b/vendor/doctrine/orm/src/Proxy/ProxyFactory.php | |||
| @@ -0,0 +1,439 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Proxy; | ||
| 6 | |||
| 7 | use Closure; | ||
| 8 | use Doctrine\ORM\EntityManagerInterface; | ||
| 9 | use Doctrine\ORM\EntityNotFoundException; | ||
| 10 | use Doctrine\ORM\ORMInvalidArgumentException; | ||
| 11 | use Doctrine\ORM\Persisters\Entity\EntityPersister; | ||
| 12 | use Doctrine\ORM\UnitOfWork; | ||
| 13 | use Doctrine\ORM\Utility\IdentifierFlattener; | ||
| 14 | use Doctrine\Persistence\Mapping\ClassMetadata; | ||
| 15 | use Doctrine\Persistence\Proxy; | ||
| 16 | use ReflectionProperty; | ||
| 17 | use Symfony\Component\VarExporter\ProxyHelper; | ||
| 18 | |||
| 19 | use function array_combine; | ||
| 20 | use function array_flip; | ||
| 21 | use function array_intersect_key; | ||
| 22 | use function assert; | ||
| 23 | use function bin2hex; | ||
| 24 | use function chmod; | ||
| 25 | use function class_exists; | ||
| 26 | use function dirname; | ||
| 27 | use function file_exists; | ||
| 28 | use function file_put_contents; | ||
| 29 | use function filemtime; | ||
| 30 | use function is_bool; | ||
| 31 | use function is_dir; | ||
| 32 | use function is_int; | ||
| 33 | use function is_writable; | ||
| 34 | use function ltrim; | ||
| 35 | use function mkdir; | ||
| 36 | use function preg_match_all; | ||
| 37 | use function random_bytes; | ||
| 38 | use function rename; | ||
| 39 | use function rtrim; | ||
| 40 | use function str_replace; | ||
| 41 | use function strpos; | ||
| 42 | use function strrpos; | ||
| 43 | use function strtr; | ||
| 44 | use function substr; | ||
| 45 | use function ucfirst; | ||
| 46 | |||
| 47 | use const DIRECTORY_SEPARATOR; | ||
| 48 | |||
| 49 | /** | ||
| 50 | * This factory is used to create proxy objects for entities at runtime. | ||
| 51 | */ | ||
| 52 | class ProxyFactory | ||
| 53 | { | ||
| 54 | /** | ||
| 55 | * Never autogenerate a proxy and rely that it was generated by some | ||
| 56 | * process before deployment. | ||
| 57 | */ | ||
| 58 | public const AUTOGENERATE_NEVER = 0; | ||
| 59 | |||
| 60 | /** | ||
| 61 | * Always generates a new proxy in every request. | ||
| 62 | * | ||
| 63 | * This is only sane during development. | ||
| 64 | */ | ||
| 65 | public const AUTOGENERATE_ALWAYS = 1; | ||
| 66 | |||
| 67 | /** | ||
| 68 | * Autogenerate the proxy class when the proxy file does not exist. | ||
| 69 | * | ||
| 70 | * This strategy causes a file_exists() call whenever any proxy is used the | ||
| 71 | * first time in a request. | ||
| 72 | */ | ||
| 73 | public const AUTOGENERATE_FILE_NOT_EXISTS = 2; | ||
| 74 | |||
| 75 | /** | ||
| 76 | * Generate the proxy classes using eval(). | ||
| 77 | * | ||
| 78 | * This strategy is only sane for development, and even then it gives me | ||
| 79 | * the creeps a little. | ||
| 80 | */ | ||
| 81 | public const AUTOGENERATE_EVAL = 3; | ||
| 82 | |||
| 83 | /** | ||
| 84 | * Autogenerate the proxy class when the proxy file does not exist or | ||
| 85 | * when the proxied file changed. | ||
| 86 | * | ||
| 87 | * This strategy causes a file_exists() call whenever any proxy is used the | ||
| 88 | * first time in a request. When the proxied file is changed, the proxy will | ||
| 89 | * be updated. | ||
| 90 | */ | ||
| 91 | public const AUTOGENERATE_FILE_NOT_EXISTS_OR_CHANGED = 4; | ||
| 92 | |||
| 93 | private const PROXY_CLASS_TEMPLATE = <<<'EOPHP' | ||
| 94 | <?php | ||
| 95 | |||
| 96 | namespace <namespace>; | ||
| 97 | |||
| 98 | /** | ||
| 99 | * DO NOT EDIT THIS FILE - IT WAS CREATED BY DOCTRINE'S PROXY GENERATOR | ||
| 100 | */ | ||
| 101 | class <proxyShortClassName> extends \<className> implements \<baseProxyInterface> | ||
| 102 | { | ||
| 103 | <useLazyGhostTrait> | ||
| 104 | |||
| 105 | public function __isInitialized(): bool | ||
| 106 | { | ||
| 107 | return isset($this->lazyObjectState) && $this->isLazyObjectInitialized(); | ||
| 108 | } | ||
| 109 | |||
| 110 | public function __serialize(): array | ||
| 111 | { | ||
| 112 | <serializeImpl> | ||
| 113 | } | ||
| 114 | } | ||
| 115 | |||
| 116 | EOPHP; | ||
| 117 | |||
| 118 | /** The UnitOfWork this factory uses to retrieve persisters */ | ||
| 119 | private readonly UnitOfWork $uow; | ||
| 120 | |||
| 121 | /** @var self::AUTOGENERATE_* */ | ||
| 122 | private $autoGenerate; | ||
| 123 | |||
| 124 | /** The IdentifierFlattener used for manipulating identifiers */ | ||
| 125 | private readonly IdentifierFlattener $identifierFlattener; | ||
| 126 | |||
| 127 | /** @var array<class-string, Closure> */ | ||
| 128 | private array $proxyFactories = []; | ||
| 129 | |||
| 130 | /** | ||
| 131 | * Initializes a new instance of the <tt>ProxyFactory</tt> class that is | ||
| 132 | * connected to the given <tt>EntityManager</tt>. | ||
| 133 | * | ||
| 134 | * @param EntityManagerInterface $em The EntityManager the new factory works for. | ||
| 135 | * @param string $proxyDir The directory to use for the proxy classes. It must exist. | ||
| 136 | * @param string $proxyNs The namespace to use for the proxy classes. | ||
| 137 | * @param bool|self::AUTOGENERATE_* $autoGenerate The strategy for automatically generating proxy classes. | ||
| 138 | */ | ||
| 139 | public function __construct( | ||
| 140 | private readonly EntityManagerInterface $em, | ||
| 141 | private readonly string $proxyDir, | ||
| 142 | private readonly string $proxyNs, | ||
| 143 | bool|int $autoGenerate = self::AUTOGENERATE_NEVER, | ||
| 144 | ) { | ||
| 145 | if (! $proxyDir) { | ||
| 146 | throw ORMInvalidArgumentException::proxyDirectoryRequired(); | ||
| 147 | } | ||
| 148 | |||
| 149 | if (! $proxyNs) { | ||
| 150 | throw ORMInvalidArgumentException::proxyNamespaceRequired(); | ||
| 151 | } | ||
| 152 | |||
| 153 | if (is_int($autoGenerate) ? $autoGenerate < 0 || $autoGenerate > 4 : ! is_bool($autoGenerate)) { | ||
| 154 | throw ORMInvalidArgumentException::invalidAutoGenerateMode($autoGenerate); | ||
| 155 | } | ||
| 156 | |||
| 157 | $this->uow = $em->getUnitOfWork(); | ||
| 158 | $this->autoGenerate = (int) $autoGenerate; | ||
| 159 | $this->identifierFlattener = new IdentifierFlattener($this->uow, $em->getMetadataFactory()); | ||
| 160 | } | ||
| 161 | |||
| 162 | /** | ||
| 163 | * @param class-string $className | ||
| 164 | * @param array<mixed> $identifier | ||
| 165 | */ | ||
| 166 | public function getProxy(string $className, array $identifier): InternalProxy | ||
| 167 | { | ||
| 168 | $proxyFactory = $this->proxyFactories[$className] ?? $this->getProxyFactory($className); | ||
| 169 | |||
| 170 | return $proxyFactory($identifier); | ||
| 171 | } | ||
| 172 | |||
| 173 | /** | ||
| 174 | * Generates proxy classes for all given classes. | ||
| 175 | * | ||
| 176 | * @param ClassMetadata[] $classes The classes (ClassMetadata instances) for which to generate proxies. | ||
| 177 | * @param string|null $proxyDir The target directory of the proxy classes. If not specified, the | ||
| 178 | * directory configured on the Configuration of the EntityManager used | ||
| 179 | * by this factory is used. | ||
| 180 | * | ||
| 181 | * @return int Number of generated proxies. | ||
| 182 | */ | ||
| 183 | public function generateProxyClasses(array $classes, string|null $proxyDir = null): int | ||
| 184 | { | ||
| 185 | $generated = 0; | ||
| 186 | |||
| 187 | foreach ($classes as $class) { | ||
| 188 | if ($this->skipClass($class)) { | ||
| 189 | continue; | ||
| 190 | } | ||
| 191 | |||
| 192 | $proxyFileName = $this->getProxyFileName($class->getName(), $proxyDir ?: $this->proxyDir); | ||
| 193 | $proxyClassName = self::generateProxyClassName($class->getName(), $this->proxyNs); | ||
| 194 | |||
| 195 | $this->generateProxyClass($class, $proxyFileName, $proxyClassName); | ||
| 196 | |||
| 197 | ++$generated; | ||
| 198 | } | ||
| 199 | |||
| 200 | return $generated; | ||
| 201 | } | ||
| 202 | |||
| 203 | protected function skipClass(ClassMetadata $metadata): bool | ||
| 204 | { | ||
| 205 | return $metadata->isMappedSuperclass | ||
| 206 | || $metadata->isEmbeddedClass | ||
| 207 | || $metadata->getReflectionClass()->isAbstract(); | ||
| 208 | } | ||
| 209 | |||
| 210 | /** | ||
| 211 | * Creates a closure capable of initializing a proxy | ||
| 212 | * | ||
| 213 | * @return Closure(InternalProxy, array):void | ||
| 214 | * | ||
| 215 | * @throws EntityNotFoundException | ||
| 216 | */ | ||
| 217 | private function createLazyInitializer(ClassMetadata $classMetadata, EntityPersister $entityPersister, IdentifierFlattener $identifierFlattener): Closure | ||
| 218 | { | ||
| 219 | return static function (InternalProxy $proxy, array $identifier) use ($entityPersister, $classMetadata, $identifierFlattener): void { | ||
| 220 | $original = $entityPersister->loadById($identifier); | ||
| 221 | |||
| 222 | if ($original === null) { | ||
| 223 | throw EntityNotFoundException::fromClassNameAndIdentifier( | ||
| 224 | $classMetadata->getName(), | ||
| 225 | $identifierFlattener->flattenIdentifier($classMetadata, $identifier), | ||
| 226 | ); | ||
| 227 | } | ||
| 228 | |||
| 229 | if ($proxy === $original) { | ||
| 230 | return; | ||
| 231 | } | ||
| 232 | |||
| 233 | $class = $entityPersister->getClassMetadata(); | ||
| 234 | |||
| 235 | foreach ($class->getReflectionProperties() as $property) { | ||
| 236 | if (! $property || isset($identifier[$property->getName()]) || ! $class->hasField($property->getName()) && ! $class->hasAssociation($property->getName())) { | ||
| 237 | continue; | ||
| 238 | } | ||
| 239 | |||
| 240 | $property->setValue($proxy, $property->getValue($original)); | ||
| 241 | } | ||
| 242 | }; | ||
| 243 | } | ||
| 244 | |||
| 245 | private function getProxyFileName(string $className, string $baseDirectory): string | ||
| 246 | { | ||
| 247 | $baseDirectory = $baseDirectory ?: $this->proxyDir; | ||
| 248 | |||
| 249 | return rtrim($baseDirectory, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . InternalProxy::MARKER | ||
| 250 | . str_replace('\\', '', $className) . '.php'; | ||
| 251 | } | ||
| 252 | |||
| 253 | private function getProxyFactory(string $className): Closure | ||
| 254 | { | ||
| 255 | $skippedProperties = []; | ||
| 256 | $class = $this->em->getClassMetadata($className); | ||
| 257 | $identifiers = array_flip($class->getIdentifierFieldNames()); | ||
| 258 | $filter = ReflectionProperty::IS_PUBLIC | ReflectionProperty::IS_PROTECTED | ReflectionProperty::IS_PRIVATE; | ||
| 259 | $reflector = $class->getReflectionClass(); | ||
| 260 | |||
| 261 | while ($reflector) { | ||
| 262 | foreach ($reflector->getProperties($filter) as $property) { | ||
| 263 | $name = $property->name; | ||
| 264 | |||
| 265 | if ($property->isStatic() || (($class->hasField($name) || $class->hasAssociation($name)) && ! isset($identifiers[$name]))) { | ||
| 266 | continue; | ||
| 267 | } | ||
| 268 | |||
| 269 | $prefix = $property->isPrivate() ? "\0" . $property->class . "\0" : ($property->isProtected() ? "\0*\0" : ''); | ||
| 270 | |||
| 271 | $skippedProperties[$prefix . $name] = true; | ||
| 272 | } | ||
| 273 | |||
| 274 | $filter = ReflectionProperty::IS_PRIVATE; | ||
| 275 | $reflector = $reflector->getParentClass(); | ||
| 276 | } | ||
| 277 | |||
| 278 | $className = $class->getName(); // aliases and case sensitivity | ||
| 279 | $entityPersister = $this->uow->getEntityPersister($className); | ||
| 280 | $initializer = $this->createLazyInitializer($class, $entityPersister, $this->identifierFlattener); | ||
| 281 | $proxyClassName = $this->loadProxyClass($class); | ||
| 282 | $identifierFields = array_intersect_key($class->getReflectionProperties(), $identifiers); | ||
| 283 | |||
| 284 | $proxyFactory = Closure::bind(static function (array $identifier) use ($initializer, $skippedProperties, $identifierFields, $className): InternalProxy { | ||
| 285 | $proxy = self::createLazyGhost(static function (InternalProxy $object) use ($initializer, $identifier): void { | ||
| 286 | $initializer($object, $identifier); | ||
| 287 | }, $skippedProperties); | ||
| 288 | |||
| 289 | foreach ($identifierFields as $idField => $reflector) { | ||
| 290 | if (! isset($identifier[$idField])) { | ||
| 291 | throw ORMInvalidArgumentException::missingPrimaryKeyValue($className, $idField); | ||
| 292 | } | ||
| 293 | |||
| 294 | assert($reflector !== null); | ||
| 295 | $reflector->setValue($proxy, $identifier[$idField]); | ||
| 296 | } | ||
| 297 | |||
| 298 | return $proxy; | ||
| 299 | }, null, $proxyClassName); | ||
| 300 | |||
| 301 | return $this->proxyFactories[$className] = $proxyFactory; | ||
| 302 | } | ||
| 303 | |||
| 304 | private function loadProxyClass(ClassMetadata $class): string | ||
| 305 | { | ||
| 306 | $proxyClassName = self::generateProxyClassName($class->getName(), $this->proxyNs); | ||
| 307 | |||
| 308 | if (class_exists($proxyClassName, false)) { | ||
| 309 | return $proxyClassName; | ||
| 310 | } | ||
| 311 | |||
| 312 | if ($this->autoGenerate === self::AUTOGENERATE_EVAL) { | ||
| 313 | $this->generateProxyClass($class, null, $proxyClassName); | ||
| 314 | |||
| 315 | return $proxyClassName; | ||
| 316 | } | ||
| 317 | |||
| 318 | $fileName = $this->getProxyFileName($class->getName(), $this->proxyDir); | ||
| 319 | |||
| 320 | switch ($this->autoGenerate) { | ||
| 321 | case self::AUTOGENERATE_FILE_NOT_EXISTS_OR_CHANGED: | ||
| 322 | if (file_exists($fileName) && filemtime($fileName) >= filemtime($class->getReflectionClass()->getFileName())) { | ||
| 323 | break; | ||
| 324 | } | ||
| 325 | // no break | ||
| 326 | case self::AUTOGENERATE_FILE_NOT_EXISTS: | ||
| 327 | if (file_exists($fileName)) { | ||
| 328 | break; | ||
| 329 | } | ||
| 330 | // no break | ||
| 331 | case self::AUTOGENERATE_ALWAYS: | ||
| 332 | $this->generateProxyClass($class, $fileName, $proxyClassName); | ||
| 333 | break; | ||
| 334 | } | ||
| 335 | |||
| 336 | require $fileName; | ||
| 337 | |||
| 338 | return $proxyClassName; | ||
| 339 | } | ||
| 340 | |||
| 341 | private function generateProxyClass(ClassMetadata $class, string|null $fileName, string $proxyClassName): void | ||
| 342 | { | ||
| 343 | $i = strrpos($proxyClassName, '\\'); | ||
| 344 | $placeholders = [ | ||
| 345 | '<className>' => $class->getName(), | ||
| 346 | '<namespace>' => substr($proxyClassName, 0, $i), | ||
| 347 | '<proxyShortClassName>' => substr($proxyClassName, 1 + $i), | ||
| 348 | '<baseProxyInterface>' => InternalProxy::class, | ||
| 349 | ]; | ||
| 350 | |||
| 351 | preg_match_all('(<([a-zA-Z]+)>)', self::PROXY_CLASS_TEMPLATE, $placeholderMatches); | ||
| 352 | |||
| 353 | foreach (array_combine($placeholderMatches[0], $placeholderMatches[1]) as $placeholder => $name) { | ||
| 354 | $placeholders[$placeholder] ?? $placeholders[$placeholder] = $this->{'generate' . ucfirst($name)}($class); | ||
| 355 | } | ||
| 356 | |||
| 357 | $proxyCode = strtr(self::PROXY_CLASS_TEMPLATE, $placeholders); | ||
| 358 | |||
| 359 | if (! $fileName) { | ||
| 360 | if (! class_exists($proxyClassName)) { | ||
| 361 | eval(substr($proxyCode, 5)); | ||
| 362 | } | ||
| 363 | |||
| 364 | return; | ||
| 365 | } | ||
| 366 | |||
| 367 | $parentDirectory = dirname($fileName); | ||
| 368 | |||
| 369 | if (! is_dir($parentDirectory) && ! @mkdir($parentDirectory, 0775, true)) { | ||
| 370 | throw ORMInvalidArgumentException::proxyDirectoryNotWritable($this->proxyDir); | ||
| 371 | } | ||
| 372 | |||
| 373 | if (! is_writable($parentDirectory)) { | ||
| 374 | throw ORMInvalidArgumentException::proxyDirectoryNotWritable($this->proxyDir); | ||
| 375 | } | ||
| 376 | |||
| 377 | $tmpFileName = $fileName . '.' . bin2hex(random_bytes(12)); | ||
| 378 | |||
| 379 | file_put_contents($tmpFileName, $proxyCode); | ||
| 380 | @chmod($tmpFileName, 0664); | ||
| 381 | rename($tmpFileName, $fileName); | ||
| 382 | } | ||
| 383 | |||
| 384 | private function generateUseLazyGhostTrait(ClassMetadata $class): string | ||
| 385 | { | ||
| 386 | $code = ProxyHelper::generateLazyGhost($class->getReflectionClass()); | ||
| 387 | $code = substr($code, 7 + (int) strpos($code, "\n{")); | ||
| 388 | $code = substr($code, 0, (int) strpos($code, "\n}")); | ||
| 389 | $code = str_replace('LazyGhostTrait;', str_replace("\n ", "\n", 'LazyGhostTrait { | ||
| 390 | initializeLazyObject as private; | ||
| 391 | setLazyObjectAsInitialized as public __setInitialized; | ||
| 392 | isLazyObjectInitialized as private; | ||
| 393 | createLazyGhost as private; | ||
| 394 | resetLazyObject as private; | ||
| 395 | } | ||
| 396 | |||
| 397 | public function __load(): void | ||
| 398 | { | ||
| 399 | $this->initializeLazyObject(); | ||
| 400 | } | ||
| 401 | '), $code); | ||
| 402 | |||
| 403 | return $code; | ||
| 404 | } | ||
| 405 | |||
| 406 | private function generateSerializeImpl(ClassMetadata $class): string | ||
| 407 | { | ||
| 408 | $reflector = $class->getReflectionClass(); | ||
| 409 | $properties = $reflector->hasMethod('__serialize') ? 'parent::__serialize()' : '(array) $this'; | ||
| 410 | |||
| 411 | $code = '$properties = ' . $properties . '; | ||
| 412 | unset($properties["\0" . self::class . "\0lazyObjectState"]); | ||
| 413 | |||
| 414 | '; | ||
| 415 | |||
| 416 | if ($reflector->hasMethod('__serialize') || ! $reflector->hasMethod('__sleep')) { | ||
| 417 | return $code . 'return $properties;'; | ||
| 418 | } | ||
| 419 | |||
| 420 | return $code . '$data = []; | ||
| 421 | |||
| 422 | foreach (parent::__sleep() as $name) { | ||
| 423 | $value = $properties[$k = $name] ?? $properties[$k = "\0*\0$name"] ?? $properties[$k = "\0' . $reflector->name . '\0$name"] ?? $k = null; | ||
| 424 | |||
| 425 | if (null === $k) { | ||
| 426 | trigger_error(sprintf(\'serialize(): "%s" returned as member variable from __sleep() but does not exist\', $name), \E_USER_NOTICE); | ||
| 427 | } else { | ||
| 428 | $data[$k] = $value; | ||
| 429 | } | ||
| 430 | } | ||
| 431 | |||
| 432 | return $data;'; | ||
| 433 | } | ||
| 434 | |||
| 435 | private static function generateProxyClassName(string $className, string $proxyNamespace): string | ||
| 436 | { | ||
| 437 | return rtrim($proxyNamespace, '\\') . '\\' . Proxy::MARKER . '\\' . ltrim($className, '\\'); | ||
| 438 | } | ||
| 439 | } | ||
diff --git a/vendor/doctrine/orm/src/Query.php b/vendor/doctrine/orm/src/Query.php new file mode 100644 index 0000000..a869316 --- /dev/null +++ b/vendor/doctrine/orm/src/Query.php | |||
| @@ -0,0 +1,682 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM; | ||
| 6 | |||
| 7 | use Doctrine\DBAL\LockMode; | ||
| 8 | use Doctrine\DBAL\Result; | ||
| 9 | use Doctrine\DBAL\Types\Type; | ||
| 10 | use Doctrine\ORM\Mapping\ClassMetadata; | ||
| 11 | use Doctrine\ORM\Query\AST\DeleteStatement; | ||
| 12 | use Doctrine\ORM\Query\AST\SelectStatement; | ||
| 13 | use Doctrine\ORM\Query\AST\UpdateStatement; | ||
| 14 | use Doctrine\ORM\Query\Exec\AbstractSqlExecutor; | ||
| 15 | use Doctrine\ORM\Query\Parameter; | ||
| 16 | use Doctrine\ORM\Query\ParameterTypeInferer; | ||
| 17 | use Doctrine\ORM\Query\Parser; | ||
| 18 | use Doctrine\ORM\Query\ParserResult; | ||
| 19 | use Doctrine\ORM\Query\QueryException; | ||
| 20 | use Doctrine\ORM\Query\ResultSetMapping; | ||
| 21 | use Doctrine\ORM\Utility\HierarchyDiscriminatorResolver; | ||
| 22 | use Psr\Cache\CacheItemPoolInterface; | ||
| 23 | |||
| 24 | use function array_keys; | ||
| 25 | use function array_values; | ||
| 26 | use function assert; | ||
| 27 | use function count; | ||
| 28 | use function get_debug_type; | ||
| 29 | use function in_array; | ||
| 30 | use function ksort; | ||
| 31 | use function md5; | ||
| 32 | use function reset; | ||
| 33 | use function serialize; | ||
| 34 | use function sha1; | ||
| 35 | use function stripos; | ||
| 36 | |||
| 37 | /** | ||
| 38 | * A Query object represents a DQL query. | ||
| 39 | * | ||
| 40 | * @final | ||
| 41 | */ | ||
| 42 | class Query extends AbstractQuery | ||
| 43 | { | ||
| 44 | /** | ||
| 45 | * A query object is in CLEAN state when it has NO unparsed/unprocessed DQL parts. | ||
| 46 | */ | ||
| 47 | public const STATE_CLEAN = 1; | ||
| 48 | |||
| 49 | /** | ||
| 50 | * A query object is in state DIRTY when it has DQL parts that have not yet been | ||
| 51 | * parsed/processed. This is automatically defined as DIRTY when addDqlQueryPart | ||
| 52 | * is called. | ||
| 53 | */ | ||
| 54 | public const STATE_DIRTY = 2; | ||
| 55 | |||
| 56 | /* Query HINTS */ | ||
| 57 | |||
| 58 | /** | ||
| 59 | * The refresh hint turns any query into a refresh query with the result that | ||
| 60 | * any local changes in entities are overridden with the fetched values. | ||
| 61 | */ | ||
| 62 | public const HINT_REFRESH = 'doctrine.refresh'; | ||
| 63 | |||
| 64 | public const HINT_CACHE_ENABLED = 'doctrine.cache.enabled'; | ||
| 65 | |||
| 66 | public const HINT_CACHE_EVICT = 'doctrine.cache.evict'; | ||
| 67 | |||
| 68 | /** | ||
| 69 | * Internal hint: is set to the proxy entity that is currently triggered for loading | ||
| 70 | */ | ||
| 71 | public const HINT_REFRESH_ENTITY = 'doctrine.refresh.entity'; | ||
| 72 | |||
| 73 | /** | ||
| 74 | * The includeMetaColumns query hint causes meta columns like foreign keys and | ||
| 75 | * discriminator columns to be selected and returned as part of the query result. | ||
| 76 | * | ||
| 77 | * This hint does only apply to non-object queries. | ||
| 78 | */ | ||
| 79 | public const HINT_INCLUDE_META_COLUMNS = 'doctrine.includeMetaColumns'; | ||
| 80 | |||
| 81 | /** | ||
| 82 | * An array of class names that implement \Doctrine\ORM\Query\TreeWalker and | ||
| 83 | * are iterated and executed after the DQL has been parsed into an AST. | ||
| 84 | */ | ||
| 85 | public const HINT_CUSTOM_TREE_WALKERS = 'doctrine.customTreeWalkers'; | ||
| 86 | |||
| 87 | /** | ||
| 88 | * A string with a class name that implements \Doctrine\ORM\Query\TreeWalker | ||
| 89 | * and is used for generating the target SQL from any DQL AST tree. | ||
| 90 | */ | ||
| 91 | public const HINT_CUSTOM_OUTPUT_WALKER = 'doctrine.customOutputWalker'; | ||
| 92 | |||
| 93 | /** | ||
| 94 | * Marks queries as creating only read only objects. | ||
| 95 | * | ||
| 96 | * If the object retrieved from the query is already in the identity map | ||
| 97 | * then it does not get marked as read only if it wasn't already. | ||
| 98 | */ | ||
| 99 | public const HINT_READ_ONLY = 'doctrine.readOnly'; | ||
| 100 | |||
| 101 | public const HINT_INTERNAL_ITERATION = 'doctrine.internal.iteration'; | ||
| 102 | |||
| 103 | public const HINT_LOCK_MODE = 'doctrine.lockMode'; | ||
| 104 | |||
| 105 | /** | ||
| 106 | * The current state of this query. | ||
| 107 | * | ||
| 108 | * @psalm-var self::STATE_* | ||
| 109 | */ | ||
| 110 | private int $state = self::STATE_DIRTY; | ||
| 111 | |||
| 112 | /** | ||
| 113 | * A snapshot of the parameter types the query was parsed with. | ||
| 114 | * | ||
| 115 | * @var array<string,Type> | ||
| 116 | */ | ||
| 117 | private array $parsedTypes = []; | ||
| 118 | |||
| 119 | /** | ||
| 120 | * Cached DQL query. | ||
| 121 | */ | ||
| 122 | private string|null $dql = null; | ||
| 123 | |||
| 124 | /** | ||
| 125 | * The parser result that holds DQL => SQL information. | ||
| 126 | */ | ||
| 127 | private ParserResult $parserResult; | ||
| 128 | |||
| 129 | /** | ||
| 130 | * The first result to return (the "offset"). | ||
| 131 | */ | ||
| 132 | private int $firstResult = 0; | ||
| 133 | |||
| 134 | /** | ||
| 135 | * The maximum number of results to return (the "limit"). | ||
| 136 | */ | ||
| 137 | private int|null $maxResults = null; | ||
| 138 | |||
| 139 | /** | ||
| 140 | * The cache driver used for caching queries. | ||
| 141 | */ | ||
| 142 | private CacheItemPoolInterface|null $queryCache = null; | ||
| 143 | |||
| 144 | /** | ||
| 145 | * Whether or not expire the query cache. | ||
| 146 | */ | ||
| 147 | private bool $expireQueryCache = false; | ||
| 148 | |||
| 149 | /** | ||
| 150 | * The query cache lifetime. | ||
| 151 | */ | ||
| 152 | private int|null $queryCacheTTL = null; | ||
| 153 | |||
| 154 | /** | ||
| 155 | * Whether to use a query cache, if available. Defaults to TRUE. | ||
| 156 | */ | ||
| 157 | private bool $useQueryCache = true; | ||
| 158 | |||
| 159 | /** | ||
| 160 | * Gets the SQL query/queries that correspond to this DQL query. | ||
| 161 | * | ||
| 162 | * @return list<string>|string The built sql query or an array of all sql queries. | ||
| 163 | */ | ||
| 164 | public function getSQL(): string|array | ||
| 165 | { | ||
| 166 | return $this->parse()->getSqlExecutor()->getSqlStatements(); | ||
| 167 | } | ||
| 168 | |||
| 169 | /** | ||
| 170 | * Returns the corresponding AST for this DQL query. | ||
| 171 | */ | ||
| 172 | public function getAST(): SelectStatement|UpdateStatement|DeleteStatement | ||
| 173 | { | ||
| 174 | $parser = new Parser($this); | ||
| 175 | |||
| 176 | return $parser->getAST(); | ||
| 177 | } | ||
| 178 | |||
| 179 | protected function getResultSetMapping(): ResultSetMapping | ||
| 180 | { | ||
| 181 | // parse query or load from cache | ||
| 182 | if ($this->resultSetMapping === null) { | ||
| 183 | $this->resultSetMapping = $this->parse()->getResultSetMapping(); | ||
| 184 | } | ||
| 185 | |||
| 186 | return $this->resultSetMapping; | ||
| 187 | } | ||
| 188 | |||
| 189 | /** | ||
| 190 | * Parses the DQL query, if necessary, and stores the parser result. | ||
| 191 | * | ||
| 192 | * Note: Populates $this->_parserResult as a side-effect. | ||
| 193 | */ | ||
| 194 | private function parse(): ParserResult | ||
| 195 | { | ||
| 196 | $types = []; | ||
| 197 | |||
| 198 | foreach ($this->parameters as $parameter) { | ||
| 199 | /** @var Query\Parameter $parameter */ | ||
| 200 | $types[$parameter->getName()] = $parameter->getType(); | ||
| 201 | } | ||
| 202 | |||
| 203 | // Return previous parser result if the query and the filter collection are both clean | ||
| 204 | if ($this->state === self::STATE_CLEAN && $this->parsedTypes === $types && $this->em->isFiltersStateClean()) { | ||
| 205 | return $this->parserResult; | ||
| 206 | } | ||
| 207 | |||
| 208 | $this->state = self::STATE_CLEAN; | ||
| 209 | $this->parsedTypes = $types; | ||
| 210 | |||
| 211 | $queryCache = $this->queryCache ?? $this->em->getConfiguration()->getQueryCache(); | ||
| 212 | // Check query cache. | ||
| 213 | if (! ($this->useQueryCache && $queryCache)) { | ||
| 214 | $parser = new Parser($this); | ||
| 215 | |||
| 216 | $this->parserResult = $parser->parse(); | ||
| 217 | |||
| 218 | return $this->parserResult; | ||
| 219 | } | ||
| 220 | |||
| 221 | $cacheItem = $queryCache->getItem($this->getQueryCacheId()); | ||
| 222 | |||
| 223 | if (! $this->expireQueryCache && $cacheItem->isHit()) { | ||
| 224 | $cached = $cacheItem->get(); | ||
| 225 | if ($cached instanceof ParserResult) { | ||
| 226 | // Cache hit. | ||
| 227 | $this->parserResult = $cached; | ||
| 228 | |||
| 229 | return $this->parserResult; | ||
| 230 | } | ||
| 231 | } | ||
| 232 | |||
| 233 | // Cache miss. | ||
| 234 | $parser = new Parser($this); | ||
| 235 | |||
| 236 | $this->parserResult = $parser->parse(); | ||
| 237 | |||
| 238 | $queryCache->save($cacheItem->set($this->parserResult)->expiresAfter($this->queryCacheTTL)); | ||
| 239 | |||
| 240 | return $this->parserResult; | ||
| 241 | } | ||
| 242 | |||
| 243 | protected function _doExecute(): Result|int | ||
| 244 | { | ||
| 245 | $executor = $this->parse()->getSqlExecutor(); | ||
| 246 | |||
| 247 | if ($this->queryCacheProfile) { | ||
| 248 | $executor->setQueryCacheProfile($this->queryCacheProfile); | ||
| 249 | } else { | ||
| 250 | $executor->removeQueryCacheProfile(); | ||
| 251 | } | ||
| 252 | |||
| 253 | if ($this->resultSetMapping === null) { | ||
| 254 | $this->resultSetMapping = $this->parserResult->getResultSetMapping(); | ||
| 255 | } | ||
| 256 | |||
| 257 | // Prepare parameters | ||
| 258 | $paramMappings = $this->parserResult->getParameterMappings(); | ||
| 259 | $paramCount = count($this->parameters); | ||
| 260 | $mappingCount = count($paramMappings); | ||
| 261 | |||
| 262 | if ($paramCount > $mappingCount) { | ||
| 263 | throw QueryException::tooManyParameters($mappingCount, $paramCount); | ||
| 264 | } | ||
| 265 | |||
| 266 | if ($paramCount < $mappingCount) { | ||
| 267 | throw QueryException::tooFewParameters($mappingCount, $paramCount); | ||
| 268 | } | ||
| 269 | |||
| 270 | // evict all cache for the entity region | ||
| 271 | if ($this->hasCache && isset($this->hints[self::HINT_CACHE_EVICT]) && $this->hints[self::HINT_CACHE_EVICT]) { | ||
| 272 | $this->evictEntityCacheRegion(); | ||
| 273 | } | ||
| 274 | |||
| 275 | [$sqlParams, $types] = $this->processParameterMappings($paramMappings); | ||
| 276 | |||
| 277 | $this->evictResultSetCache( | ||
| 278 | $executor, | ||
| 279 | $sqlParams, | ||
| 280 | $types, | ||
| 281 | $this->em->getConnection()->getParams(), | ||
| 282 | ); | ||
| 283 | |||
| 284 | return $executor->execute($this->em->getConnection(), $sqlParams, $types); | ||
| 285 | } | ||
| 286 | |||
| 287 | /** | ||
| 288 | * @param array<string,mixed> $sqlParams | ||
| 289 | * @param array<string,Type> $types | ||
| 290 | * @param array<string,mixed> $connectionParams | ||
| 291 | */ | ||
| 292 | private function evictResultSetCache( | ||
| 293 | AbstractSqlExecutor $executor, | ||
| 294 | array $sqlParams, | ||
| 295 | array $types, | ||
| 296 | array $connectionParams, | ||
| 297 | ): void { | ||
| 298 | if ($this->queryCacheProfile === null || ! $this->getExpireResultCache()) { | ||
| 299 | return; | ||
| 300 | } | ||
| 301 | |||
| 302 | $cache = $this->queryCacheProfile->getResultCache(); | ||
| 303 | |||
| 304 | assert($cache !== null); | ||
| 305 | |||
| 306 | $statements = (array) $executor->getSqlStatements(); // Type casted since it can either be a string or an array | ||
| 307 | |||
| 308 | foreach ($statements as $statement) { | ||
| 309 | $cacheKeys = $this->queryCacheProfile->generateCacheKeys($statement, $sqlParams, $types, $connectionParams); | ||
| 310 | $cache->deleteItem(reset($cacheKeys)); | ||
| 311 | } | ||
| 312 | } | ||
| 313 | |||
| 314 | /** | ||
| 315 | * Evict entity cache region | ||
| 316 | */ | ||
| 317 | private function evictEntityCacheRegion(): void | ||
| 318 | { | ||
| 319 | $AST = $this->getAST(); | ||
| 320 | |||
| 321 | if ($AST instanceof SelectStatement) { | ||
| 322 | throw new QueryException('The hint "HINT_CACHE_EVICT" is not valid for select statements.'); | ||
| 323 | } | ||
| 324 | |||
| 325 | $className = $AST instanceof DeleteStatement | ||
| 326 | ? $AST->deleteClause->abstractSchemaName | ||
| 327 | : $AST->updateClause->abstractSchemaName; | ||
| 328 | |||
| 329 | $this->em->getCache()->evictEntityRegion($className); | ||
| 330 | } | ||
| 331 | |||
| 332 | /** | ||
| 333 | * Processes query parameter mappings. | ||
| 334 | * | ||
| 335 | * @param array<list<int>> $paramMappings | ||
| 336 | * | ||
| 337 | * @return mixed[][] | ||
| 338 | * @psalm-return array{0: list<mixed>, 1: array} | ||
| 339 | * | ||
| 340 | * @throws Query\QueryException | ||
| 341 | */ | ||
| 342 | private function processParameterMappings(array $paramMappings): array | ||
| 343 | { | ||
| 344 | $sqlParams = []; | ||
| 345 | $types = []; | ||
| 346 | |||
| 347 | foreach ($this->parameters as $parameter) { | ||
| 348 | $key = $parameter->getName(); | ||
| 349 | |||
| 350 | if (! isset($paramMappings[$key])) { | ||
| 351 | throw QueryException::unknownParameter($key); | ||
| 352 | } | ||
| 353 | |||
| 354 | [$value, $type] = $this->resolveParameterValue($parameter); | ||
| 355 | |||
| 356 | foreach ($paramMappings[$key] as $position) { | ||
| 357 | $types[$position] = $type; | ||
| 358 | } | ||
| 359 | |||
| 360 | $sqlPositions = $paramMappings[$key]; | ||
| 361 | |||
| 362 | // optimized multi value sql positions away for now, | ||
| 363 | // they are not allowed in DQL anyways. | ||
| 364 | $value = [$value]; | ||
| 365 | $countValue = count($value); | ||
| 366 | |||
| 367 | for ($i = 0, $l = count($sqlPositions); $i < $l; $i++) { | ||
| 368 | $sqlParams[$sqlPositions[$i]] = $value[$i % $countValue]; | ||
| 369 | } | ||
| 370 | } | ||
| 371 | |||
| 372 | if (count($sqlParams) !== count($types)) { | ||
| 373 | throw QueryException::parameterTypeMismatch(); | ||
| 374 | } | ||
| 375 | |||
| 376 | if ($sqlParams) { | ||
| 377 | ksort($sqlParams); | ||
| 378 | $sqlParams = array_values($sqlParams); | ||
| 379 | |||
| 380 | ksort($types); | ||
| 381 | $types = array_values($types); | ||
| 382 | } | ||
| 383 | |||
| 384 | return [$sqlParams, $types]; | ||
| 385 | } | ||
| 386 | |||
| 387 | /** | ||
| 388 | * @return mixed[] tuple of (value, type) | ||
| 389 | * @psalm-return array{0: mixed, 1: mixed} | ||
| 390 | */ | ||
| 391 | private function resolveParameterValue(Parameter $parameter): array | ||
| 392 | { | ||
| 393 | if ($parameter->typeWasSpecified()) { | ||
| 394 | return [$parameter->getValue(), $parameter->getType()]; | ||
| 395 | } | ||
| 396 | |||
| 397 | $key = $parameter->getName(); | ||
| 398 | $originalValue = $parameter->getValue(); | ||
| 399 | $value = $originalValue; | ||
| 400 | $rsm = $this->getResultSetMapping(); | ||
| 401 | |||
| 402 | if ($value instanceof ClassMetadata && isset($rsm->metadataParameterMapping[$key])) { | ||
| 403 | $value = $value->getMetadataValue($rsm->metadataParameterMapping[$key]); | ||
| 404 | } | ||
| 405 | |||
| 406 | if ($value instanceof ClassMetadata && isset($rsm->discriminatorParameters[$key])) { | ||
| 407 | $value = array_keys(HierarchyDiscriminatorResolver::resolveDiscriminatorsForClass($value, $this->em)); | ||
| 408 | } | ||
| 409 | |||
| 410 | $processedValue = $this->processParameterValue($value); | ||
| 411 | |||
| 412 | return [ | ||
| 413 | $processedValue, | ||
| 414 | $originalValue === $processedValue | ||
| 415 | ? $parameter->getType() | ||
| 416 | : ParameterTypeInferer::inferType($processedValue), | ||
| 417 | ]; | ||
| 418 | } | ||
| 419 | |||
| 420 | /** | ||
| 421 | * Defines a cache driver to be used for caching queries. | ||
| 422 | * | ||
| 423 | * @return $this | ||
| 424 | */ | ||
| 425 | public function setQueryCache(CacheItemPoolInterface|null $queryCache): self | ||
| 426 | { | ||
| 427 | $this->queryCache = $queryCache; | ||
| 428 | |||
| 429 | return $this; | ||
| 430 | } | ||
| 431 | |||
| 432 | /** | ||
| 433 | * Defines whether the query should make use of a query cache, if available. | ||
| 434 | * | ||
| 435 | * @return $this | ||
| 436 | */ | ||
| 437 | public function useQueryCache(bool $bool): self | ||
| 438 | { | ||
| 439 | $this->useQueryCache = $bool; | ||
| 440 | |||
| 441 | return $this; | ||
| 442 | } | ||
| 443 | |||
| 444 | /** | ||
| 445 | * Defines how long the query cache will be active before expire. | ||
| 446 | * | ||
| 447 | * @param int|null $timeToLive How long the cache entry is valid. | ||
| 448 | * | ||
| 449 | * @return $this | ||
| 450 | */ | ||
| 451 | public function setQueryCacheLifetime(int|null $timeToLive): self | ||
| 452 | { | ||
| 453 | $this->queryCacheTTL = $timeToLive; | ||
| 454 | |||
| 455 | return $this; | ||
| 456 | } | ||
| 457 | |||
| 458 | /** | ||
| 459 | * Retrieves the lifetime of resultset cache. | ||
| 460 | */ | ||
| 461 | public function getQueryCacheLifetime(): int|null | ||
| 462 | { | ||
| 463 | return $this->queryCacheTTL; | ||
| 464 | } | ||
| 465 | |||
| 466 | /** | ||
| 467 | * Defines if the query cache is active or not. | ||
| 468 | * | ||
| 469 | * @return $this | ||
| 470 | */ | ||
| 471 | public function expireQueryCache(bool $expire = true): self | ||
| 472 | { | ||
| 473 | $this->expireQueryCache = $expire; | ||
| 474 | |||
| 475 | return $this; | ||
| 476 | } | ||
| 477 | |||
| 478 | /** | ||
| 479 | * Retrieves if the query cache is active or not. | ||
| 480 | */ | ||
| 481 | public function getExpireQueryCache(): bool | ||
| 482 | { | ||
| 483 | return $this->expireQueryCache; | ||
| 484 | } | ||
| 485 | |||
| 486 | public function free(): void | ||
| 487 | { | ||
| 488 | parent::free(); | ||
| 489 | |||
| 490 | $this->dql = null; | ||
| 491 | $this->state = self::STATE_CLEAN; | ||
| 492 | } | ||
| 493 | |||
| 494 | /** | ||
| 495 | * Sets a DQL query string. | ||
| 496 | */ | ||
| 497 | public function setDQL(string $dqlQuery): self | ||
| 498 | { | ||
| 499 | $this->dql = $dqlQuery; | ||
| 500 | $this->state = self::STATE_DIRTY; | ||
| 501 | |||
| 502 | return $this; | ||
| 503 | } | ||
| 504 | |||
| 505 | /** | ||
| 506 | * Returns the DQL query that is represented by this query object. | ||
| 507 | */ | ||
| 508 | public function getDQL(): string|null | ||
| 509 | { | ||
| 510 | return $this->dql; | ||
| 511 | } | ||
| 512 | |||
| 513 | /** | ||
| 514 | * Returns the state of this query object | ||
| 515 | * By default the type is Doctrine_ORM_Query_Abstract::STATE_CLEAN but if it appears any unprocessed DQL | ||
| 516 | * part, it is switched to Doctrine_ORM_Query_Abstract::STATE_DIRTY. | ||
| 517 | * | ||
| 518 | * @see AbstractQuery::STATE_CLEAN | ||
| 519 | * @see AbstractQuery::STATE_DIRTY | ||
| 520 | * | ||
| 521 | * @return int The query state. | ||
| 522 | * @psalm-return self::STATE_* The query state. | ||
| 523 | */ | ||
| 524 | public function getState(): int | ||
| 525 | { | ||
| 526 | return $this->state; | ||
| 527 | } | ||
| 528 | |||
| 529 | /** | ||
| 530 | * Method to check if an arbitrary piece of DQL exists | ||
| 531 | * | ||
| 532 | * @param string $dql Arbitrary piece of DQL to check for. | ||
| 533 | */ | ||
| 534 | public function contains(string $dql): bool | ||
| 535 | { | ||
| 536 | return stripos($this->getDQL(), $dql) !== false; | ||
| 537 | } | ||
| 538 | |||
| 539 | /** | ||
| 540 | * Sets the position of the first result to retrieve (the "offset"). | ||
| 541 | * | ||
| 542 | * @param int $firstResult The first result to return. | ||
| 543 | * | ||
| 544 | * @return $this | ||
| 545 | */ | ||
| 546 | public function setFirstResult(int $firstResult): self | ||
| 547 | { | ||
| 548 | $this->firstResult = $firstResult; | ||
| 549 | $this->state = self::STATE_DIRTY; | ||
| 550 | |||
| 551 | return $this; | ||
| 552 | } | ||
| 553 | |||
| 554 | /** | ||
| 555 | * Gets the position of the first result the query object was set to retrieve (the "offset"). | ||
| 556 | * Returns 0 if {@link setFirstResult} was not applied to this query. | ||
| 557 | * | ||
| 558 | * @return int The position of the first result. | ||
| 559 | */ | ||
| 560 | public function getFirstResult(): int | ||
| 561 | { | ||
| 562 | return $this->firstResult; | ||
| 563 | } | ||
| 564 | |||
| 565 | /** | ||
| 566 | * Sets the maximum number of results to retrieve (the "limit"). | ||
| 567 | * | ||
| 568 | * @return $this | ||
| 569 | */ | ||
| 570 | public function setMaxResults(int|null $maxResults): self | ||
| 571 | { | ||
| 572 | $this->maxResults = $maxResults; | ||
| 573 | $this->state = self::STATE_DIRTY; | ||
| 574 | |||
| 575 | return $this; | ||
| 576 | } | ||
| 577 | |||
| 578 | /** | ||
| 579 | * Gets the maximum number of results the query object was set to retrieve (the "limit"). | ||
| 580 | * Returns NULL if {@link setMaxResults} was not applied to this query. | ||
| 581 | * | ||
| 582 | * @return int|null Maximum number of results. | ||
| 583 | */ | ||
| 584 | public function getMaxResults(): int|null | ||
| 585 | { | ||
| 586 | return $this->maxResults; | ||
| 587 | } | ||
| 588 | |||
| 589 | /** {@inheritDoc} */ | ||
| 590 | public function toIterable(iterable $parameters = [], $hydrationMode = self::HYDRATE_OBJECT): iterable | ||
| 591 | { | ||
| 592 | $this->setHint(self::HINT_INTERNAL_ITERATION, true); | ||
| 593 | |||
| 594 | return parent::toIterable($parameters, $hydrationMode); | ||
| 595 | } | ||
| 596 | |||
| 597 | public function setHint(string $name, mixed $value): static | ||
| 598 | { | ||
| 599 | $this->state = self::STATE_DIRTY; | ||
| 600 | |||
| 601 | return parent::setHint($name, $value); | ||
| 602 | } | ||
| 603 | |||
| 604 | public function setHydrationMode(string|int $hydrationMode): static | ||
| 605 | { | ||
| 606 | $this->state = self::STATE_DIRTY; | ||
| 607 | |||
| 608 | return parent::setHydrationMode($hydrationMode); | ||
| 609 | } | ||
| 610 | |||
| 611 | /** | ||
| 612 | * Set the lock mode for this Query. | ||
| 613 | * | ||
| 614 | * @see \Doctrine\DBAL\LockMode | ||
| 615 | * | ||
| 616 | * @psalm-param LockMode::* $lockMode | ||
| 617 | * | ||
| 618 | * @return $this | ||
| 619 | * | ||
| 620 | * @throws TransactionRequiredException | ||
| 621 | */ | ||
| 622 | public function setLockMode(LockMode|int $lockMode): self | ||
| 623 | { | ||
| 624 | if (in_array($lockMode, [LockMode::NONE, LockMode::PESSIMISTIC_READ, LockMode::PESSIMISTIC_WRITE], true)) { | ||
| 625 | if (! $this->em->getConnection()->isTransactionActive()) { | ||
| 626 | throw TransactionRequiredException::transactionRequired(); | ||
| 627 | } | ||
| 628 | } | ||
| 629 | |||
| 630 | $this->setHint(self::HINT_LOCK_MODE, $lockMode); | ||
| 631 | |||
| 632 | return $this; | ||
| 633 | } | ||
| 634 | |||
| 635 | /** | ||
| 636 | * Get the current lock mode for this query. | ||
| 637 | * | ||
| 638 | * @return LockMode|int|null The current lock mode of this query or NULL if no specific lock mode is set. | ||
| 639 | * @psalm-return LockMode::*|null | ||
| 640 | */ | ||
| 641 | public function getLockMode(): LockMode|int|null | ||
| 642 | { | ||
| 643 | $lockMode = $this->getHint(self::HINT_LOCK_MODE); | ||
| 644 | |||
| 645 | if ($lockMode === false) { | ||
| 646 | return null; | ||
| 647 | } | ||
| 648 | |||
| 649 | return $lockMode; | ||
| 650 | } | ||
| 651 | |||
| 652 | /** | ||
| 653 | * Generate a cache id for the query cache - reusing the Result-Cache-Id generator. | ||
| 654 | */ | ||
| 655 | protected function getQueryCacheId(): string | ||
| 656 | { | ||
| 657 | ksort($this->hints); | ||
| 658 | |||
| 659 | return md5( | ||
| 660 | $this->getDQL() . serialize($this->hints) . | ||
| 661 | '&platform=' . get_debug_type($this->getEntityManager()->getConnection()->getDatabasePlatform()) . | ||
| 662 | ($this->em->hasFilters() ? $this->em->getFilters()->getHash() : '') . | ||
| 663 | '&firstResult=' . $this->firstResult . '&maxResult=' . $this->maxResults . | ||
| 664 | '&hydrationMode=' . $this->hydrationMode . '&types=' . serialize($this->parsedTypes) . 'DOCTRINE_QUERY_CACHE_SALT', | ||
| 665 | ); | ||
| 666 | } | ||
| 667 | |||
| 668 | protected function getHash(): string | ||
| 669 | { | ||
| 670 | return sha1(parent::getHash() . '-' . $this->firstResult . '-' . $this->maxResults); | ||
| 671 | } | ||
| 672 | |||
| 673 | /** | ||
| 674 | * Cleanup Query resource when clone is called. | ||
| 675 | */ | ||
| 676 | public function __clone() | ||
| 677 | { | ||
| 678 | parent::__clone(); | ||
| 679 | |||
| 680 | $this->state = self::STATE_DIRTY; | ||
| 681 | } | ||
| 682 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/AST/ASTException.php b/vendor/doctrine/orm/src/Query/AST/ASTException.php new file mode 100644 index 0000000..1ef890a --- /dev/null +++ b/vendor/doctrine/orm/src/Query/AST/ASTException.php | |||
| @@ -0,0 +1,20 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Query\AST; | ||
| 6 | |||
| 7 | use Doctrine\ORM\Query\QueryException; | ||
| 8 | |||
| 9 | use function get_debug_type; | ||
| 10 | |||
| 11 | /** | ||
| 12 | * Base exception class for AST exceptions. | ||
| 13 | */ | ||
| 14 | class ASTException extends QueryException | ||
| 15 | { | ||
| 16 | public static function noDispatchForNode(Node $node): self | ||
| 17 | { | ||
| 18 | return new self('Double-dispatch for node ' . get_debug_type($node) . ' is not supported.'); | ||
| 19 | } | ||
| 20 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/AST/AggregateExpression.php b/vendor/doctrine/orm/src/Query/AST/AggregateExpression.php new file mode 100644 index 0000000..468c65c --- /dev/null +++ b/vendor/doctrine/orm/src/Query/AST/AggregateExpression.php | |||
| @@ -0,0 +1,23 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Query\AST; | ||
| 6 | |||
| 7 | use Doctrine\ORM\Query\SqlWalker; | ||
| 8 | |||
| 9 | class AggregateExpression extends Node | ||
| 10 | { | ||
| 11 | /** @param bool $isDistinct Some aggregate expressions support distinct, eg COUNT. */ | ||
| 12 | public function __construct( | ||
| 13 | public string $functionName, | ||
| 14 | public Node|string $pathExpression, | ||
| 15 | public bool $isDistinct, | ||
| 16 | ) { | ||
| 17 | } | ||
| 18 | |||
| 19 | public function dispatch(SqlWalker $walker): string | ||
| 20 | { | ||
| 21 | return $walker->walkAggregateExpression($this); | ||
| 22 | } | ||
| 23 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/AST/ArithmeticExpression.php b/vendor/doctrine/orm/src/Query/AST/ArithmeticExpression.php new file mode 100644 index 0000000..a819e05 --- /dev/null +++ b/vendor/doctrine/orm/src/Query/AST/ArithmeticExpression.php | |||
| @@ -0,0 +1,34 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Query\AST; | ||
| 6 | |||
| 7 | use Doctrine\ORM\Query\SqlWalker; | ||
| 8 | |||
| 9 | /** | ||
| 10 | * ArithmeticExpression ::= SimpleArithmeticExpression | "(" Subselect ")" | ||
| 11 | * | ||
| 12 | * @link www.doctrine-project.org | ||
| 13 | */ | ||
| 14 | class ArithmeticExpression extends Node | ||
| 15 | { | ||
| 16 | public Node|string|null $simpleArithmeticExpression = null; | ||
| 17 | |||
| 18 | public Subselect|null $subselect = null; | ||
| 19 | |||
| 20 | public function isSimpleArithmeticExpression(): bool | ||
| 21 | { | ||
| 22 | return (bool) $this->simpleArithmeticExpression; | ||
| 23 | } | ||
| 24 | |||
| 25 | public function isSubselect(): bool | ||
| 26 | { | ||
| 27 | return (bool) $this->subselect; | ||
| 28 | } | ||
| 29 | |||
| 30 | public function dispatch(SqlWalker $walker): string | ||
| 31 | { | ||
| 32 | return $walker->walkArithmeticExpression($this); | ||
| 33 | } | ||
| 34 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/AST/ArithmeticFactor.php b/vendor/doctrine/orm/src/Query/AST/ArithmeticFactor.php new file mode 100644 index 0000000..278a921 --- /dev/null +++ b/vendor/doctrine/orm/src/Query/AST/ArithmeticFactor.php | |||
| @@ -0,0 +1,36 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Query\AST; | ||
| 6 | |||
| 7 | use Doctrine\ORM\Query\SqlWalker; | ||
| 8 | |||
| 9 | /** | ||
| 10 | * ArithmeticFactor ::= [("+" | "-")] ArithmeticPrimary | ||
| 11 | * | ||
| 12 | * @link www.doctrine-project.org | ||
| 13 | */ | ||
| 14 | class ArithmeticFactor extends Node | ||
| 15 | { | ||
| 16 | public function __construct( | ||
| 17 | public mixed $arithmeticPrimary, | ||
| 18 | public bool|null $sign = null, | ||
| 19 | ) { | ||
| 20 | } | ||
| 21 | |||
| 22 | public function isPositiveSigned(): bool | ||
| 23 | { | ||
| 24 | return $this->sign === true; | ||
| 25 | } | ||
| 26 | |||
| 27 | public function isNegativeSigned(): bool | ||
| 28 | { | ||
| 29 | return $this->sign === false; | ||
| 30 | } | ||
| 31 | |||
| 32 | public function dispatch(SqlWalker $walker): string | ||
| 33 | { | ||
| 34 | return $walker->walkArithmeticFactor($this); | ||
| 35 | } | ||
| 36 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/AST/ArithmeticTerm.php b/vendor/doctrine/orm/src/Query/AST/ArithmeticTerm.php new file mode 100644 index 0000000..b233612 --- /dev/null +++ b/vendor/doctrine/orm/src/Query/AST/ArithmeticTerm.php | |||
| @@ -0,0 +1,25 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Query\AST; | ||
| 6 | |||
| 7 | use Doctrine\ORM\Query\SqlWalker; | ||
| 8 | |||
| 9 | /** | ||
| 10 | * ArithmeticTerm ::= ArithmeticFactor {("*" | "/") ArithmeticFactor}* | ||
| 11 | * | ||
| 12 | * @link www.doctrine-project.org | ||
| 13 | */ | ||
| 14 | class ArithmeticTerm extends Node | ||
| 15 | { | ||
| 16 | /** @param mixed[] $arithmeticFactors */ | ||
| 17 | public function __construct(public array $arithmeticFactors) | ||
| 18 | { | ||
| 19 | } | ||
| 20 | |||
| 21 | public function dispatch(SqlWalker $walker): string | ||
| 22 | { | ||
| 23 | return $walker->walkArithmeticTerm($this); | ||
| 24 | } | ||
| 25 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/AST/BetweenExpression.php b/vendor/doctrine/orm/src/Query/AST/BetweenExpression.php new file mode 100644 index 0000000..c13292b --- /dev/null +++ b/vendor/doctrine/orm/src/Query/AST/BetweenExpression.php | |||
| @@ -0,0 +1,23 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Query\AST; | ||
| 6 | |||
| 7 | use Doctrine\ORM\Query\SqlWalker; | ||
| 8 | |||
| 9 | class BetweenExpression extends Node | ||
| 10 | { | ||
| 11 | public function __construct( | ||
| 12 | public ArithmeticExpression $expression, | ||
| 13 | public ArithmeticExpression $leftBetweenExpression, | ||
| 14 | public ArithmeticExpression $rightBetweenExpression, | ||
| 15 | public bool $not = false, | ||
| 16 | ) { | ||
| 17 | } | ||
| 18 | |||
| 19 | public function dispatch(SqlWalker $walker): string | ||
| 20 | { | ||
| 21 | return $walker->walkBetweenExpression($this); | ||
| 22 | } | ||
| 23 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/AST/CoalesceExpression.php b/vendor/doctrine/orm/src/Query/AST/CoalesceExpression.php new file mode 100644 index 0000000..89f025f --- /dev/null +++ b/vendor/doctrine/orm/src/Query/AST/CoalesceExpression.php | |||
| @@ -0,0 +1,25 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Query\AST; | ||
| 6 | |||
| 7 | use Doctrine\ORM\Query\SqlWalker; | ||
| 8 | |||
| 9 | /** | ||
| 10 | * CoalesceExpression ::= "COALESCE" "(" ScalarExpression {"," ScalarExpression}* ")" | ||
| 11 | * | ||
| 12 | * @link www.doctrine-project.org | ||
| 13 | */ | ||
| 14 | class CoalesceExpression extends Node | ||
| 15 | { | ||
| 16 | /** @param mixed[] $scalarExpressions */ | ||
| 17 | public function __construct(public array $scalarExpressions) | ||
| 18 | { | ||
| 19 | } | ||
| 20 | |||
| 21 | public function dispatch(SqlWalker $walker): string | ||
| 22 | { | ||
| 23 | return $walker->walkCoalesceExpression($this); | ||
| 24 | } | ||
| 25 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/AST/CollectionMemberExpression.php b/vendor/doctrine/orm/src/Query/AST/CollectionMemberExpression.php new file mode 100644 index 0000000..a62a191 --- /dev/null +++ b/vendor/doctrine/orm/src/Query/AST/CollectionMemberExpression.php | |||
| @@ -0,0 +1,27 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Query\AST; | ||
| 6 | |||
| 7 | use Doctrine\ORM\Query\SqlWalker; | ||
| 8 | |||
| 9 | /** | ||
| 10 | * CollectionMemberExpression ::= EntityExpression ["NOT"] "MEMBER" ["OF"] CollectionValuedPathExpression | ||
| 11 | * | ||
| 12 | * @link www.doctrine-project.org | ||
| 13 | */ | ||
| 14 | class CollectionMemberExpression extends Node | ||
| 15 | { | ||
| 16 | public function __construct( | ||
| 17 | public mixed $entityExpression, | ||
| 18 | public PathExpression $collectionValuedPathExpression, | ||
| 19 | public bool $not = false, | ||
| 20 | ) { | ||
| 21 | } | ||
| 22 | |||
| 23 | public function dispatch(SqlWalker $walker): string | ||
| 24 | { | ||
| 25 | return $walker->walkCollectionMemberExpression($this); | ||
| 26 | } | ||
| 27 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/AST/ComparisonExpression.php b/vendor/doctrine/orm/src/Query/AST/ComparisonExpression.php new file mode 100644 index 0000000..a7d91f9 --- /dev/null +++ b/vendor/doctrine/orm/src/Query/AST/ComparisonExpression.php | |||
| @@ -0,0 +1,32 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Query\AST; | ||
| 6 | |||
| 7 | use Doctrine\ORM\Query\SqlWalker; | ||
| 8 | |||
| 9 | /** | ||
| 10 | * ComparisonExpression ::= ArithmeticExpression ComparisonOperator ( QuantifiedExpression | ArithmeticExpression ) | | ||
| 11 | * StringExpression ComparisonOperator (StringExpression | QuantifiedExpression) | | ||
| 12 | * BooleanExpression ("=" | "<>" | "!=") (BooleanExpression | QuantifiedExpression) | | ||
| 13 | * EnumExpression ("=" | "<>" | "!=") (EnumExpression | QuantifiedExpression) | | ||
| 14 | * DatetimeExpression ComparisonOperator (DatetimeExpression | QuantifiedExpression) | | ||
| 15 | * EntityExpression ("=" | "<>") (EntityExpression | QuantifiedExpression) | ||
| 16 | * | ||
| 17 | * @link www.doctrine-project.org | ||
| 18 | */ | ||
| 19 | class ComparisonExpression extends Node | ||
| 20 | { | ||
| 21 | public function __construct( | ||
| 22 | public Node|string $leftExpression, | ||
| 23 | public string $operator, | ||
| 24 | public Node|string $rightExpression, | ||
| 25 | ) { | ||
| 26 | } | ||
| 27 | |||
| 28 | public function dispatch(SqlWalker $walker): string | ||
| 29 | { | ||
| 30 | return $walker->walkComparisonExpression($this); | ||
| 31 | } | ||
| 32 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/AST/ConditionalExpression.php b/vendor/doctrine/orm/src/Query/AST/ConditionalExpression.php new file mode 100644 index 0000000..26a98e5 --- /dev/null +++ b/vendor/doctrine/orm/src/Query/AST/ConditionalExpression.php | |||
| @@ -0,0 +1,25 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Query\AST; | ||
| 6 | |||
| 7 | use Doctrine\ORM\Query\SqlWalker; | ||
| 8 | |||
| 9 | /** | ||
| 10 | * ConditionalExpression ::= ConditionalTerm {"OR" ConditionalTerm}* | ||
| 11 | * | ||
| 12 | * @link www.doctrine-project.org | ||
| 13 | */ | ||
| 14 | class ConditionalExpression extends Node | ||
| 15 | { | ||
| 16 | /** @param mixed[] $conditionalTerms */ | ||
| 17 | public function __construct(public array $conditionalTerms) | ||
| 18 | { | ||
| 19 | } | ||
| 20 | |||
| 21 | public function dispatch(SqlWalker $walker): string | ||
| 22 | { | ||
| 23 | return $walker->walkConditionalExpression($this); | ||
| 24 | } | ||
| 25 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/AST/ConditionalFactor.php b/vendor/doctrine/orm/src/Query/AST/ConditionalFactor.php new file mode 100644 index 0000000..7881743 --- /dev/null +++ b/vendor/doctrine/orm/src/Query/AST/ConditionalFactor.php | |||
| @@ -0,0 +1,26 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Query\AST; | ||
| 6 | |||
| 7 | use Doctrine\ORM\Query\SqlWalker; | ||
| 8 | |||
| 9 | /** | ||
| 10 | * ConditionalFactor ::= ["NOT"] ConditionalPrimary | ||
| 11 | * | ||
| 12 | * @link www.doctrine-project.org | ||
| 13 | */ | ||
| 14 | class ConditionalFactor extends Node implements Phase2OptimizableConditional | ||
| 15 | { | ||
| 16 | public function __construct( | ||
| 17 | public ConditionalPrimary $conditionalPrimary, | ||
| 18 | public bool $not = false, | ||
| 19 | ) { | ||
| 20 | } | ||
| 21 | |||
| 22 | public function dispatch(SqlWalker $walker): string | ||
| 23 | { | ||
| 24 | return $walker->walkConditionalFactor($this); | ||
| 25 | } | ||
| 26 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/AST/ConditionalPrimary.php b/vendor/doctrine/orm/src/Query/AST/ConditionalPrimary.php new file mode 100644 index 0000000..9344cd9 --- /dev/null +++ b/vendor/doctrine/orm/src/Query/AST/ConditionalPrimary.php | |||
| @@ -0,0 +1,34 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Query\AST; | ||
| 6 | |||
| 7 | use Doctrine\ORM\Query\SqlWalker; | ||
| 8 | |||
| 9 | /** | ||
| 10 | * ConditionalPrimary ::= SimpleConditionalExpression | "(" ConditionalExpression ")" | ||
| 11 | * | ||
| 12 | * @link www.doctrine-project.org | ||
| 13 | */ | ||
| 14 | class ConditionalPrimary extends Node implements Phase2OptimizableConditional | ||
| 15 | { | ||
| 16 | public Node|null $simpleConditionalExpression = null; | ||
| 17 | |||
| 18 | public ConditionalExpression|Phase2OptimizableConditional|null $conditionalExpression = null; | ||
| 19 | |||
| 20 | public function isSimpleConditionalExpression(): bool | ||
| 21 | { | ||
| 22 | return (bool) $this->simpleConditionalExpression; | ||
| 23 | } | ||
| 24 | |||
| 25 | public function isConditionalExpression(): bool | ||
| 26 | { | ||
| 27 | return (bool) $this->conditionalExpression; | ||
| 28 | } | ||
| 29 | |||
| 30 | public function dispatch(SqlWalker $walker): string | ||
| 31 | { | ||
| 32 | return $walker->walkConditionalPrimary($this); | ||
| 33 | } | ||
| 34 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/AST/ConditionalTerm.php b/vendor/doctrine/orm/src/Query/AST/ConditionalTerm.php new file mode 100644 index 0000000..dcea50b --- /dev/null +++ b/vendor/doctrine/orm/src/Query/AST/ConditionalTerm.php | |||
| @@ -0,0 +1,25 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Query\AST; | ||
| 6 | |||
| 7 | use Doctrine\ORM\Query\SqlWalker; | ||
| 8 | |||
| 9 | /** | ||
| 10 | * ConditionalTerm ::= ConditionalFactor {"AND" ConditionalFactor}* | ||
| 11 | * | ||
| 12 | * @link www.doctrine-project.org | ||
| 13 | */ | ||
| 14 | class ConditionalTerm extends Node implements Phase2OptimizableConditional | ||
| 15 | { | ||
| 16 | /** @param mixed[] $conditionalFactors */ | ||
| 17 | public function __construct(public array $conditionalFactors) | ||
| 18 | { | ||
| 19 | } | ||
| 20 | |||
| 21 | public function dispatch(SqlWalker $walker): string | ||
| 22 | { | ||
| 23 | return $walker->walkConditionalTerm($this); | ||
| 24 | } | ||
| 25 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/AST/DeleteClause.php b/vendor/doctrine/orm/src/Query/AST/DeleteClause.php new file mode 100644 index 0000000..25e9085 --- /dev/null +++ b/vendor/doctrine/orm/src/Query/AST/DeleteClause.php | |||
| @@ -0,0 +1,26 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Query\AST; | ||
| 6 | |||
| 7 | use Doctrine\ORM\Query\SqlWalker; | ||
| 8 | |||
| 9 | /** | ||
| 10 | * DeleteClause ::= "DELETE" ["FROM"] AbstractSchemaName [["AS"] AliasIdentificationVariable] | ||
| 11 | * | ||
| 12 | * @link www.doctrine-project.org | ||
| 13 | */ | ||
| 14 | class DeleteClause extends Node | ||
| 15 | { | ||
| 16 | public string $aliasIdentificationVariable; | ||
| 17 | |||
| 18 | public function __construct(public string $abstractSchemaName) | ||
| 19 | { | ||
| 20 | } | ||
| 21 | |||
| 22 | public function dispatch(SqlWalker $walker): string | ||
| 23 | { | ||
| 24 | return $walker->walkDeleteClause($this); | ||
| 25 | } | ||
| 26 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/AST/DeleteStatement.php b/vendor/doctrine/orm/src/Query/AST/DeleteStatement.php new file mode 100644 index 0000000..f367d09 --- /dev/null +++ b/vendor/doctrine/orm/src/Query/AST/DeleteStatement.php | |||
| @@ -0,0 +1,26 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Query\AST; | ||
| 6 | |||
| 7 | use Doctrine\ORM\Query\SqlWalker; | ||
| 8 | |||
| 9 | /** | ||
| 10 | * DeleteStatement = DeleteClause [WhereClause] | ||
| 11 | * | ||
| 12 | * @link www.doctrine-project.org | ||
| 13 | */ | ||
| 14 | class DeleteStatement extends Node | ||
| 15 | { | ||
| 16 | public WhereClause|null $whereClause = null; | ||
| 17 | |||
| 18 | public function __construct(public DeleteClause $deleteClause) | ||
| 19 | { | ||
| 20 | } | ||
| 21 | |||
| 22 | public function dispatch(SqlWalker $walker): string | ||
| 23 | { | ||
| 24 | return $walker->walkDeleteStatement($this); | ||
| 25 | } | ||
| 26 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/AST/EmptyCollectionComparisonExpression.php b/vendor/doctrine/orm/src/Query/AST/EmptyCollectionComparisonExpression.php new file mode 100644 index 0000000..9978800 --- /dev/null +++ b/vendor/doctrine/orm/src/Query/AST/EmptyCollectionComparisonExpression.php | |||
| @@ -0,0 +1,26 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Query\AST; | ||
| 6 | |||
| 7 | use Doctrine\ORM\Query\SqlWalker; | ||
| 8 | |||
| 9 | /** | ||
| 10 | * EmptyCollectionComparisonExpression ::= CollectionValuedPathExpression "IS" ["NOT"] "EMPTY" | ||
| 11 | * | ||
| 12 | * @link www.doctrine-project.org | ||
| 13 | */ | ||
| 14 | class EmptyCollectionComparisonExpression extends Node | ||
| 15 | { | ||
| 16 | public function __construct( | ||
| 17 | public PathExpression $expression, | ||
| 18 | public bool $not = false, | ||
| 19 | ) { | ||
| 20 | } | ||
| 21 | |||
| 22 | public function dispatch(SqlWalker $walker): string | ||
| 23 | { | ||
| 24 | return $walker->walkEmptyCollectionComparisonExpression($this); | ||
| 25 | } | ||
| 26 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/AST/ExistsExpression.php b/vendor/doctrine/orm/src/Query/AST/ExistsExpression.php new file mode 100644 index 0000000..72757f4 --- /dev/null +++ b/vendor/doctrine/orm/src/Query/AST/ExistsExpression.php | |||
| @@ -0,0 +1,26 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Query\AST; | ||
| 6 | |||
| 7 | use Doctrine\ORM\Query\SqlWalker; | ||
| 8 | |||
| 9 | /** | ||
| 10 | * ExistsExpression ::= ["NOT"] "EXISTS" "(" Subselect ")" | ||
| 11 | * | ||
| 12 | * @link www.doctrine-project.org | ||
| 13 | */ | ||
| 14 | class ExistsExpression extends Node | ||
| 15 | { | ||
| 16 | public function __construct( | ||
| 17 | public Subselect $subselect, | ||
| 18 | public bool $not = false, | ||
| 19 | ) { | ||
| 20 | } | ||
| 21 | |||
| 22 | public function dispatch(SqlWalker $walker): string | ||
| 23 | { | ||
| 24 | return $walker->walkExistsExpression($this); | ||
| 25 | } | ||
| 26 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/AST/FromClause.php b/vendor/doctrine/orm/src/Query/AST/FromClause.php new file mode 100644 index 0000000..0b74393 --- /dev/null +++ b/vendor/doctrine/orm/src/Query/AST/FromClause.php | |||
| @@ -0,0 +1,25 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Query\AST; | ||
| 6 | |||
| 7 | use Doctrine\ORM\Query\SqlWalker; | ||
| 8 | |||
| 9 | /** | ||
| 10 | * FromClause ::= "FROM" IdentificationVariableDeclaration {"," IdentificationVariableDeclaration} | ||
| 11 | * | ||
| 12 | * @link www.doctrine-project.org | ||
| 13 | */ | ||
| 14 | class FromClause extends Node | ||
| 15 | { | ||
| 16 | /** @param mixed[] $identificationVariableDeclarations */ | ||
| 17 | public function __construct(public array $identificationVariableDeclarations) | ||
| 18 | { | ||
| 19 | } | ||
| 20 | |||
| 21 | public function dispatch(SqlWalker $walker): string | ||
| 22 | { | ||
| 23 | return $walker->walkFromClause($this); | ||
| 24 | } | ||
| 25 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/AST/Functions/AbsFunction.php b/vendor/doctrine/orm/src/Query/AST/Functions/AbsFunction.php new file mode 100644 index 0000000..4edff06 --- /dev/null +++ b/vendor/doctrine/orm/src/Query/AST/Functions/AbsFunction.php | |||
| @@ -0,0 +1,37 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Query\AST\Functions; | ||
| 6 | |||
| 7 | use Doctrine\ORM\Query\AST\Node; | ||
| 8 | use Doctrine\ORM\Query\Parser; | ||
| 9 | use Doctrine\ORM\Query\SqlWalker; | ||
| 10 | use Doctrine\ORM\Query\TokenType; | ||
| 11 | |||
| 12 | /** | ||
| 13 | * "ABS" "(" SimpleArithmeticExpression ")" | ||
| 14 | * | ||
| 15 | * @link www.doctrine-project.org | ||
| 16 | */ | ||
| 17 | class AbsFunction extends FunctionNode | ||
| 18 | { | ||
| 19 | public Node|string $simpleArithmeticExpression; | ||
| 20 | |||
| 21 | public function getSql(SqlWalker $sqlWalker): string | ||
| 22 | { | ||
| 23 | return 'ABS(' . $sqlWalker->walkSimpleArithmeticExpression( | ||
| 24 | $this->simpleArithmeticExpression, | ||
| 25 | ) . ')'; | ||
| 26 | } | ||
| 27 | |||
| 28 | public function parse(Parser $parser): void | ||
| 29 | { | ||
| 30 | $parser->match(TokenType::T_IDENTIFIER); | ||
| 31 | $parser->match(TokenType::T_OPEN_PARENTHESIS); | ||
| 32 | |||
| 33 | $this->simpleArithmeticExpression = $parser->SimpleArithmeticExpression(); | ||
| 34 | |||
| 35 | $parser->match(TokenType::T_CLOSE_PARENTHESIS); | ||
| 36 | } | ||
| 37 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/AST/Functions/AvgFunction.php b/vendor/doctrine/orm/src/Query/AST/Functions/AvgFunction.php new file mode 100644 index 0000000..ba7b7f3 --- /dev/null +++ b/vendor/doctrine/orm/src/Query/AST/Functions/AvgFunction.php | |||
| @@ -0,0 +1,27 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Query\AST\Functions; | ||
| 6 | |||
| 7 | use Doctrine\ORM\Query\AST\AggregateExpression; | ||
| 8 | use Doctrine\ORM\Query\Parser; | ||
| 9 | use Doctrine\ORM\Query\SqlWalker; | ||
| 10 | |||
| 11 | /** | ||
| 12 | * "AVG" "(" ["DISTINCT"] StringPrimary ")" | ||
| 13 | */ | ||
| 14 | final class AvgFunction extends FunctionNode | ||
| 15 | { | ||
| 16 | private AggregateExpression $aggregateExpression; | ||
| 17 | |||
| 18 | public function getSql(SqlWalker $sqlWalker): string | ||
| 19 | { | ||
| 20 | return $this->aggregateExpression->dispatch($sqlWalker); | ||
| 21 | } | ||
| 22 | |||
| 23 | public function parse(Parser $parser): void | ||
| 24 | { | ||
| 25 | $this->aggregateExpression = $parser->AggregateExpression(); | ||
| 26 | } | ||
| 27 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/AST/Functions/BitAndFunction.php b/vendor/doctrine/orm/src/Query/AST/Functions/BitAndFunction.php new file mode 100644 index 0000000..f2d3146 --- /dev/null +++ b/vendor/doctrine/orm/src/Query/AST/Functions/BitAndFunction.php | |||
| @@ -0,0 +1,43 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Query\AST\Functions; | ||
| 6 | |||
| 7 | use Doctrine\ORM\Query\AST\Node; | ||
| 8 | use Doctrine\ORM\Query\Parser; | ||
| 9 | use Doctrine\ORM\Query\SqlWalker; | ||
| 10 | use Doctrine\ORM\Query\TokenType; | ||
| 11 | |||
| 12 | /** | ||
| 13 | * "BIT_AND" "(" ArithmeticPrimary "," ArithmeticPrimary ")" | ||
| 14 | * | ||
| 15 | * @link www.doctrine-project.org | ||
| 16 | */ | ||
| 17 | class BitAndFunction extends FunctionNode | ||
| 18 | { | ||
| 19 | public Node $firstArithmetic; | ||
| 20 | public Node $secondArithmetic; | ||
| 21 | |||
| 22 | public function getSql(SqlWalker $sqlWalker): string | ||
| 23 | { | ||
| 24 | $platform = $sqlWalker->getConnection()->getDatabasePlatform(); | ||
| 25 | |||
| 26 | return $platform->getBitAndComparisonExpression( | ||
| 27 | $this->firstArithmetic->dispatch($sqlWalker), | ||
| 28 | $this->secondArithmetic->dispatch($sqlWalker), | ||
| 29 | ); | ||
| 30 | } | ||
| 31 | |||
| 32 | public function parse(Parser $parser): void | ||
| 33 | { | ||
| 34 | $parser->match(TokenType::T_IDENTIFIER); | ||
| 35 | $parser->match(TokenType::T_OPEN_PARENTHESIS); | ||
| 36 | |||
| 37 | $this->firstArithmetic = $parser->ArithmeticPrimary(); | ||
| 38 | $parser->match(TokenType::T_COMMA); | ||
| 39 | $this->secondArithmetic = $parser->ArithmeticPrimary(); | ||
| 40 | |||
| 41 | $parser->match(TokenType::T_CLOSE_PARENTHESIS); | ||
| 42 | } | ||
| 43 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/AST/Functions/BitOrFunction.php b/vendor/doctrine/orm/src/Query/AST/Functions/BitOrFunction.php new file mode 100644 index 0000000..f3f84da --- /dev/null +++ b/vendor/doctrine/orm/src/Query/AST/Functions/BitOrFunction.php | |||
| @@ -0,0 +1,43 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Query\AST\Functions; | ||
| 6 | |||
| 7 | use Doctrine\ORM\Query\AST\Node; | ||
| 8 | use Doctrine\ORM\Query\Parser; | ||
| 9 | use Doctrine\ORM\Query\SqlWalker; | ||
| 10 | use Doctrine\ORM\Query\TokenType; | ||
| 11 | |||
| 12 | /** | ||
| 13 | * "BIT_OR" "(" ArithmeticPrimary "," ArithmeticPrimary ")" | ||
| 14 | * | ||
| 15 | * @link www.doctrine-project.org | ||
| 16 | */ | ||
| 17 | class BitOrFunction extends FunctionNode | ||
| 18 | { | ||
| 19 | public Node $firstArithmetic; | ||
| 20 | public Node $secondArithmetic; | ||
| 21 | |||
| 22 | public function getSql(SqlWalker $sqlWalker): string | ||
| 23 | { | ||
| 24 | $platform = $sqlWalker->getConnection()->getDatabasePlatform(); | ||
| 25 | |||
| 26 | return $platform->getBitOrComparisonExpression( | ||
| 27 | $this->firstArithmetic->dispatch($sqlWalker), | ||
| 28 | $this->secondArithmetic->dispatch($sqlWalker), | ||
| 29 | ); | ||
| 30 | } | ||
| 31 | |||
| 32 | public function parse(Parser $parser): void | ||
| 33 | { | ||
| 34 | $parser->match(TokenType::T_IDENTIFIER); | ||
| 35 | $parser->match(TokenType::T_OPEN_PARENTHESIS); | ||
| 36 | |||
| 37 | $this->firstArithmetic = $parser->ArithmeticPrimary(); | ||
| 38 | $parser->match(TokenType::T_COMMA); | ||
| 39 | $this->secondArithmetic = $parser->ArithmeticPrimary(); | ||
| 40 | |||
| 41 | $parser->match(TokenType::T_CLOSE_PARENTHESIS); | ||
| 42 | } | ||
| 43 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/AST/Functions/ConcatFunction.php b/vendor/doctrine/orm/src/Query/AST/Functions/ConcatFunction.php new file mode 100644 index 0000000..5b8d696 --- /dev/null +++ b/vendor/doctrine/orm/src/Query/AST/Functions/ConcatFunction.php | |||
| @@ -0,0 +1,58 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Query\AST\Functions; | ||
| 6 | |||
| 7 | use Doctrine\ORM\Query\AST\Node; | ||
| 8 | use Doctrine\ORM\Query\Parser; | ||
| 9 | use Doctrine\ORM\Query\SqlWalker; | ||
| 10 | use Doctrine\ORM\Query\TokenType; | ||
| 11 | |||
| 12 | /** | ||
| 13 | * "CONCAT" "(" StringPrimary "," StringPrimary {"," StringPrimary }* ")" | ||
| 14 | * | ||
| 15 | * @link www.doctrine-project.org | ||
| 16 | */ | ||
| 17 | class ConcatFunction extends FunctionNode | ||
| 18 | { | ||
| 19 | public Node $firstStringPrimary; | ||
| 20 | public Node $secondStringPrimary; | ||
| 21 | |||
| 22 | /** @psalm-var list<Node> */ | ||
| 23 | public array $concatExpressions = []; | ||
| 24 | |||
| 25 | public function getSql(SqlWalker $sqlWalker): string | ||
| 26 | { | ||
| 27 | $platform = $sqlWalker->getConnection()->getDatabasePlatform(); | ||
| 28 | |||
| 29 | $args = []; | ||
| 30 | |||
| 31 | foreach ($this->concatExpressions as $expression) { | ||
| 32 | $args[] = $sqlWalker->walkStringPrimary($expression); | ||
| 33 | } | ||
| 34 | |||
| 35 | return $platform->getConcatExpression(...$args); | ||
| 36 | } | ||
| 37 | |||
| 38 | public function parse(Parser $parser): void | ||
| 39 | { | ||
| 40 | $parser->match(TokenType::T_IDENTIFIER); | ||
| 41 | $parser->match(TokenType::T_OPEN_PARENTHESIS); | ||
| 42 | |||
| 43 | $this->firstStringPrimary = $parser->StringPrimary(); | ||
| 44 | $this->concatExpressions[] = $this->firstStringPrimary; | ||
| 45 | |||
| 46 | $parser->match(TokenType::T_COMMA); | ||
| 47 | |||
| 48 | $this->secondStringPrimary = $parser->StringPrimary(); | ||
| 49 | $this->concatExpressions[] = $this->secondStringPrimary; | ||
| 50 | |||
| 51 | while ($parser->getLexer()->isNextToken(TokenType::T_COMMA)) { | ||
| 52 | $parser->match(TokenType::T_COMMA); | ||
| 53 | $this->concatExpressions[] = $parser->StringPrimary(); | ||
| 54 | } | ||
| 55 | |||
| 56 | $parser->match(TokenType::T_CLOSE_PARENTHESIS); | ||
| 57 | } | ||
| 58 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/AST/Functions/CountFunction.php b/vendor/doctrine/orm/src/Query/AST/Functions/CountFunction.php new file mode 100644 index 0000000..dc926a5 --- /dev/null +++ b/vendor/doctrine/orm/src/Query/AST/Functions/CountFunction.php | |||
| @@ -0,0 +1,35 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Query\AST\Functions; | ||
| 6 | |||
| 7 | use Doctrine\DBAL\Types\Type; | ||
| 8 | use Doctrine\DBAL\Types\Types; | ||
| 9 | use Doctrine\ORM\Query\AST\AggregateExpression; | ||
| 10 | use Doctrine\ORM\Query\AST\TypedExpression; | ||
| 11 | use Doctrine\ORM\Query\Parser; | ||
| 12 | use Doctrine\ORM\Query\SqlWalker; | ||
| 13 | |||
| 14 | /** | ||
| 15 | * "COUNT" "(" ["DISTINCT"] StringPrimary ")" | ||
| 16 | */ | ||
| 17 | final class CountFunction extends FunctionNode implements TypedExpression | ||
| 18 | { | ||
| 19 | private AggregateExpression $aggregateExpression; | ||
| 20 | |||
| 21 | public function getSql(SqlWalker $sqlWalker): string | ||
| 22 | { | ||
| 23 | return $this->aggregateExpression->dispatch($sqlWalker); | ||
| 24 | } | ||
| 25 | |||
| 26 | public function parse(Parser $parser): void | ||
| 27 | { | ||
| 28 | $this->aggregateExpression = $parser->AggregateExpression(); | ||
| 29 | } | ||
| 30 | |||
| 31 | public function getReturnType(): Type | ||
| 32 | { | ||
| 33 | return Type::getType(Types::INTEGER); | ||
| 34 | } | ||
| 35 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/AST/Functions/CurrentDateFunction.php b/vendor/doctrine/orm/src/Query/AST/Functions/CurrentDateFunction.php new file mode 100644 index 0000000..cec9632 --- /dev/null +++ b/vendor/doctrine/orm/src/Query/AST/Functions/CurrentDateFunction.php | |||
| @@ -0,0 +1,29 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Query\AST\Functions; | ||
| 6 | |||
| 7 | use Doctrine\ORM\Query\Parser; | ||
| 8 | use Doctrine\ORM\Query\SqlWalker; | ||
| 9 | use Doctrine\ORM\Query\TokenType; | ||
| 10 | |||
| 11 | /** | ||
| 12 | * "CURRENT_DATE" | ||
| 13 | * | ||
| 14 | * @link www.doctrine-project.org | ||
| 15 | */ | ||
| 16 | class CurrentDateFunction extends FunctionNode | ||
| 17 | { | ||
| 18 | public function getSql(SqlWalker $sqlWalker): string | ||
| 19 | { | ||
| 20 | return $sqlWalker->getConnection()->getDatabasePlatform()->getCurrentDateSQL(); | ||
| 21 | } | ||
| 22 | |||
| 23 | public function parse(Parser $parser): void | ||
| 24 | { | ||
| 25 | $parser->match(TokenType::T_IDENTIFIER); | ||
| 26 | $parser->match(TokenType::T_OPEN_PARENTHESIS); | ||
| 27 | $parser->match(TokenType::T_CLOSE_PARENTHESIS); | ||
| 28 | } | ||
| 29 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/AST/Functions/CurrentTimeFunction.php b/vendor/doctrine/orm/src/Query/AST/Functions/CurrentTimeFunction.php new file mode 100644 index 0000000..6473fce --- /dev/null +++ b/vendor/doctrine/orm/src/Query/AST/Functions/CurrentTimeFunction.php | |||
| @@ -0,0 +1,29 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Query\AST\Functions; | ||
| 6 | |||
| 7 | use Doctrine\ORM\Query\Parser; | ||
| 8 | use Doctrine\ORM\Query\SqlWalker; | ||
| 9 | use Doctrine\ORM\Query\TokenType; | ||
| 10 | |||
| 11 | /** | ||
| 12 | * "CURRENT_TIME" | ||
| 13 | * | ||
| 14 | * @link www.doctrine-project.org | ||
| 15 | */ | ||
| 16 | class CurrentTimeFunction extends FunctionNode | ||
| 17 | { | ||
| 18 | public function getSql(SqlWalker $sqlWalker): string | ||
| 19 | { | ||
| 20 | return $sqlWalker->getConnection()->getDatabasePlatform()->getCurrentTimeSQL(); | ||
| 21 | } | ||
| 22 | |||
| 23 | public function parse(Parser $parser): void | ||
| 24 | { | ||
| 25 | $parser->match(TokenType::T_IDENTIFIER); | ||
| 26 | $parser->match(TokenType::T_OPEN_PARENTHESIS); | ||
| 27 | $parser->match(TokenType::T_CLOSE_PARENTHESIS); | ||
| 28 | } | ||
| 29 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/AST/Functions/CurrentTimestampFunction.php b/vendor/doctrine/orm/src/Query/AST/Functions/CurrentTimestampFunction.php new file mode 100644 index 0000000..edcd27c --- /dev/null +++ b/vendor/doctrine/orm/src/Query/AST/Functions/CurrentTimestampFunction.php | |||
| @@ -0,0 +1,29 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Query\AST\Functions; | ||
| 6 | |||
| 7 | use Doctrine\ORM\Query\Parser; | ||
| 8 | use Doctrine\ORM\Query\SqlWalker; | ||
| 9 | use Doctrine\ORM\Query\TokenType; | ||
| 10 | |||
| 11 | /** | ||
| 12 | * "CURRENT_TIMESTAMP" | ||
| 13 | * | ||
| 14 | * @link www.doctrine-project.org | ||
| 15 | */ | ||
| 16 | class CurrentTimestampFunction extends FunctionNode | ||
| 17 | { | ||
| 18 | public function getSql(SqlWalker $sqlWalker): string | ||
| 19 | { | ||
| 20 | return $sqlWalker->getConnection()->getDatabasePlatform()->getCurrentTimestampSQL(); | ||
| 21 | } | ||
| 22 | |||
| 23 | public function parse(Parser $parser): void | ||
| 24 | { | ||
| 25 | $parser->match(TokenType::T_IDENTIFIER); | ||
| 26 | $parser->match(TokenType::T_OPEN_PARENTHESIS); | ||
| 27 | $parser->match(TokenType::T_CLOSE_PARENTHESIS); | ||
| 28 | } | ||
| 29 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/AST/Functions/DateAddFunction.php b/vendor/doctrine/orm/src/Query/AST/Functions/DateAddFunction.php new file mode 100644 index 0000000..12920dc --- /dev/null +++ b/vendor/doctrine/orm/src/Query/AST/Functions/DateAddFunction.php | |||
| @@ -0,0 +1,83 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Query\AST\Functions; | ||
| 6 | |||
| 7 | use Doctrine\ORM\Query\AST\ASTException; | ||
| 8 | use Doctrine\ORM\Query\AST\Node; | ||
| 9 | use Doctrine\ORM\Query\Parser; | ||
| 10 | use Doctrine\ORM\Query\QueryException; | ||
| 11 | use Doctrine\ORM\Query\SqlWalker; | ||
| 12 | use Doctrine\ORM\Query\TokenType; | ||
| 13 | |||
| 14 | use function strtolower; | ||
| 15 | |||
| 16 | /** | ||
| 17 | * "DATE_ADD" "(" ArithmeticPrimary "," ArithmeticPrimary "," StringPrimary ")" | ||
| 18 | * | ||
| 19 | * @link www.doctrine-project.org | ||
| 20 | */ | ||
| 21 | class DateAddFunction extends FunctionNode | ||
| 22 | { | ||
| 23 | public Node $firstDateExpression; | ||
| 24 | public Node $intervalExpression; | ||
| 25 | public Node $unit; | ||
| 26 | |||
| 27 | public function getSql(SqlWalker $sqlWalker): string | ||
| 28 | { | ||
| 29 | return match (strtolower((string) $this->unit->value)) { | ||
| 30 | 'second' => $sqlWalker->getConnection()->getDatabasePlatform()->getDateAddSecondsExpression( | ||
| 31 | $this->firstDateExpression->dispatch($sqlWalker), | ||
| 32 | $this->dispatchIntervalExpression($sqlWalker), | ||
| 33 | ), | ||
| 34 | 'minute' => $sqlWalker->getConnection()->getDatabasePlatform()->getDateAddMinutesExpression( | ||
| 35 | $this->firstDateExpression->dispatch($sqlWalker), | ||
| 36 | $this->dispatchIntervalExpression($sqlWalker), | ||
| 37 | ), | ||
| 38 | 'hour' => $sqlWalker->getConnection()->getDatabasePlatform()->getDateAddHourExpression( | ||
| 39 | $this->firstDateExpression->dispatch($sqlWalker), | ||
| 40 | $this->dispatchIntervalExpression($sqlWalker), | ||
| 41 | ), | ||
| 42 | 'day' => $sqlWalker->getConnection()->getDatabasePlatform()->getDateAddDaysExpression( | ||
| 43 | $this->firstDateExpression->dispatch($sqlWalker), | ||
| 44 | $this->dispatchIntervalExpression($sqlWalker), | ||
| 45 | ), | ||
| 46 | 'week' => $sqlWalker->getConnection()->getDatabasePlatform()->getDateAddWeeksExpression( | ||
| 47 | $this->firstDateExpression->dispatch($sqlWalker), | ||
| 48 | $this->dispatchIntervalExpression($sqlWalker), | ||
| 49 | ), | ||
| 50 | 'month' => $sqlWalker->getConnection()->getDatabasePlatform()->getDateAddMonthExpression( | ||
| 51 | $this->firstDateExpression->dispatch($sqlWalker), | ||
| 52 | $this->dispatchIntervalExpression($sqlWalker), | ||
| 53 | ), | ||
| 54 | 'year' => $sqlWalker->getConnection()->getDatabasePlatform()->getDateAddYearsExpression( | ||
| 55 | $this->firstDateExpression->dispatch($sqlWalker), | ||
| 56 | $this->dispatchIntervalExpression($sqlWalker), | ||
| 57 | ), | ||
| 58 | default => throw QueryException::semanticalError( | ||
| 59 | 'DATE_ADD() only supports units of type second, minute, hour, day, week, month and year.', | ||
| 60 | ), | ||
| 61 | }; | ||
| 62 | } | ||
| 63 | |||
| 64 | /** @throws ASTException */ | ||
| 65 | private function dispatchIntervalExpression(SqlWalker $sqlWalker): string | ||
| 66 | { | ||
| 67 | return $this->intervalExpression->dispatch($sqlWalker); | ||
| 68 | } | ||
| 69 | |||
| 70 | public function parse(Parser $parser): void | ||
| 71 | { | ||
| 72 | $parser->match(TokenType::T_IDENTIFIER); | ||
| 73 | $parser->match(TokenType::T_OPEN_PARENTHESIS); | ||
| 74 | |||
| 75 | $this->firstDateExpression = $parser->ArithmeticPrimary(); | ||
| 76 | $parser->match(TokenType::T_COMMA); | ||
| 77 | $this->intervalExpression = $parser->ArithmeticPrimary(); | ||
| 78 | $parser->match(TokenType::T_COMMA); | ||
| 79 | $this->unit = $parser->StringPrimary(); | ||
| 80 | |||
| 81 | $parser->match(TokenType::T_CLOSE_PARENTHESIS); | ||
| 82 | } | ||
| 83 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/AST/Functions/DateDiffFunction.php b/vendor/doctrine/orm/src/Query/AST/Functions/DateDiffFunction.php new file mode 100644 index 0000000..55598c0 --- /dev/null +++ b/vendor/doctrine/orm/src/Query/AST/Functions/DateDiffFunction.php | |||
| @@ -0,0 +1,41 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Query\AST\Functions; | ||
| 6 | |||
| 7 | use Doctrine\ORM\Query\AST\Node; | ||
| 8 | use Doctrine\ORM\Query\Parser; | ||
| 9 | use Doctrine\ORM\Query\SqlWalker; | ||
| 10 | use Doctrine\ORM\Query\TokenType; | ||
| 11 | |||
| 12 | /** | ||
| 13 | * "DATE_DIFF" "(" ArithmeticPrimary "," ArithmeticPrimary ")" | ||
| 14 | * | ||
| 15 | * @link www.doctrine-project.org | ||
| 16 | */ | ||
| 17 | class DateDiffFunction extends FunctionNode | ||
| 18 | { | ||
| 19 | public Node $date1; | ||
| 20 | public Node $date2; | ||
| 21 | |||
| 22 | public function getSql(SqlWalker $sqlWalker): string | ||
| 23 | { | ||
| 24 | return $sqlWalker->getConnection()->getDatabasePlatform()->getDateDiffExpression( | ||
| 25 | $this->date1->dispatch($sqlWalker), | ||
| 26 | $this->date2->dispatch($sqlWalker), | ||
| 27 | ); | ||
| 28 | } | ||
| 29 | |||
| 30 | public function parse(Parser $parser): void | ||
| 31 | { | ||
| 32 | $parser->match(TokenType::T_IDENTIFIER); | ||
| 33 | $parser->match(TokenType::T_OPEN_PARENTHESIS); | ||
| 34 | |||
| 35 | $this->date1 = $parser->ArithmeticPrimary(); | ||
| 36 | $parser->match(TokenType::T_COMMA); | ||
| 37 | $this->date2 = $parser->ArithmeticPrimary(); | ||
| 38 | |||
| 39 | $parser->match(TokenType::T_CLOSE_PARENTHESIS); | ||
| 40 | } | ||
| 41 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/AST/Functions/DateSubFunction.php b/vendor/doctrine/orm/src/Query/AST/Functions/DateSubFunction.php new file mode 100644 index 0000000..5363680 --- /dev/null +++ b/vendor/doctrine/orm/src/Query/AST/Functions/DateSubFunction.php | |||
| @@ -0,0 +1,62 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Query\AST\Functions; | ||
| 6 | |||
| 7 | use Doctrine\ORM\Query\AST\ASTException; | ||
| 8 | use Doctrine\ORM\Query\QueryException; | ||
| 9 | use Doctrine\ORM\Query\SqlWalker; | ||
| 10 | |||
| 11 | use function strtolower; | ||
| 12 | |||
| 13 | /** | ||
| 14 | * "DATE_SUB(date1, interval, unit)" | ||
| 15 | * | ||
| 16 | * @link www.doctrine-project.org | ||
| 17 | */ | ||
| 18 | class DateSubFunction extends DateAddFunction | ||
| 19 | { | ||
| 20 | public function getSql(SqlWalker $sqlWalker): string | ||
| 21 | { | ||
| 22 | return match (strtolower((string) $this->unit->value)) { | ||
| 23 | 'second' => $sqlWalker->getConnection()->getDatabasePlatform()->getDateSubSecondsExpression( | ||
| 24 | $this->firstDateExpression->dispatch($sqlWalker), | ||
| 25 | $this->dispatchIntervalExpression($sqlWalker), | ||
| 26 | ), | ||
| 27 | 'minute' => $sqlWalker->getConnection()->getDatabasePlatform()->getDateSubMinutesExpression( | ||
| 28 | $this->firstDateExpression->dispatch($sqlWalker), | ||
| 29 | $this->dispatchIntervalExpression($sqlWalker), | ||
| 30 | ), | ||
| 31 | 'hour' => $sqlWalker->getConnection()->getDatabasePlatform()->getDateSubHourExpression( | ||
| 32 | $this->firstDateExpression->dispatch($sqlWalker), | ||
| 33 | $this->dispatchIntervalExpression($sqlWalker), | ||
| 34 | ), | ||
| 35 | 'day' => $sqlWalker->getConnection()->getDatabasePlatform()->getDateSubDaysExpression( | ||
| 36 | $this->firstDateExpression->dispatch($sqlWalker), | ||
| 37 | $this->dispatchIntervalExpression($sqlWalker), | ||
| 38 | ), | ||
| 39 | 'week' => $sqlWalker->getConnection()->getDatabasePlatform()->getDateSubWeeksExpression( | ||
| 40 | $this->firstDateExpression->dispatch($sqlWalker), | ||
| 41 | $this->dispatchIntervalExpression($sqlWalker), | ||
| 42 | ), | ||
| 43 | 'month' => $sqlWalker->getConnection()->getDatabasePlatform()->getDateSubMonthExpression( | ||
| 44 | $this->firstDateExpression->dispatch($sqlWalker), | ||
| 45 | $this->dispatchIntervalExpression($sqlWalker), | ||
| 46 | ), | ||
| 47 | 'year' => $sqlWalker->getConnection()->getDatabasePlatform()->getDateSubYearsExpression( | ||
| 48 | $this->firstDateExpression->dispatch($sqlWalker), | ||
| 49 | $this->dispatchIntervalExpression($sqlWalker), | ||
| 50 | ), | ||
| 51 | default => throw QueryException::semanticalError( | ||
| 52 | 'DATE_SUB() only supports units of type second, minute, hour, day, week, month and year.', | ||
| 53 | ), | ||
| 54 | }; | ||
| 55 | } | ||
| 56 | |||
| 57 | /** @throws ASTException */ | ||
| 58 | private function dispatchIntervalExpression(SqlWalker $sqlWalker): string | ||
| 59 | { | ||
| 60 | return $this->intervalExpression->dispatch($sqlWalker); | ||
| 61 | } | ||
| 62 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/AST/Functions/FunctionNode.php b/vendor/doctrine/orm/src/Query/AST/Functions/FunctionNode.php new file mode 100644 index 0000000..4cc549e --- /dev/null +++ b/vendor/doctrine/orm/src/Query/AST/Functions/FunctionNode.php | |||
| @@ -0,0 +1,32 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Query\AST\Functions; | ||
| 6 | |||
| 7 | use Doctrine\ORM\Query\AST\Node; | ||
| 8 | use Doctrine\ORM\Query\Parser; | ||
| 9 | use Doctrine\ORM\Query\SqlWalker; | ||
| 10 | |||
| 11 | /** | ||
| 12 | * Abstract Function Node. | ||
| 13 | * | ||
| 14 | * @link www.doctrine-project.org | ||
| 15 | * | ||
| 16 | * @psalm-consistent-constructor | ||
| 17 | */ | ||
| 18 | abstract class FunctionNode extends Node | ||
| 19 | { | ||
| 20 | public function __construct(public string $name) | ||
| 21 | { | ||
| 22 | } | ||
| 23 | |||
| 24 | abstract public function getSql(SqlWalker $sqlWalker): string; | ||
| 25 | |||
| 26 | public function dispatch(SqlWalker $sqlWalker): string | ||
| 27 | { | ||
| 28 | return $sqlWalker->walkFunction($this); | ||
| 29 | } | ||
| 30 | |||
| 31 | abstract public function parse(Parser $parser): void; | ||
| 32 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/AST/Functions/IdentityFunction.php b/vendor/doctrine/orm/src/Query/AST/Functions/IdentityFunction.php new file mode 100644 index 0000000..1dd1bf5 --- /dev/null +++ b/vendor/doctrine/orm/src/Query/AST/Functions/IdentityFunction.php | |||
| @@ -0,0 +1,90 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Query\AST\Functions; | ||
| 6 | |||
| 7 | use Doctrine\ORM\Query\AST\PathExpression; | ||
| 8 | use Doctrine\ORM\Query\Parser; | ||
| 9 | use Doctrine\ORM\Query\QueryException; | ||
| 10 | use Doctrine\ORM\Query\SqlWalker; | ||
| 11 | use Doctrine\ORM\Query\TokenType; | ||
| 12 | |||
| 13 | use function assert; | ||
| 14 | use function reset; | ||
| 15 | use function sprintf; | ||
| 16 | |||
| 17 | /** | ||
| 18 | * "IDENTITY" "(" SingleValuedAssociationPathExpression {"," string} ")" | ||
| 19 | * | ||
| 20 | * @link www.doctrine-project.org | ||
| 21 | */ | ||
| 22 | class IdentityFunction extends FunctionNode | ||
| 23 | { | ||
| 24 | public PathExpression $pathExpression; | ||
| 25 | |||
| 26 | public string|null $fieldMapping = null; | ||
| 27 | |||
| 28 | public function getSql(SqlWalker $sqlWalker): string | ||
| 29 | { | ||
| 30 | assert($this->pathExpression->field !== null); | ||
| 31 | $entityManager = $sqlWalker->getEntityManager(); | ||
| 32 | $platform = $entityManager->getConnection()->getDatabasePlatform(); | ||
| 33 | $quoteStrategy = $entityManager->getConfiguration()->getQuoteStrategy(); | ||
| 34 | $dqlAlias = $this->pathExpression->identificationVariable; | ||
| 35 | $assocField = $this->pathExpression->field; | ||
| 36 | $assoc = $sqlWalker->getMetadataForDqlAlias($dqlAlias)->associationMappings[$assocField]; | ||
| 37 | $targetEntity = $entityManager->getClassMetadata($assoc->targetEntity); | ||
| 38 | |||
| 39 | assert($assoc->isToOneOwningSide()); | ||
| 40 | $joinColumn = reset($assoc->joinColumns); | ||
| 41 | |||
| 42 | if ($this->fieldMapping !== null) { | ||
| 43 | if (! isset($targetEntity->fieldMappings[$this->fieldMapping])) { | ||
| 44 | throw new QueryException(sprintf('Undefined reference field mapping "%s"', $this->fieldMapping)); | ||
| 45 | } | ||
| 46 | |||
| 47 | $field = $targetEntity->fieldMappings[$this->fieldMapping]; | ||
| 48 | $joinColumn = null; | ||
| 49 | |||
| 50 | foreach ($assoc->joinColumns as $mapping) { | ||
| 51 | if ($mapping->referencedColumnName === $field->columnName) { | ||
| 52 | $joinColumn = $mapping; | ||
| 53 | |||
| 54 | break; | ||
| 55 | } | ||
| 56 | } | ||
| 57 | |||
| 58 | if ($joinColumn === null) { | ||
| 59 | throw new QueryException(sprintf('Unable to resolve the reference field mapping "%s"', $this->fieldMapping)); | ||
| 60 | } | ||
| 61 | } | ||
| 62 | |||
| 63 | // The table with the relation may be a subclass, so get the table name from the association definition | ||
| 64 | $tableName = $entityManager->getClassMetadata($assoc->sourceEntity)->getTableName(); | ||
| 65 | |||
| 66 | $tableAlias = $sqlWalker->getSQLTableAlias($tableName, $dqlAlias); | ||
| 67 | $columnName = $quoteStrategy->getJoinColumnName($joinColumn, $targetEntity, $platform); | ||
| 68 | |||
| 69 | return $tableAlias . '.' . $columnName; | ||
| 70 | } | ||
| 71 | |||
| 72 | public function parse(Parser $parser): void | ||
| 73 | { | ||
| 74 | $parser->match(TokenType::T_IDENTIFIER); | ||
| 75 | $parser->match(TokenType::T_OPEN_PARENTHESIS); | ||
| 76 | |||
| 77 | $this->pathExpression = $parser->SingleValuedAssociationPathExpression(); | ||
| 78 | |||
| 79 | if ($parser->getLexer()->isNextToken(TokenType::T_COMMA)) { | ||
| 80 | $parser->match(TokenType::T_COMMA); | ||
| 81 | $parser->match(TokenType::T_STRING); | ||
| 82 | |||
| 83 | $token = $parser->getLexer()->token; | ||
| 84 | assert($token !== null); | ||
| 85 | $this->fieldMapping = $token->value; | ||
| 86 | } | ||
| 87 | |||
| 88 | $parser->match(TokenType::T_CLOSE_PARENTHESIS); | ||
| 89 | } | ||
| 90 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/AST/Functions/LengthFunction.php b/vendor/doctrine/orm/src/Query/AST/Functions/LengthFunction.php new file mode 100644 index 0000000..3994918 --- /dev/null +++ b/vendor/doctrine/orm/src/Query/AST/Functions/LengthFunction.php | |||
| @@ -0,0 +1,45 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Query\AST\Functions; | ||
| 6 | |||
| 7 | use Doctrine\DBAL\Types\Type; | ||
| 8 | use Doctrine\DBAL\Types\Types; | ||
| 9 | use Doctrine\ORM\Query\AST\Node; | ||
| 10 | use Doctrine\ORM\Query\AST\TypedExpression; | ||
| 11 | use Doctrine\ORM\Query\Parser; | ||
| 12 | use Doctrine\ORM\Query\SqlWalker; | ||
| 13 | use Doctrine\ORM\Query\TokenType; | ||
| 14 | |||
| 15 | /** | ||
| 16 | * "LENGTH" "(" StringPrimary ")" | ||
| 17 | * | ||
| 18 | * @link www.doctrine-project.org | ||
| 19 | */ | ||
| 20 | class LengthFunction extends FunctionNode implements TypedExpression | ||
| 21 | { | ||
| 22 | public Node $stringPrimary; | ||
| 23 | |||
| 24 | public function getSql(SqlWalker $sqlWalker): string | ||
| 25 | { | ||
| 26 | return $sqlWalker->getConnection()->getDatabasePlatform()->getLengthExpression( | ||
| 27 | $sqlWalker->walkSimpleArithmeticExpression($this->stringPrimary), | ||
| 28 | ); | ||
| 29 | } | ||
| 30 | |||
| 31 | public function parse(Parser $parser): void | ||
| 32 | { | ||
| 33 | $parser->match(TokenType::T_IDENTIFIER); | ||
| 34 | $parser->match(TokenType::T_OPEN_PARENTHESIS); | ||
| 35 | |||
| 36 | $this->stringPrimary = $parser->StringPrimary(); | ||
| 37 | |||
| 38 | $parser->match(TokenType::T_CLOSE_PARENTHESIS); | ||
| 39 | } | ||
| 40 | |||
| 41 | public function getReturnType(): Type | ||
| 42 | { | ||
| 43 | return Type::getType(Types::INTEGER); | ||
| 44 | } | ||
| 45 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/AST/Functions/LocateFunction.php b/vendor/doctrine/orm/src/Query/AST/Functions/LocateFunction.php new file mode 100644 index 0000000..c0d3b4a --- /dev/null +++ b/vendor/doctrine/orm/src/Query/AST/Functions/LocateFunction.php | |||
| @@ -0,0 +1,62 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Query\AST\Functions; | ||
| 6 | |||
| 7 | use Doctrine\ORM\Query\AST\Node; | ||
| 8 | use Doctrine\ORM\Query\Parser; | ||
| 9 | use Doctrine\ORM\Query\SqlWalker; | ||
| 10 | use Doctrine\ORM\Query\TokenType; | ||
| 11 | |||
| 12 | /** | ||
| 13 | * "LOCATE" "(" StringPrimary "," StringPrimary ["," SimpleArithmeticExpression]")" | ||
| 14 | * | ||
| 15 | * @link www.doctrine-project.org | ||
| 16 | */ | ||
| 17 | class LocateFunction extends FunctionNode | ||
| 18 | { | ||
| 19 | public Node|string $firstStringPrimary; | ||
| 20 | public Node|string $secondStringPrimary; | ||
| 21 | |||
| 22 | public Node|string|bool $simpleArithmeticExpression = false; | ||
| 23 | |||
| 24 | public function getSql(SqlWalker $sqlWalker): string | ||
| 25 | { | ||
| 26 | $platform = $sqlWalker->getConnection()->getDatabasePlatform(); | ||
| 27 | |||
| 28 | $firstString = $sqlWalker->walkStringPrimary($this->firstStringPrimary); | ||
| 29 | $secondString = $sqlWalker->walkStringPrimary($this->secondStringPrimary); | ||
| 30 | |||
| 31 | if ($this->simpleArithmeticExpression) { | ||
| 32 | return $platform->getLocateExpression( | ||
| 33 | $secondString, | ||
| 34 | $firstString, | ||
| 35 | $sqlWalker->walkSimpleArithmeticExpression($this->simpleArithmeticExpression), | ||
| 36 | ); | ||
| 37 | } | ||
| 38 | |||
| 39 | return $platform->getLocateExpression($secondString, $firstString); | ||
| 40 | } | ||
| 41 | |||
| 42 | public function parse(Parser $parser): void | ||
| 43 | { | ||
| 44 | $parser->match(TokenType::T_IDENTIFIER); | ||
| 45 | $parser->match(TokenType::T_OPEN_PARENTHESIS); | ||
| 46 | |||
| 47 | $this->firstStringPrimary = $parser->StringPrimary(); | ||
| 48 | |||
| 49 | $parser->match(TokenType::T_COMMA); | ||
| 50 | |||
| 51 | $this->secondStringPrimary = $parser->StringPrimary(); | ||
| 52 | |||
| 53 | $lexer = $parser->getLexer(); | ||
| 54 | if ($lexer->isNextToken(TokenType::T_COMMA)) { | ||
| 55 | $parser->match(TokenType::T_COMMA); | ||
| 56 | |||
| 57 | $this->simpleArithmeticExpression = $parser->SimpleArithmeticExpression(); | ||
| 58 | } | ||
| 59 | |||
| 60 | $parser->match(TokenType::T_CLOSE_PARENTHESIS); | ||
| 61 | } | ||
| 62 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/AST/Functions/LowerFunction.php b/vendor/doctrine/orm/src/Query/AST/Functions/LowerFunction.php new file mode 100644 index 0000000..8ae337a --- /dev/null +++ b/vendor/doctrine/orm/src/Query/AST/Functions/LowerFunction.php | |||
| @@ -0,0 +1,40 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Query\AST\Functions; | ||
| 6 | |||
| 7 | use Doctrine\ORM\Query\AST\Node; | ||
| 8 | use Doctrine\ORM\Query\Parser; | ||
| 9 | use Doctrine\ORM\Query\SqlWalker; | ||
| 10 | use Doctrine\ORM\Query\TokenType; | ||
| 11 | |||
| 12 | use function sprintf; | ||
| 13 | |||
| 14 | /** | ||
| 15 | * "LOWER" "(" StringPrimary ")" | ||
| 16 | * | ||
| 17 | * @link www.doctrine-project.org | ||
| 18 | */ | ||
| 19 | class LowerFunction extends FunctionNode | ||
| 20 | { | ||
| 21 | public Node $stringPrimary; | ||
| 22 | |||
| 23 | public function getSql(SqlWalker $sqlWalker): string | ||
| 24 | { | ||
| 25 | return sprintf( | ||
| 26 | 'LOWER(%s)', | ||
| 27 | $sqlWalker->walkSimpleArithmeticExpression($this->stringPrimary), | ||
| 28 | ); | ||
| 29 | } | ||
| 30 | |||
| 31 | public function parse(Parser $parser): void | ||
| 32 | { | ||
| 33 | $parser->match(TokenType::T_IDENTIFIER); | ||
| 34 | $parser->match(TokenType::T_OPEN_PARENTHESIS); | ||
| 35 | |||
| 36 | $this->stringPrimary = $parser->StringPrimary(); | ||
| 37 | |||
| 38 | $parser->match(TokenType::T_CLOSE_PARENTHESIS); | ||
| 39 | } | ||
| 40 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/AST/Functions/MaxFunction.php b/vendor/doctrine/orm/src/Query/AST/Functions/MaxFunction.php new file mode 100644 index 0000000..8a6eecf --- /dev/null +++ b/vendor/doctrine/orm/src/Query/AST/Functions/MaxFunction.php | |||
| @@ -0,0 +1,27 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Query\AST\Functions; | ||
| 6 | |||
| 7 | use Doctrine\ORM\Query\AST\AggregateExpression; | ||
| 8 | use Doctrine\ORM\Query\Parser; | ||
| 9 | use Doctrine\ORM\Query\SqlWalker; | ||
| 10 | |||
| 11 | /** | ||
| 12 | * "MAX" "(" ["DISTINCT"] StringPrimary ")" | ||
| 13 | */ | ||
| 14 | final class MaxFunction extends FunctionNode | ||
| 15 | { | ||
| 16 | private AggregateExpression $aggregateExpression; | ||
| 17 | |||
| 18 | public function getSql(SqlWalker $sqlWalker): string | ||
| 19 | { | ||
| 20 | return $this->aggregateExpression->dispatch($sqlWalker); | ||
| 21 | } | ||
| 22 | |||
| 23 | public function parse(Parser $parser): void | ||
| 24 | { | ||
| 25 | $this->aggregateExpression = $parser->AggregateExpression(); | ||
| 26 | } | ||
| 27 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/AST/Functions/MinFunction.php b/vendor/doctrine/orm/src/Query/AST/Functions/MinFunction.php new file mode 100644 index 0000000..98d73a2 --- /dev/null +++ b/vendor/doctrine/orm/src/Query/AST/Functions/MinFunction.php | |||
| @@ -0,0 +1,27 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Query\AST\Functions; | ||
| 6 | |||
| 7 | use Doctrine\ORM\Query\AST\AggregateExpression; | ||
| 8 | use Doctrine\ORM\Query\Parser; | ||
| 9 | use Doctrine\ORM\Query\SqlWalker; | ||
| 10 | |||
| 11 | /** | ||
| 12 | * "MIN" "(" ["DISTINCT"] StringPrimary ")" | ||
| 13 | */ | ||
| 14 | final class MinFunction extends FunctionNode | ||
| 15 | { | ||
| 16 | private AggregateExpression $aggregateExpression; | ||
| 17 | |||
| 18 | public function getSql(SqlWalker $sqlWalker): string | ||
| 19 | { | ||
| 20 | return $this->aggregateExpression->dispatch($sqlWalker); | ||
| 21 | } | ||
| 22 | |||
| 23 | public function parse(Parser $parser): void | ||
| 24 | { | ||
| 25 | $this->aggregateExpression = $parser->AggregateExpression(); | ||
| 26 | } | ||
| 27 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/AST/Functions/ModFunction.php b/vendor/doctrine/orm/src/Query/AST/Functions/ModFunction.php new file mode 100644 index 0000000..7c1af0b --- /dev/null +++ b/vendor/doctrine/orm/src/Query/AST/Functions/ModFunction.php | |||
| @@ -0,0 +1,43 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Query\AST\Functions; | ||
| 6 | |||
| 7 | use Doctrine\ORM\Query\AST\Node; | ||
| 8 | use Doctrine\ORM\Query\Parser; | ||
| 9 | use Doctrine\ORM\Query\SqlWalker; | ||
| 10 | use Doctrine\ORM\Query\TokenType; | ||
| 11 | |||
| 12 | /** | ||
| 13 | * "MOD" "(" SimpleArithmeticExpression "," SimpleArithmeticExpression ")" | ||
| 14 | * | ||
| 15 | * @link www.doctrine-project.org | ||
| 16 | */ | ||
| 17 | class ModFunction extends FunctionNode | ||
| 18 | { | ||
| 19 | public Node|string $firstSimpleArithmeticExpression; | ||
| 20 | public Node|string $secondSimpleArithmeticExpression; | ||
| 21 | |||
| 22 | public function getSql(SqlWalker $sqlWalker): string | ||
| 23 | { | ||
| 24 | return $sqlWalker->getConnection()->getDatabasePlatform()->getModExpression( | ||
| 25 | $sqlWalker->walkSimpleArithmeticExpression($this->firstSimpleArithmeticExpression), | ||
| 26 | $sqlWalker->walkSimpleArithmeticExpression($this->secondSimpleArithmeticExpression), | ||
| 27 | ); | ||
| 28 | } | ||
| 29 | |||
| 30 | public function parse(Parser $parser): void | ||
| 31 | { | ||
| 32 | $parser->match(TokenType::T_IDENTIFIER); | ||
| 33 | $parser->match(TokenType::T_OPEN_PARENTHESIS); | ||
| 34 | |||
| 35 | $this->firstSimpleArithmeticExpression = $parser->SimpleArithmeticExpression(); | ||
| 36 | |||
| 37 | $parser->match(TokenType::T_COMMA); | ||
| 38 | |||
| 39 | $this->secondSimpleArithmeticExpression = $parser->SimpleArithmeticExpression(); | ||
| 40 | |||
| 41 | $parser->match(TokenType::T_CLOSE_PARENTHESIS); | ||
| 42 | } | ||
| 43 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/AST/Functions/SizeFunction.php b/vendor/doctrine/orm/src/Query/AST/Functions/SizeFunction.php new file mode 100644 index 0000000..87ee713 --- /dev/null +++ b/vendor/doctrine/orm/src/Query/AST/Functions/SizeFunction.php | |||
| @@ -0,0 +1,113 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Query\AST\Functions; | ||
| 6 | |||
| 7 | use Doctrine\ORM\Query\AST\PathExpression; | ||
| 8 | use Doctrine\ORM\Query\Parser; | ||
| 9 | use Doctrine\ORM\Query\SqlWalker; | ||
| 10 | use Doctrine\ORM\Query\TokenType; | ||
| 11 | |||
| 12 | use function assert; | ||
| 13 | |||
| 14 | /** | ||
| 15 | * "SIZE" "(" CollectionValuedPathExpression ")" | ||
| 16 | * | ||
| 17 | * @link www.doctrine-project.org | ||
| 18 | */ | ||
| 19 | class SizeFunction extends FunctionNode | ||
| 20 | { | ||
| 21 | public PathExpression $collectionPathExpression; | ||
| 22 | |||
| 23 | /** | ||
| 24 | * @inheritdoc | ||
| 25 | * @todo If the collection being counted is already joined, the SQL can be simpler (more efficient). | ||
| 26 | */ | ||
| 27 | public function getSql(SqlWalker $sqlWalker): string | ||
| 28 | { | ||
| 29 | assert($this->collectionPathExpression->field !== null); | ||
| 30 | $entityManager = $sqlWalker->getEntityManager(); | ||
| 31 | $platform = $entityManager->getConnection()->getDatabasePlatform(); | ||
| 32 | $quoteStrategy = $entityManager->getConfiguration()->getQuoteStrategy(); | ||
| 33 | $dqlAlias = $this->collectionPathExpression->identificationVariable; | ||
| 34 | $assocField = $this->collectionPathExpression->field; | ||
| 35 | |||
| 36 | $class = $sqlWalker->getMetadataForDqlAlias($dqlAlias); | ||
| 37 | $assoc = $class->associationMappings[$assocField]; | ||
| 38 | $sql = 'SELECT COUNT(*) FROM '; | ||
| 39 | |||
| 40 | if ($assoc->isOneToMany()) { | ||
| 41 | $targetClass = $entityManager->getClassMetadata($assoc->targetEntity); | ||
| 42 | $targetTableAlias = $sqlWalker->getSQLTableAlias($targetClass->getTableName()); | ||
| 43 | $sourceTableAlias = $sqlWalker->getSQLTableAlias($class->getTableName(), $dqlAlias); | ||
| 44 | |||
| 45 | $sql .= $quoteStrategy->getTableName($targetClass, $platform) . ' ' . $targetTableAlias . ' WHERE '; | ||
| 46 | |||
| 47 | $owningAssoc = $targetClass->associationMappings[$assoc->mappedBy]; | ||
| 48 | assert($owningAssoc->isManyToOne()); | ||
| 49 | |||
| 50 | $first = true; | ||
| 51 | |||
| 52 | foreach ($owningAssoc->targetToSourceKeyColumns as $targetColumn => $sourceColumn) { | ||
| 53 | if ($first) { | ||
| 54 | $first = false; | ||
| 55 | } else { | ||
| 56 | $sql .= ' AND '; | ||
| 57 | } | ||
| 58 | |||
| 59 | $sql .= $targetTableAlias . '.' . $sourceColumn | ||
| 60 | . ' = ' | ||
| 61 | . $sourceTableAlias . '.' . $quoteStrategy->getColumnName($class->fieldNames[$targetColumn], $class, $platform); | ||
| 62 | } | ||
| 63 | } else { // many-to-many | ||
| 64 | assert($assoc->isManyToMany()); | ||
| 65 | $owningAssoc = $entityManager->getMetadataFactory()->getOwningSide($assoc); | ||
| 66 | $joinTable = $owningAssoc->joinTable; | ||
| 67 | |||
| 68 | // SQL table aliases | ||
| 69 | $joinTableAlias = $sqlWalker->getSQLTableAlias($joinTable->name); | ||
| 70 | $sourceTableAlias = $sqlWalker->getSQLTableAlias($class->getTableName(), $dqlAlias); | ||
| 71 | |||
| 72 | // join to target table | ||
| 73 | $targetClass = $entityManager->getClassMetadata($assoc->targetEntity); | ||
| 74 | $sql .= $quoteStrategy->getJoinTableName($owningAssoc, $targetClass, $platform) . ' ' . $joinTableAlias . ' WHERE '; | ||
| 75 | |||
| 76 | $joinColumns = $assoc->isOwningSide() | ||
| 77 | ? $joinTable->joinColumns | ||
| 78 | : $joinTable->inverseJoinColumns; | ||
| 79 | |||
| 80 | $first = true; | ||
| 81 | |||
| 82 | foreach ($joinColumns as $joinColumn) { | ||
| 83 | if ($first) { | ||
| 84 | $first = false; | ||
| 85 | } else { | ||
| 86 | $sql .= ' AND '; | ||
| 87 | } | ||
| 88 | |||
| 89 | $sourceColumnName = $quoteStrategy->getColumnName( | ||
| 90 | $class->fieldNames[$joinColumn->referencedColumnName], | ||
| 91 | $class, | ||
| 92 | $platform, | ||
| 93 | ); | ||
| 94 | |||
| 95 | $sql .= $joinTableAlias . '.' . $joinColumn->name | ||
| 96 | . ' = ' | ||
| 97 | . $sourceTableAlias . '.' . $sourceColumnName; | ||
| 98 | } | ||
| 99 | } | ||
| 100 | |||
| 101 | return '(' . $sql . ')'; | ||
| 102 | } | ||
| 103 | |||
| 104 | public function parse(Parser $parser): void | ||
| 105 | { | ||
| 106 | $parser->match(TokenType::T_IDENTIFIER); | ||
| 107 | $parser->match(TokenType::T_OPEN_PARENTHESIS); | ||
| 108 | |||
| 109 | $this->collectionPathExpression = $parser->CollectionValuedPathExpression(); | ||
| 110 | |||
| 111 | $parser->match(TokenType::T_CLOSE_PARENTHESIS); | ||
| 112 | } | ||
| 113 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/AST/Functions/SqrtFunction.php b/vendor/doctrine/orm/src/Query/AST/Functions/SqrtFunction.php new file mode 100644 index 0000000..e643663 --- /dev/null +++ b/vendor/doctrine/orm/src/Query/AST/Functions/SqrtFunction.php | |||
| @@ -0,0 +1,40 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Query\AST\Functions; | ||
| 6 | |||
| 7 | use Doctrine\ORM\Query\AST\Node; | ||
| 8 | use Doctrine\ORM\Query\Parser; | ||
| 9 | use Doctrine\ORM\Query\SqlWalker; | ||
| 10 | use Doctrine\ORM\Query\TokenType; | ||
| 11 | |||
| 12 | use function sprintf; | ||
| 13 | |||
| 14 | /** | ||
| 15 | * "SQRT" "(" SimpleArithmeticExpression ")" | ||
| 16 | * | ||
| 17 | * @link www.doctrine-project.org | ||
| 18 | */ | ||
| 19 | class SqrtFunction extends FunctionNode | ||
| 20 | { | ||
| 21 | public Node|string $simpleArithmeticExpression; | ||
| 22 | |||
| 23 | public function getSql(SqlWalker $sqlWalker): string | ||
| 24 | { | ||
| 25 | return sprintf( | ||
| 26 | 'SQRT(%s)', | ||
| 27 | $sqlWalker->walkSimpleArithmeticExpression($this->simpleArithmeticExpression), | ||
| 28 | ); | ||
| 29 | } | ||
| 30 | |||
| 31 | public function parse(Parser $parser): void | ||
| 32 | { | ||
| 33 | $parser->match(TokenType::T_IDENTIFIER); | ||
| 34 | $parser->match(TokenType::T_OPEN_PARENTHESIS); | ||
| 35 | |||
| 36 | $this->simpleArithmeticExpression = $parser->SimpleArithmeticExpression(); | ||
| 37 | |||
| 38 | $parser->match(TokenType::T_CLOSE_PARENTHESIS); | ||
| 39 | } | ||
| 40 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/AST/Functions/SubstringFunction.php b/vendor/doctrine/orm/src/Query/AST/Functions/SubstringFunction.php new file mode 100644 index 0000000..5744f08 --- /dev/null +++ b/vendor/doctrine/orm/src/Query/AST/Functions/SubstringFunction.php | |||
| @@ -0,0 +1,58 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Query\AST\Functions; | ||
| 6 | |||
| 7 | use Doctrine\ORM\Query\AST\Node; | ||
| 8 | use Doctrine\ORM\Query\Parser; | ||
| 9 | use Doctrine\ORM\Query\SqlWalker; | ||
| 10 | use Doctrine\ORM\Query\TokenType; | ||
| 11 | |||
| 12 | /** | ||
| 13 | * "SUBSTRING" "(" StringPrimary "," SimpleArithmeticExpression "," SimpleArithmeticExpression ")" | ||
| 14 | * | ||
| 15 | * @link www.doctrine-project.org | ||
| 16 | */ | ||
| 17 | class SubstringFunction extends FunctionNode | ||
| 18 | { | ||
| 19 | public Node $stringPrimary; | ||
| 20 | |||
| 21 | public Node|string $firstSimpleArithmeticExpression; | ||
| 22 | public Node|string|null $secondSimpleArithmeticExpression = null; | ||
| 23 | |||
| 24 | public function getSql(SqlWalker $sqlWalker): string | ||
| 25 | { | ||
| 26 | $optionalSecondSimpleArithmeticExpression = null; | ||
| 27 | if ($this->secondSimpleArithmeticExpression !== null) { | ||
| 28 | $optionalSecondSimpleArithmeticExpression = $sqlWalker->walkSimpleArithmeticExpression($this->secondSimpleArithmeticExpression); | ||
| 29 | } | ||
| 30 | |||
| 31 | return $sqlWalker->getConnection()->getDatabasePlatform()->getSubstringExpression( | ||
| 32 | $sqlWalker->walkStringPrimary($this->stringPrimary), | ||
| 33 | $sqlWalker->walkSimpleArithmeticExpression($this->firstSimpleArithmeticExpression), | ||
| 34 | $optionalSecondSimpleArithmeticExpression, | ||
| 35 | ); | ||
| 36 | } | ||
| 37 | |||
| 38 | public function parse(Parser $parser): void | ||
| 39 | { | ||
| 40 | $parser->match(TokenType::T_IDENTIFIER); | ||
| 41 | $parser->match(TokenType::T_OPEN_PARENTHESIS); | ||
| 42 | |||
| 43 | $this->stringPrimary = $parser->StringPrimary(); | ||
| 44 | |||
| 45 | $parser->match(TokenType::T_COMMA); | ||
| 46 | |||
| 47 | $this->firstSimpleArithmeticExpression = $parser->SimpleArithmeticExpression(); | ||
| 48 | |||
| 49 | $lexer = $parser->getLexer(); | ||
| 50 | if ($lexer->isNextToken(TokenType::T_COMMA)) { | ||
| 51 | $parser->match(TokenType::T_COMMA); | ||
| 52 | |||
| 53 | $this->secondSimpleArithmeticExpression = $parser->SimpleArithmeticExpression(); | ||
| 54 | } | ||
| 55 | |||
| 56 | $parser->match(TokenType::T_CLOSE_PARENTHESIS); | ||
| 57 | } | ||
| 58 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/AST/Functions/SumFunction.php b/vendor/doctrine/orm/src/Query/AST/Functions/SumFunction.php new file mode 100644 index 0000000..588dce9 --- /dev/null +++ b/vendor/doctrine/orm/src/Query/AST/Functions/SumFunction.php | |||
| @@ -0,0 +1,27 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Query\AST\Functions; | ||
| 6 | |||
| 7 | use Doctrine\ORM\Query\AST\AggregateExpression; | ||
| 8 | use Doctrine\ORM\Query\Parser; | ||
| 9 | use Doctrine\ORM\Query\SqlWalker; | ||
| 10 | |||
| 11 | /** | ||
| 12 | * "SUM" "(" ["DISTINCT"] StringPrimary ")" | ||
| 13 | */ | ||
| 14 | final class SumFunction extends FunctionNode | ||
| 15 | { | ||
| 16 | private AggregateExpression $aggregateExpression; | ||
| 17 | |||
| 18 | public function getSql(SqlWalker $sqlWalker): string | ||
| 19 | { | ||
| 20 | return $this->aggregateExpression->dispatch($sqlWalker); | ||
| 21 | } | ||
| 22 | |||
| 23 | public function parse(Parser $parser): void | ||
| 24 | { | ||
| 25 | $this->aggregateExpression = $parser->AggregateExpression(); | ||
| 26 | } | ||
| 27 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/AST/Functions/TrimFunction.php b/vendor/doctrine/orm/src/Query/AST/Functions/TrimFunction.php new file mode 100644 index 0000000..e0a3e99 --- /dev/null +++ b/vendor/doctrine/orm/src/Query/AST/Functions/TrimFunction.php | |||
| @@ -0,0 +1,119 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Query\AST\Functions; | ||
| 6 | |||
| 7 | use Doctrine\DBAL\Platforms\TrimMode; | ||
| 8 | use Doctrine\ORM\Query\AST\Node; | ||
| 9 | use Doctrine\ORM\Query\Parser; | ||
| 10 | use Doctrine\ORM\Query\SqlWalker; | ||
| 11 | use Doctrine\ORM\Query\TokenType; | ||
| 12 | |||
| 13 | use function assert; | ||
| 14 | use function strcasecmp; | ||
| 15 | |||
| 16 | /** | ||
| 17 | * "TRIM" "(" [["LEADING" | "TRAILING" | "BOTH"] [char] "FROM"] StringPrimary ")" | ||
| 18 | * | ||
| 19 | * @link www.doctrine-project.org | ||
| 20 | */ | ||
| 21 | class TrimFunction extends FunctionNode | ||
| 22 | { | ||
| 23 | public bool $leading = false; | ||
| 24 | public bool $trailing = false; | ||
| 25 | public bool $both = false; | ||
| 26 | public string|false $trimChar = false; | ||
| 27 | public Node $stringPrimary; | ||
| 28 | |||
| 29 | public function getSql(SqlWalker $sqlWalker): string | ||
| 30 | { | ||
| 31 | $stringPrimary = $sqlWalker->walkStringPrimary($this->stringPrimary); | ||
| 32 | $platform = $sqlWalker->getConnection()->getDatabasePlatform(); | ||
| 33 | $trimMode = $this->getTrimMode(); | ||
| 34 | |||
| 35 | if ($this->trimChar !== false) { | ||
| 36 | return $platform->getTrimExpression( | ||
| 37 | $stringPrimary, | ||
| 38 | $trimMode, | ||
| 39 | $platform->quoteStringLiteral($this->trimChar), | ||
| 40 | ); | ||
| 41 | } | ||
| 42 | |||
| 43 | return $platform->getTrimExpression($stringPrimary, $trimMode); | ||
| 44 | } | ||
| 45 | |||
| 46 | public function parse(Parser $parser): void | ||
| 47 | { | ||
| 48 | $lexer = $parser->getLexer(); | ||
| 49 | |||
| 50 | $parser->match(TokenType::T_IDENTIFIER); | ||
| 51 | $parser->match(TokenType::T_OPEN_PARENTHESIS); | ||
| 52 | |||
| 53 | $this->parseTrimMode($parser); | ||
| 54 | |||
| 55 | if ($lexer->isNextToken(TokenType::T_STRING)) { | ||
| 56 | $parser->match(TokenType::T_STRING); | ||
| 57 | |||
| 58 | assert($lexer->token !== null); | ||
| 59 | $this->trimChar = $lexer->token->value; | ||
| 60 | } | ||
| 61 | |||
| 62 | if ($this->leading || $this->trailing || $this->both || ($this->trimChar !== false)) { | ||
| 63 | $parser->match(TokenType::T_FROM); | ||
| 64 | } | ||
| 65 | |||
| 66 | $this->stringPrimary = $parser->StringPrimary(); | ||
| 67 | |||
| 68 | $parser->match(TokenType::T_CLOSE_PARENTHESIS); | ||
| 69 | } | ||
| 70 | |||
| 71 | /** @psalm-return TrimMode::* */ | ||
| 72 | private function getTrimMode(): TrimMode|int | ||
| 73 | { | ||
| 74 | if ($this->leading) { | ||
| 75 | return TrimMode::LEADING; | ||
| 76 | } | ||
| 77 | |||
| 78 | if ($this->trailing) { | ||
| 79 | return TrimMode::TRAILING; | ||
| 80 | } | ||
| 81 | |||
| 82 | if ($this->both) { | ||
| 83 | return TrimMode::BOTH; | ||
| 84 | } | ||
| 85 | |||
| 86 | return TrimMode::UNSPECIFIED; | ||
| 87 | } | ||
| 88 | |||
| 89 | private function parseTrimMode(Parser $parser): void | ||
| 90 | { | ||
| 91 | $lexer = $parser->getLexer(); | ||
| 92 | assert($lexer->lookahead !== null); | ||
| 93 | $value = $lexer->lookahead->value; | ||
| 94 | |||
| 95 | if (strcasecmp('leading', $value) === 0) { | ||
| 96 | $parser->match(TokenType::T_LEADING); | ||
| 97 | |||
| 98 | $this->leading = true; | ||
| 99 | |||
| 100 | return; | ||
| 101 | } | ||
| 102 | |||
| 103 | if (strcasecmp('trailing', $value) === 0) { | ||
| 104 | $parser->match(TokenType::T_TRAILING); | ||
| 105 | |||
| 106 | $this->trailing = true; | ||
| 107 | |||
| 108 | return; | ||
| 109 | } | ||
| 110 | |||
| 111 | if (strcasecmp('both', $value) === 0) { | ||
| 112 | $parser->match(TokenType::T_BOTH); | ||
| 113 | |||
| 114 | $this->both = true; | ||
| 115 | |||
| 116 | return; | ||
| 117 | } | ||
| 118 | } | ||
| 119 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/AST/Functions/UpperFunction.php b/vendor/doctrine/orm/src/Query/AST/Functions/UpperFunction.php new file mode 100644 index 0000000..1ecef66 --- /dev/null +++ b/vendor/doctrine/orm/src/Query/AST/Functions/UpperFunction.php | |||
| @@ -0,0 +1,40 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Query\AST\Functions; | ||
| 6 | |||
| 7 | use Doctrine\ORM\Query\AST\Node; | ||
| 8 | use Doctrine\ORM\Query\Parser; | ||
| 9 | use Doctrine\ORM\Query\SqlWalker; | ||
| 10 | use Doctrine\ORM\Query\TokenType; | ||
| 11 | |||
| 12 | use function sprintf; | ||
| 13 | |||
| 14 | /** | ||
| 15 | * "UPPER" "(" StringPrimary ")" | ||
| 16 | * | ||
| 17 | * @link www.doctrine-project.org | ||
| 18 | */ | ||
| 19 | class UpperFunction extends FunctionNode | ||
| 20 | { | ||
| 21 | public Node $stringPrimary; | ||
| 22 | |||
| 23 | public function getSql(SqlWalker $sqlWalker): string | ||
| 24 | { | ||
| 25 | return sprintf( | ||
| 26 | 'UPPER(%s)', | ||
| 27 | $sqlWalker->walkSimpleArithmeticExpression($this->stringPrimary), | ||
| 28 | ); | ||
| 29 | } | ||
| 30 | |||
| 31 | public function parse(Parser $parser): void | ||
| 32 | { | ||
| 33 | $parser->match(TokenType::T_IDENTIFIER); | ||
| 34 | $parser->match(TokenType::T_OPEN_PARENTHESIS); | ||
| 35 | |||
| 36 | $this->stringPrimary = $parser->StringPrimary(); | ||
| 37 | |||
| 38 | $parser->match(TokenType::T_CLOSE_PARENTHESIS); | ||
| 39 | } | ||
| 40 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/AST/GeneralCaseExpression.php b/vendor/doctrine/orm/src/Query/AST/GeneralCaseExpression.php new file mode 100644 index 0000000..39d760a --- /dev/null +++ b/vendor/doctrine/orm/src/Query/AST/GeneralCaseExpression.php | |||
| @@ -0,0 +1,27 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Query\AST; | ||
| 6 | |||
| 7 | use Doctrine\ORM\Query\SqlWalker; | ||
| 8 | |||
| 9 | /** | ||
| 10 | * GeneralCaseExpression ::= "CASE" WhenClause {WhenClause}* "ELSE" ScalarExpression "END" | ||
| 11 | * | ||
| 12 | * @link www.doctrine-project.org | ||
| 13 | */ | ||
| 14 | class GeneralCaseExpression extends Node | ||
| 15 | { | ||
| 16 | /** @param mixed[] $whenClauses */ | ||
| 17 | public function __construct( | ||
| 18 | public array $whenClauses, | ||
| 19 | public mixed $elseScalarExpression = null, | ||
| 20 | ) { | ||
| 21 | } | ||
| 22 | |||
| 23 | public function dispatch(SqlWalker $walker): string | ||
| 24 | { | ||
| 25 | return $walker->walkGeneralCaseExpression($this); | ||
| 26 | } | ||
| 27 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/AST/GroupByClause.php b/vendor/doctrine/orm/src/Query/AST/GroupByClause.php new file mode 100644 index 0000000..eb0f1b9 --- /dev/null +++ b/vendor/doctrine/orm/src/Query/AST/GroupByClause.php | |||
| @@ -0,0 +1,20 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Query\AST; | ||
| 6 | |||
| 7 | use Doctrine\ORM\Query\SqlWalker; | ||
| 8 | |||
| 9 | class GroupByClause extends Node | ||
| 10 | { | ||
| 11 | /** @param mixed[] $groupByItems */ | ||
| 12 | public function __construct(public array $groupByItems) | ||
| 13 | { | ||
| 14 | } | ||
| 15 | |||
| 16 | public function dispatch(SqlWalker $walker): string | ||
| 17 | { | ||
| 18 | return $walker->walkGroupByClause($this); | ||
| 19 | } | ||
| 20 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/AST/HavingClause.php b/vendor/doctrine/orm/src/Query/AST/HavingClause.php new file mode 100644 index 0000000..0d4d821 --- /dev/null +++ b/vendor/doctrine/orm/src/Query/AST/HavingClause.php | |||
| @@ -0,0 +1,19 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Query\AST; | ||
| 6 | |||
| 7 | use Doctrine\ORM\Query\SqlWalker; | ||
| 8 | |||
| 9 | class HavingClause extends Node | ||
| 10 | { | ||
| 11 | public function __construct(public ConditionalExpression|Phase2OptimizableConditional $conditionalExpression) | ||
| 12 | { | ||
| 13 | } | ||
| 14 | |||
| 15 | public function dispatch(SqlWalker $walker): string | ||
| 16 | { | ||
| 17 | return $walker->walkHavingClause($this); | ||
| 18 | } | ||
| 19 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/AST/IdentificationVariableDeclaration.php b/vendor/doctrine/orm/src/Query/AST/IdentificationVariableDeclaration.php new file mode 100644 index 0000000..c4c7cca --- /dev/null +++ b/vendor/doctrine/orm/src/Query/AST/IdentificationVariableDeclaration.php | |||
| @@ -0,0 +1,28 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Query\AST; | ||
| 6 | |||
| 7 | use Doctrine\ORM\Query\SqlWalker; | ||
| 8 | |||
| 9 | /** | ||
| 10 | * IdentificationVariableDeclaration ::= RangeVariableDeclaration [IndexBy] {JoinVariableDeclaration}* | ||
| 11 | * | ||
| 12 | * @link www.doctrine-project.org | ||
| 13 | */ | ||
| 14 | class IdentificationVariableDeclaration extends Node | ||
| 15 | { | ||
| 16 | /** @param mixed[] $joins */ | ||
| 17 | public function __construct( | ||
| 18 | public RangeVariableDeclaration|null $rangeVariableDeclaration = null, | ||
| 19 | public IndexBy|null $indexBy = null, | ||
| 20 | public array $joins = [], | ||
| 21 | ) { | ||
| 22 | } | ||
| 23 | |||
| 24 | public function dispatch(SqlWalker $walker): string | ||
| 25 | { | ||
| 26 | return $walker->walkIdentificationVariableDeclaration($this); | ||
| 27 | } | ||
| 28 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/AST/InListExpression.php b/vendor/doctrine/orm/src/Query/AST/InListExpression.php new file mode 100644 index 0000000..dc0f32b --- /dev/null +++ b/vendor/doctrine/orm/src/Query/AST/InListExpression.php | |||
| @@ -0,0 +1,23 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Query\AST; | ||
| 6 | |||
| 7 | use Doctrine\ORM\Query\SqlWalker; | ||
| 8 | |||
| 9 | class InListExpression extends Node | ||
| 10 | { | ||
| 11 | /** @param non-empty-list<mixed> $literals */ | ||
| 12 | public function __construct( | ||
| 13 | public ArithmeticExpression $expression, | ||
| 14 | public array $literals, | ||
| 15 | public bool $not = false, | ||
| 16 | ) { | ||
| 17 | } | ||
| 18 | |||
| 19 | public function dispatch(SqlWalker $walker): string | ||
| 20 | { | ||
| 21 | return $walker->walkInListExpression($this); | ||
| 22 | } | ||
| 23 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/AST/InSubselectExpression.php b/vendor/doctrine/orm/src/Query/AST/InSubselectExpression.php new file mode 100644 index 0000000..1128285 --- /dev/null +++ b/vendor/doctrine/orm/src/Query/AST/InSubselectExpression.php | |||
| @@ -0,0 +1,22 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Query\AST; | ||
| 6 | |||
| 7 | use Doctrine\ORM\Query\SqlWalker; | ||
| 8 | |||
| 9 | class InSubselectExpression extends Node | ||
| 10 | { | ||
| 11 | public function __construct( | ||
| 12 | public ArithmeticExpression $expression, | ||
| 13 | public Subselect $subselect, | ||
| 14 | public bool $not = false, | ||
| 15 | ) { | ||
| 16 | } | ||
| 17 | |||
| 18 | public function dispatch(SqlWalker $walker): string | ||
| 19 | { | ||
| 20 | return $walker->walkInSubselectExpression($this); | ||
| 21 | } | ||
| 22 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/AST/IndexBy.php b/vendor/doctrine/orm/src/Query/AST/IndexBy.php new file mode 100644 index 0000000..3d90265 --- /dev/null +++ b/vendor/doctrine/orm/src/Query/AST/IndexBy.php | |||
| @@ -0,0 +1,26 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Query\AST; | ||
| 6 | |||
| 7 | use Doctrine\ORM\Query\SqlWalker; | ||
| 8 | |||
| 9 | /** | ||
| 10 | * IndexBy ::= "INDEX" "BY" SingleValuedPathExpression | ||
| 11 | * | ||
| 12 | * @link www.doctrine-project.org | ||
| 13 | */ | ||
| 14 | class IndexBy extends Node | ||
| 15 | { | ||
| 16 | public function __construct(public PathExpression $singleValuedPathExpression) | ||
| 17 | { | ||
| 18 | } | ||
| 19 | |||
| 20 | public function dispatch(SqlWalker $walker): string | ||
| 21 | { | ||
| 22 | $walker->walkIndexBy($this); | ||
| 23 | |||
| 24 | return ''; | ||
| 25 | } | ||
| 26 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/AST/InputParameter.php b/vendor/doctrine/orm/src/Query/AST/InputParameter.php new file mode 100644 index 0000000..a8e0a3b --- /dev/null +++ b/vendor/doctrine/orm/src/Query/AST/InputParameter.php | |||
| @@ -0,0 +1,35 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Query\AST; | ||
| 6 | |||
| 7 | use Doctrine\ORM\Query\QueryException; | ||
| 8 | use Doctrine\ORM\Query\SqlWalker; | ||
| 9 | |||
| 10 | use function is_numeric; | ||
| 11 | use function strlen; | ||
| 12 | use function substr; | ||
| 13 | |||
| 14 | class InputParameter extends Node | ||
| 15 | { | ||
| 16 | public bool $isNamed; | ||
| 17 | public string $name; | ||
| 18 | |||
| 19 | /** @throws QueryException */ | ||
| 20 | public function __construct(string $value) | ||
| 21 | { | ||
| 22 | if (strlen($value) === 1) { | ||
| 23 | throw QueryException::invalidParameterFormat($value); | ||
| 24 | } | ||
| 25 | |||
| 26 | $param = substr($value, 1); | ||
| 27 | $this->isNamed = ! is_numeric($param); | ||
| 28 | $this->name = $param; | ||
| 29 | } | ||
| 30 | |||
| 31 | public function dispatch(SqlWalker $walker): string | ||
| 32 | { | ||
| 33 | return $walker->walkInputParameter($this); | ||
| 34 | } | ||
| 35 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/AST/InstanceOfExpression.php b/vendor/doctrine/orm/src/Query/AST/InstanceOfExpression.php new file mode 100644 index 0000000..3a4e75f --- /dev/null +++ b/vendor/doctrine/orm/src/Query/AST/InstanceOfExpression.php | |||
| @@ -0,0 +1,29 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Query\AST; | ||
| 6 | |||
| 7 | use Doctrine\ORM\Query\SqlWalker; | ||
| 8 | |||
| 9 | /** | ||
| 10 | * InstanceOfExpression ::= IdentificationVariable ["NOT"] "INSTANCE" ["OF"] (InstanceOfParameter | "(" InstanceOfParameter {"," InstanceOfParameter}* ")") | ||
| 11 | * InstanceOfParameter ::= AbstractSchemaName | InputParameter | ||
| 12 | * | ||
| 13 | * @link www.doctrine-project.org | ||
| 14 | */ | ||
| 15 | class InstanceOfExpression extends Node | ||
| 16 | { | ||
| 17 | /** @param non-empty-list<InputParameter|string> $value */ | ||
| 18 | public function __construct( | ||
| 19 | public string $identificationVariable, | ||
| 20 | public array $value, | ||
| 21 | public bool $not = false, | ||
| 22 | ) { | ||
| 23 | } | ||
| 24 | |||
| 25 | public function dispatch(SqlWalker $walker): string | ||
| 26 | { | ||
| 27 | return $walker->walkInstanceOfExpression($this); | ||
| 28 | } | ||
| 29 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/AST/Join.php b/vendor/doctrine/orm/src/Query/AST/Join.php new file mode 100644 index 0000000..34ce830 --- /dev/null +++ b/vendor/doctrine/orm/src/Query/AST/Join.php | |||
| @@ -0,0 +1,34 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Query\AST; | ||
| 6 | |||
| 7 | use Doctrine\ORM\Query\SqlWalker; | ||
| 8 | |||
| 9 | /** | ||
| 10 | * Join ::= ["LEFT" ["OUTER"] | "INNER"] "JOIN" JoinAssociationPathExpression | ||
| 11 | * ["AS"] AliasIdentificationVariable [("ON" | "WITH") ConditionalExpression] | ||
| 12 | * | ||
| 13 | * @link www.doctrine-project.org | ||
| 14 | */ | ||
| 15 | class Join extends Node | ||
| 16 | { | ||
| 17 | final public const JOIN_TYPE_LEFT = 1; | ||
| 18 | final public const JOIN_TYPE_LEFTOUTER = 2; | ||
| 19 | final public const JOIN_TYPE_INNER = 3; | ||
| 20 | |||
| 21 | public ConditionalExpression|Phase2OptimizableConditional|null $conditionalExpression = null; | ||
| 22 | |||
| 23 | /** @psalm-param self::JOIN_TYPE_* $joinType */ | ||
| 24 | public function __construct( | ||
| 25 | public int $joinType, | ||
| 26 | public Node|null $joinAssociationDeclaration = null, | ||
| 27 | ) { | ||
| 28 | } | ||
| 29 | |||
| 30 | public function dispatch(SqlWalker $walker): string | ||
| 31 | { | ||
| 32 | return $walker->walkJoin($this); | ||
| 33 | } | ||
| 34 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/AST/JoinAssociationDeclaration.php b/vendor/doctrine/orm/src/Query/AST/JoinAssociationDeclaration.php new file mode 100644 index 0000000..e08d7f5 --- /dev/null +++ b/vendor/doctrine/orm/src/Query/AST/JoinAssociationDeclaration.php | |||
| @@ -0,0 +1,27 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Query\AST; | ||
| 6 | |||
| 7 | use Doctrine\ORM\Query\SqlWalker; | ||
| 8 | |||
| 9 | /** | ||
| 10 | * JoinAssociationDeclaration ::= JoinAssociationPathExpression ["AS"] AliasIdentificationVariable | ||
| 11 | * | ||
| 12 | * @link www.doctrine-project.org | ||
| 13 | */ | ||
| 14 | class JoinAssociationDeclaration extends Node | ||
| 15 | { | ||
| 16 | public function __construct( | ||
| 17 | public JoinAssociationPathExpression $joinAssociationPathExpression, | ||
| 18 | public string $aliasIdentificationVariable, | ||
| 19 | public IndexBy|null $indexBy, | ||
| 20 | ) { | ||
| 21 | } | ||
| 22 | |||
| 23 | public function dispatch(SqlWalker $walker): string | ||
| 24 | { | ||
| 25 | return $walker->walkJoinAssociationDeclaration($this); | ||
| 26 | } | ||
| 27 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/AST/JoinAssociationPathExpression.php b/vendor/doctrine/orm/src/Query/AST/JoinAssociationPathExpression.php new file mode 100644 index 0000000..230be36 --- /dev/null +++ b/vendor/doctrine/orm/src/Query/AST/JoinAssociationPathExpression.php | |||
| @@ -0,0 +1,19 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Query\AST; | ||
| 6 | |||
| 7 | /** | ||
| 8 | * JoinAssociationPathExpression ::= IdentificationVariable "." (SingleValuedAssociationField | CollectionValuedAssociationField) | ||
| 9 | * | ||
| 10 | * @link www.doctrine-project.org | ||
| 11 | */ | ||
| 12 | class JoinAssociationPathExpression extends Node | ||
| 13 | { | ||
| 14 | public function __construct( | ||
| 15 | public string $identificationVariable, | ||
| 16 | public string $associationField, | ||
| 17 | ) { | ||
| 18 | } | ||
| 19 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/AST/JoinClassPathExpression.php b/vendor/doctrine/orm/src/Query/AST/JoinClassPathExpression.php new file mode 100644 index 0000000..cc92782 --- /dev/null +++ b/vendor/doctrine/orm/src/Query/AST/JoinClassPathExpression.php | |||
| @@ -0,0 +1,26 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Query\AST; | ||
| 6 | |||
| 7 | use Doctrine\ORM\Query\SqlWalker; | ||
| 8 | |||
| 9 | /** | ||
| 10 | * JoinClassPathExpression ::= AbstractSchemaName ["AS"] AliasIdentificationVariable | ||
| 11 | * | ||
| 12 | * @link www.doctrine-project.org | ||
| 13 | */ | ||
| 14 | class JoinClassPathExpression extends Node | ||
| 15 | { | ||
| 16 | public function __construct( | ||
| 17 | public mixed $abstractSchemaName, | ||
| 18 | public mixed $aliasIdentificationVariable, | ||
| 19 | ) { | ||
| 20 | } | ||
| 21 | |||
| 22 | public function dispatch(SqlWalker $walker): string | ||
| 23 | { | ||
| 24 | return $walker->walkJoinPathExpression($this); | ||
| 25 | } | ||
| 26 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/AST/JoinVariableDeclaration.php b/vendor/doctrine/orm/src/Query/AST/JoinVariableDeclaration.php new file mode 100644 index 0000000..bf76695 --- /dev/null +++ b/vendor/doctrine/orm/src/Query/AST/JoinVariableDeclaration.php | |||
| @@ -0,0 +1,24 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Query\AST; | ||
| 6 | |||
| 7 | use Doctrine\ORM\Query\SqlWalker; | ||
| 8 | |||
| 9 | /** | ||
| 10 | * JoinVariableDeclaration ::= Join [IndexBy] | ||
| 11 | * | ||
| 12 | * @link www.doctrine-project.org | ||
| 13 | */ | ||
| 14 | class JoinVariableDeclaration extends Node | ||
| 15 | { | ||
| 16 | public function __construct(public Join $join, public IndexBy|null $indexBy) | ||
| 17 | { | ||
| 18 | } | ||
| 19 | |||
| 20 | public function dispatch(SqlWalker $walker): string | ||
| 21 | { | ||
| 22 | return $walker->walkJoinVariableDeclaration($this); | ||
| 23 | } | ||
| 24 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/AST/LikeExpression.php b/vendor/doctrine/orm/src/Query/AST/LikeExpression.php new file mode 100644 index 0000000..e3f67f8 --- /dev/null +++ b/vendor/doctrine/orm/src/Query/AST/LikeExpression.php | |||
| @@ -0,0 +1,29 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Query\AST; | ||
| 6 | |||
| 7 | use Doctrine\ORM\Query\AST\Functions\FunctionNode; | ||
| 8 | use Doctrine\ORM\Query\SqlWalker; | ||
| 9 | |||
| 10 | /** | ||
| 11 | * LikeExpression ::= StringExpression ["NOT"] "LIKE" string ["ESCAPE" char] | ||
| 12 | * | ||
| 13 | * @link www.doctrine-project.org | ||
| 14 | */ | ||
| 15 | class LikeExpression extends Node | ||
| 16 | { | ||
| 17 | public function __construct( | ||
| 18 | public Node|string $stringExpression, | ||
| 19 | public InputParameter|FunctionNode|PathExpression|Literal $stringPattern, | ||
| 20 | public Literal|null $escapeChar = null, | ||
| 21 | public bool $not = false, | ||
| 22 | ) { | ||
| 23 | } | ||
| 24 | |||
| 25 | public function dispatch(SqlWalker $walker): string | ||
| 26 | { | ||
| 27 | return $walker->walkLikeExpression($this); | ||
| 28 | } | ||
| 29 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/AST/Literal.php b/vendor/doctrine/orm/src/Query/AST/Literal.php new file mode 100644 index 0000000..9ec2036 --- /dev/null +++ b/vendor/doctrine/orm/src/Query/AST/Literal.php | |||
| @@ -0,0 +1,26 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Query\AST; | ||
| 6 | |||
| 7 | use Doctrine\ORM\Query\SqlWalker; | ||
| 8 | |||
| 9 | class Literal extends Node | ||
| 10 | { | ||
| 11 | final public const STRING = 1; | ||
| 12 | final public const BOOLEAN = 2; | ||
| 13 | final public const NUMERIC = 3; | ||
| 14 | |||
| 15 | /** @psalm-param self::* $type */ | ||
| 16 | public function __construct( | ||
| 17 | public int $type, | ||
| 18 | public mixed $value, | ||
| 19 | ) { | ||
| 20 | } | ||
| 21 | |||
| 22 | public function dispatch(SqlWalker $walker): string | ||
| 23 | { | ||
| 24 | return $walker->walkLiteral($this); | ||
| 25 | } | ||
| 26 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/AST/NewObjectExpression.php b/vendor/doctrine/orm/src/Query/AST/NewObjectExpression.php new file mode 100644 index 0000000..7383c48 --- /dev/null +++ b/vendor/doctrine/orm/src/Query/AST/NewObjectExpression.php | |||
| @@ -0,0 +1,25 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Query\AST; | ||
| 6 | |||
| 7 | use Doctrine\ORM\Query\SqlWalker; | ||
| 8 | |||
| 9 | /** | ||
| 10 | * NewObjectExpression ::= "NEW" IdentificationVariable "(" NewObjectArg {"," NewObjectArg}* ")" | ||
| 11 | * | ||
| 12 | * @link www.doctrine-project.org | ||
| 13 | */ | ||
| 14 | class NewObjectExpression extends Node | ||
| 15 | { | ||
| 16 | /** @param mixed[] $args */ | ||
| 17 | public function __construct(public string $className, public array $args) | ||
| 18 | { | ||
| 19 | } | ||
| 20 | |||
| 21 | public function dispatch(SqlWalker $walker): string | ||
| 22 | { | ||
| 23 | return $walker->walkNewObject($this); | ||
| 24 | } | ||
| 25 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/AST/Node.php b/vendor/doctrine/orm/src/Query/AST/Node.php new file mode 100644 index 0000000..cdb5855 --- /dev/null +++ b/vendor/doctrine/orm/src/Query/AST/Node.php | |||
| @@ -0,0 +1,85 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Query\AST; | ||
| 6 | |||
| 7 | use Doctrine\ORM\Query\SqlWalker; | ||
| 8 | use Stringable; | ||
| 9 | |||
| 10 | use function get_debug_type; | ||
| 11 | use function get_object_vars; | ||
| 12 | use function is_array; | ||
| 13 | use function is_object; | ||
| 14 | use function str_repeat; | ||
| 15 | use function var_export; | ||
| 16 | |||
| 17 | use const PHP_EOL; | ||
| 18 | |||
| 19 | /** | ||
| 20 | * Abstract class of an AST node. | ||
| 21 | * | ||
| 22 | * @link www.doctrine-project.org | ||
| 23 | */ | ||
| 24 | abstract class Node implements Stringable | ||
| 25 | { | ||
| 26 | /** | ||
| 27 | * Double-dispatch method, supposed to dispatch back to the walker. | ||
| 28 | * | ||
| 29 | * Implementation is not mandatory for all nodes. | ||
| 30 | * | ||
| 31 | * @throws ASTException | ||
| 32 | */ | ||
| 33 | public function dispatch(SqlWalker $walker): string | ||
| 34 | { | ||
| 35 | throw ASTException::noDispatchForNode($this); | ||
| 36 | } | ||
| 37 | |||
| 38 | /** | ||
| 39 | * Dumps the AST Node into a string representation for information purpose only. | ||
| 40 | */ | ||
| 41 | public function __toString(): string | ||
| 42 | { | ||
| 43 | return $this->dump($this); | ||
| 44 | } | ||
| 45 | |||
| 46 | public function dump(mixed $value): string | ||
| 47 | { | ||
| 48 | static $ident = 0; | ||
| 49 | |||
| 50 | $str = ''; | ||
| 51 | |||
| 52 | if ($value instanceof Node) { | ||
| 53 | $str .= get_debug_type($value) . '(' . PHP_EOL; | ||
| 54 | $props = get_object_vars($value); | ||
| 55 | |||
| 56 | foreach ($props as $name => $prop) { | ||
| 57 | $ident += 4; | ||
| 58 | $str .= str_repeat(' ', $ident) . '"' . $name . '": ' | ||
| 59 | . $this->dump($prop) . ',' . PHP_EOL; | ||
| 60 | $ident -= 4; | ||
| 61 | } | ||
| 62 | |||
| 63 | $str .= str_repeat(' ', $ident) . ')'; | ||
| 64 | } elseif (is_array($value)) { | ||
| 65 | $ident += 4; | ||
| 66 | $str .= 'array('; | ||
| 67 | $some = false; | ||
| 68 | |||
| 69 | foreach ($value as $k => $v) { | ||
| 70 | $str .= PHP_EOL . str_repeat(' ', $ident) . '"' | ||
| 71 | . $k . '" => ' . $this->dump($v) . ','; | ||
| 72 | $some = true; | ||
| 73 | } | ||
| 74 | |||
| 75 | $ident -= 4; | ||
| 76 | $str .= ($some ? PHP_EOL . str_repeat(' ', $ident) : '') . ')'; | ||
| 77 | } elseif (is_object($value)) { | ||
| 78 | $str .= 'instanceof(' . get_debug_type($value) . ')'; | ||
| 79 | } else { | ||
| 80 | $str .= var_export($value, true); | ||
| 81 | } | ||
| 82 | |||
| 83 | return $str; | ||
| 84 | } | ||
| 85 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/AST/NullComparisonExpression.php b/vendor/doctrine/orm/src/Query/AST/NullComparisonExpression.php new file mode 100644 index 0000000..e60cb04 --- /dev/null +++ b/vendor/doctrine/orm/src/Query/AST/NullComparisonExpression.php | |||
| @@ -0,0 +1,26 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Query\AST; | ||
| 6 | |||
| 7 | use Doctrine\ORM\Query\SqlWalker; | ||
| 8 | |||
| 9 | /** | ||
| 10 | * NullComparisonExpression ::= (SingleValuedPathExpression | InputParameter) "IS" ["NOT"] "NULL" | ||
| 11 | * | ||
| 12 | * @link www.doctrine-project.org | ||
| 13 | */ | ||
| 14 | class NullComparisonExpression extends Node | ||
| 15 | { | ||
| 16 | public function __construct( | ||
| 17 | public Node|string $expression, | ||
| 18 | public bool $not = false, | ||
| 19 | ) { | ||
| 20 | } | ||
| 21 | |||
| 22 | public function dispatch(SqlWalker $walker): string | ||
| 23 | { | ||
| 24 | return $walker->walkNullComparisonExpression($this); | ||
| 25 | } | ||
| 26 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/AST/NullIfExpression.php b/vendor/doctrine/orm/src/Query/AST/NullIfExpression.php new file mode 100644 index 0000000..6fffeeb --- /dev/null +++ b/vendor/doctrine/orm/src/Query/AST/NullIfExpression.php | |||
| @@ -0,0 +1,24 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Query\AST; | ||
| 6 | |||
| 7 | use Doctrine\ORM\Query\SqlWalker; | ||
| 8 | |||
| 9 | /** | ||
| 10 | * NullIfExpression ::= "NULLIF" "(" ScalarExpression "," ScalarExpression ")" | ||
| 11 | * | ||
| 12 | * @link www.doctrine-project.org | ||
| 13 | */ | ||
| 14 | class NullIfExpression extends Node | ||
| 15 | { | ||
| 16 | public function __construct(public mixed $firstExpression, public mixed $secondExpression) | ||
| 17 | { | ||
| 18 | } | ||
| 19 | |||
| 20 | public function dispatch(SqlWalker $walker): string | ||
| 21 | { | ||
| 22 | return $walker->walkNullIfExpression($this); | ||
| 23 | } | ||
| 24 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/AST/OrderByClause.php b/vendor/doctrine/orm/src/Query/AST/OrderByClause.php new file mode 100644 index 0000000..f6d7a67 --- /dev/null +++ b/vendor/doctrine/orm/src/Query/AST/OrderByClause.php | |||
| @@ -0,0 +1,25 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Query\AST; | ||
| 6 | |||
| 7 | use Doctrine\ORM\Query\SqlWalker; | ||
| 8 | |||
| 9 | /** | ||
| 10 | * OrderByClause ::= "ORDER" "BY" OrderByItem {"," OrderByItem}* | ||
| 11 | * | ||
| 12 | * @link www.doctrine-project.org | ||
| 13 | */ | ||
| 14 | class OrderByClause extends Node | ||
| 15 | { | ||
| 16 | /** @param OrderByItem[] $orderByItems */ | ||
| 17 | public function __construct(public array $orderByItems) | ||
| 18 | { | ||
| 19 | } | ||
| 20 | |||
| 21 | public function dispatch(SqlWalker $walker): string | ||
| 22 | { | ||
| 23 | return $walker->walkOrderByClause($this); | ||
| 24 | } | ||
| 25 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/AST/OrderByItem.php b/vendor/doctrine/orm/src/Query/AST/OrderByItem.php new file mode 100644 index 0000000..64b3f40 --- /dev/null +++ b/vendor/doctrine/orm/src/Query/AST/OrderByItem.php | |||
| @@ -0,0 +1,38 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Query\AST; | ||
| 6 | |||
| 7 | use Doctrine\ORM\Query\SqlWalker; | ||
| 8 | |||
| 9 | use function strtoupper; | ||
| 10 | |||
| 11 | /** | ||
| 12 | * OrderByItem ::= (ResultVariable | StateFieldPathExpression) ["ASC" | "DESC"] | ||
| 13 | * | ||
| 14 | * @link www.doctrine-project.org | ||
| 15 | */ | ||
| 16 | class OrderByItem extends Node | ||
| 17 | { | ||
| 18 | public string $type; | ||
| 19 | |||
| 20 | public function __construct(public mixed $expression) | ||
| 21 | { | ||
| 22 | } | ||
| 23 | |||
| 24 | public function isAsc(): bool | ||
| 25 | { | ||
| 26 | return strtoupper($this->type) === 'ASC'; | ||
| 27 | } | ||
| 28 | |||
| 29 | public function isDesc(): bool | ||
| 30 | { | ||
| 31 | return strtoupper($this->type) === 'DESC'; | ||
| 32 | } | ||
| 33 | |||
| 34 | public function dispatch(SqlWalker $walker): string | ||
| 35 | { | ||
| 36 | return $walker->walkOrderByItem($this); | ||
| 37 | } | ||
| 38 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/AST/ParenthesisExpression.php b/vendor/doctrine/orm/src/Query/AST/ParenthesisExpression.php new file mode 100644 index 0000000..cda6d19 --- /dev/null +++ b/vendor/doctrine/orm/src/Query/AST/ParenthesisExpression.php | |||
| @@ -0,0 +1,22 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Query\AST; | ||
| 6 | |||
| 7 | use Doctrine\ORM\Query\SqlWalker; | ||
| 8 | |||
| 9 | /** | ||
| 10 | * ParenthesisExpression ::= "(" ArithmeticPrimary ")" | ||
| 11 | */ | ||
| 12 | class ParenthesisExpression extends Node | ||
| 13 | { | ||
| 14 | public function __construct(public Node $expression) | ||
| 15 | { | ||
| 16 | } | ||
| 17 | |||
| 18 | public function dispatch(SqlWalker $walker): string | ||
| 19 | { | ||
| 20 | return $walker->walkParenthesisExpression($this); | ||
| 21 | } | ||
| 22 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/AST/PathExpression.php b/vendor/doctrine/orm/src/Query/AST/PathExpression.php new file mode 100644 index 0000000..4a56fcd --- /dev/null +++ b/vendor/doctrine/orm/src/Query/AST/PathExpression.php | |||
| @@ -0,0 +1,39 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Query\AST; | ||
| 6 | |||
| 7 | use Doctrine\ORM\Query\SqlWalker; | ||
| 8 | |||
| 9 | /** | ||
| 10 | * AssociationPathExpression ::= CollectionValuedPathExpression | SingleValuedAssociationPathExpression | ||
| 11 | * SingleValuedPathExpression ::= StateFieldPathExpression | SingleValuedAssociationPathExpression | ||
| 12 | * StateFieldPathExpression ::= SimpleStateFieldPathExpression | SimpleStateFieldAssociationPathExpression | ||
| 13 | * SingleValuedAssociationPathExpression ::= IdentificationVariable "." SingleValuedAssociationField | ||
| 14 | * CollectionValuedPathExpression ::= IdentificationVariable "." CollectionValuedAssociationField | ||
| 15 | * StateField ::= {EmbeddedClassStateField "."}* SimpleStateField | ||
| 16 | * SimpleStateFieldPathExpression ::= IdentificationVariable "." StateField | ||
| 17 | */ | ||
| 18 | class PathExpression extends Node | ||
| 19 | { | ||
| 20 | final public const TYPE_COLLECTION_VALUED_ASSOCIATION = 2; | ||
| 21 | final public const TYPE_SINGLE_VALUED_ASSOCIATION = 4; | ||
| 22 | final public const TYPE_STATE_FIELD = 8; | ||
| 23 | |||
| 24 | /** @psalm-var self::TYPE_*|null */ | ||
| 25 | public int|null $type = null; | ||
| 26 | |||
| 27 | /** @psalm-param int-mask-of<self::TYPE_*> $expectedType */ | ||
| 28 | public function __construct( | ||
| 29 | public int $expectedType, | ||
| 30 | public string $identificationVariable, | ||
| 31 | public string|null $field = null, | ||
| 32 | ) { | ||
| 33 | } | ||
| 34 | |||
| 35 | public function dispatch(SqlWalker $walker): string | ||
| 36 | { | ||
| 37 | return $walker->walkPathExpression($this); | ||
| 38 | } | ||
| 39 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/AST/Phase2OptimizableConditional.php b/vendor/doctrine/orm/src/Query/AST/Phase2OptimizableConditional.php new file mode 100644 index 0000000..276f8f8 --- /dev/null +++ b/vendor/doctrine/orm/src/Query/AST/Phase2OptimizableConditional.php | |||
| @@ -0,0 +1,17 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Query\AST; | ||
| 6 | |||
| 7 | /** | ||
| 8 | * Marks types that can be used in place of a ConditionalExpression as a phase | ||
| 9 | * 2 optimization. | ||
| 10 | * | ||
| 11 | * @internal | ||
| 12 | * | ||
| 13 | * @psalm-inheritors ConditionalPrimary|ConditionalFactor|ConditionalTerm | ||
| 14 | */ | ||
| 15 | interface Phase2OptimizableConditional | ||
| 16 | { | ||
| 17 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/AST/QuantifiedExpression.php b/vendor/doctrine/orm/src/Query/AST/QuantifiedExpression.php new file mode 100644 index 0000000..90331cd --- /dev/null +++ b/vendor/doctrine/orm/src/Query/AST/QuantifiedExpression.php | |||
| @@ -0,0 +1,43 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Query\AST; | ||
| 6 | |||
| 7 | use Doctrine\ORM\Query\SqlWalker; | ||
| 8 | |||
| 9 | use function strtoupper; | ||
| 10 | |||
| 11 | /** | ||
| 12 | * QuantifiedExpression ::= ("ALL" | "ANY" | "SOME") "(" Subselect ")" | ||
| 13 | * | ||
| 14 | * @link www.doctrine-project.org | ||
| 15 | */ | ||
| 16 | class QuantifiedExpression extends Node | ||
| 17 | { | ||
| 18 | public string $type; | ||
| 19 | |||
| 20 | public function __construct(public Subselect $subselect) | ||
| 21 | { | ||
| 22 | } | ||
| 23 | |||
| 24 | public function isAll(): bool | ||
| 25 | { | ||
| 26 | return strtoupper($this->type) === 'ALL'; | ||
| 27 | } | ||
| 28 | |||
| 29 | public function isAny(): bool | ||
| 30 | { | ||
| 31 | return strtoupper($this->type) === 'ANY'; | ||
| 32 | } | ||
| 33 | |||
| 34 | public function isSome(): bool | ||
| 35 | { | ||
| 36 | return strtoupper($this->type) === 'SOME'; | ||
| 37 | } | ||
| 38 | |||
| 39 | public function dispatch(SqlWalker $walker): string | ||
| 40 | { | ||
| 41 | return $walker->walkQuantifiedExpression($this); | ||
| 42 | } | ||
| 43 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/AST/RangeVariableDeclaration.php b/vendor/doctrine/orm/src/Query/AST/RangeVariableDeclaration.php new file mode 100644 index 0000000..59bd5c8 --- /dev/null +++ b/vendor/doctrine/orm/src/Query/AST/RangeVariableDeclaration.php | |||
| @@ -0,0 +1,27 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Query\AST; | ||
| 6 | |||
| 7 | use Doctrine\ORM\Query\SqlWalker; | ||
| 8 | |||
| 9 | /** | ||
| 10 | * RangeVariableDeclaration ::= AbstractSchemaName ["AS"] AliasIdentificationVariable | ||
| 11 | * | ||
| 12 | * @link www.doctrine-project.org | ||
| 13 | */ | ||
| 14 | class RangeVariableDeclaration extends Node | ||
| 15 | { | ||
| 16 | public function __construct( | ||
| 17 | public string $abstractSchemaName, | ||
| 18 | public string $aliasIdentificationVariable, | ||
| 19 | public bool $isRoot = true, | ||
| 20 | ) { | ||
| 21 | } | ||
| 22 | |||
| 23 | public function dispatch(SqlWalker $walker): string | ||
| 24 | { | ||
| 25 | return $walker->walkRangeVariableDeclaration($this); | ||
| 26 | } | ||
| 27 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/AST/SelectClause.php b/vendor/doctrine/orm/src/Query/AST/SelectClause.php new file mode 100644 index 0000000..ad50e67 --- /dev/null +++ b/vendor/doctrine/orm/src/Query/AST/SelectClause.php | |||
| @@ -0,0 +1,27 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Query\AST; | ||
| 6 | |||
| 7 | use Doctrine\ORM\Query\SqlWalker; | ||
| 8 | |||
| 9 | /** | ||
| 10 | * SelectClause = "SELECT" ["DISTINCT"] SelectExpression {"," SelectExpression} | ||
| 11 | * | ||
| 12 | * @link www.doctrine-project.org | ||
| 13 | */ | ||
| 14 | class SelectClause extends Node | ||
| 15 | { | ||
| 16 | /** @param mixed[] $selectExpressions */ | ||
| 17 | public function __construct( | ||
| 18 | public array $selectExpressions, | ||
| 19 | public bool $isDistinct, | ||
| 20 | ) { | ||
| 21 | } | ||
| 22 | |||
| 23 | public function dispatch(SqlWalker $walker): string | ||
| 24 | { | ||
| 25 | return $walker->walkSelectClause($this); | ||
| 26 | } | ||
| 27 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/AST/SelectExpression.php b/vendor/doctrine/orm/src/Query/AST/SelectExpression.php new file mode 100644 index 0000000..f09f3cd --- /dev/null +++ b/vendor/doctrine/orm/src/Query/AST/SelectExpression.php | |||
| @@ -0,0 +1,28 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Query\AST; | ||
| 6 | |||
| 7 | use Doctrine\ORM\Query\SqlWalker; | ||
| 8 | |||
| 9 | /** | ||
| 10 | * SelectExpression ::= IdentificationVariable ["." "*"] | StateFieldPathExpression | | ||
| 11 | * (AggregateExpression | "(" Subselect ")") [["AS"] ["HIDDEN"] FieldAliasIdentificationVariable] | ||
| 12 | * | ||
| 13 | * @link www.doctrine-project.org | ||
| 14 | */ | ||
| 15 | class SelectExpression extends Node | ||
| 16 | { | ||
| 17 | public function __construct( | ||
| 18 | public mixed $expression, | ||
| 19 | public string|null $fieldIdentificationVariable, | ||
| 20 | public bool $hiddenAliasResultVariable = false, | ||
| 21 | ) { | ||
| 22 | } | ||
| 23 | |||
| 24 | public function dispatch(SqlWalker $walker): string | ||
| 25 | { | ||
| 26 | return $walker->walkSelectExpression($this); | ||
| 27 | } | ||
| 28 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/AST/SelectStatement.php b/vendor/doctrine/orm/src/Query/AST/SelectStatement.php new file mode 100644 index 0000000..399462f --- /dev/null +++ b/vendor/doctrine/orm/src/Query/AST/SelectStatement.php | |||
| @@ -0,0 +1,32 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Query\AST; | ||
| 6 | |||
| 7 | use Doctrine\ORM\Query\SqlWalker; | ||
| 8 | |||
| 9 | /** | ||
| 10 | * SelectStatement = SelectClause FromClause [WhereClause] [GroupByClause] [HavingClause] [OrderByClause] | ||
| 11 | * | ||
| 12 | * @link www.doctrine-project.org | ||
| 13 | */ | ||
| 14 | class SelectStatement extends Node | ||
| 15 | { | ||
| 16 | public WhereClause|null $whereClause = null; | ||
| 17 | |||
| 18 | public GroupByClause|null $groupByClause = null; | ||
| 19 | |||
| 20 | public HavingClause|null $havingClause = null; | ||
| 21 | |||
| 22 | public OrderByClause|null $orderByClause = null; | ||
| 23 | |||
| 24 | public function __construct(public SelectClause $selectClause, public FromClause $fromClause) | ||
| 25 | { | ||
| 26 | } | ||
| 27 | |||
| 28 | public function dispatch(SqlWalker $walker): string | ||
| 29 | { | ||
| 30 | return $walker->walkSelectStatement($this); | ||
| 31 | } | ||
| 32 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/AST/SimpleArithmeticExpression.php b/vendor/doctrine/orm/src/Query/AST/SimpleArithmeticExpression.php new file mode 100644 index 0000000..ae7ca44 --- /dev/null +++ b/vendor/doctrine/orm/src/Query/AST/SimpleArithmeticExpression.php | |||
| @@ -0,0 +1,25 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Query\AST; | ||
| 6 | |||
| 7 | use Doctrine\ORM\Query\SqlWalker; | ||
| 8 | |||
| 9 | /** | ||
| 10 | * SimpleArithmeticExpression ::= ArithmeticTerm {("+" | "-") ArithmeticTerm}* | ||
| 11 | * | ||
| 12 | * @link www.doctrine-project.org | ||
| 13 | */ | ||
| 14 | class SimpleArithmeticExpression extends Node | ||
| 15 | { | ||
| 16 | /** @param mixed[] $arithmeticTerms */ | ||
| 17 | public function __construct(public array $arithmeticTerms) | ||
| 18 | { | ||
| 19 | } | ||
| 20 | |||
| 21 | public function dispatch(SqlWalker $walker): string | ||
| 22 | { | ||
| 23 | return $walker->walkSimpleArithmeticExpression($this); | ||
| 24 | } | ||
| 25 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/AST/SimpleCaseExpression.php b/vendor/doctrine/orm/src/Query/AST/SimpleCaseExpression.php new file mode 100644 index 0000000..b3764ba --- /dev/null +++ b/vendor/doctrine/orm/src/Query/AST/SimpleCaseExpression.php | |||
| @@ -0,0 +1,28 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Query\AST; | ||
| 6 | |||
| 7 | use Doctrine\ORM\Query\SqlWalker; | ||
| 8 | |||
| 9 | /** | ||
| 10 | * SimpleCaseExpression ::= "CASE" CaseOperand SimpleWhenClause {SimpleWhenClause}* "ELSE" ScalarExpression "END" | ||
| 11 | * | ||
| 12 | * @link www.doctrine-project.org | ||
| 13 | */ | ||
| 14 | class SimpleCaseExpression extends Node | ||
| 15 | { | ||
| 16 | /** @param mixed[] $simpleWhenClauses */ | ||
| 17 | public function __construct( | ||
| 18 | public PathExpression|null $caseOperand = null, | ||
| 19 | public array $simpleWhenClauses = [], | ||
| 20 | public mixed $elseScalarExpression = null, | ||
| 21 | ) { | ||
| 22 | } | ||
| 23 | |||
| 24 | public function dispatch(SqlWalker $walker): string | ||
| 25 | { | ||
| 26 | return $walker->walkSimpleCaseExpression($this); | ||
| 27 | } | ||
| 28 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/AST/SimpleSelectClause.php b/vendor/doctrine/orm/src/Query/AST/SimpleSelectClause.php new file mode 100644 index 0000000..0259e3b --- /dev/null +++ b/vendor/doctrine/orm/src/Query/AST/SimpleSelectClause.php | |||
| @@ -0,0 +1,26 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Query\AST; | ||
| 6 | |||
| 7 | use Doctrine\ORM\Query\SqlWalker; | ||
| 8 | |||
| 9 | /** | ||
| 10 | * SimpleSelectClause ::= "SELECT" ["DISTINCT"] SimpleSelectExpression | ||
| 11 | * | ||
| 12 | * @link www.doctrine-project.org | ||
| 13 | */ | ||
| 14 | class SimpleSelectClause extends Node | ||
| 15 | { | ||
| 16 | public function __construct( | ||
| 17 | public SimpleSelectExpression $simpleSelectExpression, | ||
| 18 | public bool $isDistinct = false, | ||
| 19 | ) { | ||
| 20 | } | ||
| 21 | |||
| 22 | public function dispatch(SqlWalker $walker): string | ||
| 23 | { | ||
| 24 | return $walker->walkSimpleSelectClause($this); | ||
| 25 | } | ||
| 26 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/AST/SimpleSelectExpression.php b/vendor/doctrine/orm/src/Query/AST/SimpleSelectExpression.php new file mode 100644 index 0000000..97e8f08 --- /dev/null +++ b/vendor/doctrine/orm/src/Query/AST/SimpleSelectExpression.php | |||
| @@ -0,0 +1,27 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Query\AST; | ||
| 6 | |||
| 7 | use Doctrine\ORM\Query\SqlWalker; | ||
| 8 | |||
| 9 | /** | ||
| 10 | * SimpleSelectExpression ::= StateFieldPathExpression | IdentificationVariable | ||
| 11 | * | (AggregateExpression [["AS"] FieldAliasIdentificationVariable]) | ||
| 12 | * | ||
| 13 | * @link www.doctrine-project.org | ||
| 14 | */ | ||
| 15 | class SimpleSelectExpression extends Node | ||
| 16 | { | ||
| 17 | public string|null $fieldIdentificationVariable = null; | ||
| 18 | |||
| 19 | public function __construct(public Node|string $expression) | ||
| 20 | { | ||
| 21 | } | ||
| 22 | |||
| 23 | public function dispatch(SqlWalker $walker): string | ||
| 24 | { | ||
| 25 | return $walker->walkSimpleSelectExpression($this); | ||
| 26 | } | ||
| 27 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/AST/SimpleWhenClause.php b/vendor/doctrine/orm/src/Query/AST/SimpleWhenClause.php new file mode 100644 index 0000000..892165a --- /dev/null +++ b/vendor/doctrine/orm/src/Query/AST/SimpleWhenClause.php | |||
| @@ -0,0 +1,26 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Query\AST; | ||
| 6 | |||
| 7 | use Doctrine\ORM\Query\SqlWalker; | ||
| 8 | |||
| 9 | /** | ||
| 10 | * SimpleWhenClause ::= "WHEN" ScalarExpression "THEN" ScalarExpression | ||
| 11 | * | ||
| 12 | * @link www.doctrine-project.org | ||
| 13 | */ | ||
| 14 | class SimpleWhenClause extends Node | ||
| 15 | { | ||
| 16 | public function __construct( | ||
| 17 | public mixed $caseScalarExpression = null, | ||
| 18 | public mixed $thenScalarExpression = null, | ||
| 19 | ) { | ||
| 20 | } | ||
| 21 | |||
| 22 | public function dispatch(SqlWalker $walker): string | ||
| 23 | { | ||
| 24 | return $walker->walkWhenClauseExpression($this); | ||
| 25 | } | ||
| 26 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/AST/Subselect.php b/vendor/doctrine/orm/src/Query/AST/Subselect.php new file mode 100644 index 0000000..8ff8595 --- /dev/null +++ b/vendor/doctrine/orm/src/Query/AST/Subselect.php | |||
| @@ -0,0 +1,32 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Query\AST; | ||
| 6 | |||
| 7 | use Doctrine\ORM\Query\SqlWalker; | ||
| 8 | |||
| 9 | /** | ||
| 10 | * Subselect ::= SimpleSelectClause SubselectFromClause [WhereClause] [GroupByClause] [HavingClause] [OrderByClause] | ||
| 11 | * | ||
| 12 | * @link www.doctrine-project.org | ||
| 13 | */ | ||
| 14 | class Subselect extends Node | ||
| 15 | { | ||
| 16 | public WhereClause|null $whereClause = null; | ||
| 17 | |||
| 18 | public GroupByClause|null $groupByClause = null; | ||
| 19 | |||
| 20 | public HavingClause|null $havingClause = null; | ||
| 21 | |||
| 22 | public OrderByClause|null $orderByClause = null; | ||
| 23 | |||
| 24 | public function __construct(public SimpleSelectClause $simpleSelectClause, public SubselectFromClause $subselectFromClause) | ||
| 25 | { | ||
| 26 | } | ||
| 27 | |||
| 28 | public function dispatch(SqlWalker $walker): string | ||
| 29 | { | ||
| 30 | return $walker->walkSubselect($this); | ||
| 31 | } | ||
| 32 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/AST/SubselectFromClause.php b/vendor/doctrine/orm/src/Query/AST/SubselectFromClause.php new file mode 100644 index 0000000..7cf01e2 --- /dev/null +++ b/vendor/doctrine/orm/src/Query/AST/SubselectFromClause.php | |||
| @@ -0,0 +1,25 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Query\AST; | ||
| 6 | |||
| 7 | use Doctrine\ORM\Query\SqlWalker; | ||
| 8 | |||
| 9 | /** | ||
| 10 | * SubselectFromClause ::= "FROM" SubselectIdentificationVariableDeclaration {"," SubselectIdentificationVariableDeclaration}* | ||
| 11 | * | ||
| 12 | * @link www.doctrine-project.org | ||
| 13 | */ | ||
| 14 | class SubselectFromClause extends Node | ||
| 15 | { | ||
| 16 | /** @param mixed[] $identificationVariableDeclarations */ | ||
| 17 | public function __construct(public array $identificationVariableDeclarations) | ||
| 18 | { | ||
| 19 | } | ||
| 20 | |||
| 21 | public function dispatch(SqlWalker $walker): string | ||
| 22 | { | ||
| 23 | return $walker->walkSubselectFromClause($this); | ||
| 24 | } | ||
| 25 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/AST/SubselectIdentificationVariableDeclaration.php b/vendor/doctrine/orm/src/Query/AST/SubselectIdentificationVariableDeclaration.php new file mode 100644 index 0000000..eadf6bc --- /dev/null +++ b/vendor/doctrine/orm/src/Query/AST/SubselectIdentificationVariableDeclaration.php | |||
| @@ -0,0 +1,19 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Query\AST; | ||
| 6 | |||
| 7 | /** | ||
| 8 | * SubselectIdentificationVariableDeclaration ::= AssociationPathExpression ["AS"] AliasIdentificationVariable | ||
| 9 | * | ||
| 10 | * @link www.doctrine-project.org | ||
| 11 | */ | ||
| 12 | class SubselectIdentificationVariableDeclaration | ||
| 13 | { | ||
| 14 | public function __construct( | ||
| 15 | public PathExpression $associationPathExpression, | ||
| 16 | public string $aliasIdentificationVariable, | ||
| 17 | ) { | ||
| 18 | } | ||
| 19 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/AST/TypedExpression.php b/vendor/doctrine/orm/src/Query/AST/TypedExpression.php new file mode 100644 index 0000000..d7cf76f --- /dev/null +++ b/vendor/doctrine/orm/src/Query/AST/TypedExpression.php | |||
| @@ -0,0 +1,15 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Query\AST; | ||
| 6 | |||
| 7 | use Doctrine\DBAL\Types\Type; | ||
| 8 | |||
| 9 | /** | ||
| 10 | * Provides an API for resolving the type of a Node | ||
| 11 | */ | ||
| 12 | interface TypedExpression | ||
| 13 | { | ||
| 14 | public function getReturnType(): Type; | ||
| 15 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/AST/UpdateClause.php b/vendor/doctrine/orm/src/Query/AST/UpdateClause.php new file mode 100644 index 0000000..cafcff0 --- /dev/null +++ b/vendor/doctrine/orm/src/Query/AST/UpdateClause.php | |||
| @@ -0,0 +1,29 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Query\AST; | ||
| 6 | |||
| 7 | use Doctrine\ORM\Query\SqlWalker; | ||
| 8 | |||
| 9 | /** | ||
| 10 | * UpdateClause ::= "UPDATE" AbstractSchemaName [["AS"] AliasIdentificationVariable] "SET" UpdateItem {"," UpdateItem}* | ||
| 11 | * | ||
| 12 | * @link www.doctrine-project.org | ||
| 13 | */ | ||
| 14 | class UpdateClause extends Node | ||
| 15 | { | ||
| 16 | public string $aliasIdentificationVariable; | ||
| 17 | |||
| 18 | /** @param mixed[] $updateItems */ | ||
| 19 | public function __construct( | ||
| 20 | public string $abstractSchemaName, | ||
| 21 | public array $updateItems, | ||
| 22 | ) { | ||
| 23 | } | ||
| 24 | |||
| 25 | public function dispatch(SqlWalker $walker): string | ||
| 26 | { | ||
| 27 | return $walker->walkUpdateClause($this); | ||
| 28 | } | ||
| 29 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/AST/UpdateItem.php b/vendor/doctrine/orm/src/Query/AST/UpdateItem.php new file mode 100644 index 0000000..b540593 --- /dev/null +++ b/vendor/doctrine/orm/src/Query/AST/UpdateItem.php | |||
| @@ -0,0 +1,26 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Query\AST; | ||
| 6 | |||
| 7 | use Doctrine\ORM\Query\SqlWalker; | ||
| 8 | |||
| 9 | /** | ||
| 10 | * UpdateItem ::= [IdentificationVariable "."] {StateField | SingleValuedAssociationField} "=" NewValue | ||
| 11 | * NewValue ::= SimpleArithmeticExpression | StringPrimary | DatetimePrimary | BooleanPrimary | | ||
| 12 | * EnumPrimary | SimpleEntityExpression | "NULL" | ||
| 13 | * | ||
| 14 | * @link www.doctrine-project.org | ||
| 15 | */ | ||
| 16 | class UpdateItem extends Node | ||
| 17 | { | ||
| 18 | public function __construct(public PathExpression $pathExpression, public InputParameter|ArithmeticExpression|null $newValue) | ||
| 19 | { | ||
| 20 | } | ||
| 21 | |||
| 22 | public function dispatch(SqlWalker $walker): string | ||
| 23 | { | ||
| 24 | return $walker->walkUpdateItem($this); | ||
| 25 | } | ||
| 26 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/AST/UpdateStatement.php b/vendor/doctrine/orm/src/Query/AST/UpdateStatement.php new file mode 100644 index 0000000..7ea5076 --- /dev/null +++ b/vendor/doctrine/orm/src/Query/AST/UpdateStatement.php | |||
| @@ -0,0 +1,26 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Query\AST; | ||
| 6 | |||
| 7 | use Doctrine\ORM\Query\SqlWalker; | ||
| 8 | |||
| 9 | /** | ||
| 10 | * UpdateStatement = UpdateClause [WhereClause] | ||
| 11 | * | ||
| 12 | * @link www.doctrine-project.org | ||
| 13 | */ | ||
| 14 | class UpdateStatement extends Node | ||
| 15 | { | ||
| 16 | public WhereClause|null $whereClause = null; | ||
| 17 | |||
| 18 | public function __construct(public UpdateClause $updateClause) | ||
| 19 | { | ||
| 20 | } | ||
| 21 | |||
| 22 | public function dispatch(SqlWalker $walker): string | ||
| 23 | { | ||
| 24 | return $walker->walkUpdateStatement($this); | ||
| 25 | } | ||
| 26 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/AST/WhenClause.php b/vendor/doctrine/orm/src/Query/AST/WhenClause.php new file mode 100644 index 0000000..9bf194e --- /dev/null +++ b/vendor/doctrine/orm/src/Query/AST/WhenClause.php | |||
| @@ -0,0 +1,26 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Query\AST; | ||
| 6 | |||
| 7 | use Doctrine\ORM\Query\SqlWalker; | ||
| 8 | |||
| 9 | /** | ||
| 10 | * WhenClause ::= "WHEN" ConditionalExpression "THEN" ScalarExpression | ||
| 11 | * | ||
| 12 | * @link www.doctrine-project.org | ||
| 13 | */ | ||
| 14 | class WhenClause extends Node | ||
| 15 | { | ||
| 16 | public function __construct( | ||
| 17 | public ConditionalExpression|Phase2OptimizableConditional $caseConditionExpression, | ||
| 18 | public mixed $thenScalarExpression = null, | ||
| 19 | ) { | ||
| 20 | } | ||
| 21 | |||
| 22 | public function dispatch(SqlWalker $walker): string | ||
| 23 | { | ||
| 24 | return $walker->walkWhenClauseExpression($this); | ||
| 25 | } | ||
| 26 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/AST/WhereClause.php b/vendor/doctrine/orm/src/Query/AST/WhereClause.php new file mode 100644 index 0000000..e4d7b66 --- /dev/null +++ b/vendor/doctrine/orm/src/Query/AST/WhereClause.php | |||
| @@ -0,0 +1,24 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Query\AST; | ||
| 6 | |||
| 7 | use Doctrine\ORM\Query\SqlWalker; | ||
| 8 | |||
| 9 | /** | ||
| 10 | * WhereClause ::= "WHERE" ConditionalExpression | ||
| 11 | * | ||
| 12 | * @link www.doctrine-project.org | ||
| 13 | */ | ||
| 14 | class WhereClause extends Node | ||
| 15 | { | ||
| 16 | public function __construct(public ConditionalExpression|Phase2OptimizableConditional $conditionalExpression) | ||
| 17 | { | ||
| 18 | } | ||
| 19 | |||
| 20 | public function dispatch(SqlWalker $walker): string | ||
| 21 | { | ||
| 22 | return $walker->walkWhereClause($this); | ||
| 23 | } | ||
| 24 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/Exec/AbstractSqlExecutor.php b/vendor/doctrine/orm/src/Query/Exec/AbstractSqlExecutor.php new file mode 100644 index 0000000..101bf26 --- /dev/null +++ b/vendor/doctrine/orm/src/Query/Exec/AbstractSqlExecutor.php | |||
| @@ -0,0 +1,61 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Query\Exec; | ||
| 6 | |||
| 7 | use Doctrine\DBAL\ArrayParameterType; | ||
| 8 | use Doctrine\DBAL\Cache\QueryCacheProfile; | ||
| 9 | use Doctrine\DBAL\Connection; | ||
| 10 | use Doctrine\DBAL\ParameterType; | ||
| 11 | use Doctrine\DBAL\Result; | ||
| 12 | use Doctrine\DBAL\Types\Type; | ||
| 13 | |||
| 14 | /** | ||
| 15 | * Base class for SQL statement executors. | ||
| 16 | * | ||
| 17 | * @link http://www.doctrine-project.org | ||
| 18 | * | ||
| 19 | * @todo Rename: AbstractSQLExecutor | ||
| 20 | * @psalm-type WrapperParameterType = string|Type|ParameterType::*|ArrayParameterType::* | ||
| 21 | * @psalm-type WrapperParameterTypeArray = array<int<0, max>, WrapperParameterType>|array<string, WrapperParameterType> | ||
| 22 | */ | ||
| 23 | abstract class AbstractSqlExecutor | ||
| 24 | { | ||
| 25 | /** @var list<string>|string */ | ||
| 26 | protected array|string $sqlStatements; | ||
| 27 | |||
| 28 | protected QueryCacheProfile|null $queryCacheProfile = null; | ||
| 29 | |||
| 30 | /** | ||
| 31 | * Gets the SQL statements that are executed by the executor. | ||
| 32 | * | ||
| 33 | * @return list<string>|string All the SQL update statements. | ||
| 34 | */ | ||
| 35 | public function getSqlStatements(): array|string | ||
| 36 | { | ||
| 37 | return $this->sqlStatements; | ||
| 38 | } | ||
| 39 | |||
| 40 | public function setQueryCacheProfile(QueryCacheProfile $qcp): void | ||
| 41 | { | ||
| 42 | $this->queryCacheProfile = $qcp; | ||
| 43 | } | ||
| 44 | |||
| 45 | /** | ||
| 46 | * Do not use query cache | ||
| 47 | */ | ||
| 48 | public function removeQueryCacheProfile(): void | ||
| 49 | { | ||
| 50 | $this->queryCacheProfile = null; | ||
| 51 | } | ||
| 52 | |||
| 53 | /** | ||
| 54 | * Executes all sql statements. | ||
| 55 | * | ||
| 56 | * @param Connection $conn The database connection that is used to execute the queries. | ||
| 57 | * @param list<mixed>|array<string, mixed> $params The parameters. | ||
| 58 | * @psalm-param WrapperParameterTypeArray $types The parameter types. | ||
| 59 | */ | ||
| 60 | abstract public function execute(Connection $conn, array $params, array $types): Result|int; | ||
| 61 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/Exec/MultiTableDeleteExecutor.php b/vendor/doctrine/orm/src/Query/Exec/MultiTableDeleteExecutor.php new file mode 100644 index 0000000..6096462 --- /dev/null +++ b/vendor/doctrine/orm/src/Query/Exec/MultiTableDeleteExecutor.php | |||
| @@ -0,0 +1,131 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Query\Exec; | ||
| 6 | |||
| 7 | use Doctrine\DBAL\Connection; | ||
| 8 | use Doctrine\DBAL\Connections\PrimaryReadReplicaConnection; | ||
| 9 | use Doctrine\DBAL\Types\Type; | ||
| 10 | use Doctrine\ORM\Query\AST; | ||
| 11 | use Doctrine\ORM\Query\AST\DeleteStatement; | ||
| 12 | use Doctrine\ORM\Query\SqlWalker; | ||
| 13 | use Doctrine\ORM\Utility\PersisterHelper; | ||
| 14 | use Throwable; | ||
| 15 | |||
| 16 | use function array_reverse; | ||
| 17 | use function implode; | ||
| 18 | |||
| 19 | /** | ||
| 20 | * Executes the SQL statements for bulk DQL DELETE statements on classes in | ||
| 21 | * Class Table Inheritance (JOINED). | ||
| 22 | * | ||
| 23 | * @link http://www.doctrine-project.org | ||
| 24 | */ | ||
| 25 | class MultiTableDeleteExecutor extends AbstractSqlExecutor | ||
| 26 | { | ||
| 27 | private readonly string $createTempTableSql; | ||
| 28 | private readonly string $dropTempTableSql; | ||
| 29 | private readonly string $insertSql; | ||
| 30 | |||
| 31 | /** | ||
| 32 | * Initializes a new <tt>MultiTableDeleteExecutor</tt>. | ||
| 33 | * | ||
| 34 | * Internal note: Any SQL construction and preparation takes place in the constructor for | ||
| 35 | * best performance. With a query cache the executor will be cached. | ||
| 36 | * | ||
| 37 | * @param DeleteStatement $AST The root AST node of the DQL query. | ||
| 38 | * @param SqlWalker $sqlWalker The walker used for SQL generation from the AST. | ||
| 39 | */ | ||
| 40 | public function __construct(AST\Node $AST, SqlWalker $sqlWalker) | ||
| 41 | { | ||
| 42 | $em = $sqlWalker->getEntityManager(); | ||
| 43 | $conn = $em->getConnection(); | ||
| 44 | $platform = $conn->getDatabasePlatform(); | ||
| 45 | $quoteStrategy = $em->getConfiguration()->getQuoteStrategy(); | ||
| 46 | |||
| 47 | if ($conn instanceof PrimaryReadReplicaConnection) { | ||
| 48 | $conn->ensureConnectedToPrimary(); | ||
| 49 | } | ||
| 50 | |||
| 51 | $primaryClass = $em->getClassMetadata($AST->deleteClause->abstractSchemaName); | ||
| 52 | $primaryDqlAlias = $AST->deleteClause->aliasIdentificationVariable; | ||
| 53 | $rootClass = $em->getClassMetadata($primaryClass->rootEntityName); | ||
| 54 | |||
| 55 | $tempTable = $platform->getTemporaryTableName($rootClass->getTemporaryIdTableName()); | ||
| 56 | $idColumnNames = $rootClass->getIdentifierColumnNames(); | ||
| 57 | $idColumnList = implode(', ', $idColumnNames); | ||
| 58 | |||
| 59 | // 1. Create an INSERT INTO temptable ... SELECT identifiers WHERE $AST->getWhereClause() | ||
| 60 | $sqlWalker->setSQLTableAlias($primaryClass->getTableName(), 't0', $primaryDqlAlias); | ||
| 61 | |||
| 62 | $insertSql = 'INSERT INTO ' . $tempTable . ' (' . $idColumnList . ')' | ||
| 63 | . ' SELECT t0.' . implode(', t0.', $idColumnNames); | ||
| 64 | |||
| 65 | $rangeDecl = new AST\RangeVariableDeclaration($primaryClass->name, $primaryDqlAlias); | ||
| 66 | $fromClause = new AST\FromClause([new AST\IdentificationVariableDeclaration($rangeDecl, null, [])]); | ||
| 67 | $insertSql .= $sqlWalker->walkFromClause($fromClause); | ||
| 68 | |||
| 69 | // Append WHERE clause, if there is one. | ||
| 70 | if ($AST->whereClause) { | ||
| 71 | $insertSql .= $sqlWalker->walkWhereClause($AST->whereClause); | ||
| 72 | } | ||
| 73 | |||
| 74 | $this->insertSql = $insertSql; | ||
| 75 | |||
| 76 | // 2. Create ID subselect statement used in DELETE ... WHERE ... IN (subselect) | ||
| 77 | $idSubselect = 'SELECT ' . $idColumnList . ' FROM ' . $tempTable; | ||
| 78 | |||
| 79 | // 3. Create and store DELETE statements | ||
| 80 | $classNames = [...$primaryClass->parentClasses, ...[$primaryClass->name], ...$primaryClass->subClasses]; | ||
| 81 | foreach (array_reverse($classNames) as $className) { | ||
| 82 | $tableName = $quoteStrategy->getTableName($em->getClassMetadata($className), $platform); | ||
| 83 | $this->sqlStatements[] = 'DELETE FROM ' . $tableName | ||
| 84 | . ' WHERE (' . $idColumnList . ') IN (' . $idSubselect . ')'; | ||
| 85 | } | ||
| 86 | |||
| 87 | // 4. Store DDL for temporary identifier table. | ||
| 88 | $columnDefinitions = []; | ||
| 89 | foreach ($idColumnNames as $idColumnName) { | ||
| 90 | $columnDefinitions[$idColumnName] = [ | ||
| 91 | 'name' => $idColumnName, | ||
| 92 | 'notnull' => true, | ||
| 93 | 'type' => Type::getType(PersisterHelper::getTypeOfColumn($idColumnName, $rootClass, $em)), | ||
| 94 | ]; | ||
| 95 | } | ||
| 96 | |||
| 97 | $this->createTempTableSql = $platform->getCreateTemporaryTableSnippetSQL() . ' ' . $tempTable . ' (' | ||
| 98 | . $platform->getColumnDeclarationListSQL($columnDefinitions) . ', PRIMARY KEY(' . implode(',', $idColumnNames) . '))'; | ||
| 99 | $this->dropTempTableSql = $platform->getDropTemporaryTableSQL($tempTable); | ||
| 100 | } | ||
| 101 | |||
| 102 | /** | ||
| 103 | * {@inheritDoc} | ||
| 104 | */ | ||
| 105 | public function execute(Connection $conn, array $params, array $types): int | ||
| 106 | { | ||
| 107 | // Create temporary id table | ||
| 108 | $conn->executeStatement($this->createTempTableSql); | ||
| 109 | |||
| 110 | try { | ||
| 111 | // Insert identifiers | ||
| 112 | $numDeleted = $conn->executeStatement($this->insertSql, $params, $types); | ||
| 113 | |||
| 114 | // Execute DELETE statements | ||
| 115 | foreach ($this->sqlStatements as $sql) { | ||
| 116 | $conn->executeStatement($sql); | ||
| 117 | } | ||
| 118 | } catch (Throwable $exception) { | ||
| 119 | // FAILURE! Drop temporary table to avoid possible collisions | ||
| 120 | $conn->executeStatement($this->dropTempTableSql); | ||
| 121 | |||
| 122 | // Re-throw exception | ||
| 123 | throw $exception; | ||
| 124 | } | ||
| 125 | |||
| 126 | // Drop temporary table | ||
| 127 | $conn->executeStatement($this->dropTempTableSql); | ||
| 128 | |||
| 129 | return $numDeleted; | ||
| 130 | } | ||
| 131 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/Exec/MultiTableUpdateExecutor.php b/vendor/doctrine/orm/src/Query/Exec/MultiTableUpdateExecutor.php new file mode 100644 index 0000000..dab1b61 --- /dev/null +++ b/vendor/doctrine/orm/src/Query/Exec/MultiTableUpdateExecutor.php | |||
| @@ -0,0 +1,180 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Query\Exec; | ||
| 6 | |||
| 7 | use Doctrine\DBAL\Connection; | ||
| 8 | use Doctrine\DBAL\Connections\PrimaryReadReplicaConnection; | ||
| 9 | use Doctrine\DBAL\Types\Type; | ||
| 10 | use Doctrine\ORM\Query\AST; | ||
| 11 | use Doctrine\ORM\Query\AST\UpdateStatement; | ||
| 12 | use Doctrine\ORM\Query\ParameterTypeInferer; | ||
| 13 | use Doctrine\ORM\Query\SqlWalker; | ||
| 14 | use Doctrine\ORM\Utility\PersisterHelper; | ||
| 15 | |||
| 16 | use function array_reverse; | ||
| 17 | use function array_slice; | ||
| 18 | use function implode; | ||
| 19 | |||
| 20 | /** | ||
| 21 | * Executes the SQL statements for bulk DQL UPDATE statements on classes in | ||
| 22 | * Class Table Inheritance (JOINED). | ||
| 23 | */ | ||
| 24 | class MultiTableUpdateExecutor extends AbstractSqlExecutor | ||
| 25 | { | ||
| 26 | private readonly string $createTempTableSql; | ||
| 27 | private readonly string $dropTempTableSql; | ||
| 28 | private readonly string $insertSql; | ||
| 29 | |||
| 30 | /** @var mixed[] */ | ||
| 31 | private array $sqlParameters = []; | ||
| 32 | private int $numParametersInUpdateClause = 0; | ||
| 33 | |||
| 34 | /** | ||
| 35 | * Initializes a new <tt>MultiTableUpdateExecutor</tt>. | ||
| 36 | * | ||
| 37 | * Internal note: Any SQL construction and preparation takes place in the constructor for | ||
| 38 | * best performance. With a query cache the executor will be cached. | ||
| 39 | * | ||
| 40 | * @param UpdateStatement $AST The root AST node of the DQL query. | ||
| 41 | * @param SqlWalker $sqlWalker The walker used for SQL generation from the AST. | ||
| 42 | */ | ||
| 43 | public function __construct(AST\Node $AST, SqlWalker $sqlWalker) | ||
| 44 | { | ||
| 45 | $em = $sqlWalker->getEntityManager(); | ||
| 46 | $conn = $em->getConnection(); | ||
| 47 | $platform = $conn->getDatabasePlatform(); | ||
| 48 | $quoteStrategy = $em->getConfiguration()->getQuoteStrategy(); | ||
| 49 | $this->sqlStatements = []; | ||
| 50 | |||
| 51 | if ($conn instanceof PrimaryReadReplicaConnection) { | ||
| 52 | $conn->ensureConnectedToPrimary(); | ||
| 53 | } | ||
| 54 | |||
| 55 | $updateClause = $AST->updateClause; | ||
| 56 | $primaryClass = $sqlWalker->getEntityManager()->getClassMetadata($updateClause->abstractSchemaName); | ||
| 57 | $rootClass = $em->getClassMetadata($primaryClass->rootEntityName); | ||
| 58 | |||
| 59 | $updateItems = $updateClause->updateItems; | ||
| 60 | |||
| 61 | $tempTable = $platform->getTemporaryTableName($rootClass->getTemporaryIdTableName()); | ||
| 62 | $idColumnNames = $rootClass->getIdentifierColumnNames(); | ||
| 63 | $idColumnList = implode(', ', $idColumnNames); | ||
| 64 | |||
| 65 | // 1. Create an INSERT INTO temptable ... SELECT identifiers WHERE $AST->getWhereClause() | ||
| 66 | $sqlWalker->setSQLTableAlias($primaryClass->getTableName(), 't0', $updateClause->aliasIdentificationVariable); | ||
| 67 | |||
| 68 | $insertSql = 'INSERT INTO ' . $tempTable . ' (' . $idColumnList . ')' | ||
| 69 | . ' SELECT t0.' . implode(', t0.', $idColumnNames); | ||
| 70 | |||
| 71 | $rangeDecl = new AST\RangeVariableDeclaration($primaryClass->name, $updateClause->aliasIdentificationVariable); | ||
| 72 | $fromClause = new AST\FromClause([new AST\IdentificationVariableDeclaration($rangeDecl, null, [])]); | ||
| 73 | |||
| 74 | $insertSql .= $sqlWalker->walkFromClause($fromClause); | ||
| 75 | |||
| 76 | // 2. Create ID subselect statement used in UPDATE ... WHERE ... IN (subselect) | ||
| 77 | $idSubselect = 'SELECT ' . $idColumnList . ' FROM ' . $tempTable; | ||
| 78 | |||
| 79 | // 3. Create and store UPDATE statements | ||
| 80 | $classNames = [...$primaryClass->parentClasses, ...[$primaryClass->name], ...$primaryClass->subClasses]; | ||
| 81 | |||
| 82 | foreach (array_reverse($classNames) as $className) { | ||
| 83 | $affected = false; | ||
| 84 | $class = $em->getClassMetadata($className); | ||
| 85 | $updateSql = 'UPDATE ' . $quoteStrategy->getTableName($class, $platform) . ' SET '; | ||
| 86 | |||
| 87 | $sqlParameters = []; | ||
| 88 | foreach ($updateItems as $updateItem) { | ||
| 89 | $field = $updateItem->pathExpression->field; | ||
| 90 | |||
| 91 | if ( | ||
| 92 | (isset($class->fieldMappings[$field]) && ! isset($class->fieldMappings[$field]->inherited)) || | ||
| 93 | (isset($class->associationMappings[$field]) && ! isset($class->associationMappings[$field]->inherited)) | ||
| 94 | ) { | ||
| 95 | $newValue = $updateItem->newValue; | ||
| 96 | |||
| 97 | if (! $affected) { | ||
| 98 | $affected = true; | ||
| 99 | } else { | ||
| 100 | $updateSql .= ', '; | ||
| 101 | } | ||
| 102 | |||
| 103 | $updateSql .= $sqlWalker->walkUpdateItem($updateItem); | ||
| 104 | |||
| 105 | if ($newValue instanceof AST\InputParameter) { | ||
| 106 | $sqlParameters[] = $newValue->name; | ||
| 107 | |||
| 108 | ++$this->numParametersInUpdateClause; | ||
| 109 | } | ||
| 110 | } | ||
| 111 | } | ||
| 112 | |||
| 113 | if ($affected) { | ||
| 114 | $this->sqlParameters[] = $sqlParameters; | ||
| 115 | $this->sqlStatements[] = $updateSql . ' WHERE (' . $idColumnList . ') IN (' . $idSubselect . ')'; | ||
| 116 | } | ||
| 117 | } | ||
| 118 | |||
| 119 | // Append WHERE clause to insertSql, if there is one. | ||
| 120 | if ($AST->whereClause) { | ||
| 121 | $insertSql .= $sqlWalker->walkWhereClause($AST->whereClause); | ||
| 122 | } | ||
| 123 | |||
| 124 | $this->insertSql = $insertSql; | ||
| 125 | |||
| 126 | // 4. Store DDL for temporary identifier table. | ||
| 127 | $columnDefinitions = []; | ||
| 128 | |||
| 129 | foreach ($idColumnNames as $idColumnName) { | ||
| 130 | $columnDefinitions[$idColumnName] = [ | ||
| 131 | 'name' => $idColumnName, | ||
| 132 | 'notnull' => true, | ||
| 133 | 'type' => Type::getType(PersisterHelper::getTypeOfColumn($idColumnName, $rootClass, $em)), | ||
| 134 | ]; | ||
| 135 | } | ||
| 136 | |||
| 137 | $this->createTempTableSql = $platform->getCreateTemporaryTableSnippetSQL() . ' ' . $tempTable . ' (' | ||
| 138 | . $platform->getColumnDeclarationListSQL($columnDefinitions) . ', PRIMARY KEY(' . implode(',', $idColumnNames) . '))'; | ||
| 139 | |||
| 140 | $this->dropTempTableSql = $platform->getDropTemporaryTableSQL($tempTable); | ||
| 141 | } | ||
| 142 | |||
| 143 | /** | ||
| 144 | * {@inheritDoc} | ||
| 145 | */ | ||
| 146 | public function execute(Connection $conn, array $params, array $types): int | ||
| 147 | { | ||
| 148 | // Create temporary id table | ||
| 149 | $conn->executeStatement($this->createTempTableSql); | ||
| 150 | |||
| 151 | try { | ||
| 152 | // Insert identifiers. Parameters from the update clause are cut off. | ||
| 153 | $numUpdated = $conn->executeStatement( | ||
| 154 | $this->insertSql, | ||
| 155 | array_slice($params, $this->numParametersInUpdateClause), | ||
| 156 | array_slice($types, $this->numParametersInUpdateClause), | ||
| 157 | ); | ||
| 158 | |||
| 159 | // Execute UPDATE statements | ||
| 160 | foreach ($this->sqlStatements as $key => $statement) { | ||
| 161 | $paramValues = []; | ||
| 162 | $paramTypes = []; | ||
| 163 | |||
| 164 | if (isset($this->sqlParameters[$key])) { | ||
| 165 | foreach ($this->sqlParameters[$key] as $parameterKey => $parameterName) { | ||
| 166 | $paramValues[] = $params[$parameterKey]; | ||
| 167 | $paramTypes[] = $types[$parameterKey] ?? ParameterTypeInferer::inferType($params[$parameterKey]); | ||
| 168 | } | ||
| 169 | } | ||
| 170 | |||
| 171 | $conn->executeStatement($statement, $paramValues, $paramTypes); | ||
| 172 | } | ||
| 173 | } finally { | ||
| 174 | // Drop temporary table | ||
| 175 | $conn->executeStatement($this->dropTempTableSql); | ||
| 176 | } | ||
| 177 | |||
| 178 | return $numUpdated; | ||
| 179 | } | ||
| 180 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/Exec/SingleSelectExecutor.php b/vendor/doctrine/orm/src/Query/Exec/SingleSelectExecutor.php new file mode 100644 index 0000000..5445edb --- /dev/null +++ b/vendor/doctrine/orm/src/Query/Exec/SingleSelectExecutor.php | |||
| @@ -0,0 +1,31 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Query\Exec; | ||
| 6 | |||
| 7 | use Doctrine\DBAL\Connection; | ||
| 8 | use Doctrine\DBAL\Result; | ||
| 9 | use Doctrine\ORM\Query\AST\SelectStatement; | ||
| 10 | use Doctrine\ORM\Query\SqlWalker; | ||
| 11 | |||
| 12 | /** | ||
| 13 | * Executor that executes the SQL statement for simple DQL SELECT statements. | ||
| 14 | * | ||
| 15 | * @link www.doctrine-project.org | ||
| 16 | */ | ||
| 17 | class SingleSelectExecutor extends AbstractSqlExecutor | ||
| 18 | { | ||
| 19 | public function __construct(SelectStatement $AST, SqlWalker $sqlWalker) | ||
| 20 | { | ||
| 21 | $this->sqlStatements = $sqlWalker->walkSelectStatement($AST); | ||
| 22 | } | ||
| 23 | |||
| 24 | /** | ||
| 25 | * {@inheritDoc} | ||
| 26 | */ | ||
| 27 | public function execute(Connection $conn, array $params, array $types): Result | ||
| 28 | { | ||
| 29 | return $conn->executeQuery($this->sqlStatements, $params, $types, $this->queryCacheProfile); | ||
| 30 | } | ||
| 31 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/Exec/SingleTableDeleteUpdateExecutor.php b/vendor/doctrine/orm/src/Query/Exec/SingleTableDeleteUpdateExecutor.php new file mode 100644 index 0000000..66696db --- /dev/null +++ b/vendor/doctrine/orm/src/Query/Exec/SingleTableDeleteUpdateExecutor.php | |||
| @@ -0,0 +1,42 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Query\Exec; | ||
| 6 | |||
| 7 | use Doctrine\DBAL\Connection; | ||
| 8 | use Doctrine\DBAL\Connections\PrimaryReadReplicaConnection; | ||
| 9 | use Doctrine\ORM\Query\AST; | ||
| 10 | use Doctrine\ORM\Query\SqlWalker; | ||
| 11 | |||
| 12 | /** | ||
| 13 | * Executor that executes the SQL statements for DQL DELETE/UPDATE statements on classes | ||
| 14 | * that are mapped to a single table. | ||
| 15 | * | ||
| 16 | * @link www.doctrine-project.org | ||
| 17 | * | ||
| 18 | * @todo This is exactly the same as SingleSelectExecutor. Unify in SingleStatementExecutor. | ||
| 19 | */ | ||
| 20 | class SingleTableDeleteUpdateExecutor extends AbstractSqlExecutor | ||
| 21 | { | ||
| 22 | public function __construct(AST\Node $AST, SqlWalker $sqlWalker) | ||
| 23 | { | ||
| 24 | if ($AST instanceof AST\UpdateStatement) { | ||
| 25 | $this->sqlStatements = $sqlWalker->walkUpdateStatement($AST); | ||
| 26 | } elseif ($AST instanceof AST\DeleteStatement) { | ||
| 27 | $this->sqlStatements = $sqlWalker->walkDeleteStatement($AST); | ||
| 28 | } | ||
| 29 | } | ||
| 30 | |||
| 31 | /** | ||
| 32 | * {@inheritDoc} | ||
| 33 | */ | ||
| 34 | public function execute(Connection $conn, array $params, array $types): int | ||
| 35 | { | ||
| 36 | if ($conn instanceof PrimaryReadReplicaConnection) { | ||
| 37 | $conn->ensureConnectedToPrimary(); | ||
| 38 | } | ||
| 39 | |||
| 40 | return $conn->executeStatement($this->sqlStatements, $params, $types); | ||
| 41 | } | ||
| 42 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/Expr.php b/vendor/doctrine/orm/src/Query/Expr.php new file mode 100644 index 0000000..65f3082 --- /dev/null +++ b/vendor/doctrine/orm/src/Query/Expr.php | |||
| @@ -0,0 +1,615 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Query; | ||
| 6 | |||
| 7 | use Doctrine\ORM\Internal\NoUnknownNamedArguments; | ||
| 8 | use Traversable; | ||
| 9 | |||
| 10 | use function implode; | ||
| 11 | use function is_bool; | ||
| 12 | use function is_float; | ||
| 13 | use function is_int; | ||
| 14 | use function is_iterable; | ||
| 15 | use function iterator_to_array; | ||
| 16 | use function str_replace; | ||
| 17 | |||
| 18 | /** | ||
| 19 | * This class is used to generate DQL expressions via a set of PHP static functions. | ||
| 20 | * | ||
| 21 | * @link www.doctrine-project.org | ||
| 22 | * | ||
| 23 | * @todo Rename: ExpressionBuilder | ||
| 24 | */ | ||
| 25 | class Expr | ||
| 26 | { | ||
| 27 | use NoUnknownNamedArguments; | ||
| 28 | |||
| 29 | /** | ||
| 30 | * Creates a conjunction of the given boolean expressions. | ||
| 31 | * | ||
| 32 | * Example: | ||
| 33 | * | ||
| 34 | * [php] | ||
| 35 | * // (u.type = ?1) AND (u.role = ?2) | ||
| 36 | * $expr->andX($expr->eq('u.type', ':1'), $expr->eq('u.role', ':2')); | ||
| 37 | * | ||
| 38 | * @param Expr\Comparison|Expr\Func|Expr\Andx|Expr\Orx|string ...$x Optional clause. Defaults to null, | ||
| 39 | * but requires at least one defined | ||
| 40 | * when converting to string. | ||
| 41 | */ | ||
| 42 | public function andX(Expr\Comparison|Expr\Func|Expr\Andx|Expr\Orx|string ...$x): Expr\Andx | ||
| 43 | { | ||
| 44 | self::validateVariadicParameter($x); | ||
| 45 | |||
| 46 | return new Expr\Andx($x); | ||
| 47 | } | ||
| 48 | |||
| 49 | /** | ||
| 50 | * Creates a disjunction of the given boolean expressions. | ||
| 51 | * | ||
| 52 | * Example: | ||
| 53 | * | ||
| 54 | * [php] | ||
| 55 | * // (u.type = ?1) OR (u.role = ?2) | ||
| 56 | * $q->where($q->expr()->orX('u.type = ?1', 'u.role = ?2')); | ||
| 57 | * | ||
| 58 | * @param Expr\Comparison|Expr\Func|Expr\Andx|Expr\Orx|string ...$x Optional clause. Defaults to null, | ||
| 59 | * but requires at least one defined | ||
| 60 | * when converting to string. | ||
| 61 | */ | ||
| 62 | public function orX(Expr\Comparison|Expr\Func|Expr\Andx|Expr\Orx|string ...$x): Expr\Orx | ||
| 63 | { | ||
| 64 | self::validateVariadicParameter($x); | ||
| 65 | |||
| 66 | return new Expr\Orx($x); | ||
| 67 | } | ||
| 68 | |||
| 69 | /** | ||
| 70 | * Creates an ASCending order expression. | ||
| 71 | */ | ||
| 72 | public function asc(mixed $expr): Expr\OrderBy | ||
| 73 | { | ||
| 74 | return new Expr\OrderBy($expr, 'ASC'); | ||
| 75 | } | ||
| 76 | |||
| 77 | /** | ||
| 78 | * Creates a DESCending order expression. | ||
| 79 | */ | ||
| 80 | public function desc(mixed $expr): Expr\OrderBy | ||
| 81 | { | ||
| 82 | return new Expr\OrderBy($expr, 'DESC'); | ||
| 83 | } | ||
| 84 | |||
| 85 | /** | ||
| 86 | * Creates an equality comparison expression with the given arguments. | ||
| 87 | * | ||
| 88 | * First argument is considered the left expression and the second is the right expression. | ||
| 89 | * When converted to string, it will generated a <left expr> = <right expr>. Example: | ||
| 90 | * | ||
| 91 | * [php] | ||
| 92 | * // u.id = ?1 | ||
| 93 | * $expr->eq('u.id', '?1'); | ||
| 94 | * | ||
| 95 | * @param mixed $x Left expression. | ||
| 96 | * @param mixed $y Right expression. | ||
| 97 | */ | ||
| 98 | public function eq(mixed $x, mixed $y): Expr\Comparison | ||
| 99 | { | ||
| 100 | return new Expr\Comparison($x, Expr\Comparison::EQ, $y); | ||
| 101 | } | ||
| 102 | |||
| 103 | /** | ||
| 104 | * Creates an instance of Expr\Comparison, with the given arguments. | ||
| 105 | * First argument is considered the left expression and the second is the right expression. | ||
| 106 | * When converted to string, it will generated a <left expr> <> <right expr>. Example: | ||
| 107 | * | ||
| 108 | * [php] | ||
| 109 | * // u.id <> ?1 | ||
| 110 | * $q->where($q->expr()->neq('u.id', '?1')); | ||
| 111 | * | ||
| 112 | * @param mixed $x Left expression. | ||
| 113 | * @param mixed $y Right expression. | ||
| 114 | */ | ||
| 115 | public function neq(mixed $x, mixed $y): Expr\Comparison | ||
| 116 | { | ||
| 117 | return new Expr\Comparison($x, Expr\Comparison::NEQ, $y); | ||
| 118 | } | ||
| 119 | |||
| 120 | /** | ||
| 121 | * Creates an instance of Expr\Comparison, with the given arguments. | ||
| 122 | * First argument is considered the left expression and the second is the right expression. | ||
| 123 | * When converted to string, it will generated a <left expr> < <right expr>. Example: | ||
| 124 | * | ||
| 125 | * [php] | ||
| 126 | * // u.id < ?1 | ||
| 127 | * $q->where($q->expr()->lt('u.id', '?1')); | ||
| 128 | * | ||
| 129 | * @param mixed $x Left expression. | ||
| 130 | * @param mixed $y Right expression. | ||
| 131 | */ | ||
| 132 | public function lt(mixed $x, mixed $y): Expr\Comparison | ||
| 133 | { | ||
| 134 | return new Expr\Comparison($x, Expr\Comparison::LT, $y); | ||
| 135 | } | ||
| 136 | |||
| 137 | /** | ||
| 138 | * Creates an instance of Expr\Comparison, with the given arguments. | ||
| 139 | * First argument is considered the left expression and the second is the right expression. | ||
| 140 | * When converted to string, it will generated a <left expr> <= <right expr>. Example: | ||
| 141 | * | ||
| 142 | * [php] | ||
| 143 | * // u.id <= ?1 | ||
| 144 | * $q->where($q->expr()->lte('u.id', '?1')); | ||
| 145 | * | ||
| 146 | * @param mixed $x Left expression. | ||
| 147 | * @param mixed $y Right expression. | ||
| 148 | */ | ||
| 149 | public function lte(mixed $x, mixed $y): Expr\Comparison | ||
| 150 | { | ||
| 151 | return new Expr\Comparison($x, Expr\Comparison::LTE, $y); | ||
| 152 | } | ||
| 153 | |||
| 154 | /** | ||
| 155 | * Creates an instance of Expr\Comparison, with the given arguments. | ||
| 156 | * First argument is considered the left expression and the second is the right expression. | ||
| 157 | * When converted to string, it will generated a <left expr> > <right expr>. Example: | ||
| 158 | * | ||
| 159 | * [php] | ||
| 160 | * // u.id > ?1 | ||
| 161 | * $q->where($q->expr()->gt('u.id', '?1')); | ||
| 162 | * | ||
| 163 | * @param mixed $x Left expression. | ||
| 164 | * @param mixed $y Right expression. | ||
| 165 | */ | ||
| 166 | public function gt(mixed $x, mixed $y): Expr\Comparison | ||
| 167 | { | ||
| 168 | return new Expr\Comparison($x, Expr\Comparison::GT, $y); | ||
| 169 | } | ||
| 170 | |||
| 171 | /** | ||
| 172 | * Creates an instance of Expr\Comparison, with the given arguments. | ||
| 173 | * First argument is considered the left expression and the second is the right expression. | ||
| 174 | * When converted to string, it will generated a <left expr> >= <right expr>. Example: | ||
| 175 | * | ||
| 176 | * [php] | ||
| 177 | * // u.id >= ?1 | ||
| 178 | * $q->where($q->expr()->gte('u.id', '?1')); | ||
| 179 | * | ||
| 180 | * @param mixed $x Left expression. | ||
| 181 | * @param mixed $y Right expression. | ||
| 182 | */ | ||
| 183 | public function gte(mixed $x, mixed $y): Expr\Comparison | ||
| 184 | { | ||
| 185 | return new Expr\Comparison($x, Expr\Comparison::GTE, $y); | ||
| 186 | } | ||
| 187 | |||
| 188 | /** | ||
| 189 | * Creates an instance of AVG() function, with the given argument. | ||
| 190 | * | ||
| 191 | * @param mixed $x Argument to be used in AVG() function. | ||
| 192 | */ | ||
| 193 | public function avg(mixed $x): Expr\Func | ||
| 194 | { | ||
| 195 | return new Expr\Func('AVG', [$x]); | ||
| 196 | } | ||
| 197 | |||
| 198 | /** | ||
| 199 | * Creates an instance of MAX() function, with the given argument. | ||
| 200 | * | ||
| 201 | * @param mixed $x Argument to be used in MAX() function. | ||
| 202 | */ | ||
| 203 | public function max(mixed $x): Expr\Func | ||
| 204 | { | ||
| 205 | return new Expr\Func('MAX', [$x]); | ||
| 206 | } | ||
| 207 | |||
| 208 | /** | ||
| 209 | * Creates an instance of MIN() function, with the given argument. | ||
| 210 | * | ||
| 211 | * @param mixed $x Argument to be used in MIN() function. | ||
| 212 | */ | ||
| 213 | public function min(mixed $x): Expr\Func | ||
| 214 | { | ||
| 215 | return new Expr\Func('MIN', [$x]); | ||
| 216 | } | ||
| 217 | |||
| 218 | /** | ||
| 219 | * Creates an instance of COUNT() function, with the given argument. | ||
| 220 | * | ||
| 221 | * @param mixed $x Argument to be used in COUNT() function. | ||
| 222 | */ | ||
| 223 | public function count(mixed $x): Expr\Func | ||
| 224 | { | ||
| 225 | return new Expr\Func('COUNT', [$x]); | ||
| 226 | } | ||
| 227 | |||
| 228 | /** | ||
| 229 | * Creates an instance of COUNT(DISTINCT) function, with the given argument. | ||
| 230 | * | ||
| 231 | * @param mixed ...$x Argument to be used in COUNT(DISTINCT) function. | ||
| 232 | */ | ||
| 233 | public function countDistinct(mixed ...$x): string | ||
| 234 | { | ||
| 235 | self::validateVariadicParameter($x); | ||
| 236 | |||
| 237 | return 'COUNT(DISTINCT ' . implode(', ', $x) . ')'; | ||
| 238 | } | ||
| 239 | |||
| 240 | /** | ||
| 241 | * Creates an instance of EXISTS() function, with the given DQL Subquery. | ||
| 242 | * | ||
| 243 | * @param mixed $subquery DQL Subquery to be used in EXISTS() function. | ||
| 244 | */ | ||
| 245 | public function exists(mixed $subquery): Expr\Func | ||
| 246 | { | ||
| 247 | return new Expr\Func('EXISTS', [$subquery]); | ||
| 248 | } | ||
| 249 | |||
| 250 | /** | ||
| 251 | * Creates an instance of ALL() function, with the given DQL Subquery. | ||
| 252 | * | ||
| 253 | * @param mixed $subquery DQL Subquery to be used in ALL() function. | ||
| 254 | */ | ||
| 255 | public function all(mixed $subquery): Expr\Func | ||
| 256 | { | ||
| 257 | return new Expr\Func('ALL', [$subquery]); | ||
| 258 | } | ||
| 259 | |||
| 260 | /** | ||
| 261 | * Creates a SOME() function expression with the given DQL subquery. | ||
| 262 | * | ||
| 263 | * @param mixed $subquery DQL Subquery to be used in SOME() function. | ||
| 264 | */ | ||
| 265 | public function some(mixed $subquery): Expr\Func | ||
| 266 | { | ||
| 267 | return new Expr\Func('SOME', [$subquery]); | ||
| 268 | } | ||
| 269 | |||
| 270 | /** | ||
| 271 | * Creates an ANY() function expression with the given DQL subquery. | ||
| 272 | * | ||
| 273 | * @param mixed $subquery DQL Subquery to be used in ANY() function. | ||
| 274 | */ | ||
| 275 | public function any(mixed $subquery): Expr\Func | ||
| 276 | { | ||
| 277 | return new Expr\Func('ANY', [$subquery]); | ||
| 278 | } | ||
| 279 | |||
| 280 | /** | ||
| 281 | * Creates a negation expression of the given restriction. | ||
| 282 | * | ||
| 283 | * @param mixed $restriction Restriction to be used in NOT() function. | ||
| 284 | */ | ||
| 285 | public function not(mixed $restriction): Expr\Func | ||
| 286 | { | ||
| 287 | return new Expr\Func('NOT', [$restriction]); | ||
| 288 | } | ||
| 289 | |||
| 290 | /** | ||
| 291 | * Creates an ABS() function expression with the given argument. | ||
| 292 | * | ||
| 293 | * @param mixed $x Argument to be used in ABS() function. | ||
| 294 | */ | ||
| 295 | public function abs(mixed $x): Expr\Func | ||
| 296 | { | ||
| 297 | return new Expr\Func('ABS', [$x]); | ||
| 298 | } | ||
| 299 | |||
| 300 | /** | ||
| 301 | * Creates a MOD($x, $y) function expression to return the remainder of $x divided by $y. | ||
| 302 | */ | ||
| 303 | public function mod(mixed $x, mixed $y): Expr\Func | ||
| 304 | { | ||
| 305 | return new Expr\Func('MOD', [$x, $y]); | ||
| 306 | } | ||
| 307 | |||
| 308 | /** | ||
| 309 | * Creates a product mathematical expression with the given arguments. | ||
| 310 | * | ||
| 311 | * First argument is considered the left expression and the second is the right expression. | ||
| 312 | * When converted to string, it will generated a <left expr> * <right expr>. Example: | ||
| 313 | * | ||
| 314 | * [php] | ||
| 315 | * // u.salary * u.percentAnnualSalaryIncrease | ||
| 316 | * $q->expr()->prod('u.salary', 'u.percentAnnualSalaryIncrease') | ||
| 317 | * | ||
| 318 | * @param mixed $x Left expression. | ||
| 319 | * @param mixed $y Right expression. | ||
| 320 | */ | ||
| 321 | public function prod(mixed $x, mixed $y): Expr\Math | ||
| 322 | { | ||
| 323 | return new Expr\Math($x, '*', $y); | ||
| 324 | } | ||
| 325 | |||
| 326 | /** | ||
| 327 | * Creates a difference mathematical expression with the given arguments. | ||
| 328 | * First argument is considered the left expression and the second is the right expression. | ||
| 329 | * When converted to string, it will generated a <left expr> - <right expr>. Example: | ||
| 330 | * | ||
| 331 | * [php] | ||
| 332 | * // u.monthlySubscriptionCount - 1 | ||
| 333 | * $q->expr()->diff('u.monthlySubscriptionCount', '1') | ||
| 334 | * | ||
| 335 | * @param mixed $x Left expression. | ||
| 336 | * @param mixed $y Right expression. | ||
| 337 | */ | ||
| 338 | public function diff(mixed $x, mixed $y): Expr\Math | ||
| 339 | { | ||
| 340 | return new Expr\Math($x, '-', $y); | ||
| 341 | } | ||
| 342 | |||
| 343 | /** | ||
| 344 | * Creates a sum mathematical expression with the given arguments. | ||
| 345 | * First argument is considered the left expression and the second is the right expression. | ||
| 346 | * When converted to string, it will generated a <left expr> + <right expr>. Example: | ||
| 347 | * | ||
| 348 | * [php] | ||
| 349 | * // u.numChildren + 1 | ||
| 350 | * $q->expr()->sum('u.numChildren', '1') | ||
| 351 | * | ||
| 352 | * @param mixed $x Left expression. | ||
| 353 | * @param mixed $y Right expression. | ||
| 354 | */ | ||
| 355 | public function sum(mixed $x, mixed $y): Expr\Math | ||
| 356 | { | ||
| 357 | return new Expr\Math($x, '+', $y); | ||
| 358 | } | ||
| 359 | |||
| 360 | /** | ||
| 361 | * Creates a quotient mathematical expression with the given arguments. | ||
| 362 | * First argument is considered the left expression and the second is the right expression. | ||
| 363 | * When converted to string, it will generated a <left expr> / <right expr>. Example: | ||
| 364 | * | ||
| 365 | * [php] | ||
| 366 | * // u.total / u.period | ||
| 367 | * $expr->quot('u.total', 'u.period') | ||
| 368 | * | ||
| 369 | * @param mixed $x Left expression. | ||
| 370 | * @param mixed $y Right expression. | ||
| 371 | */ | ||
| 372 | public function quot(mixed $x, mixed $y): Expr\Math | ||
| 373 | { | ||
| 374 | return new Expr\Math($x, '/', $y); | ||
| 375 | } | ||
| 376 | |||
| 377 | /** | ||
| 378 | * Creates a SQRT() function expression with the given argument. | ||
| 379 | * | ||
| 380 | * @param mixed $x Argument to be used in SQRT() function. | ||
| 381 | */ | ||
| 382 | public function sqrt(mixed $x): Expr\Func | ||
| 383 | { | ||
| 384 | return new Expr\Func('SQRT', [$x]); | ||
| 385 | } | ||
| 386 | |||
| 387 | /** | ||
| 388 | * Creates an IN() expression with the given arguments. | ||
| 389 | * | ||
| 390 | * @param string $x Field in string format to be restricted by IN() function. | ||
| 391 | * @param mixed $y Argument to be used in IN() function. | ||
| 392 | */ | ||
| 393 | public function in(string $x, mixed $y): Expr\Func | ||
| 394 | { | ||
| 395 | if (is_iterable($y)) { | ||
| 396 | if ($y instanceof Traversable) { | ||
| 397 | $y = iterator_to_array($y); | ||
| 398 | } | ||
| 399 | |||
| 400 | foreach ($y as &$literal) { | ||
| 401 | if (! ($literal instanceof Expr\Literal)) { | ||
| 402 | $literal = $this->quoteLiteral($literal); | ||
| 403 | } | ||
| 404 | } | ||
| 405 | } | ||
| 406 | |||
| 407 | return new Expr\Func($x . ' IN', (array) $y); | ||
| 408 | } | ||
| 409 | |||
| 410 | /** | ||
| 411 | * Creates a NOT IN() expression with the given arguments. | ||
| 412 | * | ||
| 413 | * @param string $x Field in string format to be restricted by NOT IN() function. | ||
| 414 | * @param mixed $y Argument to be used in NOT IN() function. | ||
| 415 | */ | ||
| 416 | public function notIn(string $x, mixed $y): Expr\Func | ||
| 417 | { | ||
| 418 | if (is_iterable($y)) { | ||
| 419 | if ($y instanceof Traversable) { | ||
| 420 | $y = iterator_to_array($y); | ||
| 421 | } | ||
| 422 | |||
| 423 | foreach ($y as &$literal) { | ||
| 424 | if (! ($literal instanceof Expr\Literal)) { | ||
| 425 | $literal = $this->quoteLiteral($literal); | ||
| 426 | } | ||
| 427 | } | ||
| 428 | } | ||
| 429 | |||
| 430 | return new Expr\Func($x . ' NOT IN', (array) $y); | ||
| 431 | } | ||
| 432 | |||
| 433 | /** | ||
| 434 | * Creates an IS NULL expression with the given arguments. | ||
| 435 | * | ||
| 436 | * @param string $x Field in string format to be restricted by IS NULL. | ||
| 437 | */ | ||
| 438 | public function isNull(string $x): string | ||
| 439 | { | ||
| 440 | return $x . ' IS NULL'; | ||
| 441 | } | ||
| 442 | |||
| 443 | /** | ||
| 444 | * Creates an IS NOT NULL expression with the given arguments. | ||
| 445 | * | ||
| 446 | * @param string $x Field in string format to be restricted by IS NOT NULL. | ||
| 447 | */ | ||
| 448 | public function isNotNull(string $x): string | ||
| 449 | { | ||
| 450 | return $x . ' IS NOT NULL'; | ||
| 451 | } | ||
| 452 | |||
| 453 | /** | ||
| 454 | * Creates a LIKE() comparison expression with the given arguments. | ||
| 455 | * | ||
| 456 | * @param string $x Field in string format to be inspected by LIKE() comparison. | ||
| 457 | * @param mixed $y Argument to be used in LIKE() comparison. | ||
| 458 | */ | ||
| 459 | public function like(string $x, mixed $y): Expr\Comparison | ||
| 460 | { | ||
| 461 | return new Expr\Comparison($x, 'LIKE', $y); | ||
| 462 | } | ||
| 463 | |||
| 464 | /** | ||
| 465 | * Creates a NOT LIKE() comparison expression with the given arguments. | ||
| 466 | * | ||
| 467 | * @param string $x Field in string format to be inspected by LIKE() comparison. | ||
| 468 | * @param mixed $y Argument to be used in LIKE() comparison. | ||
| 469 | */ | ||
| 470 | public function notLike(string $x, mixed $y): Expr\Comparison | ||
| 471 | { | ||
| 472 | return new Expr\Comparison($x, 'NOT LIKE', $y); | ||
| 473 | } | ||
| 474 | |||
| 475 | /** | ||
| 476 | * Creates a CONCAT() function expression with the given arguments. | ||
| 477 | * | ||
| 478 | * @param mixed ...$x Arguments to be used in CONCAT() function. | ||
| 479 | */ | ||
| 480 | public function concat(mixed ...$x): Expr\Func | ||
| 481 | { | ||
| 482 | self::validateVariadicParameter($x); | ||
| 483 | |||
| 484 | return new Expr\Func('CONCAT', $x); | ||
| 485 | } | ||
| 486 | |||
| 487 | /** | ||
| 488 | * Creates a SUBSTRING() function expression with the given arguments. | ||
| 489 | * | ||
| 490 | * @param mixed $x Argument to be used as string to be cropped by SUBSTRING() function. | ||
| 491 | * @param int $from Initial offset to start cropping string. May accept negative values. | ||
| 492 | * @param int|null $len Length of crop. May accept negative values. | ||
| 493 | */ | ||
| 494 | public function substring(mixed $x, int $from, int|null $len = null): Expr\Func | ||
| 495 | { | ||
| 496 | $args = [$x, $from]; | ||
| 497 | if ($len !== null) { | ||
| 498 | $args[] = $len; | ||
| 499 | } | ||
| 500 | |||
| 501 | return new Expr\Func('SUBSTRING', $args); | ||
| 502 | } | ||
| 503 | |||
| 504 | /** | ||
| 505 | * Creates a LOWER() function expression with the given argument. | ||
| 506 | * | ||
| 507 | * @param mixed $x Argument to be used in LOWER() function. | ||
| 508 | * | ||
| 509 | * @return Expr\Func A LOWER function expression. | ||
| 510 | */ | ||
| 511 | public function lower(mixed $x): Expr\Func | ||
| 512 | { | ||
| 513 | return new Expr\Func('LOWER', [$x]); | ||
| 514 | } | ||
| 515 | |||
| 516 | /** | ||
| 517 | * Creates an UPPER() function expression with the given argument. | ||
| 518 | * | ||
| 519 | * @param mixed $x Argument to be used in UPPER() function. | ||
| 520 | * | ||
| 521 | * @return Expr\Func An UPPER function expression. | ||
| 522 | */ | ||
| 523 | public function upper(mixed $x): Expr\Func | ||
| 524 | { | ||
| 525 | return new Expr\Func('UPPER', [$x]); | ||
| 526 | } | ||
| 527 | |||
| 528 | /** | ||
| 529 | * Creates a LENGTH() function expression with the given argument. | ||
| 530 | * | ||
| 531 | * @param mixed $x Argument to be used as argument of LENGTH() function. | ||
| 532 | * | ||
| 533 | * @return Expr\Func A LENGTH function expression. | ||
| 534 | */ | ||
| 535 | public function length(mixed $x): Expr\Func | ||
| 536 | { | ||
| 537 | return new Expr\Func('LENGTH', [$x]); | ||
| 538 | } | ||
| 539 | |||
| 540 | /** | ||
| 541 | * Creates a literal expression of the given argument. | ||
| 542 | * | ||
| 543 | * @param scalar $literal Argument to be converted to literal. | ||
| 544 | */ | ||
| 545 | public function literal(bool|string|int|float $literal): Expr\Literal | ||
| 546 | { | ||
| 547 | return new Expr\Literal($this->quoteLiteral($literal)); | ||
| 548 | } | ||
| 549 | |||
| 550 | /** | ||
| 551 | * Quotes a literal value, if necessary, according to the DQL syntax. | ||
| 552 | * | ||
| 553 | * @param scalar $literal The literal value. | ||
| 554 | */ | ||
| 555 | private function quoteLiteral(bool|string|int|float $literal): string | ||
| 556 | { | ||
| 557 | if (is_int($literal) || is_float($literal)) { | ||
| 558 | return (string) $literal; | ||
| 559 | } | ||
| 560 | |||
| 561 | if (is_bool($literal)) { | ||
| 562 | return $literal ? 'true' : 'false'; | ||
| 563 | } | ||
| 564 | |||
| 565 | return "'" . str_replace("'", "''", $literal) . "'"; | ||
| 566 | } | ||
| 567 | |||
| 568 | /** | ||
| 569 | * Creates an instance of BETWEEN() function, with the given argument. | ||
| 570 | * | ||
| 571 | * @param mixed $val Valued to be inspected by range values. | ||
| 572 | * @param int|string $x Starting range value to be used in BETWEEN() function. | ||
| 573 | * @param int|string $y End point value to be used in BETWEEN() function. | ||
| 574 | * | ||
| 575 | * @return string A BETWEEN expression. | ||
| 576 | */ | ||
| 577 | public function between(mixed $val, int|string $x, int|string $y): string | ||
| 578 | { | ||
| 579 | return $val . ' BETWEEN ' . $x . ' AND ' . $y; | ||
| 580 | } | ||
| 581 | |||
| 582 | /** | ||
| 583 | * Creates an instance of TRIM() function, with the given argument. | ||
| 584 | * | ||
| 585 | * @param mixed $x Argument to be used as argument of TRIM() function. | ||
| 586 | * | ||
| 587 | * @return Expr\Func a TRIM expression. | ||
| 588 | */ | ||
| 589 | public function trim(mixed $x): Expr\Func | ||
| 590 | { | ||
| 591 | return new Expr\Func('TRIM', $x); | ||
| 592 | } | ||
| 593 | |||
| 594 | /** | ||
| 595 | * Creates an instance of MEMBER OF function, with the given arguments. | ||
| 596 | * | ||
| 597 | * @param string $x Value to be checked | ||
| 598 | * @param string $y Value to be checked against | ||
| 599 | */ | ||
| 600 | public function isMemberOf(string $x, string $y): Expr\Comparison | ||
| 601 | { | ||
| 602 | return new Expr\Comparison($x, 'MEMBER OF', $y); | ||
| 603 | } | ||
| 604 | |||
| 605 | /** | ||
| 606 | * Creates an instance of INSTANCE OF function, with the given arguments. | ||
| 607 | * | ||
| 608 | * @param string $x Value to be checked | ||
| 609 | * @param string $y Value to be checked against | ||
| 610 | */ | ||
| 611 | public function isInstanceOf(string $x, string $y): Expr\Comparison | ||
| 612 | { | ||
| 613 | return new Expr\Comparison($x, 'INSTANCE OF', $y); | ||
| 614 | } | ||
| 615 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/Expr/Andx.php b/vendor/doctrine/orm/src/Query/Expr/Andx.php new file mode 100644 index 0000000..a20bcef --- /dev/null +++ b/vendor/doctrine/orm/src/Query/Expr/Andx.php | |||
| @@ -0,0 +1,32 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Query\Expr; | ||
| 6 | |||
| 7 | /** | ||
| 8 | * Expression class for building DQL and parts. | ||
| 9 | * | ||
| 10 | * @link www.doctrine-project.org | ||
| 11 | */ | ||
| 12 | class Andx extends Composite | ||
| 13 | { | ||
| 14 | protected string $separator = ' AND '; | ||
| 15 | |||
| 16 | /** @var string[] */ | ||
| 17 | protected array $allowedClasses = [ | ||
| 18 | Comparison::class, | ||
| 19 | Func::class, | ||
| 20 | Orx::class, | ||
| 21 | self::class, | ||
| 22 | ]; | ||
| 23 | |||
| 24 | /** @psalm-var list<string|Comparison|Func|Orx|self> */ | ||
| 25 | protected array $parts = []; | ||
| 26 | |||
| 27 | /** @psalm-return list<string|Comparison|Func|Orx|self> */ | ||
| 28 | public function getParts(): array | ||
| 29 | { | ||
| 30 | return $this->parts; | ||
| 31 | } | ||
| 32 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/Expr/Base.php b/vendor/doctrine/orm/src/Query/Expr/Base.php new file mode 100644 index 0000000..e0f2572 --- /dev/null +++ b/vendor/doctrine/orm/src/Query/Expr/Base.php | |||
| @@ -0,0 +1,96 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Query\Expr; | ||
| 6 | |||
| 7 | use InvalidArgumentException; | ||
| 8 | use Stringable; | ||
| 9 | |||
| 10 | use function array_key_exists; | ||
| 11 | use function count; | ||
| 12 | use function get_debug_type; | ||
| 13 | use function implode; | ||
| 14 | use function in_array; | ||
| 15 | use function is_array; | ||
| 16 | use function is_string; | ||
| 17 | use function sprintf; | ||
| 18 | |||
| 19 | /** | ||
| 20 | * Abstract base Expr class for building DQL parts. | ||
| 21 | * | ||
| 22 | * @link www.doctrine-project.org | ||
| 23 | */ | ||
| 24 | abstract class Base implements Stringable | ||
| 25 | { | ||
| 26 | protected string $preSeparator = '('; | ||
| 27 | protected string $separator = ', '; | ||
| 28 | protected string $postSeparator = ')'; | ||
| 29 | |||
| 30 | /** @var list<class-string> */ | ||
| 31 | protected array $allowedClasses = []; | ||
| 32 | |||
| 33 | /** @var list<string|Stringable> */ | ||
| 34 | protected array $parts = []; | ||
| 35 | |||
| 36 | public function __construct(mixed $args = []) | ||
| 37 | { | ||
| 38 | if (is_array($args) && array_key_exists(0, $args) && is_array($args[0])) { | ||
| 39 | $args = $args[0]; | ||
| 40 | } | ||
| 41 | |||
| 42 | $this->addMultiple($args); | ||
| 43 | } | ||
| 44 | |||
| 45 | /** | ||
| 46 | * @param string[]|object[]|string|object $args | ||
| 47 | * @psalm-param list<string|object>|string|object $args | ||
| 48 | * | ||
| 49 | * @return $this | ||
| 50 | */ | ||
| 51 | public function addMultiple(array|string|object $args = []): static | ||
| 52 | { | ||
| 53 | foreach ((array) $args as $arg) { | ||
| 54 | $this->add($arg); | ||
| 55 | } | ||
| 56 | |||
| 57 | return $this; | ||
| 58 | } | ||
| 59 | |||
| 60 | /** | ||
| 61 | * @return $this | ||
| 62 | * | ||
| 63 | * @throws InvalidArgumentException | ||
| 64 | */ | ||
| 65 | public function add(mixed $arg): static | ||
| 66 | { | ||
| 67 | if ($arg !== null && (! $arg instanceof self || $arg->count() > 0)) { | ||
| 68 | // If we decide to keep Expr\Base instances, we can use this check | ||
| 69 | if (! is_string($arg) && ! in_array($arg::class, $this->allowedClasses, true)) { | ||
| 70 | throw new InvalidArgumentException(sprintf( | ||
| 71 | "Expression of type '%s' not allowed in this context.", | ||
| 72 | get_debug_type($arg), | ||
| 73 | )); | ||
| 74 | } | ||
| 75 | |||
| 76 | $this->parts[] = $arg; | ||
| 77 | } | ||
| 78 | |||
| 79 | return $this; | ||
| 80 | } | ||
| 81 | |||
| 82 | /** @psalm-return 0|positive-int */ | ||
| 83 | public function count(): int | ||
| 84 | { | ||
| 85 | return count($this->parts); | ||
| 86 | } | ||
| 87 | |||
| 88 | public function __toString(): string | ||
| 89 | { | ||
| 90 | if ($this->count() === 1) { | ||
| 91 | return (string) $this->parts[0]; | ||
| 92 | } | ||
| 93 | |||
| 94 | return $this->preSeparator . implode($this->separator, $this->parts) . $this->postSeparator; | ||
| 95 | } | ||
| 96 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/Expr/Comparison.php b/vendor/doctrine/orm/src/Query/Expr/Comparison.php new file mode 100644 index 0000000..ec8ef21 --- /dev/null +++ b/vendor/doctrine/orm/src/Query/Expr/Comparison.php | |||
| @@ -0,0 +1,47 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Query\Expr; | ||
| 6 | |||
| 7 | use Stringable; | ||
| 8 | |||
| 9 | /** | ||
| 10 | * Expression class for DQL comparison expressions. | ||
| 11 | * | ||
| 12 | * @link www.doctrine-project.org | ||
| 13 | */ | ||
| 14 | class Comparison implements Stringable | ||
| 15 | { | ||
| 16 | final public const EQ = '='; | ||
| 17 | final public const NEQ = '<>'; | ||
| 18 | final public const LT = '<'; | ||
| 19 | final public const LTE = '<='; | ||
| 20 | final public const GT = '>'; | ||
| 21 | final public const GTE = '>='; | ||
| 22 | |||
| 23 | /** Creates a comparison expression with the given arguments. */ | ||
| 24 | public function __construct(protected mixed $leftExpr, protected string $operator, protected mixed $rightExpr) | ||
| 25 | { | ||
| 26 | } | ||
| 27 | |||
| 28 | public function getLeftExpr(): mixed | ||
| 29 | { | ||
| 30 | return $this->leftExpr; | ||
| 31 | } | ||
| 32 | |||
| 33 | public function getOperator(): string | ||
| 34 | { | ||
| 35 | return $this->operator; | ||
| 36 | } | ||
| 37 | |||
| 38 | public function getRightExpr(): mixed | ||
| 39 | { | ||
| 40 | return $this->rightExpr; | ||
| 41 | } | ||
| 42 | |||
| 43 | public function __toString(): string | ||
| 44 | { | ||
| 45 | return $this->leftExpr . ' ' . $this->operator . ' ' . $this->rightExpr; | ||
| 46 | } | ||
| 47 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/Expr/Composite.php b/vendor/doctrine/orm/src/Query/Expr/Composite.php new file mode 100644 index 0000000..f3007a7 --- /dev/null +++ b/vendor/doctrine/orm/src/Query/Expr/Composite.php | |||
| @@ -0,0 +1,50 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Query\Expr; | ||
| 6 | |||
| 7 | use Stringable; | ||
| 8 | |||
| 9 | use function implode; | ||
| 10 | use function is_object; | ||
| 11 | use function preg_match; | ||
| 12 | |||
| 13 | /** | ||
| 14 | * Expression class for building DQL and parts. | ||
| 15 | * | ||
| 16 | * @link www.doctrine-project.org | ||
| 17 | */ | ||
| 18 | class Composite extends Base | ||
| 19 | { | ||
| 20 | public function __toString(): string | ||
| 21 | { | ||
| 22 | if ($this->count() === 1) { | ||
| 23 | return (string) $this->parts[0]; | ||
| 24 | } | ||
| 25 | |||
| 26 | $components = []; | ||
| 27 | |||
| 28 | foreach ($this->parts as $part) { | ||
| 29 | $components[] = $this->processQueryPart($part); | ||
| 30 | } | ||
| 31 | |||
| 32 | return implode($this->separator, $components); | ||
| 33 | } | ||
| 34 | |||
| 35 | private function processQueryPart(string|Stringable $part): string | ||
| 36 | { | ||
| 37 | $queryPart = (string) $part; | ||
| 38 | |||
| 39 | if (is_object($part) && $part instanceof self && $part->count() > 1) { | ||
| 40 | return $this->preSeparator . $queryPart . $this->postSeparator; | ||
| 41 | } | ||
| 42 | |||
| 43 | // Fixes DDC-1237: User may have added a where item containing nested expression (with "OR" or "AND") | ||
| 44 | if (preg_match('/\s(OR|AND)\s/i', $queryPart)) { | ||
| 45 | return $this->preSeparator . $queryPart . $this->postSeparator; | ||
| 46 | } | ||
| 47 | |||
| 48 | return $queryPart; | ||
| 49 | } | ||
| 50 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/Expr/From.php b/vendor/doctrine/orm/src/Query/Expr/From.php new file mode 100644 index 0000000..21af078 --- /dev/null +++ b/vendor/doctrine/orm/src/Query/Expr/From.php | |||
| @@ -0,0 +1,48 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Query\Expr; | ||
| 6 | |||
| 7 | use Stringable; | ||
| 8 | |||
| 9 | /** | ||
| 10 | * Expression class for DQL from. | ||
| 11 | * | ||
| 12 | * @link www.doctrine-project.org | ||
| 13 | */ | ||
| 14 | class From implements Stringable | ||
| 15 | { | ||
| 16 | /** | ||
| 17 | * @param class-string $from The class name. | ||
| 18 | * @param string $alias The alias of the class. | ||
| 19 | */ | ||
| 20 | public function __construct( | ||
| 21 | protected string $from, | ||
| 22 | protected string $alias, | ||
| 23 | protected string|null $indexBy = null, | ||
| 24 | ) { | ||
| 25 | } | ||
| 26 | |||
| 27 | /** @return class-string */ | ||
| 28 | public function getFrom(): string | ||
| 29 | { | ||
| 30 | return $this->from; | ||
| 31 | } | ||
| 32 | |||
| 33 | public function getAlias(): string | ||
| 34 | { | ||
| 35 | return $this->alias; | ||
| 36 | } | ||
| 37 | |||
| 38 | public function getIndexBy(): string|null | ||
| 39 | { | ||
| 40 | return $this->indexBy; | ||
| 41 | } | ||
| 42 | |||
| 43 | public function __toString(): string | ||
| 44 | { | ||
| 45 | return $this->from . ' ' . $this->alias . | ||
| 46 | ($this->indexBy ? ' INDEX BY ' . $this->indexBy : ''); | ||
| 47 | } | ||
| 48 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/Expr/Func.php b/vendor/doctrine/orm/src/Query/Expr/Func.php new file mode 100644 index 0000000..cd9e8e0 --- /dev/null +++ b/vendor/doctrine/orm/src/Query/Expr/Func.php | |||
| @@ -0,0 +1,48 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Query\Expr; | ||
| 6 | |||
| 7 | use Stringable; | ||
| 8 | |||
| 9 | use function implode; | ||
| 10 | |||
| 11 | /** | ||
| 12 | * Expression class for generating DQL functions. | ||
| 13 | * | ||
| 14 | * @link www.doctrine-project.org | ||
| 15 | */ | ||
| 16 | class Func implements Stringable | ||
| 17 | { | ||
| 18 | /** @var mixed[] */ | ||
| 19 | protected array $arguments; | ||
| 20 | |||
| 21 | /** | ||
| 22 | * Creates a function, with the given argument. | ||
| 23 | * | ||
| 24 | * @psalm-param list<mixed>|mixed $arguments | ||
| 25 | */ | ||
| 26 | public function __construct( | ||
| 27 | protected string $name, | ||
| 28 | mixed $arguments, | ||
| 29 | ) { | ||
| 30 | $this->arguments = (array) $arguments; | ||
| 31 | } | ||
| 32 | |||
| 33 | public function getName(): string | ||
| 34 | { | ||
| 35 | return $this->name; | ||
| 36 | } | ||
| 37 | |||
| 38 | /** @psalm-return list<mixed> */ | ||
| 39 | public function getArguments(): array | ||
| 40 | { | ||
| 41 | return $this->arguments; | ||
| 42 | } | ||
| 43 | |||
| 44 | public function __toString(): string | ||
| 45 | { | ||
| 46 | return $this->name . '(' . implode(', ', $this->arguments) . ')'; | ||
| 47 | } | ||
| 48 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/Expr/GroupBy.php b/vendor/doctrine/orm/src/Query/Expr/GroupBy.php new file mode 100644 index 0000000..fa4625a --- /dev/null +++ b/vendor/doctrine/orm/src/Query/Expr/GroupBy.php | |||
| @@ -0,0 +1,25 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Query\Expr; | ||
| 6 | |||
| 7 | /** | ||
| 8 | * Expression class for building DQL Group By parts. | ||
| 9 | * | ||
| 10 | * @link www.doctrine-project.org | ||
| 11 | */ | ||
| 12 | class GroupBy extends Base | ||
| 13 | { | ||
| 14 | protected string $preSeparator = ''; | ||
| 15 | protected string $postSeparator = ''; | ||
| 16 | |||
| 17 | /** @psalm-var list<string> */ | ||
| 18 | protected array $parts = []; | ||
| 19 | |||
| 20 | /** @psalm-return list<string> */ | ||
| 21 | public function getParts(): array | ||
| 22 | { | ||
| 23 | return $this->parts; | ||
| 24 | } | ||
| 25 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/Expr/Join.php b/vendor/doctrine/orm/src/Query/Expr/Join.php new file mode 100644 index 0000000..c3b6dc9 --- /dev/null +++ b/vendor/doctrine/orm/src/Query/Expr/Join.php | |||
| @@ -0,0 +1,77 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Query\Expr; | ||
| 6 | |||
| 7 | use Stringable; | ||
| 8 | |||
| 9 | use function strtoupper; | ||
| 10 | |||
| 11 | /** | ||
| 12 | * Expression class for DQL join. | ||
| 13 | * | ||
| 14 | * @link www.doctrine-project.org | ||
| 15 | */ | ||
| 16 | class Join implements Stringable | ||
| 17 | { | ||
| 18 | final public const INNER_JOIN = 'INNER'; | ||
| 19 | final public const LEFT_JOIN = 'LEFT'; | ||
| 20 | |||
| 21 | final public const ON = 'ON'; | ||
| 22 | final public const WITH = 'WITH'; | ||
| 23 | |||
| 24 | /** | ||
| 25 | * @psalm-param self::INNER_JOIN|self::LEFT_JOIN $joinType | ||
| 26 | * @psalm-param self::ON|self::WITH|null $conditionType | ||
| 27 | */ | ||
| 28 | public function __construct( | ||
| 29 | protected string $joinType, | ||
| 30 | protected string $join, | ||
| 31 | protected string|null $alias = null, | ||
| 32 | protected string|null $conditionType = null, | ||
| 33 | protected string|Comparison|Composite|Func|null $condition = null, | ||
| 34 | protected string|null $indexBy = null, | ||
| 35 | ) { | ||
| 36 | } | ||
| 37 | |||
| 38 | /** @psalm-return self::INNER_JOIN|self::LEFT_JOIN */ | ||
| 39 | public function getJoinType(): string | ||
| 40 | { | ||
| 41 | return $this->joinType; | ||
| 42 | } | ||
| 43 | |||
| 44 | public function getJoin(): string | ||
| 45 | { | ||
| 46 | return $this->join; | ||
| 47 | } | ||
| 48 | |||
| 49 | public function getAlias(): string|null | ||
| 50 | { | ||
| 51 | return $this->alias; | ||
| 52 | } | ||
| 53 | |||
| 54 | /** @psalm-return self::ON|self::WITH|null */ | ||
| 55 | public function getConditionType(): string|null | ||
| 56 | { | ||
| 57 | return $this->conditionType; | ||
| 58 | } | ||
| 59 | |||
| 60 | public function getCondition(): string|Comparison|Composite|Func|null | ||
| 61 | { | ||
| 62 | return $this->condition; | ||
| 63 | } | ||
| 64 | |||
| 65 | public function getIndexBy(): string|null | ||
| 66 | { | ||
| 67 | return $this->indexBy; | ||
| 68 | } | ||
| 69 | |||
| 70 | public function __toString(): string | ||
| 71 | { | ||
| 72 | return strtoupper($this->joinType) . ' JOIN ' . $this->join | ||
| 73 | . ($this->alias ? ' ' . $this->alias : '') | ||
| 74 | . ($this->indexBy ? ' INDEX BY ' . $this->indexBy : '') | ||
| 75 | . ($this->condition ? ' ' . strtoupper($this->conditionType) . ' ' . $this->condition : ''); | ||
| 76 | } | ||
| 77 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/Expr/Literal.php b/vendor/doctrine/orm/src/Query/Expr/Literal.php new file mode 100644 index 0000000..0c13030 --- /dev/null +++ b/vendor/doctrine/orm/src/Query/Expr/Literal.php | |||
| @@ -0,0 +1,25 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Query\Expr; | ||
| 6 | |||
| 7 | /** | ||
| 8 | * Expression class for generating DQL functions. | ||
| 9 | * | ||
| 10 | * @link www.doctrine-project.org | ||
| 11 | */ | ||
| 12 | class Literal extends Base | ||
| 13 | { | ||
| 14 | protected string $preSeparator = ''; | ||
| 15 | protected string $postSeparator = ''; | ||
| 16 | |||
| 17 | /** @psalm-var list<string> */ | ||
| 18 | protected array $parts = []; | ||
| 19 | |||
| 20 | /** @psalm-return list<string> */ | ||
| 21 | public function getParts(): array | ||
| 22 | { | ||
| 23 | return $this->parts; | ||
| 24 | } | ||
| 25 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/Expr/Math.php b/vendor/doctrine/orm/src/Query/Expr/Math.php new file mode 100644 index 0000000..05e0b39 --- /dev/null +++ b/vendor/doctrine/orm/src/Query/Expr/Math.php | |||
| @@ -0,0 +1,59 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Query\Expr; | ||
| 6 | |||
| 7 | use Stringable; | ||
| 8 | |||
| 9 | /** | ||
| 10 | * Expression class for DQL math statements. | ||
| 11 | * | ||
| 12 | * @link www.doctrine-project.org | ||
| 13 | */ | ||
| 14 | class Math implements Stringable | ||
| 15 | { | ||
| 16 | /** | ||
| 17 | * Creates a mathematical expression with the given arguments. | ||
| 18 | */ | ||
| 19 | public function __construct( | ||
| 20 | protected mixed $leftExpr, | ||
| 21 | protected string $operator, | ||
| 22 | protected mixed $rightExpr, | ||
| 23 | ) { | ||
| 24 | } | ||
| 25 | |||
| 26 | public function getLeftExpr(): mixed | ||
| 27 | { | ||
| 28 | return $this->leftExpr; | ||
| 29 | } | ||
| 30 | |||
| 31 | public function getOperator(): string | ||
| 32 | { | ||
| 33 | return $this->operator; | ||
| 34 | } | ||
| 35 | |||
| 36 | public function getRightExpr(): mixed | ||
| 37 | { | ||
| 38 | return $this->rightExpr; | ||
| 39 | } | ||
| 40 | |||
| 41 | public function __toString(): string | ||
| 42 | { | ||
| 43 | // Adjusting Left Expression | ||
| 44 | $leftExpr = (string) $this->leftExpr; | ||
| 45 | |||
| 46 | if ($this->leftExpr instanceof Math) { | ||
| 47 | $leftExpr = '(' . $leftExpr . ')'; | ||
| 48 | } | ||
| 49 | |||
| 50 | // Adjusting Right Expression | ||
| 51 | $rightExpr = (string) $this->rightExpr; | ||
| 52 | |||
| 53 | if ($this->rightExpr instanceof Math) { | ||
| 54 | $rightExpr = '(' . $rightExpr . ')'; | ||
| 55 | } | ||
| 56 | |||
| 57 | return $leftExpr . ' ' . $this->operator . ' ' . $rightExpr; | ||
| 58 | } | ||
| 59 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/Expr/OrderBy.php b/vendor/doctrine/orm/src/Query/Expr/OrderBy.php new file mode 100644 index 0000000..ac9e160 --- /dev/null +++ b/vendor/doctrine/orm/src/Query/Expr/OrderBy.php | |||
| @@ -0,0 +1,60 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Query\Expr; | ||
| 6 | |||
| 7 | use Stringable; | ||
| 8 | |||
| 9 | use function count; | ||
| 10 | use function implode; | ||
| 11 | |||
| 12 | /** | ||
| 13 | * Expression class for building DQL Order By parts. | ||
| 14 | * | ||
| 15 | * @link www.doctrine-project.org | ||
| 16 | */ | ||
| 17 | class OrderBy implements Stringable | ||
| 18 | { | ||
| 19 | protected string $preSeparator = ''; | ||
| 20 | protected string $separator = ', '; | ||
| 21 | protected string $postSeparator = ''; | ||
| 22 | |||
| 23 | /** @var string[] */ | ||
| 24 | protected array $allowedClasses = []; | ||
| 25 | |||
| 26 | /** @psalm-var list<string> */ | ||
| 27 | protected array $parts = []; | ||
| 28 | |||
| 29 | public function __construct( | ||
| 30 | string|null $sort = null, | ||
| 31 | string|null $order = null, | ||
| 32 | ) { | ||
| 33 | if ($sort) { | ||
| 34 | $this->add($sort, $order); | ||
| 35 | } | ||
| 36 | } | ||
| 37 | |||
| 38 | public function add(string $sort, string|null $order = null): void | ||
| 39 | { | ||
| 40 | $order = ! $order ? 'ASC' : $order; | ||
| 41 | $this->parts[] = $sort . ' ' . $order; | ||
| 42 | } | ||
| 43 | |||
| 44 | /** @psalm-return 0|positive-int */ | ||
| 45 | public function count(): int | ||
| 46 | { | ||
| 47 | return count($this->parts); | ||
| 48 | } | ||
| 49 | |||
| 50 | /** @psalm-return list<string> */ | ||
| 51 | public function getParts(): array | ||
| 52 | { | ||
| 53 | return $this->parts; | ||
| 54 | } | ||
| 55 | |||
| 56 | public function __toString(): string | ||
| 57 | { | ||
| 58 | return $this->preSeparator . implode($this->separator, $this->parts) . $this->postSeparator; | ||
| 59 | } | ||
| 60 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/Expr/Orx.php b/vendor/doctrine/orm/src/Query/Expr/Orx.php new file mode 100644 index 0000000..2ae2332 --- /dev/null +++ b/vendor/doctrine/orm/src/Query/Expr/Orx.php | |||
| @@ -0,0 +1,32 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Query\Expr; | ||
| 6 | |||
| 7 | /** | ||
| 8 | * Expression class for building DQL OR clauses. | ||
| 9 | * | ||
| 10 | * @link www.doctrine-project.org | ||
| 11 | */ | ||
| 12 | class Orx extends Composite | ||
| 13 | { | ||
| 14 | protected string $separator = ' OR '; | ||
| 15 | |||
| 16 | /** @var string[] */ | ||
| 17 | protected array $allowedClasses = [ | ||
| 18 | Comparison::class, | ||
| 19 | Func::class, | ||
| 20 | Andx::class, | ||
| 21 | self::class, | ||
| 22 | ]; | ||
| 23 | |||
| 24 | /** @psalm-var list<string|Comparison|Func|Andx|self> */ | ||
| 25 | protected array $parts = []; | ||
| 26 | |||
| 27 | /** @psalm-return list<string|Comparison|Func|Andx|self> */ | ||
| 28 | public function getParts(): array | ||
| 29 | { | ||
| 30 | return $this->parts; | ||
| 31 | } | ||
| 32 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/Expr/Select.php b/vendor/doctrine/orm/src/Query/Expr/Select.php new file mode 100644 index 0000000..91b0b60 --- /dev/null +++ b/vendor/doctrine/orm/src/Query/Expr/Select.php | |||
| @@ -0,0 +1,28 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Query\Expr; | ||
| 6 | |||
| 7 | /** | ||
| 8 | * Expression class for building DQL select statements. | ||
| 9 | * | ||
| 10 | * @link www.doctrine-project.org | ||
| 11 | */ | ||
| 12 | class Select extends Base | ||
| 13 | { | ||
| 14 | protected string $preSeparator = ''; | ||
| 15 | protected string $postSeparator = ''; | ||
| 16 | |||
| 17 | /** @var string[] */ | ||
| 18 | protected array $allowedClasses = [Func::class]; | ||
| 19 | |||
| 20 | /** @psalm-var list<string|Func> */ | ||
| 21 | protected array $parts = []; | ||
| 22 | |||
| 23 | /** @psalm-return list<string|Func> */ | ||
| 24 | public function getParts(): array | ||
| 25 | { | ||
| 26 | return $this->parts; | ||
| 27 | } | ||
| 28 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/Filter/FilterException.php b/vendor/doctrine/orm/src/Query/Filter/FilterException.php new file mode 100644 index 0000000..37f12bc --- /dev/null +++ b/vendor/doctrine/orm/src/Query/Filter/FilterException.php | |||
| @@ -0,0 +1,23 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Query\Filter; | ||
| 6 | |||
| 7 | use Doctrine\ORM\Exception\ORMException; | ||
| 8 | use LogicException; | ||
| 9 | |||
| 10 | use function sprintf; | ||
| 11 | |||
| 12 | class FilterException extends LogicException implements ORMException | ||
| 13 | { | ||
| 14 | public static function cannotConvertListParameterIntoSingleValue(string $name): self | ||
| 15 | { | ||
| 16 | return new self(sprintf('Cannot convert list-based SQL filter parameter "%s" into a single value.', $name)); | ||
| 17 | } | ||
| 18 | |||
| 19 | public static function cannotConvertSingleParameterIntoListValue(string $name): self | ||
| 20 | { | ||
| 21 | return new self(sprintf('Cannot convert single SQL filter parameter "%s" into a list value.', $name)); | ||
| 22 | } | ||
| 23 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/Filter/SQLFilter.php b/vendor/doctrine/orm/src/Query/Filter/SQLFilter.php new file mode 100644 index 0000000..29f3775 --- /dev/null +++ b/vendor/doctrine/orm/src/Query/Filter/SQLFilter.php | |||
| @@ -0,0 +1,174 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Query\Filter; | ||
| 6 | |||
| 7 | use Doctrine\DBAL\Connection; | ||
| 8 | use Doctrine\DBAL\Types\Types; | ||
| 9 | use Doctrine\ORM\EntityManagerInterface; | ||
| 10 | use Doctrine\ORM\Mapping\ClassMetadata; | ||
| 11 | use Doctrine\ORM\Query\ParameterTypeInferer; | ||
| 12 | use InvalidArgumentException; | ||
| 13 | use Stringable; | ||
| 14 | |||
| 15 | use function array_map; | ||
| 16 | use function implode; | ||
| 17 | use function ksort; | ||
| 18 | use function serialize; | ||
| 19 | |||
| 20 | /** | ||
| 21 | * The base class that user defined filters should extend. | ||
| 22 | * | ||
| 23 | * Handles the setting and escaping of parameters. | ||
| 24 | * | ||
| 25 | * @abstract | ||
| 26 | */ | ||
| 27 | abstract class SQLFilter implements Stringable | ||
| 28 | { | ||
| 29 | /** | ||
| 30 | * Parameters for the filter. | ||
| 31 | * | ||
| 32 | * @psalm-var array<string,array{type: string, value: mixed, is_list: bool}> | ||
| 33 | */ | ||
| 34 | private array $parameters = []; | ||
| 35 | |||
| 36 | final public function __construct( | ||
| 37 | private readonly EntityManagerInterface $em, | ||
| 38 | ) { | ||
| 39 | } | ||
| 40 | |||
| 41 | /** | ||
| 42 | * Sets a parameter list that can be used by the filter. | ||
| 43 | * | ||
| 44 | * @param array<mixed> $values List of parameter values. | ||
| 45 | * @param string $type The parameter type. If specified, the given value will be run through | ||
| 46 | * the type conversion of this type. | ||
| 47 | * | ||
| 48 | * @return $this | ||
| 49 | */ | ||
| 50 | final public function setParameterList(string $name, array $values, string $type = Types::STRING): static | ||
| 51 | { | ||
| 52 | $this->parameters[$name] = ['value' => $values, 'type' => $type, 'is_list' => true]; | ||
| 53 | |||
| 54 | // Keep the parameters sorted for the hash | ||
| 55 | ksort($this->parameters); | ||
| 56 | |||
| 57 | // The filter collection of the EM is now dirty | ||
| 58 | $this->em->getFilters()->setFiltersStateDirty(); | ||
| 59 | |||
| 60 | return $this; | ||
| 61 | } | ||
| 62 | |||
| 63 | /** | ||
| 64 | * Sets a parameter that can be used by the filter. | ||
| 65 | * | ||
| 66 | * @param string|null $type The parameter type. If specified, the given value will be run through | ||
| 67 | * the type conversion of this type. This is usually not needed for | ||
| 68 | * strings and numeric types. | ||
| 69 | * | ||
| 70 | * @return $this | ||
| 71 | */ | ||
| 72 | final public function setParameter(string $name, mixed $value, string|null $type = null): static | ||
| 73 | { | ||
| 74 | if ($type === null) { | ||
| 75 | $type = ParameterTypeInferer::inferType($value); | ||
| 76 | } | ||
| 77 | |||
| 78 | $this->parameters[$name] = ['value' => $value, 'type' => $type, 'is_list' => false]; | ||
| 79 | |||
| 80 | // Keep the parameters sorted for the hash | ||
| 81 | ksort($this->parameters); | ||
| 82 | |||
| 83 | // The filter collection of the EM is now dirty | ||
| 84 | $this->em->getFilters()->setFiltersStateDirty(); | ||
| 85 | |||
| 86 | return $this; | ||
| 87 | } | ||
| 88 | |||
| 89 | /** | ||
| 90 | * Gets a parameter to use in a query. | ||
| 91 | * | ||
| 92 | * The function is responsible for the right output escaping to use the | ||
| 93 | * value in a query. | ||
| 94 | * | ||
| 95 | * @return string The SQL escaped parameter to use in a query. | ||
| 96 | * | ||
| 97 | * @throws InvalidArgumentException | ||
| 98 | */ | ||
| 99 | final public function getParameter(string $name): string | ||
| 100 | { | ||
| 101 | if (! isset($this->parameters[$name])) { | ||
| 102 | throw new InvalidArgumentException("Parameter '" . $name . "' does not exist."); | ||
| 103 | } | ||
| 104 | |||
| 105 | if ($this->parameters[$name]['is_list']) { | ||
| 106 | throw FilterException::cannotConvertListParameterIntoSingleValue($name); | ||
| 107 | } | ||
| 108 | |||
| 109 | return $this->em->getConnection()->quote((string) $this->parameters[$name]['value']); | ||
| 110 | } | ||
| 111 | |||
| 112 | /** | ||
| 113 | * Gets a parameter to use in a query assuming it's a list of entries. | ||
| 114 | * | ||
| 115 | * The function is responsible for the right output escaping to use the | ||
| 116 | * value in a query, separating each entry by comma to inline it into | ||
| 117 | * an IN() query part. | ||
| 118 | * | ||
| 119 | * @throws InvalidArgumentException | ||
| 120 | */ | ||
| 121 | final public function getParameterList(string $name): string | ||
| 122 | { | ||
| 123 | if (! isset($this->parameters[$name])) { | ||
| 124 | throw new InvalidArgumentException("Parameter '" . $name . "' does not exist."); | ||
| 125 | } | ||
| 126 | |||
| 127 | if ($this->parameters[$name]['is_list'] === false) { | ||
| 128 | throw FilterException::cannotConvertSingleParameterIntoListValue($name); | ||
| 129 | } | ||
| 130 | |||
| 131 | $param = $this->parameters[$name]; | ||
| 132 | $connection = $this->em->getConnection(); | ||
| 133 | |||
| 134 | $quoted = array_map( | ||
| 135 | static fn (mixed $value): string => $connection->quote((string) $value), | ||
| 136 | $param['value'], | ||
| 137 | ); | ||
| 138 | |||
| 139 | return implode(',', $quoted); | ||
| 140 | } | ||
| 141 | |||
| 142 | /** | ||
| 143 | * Checks if a parameter was set for the filter. | ||
| 144 | */ | ||
| 145 | final public function hasParameter(string $name): bool | ||
| 146 | { | ||
| 147 | return isset($this->parameters[$name]); | ||
| 148 | } | ||
| 149 | |||
| 150 | /** | ||
| 151 | * Returns as string representation of the SQLFilter parameters (the state). | ||
| 152 | */ | ||
| 153 | final public function __toString(): string | ||
| 154 | { | ||
| 155 | return serialize($this->parameters); | ||
| 156 | } | ||
| 157 | |||
| 158 | /** | ||
| 159 | * Returns the database connection used by the entity manager | ||
| 160 | */ | ||
| 161 | final protected function getConnection(): Connection | ||
| 162 | { | ||
| 163 | return $this->em->getConnection(); | ||
| 164 | } | ||
| 165 | |||
| 166 | /** | ||
| 167 | * Gets the SQL query part to add to a query. | ||
| 168 | * | ||
| 169 | * @psalm-param ClassMetadata<object> $targetEntity | ||
| 170 | * | ||
| 171 | * @return string The constraint SQL if there is available, empty string otherwise. | ||
| 172 | */ | ||
| 173 | abstract public function addFilterConstraint(ClassMetadata $targetEntity, string $targetTableAlias): string; | ||
| 174 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/FilterCollection.php b/vendor/doctrine/orm/src/Query/FilterCollection.php new file mode 100644 index 0000000..3d3c576 --- /dev/null +++ b/vendor/doctrine/orm/src/Query/FilterCollection.php | |||
| @@ -0,0 +1,260 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Query; | ||
| 6 | |||
| 7 | use Doctrine\ORM\Configuration; | ||
| 8 | use Doctrine\ORM\EntityManagerInterface; | ||
| 9 | use Doctrine\ORM\Query\Filter\SQLFilter; | ||
| 10 | use InvalidArgumentException; | ||
| 11 | |||
| 12 | use function assert; | ||
| 13 | use function ksort; | ||
| 14 | |||
| 15 | /** | ||
| 16 | * Collection class for all the query filters. | ||
| 17 | */ | ||
| 18 | class FilterCollection | ||
| 19 | { | ||
| 20 | /* Filter STATES */ | ||
| 21 | |||
| 22 | /** | ||
| 23 | * A filter object is in CLEAN state when it has no changed parameters. | ||
| 24 | */ | ||
| 25 | public const FILTERS_STATE_CLEAN = 1; | ||
| 26 | |||
| 27 | /** | ||
| 28 | * A filter object is in DIRTY state when it has changed parameters. | ||
| 29 | */ | ||
| 30 | public const FILTERS_STATE_DIRTY = 2; | ||
| 31 | |||
| 32 | private readonly Configuration $config; | ||
| 33 | |||
| 34 | /** | ||
| 35 | * Instances of enabled filters. | ||
| 36 | * | ||
| 37 | * @var array<string, SQLFilter> | ||
| 38 | */ | ||
| 39 | private array $enabledFilters = []; | ||
| 40 | |||
| 41 | /** The filter hash from the last time the query was parsed. */ | ||
| 42 | private string $filterHash = ''; | ||
| 43 | |||
| 44 | /** | ||
| 45 | * Instances of suspended filters. | ||
| 46 | * | ||
| 47 | * @var SQLFilter[] | ||
| 48 | * @psalm-var array<string, SQLFilter> | ||
| 49 | */ | ||
| 50 | private array $suspendedFilters = []; | ||
| 51 | |||
| 52 | /** | ||
| 53 | * The current state of this filter. | ||
| 54 | * | ||
| 55 | * @psalm-var self::FILTERS_STATE_* | ||
| 56 | */ | ||
| 57 | private int $filtersState = self::FILTERS_STATE_CLEAN; | ||
| 58 | |||
| 59 | public function __construct( | ||
| 60 | private readonly EntityManagerInterface $em, | ||
| 61 | ) { | ||
| 62 | $this->config = $em->getConfiguration(); | ||
| 63 | } | ||
| 64 | |||
| 65 | /** | ||
| 66 | * Gets all the enabled filters. | ||
| 67 | * | ||
| 68 | * @return array<string, SQLFilter> The enabled filters. | ||
| 69 | */ | ||
| 70 | public function getEnabledFilters(): array | ||
| 71 | { | ||
| 72 | return $this->enabledFilters; | ||
| 73 | } | ||
| 74 | |||
| 75 | /** | ||
| 76 | * Gets all the suspended filters. | ||
| 77 | * | ||
| 78 | * @return SQLFilter[] The suspended filters. | ||
| 79 | * @psalm-return array<string, SQLFilter> | ||
| 80 | */ | ||
| 81 | public function getSuspendedFilters(): array | ||
| 82 | { | ||
| 83 | return $this->suspendedFilters; | ||
| 84 | } | ||
| 85 | |||
| 86 | /** | ||
| 87 | * Enables a filter from the collection. | ||
| 88 | * | ||
| 89 | * @throws InvalidArgumentException If the filter does not exist. | ||
| 90 | */ | ||
| 91 | public function enable(string $name): SQLFilter | ||
| 92 | { | ||
| 93 | if (! $this->has($name)) { | ||
| 94 | throw new InvalidArgumentException("Filter '" . $name . "' does not exist."); | ||
| 95 | } | ||
| 96 | |||
| 97 | if (! $this->isEnabled($name)) { | ||
| 98 | $filterClass = $this->config->getFilterClassName($name); | ||
| 99 | |||
| 100 | assert($filterClass !== null); | ||
| 101 | |||
| 102 | $this->enabledFilters[$name] = new $filterClass($this->em); | ||
| 103 | |||
| 104 | // In case a suspended filter with the same name was forgotten | ||
| 105 | unset($this->suspendedFilters[$name]); | ||
| 106 | |||
| 107 | // Keep the enabled filters sorted for the hash | ||
| 108 | ksort($this->enabledFilters); | ||
| 109 | |||
| 110 | $this->setFiltersStateDirty(); | ||
| 111 | } | ||
| 112 | |||
| 113 | return $this->enabledFilters[$name]; | ||
| 114 | } | ||
| 115 | |||
| 116 | /** | ||
| 117 | * Disables a filter. | ||
| 118 | * | ||
| 119 | * @throws InvalidArgumentException If the filter does not exist. | ||
| 120 | */ | ||
| 121 | public function disable(string $name): SQLFilter | ||
| 122 | { | ||
| 123 | // Get the filter to return it | ||
| 124 | $filter = $this->getFilter($name); | ||
| 125 | |||
| 126 | unset($this->enabledFilters[$name]); | ||
| 127 | |||
| 128 | $this->setFiltersStateDirty(); | ||
| 129 | |||
| 130 | return $filter; | ||
| 131 | } | ||
| 132 | |||
| 133 | /** | ||
| 134 | * Suspend a filter. | ||
| 135 | * | ||
| 136 | * @param string $name Name of the filter. | ||
| 137 | * | ||
| 138 | * @return SQLFilter The suspended filter. | ||
| 139 | * | ||
| 140 | * @throws InvalidArgumentException If the filter does not exist. | ||
| 141 | */ | ||
| 142 | public function suspend(string $name): SQLFilter | ||
| 143 | { | ||
| 144 | // Get the filter to return it | ||
| 145 | $filter = $this->getFilter($name); | ||
| 146 | |||
| 147 | $this->suspendedFilters[$name] = $filter; | ||
| 148 | unset($this->enabledFilters[$name]); | ||
| 149 | |||
| 150 | $this->setFiltersStateDirty(); | ||
| 151 | |||
| 152 | return $filter; | ||
| 153 | } | ||
| 154 | |||
| 155 | /** | ||
| 156 | * Restore a disabled filter from the collection. | ||
| 157 | * | ||
| 158 | * @param string $name Name of the filter. | ||
| 159 | * | ||
| 160 | * @return SQLFilter The restored filter. | ||
| 161 | * | ||
| 162 | * @throws InvalidArgumentException If the filter does not exist. | ||
| 163 | */ | ||
| 164 | public function restore(string $name): SQLFilter | ||
| 165 | { | ||
| 166 | if (! $this->isSuspended($name)) { | ||
| 167 | throw new InvalidArgumentException("Filter '" . $name . "' is not suspended."); | ||
| 168 | } | ||
| 169 | |||
| 170 | $this->enabledFilters[$name] = $this->suspendedFilters[$name]; | ||
| 171 | unset($this->suspendedFilters[$name]); | ||
| 172 | |||
| 173 | // Keep the enabled filters sorted for the hash | ||
| 174 | ksort($this->enabledFilters); | ||
| 175 | |||
| 176 | $this->setFiltersStateDirty(); | ||
| 177 | |||
| 178 | return $this->enabledFilters[$name]; | ||
| 179 | } | ||
| 180 | |||
| 181 | /** | ||
| 182 | * Gets an enabled filter from the collection. | ||
| 183 | * | ||
| 184 | * @throws InvalidArgumentException If the filter is not enabled. | ||
| 185 | */ | ||
| 186 | public function getFilter(string $name): SQLFilter | ||
| 187 | { | ||
| 188 | if (! $this->isEnabled($name)) { | ||
| 189 | throw new InvalidArgumentException("Filter '" . $name . "' is not enabled."); | ||
| 190 | } | ||
| 191 | |||
| 192 | return $this->enabledFilters[$name]; | ||
| 193 | } | ||
| 194 | |||
| 195 | /** | ||
| 196 | * Checks whether filter with given name is defined. | ||
| 197 | */ | ||
| 198 | public function has(string $name): bool | ||
| 199 | { | ||
| 200 | return $this->config->getFilterClassName($name) !== null; | ||
| 201 | } | ||
| 202 | |||
| 203 | /** | ||
| 204 | * Checks if a filter is enabled. | ||
| 205 | */ | ||
| 206 | public function isEnabled(string $name): bool | ||
| 207 | { | ||
| 208 | return isset($this->enabledFilters[$name]); | ||
| 209 | } | ||
| 210 | |||
| 211 | /** | ||
| 212 | * Checks if a filter is suspended. | ||
| 213 | * | ||
| 214 | * @param string $name Name of the filter. | ||
| 215 | * | ||
| 216 | * @return bool True if the filter is suspended, false otherwise. | ||
| 217 | */ | ||
| 218 | public function isSuspended(string $name): bool | ||
| 219 | { | ||
| 220 | return isset($this->suspendedFilters[$name]); | ||
| 221 | } | ||
| 222 | |||
| 223 | /** | ||
| 224 | * Checks if the filter collection is clean. | ||
| 225 | */ | ||
| 226 | public function isClean(): bool | ||
| 227 | { | ||
| 228 | return $this->filtersState === self::FILTERS_STATE_CLEAN; | ||
| 229 | } | ||
| 230 | |||
| 231 | /** | ||
| 232 | * Generates a string of currently enabled filters to use for the cache id. | ||
| 233 | */ | ||
| 234 | public function getHash(): string | ||
| 235 | { | ||
| 236 | // If there are only clean filters, the previous hash can be returned | ||
| 237 | if ($this->filtersState === self::FILTERS_STATE_CLEAN) { | ||
| 238 | return $this->filterHash; | ||
| 239 | } | ||
| 240 | |||
| 241 | $filterHash = ''; | ||
| 242 | |||
| 243 | foreach ($this->enabledFilters as $name => $filter) { | ||
| 244 | $filterHash .= $name . $filter; | ||
| 245 | } | ||
| 246 | |||
| 247 | $this->filterHash = $filterHash; | ||
| 248 | $this->filtersState = self::FILTERS_STATE_CLEAN; | ||
| 249 | |||
| 250 | return $filterHash; | ||
| 251 | } | ||
| 252 | |||
| 253 | /** | ||
| 254 | * Sets the filter state to dirty. | ||
| 255 | */ | ||
| 256 | public function setFiltersStateDirty(): void | ||
| 257 | { | ||
| 258 | $this->filtersState = self::FILTERS_STATE_DIRTY; | ||
| 259 | } | ||
| 260 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/Lexer.php b/vendor/doctrine/orm/src/Query/Lexer.php new file mode 100644 index 0000000..c446675 --- /dev/null +++ b/vendor/doctrine/orm/src/Query/Lexer.php | |||
| @@ -0,0 +1,150 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Query; | ||
| 6 | |||
| 7 | use Doctrine\Common\Lexer\AbstractLexer; | ||
| 8 | |||
| 9 | use function constant; | ||
| 10 | use function ctype_alpha; | ||
| 11 | use function defined; | ||
| 12 | use function is_numeric; | ||
| 13 | use function str_contains; | ||
| 14 | use function str_replace; | ||
| 15 | use function stripos; | ||
| 16 | use function strlen; | ||
| 17 | use function strtoupper; | ||
| 18 | use function substr; | ||
| 19 | |||
| 20 | /** | ||
| 21 | * Scans a DQL query for tokens. | ||
| 22 | * | ||
| 23 | * @extends AbstractLexer<TokenType, string> | ||
| 24 | */ | ||
| 25 | class Lexer extends AbstractLexer | ||
| 26 | { | ||
| 27 | /** | ||
| 28 | * Creates a new query scanner object. | ||
| 29 | * | ||
| 30 | * @param string $input A query string. | ||
| 31 | */ | ||
| 32 | public function __construct(string $input) | ||
| 33 | { | ||
| 34 | $this->setInput($input); | ||
| 35 | } | ||
| 36 | |||
| 37 | /** | ||
| 38 | * {@inheritDoc} | ||
| 39 | */ | ||
| 40 | protected function getCatchablePatterns(): array | ||
| 41 | { | ||
| 42 | return [ | ||
| 43 | '[a-z_][a-z0-9_]*\:[a-z_][a-z0-9_]*(?:\\\[a-z_][a-z0-9_]*)*', // aliased name | ||
| 44 | '[a-z_\\\][a-z0-9_]*(?:\\\[a-z_][a-z0-9_]*)*', // identifier or qualified name | ||
| 45 | '(?:[0-9]+(?:[\.][0-9]+)*)(?:e[+-]?[0-9]+)?', // numbers | ||
| 46 | "'(?:[^']|'')*'", // quoted strings | ||
| 47 | '\?[0-9]*|:[a-z_][a-z0-9_]*', // parameters | ||
| 48 | ]; | ||
| 49 | } | ||
| 50 | |||
| 51 | /** | ||
| 52 | * {@inheritDoc} | ||
| 53 | */ | ||
| 54 | protected function getNonCatchablePatterns(): array | ||
| 55 | { | ||
| 56 | return ['\s+', '--.*', '(.)']; | ||
| 57 | } | ||
| 58 | |||
| 59 | protected function getType(string &$value): TokenType | ||
| 60 | { | ||
| 61 | $type = TokenType::T_NONE; | ||
| 62 | |||
| 63 | switch (true) { | ||
| 64 | // Recognize numeric values | ||
| 65 | case is_numeric($value): | ||
| 66 | if (str_contains($value, '.') || stripos($value, 'e') !== false) { | ||
| 67 | return TokenType::T_FLOAT; | ||
| 68 | } | ||
| 69 | |||
| 70 | return TokenType::T_INTEGER; | ||
| 71 | |||
| 72 | // Recognize quoted strings | ||
| 73 | case $value[0] === "'": | ||
| 74 | $value = str_replace("''", "'", substr($value, 1, strlen($value) - 2)); | ||
| 75 | |||
| 76 | return TokenType::T_STRING; | ||
| 77 | |||
| 78 | // Recognize identifiers, aliased or qualified names | ||
| 79 | case ctype_alpha($value[0]) || $value[0] === '_' || $value[0] === '\\': | ||
| 80 | $name = 'Doctrine\ORM\Query\TokenType::T_' . strtoupper($value); | ||
| 81 | |||
| 82 | if (defined($name)) { | ||
| 83 | $type = constant($name); | ||
| 84 | |||
| 85 | if ($type->value > 100) { | ||
| 86 | return $type; | ||
| 87 | } | ||
| 88 | } | ||
| 89 | |||
| 90 | if (str_contains($value, '\\')) { | ||
| 91 | return TokenType::T_FULLY_QUALIFIED_NAME; | ||
| 92 | } | ||
| 93 | |||
| 94 | return TokenType::T_IDENTIFIER; | ||
| 95 | |||
| 96 | // Recognize input parameters | ||
| 97 | case $value[0] === '?' || $value[0] === ':': | ||
| 98 | return TokenType::T_INPUT_PARAMETER; | ||
| 99 | |||
| 100 | // Recognize symbols | ||
| 101 | case $value === '.': | ||
| 102 | return TokenType::T_DOT; | ||
| 103 | |||
| 104 | case $value === ',': | ||
| 105 | return TokenType::T_COMMA; | ||
| 106 | |||
| 107 | case $value === '(': | ||
| 108 | return TokenType::T_OPEN_PARENTHESIS; | ||
| 109 | |||
| 110 | case $value === ')': | ||
| 111 | return TokenType::T_CLOSE_PARENTHESIS; | ||
| 112 | |||
| 113 | case $value === '=': | ||
| 114 | return TokenType::T_EQUALS; | ||
| 115 | |||
| 116 | case $value === '>': | ||
| 117 | return TokenType::T_GREATER_THAN; | ||
| 118 | |||
| 119 | case $value === '<': | ||
| 120 | return TokenType::T_LOWER_THAN; | ||
| 121 | |||
| 122 | case $value === '+': | ||
| 123 | return TokenType::T_PLUS; | ||
| 124 | |||
| 125 | case $value === '-': | ||
| 126 | return TokenType::T_MINUS; | ||
| 127 | |||
| 128 | case $value === '*': | ||
| 129 | return TokenType::T_MULTIPLY; | ||
| 130 | |||
| 131 | case $value === '/': | ||
| 132 | return TokenType::T_DIVIDE; | ||
| 133 | |||
| 134 | case $value === '!': | ||
| 135 | return TokenType::T_NEGATE; | ||
| 136 | |||
| 137 | case $value === '{': | ||
| 138 | return TokenType::T_OPEN_CURLY_BRACE; | ||
| 139 | |||
| 140 | case $value === '}': | ||
| 141 | return TokenType::T_CLOSE_CURLY_BRACE; | ||
| 142 | |||
| 143 | // Default | ||
| 144 | default: | ||
| 145 | // Do nothing | ||
| 146 | } | ||
| 147 | |||
| 148 | return $type; | ||
| 149 | } | ||
| 150 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/Parameter.php b/vendor/doctrine/orm/src/Query/Parameter.php new file mode 100644 index 0000000..43eb7a4 --- /dev/null +++ b/vendor/doctrine/orm/src/Query/Parameter.php | |||
| @@ -0,0 +1,89 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Query; | ||
| 6 | |||
| 7 | use function trim; | ||
| 8 | |||
| 9 | /** | ||
| 10 | * Defines a Query Parameter. | ||
| 11 | * | ||
| 12 | * @link www.doctrine-project.org | ||
| 13 | */ | ||
| 14 | class Parameter | ||
| 15 | { | ||
| 16 | /** | ||
| 17 | * Returns the internal representation of a parameter name. | ||
| 18 | */ | ||
| 19 | public static function normalizeName(int|string $name): string | ||
| 20 | { | ||
| 21 | return trim((string) $name, ':'); | ||
| 22 | } | ||
| 23 | |||
| 24 | /** | ||
| 25 | * The parameter name. | ||
| 26 | */ | ||
| 27 | private readonly string $name; | ||
| 28 | |||
| 29 | /** | ||
| 30 | * The parameter value. | ||
| 31 | */ | ||
| 32 | private mixed $value; | ||
| 33 | |||
| 34 | /** | ||
| 35 | * The parameter type. | ||
| 36 | */ | ||
| 37 | private mixed $type; | ||
| 38 | |||
| 39 | /** | ||
| 40 | * Whether the parameter type was explicitly specified or not | ||
| 41 | */ | ||
| 42 | private readonly bool $typeSpecified; | ||
| 43 | |||
| 44 | public function __construct(int|string $name, mixed $value, mixed $type = null) | ||
| 45 | { | ||
| 46 | $this->name = self::normalizeName($name); | ||
| 47 | $this->typeSpecified = $type !== null; | ||
| 48 | |||
| 49 | $this->setValue($value, $type); | ||
| 50 | } | ||
| 51 | |||
| 52 | /** | ||
| 53 | * Retrieves the Parameter name. | ||
| 54 | */ | ||
| 55 | public function getName(): string | ||
| 56 | { | ||
| 57 | return $this->name; | ||
| 58 | } | ||
| 59 | |||
| 60 | /** | ||
| 61 | * Retrieves the Parameter value. | ||
| 62 | */ | ||
| 63 | public function getValue(): mixed | ||
| 64 | { | ||
| 65 | return $this->value; | ||
| 66 | } | ||
| 67 | |||
| 68 | /** | ||
| 69 | * Retrieves the Parameter type. | ||
| 70 | */ | ||
| 71 | public function getType(): mixed | ||
| 72 | { | ||
| 73 | return $this->type; | ||
| 74 | } | ||
| 75 | |||
| 76 | /** | ||
| 77 | * Defines the Parameter value. | ||
| 78 | */ | ||
| 79 | public function setValue(mixed $value, mixed $type = null): void | ||
| 80 | { | ||
| 81 | $this->value = $value; | ||
| 82 | $this->type = $type ?: ParameterTypeInferer::inferType($value); | ||
| 83 | } | ||
| 84 | |||
| 85 | public function typeWasSpecified(): bool | ||
| 86 | { | ||
| 87 | return $this->typeSpecified; | ||
| 88 | } | ||
| 89 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/ParameterTypeInferer.php b/vendor/doctrine/orm/src/Query/ParameterTypeInferer.php new file mode 100644 index 0000000..dae28fa --- /dev/null +++ b/vendor/doctrine/orm/src/Query/ParameterTypeInferer.php | |||
| @@ -0,0 +1,77 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Query; | ||
| 6 | |||
| 7 | use BackedEnum; | ||
| 8 | use DateInterval; | ||
| 9 | use DateTimeImmutable; | ||
| 10 | use DateTimeInterface; | ||
| 11 | use Doctrine\DBAL\ArrayParameterType; | ||
| 12 | use Doctrine\DBAL\ParameterType; | ||
| 13 | use Doctrine\DBAL\Types\Types; | ||
| 14 | |||
| 15 | use function current; | ||
| 16 | use function is_array; | ||
| 17 | use function is_bool; | ||
| 18 | use function is_int; | ||
| 19 | |||
| 20 | /** | ||
| 21 | * Provides an enclosed support for parameter inferring. | ||
| 22 | * | ||
| 23 | * @link www.doctrine-project.org | ||
| 24 | */ | ||
| 25 | final class ParameterTypeInferer | ||
| 26 | { | ||
| 27 | /** | ||
| 28 | * Infers type of a given value, returning a compatible constant: | ||
| 29 | * - Type (\Doctrine\DBAL\Types\Type::*) | ||
| 30 | * - Connection (\Doctrine\DBAL\Connection::PARAM_*) | ||
| 31 | */ | ||
| 32 | public static function inferType(mixed $value): ParameterType|ArrayParameterType|int|string | ||
| 33 | { | ||
| 34 | if (is_int($value)) { | ||
| 35 | return Types::INTEGER; | ||
| 36 | } | ||
| 37 | |||
| 38 | if (is_bool($value)) { | ||
| 39 | return Types::BOOLEAN; | ||
| 40 | } | ||
| 41 | |||
| 42 | if ($value instanceof DateTimeImmutable) { | ||
| 43 | return Types::DATETIME_IMMUTABLE; | ||
| 44 | } | ||
| 45 | |||
| 46 | if ($value instanceof DateTimeInterface) { | ||
| 47 | return Types::DATETIME_MUTABLE; | ||
| 48 | } | ||
| 49 | |||
| 50 | if ($value instanceof DateInterval) { | ||
| 51 | return Types::DATEINTERVAL; | ||
| 52 | } | ||
| 53 | |||
| 54 | if ($value instanceof BackedEnum) { | ||
| 55 | return is_int($value->value) | ||
| 56 | ? Types::INTEGER | ||
| 57 | : Types::STRING; | ||
| 58 | } | ||
| 59 | |||
| 60 | if (is_array($value)) { | ||
| 61 | $firstValue = current($value); | ||
| 62 | if ($firstValue instanceof BackedEnum) { | ||
| 63 | $firstValue = $firstValue->value; | ||
| 64 | } | ||
| 65 | |||
| 66 | return is_int($firstValue) | ||
| 67 | ? ArrayParameterType::INTEGER | ||
| 68 | : ArrayParameterType::STRING; | ||
| 69 | } | ||
| 70 | |||
| 71 | return ParameterType::STRING; | ||
| 72 | } | ||
| 73 | |||
| 74 | private function __construct() | ||
| 75 | { | ||
| 76 | } | ||
| 77 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/Parser.php b/vendor/doctrine/orm/src/Query/Parser.php new file mode 100644 index 0000000..e948f2c --- /dev/null +++ b/vendor/doctrine/orm/src/Query/Parser.php | |||
| @@ -0,0 +1,3269 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Query; | ||
| 6 | |||
| 7 | use Doctrine\Common\Lexer\Token; | ||
| 8 | use Doctrine\ORM\EntityManagerInterface; | ||
| 9 | use Doctrine\ORM\Mapping\AssociationMapping; | ||
| 10 | use Doctrine\ORM\Mapping\ClassMetadata; | ||
| 11 | use Doctrine\ORM\Query; | ||
| 12 | use Doctrine\ORM\Query\AST\Functions; | ||
| 13 | use LogicException; | ||
| 14 | use ReflectionClass; | ||
| 15 | |||
| 16 | use function array_search; | ||
| 17 | use function assert; | ||
| 18 | use function class_exists; | ||
| 19 | use function count; | ||
| 20 | use function implode; | ||
| 21 | use function in_array; | ||
| 22 | use function interface_exists; | ||
| 23 | use function is_string; | ||
| 24 | use function sprintf; | ||
| 25 | use function str_contains; | ||
| 26 | use function strlen; | ||
| 27 | use function strpos; | ||
| 28 | use function strrpos; | ||
| 29 | use function strtolower; | ||
| 30 | use function substr; | ||
| 31 | |||
| 32 | /** | ||
| 33 | * An LL(*) recursive-descent parser for the context-free grammar of the Doctrine Query Language. | ||
| 34 | * Parses a DQL query, reports any errors in it, and generates an AST. | ||
| 35 | * | ||
| 36 | * @psalm-type DqlToken = Token<TokenType, string> | ||
| 37 | * @psalm-type QueryComponent = array{ | ||
| 38 | * metadata?: ClassMetadata<object>, | ||
| 39 | * parent?: string|null, | ||
| 40 | * relation?: AssociationMapping|null, | ||
| 41 | * map?: string|null, | ||
| 42 | * resultVariable?: AST\Node|string, | ||
| 43 | * nestingLevel: int, | ||
| 44 | * token: DqlToken, | ||
| 45 | * } | ||
| 46 | */ | ||
| 47 | final class Parser | ||
| 48 | { | ||
| 49 | /** | ||
| 50 | * @readonly Maps BUILT-IN string function names to AST class names. | ||
| 51 | * @psalm-var array<string, class-string<Functions\FunctionNode>> | ||
| 52 | */ | ||
| 53 | private static array $stringFunctions = [ | ||
| 54 | 'concat' => Functions\ConcatFunction::class, | ||
| 55 | 'substring' => Functions\SubstringFunction::class, | ||
| 56 | 'trim' => Functions\TrimFunction::class, | ||
| 57 | 'lower' => Functions\LowerFunction::class, | ||
| 58 | 'upper' => Functions\UpperFunction::class, | ||
| 59 | 'identity' => Functions\IdentityFunction::class, | ||
| 60 | ]; | ||
| 61 | |||
| 62 | /** | ||
| 63 | * @readonly Maps BUILT-IN numeric function names to AST class names. | ||
| 64 | * @psalm-var array<string, class-string<Functions\FunctionNode>> | ||
| 65 | */ | ||
| 66 | private static array $numericFunctions = [ | ||
| 67 | 'length' => Functions\LengthFunction::class, | ||
| 68 | 'locate' => Functions\LocateFunction::class, | ||
| 69 | 'abs' => Functions\AbsFunction::class, | ||
| 70 | 'sqrt' => Functions\SqrtFunction::class, | ||
| 71 | 'mod' => Functions\ModFunction::class, | ||
| 72 | 'size' => Functions\SizeFunction::class, | ||
| 73 | 'date_diff' => Functions\DateDiffFunction::class, | ||
| 74 | 'bit_and' => Functions\BitAndFunction::class, | ||
| 75 | 'bit_or' => Functions\BitOrFunction::class, | ||
| 76 | |||
| 77 | // Aggregate functions | ||
| 78 | 'min' => Functions\MinFunction::class, | ||
| 79 | 'max' => Functions\MaxFunction::class, | ||
| 80 | 'avg' => Functions\AvgFunction::class, | ||
| 81 | 'sum' => Functions\SumFunction::class, | ||
| 82 | 'count' => Functions\CountFunction::class, | ||
| 83 | ]; | ||
| 84 | |||
| 85 | /** | ||
| 86 | * @readonly Maps BUILT-IN datetime function names to AST class names. | ||
| 87 | * @psalm-var array<string, class-string<Functions\FunctionNode>> | ||
| 88 | */ | ||
| 89 | private static array $datetimeFunctions = [ | ||
| 90 | 'current_date' => Functions\CurrentDateFunction::class, | ||
| 91 | 'current_time' => Functions\CurrentTimeFunction::class, | ||
| 92 | 'current_timestamp' => Functions\CurrentTimestampFunction::class, | ||
| 93 | 'date_add' => Functions\DateAddFunction::class, | ||
| 94 | 'date_sub' => Functions\DateSubFunction::class, | ||
| 95 | ]; | ||
| 96 | |||
| 97 | /* | ||
| 98 | * Expressions that were encountered during parsing of identifiers and expressions | ||
| 99 | * and still need to be validated. | ||
| 100 | */ | ||
| 101 | |||
| 102 | /** @psalm-var list<array{token: DqlToken|null, expression: mixed, nestingLevel: int}> */ | ||
| 103 | private array $deferredIdentificationVariables = []; | ||
| 104 | |||
| 105 | /** @psalm-var list<array{token: DqlToken|null, expression: AST\PathExpression, nestingLevel: int}> */ | ||
| 106 | private array $deferredPathExpressions = []; | ||
| 107 | |||
| 108 | /** @psalm-var list<array{token: DqlToken|null, expression: mixed, nestingLevel: int}> */ | ||
| 109 | private array $deferredResultVariables = []; | ||
| 110 | |||
| 111 | /** @psalm-var list<array{token: DqlToken|null, expression: AST\NewObjectExpression, nestingLevel: int}> */ | ||
| 112 | private array $deferredNewObjectExpressions = []; | ||
| 113 | |||
| 114 | /** | ||
| 115 | * The lexer. | ||
| 116 | */ | ||
| 117 | private readonly Lexer $lexer; | ||
| 118 | |||
| 119 | /** | ||
| 120 | * The parser result. | ||
| 121 | */ | ||
| 122 | private readonly ParserResult $parserResult; | ||
| 123 | |||
| 124 | /** | ||
| 125 | * The EntityManager. | ||
| 126 | */ | ||
| 127 | private readonly EntityManagerInterface $em; | ||
| 128 | |||
| 129 | /** | ||
| 130 | * Map of declared query components in the parsed query. | ||
| 131 | * | ||
| 132 | * @psalm-var array<string, QueryComponent> | ||
| 133 | */ | ||
| 134 | private array $queryComponents = []; | ||
| 135 | |||
| 136 | /** | ||
| 137 | * Keeps the nesting level of defined ResultVariables. | ||
| 138 | */ | ||
| 139 | private int $nestingLevel = 0; | ||
| 140 | |||
| 141 | /** | ||
| 142 | * Any additional custom tree walkers that modify the AST. | ||
| 143 | * | ||
| 144 | * @psalm-var list<class-string<TreeWalker>> | ||
| 145 | */ | ||
| 146 | private array $customTreeWalkers = []; | ||
| 147 | |||
| 148 | /** | ||
| 149 | * The custom last tree walker, if any, that is responsible for producing the output. | ||
| 150 | * | ||
| 151 | * @var class-string<SqlWalker>|null | ||
| 152 | */ | ||
| 153 | private $customOutputWalker; | ||
| 154 | |||
| 155 | /** @psalm-var array<string, AST\SelectExpression> */ | ||
| 156 | private array $identVariableExpressions = []; | ||
| 157 | |||
| 158 | /** | ||
| 159 | * Creates a new query parser object. | ||
| 160 | * | ||
| 161 | * @param Query $query The Query to parse. | ||
| 162 | */ | ||
| 163 | public function __construct(private readonly Query $query) | ||
| 164 | { | ||
| 165 | $this->em = $query->getEntityManager(); | ||
| 166 | $this->lexer = new Lexer((string) $query->getDQL()); | ||
| 167 | $this->parserResult = new ParserResult(); | ||
| 168 | } | ||
| 169 | |||
| 170 | /** | ||
| 171 | * Sets a custom tree walker that produces output. | ||
| 172 | * This tree walker will be run last over the AST, after any other walkers. | ||
| 173 | * | ||
| 174 | * @psalm-param class-string<SqlWalker> $className | ||
| 175 | */ | ||
| 176 | public function setCustomOutputTreeWalker(string $className): void | ||
| 177 | { | ||
| 178 | $this->customOutputWalker = $className; | ||
| 179 | } | ||
| 180 | |||
| 181 | /** | ||
| 182 | * Adds a custom tree walker for modifying the AST. | ||
| 183 | * | ||
| 184 | * @psalm-param class-string<TreeWalker> $className | ||
| 185 | */ | ||
| 186 | public function addCustomTreeWalker(string $className): void | ||
| 187 | { | ||
| 188 | $this->customTreeWalkers[] = $className; | ||
| 189 | } | ||
| 190 | |||
| 191 | /** | ||
| 192 | * Gets the lexer used by the parser. | ||
| 193 | */ | ||
| 194 | public function getLexer(): Lexer | ||
| 195 | { | ||
| 196 | return $this->lexer; | ||
| 197 | } | ||
| 198 | |||
| 199 | /** | ||
| 200 | * Gets the ParserResult that is being filled with information during parsing. | ||
| 201 | */ | ||
| 202 | public function getParserResult(): ParserResult | ||
| 203 | { | ||
| 204 | return $this->parserResult; | ||
| 205 | } | ||
| 206 | |||
| 207 | /** | ||
| 208 | * Gets the EntityManager used by the parser. | ||
| 209 | */ | ||
| 210 | public function getEntityManager(): EntityManagerInterface | ||
| 211 | { | ||
| 212 | return $this->em; | ||
| 213 | } | ||
| 214 | |||
| 215 | /** | ||
| 216 | * Parses and builds AST for the given Query. | ||
| 217 | */ | ||
| 218 | public function getAST(): AST\SelectStatement|AST\UpdateStatement|AST\DeleteStatement | ||
| 219 | { | ||
| 220 | // Parse & build AST | ||
| 221 | $AST = $this->QueryLanguage(); | ||
| 222 | |||
| 223 | // Process any deferred validations of some nodes in the AST. | ||
| 224 | // This also allows post-processing of the AST for modification purposes. | ||
| 225 | $this->processDeferredIdentificationVariables(); | ||
| 226 | |||
| 227 | if ($this->deferredPathExpressions) { | ||
| 228 | $this->processDeferredPathExpressions(); | ||
| 229 | } | ||
| 230 | |||
| 231 | if ($this->deferredResultVariables) { | ||
| 232 | $this->processDeferredResultVariables(); | ||
| 233 | } | ||
| 234 | |||
| 235 | if ($this->deferredNewObjectExpressions) { | ||
| 236 | $this->processDeferredNewObjectExpressions($AST); | ||
| 237 | } | ||
| 238 | |||
| 239 | $this->processRootEntityAliasSelected(); | ||
| 240 | |||
| 241 | // TODO: Is there a way to remove this? It may impact the mixed hydration resultset a lot! | ||
| 242 | $this->fixIdentificationVariableOrder($AST); | ||
| 243 | |||
| 244 | return $AST; | ||
| 245 | } | ||
| 246 | |||
| 247 | /** | ||
| 248 | * Attempts to match the given token with the current lookahead token. | ||
| 249 | * | ||
| 250 | * If they match, updates the lookahead token; otherwise raises a syntax | ||
| 251 | * error. | ||
| 252 | * | ||
| 253 | * @throws QueryException If the tokens don't match. | ||
| 254 | */ | ||
| 255 | public function match(TokenType $token): void | ||
| 256 | { | ||
| 257 | $lookaheadType = $this->lexer->lookahead->type ?? null; | ||
| 258 | |||
| 259 | // Short-circuit on first condition, usually types match | ||
| 260 | if ($lookaheadType === $token) { | ||
| 261 | $this->lexer->moveNext(); | ||
| 262 | |||
| 263 | return; | ||
| 264 | } | ||
| 265 | |||
| 266 | // If parameter is not identifier (1-99) must be exact match | ||
| 267 | if ($token->value < TokenType::T_IDENTIFIER->value) { | ||
| 268 | $this->syntaxError($this->lexer->getLiteral($token)); | ||
| 269 | } | ||
| 270 | |||
| 271 | // If parameter is keyword (200+) must be exact match | ||
| 272 | if ($token->value > TokenType::T_IDENTIFIER->value) { | ||
| 273 | $this->syntaxError($this->lexer->getLiteral($token)); | ||
| 274 | } | ||
| 275 | |||
| 276 | // If parameter is T_IDENTIFIER, then matches T_IDENTIFIER (100) and keywords (200+) | ||
| 277 | if ($token->value === TokenType::T_IDENTIFIER->value && $lookaheadType->value < TokenType::T_IDENTIFIER->value) { | ||
| 278 | $this->syntaxError($this->lexer->getLiteral($token)); | ||
| 279 | } | ||
| 280 | |||
| 281 | $this->lexer->moveNext(); | ||
| 282 | } | ||
| 283 | |||
| 284 | /** | ||
| 285 | * Frees this parser, enabling it to be reused. | ||
| 286 | * | ||
| 287 | * @param bool $deep Whether to clean peek and reset errors. | ||
| 288 | * @param int $position Position to reset. | ||
| 289 | */ | ||
| 290 | public function free(bool $deep = false, int $position = 0): void | ||
| 291 | { | ||
| 292 | // WARNING! Use this method with care. It resets the scanner! | ||
| 293 | $this->lexer->resetPosition($position); | ||
| 294 | |||
| 295 | // Deep = true cleans peek and also any previously defined errors | ||
| 296 | if ($deep) { | ||
| 297 | $this->lexer->resetPeek(); | ||
| 298 | } | ||
| 299 | |||
| 300 | $this->lexer->token = null; | ||
| 301 | $this->lexer->lookahead = null; | ||
| 302 | } | ||
| 303 | |||
| 304 | /** | ||
| 305 | * Parses a query string. | ||
| 306 | */ | ||
| 307 | public function parse(): ParserResult | ||
| 308 | { | ||
| 309 | $AST = $this->getAST(); | ||
| 310 | |||
| 311 | $customWalkers = $this->query->getHint(Query::HINT_CUSTOM_TREE_WALKERS); | ||
| 312 | if ($customWalkers !== false) { | ||
| 313 | $this->customTreeWalkers = $customWalkers; | ||
| 314 | } | ||
| 315 | |||
| 316 | $customOutputWalker = $this->query->getHint(Query::HINT_CUSTOM_OUTPUT_WALKER); | ||
| 317 | if ($customOutputWalker !== false) { | ||
| 318 | $this->customOutputWalker = $customOutputWalker; | ||
| 319 | } | ||
| 320 | |||
| 321 | // Run any custom tree walkers over the AST | ||
| 322 | if ($this->customTreeWalkers) { | ||
| 323 | $treeWalkerChain = new TreeWalkerChain($this->query, $this->parserResult, $this->queryComponents); | ||
| 324 | |||
| 325 | foreach ($this->customTreeWalkers as $walker) { | ||
| 326 | $treeWalkerChain->addTreeWalker($walker); | ||
| 327 | } | ||
| 328 | |||
| 329 | match (true) { | ||
| 330 | $AST instanceof AST\UpdateStatement => $treeWalkerChain->walkUpdateStatement($AST), | ||
| 331 | $AST instanceof AST\DeleteStatement => $treeWalkerChain->walkDeleteStatement($AST), | ||
| 332 | $AST instanceof AST\SelectStatement => $treeWalkerChain->walkSelectStatement($AST), | ||
| 333 | }; | ||
| 334 | |||
| 335 | $this->queryComponents = $treeWalkerChain->getQueryComponents(); | ||
| 336 | } | ||
| 337 | |||
| 338 | $outputWalkerClass = $this->customOutputWalker ?: SqlWalker::class; | ||
| 339 | $outputWalker = new $outputWalkerClass($this->query, $this->parserResult, $this->queryComponents); | ||
| 340 | |||
| 341 | // Assign an SQL executor to the parser result | ||
| 342 | $this->parserResult->setSqlExecutor($outputWalker->getExecutor($AST)); | ||
| 343 | |||
| 344 | return $this->parserResult; | ||
| 345 | } | ||
| 346 | |||
| 347 | /** | ||
| 348 | * Fixes order of identification variables. | ||
| 349 | * | ||
| 350 | * They have to appear in the select clause in the same order as the | ||
| 351 | * declarations (from ... x join ... y join ... z ...) appear in the query | ||
| 352 | * as the hydration process relies on that order for proper operation. | ||
| 353 | */ | ||
| 354 | private function fixIdentificationVariableOrder(AST\SelectStatement|AST\DeleteStatement|AST\UpdateStatement $AST): void | ||
| 355 | { | ||
| 356 | if (count($this->identVariableExpressions) <= 1) { | ||
| 357 | return; | ||
| 358 | } | ||
| 359 | |||
| 360 | assert($AST instanceof AST\SelectStatement); | ||
| 361 | |||
| 362 | foreach ($this->queryComponents as $dqlAlias => $qComp) { | ||
| 363 | if (! isset($this->identVariableExpressions[$dqlAlias])) { | ||
| 364 | continue; | ||
| 365 | } | ||
| 366 | |||
| 367 | $expr = $this->identVariableExpressions[$dqlAlias]; | ||
| 368 | $key = array_search($expr, $AST->selectClause->selectExpressions, true); | ||
| 369 | |||
| 370 | unset($AST->selectClause->selectExpressions[$key]); | ||
| 371 | |||
| 372 | $AST->selectClause->selectExpressions[] = $expr; | ||
| 373 | } | ||
| 374 | } | ||
| 375 | |||
| 376 | /** | ||
| 377 | * Generates a new syntax error. | ||
| 378 | * | ||
| 379 | * @param string $expected Expected string. | ||
| 380 | * @param DqlToken|null $token Got token. | ||
| 381 | * | ||
| 382 | * @throws QueryException | ||
| 383 | */ | ||
| 384 | public function syntaxError(string $expected = '', Token|null $token = null): never | ||
| 385 | { | ||
| 386 | if ($token === null) { | ||
| 387 | $token = $this->lexer->lookahead; | ||
| 388 | } | ||
| 389 | |||
| 390 | $tokenPos = $token->position ?? '-1'; | ||
| 391 | |||
| 392 | $message = sprintf('line 0, col %d: Error: ', $tokenPos); | ||
| 393 | $message .= $expected !== '' ? sprintf('Expected %s, got ', $expected) : 'Unexpected '; | ||
| 394 | $message .= $this->lexer->lookahead === null ? 'end of string.' : sprintf("'%s'", $token->value); | ||
| 395 | |||
| 396 | throw QueryException::syntaxError($message, QueryException::dqlError($this->query->getDQL() ?? '')); | ||
| 397 | } | ||
| 398 | |||
| 399 | /** | ||
| 400 | * Generates a new semantical error. | ||
| 401 | * | ||
| 402 | * @param string $message Optional message. | ||
| 403 | * @psalm-param DqlToken|null $token | ||
| 404 | * | ||
| 405 | * @throws QueryException | ||
| 406 | */ | ||
| 407 | public function semanticalError(string $message = '', Token|null $token = null): never | ||
| 408 | { | ||
| 409 | if ($token === null) { | ||
| 410 | $token = $this->lexer->lookahead ?? new Token('fake token', 42, 0); | ||
| 411 | } | ||
| 412 | |||
| 413 | // Minimum exposed chars ahead of token | ||
| 414 | $distance = 12; | ||
| 415 | |||
| 416 | // Find a position of a final word to display in error string | ||
| 417 | $dql = $this->query->getDQL(); | ||
| 418 | $length = strlen($dql); | ||
| 419 | $pos = $token->position + $distance; | ||
| 420 | $pos = strpos($dql, ' ', $length > $pos ? $pos : $length); | ||
| 421 | $length = $pos !== false ? $pos - $token->position : $distance; | ||
| 422 | |||
| 423 | $tokenPos = $token->position > 0 ? $token->position : '-1'; | ||
| 424 | $tokenStr = substr($dql, $token->position, $length); | ||
| 425 | |||
| 426 | // Building informative message | ||
| 427 | $message = 'line 0, col ' . $tokenPos . " near '" . $tokenStr . "': Error: " . $message; | ||
| 428 | |||
| 429 | throw QueryException::semanticalError($message, QueryException::dqlError($this->query->getDQL())); | ||
| 430 | } | ||
| 431 | |||
| 432 | /** | ||
| 433 | * Peeks beyond the matched closing parenthesis and returns the first token after that one. | ||
| 434 | * | ||
| 435 | * @param bool $resetPeek Reset peek after finding the closing parenthesis. | ||
| 436 | * | ||
| 437 | * @psalm-return DqlToken|null | ||
| 438 | */ | ||
| 439 | private function peekBeyondClosingParenthesis(bool $resetPeek = true): Token|null | ||
| 440 | { | ||
| 441 | $token = $this->lexer->peek(); | ||
| 442 | $numUnmatched = 1; | ||
| 443 | |||
| 444 | while ($numUnmatched > 0 && $token !== null) { | ||
| 445 | switch ($token->type) { | ||
| 446 | case TokenType::T_OPEN_PARENTHESIS: | ||
| 447 | ++$numUnmatched; | ||
| 448 | break; | ||
| 449 | |||
| 450 | case TokenType::T_CLOSE_PARENTHESIS: | ||
| 451 | --$numUnmatched; | ||
| 452 | break; | ||
| 453 | |||
| 454 | default: | ||
| 455 | // Do nothing | ||
| 456 | } | ||
| 457 | |||
| 458 | $token = $this->lexer->peek(); | ||
| 459 | } | ||
| 460 | |||
| 461 | if ($resetPeek) { | ||
| 462 | $this->lexer->resetPeek(); | ||
| 463 | } | ||
| 464 | |||
| 465 | return $token; | ||
| 466 | } | ||
| 467 | |||
| 468 | /** | ||
| 469 | * Checks if the given token indicates a mathematical operator. | ||
| 470 | * | ||
| 471 | * @psalm-param DqlToken|null $token | ||
| 472 | */ | ||
| 473 | private function isMathOperator(Token|null $token): bool | ||
| 474 | { | ||
| 475 | return $token !== null && in_array($token->type, [TokenType::T_PLUS, TokenType::T_MINUS, TokenType::T_DIVIDE, TokenType::T_MULTIPLY], true); | ||
| 476 | } | ||
| 477 | |||
| 478 | /** | ||
| 479 | * Checks if the next-next (after lookahead) token starts a function. | ||
| 480 | * | ||
| 481 | * @return bool TRUE if the next-next tokens start a function, FALSE otherwise. | ||
| 482 | */ | ||
| 483 | private function isFunction(): bool | ||
| 484 | { | ||
| 485 | assert($this->lexer->lookahead !== null); | ||
| 486 | $lookaheadType = $this->lexer->lookahead->type; | ||
| 487 | $peek = $this->lexer->peek(); | ||
| 488 | |||
| 489 | $this->lexer->resetPeek(); | ||
| 490 | |||
| 491 | return $lookaheadType->value >= TokenType::T_IDENTIFIER->value && $peek !== null && $peek->type === TokenType::T_OPEN_PARENTHESIS; | ||
| 492 | } | ||
| 493 | |||
| 494 | /** | ||
| 495 | * Checks whether the given token type indicates an aggregate function. | ||
| 496 | * | ||
| 497 | * @return bool TRUE if the token type is an aggregate function, FALSE otherwise. | ||
| 498 | */ | ||
| 499 | private function isAggregateFunction(TokenType $tokenType): bool | ||
| 500 | { | ||
| 501 | return in_array( | ||
| 502 | $tokenType, | ||
| 503 | [TokenType::T_AVG, TokenType::T_MIN, TokenType::T_MAX, TokenType::T_SUM, TokenType::T_COUNT], | ||
| 504 | true, | ||
| 505 | ); | ||
| 506 | } | ||
| 507 | |||
| 508 | /** | ||
| 509 | * Checks whether the current lookahead token of the lexer has the type T_ALL, T_ANY or T_SOME. | ||
| 510 | */ | ||
| 511 | private function isNextAllAnySome(): bool | ||
| 512 | { | ||
| 513 | assert($this->lexer->lookahead !== null); | ||
| 514 | |||
| 515 | return in_array( | ||
| 516 | $this->lexer->lookahead->type, | ||
| 517 | [TokenType::T_ALL, TokenType::T_ANY, TokenType::T_SOME], | ||
| 518 | true, | ||
| 519 | ); | ||
| 520 | } | ||
| 521 | |||
| 522 | /** | ||
| 523 | * Validates that the given <tt>IdentificationVariable</tt> is semantically correct. | ||
| 524 | * It must exist in query components list. | ||
| 525 | */ | ||
| 526 | private function processDeferredIdentificationVariables(): void | ||
| 527 | { | ||
| 528 | foreach ($this->deferredIdentificationVariables as $deferredItem) { | ||
| 529 | $identVariable = $deferredItem['expression']; | ||
| 530 | |||
| 531 | // Check if IdentificationVariable exists in queryComponents | ||
| 532 | if (! isset($this->queryComponents[$identVariable])) { | ||
| 533 | $this->semanticalError( | ||
| 534 | sprintf("'%s' is not defined.", $identVariable), | ||
| 535 | $deferredItem['token'], | ||
| 536 | ); | ||
| 537 | } | ||
| 538 | |||
| 539 | $qComp = $this->queryComponents[$identVariable]; | ||
| 540 | |||
| 541 | // Check if queryComponent points to an AbstractSchemaName or a ResultVariable | ||
| 542 | if (! isset($qComp['metadata'])) { | ||
| 543 | $this->semanticalError( | ||
| 544 | sprintf("'%s' does not point to a Class.", $identVariable), | ||
| 545 | $deferredItem['token'], | ||
| 546 | ); | ||
| 547 | } | ||
| 548 | |||
| 549 | // Validate if identification variable nesting level is lower or equal than the current one | ||
| 550 | if ($qComp['nestingLevel'] > $deferredItem['nestingLevel']) { | ||
| 551 | $this->semanticalError( | ||
| 552 | sprintf("'%s' is used outside the scope of its declaration.", $identVariable), | ||
| 553 | $deferredItem['token'], | ||
| 554 | ); | ||
| 555 | } | ||
| 556 | } | ||
| 557 | } | ||
| 558 | |||
| 559 | /** | ||
| 560 | * Validates that the given <tt>NewObjectExpression</tt>. | ||
| 561 | */ | ||
| 562 | private function processDeferredNewObjectExpressions(AST\SelectStatement $AST): void | ||
| 563 | { | ||
| 564 | foreach ($this->deferredNewObjectExpressions as $deferredItem) { | ||
| 565 | $expression = $deferredItem['expression']; | ||
| 566 | $token = $deferredItem['token']; | ||
| 567 | $className = $expression->className; | ||
| 568 | $args = $expression->args; | ||
| 569 | $fromClassName = $AST->fromClause->identificationVariableDeclarations[0]->rangeVariableDeclaration->abstractSchemaName ?? null; | ||
| 570 | |||
| 571 | // If the namespace is not given then assumes the first FROM entity namespace | ||
| 572 | if (! str_contains($className, '\\') && ! class_exists($className) && is_string($fromClassName) && str_contains($fromClassName, '\\')) { | ||
| 573 | $namespace = substr($fromClassName, 0, strrpos($fromClassName, '\\')); | ||
| 574 | $fqcn = $namespace . '\\' . $className; | ||
| 575 | |||
| 576 | if (class_exists($fqcn)) { | ||
| 577 | $expression->className = $fqcn; | ||
| 578 | $className = $fqcn; | ||
| 579 | } | ||
| 580 | } | ||
| 581 | |||
| 582 | if (! class_exists($className)) { | ||
| 583 | $this->semanticalError(sprintf('Class "%s" is not defined.', $className), $token); | ||
| 584 | } | ||
| 585 | |||
| 586 | $class = new ReflectionClass($className); | ||
| 587 | |||
| 588 | if (! $class->isInstantiable()) { | ||
| 589 | $this->semanticalError(sprintf('Class "%s" can not be instantiated.', $className), $token); | ||
| 590 | } | ||
| 591 | |||
| 592 | if ($class->getConstructor() === null) { | ||
| 593 | $this->semanticalError(sprintf('Class "%s" has not a valid constructor.', $className), $token); | ||
| 594 | } | ||
| 595 | |||
| 596 | if ($class->getConstructor()->getNumberOfRequiredParameters() > count($args)) { | ||
| 597 | $this->semanticalError(sprintf('Number of arguments does not match with "%s" constructor declaration.', $className), $token); | ||
| 598 | } | ||
| 599 | } | ||
| 600 | } | ||
| 601 | |||
| 602 | /** | ||
| 603 | * Validates that the given <tt>ResultVariable</tt> is semantically correct. | ||
| 604 | * It must exist in query components list. | ||
| 605 | */ | ||
| 606 | private function processDeferredResultVariables(): void | ||
| 607 | { | ||
| 608 | foreach ($this->deferredResultVariables as $deferredItem) { | ||
| 609 | $resultVariable = $deferredItem['expression']; | ||
| 610 | |||
| 611 | // Check if ResultVariable exists in queryComponents | ||
| 612 | if (! isset($this->queryComponents[$resultVariable])) { | ||
| 613 | $this->semanticalError( | ||
| 614 | sprintf("'%s' is not defined.", $resultVariable), | ||
| 615 | $deferredItem['token'], | ||
| 616 | ); | ||
| 617 | } | ||
| 618 | |||
| 619 | $qComp = $this->queryComponents[$resultVariable]; | ||
| 620 | |||
| 621 | // Check if queryComponent points to an AbstractSchemaName or a ResultVariable | ||
| 622 | if (! isset($qComp['resultVariable'])) { | ||
| 623 | $this->semanticalError( | ||
| 624 | sprintf("'%s' does not point to a ResultVariable.", $resultVariable), | ||
| 625 | $deferredItem['token'], | ||
| 626 | ); | ||
| 627 | } | ||
| 628 | |||
| 629 | // Validate if identification variable nesting level is lower or equal than the current one | ||
| 630 | if ($qComp['nestingLevel'] > $deferredItem['nestingLevel']) { | ||
| 631 | $this->semanticalError( | ||
| 632 | sprintf("'%s' is used outside the scope of its declaration.", $resultVariable), | ||
| 633 | $deferredItem['token'], | ||
| 634 | ); | ||
| 635 | } | ||
| 636 | } | ||
| 637 | } | ||
| 638 | |||
| 639 | /** | ||
| 640 | * Validates that the given <tt>PathExpression</tt> is semantically correct for grammar rules: | ||
| 641 | * | ||
| 642 | * AssociationPathExpression ::= CollectionValuedPathExpression | SingleValuedAssociationPathExpression | ||
| 643 | * SingleValuedPathExpression ::= StateFieldPathExpression | SingleValuedAssociationPathExpression | ||
| 644 | * StateFieldPathExpression ::= IdentificationVariable "." StateField | ||
| 645 | * SingleValuedAssociationPathExpression ::= IdentificationVariable "." SingleValuedAssociationField | ||
| 646 | * CollectionValuedPathExpression ::= IdentificationVariable "." CollectionValuedAssociationField | ||
| 647 | */ | ||
| 648 | private function processDeferredPathExpressions(): void | ||
| 649 | { | ||
| 650 | foreach ($this->deferredPathExpressions as $deferredItem) { | ||
| 651 | $pathExpression = $deferredItem['expression']; | ||
| 652 | |||
| 653 | $class = $this->getMetadataForDqlAlias($pathExpression->identificationVariable); | ||
| 654 | |||
| 655 | $field = $pathExpression->field; | ||
| 656 | if ($field === null) { | ||
| 657 | $field = $pathExpression->field = $class->identifier[0]; | ||
| 658 | } | ||
| 659 | |||
| 660 | // Check if field or association exists | ||
| 661 | if (! isset($class->associationMappings[$field]) && ! isset($class->fieldMappings[$field])) { | ||
| 662 | $this->semanticalError( | ||
| 663 | 'Class ' . $class->name . ' has no field or association named ' . $field, | ||
| 664 | $deferredItem['token'], | ||
| 665 | ); | ||
| 666 | } | ||
| 667 | |||
| 668 | $fieldType = AST\PathExpression::TYPE_STATE_FIELD; | ||
| 669 | |||
| 670 | if (isset($class->associationMappings[$field])) { | ||
| 671 | $assoc = $class->associationMappings[$field]; | ||
| 672 | |||
| 673 | $fieldType = $assoc->isToOne() | ||
| 674 | ? AST\PathExpression::TYPE_SINGLE_VALUED_ASSOCIATION | ||
| 675 | : AST\PathExpression::TYPE_COLLECTION_VALUED_ASSOCIATION; | ||
| 676 | } | ||
| 677 | |||
| 678 | // Validate if PathExpression is one of the expected types | ||
| 679 | $expectedType = $pathExpression->expectedType; | ||
| 680 | |||
| 681 | if (! ($expectedType & $fieldType)) { | ||
| 682 | // We need to recognize which was expected type(s) | ||
| 683 | $expectedStringTypes = []; | ||
| 684 | |||
| 685 | // Validate state field type | ||
| 686 | if ($expectedType & AST\PathExpression::TYPE_STATE_FIELD) { | ||
| 687 | $expectedStringTypes[] = 'StateFieldPathExpression'; | ||
| 688 | } | ||
| 689 | |||
| 690 | // Validate single valued association (*-to-one) | ||
| 691 | if ($expectedType & AST\PathExpression::TYPE_SINGLE_VALUED_ASSOCIATION) { | ||
| 692 | $expectedStringTypes[] = 'SingleValuedAssociationField'; | ||
| 693 | } | ||
| 694 | |||
| 695 | // Validate single valued association (*-to-many) | ||
| 696 | if ($expectedType & AST\PathExpression::TYPE_COLLECTION_VALUED_ASSOCIATION) { | ||
| 697 | $expectedStringTypes[] = 'CollectionValuedAssociationField'; | ||
| 698 | } | ||
| 699 | |||
| 700 | // Build the error message | ||
| 701 | $semanticalError = 'Invalid PathExpression. '; | ||
| 702 | $semanticalError .= count($expectedStringTypes) === 1 | ||
| 703 | ? 'Must be a ' . $expectedStringTypes[0] . '.' | ||
| 704 | : implode(' or ', $expectedStringTypes) . ' expected.'; | ||
| 705 | |||
| 706 | $this->semanticalError($semanticalError, $deferredItem['token']); | ||
| 707 | } | ||
| 708 | |||
| 709 | // We need to force the type in PathExpression | ||
| 710 | $pathExpression->type = $fieldType; | ||
| 711 | } | ||
| 712 | } | ||
| 713 | |||
| 714 | private function processRootEntityAliasSelected(): void | ||
| 715 | { | ||
| 716 | if (! count($this->identVariableExpressions)) { | ||
| 717 | return; | ||
| 718 | } | ||
| 719 | |||
| 720 | foreach ($this->identVariableExpressions as $dqlAlias => $expr) { | ||
| 721 | if (isset($this->queryComponents[$dqlAlias]) && ! isset($this->queryComponents[$dqlAlias]['parent'])) { | ||
| 722 | return; | ||
| 723 | } | ||
| 724 | } | ||
| 725 | |||
| 726 | $this->semanticalError('Cannot select entity through identification variables without choosing at least one root entity alias.'); | ||
| 727 | } | ||
| 728 | |||
| 729 | /** | ||
| 730 | * QueryLanguage ::= SelectStatement | UpdateStatement | DeleteStatement | ||
| 731 | */ | ||
| 732 | public function QueryLanguage(): AST\SelectStatement|AST\UpdateStatement|AST\DeleteStatement | ||
| 733 | { | ||
| 734 | $statement = null; | ||
| 735 | |||
| 736 | $this->lexer->moveNext(); | ||
| 737 | |||
| 738 | switch ($this->lexer->lookahead->type ?? null) { | ||
| 739 | case TokenType::T_SELECT: | ||
| 740 | $statement = $this->SelectStatement(); | ||
| 741 | break; | ||
| 742 | |||
| 743 | case TokenType::T_UPDATE: | ||
| 744 | $statement = $this->UpdateStatement(); | ||
| 745 | break; | ||
| 746 | |||
| 747 | case TokenType::T_DELETE: | ||
| 748 | $statement = $this->DeleteStatement(); | ||
| 749 | break; | ||
| 750 | |||
| 751 | default: | ||
| 752 | $this->syntaxError('SELECT, UPDATE or DELETE'); | ||
| 753 | break; | ||
| 754 | } | ||
| 755 | |||
| 756 | // Check for end of string | ||
| 757 | if ($this->lexer->lookahead !== null) { | ||
| 758 | $this->syntaxError('end of string'); | ||
| 759 | } | ||
| 760 | |||
| 761 | return $statement; | ||
| 762 | } | ||
| 763 | |||
| 764 | /** | ||
| 765 | * SelectStatement ::= SelectClause FromClause [WhereClause] [GroupByClause] [HavingClause] [OrderByClause] | ||
| 766 | */ | ||
| 767 | public function SelectStatement(): AST\SelectStatement | ||
| 768 | { | ||
| 769 | $selectStatement = new AST\SelectStatement($this->SelectClause(), $this->FromClause()); | ||
| 770 | |||
| 771 | $selectStatement->whereClause = $this->lexer->isNextToken(TokenType::T_WHERE) ? $this->WhereClause() : null; | ||
| 772 | $selectStatement->groupByClause = $this->lexer->isNextToken(TokenType::T_GROUP) ? $this->GroupByClause() : null; | ||
| 773 | $selectStatement->havingClause = $this->lexer->isNextToken(TokenType::T_HAVING) ? $this->HavingClause() : null; | ||
| 774 | $selectStatement->orderByClause = $this->lexer->isNextToken(TokenType::T_ORDER) ? $this->OrderByClause() : null; | ||
| 775 | |||
| 776 | return $selectStatement; | ||
| 777 | } | ||
| 778 | |||
| 779 | /** | ||
| 780 | * UpdateStatement ::= UpdateClause [WhereClause] | ||
| 781 | */ | ||
| 782 | public function UpdateStatement(): AST\UpdateStatement | ||
| 783 | { | ||
| 784 | $updateStatement = new AST\UpdateStatement($this->UpdateClause()); | ||
| 785 | |||
| 786 | $updateStatement->whereClause = $this->lexer->isNextToken(TokenType::T_WHERE) ? $this->WhereClause() : null; | ||
| 787 | |||
| 788 | return $updateStatement; | ||
| 789 | } | ||
| 790 | |||
| 791 | /** | ||
| 792 | * DeleteStatement ::= DeleteClause [WhereClause] | ||
| 793 | */ | ||
| 794 | public function DeleteStatement(): AST\DeleteStatement | ||
| 795 | { | ||
| 796 | $deleteStatement = new AST\DeleteStatement($this->DeleteClause()); | ||
| 797 | |||
| 798 | $deleteStatement->whereClause = $this->lexer->isNextToken(TokenType::T_WHERE) ? $this->WhereClause() : null; | ||
| 799 | |||
| 800 | return $deleteStatement; | ||
| 801 | } | ||
| 802 | |||
| 803 | /** | ||
| 804 | * IdentificationVariable ::= identifier | ||
| 805 | */ | ||
| 806 | public function IdentificationVariable(): string | ||
| 807 | { | ||
| 808 | $this->match(TokenType::T_IDENTIFIER); | ||
| 809 | |||
| 810 | assert($this->lexer->token !== null); | ||
| 811 | $identVariable = $this->lexer->token->value; | ||
| 812 | |||
| 813 | $this->deferredIdentificationVariables[] = [ | ||
| 814 | 'expression' => $identVariable, | ||
| 815 | 'nestingLevel' => $this->nestingLevel, | ||
| 816 | 'token' => $this->lexer->token, | ||
| 817 | ]; | ||
| 818 | |||
| 819 | return $identVariable; | ||
| 820 | } | ||
| 821 | |||
| 822 | /** | ||
| 823 | * AliasIdentificationVariable = identifier | ||
| 824 | */ | ||
| 825 | public function AliasIdentificationVariable(): string | ||
| 826 | { | ||
| 827 | $this->match(TokenType::T_IDENTIFIER); | ||
| 828 | |||
| 829 | assert($this->lexer->token !== null); | ||
| 830 | $aliasIdentVariable = $this->lexer->token->value; | ||
| 831 | $exists = isset($this->queryComponents[$aliasIdentVariable]); | ||
| 832 | |||
| 833 | if ($exists) { | ||
| 834 | $this->semanticalError( | ||
| 835 | sprintf("'%s' is already defined.", $aliasIdentVariable), | ||
| 836 | $this->lexer->token, | ||
| 837 | ); | ||
| 838 | } | ||
| 839 | |||
| 840 | return $aliasIdentVariable; | ||
| 841 | } | ||
| 842 | |||
| 843 | /** | ||
| 844 | * AbstractSchemaName ::= fully_qualified_name | identifier | ||
| 845 | */ | ||
| 846 | public function AbstractSchemaName(): string | ||
| 847 | { | ||
| 848 | if ($this->lexer->isNextToken(TokenType::T_FULLY_QUALIFIED_NAME)) { | ||
| 849 | $this->match(TokenType::T_FULLY_QUALIFIED_NAME); | ||
| 850 | assert($this->lexer->token !== null); | ||
| 851 | |||
| 852 | return $this->lexer->token->value; | ||
| 853 | } | ||
| 854 | |||
| 855 | $this->match(TokenType::T_IDENTIFIER); | ||
| 856 | assert($this->lexer->token !== null); | ||
| 857 | |||
| 858 | return $this->lexer->token->value; | ||
| 859 | } | ||
| 860 | |||
| 861 | /** | ||
| 862 | * Validates an AbstractSchemaName, making sure the class exists. | ||
| 863 | * | ||
| 864 | * @param string $schemaName The name to validate. | ||
| 865 | * | ||
| 866 | * @throws QueryException if the name does not exist. | ||
| 867 | */ | ||
| 868 | private function validateAbstractSchemaName(string $schemaName): void | ||
| 869 | { | ||
| 870 | assert($this->lexer->token !== null); | ||
| 871 | if (! (class_exists($schemaName, true) || interface_exists($schemaName, true))) { | ||
| 872 | $this->semanticalError( | ||
| 873 | sprintf("Class '%s' is not defined.", $schemaName), | ||
| 874 | $this->lexer->token, | ||
| 875 | ); | ||
| 876 | } | ||
| 877 | } | ||
| 878 | |||
| 879 | /** | ||
| 880 | * AliasResultVariable ::= identifier | ||
| 881 | */ | ||
| 882 | public function AliasResultVariable(): string | ||
| 883 | { | ||
| 884 | $this->match(TokenType::T_IDENTIFIER); | ||
| 885 | |||
| 886 | assert($this->lexer->token !== null); | ||
| 887 | $resultVariable = $this->lexer->token->value; | ||
| 888 | $exists = isset($this->queryComponents[$resultVariable]); | ||
| 889 | |||
| 890 | if ($exists) { | ||
| 891 | $this->semanticalError( | ||
| 892 | sprintf("'%s' is already defined.", $resultVariable), | ||
| 893 | $this->lexer->token, | ||
| 894 | ); | ||
| 895 | } | ||
| 896 | |||
| 897 | return $resultVariable; | ||
| 898 | } | ||
| 899 | |||
| 900 | /** | ||
| 901 | * ResultVariable ::= identifier | ||
| 902 | */ | ||
| 903 | public function ResultVariable(): string | ||
| 904 | { | ||
| 905 | $this->match(TokenType::T_IDENTIFIER); | ||
| 906 | |||
| 907 | assert($this->lexer->token !== null); | ||
| 908 | $resultVariable = $this->lexer->token->value; | ||
| 909 | |||
| 910 | // Defer ResultVariable validation | ||
| 911 | $this->deferredResultVariables[] = [ | ||
| 912 | 'expression' => $resultVariable, | ||
| 913 | 'nestingLevel' => $this->nestingLevel, | ||
| 914 | 'token' => $this->lexer->token, | ||
| 915 | ]; | ||
| 916 | |||
| 917 | return $resultVariable; | ||
| 918 | } | ||
| 919 | |||
| 920 | /** | ||
| 921 | * JoinAssociationPathExpression ::= IdentificationVariable "." (CollectionValuedAssociationField | SingleValuedAssociationField) | ||
| 922 | */ | ||
| 923 | public function JoinAssociationPathExpression(): AST\JoinAssociationPathExpression | ||
| 924 | { | ||
| 925 | $identVariable = $this->IdentificationVariable(); | ||
| 926 | |||
| 927 | if (! isset($this->queryComponents[$identVariable])) { | ||
| 928 | $this->semanticalError( | ||
| 929 | 'Identification Variable ' . $identVariable . ' used in join path expression but was not defined before.', | ||
| 930 | ); | ||
| 931 | } | ||
| 932 | |||
| 933 | $this->match(TokenType::T_DOT); | ||
| 934 | $this->match(TokenType::T_IDENTIFIER); | ||
| 935 | |||
| 936 | assert($this->lexer->token !== null); | ||
| 937 | $field = $this->lexer->token->value; | ||
| 938 | |||
| 939 | // Validate association field | ||
| 940 | $class = $this->getMetadataForDqlAlias($identVariable); | ||
| 941 | |||
| 942 | if (! $class->hasAssociation($field)) { | ||
| 943 | $this->semanticalError('Class ' . $class->name . ' has no association named ' . $field); | ||
| 944 | } | ||
| 945 | |||
| 946 | return new AST\JoinAssociationPathExpression($identVariable, $field); | ||
| 947 | } | ||
| 948 | |||
| 949 | /** | ||
| 950 | * Parses an arbitrary path expression and defers semantical validation | ||
| 951 | * based on expected types. | ||
| 952 | * | ||
| 953 | * PathExpression ::= IdentificationVariable {"." identifier}* | ||
| 954 | * | ||
| 955 | * @psalm-param int-mask-of<AST\PathExpression::TYPE_*> $expectedTypes | ||
| 956 | */ | ||
| 957 | public function PathExpression(int $expectedTypes): AST\PathExpression | ||
| 958 | { | ||
| 959 | $identVariable = $this->IdentificationVariable(); | ||
| 960 | $field = null; | ||
| 961 | |||
| 962 | assert($this->lexer->token !== null); | ||
| 963 | if ($this->lexer->isNextToken(TokenType::T_DOT)) { | ||
| 964 | $this->match(TokenType::T_DOT); | ||
| 965 | $this->match(TokenType::T_IDENTIFIER); | ||
| 966 | |||
| 967 | $field = $this->lexer->token->value; | ||
| 968 | |||
| 969 | while ($this->lexer->isNextToken(TokenType::T_DOT)) { | ||
| 970 | $this->match(TokenType::T_DOT); | ||
| 971 | $this->match(TokenType::T_IDENTIFIER); | ||
| 972 | $field .= '.' . $this->lexer->token->value; | ||
| 973 | } | ||
| 974 | } | ||
| 975 | |||
| 976 | // Creating AST node | ||
| 977 | $pathExpr = new AST\PathExpression($expectedTypes, $identVariable, $field); | ||
| 978 | |||
| 979 | // Defer PathExpression validation if requested to be deferred | ||
| 980 | $this->deferredPathExpressions[] = [ | ||
| 981 | 'expression' => $pathExpr, | ||
| 982 | 'nestingLevel' => $this->nestingLevel, | ||
| 983 | 'token' => $this->lexer->token, | ||
| 984 | ]; | ||
| 985 | |||
| 986 | return $pathExpr; | ||
| 987 | } | ||
| 988 | |||
| 989 | /** | ||
| 990 | * AssociationPathExpression ::= CollectionValuedPathExpression | SingleValuedAssociationPathExpression | ||
| 991 | */ | ||
| 992 | public function AssociationPathExpression(): AST\PathExpression | ||
| 993 | { | ||
| 994 | return $this->PathExpression( | ||
| 995 | AST\PathExpression::TYPE_SINGLE_VALUED_ASSOCIATION | | ||
| 996 | AST\PathExpression::TYPE_COLLECTION_VALUED_ASSOCIATION, | ||
| 997 | ); | ||
| 998 | } | ||
| 999 | |||
| 1000 | /** | ||
| 1001 | * SingleValuedPathExpression ::= StateFieldPathExpression | SingleValuedAssociationPathExpression | ||
| 1002 | */ | ||
| 1003 | public function SingleValuedPathExpression(): AST\PathExpression | ||
| 1004 | { | ||
| 1005 | return $this->PathExpression( | ||
| 1006 | AST\PathExpression::TYPE_STATE_FIELD | | ||
| 1007 | AST\PathExpression::TYPE_SINGLE_VALUED_ASSOCIATION, | ||
| 1008 | ); | ||
| 1009 | } | ||
| 1010 | |||
| 1011 | /** | ||
| 1012 | * StateFieldPathExpression ::= IdentificationVariable "." StateField | ||
| 1013 | */ | ||
| 1014 | public function StateFieldPathExpression(): AST\PathExpression | ||
| 1015 | { | ||
| 1016 | return $this->PathExpression(AST\PathExpression::TYPE_STATE_FIELD); | ||
| 1017 | } | ||
| 1018 | |||
| 1019 | /** | ||
| 1020 | * SingleValuedAssociationPathExpression ::= IdentificationVariable "." SingleValuedAssociationField | ||
| 1021 | */ | ||
| 1022 | public function SingleValuedAssociationPathExpression(): AST\PathExpression | ||
| 1023 | { | ||
| 1024 | return $this->PathExpression(AST\PathExpression::TYPE_SINGLE_VALUED_ASSOCIATION); | ||
| 1025 | } | ||
| 1026 | |||
| 1027 | /** | ||
| 1028 | * CollectionValuedPathExpression ::= IdentificationVariable "." CollectionValuedAssociationField | ||
| 1029 | */ | ||
| 1030 | public function CollectionValuedPathExpression(): AST\PathExpression | ||
| 1031 | { | ||
| 1032 | return $this->PathExpression(AST\PathExpression::TYPE_COLLECTION_VALUED_ASSOCIATION); | ||
| 1033 | } | ||
| 1034 | |||
| 1035 | /** | ||
| 1036 | * SelectClause ::= "SELECT" ["DISTINCT"] SelectExpression {"," SelectExpression} | ||
| 1037 | */ | ||
| 1038 | public function SelectClause(): AST\SelectClause | ||
| 1039 | { | ||
| 1040 | $isDistinct = false; | ||
| 1041 | $this->match(TokenType::T_SELECT); | ||
| 1042 | |||
| 1043 | // Check for DISTINCT | ||
| 1044 | if ($this->lexer->isNextToken(TokenType::T_DISTINCT)) { | ||
| 1045 | $this->match(TokenType::T_DISTINCT); | ||
| 1046 | |||
| 1047 | $isDistinct = true; | ||
| 1048 | } | ||
| 1049 | |||
| 1050 | // Process SelectExpressions (1..N) | ||
| 1051 | $selectExpressions = []; | ||
| 1052 | $selectExpressions[] = $this->SelectExpression(); | ||
| 1053 | |||
| 1054 | while ($this->lexer->isNextToken(TokenType::T_COMMA)) { | ||
| 1055 | $this->match(TokenType::T_COMMA); | ||
| 1056 | |||
| 1057 | $selectExpressions[] = $this->SelectExpression(); | ||
| 1058 | } | ||
| 1059 | |||
| 1060 | return new AST\SelectClause($selectExpressions, $isDistinct); | ||
| 1061 | } | ||
| 1062 | |||
| 1063 | /** | ||
| 1064 | * SimpleSelectClause ::= "SELECT" ["DISTINCT"] SimpleSelectExpression | ||
| 1065 | */ | ||
| 1066 | public function SimpleSelectClause(): AST\SimpleSelectClause | ||
| 1067 | { | ||
| 1068 | $isDistinct = false; | ||
| 1069 | $this->match(TokenType::T_SELECT); | ||
| 1070 | |||
| 1071 | if ($this->lexer->isNextToken(TokenType::T_DISTINCT)) { | ||
| 1072 | $this->match(TokenType::T_DISTINCT); | ||
| 1073 | |||
| 1074 | $isDistinct = true; | ||
| 1075 | } | ||
| 1076 | |||
| 1077 | return new AST\SimpleSelectClause($this->SimpleSelectExpression(), $isDistinct); | ||
| 1078 | } | ||
| 1079 | |||
| 1080 | /** | ||
| 1081 | * UpdateClause ::= "UPDATE" AbstractSchemaName ["AS"] AliasIdentificationVariable "SET" UpdateItem {"," UpdateItem}* | ||
| 1082 | */ | ||
| 1083 | public function UpdateClause(): AST\UpdateClause | ||
| 1084 | { | ||
| 1085 | $this->match(TokenType::T_UPDATE); | ||
| 1086 | assert($this->lexer->lookahead !== null); | ||
| 1087 | |||
| 1088 | $token = $this->lexer->lookahead; | ||
| 1089 | $abstractSchemaName = $this->AbstractSchemaName(); | ||
| 1090 | |||
| 1091 | $this->validateAbstractSchemaName($abstractSchemaName); | ||
| 1092 | |||
| 1093 | if ($this->lexer->isNextToken(TokenType::T_AS)) { | ||
| 1094 | $this->match(TokenType::T_AS); | ||
| 1095 | } | ||
| 1096 | |||
| 1097 | $aliasIdentificationVariable = $this->AliasIdentificationVariable(); | ||
| 1098 | |||
| 1099 | $class = $this->em->getClassMetadata($abstractSchemaName); | ||
| 1100 | |||
| 1101 | // Building queryComponent | ||
| 1102 | $queryComponent = [ | ||
| 1103 | 'metadata' => $class, | ||
| 1104 | 'parent' => null, | ||
| 1105 | 'relation' => null, | ||
| 1106 | 'map' => null, | ||
| 1107 | 'nestingLevel' => $this->nestingLevel, | ||
| 1108 | 'token' => $token, | ||
| 1109 | ]; | ||
| 1110 | |||
| 1111 | $this->queryComponents[$aliasIdentificationVariable] = $queryComponent; | ||
| 1112 | |||
| 1113 | $this->match(TokenType::T_SET); | ||
| 1114 | |||
| 1115 | $updateItems = []; | ||
| 1116 | $updateItems[] = $this->UpdateItem(); | ||
| 1117 | |||
| 1118 | while ($this->lexer->isNextToken(TokenType::T_COMMA)) { | ||
| 1119 | $this->match(TokenType::T_COMMA); | ||
| 1120 | |||
| 1121 | $updateItems[] = $this->UpdateItem(); | ||
| 1122 | } | ||
| 1123 | |||
| 1124 | $updateClause = new AST\UpdateClause($abstractSchemaName, $updateItems); | ||
| 1125 | $updateClause->aliasIdentificationVariable = $aliasIdentificationVariable; | ||
| 1126 | |||
| 1127 | return $updateClause; | ||
| 1128 | } | ||
| 1129 | |||
| 1130 | /** | ||
| 1131 | * DeleteClause ::= "DELETE" ["FROM"] AbstractSchemaName ["AS"] AliasIdentificationVariable | ||
| 1132 | */ | ||
| 1133 | public function DeleteClause(): AST\DeleteClause | ||
| 1134 | { | ||
| 1135 | $this->match(TokenType::T_DELETE); | ||
| 1136 | |||
| 1137 | if ($this->lexer->isNextToken(TokenType::T_FROM)) { | ||
| 1138 | $this->match(TokenType::T_FROM); | ||
| 1139 | } | ||
| 1140 | |||
| 1141 | assert($this->lexer->lookahead !== null); | ||
| 1142 | $token = $this->lexer->lookahead; | ||
| 1143 | $abstractSchemaName = $this->AbstractSchemaName(); | ||
| 1144 | |||
| 1145 | $this->validateAbstractSchemaName($abstractSchemaName); | ||
| 1146 | |||
| 1147 | $deleteClause = new AST\DeleteClause($abstractSchemaName); | ||
| 1148 | |||
| 1149 | if ($this->lexer->isNextToken(TokenType::T_AS)) { | ||
| 1150 | $this->match(TokenType::T_AS); | ||
| 1151 | } | ||
| 1152 | |||
| 1153 | $aliasIdentificationVariable = $this->lexer->isNextToken(TokenType::T_IDENTIFIER) | ||
| 1154 | ? $this->AliasIdentificationVariable() | ||
| 1155 | : 'alias_should_have_been_set'; | ||
| 1156 | |||
| 1157 | $deleteClause->aliasIdentificationVariable = $aliasIdentificationVariable; | ||
| 1158 | $class = $this->em->getClassMetadata($deleteClause->abstractSchemaName); | ||
| 1159 | |||
| 1160 | // Building queryComponent | ||
| 1161 | $queryComponent = [ | ||
| 1162 | 'metadata' => $class, | ||
| 1163 | 'parent' => null, | ||
| 1164 | 'relation' => null, | ||
| 1165 | 'map' => null, | ||
| 1166 | 'nestingLevel' => $this->nestingLevel, | ||
| 1167 | 'token' => $token, | ||
| 1168 | ]; | ||
| 1169 | |||
| 1170 | $this->queryComponents[$aliasIdentificationVariable] = $queryComponent; | ||
| 1171 | |||
| 1172 | return $deleteClause; | ||
| 1173 | } | ||
| 1174 | |||
| 1175 | /** | ||
| 1176 | * FromClause ::= "FROM" IdentificationVariableDeclaration {"," IdentificationVariableDeclaration}* | ||
| 1177 | */ | ||
| 1178 | public function FromClause(): AST\FromClause | ||
| 1179 | { | ||
| 1180 | $this->match(TokenType::T_FROM); | ||
| 1181 | |||
| 1182 | $identificationVariableDeclarations = []; | ||
| 1183 | $identificationVariableDeclarations[] = $this->IdentificationVariableDeclaration(); | ||
| 1184 | |||
| 1185 | while ($this->lexer->isNextToken(TokenType::T_COMMA)) { | ||
| 1186 | $this->match(TokenType::T_COMMA); | ||
| 1187 | |||
| 1188 | $identificationVariableDeclarations[] = $this->IdentificationVariableDeclaration(); | ||
| 1189 | } | ||
| 1190 | |||
| 1191 | return new AST\FromClause($identificationVariableDeclarations); | ||
| 1192 | } | ||
| 1193 | |||
| 1194 | /** | ||
| 1195 | * SubselectFromClause ::= "FROM" SubselectIdentificationVariableDeclaration {"," SubselectIdentificationVariableDeclaration}* | ||
| 1196 | */ | ||
| 1197 | public function SubselectFromClause(): AST\SubselectFromClause | ||
| 1198 | { | ||
| 1199 | $this->match(TokenType::T_FROM); | ||
| 1200 | |||
| 1201 | $identificationVariables = []; | ||
| 1202 | $identificationVariables[] = $this->SubselectIdentificationVariableDeclaration(); | ||
| 1203 | |||
| 1204 | while ($this->lexer->isNextToken(TokenType::T_COMMA)) { | ||
| 1205 | $this->match(TokenType::T_COMMA); | ||
| 1206 | |||
| 1207 | $identificationVariables[] = $this->SubselectIdentificationVariableDeclaration(); | ||
| 1208 | } | ||
| 1209 | |||
| 1210 | return new AST\SubselectFromClause($identificationVariables); | ||
| 1211 | } | ||
| 1212 | |||
| 1213 | /** | ||
| 1214 | * WhereClause ::= "WHERE" ConditionalExpression | ||
| 1215 | */ | ||
| 1216 | public function WhereClause(): AST\WhereClause | ||
| 1217 | { | ||
| 1218 | $this->match(TokenType::T_WHERE); | ||
| 1219 | |||
| 1220 | return new AST\WhereClause($this->ConditionalExpression()); | ||
| 1221 | } | ||
| 1222 | |||
| 1223 | /** | ||
| 1224 | * HavingClause ::= "HAVING" ConditionalExpression | ||
| 1225 | */ | ||
| 1226 | public function HavingClause(): AST\HavingClause | ||
| 1227 | { | ||
| 1228 | $this->match(TokenType::T_HAVING); | ||
| 1229 | |||
| 1230 | return new AST\HavingClause($this->ConditionalExpression()); | ||
| 1231 | } | ||
| 1232 | |||
| 1233 | /** | ||
| 1234 | * GroupByClause ::= "GROUP" "BY" GroupByItem {"," GroupByItem}* | ||
| 1235 | */ | ||
| 1236 | public function GroupByClause(): AST\GroupByClause | ||
| 1237 | { | ||
| 1238 | $this->match(TokenType::T_GROUP); | ||
| 1239 | $this->match(TokenType::T_BY); | ||
| 1240 | |||
| 1241 | $groupByItems = [$this->GroupByItem()]; | ||
| 1242 | |||
| 1243 | while ($this->lexer->isNextToken(TokenType::T_COMMA)) { | ||
| 1244 | $this->match(TokenType::T_COMMA); | ||
| 1245 | |||
| 1246 | $groupByItems[] = $this->GroupByItem(); | ||
| 1247 | } | ||
| 1248 | |||
| 1249 | return new AST\GroupByClause($groupByItems); | ||
| 1250 | } | ||
| 1251 | |||
| 1252 | /** | ||
| 1253 | * OrderByClause ::= "ORDER" "BY" OrderByItem {"," OrderByItem}* | ||
| 1254 | */ | ||
| 1255 | public function OrderByClause(): AST\OrderByClause | ||
| 1256 | { | ||
| 1257 | $this->match(TokenType::T_ORDER); | ||
| 1258 | $this->match(TokenType::T_BY); | ||
| 1259 | |||
| 1260 | $orderByItems = []; | ||
| 1261 | $orderByItems[] = $this->OrderByItem(); | ||
| 1262 | |||
| 1263 | while ($this->lexer->isNextToken(TokenType::T_COMMA)) { | ||
| 1264 | $this->match(TokenType::T_COMMA); | ||
| 1265 | |||
| 1266 | $orderByItems[] = $this->OrderByItem(); | ||
| 1267 | } | ||
| 1268 | |||
| 1269 | return new AST\OrderByClause($orderByItems); | ||
| 1270 | } | ||
| 1271 | |||
| 1272 | /** | ||
| 1273 | * Subselect ::= SimpleSelectClause SubselectFromClause [WhereClause] [GroupByClause] [HavingClause] [OrderByClause] | ||
| 1274 | */ | ||
| 1275 | public function Subselect(): AST\Subselect | ||
| 1276 | { | ||
| 1277 | // Increase query nesting level | ||
| 1278 | $this->nestingLevel++; | ||
| 1279 | |||
| 1280 | $subselect = new AST\Subselect($this->SimpleSelectClause(), $this->SubselectFromClause()); | ||
| 1281 | |||
| 1282 | $subselect->whereClause = $this->lexer->isNextToken(TokenType::T_WHERE) ? $this->WhereClause() : null; | ||
| 1283 | $subselect->groupByClause = $this->lexer->isNextToken(TokenType::T_GROUP) ? $this->GroupByClause() : null; | ||
| 1284 | $subselect->havingClause = $this->lexer->isNextToken(TokenType::T_HAVING) ? $this->HavingClause() : null; | ||
| 1285 | $subselect->orderByClause = $this->lexer->isNextToken(TokenType::T_ORDER) ? $this->OrderByClause() : null; | ||
| 1286 | |||
| 1287 | // Decrease query nesting level | ||
| 1288 | $this->nestingLevel--; | ||
| 1289 | |||
| 1290 | return $subselect; | ||
| 1291 | } | ||
| 1292 | |||
| 1293 | /** | ||
| 1294 | * UpdateItem ::= SingleValuedPathExpression "=" NewValue | ||
| 1295 | */ | ||
| 1296 | public function UpdateItem(): AST\UpdateItem | ||
| 1297 | { | ||
| 1298 | $pathExpr = $this->SingleValuedPathExpression(); | ||
| 1299 | |||
| 1300 | $this->match(TokenType::T_EQUALS); | ||
| 1301 | |||
| 1302 | return new AST\UpdateItem($pathExpr, $this->NewValue()); | ||
| 1303 | } | ||
| 1304 | |||
| 1305 | /** | ||
| 1306 | * GroupByItem ::= IdentificationVariable | ResultVariable | SingleValuedPathExpression | ||
| 1307 | */ | ||
| 1308 | public function GroupByItem(): string|AST\PathExpression | ||
| 1309 | { | ||
| 1310 | // We need to check if we are in a IdentificationVariable or SingleValuedPathExpression | ||
| 1311 | $glimpse = $this->lexer->glimpse(); | ||
| 1312 | |||
| 1313 | if ($glimpse !== null && $glimpse->type === TokenType::T_DOT) { | ||
| 1314 | return $this->SingleValuedPathExpression(); | ||
| 1315 | } | ||
| 1316 | |||
| 1317 | assert($this->lexer->lookahead !== null); | ||
| 1318 | // Still need to decide between IdentificationVariable or ResultVariable | ||
| 1319 | $lookaheadValue = $this->lexer->lookahead->value; | ||
| 1320 | |||
| 1321 | if (! isset($this->queryComponents[$lookaheadValue])) { | ||
| 1322 | $this->semanticalError('Cannot group by undefined identification or result variable.'); | ||
| 1323 | } | ||
| 1324 | |||
| 1325 | return isset($this->queryComponents[$lookaheadValue]['metadata']) | ||
| 1326 | ? $this->IdentificationVariable() | ||
| 1327 | : $this->ResultVariable(); | ||
| 1328 | } | ||
| 1329 | |||
| 1330 | /** | ||
| 1331 | * OrderByItem ::= ( | ||
| 1332 | * SimpleArithmeticExpression | SingleValuedPathExpression | CaseExpression | | ||
| 1333 | * ScalarExpression | ResultVariable | FunctionDeclaration | ||
| 1334 | * ) ["ASC" | "DESC"] | ||
| 1335 | */ | ||
| 1336 | public function OrderByItem(): AST\OrderByItem | ||
| 1337 | { | ||
| 1338 | $this->lexer->peek(); // lookahead => '.' | ||
| 1339 | $this->lexer->peek(); // lookahead => token after '.' | ||
| 1340 | |||
| 1341 | $peek = $this->lexer->peek(); // lookahead => token after the token after the '.' | ||
| 1342 | |||
| 1343 | $this->lexer->resetPeek(); | ||
| 1344 | |||
| 1345 | $glimpse = $this->lexer->glimpse(); | ||
| 1346 | |||
| 1347 | assert($this->lexer->lookahead !== null); | ||
| 1348 | $expr = match (true) { | ||
| 1349 | $this->isMathOperator($peek) => $this->SimpleArithmeticExpression(), | ||
| 1350 | $glimpse !== null && $glimpse->type === TokenType::T_DOT => $this->SingleValuedPathExpression(), | ||
| 1351 | $this->lexer->peek() && $this->isMathOperator($this->peekBeyondClosingParenthesis()) => $this->ScalarExpression(), | ||
| 1352 | $this->lexer->lookahead->type === TokenType::T_CASE => $this->CaseExpression(), | ||
| 1353 | $this->isFunction() => $this->FunctionDeclaration(), | ||
| 1354 | default => $this->ResultVariable(), | ||
| 1355 | }; | ||
| 1356 | |||
| 1357 | $type = 'ASC'; | ||
| 1358 | $item = new AST\OrderByItem($expr); | ||
| 1359 | |||
| 1360 | switch (true) { | ||
| 1361 | case $this->lexer->isNextToken(TokenType::T_DESC): | ||
| 1362 | $this->match(TokenType::T_DESC); | ||
| 1363 | $type = 'DESC'; | ||
| 1364 | break; | ||
| 1365 | |||
| 1366 | case $this->lexer->isNextToken(TokenType::T_ASC): | ||
| 1367 | $this->match(TokenType::T_ASC); | ||
| 1368 | break; | ||
| 1369 | |||
| 1370 | default: | ||
| 1371 | // Do nothing | ||
| 1372 | } | ||
| 1373 | |||
| 1374 | $item->type = $type; | ||
| 1375 | |||
| 1376 | return $item; | ||
| 1377 | } | ||
| 1378 | |||
| 1379 | /** | ||
| 1380 | * NewValue ::= SimpleArithmeticExpression | StringPrimary | DatetimePrimary | BooleanPrimary | | ||
| 1381 | * EnumPrimary | SimpleEntityExpression | "NULL" | ||
| 1382 | * | ||
| 1383 | * NOTE: Since it is not possible to correctly recognize individual types, here is the full | ||
| 1384 | * grammar that needs to be supported: | ||
| 1385 | * | ||
| 1386 | * NewValue ::= SimpleArithmeticExpression | "NULL" | ||
| 1387 | * | ||
| 1388 | * SimpleArithmeticExpression covers all *Primary grammar rules and also SimpleEntityExpression | ||
| 1389 | */ | ||
| 1390 | public function NewValue(): AST\ArithmeticExpression|AST\InputParameter|null | ||
| 1391 | { | ||
| 1392 | if ($this->lexer->isNextToken(TokenType::T_NULL)) { | ||
| 1393 | $this->match(TokenType::T_NULL); | ||
| 1394 | |||
| 1395 | return null; | ||
| 1396 | } | ||
| 1397 | |||
| 1398 | if ($this->lexer->isNextToken(TokenType::T_INPUT_PARAMETER)) { | ||
| 1399 | $this->match(TokenType::T_INPUT_PARAMETER); | ||
| 1400 | assert($this->lexer->token !== null); | ||
| 1401 | |||
| 1402 | return new AST\InputParameter($this->lexer->token->value); | ||
| 1403 | } | ||
| 1404 | |||
| 1405 | return $this->ArithmeticExpression(); | ||
| 1406 | } | ||
| 1407 | |||
| 1408 | /** | ||
| 1409 | * IdentificationVariableDeclaration ::= RangeVariableDeclaration [IndexBy] {Join}* | ||
| 1410 | */ | ||
| 1411 | public function IdentificationVariableDeclaration(): AST\IdentificationVariableDeclaration | ||
| 1412 | { | ||
| 1413 | $joins = []; | ||
| 1414 | $rangeVariableDeclaration = $this->RangeVariableDeclaration(); | ||
| 1415 | $indexBy = $this->lexer->isNextToken(TokenType::T_INDEX) | ||
| 1416 | ? $this->IndexBy() | ||
| 1417 | : null; | ||
| 1418 | |||
| 1419 | $rangeVariableDeclaration->isRoot = true; | ||
| 1420 | |||
| 1421 | while ( | ||
| 1422 | $this->lexer->isNextToken(TokenType::T_LEFT) || | ||
| 1423 | $this->lexer->isNextToken(TokenType::T_INNER) || | ||
| 1424 | $this->lexer->isNextToken(TokenType::T_JOIN) | ||
| 1425 | ) { | ||
| 1426 | $joins[] = $this->Join(); | ||
| 1427 | } | ||
| 1428 | |||
| 1429 | return new AST\IdentificationVariableDeclaration( | ||
| 1430 | $rangeVariableDeclaration, | ||
| 1431 | $indexBy, | ||
| 1432 | $joins, | ||
| 1433 | ); | ||
| 1434 | } | ||
| 1435 | |||
| 1436 | /** | ||
| 1437 | * SubselectIdentificationVariableDeclaration ::= IdentificationVariableDeclaration | ||
| 1438 | * | ||
| 1439 | * {Internal note: WARNING: Solution is harder than a bare implementation. | ||
| 1440 | * Desired EBNF support: | ||
| 1441 | * | ||
| 1442 | * SubselectIdentificationVariableDeclaration ::= IdentificationVariableDeclaration | (AssociationPathExpression ["AS"] AliasIdentificationVariable) | ||
| 1443 | * | ||
| 1444 | * It demands that entire SQL generation to become programmatical. This is | ||
| 1445 | * needed because association based subselect requires "WHERE" conditional | ||
| 1446 | * expressions to be injected, but there is no scope to do that. Only scope | ||
| 1447 | * accessible is "FROM", prohibiting an easy implementation without larger | ||
| 1448 | * changes.} | ||
| 1449 | */ | ||
| 1450 | public function SubselectIdentificationVariableDeclaration(): AST\IdentificationVariableDeclaration | ||
| 1451 | { | ||
| 1452 | /* | ||
| 1453 | NOT YET IMPLEMENTED! | ||
| 1454 | |||
| 1455 | $glimpse = $this->lexer->glimpse(); | ||
| 1456 | |||
| 1457 | if ($glimpse->type == TokenType::T_DOT) { | ||
| 1458 | $associationPathExpression = $this->AssociationPathExpression(); | ||
| 1459 | |||
| 1460 | if ($this->lexer->isNextToken(TokenType::T_AS)) { | ||
| 1461 | $this->match(TokenType::T_AS); | ||
| 1462 | } | ||
| 1463 | |||
| 1464 | $aliasIdentificationVariable = $this->AliasIdentificationVariable(); | ||
| 1465 | $identificationVariable = $associationPathExpression->identificationVariable; | ||
| 1466 | $field = $associationPathExpression->associationField; | ||
| 1467 | |||
| 1468 | $class = $this->queryComponents[$identificationVariable]['metadata']; | ||
| 1469 | $targetClass = $this->em->getClassMetadata($class->associationMappings[$field]['targetEntity']); | ||
| 1470 | |||
| 1471 | // Building queryComponent | ||
| 1472 | $joinQueryComponent = array( | ||
| 1473 | 'metadata' => $targetClass, | ||
| 1474 | 'parent' => $identificationVariable, | ||
| 1475 | 'relation' => $class->getAssociationMapping($field), | ||
| 1476 | 'map' => null, | ||
| 1477 | 'nestingLevel' => $this->nestingLevel, | ||
| 1478 | 'token' => $this->lexer->lookahead | ||
| 1479 | ); | ||
| 1480 | |||
| 1481 | $this->queryComponents[$aliasIdentificationVariable] = $joinQueryComponent; | ||
| 1482 | |||
| 1483 | return new AST\SubselectIdentificationVariableDeclaration( | ||
| 1484 | $associationPathExpression, $aliasIdentificationVariable | ||
| 1485 | ); | ||
| 1486 | } | ||
| 1487 | */ | ||
| 1488 | |||
| 1489 | return $this->IdentificationVariableDeclaration(); | ||
| 1490 | } | ||
| 1491 | |||
| 1492 | /** | ||
| 1493 | * Join ::= ["LEFT" ["OUTER"] | "INNER"] "JOIN" | ||
| 1494 | * (JoinAssociationDeclaration | RangeVariableDeclaration) | ||
| 1495 | * ["WITH" ConditionalExpression] | ||
| 1496 | */ | ||
| 1497 | public function Join(): AST\Join | ||
| 1498 | { | ||
| 1499 | // Check Join type | ||
| 1500 | $joinType = AST\Join::JOIN_TYPE_INNER; | ||
| 1501 | |||
| 1502 | switch (true) { | ||
| 1503 | case $this->lexer->isNextToken(TokenType::T_LEFT): | ||
| 1504 | $this->match(TokenType::T_LEFT); | ||
| 1505 | |||
| 1506 | $joinType = AST\Join::JOIN_TYPE_LEFT; | ||
| 1507 | |||
| 1508 | // Possible LEFT OUTER join | ||
| 1509 | if ($this->lexer->isNextToken(TokenType::T_OUTER)) { | ||
| 1510 | $this->match(TokenType::T_OUTER); | ||
| 1511 | |||
| 1512 | $joinType = AST\Join::JOIN_TYPE_LEFTOUTER; | ||
| 1513 | } | ||
| 1514 | |||
| 1515 | break; | ||
| 1516 | |||
| 1517 | case $this->lexer->isNextToken(TokenType::T_INNER): | ||
| 1518 | $this->match(TokenType::T_INNER); | ||
| 1519 | break; | ||
| 1520 | |||
| 1521 | default: | ||
| 1522 | // Do nothing | ||
| 1523 | } | ||
| 1524 | |||
| 1525 | $this->match(TokenType::T_JOIN); | ||
| 1526 | |||
| 1527 | $next = $this->lexer->glimpse(); | ||
| 1528 | assert($next !== null); | ||
| 1529 | $joinDeclaration = $next->type === TokenType::T_DOT ? $this->JoinAssociationDeclaration() : $this->RangeVariableDeclaration(); | ||
| 1530 | $adhocConditions = $this->lexer->isNextToken(TokenType::T_WITH); | ||
| 1531 | $join = new AST\Join($joinType, $joinDeclaration); | ||
| 1532 | |||
| 1533 | // Describe non-root join declaration | ||
| 1534 | if ($joinDeclaration instanceof AST\RangeVariableDeclaration) { | ||
| 1535 | $joinDeclaration->isRoot = false; | ||
| 1536 | } | ||
| 1537 | |||
| 1538 | // Check for ad-hoc Join conditions | ||
| 1539 | if ($adhocConditions) { | ||
| 1540 | $this->match(TokenType::T_WITH); | ||
| 1541 | |||
| 1542 | $join->conditionalExpression = $this->ConditionalExpression(); | ||
| 1543 | } | ||
| 1544 | |||
| 1545 | return $join; | ||
| 1546 | } | ||
| 1547 | |||
| 1548 | /** | ||
| 1549 | * RangeVariableDeclaration ::= AbstractSchemaName ["AS"] AliasIdentificationVariable | ||
| 1550 | * | ||
| 1551 | * @throws QueryException | ||
| 1552 | */ | ||
| 1553 | public function RangeVariableDeclaration(): AST\RangeVariableDeclaration | ||
| 1554 | { | ||
| 1555 | if ($this->lexer->isNextToken(TokenType::T_OPEN_PARENTHESIS) && $this->lexer->glimpse()->type === TokenType::T_SELECT) { | ||
| 1556 | $this->semanticalError('Subquery is not supported here', $this->lexer->token); | ||
| 1557 | } | ||
| 1558 | |||
| 1559 | $abstractSchemaName = $this->AbstractSchemaName(); | ||
| 1560 | |||
| 1561 | $this->validateAbstractSchemaName($abstractSchemaName); | ||
| 1562 | |||
| 1563 | if ($this->lexer->isNextToken(TokenType::T_AS)) { | ||
| 1564 | $this->match(TokenType::T_AS); | ||
| 1565 | } | ||
| 1566 | |||
| 1567 | assert($this->lexer->lookahead !== null); | ||
| 1568 | $token = $this->lexer->lookahead; | ||
| 1569 | $aliasIdentificationVariable = $this->AliasIdentificationVariable(); | ||
| 1570 | $classMetadata = $this->em->getClassMetadata($abstractSchemaName); | ||
| 1571 | |||
| 1572 | // Building queryComponent | ||
| 1573 | $queryComponent = [ | ||
| 1574 | 'metadata' => $classMetadata, | ||
| 1575 | 'parent' => null, | ||
| 1576 | 'relation' => null, | ||
| 1577 | 'map' => null, | ||
| 1578 | 'nestingLevel' => $this->nestingLevel, | ||
| 1579 | 'token' => $token, | ||
| 1580 | ]; | ||
| 1581 | |||
| 1582 | $this->queryComponents[$aliasIdentificationVariable] = $queryComponent; | ||
| 1583 | |||
| 1584 | return new AST\RangeVariableDeclaration($abstractSchemaName, $aliasIdentificationVariable); | ||
| 1585 | } | ||
| 1586 | |||
| 1587 | /** | ||
| 1588 | * JoinAssociationDeclaration ::= JoinAssociationPathExpression ["AS"] AliasIdentificationVariable [IndexBy] | ||
| 1589 | */ | ||
| 1590 | public function JoinAssociationDeclaration(): AST\JoinAssociationDeclaration | ||
| 1591 | { | ||
| 1592 | $joinAssociationPathExpression = $this->JoinAssociationPathExpression(); | ||
| 1593 | |||
| 1594 | if ($this->lexer->isNextToken(TokenType::T_AS)) { | ||
| 1595 | $this->match(TokenType::T_AS); | ||
| 1596 | } | ||
| 1597 | |||
| 1598 | assert($this->lexer->lookahead !== null); | ||
| 1599 | |||
| 1600 | $aliasIdentificationVariable = $this->AliasIdentificationVariable(); | ||
| 1601 | $indexBy = $this->lexer->isNextToken(TokenType::T_INDEX) ? $this->IndexBy() : null; | ||
| 1602 | |||
| 1603 | $identificationVariable = $joinAssociationPathExpression->identificationVariable; | ||
| 1604 | $field = $joinAssociationPathExpression->associationField; | ||
| 1605 | |||
| 1606 | $class = $this->getMetadataForDqlAlias($identificationVariable); | ||
| 1607 | $targetClass = $this->em->getClassMetadata($class->associationMappings[$field]->targetEntity); | ||
| 1608 | |||
| 1609 | // Building queryComponent | ||
| 1610 | $joinQueryComponent = [ | ||
| 1611 | 'metadata' => $targetClass, | ||
| 1612 | 'parent' => $joinAssociationPathExpression->identificationVariable, | ||
| 1613 | 'relation' => $class->getAssociationMapping($field), | ||
| 1614 | 'map' => null, | ||
| 1615 | 'nestingLevel' => $this->nestingLevel, | ||
| 1616 | 'token' => $this->lexer->lookahead, | ||
| 1617 | ]; | ||
| 1618 | |||
| 1619 | $this->queryComponents[$aliasIdentificationVariable] = $joinQueryComponent; | ||
| 1620 | |||
| 1621 | return new AST\JoinAssociationDeclaration($joinAssociationPathExpression, $aliasIdentificationVariable, $indexBy); | ||
| 1622 | } | ||
| 1623 | |||
| 1624 | /** | ||
| 1625 | * NewObjectExpression ::= "NEW" AbstractSchemaName "(" NewObjectArg {"," NewObjectArg}* ")" | ||
| 1626 | */ | ||
| 1627 | public function NewObjectExpression(): AST\NewObjectExpression | ||
| 1628 | { | ||
| 1629 | $args = []; | ||
| 1630 | $this->match(TokenType::T_NEW); | ||
| 1631 | |||
| 1632 | $className = $this->AbstractSchemaName(); // note that this is not yet validated | ||
| 1633 | $token = $this->lexer->token; | ||
| 1634 | |||
| 1635 | $this->match(TokenType::T_OPEN_PARENTHESIS); | ||
| 1636 | |||
| 1637 | $args[] = $this->NewObjectArg(); | ||
| 1638 | |||
| 1639 | while ($this->lexer->isNextToken(TokenType::T_COMMA)) { | ||
| 1640 | $this->match(TokenType::T_COMMA); | ||
| 1641 | |||
| 1642 | $args[] = $this->NewObjectArg(); | ||
| 1643 | } | ||
| 1644 | |||
| 1645 | $this->match(TokenType::T_CLOSE_PARENTHESIS); | ||
| 1646 | |||
| 1647 | $expression = new AST\NewObjectExpression($className, $args); | ||
| 1648 | |||
| 1649 | // Defer NewObjectExpression validation | ||
| 1650 | $this->deferredNewObjectExpressions[] = [ | ||
| 1651 | 'token' => $token, | ||
| 1652 | 'expression' => $expression, | ||
| 1653 | 'nestingLevel' => $this->nestingLevel, | ||
| 1654 | ]; | ||
| 1655 | |||
| 1656 | return $expression; | ||
| 1657 | } | ||
| 1658 | |||
| 1659 | /** | ||
| 1660 | * NewObjectArg ::= ScalarExpression | "(" Subselect ")" | ||
| 1661 | */ | ||
| 1662 | public function NewObjectArg(): mixed | ||
| 1663 | { | ||
| 1664 | assert($this->lexer->lookahead !== null); | ||
| 1665 | $token = $this->lexer->lookahead; | ||
| 1666 | $peek = $this->lexer->glimpse(); | ||
| 1667 | |||
| 1668 | assert($peek !== null); | ||
| 1669 | if ($token->type === TokenType::T_OPEN_PARENTHESIS && $peek->type === TokenType::T_SELECT) { | ||
| 1670 | $this->match(TokenType::T_OPEN_PARENTHESIS); | ||
| 1671 | $expression = $this->Subselect(); | ||
| 1672 | $this->match(TokenType::T_CLOSE_PARENTHESIS); | ||
| 1673 | |||
| 1674 | return $expression; | ||
| 1675 | } | ||
| 1676 | |||
| 1677 | return $this->ScalarExpression(); | ||
| 1678 | } | ||
| 1679 | |||
| 1680 | /** | ||
| 1681 | * IndexBy ::= "INDEX" "BY" SingleValuedPathExpression | ||
| 1682 | */ | ||
| 1683 | public function IndexBy(): AST\IndexBy | ||
| 1684 | { | ||
| 1685 | $this->match(TokenType::T_INDEX); | ||
| 1686 | $this->match(TokenType::T_BY); | ||
| 1687 | $pathExpr = $this->SingleValuedPathExpression(); | ||
| 1688 | |||
| 1689 | // Add the INDEX BY info to the query component | ||
| 1690 | $this->queryComponents[$pathExpr->identificationVariable]['map'] = $pathExpr->field; | ||
| 1691 | |||
| 1692 | return new AST\IndexBy($pathExpr); | ||
| 1693 | } | ||
| 1694 | |||
| 1695 | /** | ||
| 1696 | * ScalarExpression ::= SimpleArithmeticExpression | StringPrimary | DateTimePrimary | | ||
| 1697 | * StateFieldPathExpression | BooleanPrimary | CaseExpression | | ||
| 1698 | * InstanceOfExpression | ||
| 1699 | * | ||
| 1700 | * @return mixed One of the possible expressions or subexpressions. | ||
| 1701 | */ | ||
| 1702 | public function ScalarExpression(): mixed | ||
| 1703 | { | ||
| 1704 | assert($this->lexer->token !== null); | ||
| 1705 | assert($this->lexer->lookahead !== null); | ||
| 1706 | $lookahead = $this->lexer->lookahead->type; | ||
| 1707 | $peek = $this->lexer->glimpse(); | ||
| 1708 | |||
| 1709 | switch (true) { | ||
| 1710 | case $lookahead === TokenType::T_INTEGER: | ||
| 1711 | case $lookahead === TokenType::T_FLOAT: | ||
| 1712 | // SimpleArithmeticExpression : (- u.value ) or ( + u.value ) or ( - 1 ) or ( + 1 ) | ||
| 1713 | case $lookahead === TokenType::T_MINUS: | ||
| 1714 | case $lookahead === TokenType::T_PLUS: | ||
| 1715 | return $this->SimpleArithmeticExpression(); | ||
| 1716 | |||
| 1717 | case $lookahead === TokenType::T_STRING: | ||
| 1718 | return $this->StringPrimary(); | ||
| 1719 | |||
| 1720 | case $lookahead === TokenType::T_TRUE: | ||
| 1721 | case $lookahead === TokenType::T_FALSE: | ||
| 1722 | $this->match($lookahead); | ||
| 1723 | |||
| 1724 | return new AST\Literal(AST\Literal::BOOLEAN, $this->lexer->token->value); | ||
| 1725 | |||
| 1726 | case $lookahead === TokenType::T_INPUT_PARAMETER: | ||
| 1727 | return match (true) { | ||
| 1728 | $this->isMathOperator($peek) => $this->SimpleArithmeticExpression(), | ||
| 1729 | default => $this->InputParameter(), | ||
| 1730 | }; | ||
| 1731 | |||
| 1732 | case $lookahead === TokenType::T_CASE: | ||
| 1733 | case $lookahead === TokenType::T_COALESCE: | ||
| 1734 | case $lookahead === TokenType::T_NULLIF: | ||
| 1735 | // Since NULLIF and COALESCE can be identified as a function, | ||
| 1736 | // we need to check these before checking for FunctionDeclaration | ||
| 1737 | return $this->CaseExpression(); | ||
| 1738 | |||
| 1739 | case $lookahead === TokenType::T_OPEN_PARENTHESIS: | ||
| 1740 | return $this->SimpleArithmeticExpression(); | ||
| 1741 | |||
| 1742 | // this check must be done before checking for a filed path expression | ||
| 1743 | case $this->isFunction(): | ||
| 1744 | $this->lexer->peek(); | ||
| 1745 | |||
| 1746 | return match (true) { | ||
| 1747 | $this->isMathOperator($this->peekBeyondClosingParenthesis()) => $this->SimpleArithmeticExpression(), | ||
| 1748 | default => $this->FunctionDeclaration(), | ||
| 1749 | }; | ||
| 1750 | |||
| 1751 | // it is no function, so it must be a field path | ||
| 1752 | case $lookahead === TokenType::T_IDENTIFIER: | ||
| 1753 | $this->lexer->peek(); // lookahead => '.' | ||
| 1754 | $this->lexer->peek(); // lookahead => token after '.' | ||
| 1755 | $peek = $this->lexer->peek(); // lookahead => token after the token after the '.' | ||
| 1756 | $this->lexer->resetPeek(); | ||
| 1757 | |||
| 1758 | if ($this->isMathOperator($peek)) { | ||
| 1759 | return $this->SimpleArithmeticExpression(); | ||
| 1760 | } | ||
| 1761 | |||
| 1762 | return $this->StateFieldPathExpression(); | ||
| 1763 | |||
| 1764 | default: | ||
| 1765 | $this->syntaxError(); | ||
| 1766 | } | ||
| 1767 | } | ||
| 1768 | |||
| 1769 | /** | ||
| 1770 | * CaseExpression ::= GeneralCaseExpression | SimpleCaseExpression | CoalesceExpression | NullifExpression | ||
| 1771 | * GeneralCaseExpression ::= "CASE" WhenClause {WhenClause}* "ELSE" ScalarExpression "END" | ||
| 1772 | * WhenClause ::= "WHEN" ConditionalExpression "THEN" ScalarExpression | ||
| 1773 | * SimpleCaseExpression ::= "CASE" CaseOperand SimpleWhenClause {SimpleWhenClause}* "ELSE" ScalarExpression "END" | ||
| 1774 | * CaseOperand ::= StateFieldPathExpression | TypeDiscriminator | ||
| 1775 | * SimpleWhenClause ::= "WHEN" ScalarExpression "THEN" ScalarExpression | ||
| 1776 | * CoalesceExpression ::= "COALESCE" "(" ScalarExpression {"," ScalarExpression}* ")" | ||
| 1777 | * NullifExpression ::= "NULLIF" "(" ScalarExpression "," ScalarExpression ")" | ||
| 1778 | * | ||
| 1779 | * @return mixed One of the possible expressions or subexpressions. | ||
| 1780 | */ | ||
| 1781 | public function CaseExpression(): mixed | ||
| 1782 | { | ||
| 1783 | assert($this->lexer->lookahead !== null); | ||
| 1784 | $lookahead = $this->lexer->lookahead->type; | ||
| 1785 | |||
| 1786 | switch ($lookahead) { | ||
| 1787 | case TokenType::T_NULLIF: | ||
| 1788 | return $this->NullIfExpression(); | ||
| 1789 | |||
| 1790 | case TokenType::T_COALESCE: | ||
| 1791 | return $this->CoalesceExpression(); | ||
| 1792 | |||
| 1793 | case TokenType::T_CASE: | ||
| 1794 | $this->lexer->resetPeek(); | ||
| 1795 | $peek = $this->lexer->peek(); | ||
| 1796 | |||
| 1797 | assert($peek !== null); | ||
| 1798 | if ($peek->type === TokenType::T_WHEN) { | ||
| 1799 | return $this->GeneralCaseExpression(); | ||
| 1800 | } | ||
| 1801 | |||
| 1802 | return $this->SimpleCaseExpression(); | ||
| 1803 | |||
| 1804 | default: | ||
| 1805 | // Do nothing | ||
| 1806 | break; | ||
| 1807 | } | ||
| 1808 | |||
| 1809 | $this->syntaxError(); | ||
| 1810 | } | ||
| 1811 | |||
| 1812 | /** | ||
| 1813 | * CoalesceExpression ::= "COALESCE" "(" ScalarExpression {"," ScalarExpression}* ")" | ||
| 1814 | */ | ||
| 1815 | public function CoalesceExpression(): AST\CoalesceExpression | ||
| 1816 | { | ||
| 1817 | $this->match(TokenType::T_COALESCE); | ||
| 1818 | $this->match(TokenType::T_OPEN_PARENTHESIS); | ||
| 1819 | |||
| 1820 | // Process ScalarExpressions (1..N) | ||
| 1821 | $scalarExpressions = []; | ||
| 1822 | $scalarExpressions[] = $this->ScalarExpression(); | ||
| 1823 | |||
| 1824 | while ($this->lexer->isNextToken(TokenType::T_COMMA)) { | ||
| 1825 | $this->match(TokenType::T_COMMA); | ||
| 1826 | |||
| 1827 | $scalarExpressions[] = $this->ScalarExpression(); | ||
| 1828 | } | ||
| 1829 | |||
| 1830 | $this->match(TokenType::T_CLOSE_PARENTHESIS); | ||
| 1831 | |||
| 1832 | return new AST\CoalesceExpression($scalarExpressions); | ||
| 1833 | } | ||
| 1834 | |||
| 1835 | /** | ||
| 1836 | * NullIfExpression ::= "NULLIF" "(" ScalarExpression "," ScalarExpression ")" | ||
| 1837 | */ | ||
| 1838 | public function NullIfExpression(): AST\NullIfExpression | ||
| 1839 | { | ||
| 1840 | $this->match(TokenType::T_NULLIF); | ||
| 1841 | $this->match(TokenType::T_OPEN_PARENTHESIS); | ||
| 1842 | |||
| 1843 | $firstExpression = $this->ScalarExpression(); | ||
| 1844 | $this->match(TokenType::T_COMMA); | ||
| 1845 | $secondExpression = $this->ScalarExpression(); | ||
| 1846 | |||
| 1847 | $this->match(TokenType::T_CLOSE_PARENTHESIS); | ||
| 1848 | |||
| 1849 | return new AST\NullIfExpression($firstExpression, $secondExpression); | ||
| 1850 | } | ||
| 1851 | |||
| 1852 | /** | ||
| 1853 | * GeneralCaseExpression ::= "CASE" WhenClause {WhenClause}* "ELSE" ScalarExpression "END" | ||
| 1854 | */ | ||
| 1855 | public function GeneralCaseExpression(): AST\GeneralCaseExpression | ||
| 1856 | { | ||
| 1857 | $this->match(TokenType::T_CASE); | ||
| 1858 | |||
| 1859 | // Process WhenClause (1..N) | ||
| 1860 | $whenClauses = []; | ||
| 1861 | |||
| 1862 | do { | ||
| 1863 | $whenClauses[] = $this->WhenClause(); | ||
| 1864 | } while ($this->lexer->isNextToken(TokenType::T_WHEN)); | ||
| 1865 | |||
| 1866 | $this->match(TokenType::T_ELSE); | ||
| 1867 | $scalarExpression = $this->ScalarExpression(); | ||
| 1868 | $this->match(TokenType::T_END); | ||
| 1869 | |||
| 1870 | return new AST\GeneralCaseExpression($whenClauses, $scalarExpression); | ||
| 1871 | } | ||
| 1872 | |||
| 1873 | /** | ||
| 1874 | * SimpleCaseExpression ::= "CASE" CaseOperand SimpleWhenClause {SimpleWhenClause}* "ELSE" ScalarExpression "END" | ||
| 1875 | * CaseOperand ::= StateFieldPathExpression | TypeDiscriminator | ||
| 1876 | */ | ||
| 1877 | public function SimpleCaseExpression(): AST\SimpleCaseExpression | ||
| 1878 | { | ||
| 1879 | $this->match(TokenType::T_CASE); | ||
| 1880 | $caseOperand = $this->StateFieldPathExpression(); | ||
| 1881 | |||
| 1882 | // Process SimpleWhenClause (1..N) | ||
| 1883 | $simpleWhenClauses = []; | ||
| 1884 | |||
| 1885 | do { | ||
| 1886 | $simpleWhenClauses[] = $this->SimpleWhenClause(); | ||
| 1887 | } while ($this->lexer->isNextToken(TokenType::T_WHEN)); | ||
| 1888 | |||
| 1889 | $this->match(TokenType::T_ELSE); | ||
| 1890 | $scalarExpression = $this->ScalarExpression(); | ||
| 1891 | $this->match(TokenType::T_END); | ||
| 1892 | |||
| 1893 | return new AST\SimpleCaseExpression($caseOperand, $simpleWhenClauses, $scalarExpression); | ||
| 1894 | } | ||
| 1895 | |||
| 1896 | /** | ||
| 1897 | * WhenClause ::= "WHEN" ConditionalExpression "THEN" ScalarExpression | ||
| 1898 | */ | ||
| 1899 | public function WhenClause(): AST\WhenClause | ||
| 1900 | { | ||
| 1901 | $this->match(TokenType::T_WHEN); | ||
| 1902 | $conditionalExpression = $this->ConditionalExpression(); | ||
| 1903 | $this->match(TokenType::T_THEN); | ||
| 1904 | |||
| 1905 | return new AST\WhenClause($conditionalExpression, $this->ScalarExpression()); | ||
| 1906 | } | ||
| 1907 | |||
| 1908 | /** | ||
| 1909 | * SimpleWhenClause ::= "WHEN" ScalarExpression "THEN" ScalarExpression | ||
| 1910 | */ | ||
| 1911 | public function SimpleWhenClause(): AST\SimpleWhenClause | ||
| 1912 | { | ||
| 1913 | $this->match(TokenType::T_WHEN); | ||
| 1914 | $conditionalExpression = $this->ScalarExpression(); | ||
| 1915 | $this->match(TokenType::T_THEN); | ||
| 1916 | |||
| 1917 | return new AST\SimpleWhenClause($conditionalExpression, $this->ScalarExpression()); | ||
| 1918 | } | ||
| 1919 | |||
| 1920 | /** | ||
| 1921 | * SelectExpression ::= ( | ||
| 1922 | * IdentificationVariable | ScalarExpression | AggregateExpression | FunctionDeclaration | | ||
| 1923 | * "(" Subselect ")" | CaseExpression | NewObjectExpression | ||
| 1924 | * ) [["AS"] ["HIDDEN"] AliasResultVariable] | ||
| 1925 | */ | ||
| 1926 | public function SelectExpression(): AST\SelectExpression | ||
| 1927 | { | ||
| 1928 | assert($this->lexer->lookahead !== null); | ||
| 1929 | $expression = null; | ||
| 1930 | $identVariable = null; | ||
| 1931 | $peek = $this->lexer->glimpse(); | ||
| 1932 | $lookaheadType = $this->lexer->lookahead->type; | ||
| 1933 | assert($peek !== null); | ||
| 1934 | |||
| 1935 | switch (true) { | ||
| 1936 | // ScalarExpression (u.name) | ||
| 1937 | case $lookaheadType === TokenType::T_IDENTIFIER && $peek->type === TokenType::T_DOT: | ||
| 1938 | $expression = $this->ScalarExpression(); | ||
| 1939 | break; | ||
| 1940 | |||
| 1941 | // IdentificationVariable (u) | ||
| 1942 | case $lookaheadType === TokenType::T_IDENTIFIER && $peek->type !== TokenType::T_OPEN_PARENTHESIS: | ||
| 1943 | $expression = $identVariable = $this->IdentificationVariable(); | ||
| 1944 | break; | ||
| 1945 | |||
| 1946 | // CaseExpression (CASE ... or NULLIF(...) or COALESCE(...)) | ||
| 1947 | case $lookaheadType === TokenType::T_CASE: | ||
| 1948 | case $lookaheadType === TokenType::T_COALESCE: | ||
| 1949 | case $lookaheadType === TokenType::T_NULLIF: | ||
| 1950 | $expression = $this->CaseExpression(); | ||
| 1951 | break; | ||
| 1952 | |||
| 1953 | // DQL Function (SUM(u.value) or SUM(u.value) + 1) | ||
| 1954 | case $this->isFunction(): | ||
| 1955 | $this->lexer->peek(); // "(" | ||
| 1956 | |||
| 1957 | $expression = match (true) { | ||
| 1958 | $this->isMathOperator($this->peekBeyondClosingParenthesis()) => $this->ScalarExpression(), | ||
| 1959 | default => $this->FunctionDeclaration(), | ||
| 1960 | }; | ||
| 1961 | |||
| 1962 | break; | ||
| 1963 | |||
| 1964 | // Subselect | ||
| 1965 | case $lookaheadType === TokenType::T_OPEN_PARENTHESIS && $peek->type === TokenType::T_SELECT: | ||
| 1966 | $this->match(TokenType::T_OPEN_PARENTHESIS); | ||
| 1967 | $expression = $this->Subselect(); | ||
| 1968 | $this->match(TokenType::T_CLOSE_PARENTHESIS); | ||
| 1969 | break; | ||
| 1970 | |||
| 1971 | // Shortcut: ScalarExpression => SimpleArithmeticExpression | ||
| 1972 | case $lookaheadType === TokenType::T_OPEN_PARENTHESIS: | ||
| 1973 | case $lookaheadType === TokenType::T_INTEGER: | ||
| 1974 | case $lookaheadType === TokenType::T_STRING: | ||
| 1975 | case $lookaheadType === TokenType::T_FLOAT: | ||
| 1976 | // SimpleArithmeticExpression : (- u.value ) or ( + u.value ) | ||
| 1977 | case $lookaheadType === TokenType::T_MINUS: | ||
| 1978 | case $lookaheadType === TokenType::T_PLUS: | ||
| 1979 | $expression = $this->SimpleArithmeticExpression(); | ||
| 1980 | break; | ||
| 1981 | |||
| 1982 | // NewObjectExpression (New ClassName(id, name)) | ||
| 1983 | case $lookaheadType === TokenType::T_NEW: | ||
| 1984 | $expression = $this->NewObjectExpression(); | ||
| 1985 | break; | ||
| 1986 | |||
| 1987 | default: | ||
| 1988 | $this->syntaxError( | ||
| 1989 | 'IdentificationVariable | ScalarExpression | AggregateExpression | FunctionDeclaration | "(" Subselect ")" | CaseExpression', | ||
| 1990 | $this->lexer->lookahead, | ||
| 1991 | ); | ||
| 1992 | } | ||
| 1993 | |||
| 1994 | // [["AS"] ["HIDDEN"] AliasResultVariable] | ||
| 1995 | $mustHaveAliasResultVariable = false; | ||
| 1996 | |||
| 1997 | if ($this->lexer->isNextToken(TokenType::T_AS)) { | ||
| 1998 | $this->match(TokenType::T_AS); | ||
| 1999 | |||
| 2000 | $mustHaveAliasResultVariable = true; | ||
| 2001 | } | ||
| 2002 | |||
| 2003 | $hiddenAliasResultVariable = false; | ||
| 2004 | |||
| 2005 | if ($this->lexer->isNextToken(TokenType::T_HIDDEN)) { | ||
| 2006 | $this->match(TokenType::T_HIDDEN); | ||
| 2007 | |||
| 2008 | $hiddenAliasResultVariable = true; | ||
| 2009 | } | ||
| 2010 | |||
| 2011 | $aliasResultVariable = null; | ||
| 2012 | |||
| 2013 | if ($mustHaveAliasResultVariable || $this->lexer->isNextToken(TokenType::T_IDENTIFIER)) { | ||
| 2014 | assert($expression instanceof AST\Node || is_string($expression)); | ||
| 2015 | $token = $this->lexer->lookahead; | ||
| 2016 | $aliasResultVariable = $this->AliasResultVariable(); | ||
| 2017 | |||
| 2018 | // Include AliasResultVariable in query components. | ||
| 2019 | $this->queryComponents[$aliasResultVariable] = [ | ||
| 2020 | 'resultVariable' => $expression, | ||
| 2021 | 'nestingLevel' => $this->nestingLevel, | ||
| 2022 | 'token' => $token, | ||
| 2023 | ]; | ||
| 2024 | } | ||
| 2025 | |||
| 2026 | // AST | ||
| 2027 | |||
| 2028 | $expr = new AST\SelectExpression($expression, $aliasResultVariable, $hiddenAliasResultVariable); | ||
| 2029 | |||
| 2030 | if ($identVariable) { | ||
| 2031 | $this->identVariableExpressions[$identVariable] = $expr; | ||
| 2032 | } | ||
| 2033 | |||
| 2034 | return $expr; | ||
| 2035 | } | ||
| 2036 | |||
| 2037 | /** | ||
| 2038 | * SimpleSelectExpression ::= ( | ||
| 2039 | * StateFieldPathExpression | IdentificationVariable | FunctionDeclaration | | ||
| 2040 | * AggregateExpression | "(" Subselect ")" | ScalarExpression | ||
| 2041 | * ) [["AS"] AliasResultVariable] | ||
| 2042 | */ | ||
| 2043 | public function SimpleSelectExpression(): AST\SimpleSelectExpression | ||
| 2044 | { | ||
| 2045 | assert($this->lexer->lookahead !== null); | ||
| 2046 | $peek = $this->lexer->glimpse(); | ||
| 2047 | assert($peek !== null); | ||
| 2048 | |||
| 2049 | switch ($this->lexer->lookahead->type) { | ||
| 2050 | case TokenType::T_IDENTIFIER: | ||
| 2051 | switch (true) { | ||
| 2052 | case $peek->type === TokenType::T_DOT: | ||
| 2053 | $expression = $this->StateFieldPathExpression(); | ||
| 2054 | |||
| 2055 | return new AST\SimpleSelectExpression($expression); | ||
| 2056 | |||
| 2057 | case $peek->type !== TokenType::T_OPEN_PARENTHESIS: | ||
| 2058 | $expression = $this->IdentificationVariable(); | ||
| 2059 | |||
| 2060 | return new AST\SimpleSelectExpression($expression); | ||
| 2061 | |||
| 2062 | case $this->isFunction(): | ||
| 2063 | // SUM(u.id) + COUNT(u.id) | ||
| 2064 | if ($this->isMathOperator($this->peekBeyondClosingParenthesis())) { | ||
| 2065 | return new AST\SimpleSelectExpression($this->ScalarExpression()); | ||
| 2066 | } | ||
| 2067 | |||
| 2068 | // COUNT(u.id) | ||
| 2069 | if ($this->isAggregateFunction($this->lexer->lookahead->type)) { | ||
| 2070 | return new AST\SimpleSelectExpression($this->AggregateExpression()); | ||
| 2071 | } | ||
| 2072 | |||
| 2073 | // IDENTITY(u) | ||
| 2074 | return new AST\SimpleSelectExpression($this->FunctionDeclaration()); | ||
| 2075 | |||
| 2076 | default: | ||
| 2077 | // Do nothing | ||
| 2078 | } | ||
| 2079 | |||
| 2080 | break; | ||
| 2081 | |||
| 2082 | case TokenType::T_OPEN_PARENTHESIS: | ||
| 2083 | if ($peek->type !== TokenType::T_SELECT) { | ||
| 2084 | // Shortcut: ScalarExpression => SimpleArithmeticExpression | ||
| 2085 | $expression = $this->SimpleArithmeticExpression(); | ||
| 2086 | |||
| 2087 | return new AST\SimpleSelectExpression($expression); | ||
| 2088 | } | ||
| 2089 | |||
| 2090 | // Subselect | ||
| 2091 | $this->match(TokenType::T_OPEN_PARENTHESIS); | ||
| 2092 | $expression = $this->Subselect(); | ||
| 2093 | $this->match(TokenType::T_CLOSE_PARENTHESIS); | ||
| 2094 | |||
| 2095 | return new AST\SimpleSelectExpression($expression); | ||
| 2096 | |||
| 2097 | default: | ||
| 2098 | // Do nothing | ||
| 2099 | } | ||
| 2100 | |||
| 2101 | $this->lexer->peek(); | ||
| 2102 | |||
| 2103 | $expression = $this->ScalarExpression(); | ||
| 2104 | $expr = new AST\SimpleSelectExpression($expression); | ||
| 2105 | |||
| 2106 | if ($this->lexer->isNextToken(TokenType::T_AS)) { | ||
| 2107 | $this->match(TokenType::T_AS); | ||
| 2108 | } | ||
| 2109 | |||
| 2110 | if ($this->lexer->isNextToken(TokenType::T_IDENTIFIER)) { | ||
| 2111 | $token = $this->lexer->lookahead; | ||
| 2112 | $resultVariable = $this->AliasResultVariable(); | ||
| 2113 | $expr->fieldIdentificationVariable = $resultVariable; | ||
| 2114 | |||
| 2115 | // Include AliasResultVariable in query components. | ||
| 2116 | $this->queryComponents[$resultVariable] = [ | ||
| 2117 | 'resultvariable' => $expr, | ||
| 2118 | 'nestingLevel' => $this->nestingLevel, | ||
| 2119 | 'token' => $token, | ||
| 2120 | ]; | ||
| 2121 | } | ||
| 2122 | |||
| 2123 | return $expr; | ||
| 2124 | } | ||
| 2125 | |||
| 2126 | /** | ||
| 2127 | * ConditionalExpression ::= ConditionalTerm {"OR" ConditionalTerm}* | ||
| 2128 | */ | ||
| 2129 | public function ConditionalExpression(): AST\ConditionalExpression|AST\ConditionalFactor|AST\ConditionalPrimary|AST\ConditionalTerm | ||
| 2130 | { | ||
| 2131 | $conditionalTerms = []; | ||
| 2132 | $conditionalTerms[] = $this->ConditionalTerm(); | ||
| 2133 | |||
| 2134 | while ($this->lexer->isNextToken(TokenType::T_OR)) { | ||
| 2135 | $this->match(TokenType::T_OR); | ||
| 2136 | |||
| 2137 | $conditionalTerms[] = $this->ConditionalTerm(); | ||
| 2138 | } | ||
| 2139 | |||
| 2140 | // Phase 1 AST optimization: Prevent AST\ConditionalExpression | ||
| 2141 | // if only one AST\ConditionalTerm is defined | ||
| 2142 | if (count($conditionalTerms) === 1) { | ||
| 2143 | return $conditionalTerms[0]; | ||
| 2144 | } | ||
| 2145 | |||
| 2146 | return new AST\ConditionalExpression($conditionalTerms); | ||
| 2147 | } | ||
| 2148 | |||
| 2149 | /** | ||
| 2150 | * ConditionalTerm ::= ConditionalFactor {"AND" ConditionalFactor}* | ||
| 2151 | */ | ||
| 2152 | public function ConditionalTerm(): AST\ConditionalFactor|AST\ConditionalPrimary|AST\ConditionalTerm | ||
| 2153 | { | ||
| 2154 | $conditionalFactors = []; | ||
| 2155 | $conditionalFactors[] = $this->ConditionalFactor(); | ||
| 2156 | |||
| 2157 | while ($this->lexer->isNextToken(TokenType::T_AND)) { | ||
| 2158 | $this->match(TokenType::T_AND); | ||
| 2159 | |||
| 2160 | $conditionalFactors[] = $this->ConditionalFactor(); | ||
| 2161 | } | ||
| 2162 | |||
| 2163 | // Phase 1 AST optimization: Prevent AST\ConditionalTerm | ||
| 2164 | // if only one AST\ConditionalFactor is defined | ||
| 2165 | if (count($conditionalFactors) === 1) { | ||
| 2166 | return $conditionalFactors[0]; | ||
| 2167 | } | ||
| 2168 | |||
| 2169 | return new AST\ConditionalTerm($conditionalFactors); | ||
| 2170 | } | ||
| 2171 | |||
| 2172 | /** | ||
| 2173 | * ConditionalFactor ::= ["NOT"] ConditionalPrimary | ||
| 2174 | */ | ||
| 2175 | public function ConditionalFactor(): AST\ConditionalFactor|AST\ConditionalPrimary | ||
| 2176 | { | ||
| 2177 | $not = false; | ||
| 2178 | |||
| 2179 | if ($this->lexer->isNextToken(TokenType::T_NOT)) { | ||
| 2180 | $this->match(TokenType::T_NOT); | ||
| 2181 | |||
| 2182 | $not = true; | ||
| 2183 | } | ||
| 2184 | |||
| 2185 | $conditionalPrimary = $this->ConditionalPrimary(); | ||
| 2186 | |||
| 2187 | // Phase 1 AST optimization: Prevent AST\ConditionalFactor | ||
| 2188 | // if only one AST\ConditionalPrimary is defined | ||
| 2189 | if (! $not) { | ||
| 2190 | return $conditionalPrimary; | ||
| 2191 | } | ||
| 2192 | |||
| 2193 | return new AST\ConditionalFactor($conditionalPrimary, $not); | ||
| 2194 | } | ||
| 2195 | |||
| 2196 | /** | ||
| 2197 | * ConditionalPrimary ::= SimpleConditionalExpression | "(" ConditionalExpression ")" | ||
| 2198 | */ | ||
| 2199 | public function ConditionalPrimary(): AST\ConditionalPrimary | ||
| 2200 | { | ||
| 2201 | $condPrimary = new AST\ConditionalPrimary(); | ||
| 2202 | |||
| 2203 | if (! $this->lexer->isNextToken(TokenType::T_OPEN_PARENTHESIS)) { | ||
| 2204 | $condPrimary->simpleConditionalExpression = $this->SimpleConditionalExpression(); | ||
| 2205 | |||
| 2206 | return $condPrimary; | ||
| 2207 | } | ||
| 2208 | |||
| 2209 | // Peek beyond the matching closing parenthesis ')' | ||
| 2210 | $peek = $this->peekBeyondClosingParenthesis(); | ||
| 2211 | |||
| 2212 | if ( | ||
| 2213 | $peek !== null && ( | ||
| 2214 | in_array($peek->value, ['=', '<', '<=', '<>', '>', '>=', '!='], true) || | ||
| 2215 | in_array($peek->type, [TokenType::T_NOT, TokenType::T_BETWEEN, TokenType::T_LIKE, TokenType::T_IN, TokenType::T_IS, TokenType::T_EXISTS], true) || | ||
| 2216 | $this->isMathOperator($peek) | ||
| 2217 | ) | ||
| 2218 | ) { | ||
| 2219 | $condPrimary->simpleConditionalExpression = $this->SimpleConditionalExpression(); | ||
| 2220 | |||
| 2221 | return $condPrimary; | ||
| 2222 | } | ||
| 2223 | |||
| 2224 | $this->match(TokenType::T_OPEN_PARENTHESIS); | ||
| 2225 | $condPrimary->conditionalExpression = $this->ConditionalExpression(); | ||
| 2226 | $this->match(TokenType::T_CLOSE_PARENTHESIS); | ||
| 2227 | |||
| 2228 | return $condPrimary; | ||
| 2229 | } | ||
| 2230 | |||
| 2231 | /** | ||
| 2232 | * SimpleConditionalExpression ::= | ||
| 2233 | * ComparisonExpression | BetweenExpression | LikeExpression | | ||
| 2234 | * InExpression | NullComparisonExpression | ExistsExpression | | ||
| 2235 | * EmptyCollectionComparisonExpression | CollectionMemberExpression | | ||
| 2236 | * InstanceOfExpression | ||
| 2237 | */ | ||
| 2238 | public function SimpleConditionalExpression(): AST\ExistsExpression|AST\BetweenExpression|AST\LikeExpression|AST\InListExpression|AST\InSubselectExpression|AST\InstanceOfExpression|AST\CollectionMemberExpression|AST\NullComparisonExpression|AST\EmptyCollectionComparisonExpression|AST\ComparisonExpression | ||
| 2239 | { | ||
| 2240 | assert($this->lexer->lookahead !== null); | ||
| 2241 | if ($this->lexer->isNextToken(TokenType::T_EXISTS)) { | ||
| 2242 | return $this->ExistsExpression(); | ||
| 2243 | } | ||
| 2244 | |||
| 2245 | $token = $this->lexer->lookahead; | ||
| 2246 | $peek = $this->lexer->glimpse(); | ||
| 2247 | $lookahead = $token; | ||
| 2248 | |||
| 2249 | if ($this->lexer->isNextToken(TokenType::T_NOT)) { | ||
| 2250 | $token = $this->lexer->glimpse(); | ||
| 2251 | } | ||
| 2252 | |||
| 2253 | assert($token !== null); | ||
| 2254 | assert($peek !== null); | ||
| 2255 | if ($token->type === TokenType::T_IDENTIFIER || $token->type === TokenType::T_INPUT_PARAMETER || $this->isFunction()) { | ||
| 2256 | // Peek beyond the matching closing parenthesis. | ||
| 2257 | $beyond = $this->lexer->peek(); | ||
| 2258 | |||
| 2259 | switch ($peek->value) { | ||
| 2260 | case '(': | ||
| 2261 | // Peeks beyond the matched closing parenthesis. | ||
| 2262 | $token = $this->peekBeyondClosingParenthesis(false); | ||
| 2263 | assert($token !== null); | ||
| 2264 | |||
| 2265 | if ($token->type === TokenType::T_NOT) { | ||
| 2266 | $token = $this->lexer->peek(); | ||
| 2267 | assert($token !== null); | ||
| 2268 | } | ||
| 2269 | |||
| 2270 | if ($token->type === TokenType::T_IS) { | ||
| 2271 | $lookahead = $this->lexer->peek(); | ||
| 2272 | } | ||
| 2273 | |||
| 2274 | break; | ||
| 2275 | |||
| 2276 | default: | ||
| 2277 | // Peek beyond the PathExpression or InputParameter. | ||
| 2278 | $token = $beyond; | ||
| 2279 | |||
| 2280 | while ($token->value === '.') { | ||
| 2281 | $this->lexer->peek(); | ||
| 2282 | |||
| 2283 | $token = $this->lexer->peek(); | ||
| 2284 | assert($token !== null); | ||
| 2285 | } | ||
| 2286 | |||
| 2287 | // Also peek beyond a NOT if there is one. | ||
| 2288 | assert($token !== null); | ||
| 2289 | if ($token->type === TokenType::T_NOT) { | ||
| 2290 | $token = $this->lexer->peek(); | ||
| 2291 | assert($token !== null); | ||
| 2292 | } | ||
| 2293 | |||
| 2294 | // We need to go even further in case of IS (differentiate between NULL and EMPTY) | ||
| 2295 | $lookahead = $this->lexer->peek(); | ||
| 2296 | } | ||
| 2297 | |||
| 2298 | assert($lookahead !== null); | ||
| 2299 | // Also peek beyond a NOT if there is one. | ||
| 2300 | if ($lookahead->type === TokenType::T_NOT) { | ||
| 2301 | $lookahead = $this->lexer->peek(); | ||
| 2302 | } | ||
| 2303 | |||
| 2304 | $this->lexer->resetPeek(); | ||
| 2305 | } | ||
| 2306 | |||
| 2307 | if ($token->type === TokenType::T_BETWEEN) { | ||
| 2308 | return $this->BetweenExpression(); | ||
| 2309 | } | ||
| 2310 | |||
| 2311 | if ($token->type === TokenType::T_LIKE) { | ||
| 2312 | return $this->LikeExpression(); | ||
| 2313 | } | ||
| 2314 | |||
| 2315 | if ($token->type === TokenType::T_IN) { | ||
| 2316 | return $this->InExpression(); | ||
| 2317 | } | ||
| 2318 | |||
| 2319 | if ($token->type === TokenType::T_INSTANCE) { | ||
| 2320 | return $this->InstanceOfExpression(); | ||
| 2321 | } | ||
| 2322 | |||
| 2323 | if ($token->type === TokenType::T_MEMBER) { | ||
| 2324 | return $this->CollectionMemberExpression(); | ||
| 2325 | } | ||
| 2326 | |||
| 2327 | assert($lookahead !== null); | ||
| 2328 | if ($token->type === TokenType::T_IS && $lookahead->type === TokenType::T_NULL) { | ||
| 2329 | return $this->NullComparisonExpression(); | ||
| 2330 | } | ||
| 2331 | |||
| 2332 | if ($token->type === TokenType::T_IS && $lookahead->type === TokenType::T_EMPTY) { | ||
| 2333 | return $this->EmptyCollectionComparisonExpression(); | ||
| 2334 | } | ||
| 2335 | |||
| 2336 | return $this->ComparisonExpression(); | ||
| 2337 | } | ||
| 2338 | |||
| 2339 | /** | ||
| 2340 | * EmptyCollectionComparisonExpression ::= CollectionValuedPathExpression "IS" ["NOT"] "EMPTY" | ||
| 2341 | */ | ||
| 2342 | public function EmptyCollectionComparisonExpression(): AST\EmptyCollectionComparisonExpression | ||
| 2343 | { | ||
| 2344 | $pathExpression = $this->CollectionValuedPathExpression(); | ||
| 2345 | $this->match(TokenType::T_IS); | ||
| 2346 | |||
| 2347 | $not = false; | ||
| 2348 | if ($this->lexer->isNextToken(TokenType::T_NOT)) { | ||
| 2349 | $this->match(TokenType::T_NOT); | ||
| 2350 | $not = true; | ||
| 2351 | } | ||
| 2352 | |||
| 2353 | $this->match(TokenType::T_EMPTY); | ||
| 2354 | |||
| 2355 | return new AST\EmptyCollectionComparisonExpression( | ||
| 2356 | $pathExpression, | ||
| 2357 | $not, | ||
| 2358 | ); | ||
| 2359 | } | ||
| 2360 | |||
| 2361 | /** | ||
| 2362 | * CollectionMemberExpression ::= EntityExpression ["NOT"] "MEMBER" ["OF"] CollectionValuedPathExpression | ||
| 2363 | * | ||
| 2364 | * EntityExpression ::= SingleValuedAssociationPathExpression | SimpleEntityExpression | ||
| 2365 | * SimpleEntityExpression ::= IdentificationVariable | InputParameter | ||
| 2366 | */ | ||
| 2367 | public function CollectionMemberExpression(): AST\CollectionMemberExpression | ||
| 2368 | { | ||
| 2369 | $not = false; | ||
| 2370 | $entityExpr = $this->EntityExpression(); | ||
| 2371 | |||
| 2372 | if ($this->lexer->isNextToken(TokenType::T_NOT)) { | ||
| 2373 | $this->match(TokenType::T_NOT); | ||
| 2374 | |||
| 2375 | $not = true; | ||
| 2376 | } | ||
| 2377 | |||
| 2378 | $this->match(TokenType::T_MEMBER); | ||
| 2379 | |||
| 2380 | if ($this->lexer->isNextToken(TokenType::T_OF)) { | ||
| 2381 | $this->match(TokenType::T_OF); | ||
| 2382 | } | ||
| 2383 | |||
| 2384 | return new AST\CollectionMemberExpression( | ||
| 2385 | $entityExpr, | ||
| 2386 | $this->CollectionValuedPathExpression(), | ||
| 2387 | $not, | ||
| 2388 | ); | ||
| 2389 | } | ||
| 2390 | |||
| 2391 | /** | ||
| 2392 | * Literal ::= string | char | integer | float | boolean | ||
| 2393 | */ | ||
| 2394 | public function Literal(): AST\Literal | ||
| 2395 | { | ||
| 2396 | assert($this->lexer->lookahead !== null); | ||
| 2397 | assert($this->lexer->token !== null); | ||
| 2398 | switch ($this->lexer->lookahead->type) { | ||
| 2399 | case TokenType::T_STRING: | ||
| 2400 | $this->match(TokenType::T_STRING); | ||
| 2401 | |||
| 2402 | return new AST\Literal(AST\Literal::STRING, $this->lexer->token->value); | ||
| 2403 | |||
| 2404 | case TokenType::T_INTEGER: | ||
| 2405 | case TokenType::T_FLOAT: | ||
| 2406 | $this->match( | ||
| 2407 | $this->lexer->isNextToken(TokenType::T_INTEGER) ? TokenType::T_INTEGER : TokenType::T_FLOAT, | ||
| 2408 | ); | ||
| 2409 | |||
| 2410 | return new AST\Literal(AST\Literal::NUMERIC, $this->lexer->token->value); | ||
| 2411 | |||
| 2412 | case TokenType::T_TRUE: | ||
| 2413 | case TokenType::T_FALSE: | ||
| 2414 | $this->match( | ||
| 2415 | $this->lexer->isNextToken(TokenType::T_TRUE) ? TokenType::T_TRUE : TokenType::T_FALSE, | ||
| 2416 | ); | ||
| 2417 | |||
| 2418 | return new AST\Literal(AST\Literal::BOOLEAN, $this->lexer->token->value); | ||
| 2419 | |||
| 2420 | default: | ||
| 2421 | $this->syntaxError('Literal'); | ||
| 2422 | } | ||
| 2423 | } | ||
| 2424 | |||
| 2425 | /** | ||
| 2426 | * InParameter ::= ArithmeticExpression | InputParameter | ||
| 2427 | */ | ||
| 2428 | public function InParameter(): AST\InputParameter|AST\ArithmeticExpression | ||
| 2429 | { | ||
| 2430 | assert($this->lexer->lookahead !== null); | ||
| 2431 | if ($this->lexer->lookahead->type === TokenType::T_INPUT_PARAMETER) { | ||
| 2432 | return $this->InputParameter(); | ||
| 2433 | } | ||
| 2434 | |||
| 2435 | return $this->ArithmeticExpression(); | ||
| 2436 | } | ||
| 2437 | |||
| 2438 | /** | ||
| 2439 | * InputParameter ::= PositionalParameter | NamedParameter | ||
| 2440 | */ | ||
| 2441 | public function InputParameter(): AST\InputParameter | ||
| 2442 | { | ||
| 2443 | $this->match(TokenType::T_INPUT_PARAMETER); | ||
| 2444 | assert($this->lexer->token !== null); | ||
| 2445 | |||
| 2446 | return new AST\InputParameter($this->lexer->token->value); | ||
| 2447 | } | ||
| 2448 | |||
| 2449 | /** | ||
| 2450 | * ArithmeticExpression ::= SimpleArithmeticExpression | "(" Subselect ")" | ||
| 2451 | */ | ||
| 2452 | public function ArithmeticExpression(): AST\ArithmeticExpression | ||
| 2453 | { | ||
| 2454 | $expr = new AST\ArithmeticExpression(); | ||
| 2455 | |||
| 2456 | if ($this->lexer->isNextToken(TokenType::T_OPEN_PARENTHESIS)) { | ||
| 2457 | $peek = $this->lexer->glimpse(); | ||
| 2458 | assert($peek !== null); | ||
| 2459 | |||
| 2460 | if ($peek->type === TokenType::T_SELECT) { | ||
| 2461 | $this->match(TokenType::T_OPEN_PARENTHESIS); | ||
| 2462 | $expr->subselect = $this->Subselect(); | ||
| 2463 | $this->match(TokenType::T_CLOSE_PARENTHESIS); | ||
| 2464 | |||
| 2465 | return $expr; | ||
| 2466 | } | ||
| 2467 | } | ||
| 2468 | |||
| 2469 | $expr->simpleArithmeticExpression = $this->SimpleArithmeticExpression(); | ||
| 2470 | |||
| 2471 | return $expr; | ||
| 2472 | } | ||
| 2473 | |||
| 2474 | /** | ||
| 2475 | * SimpleArithmeticExpression ::= ArithmeticTerm {("+" | "-") ArithmeticTerm}* | ||
| 2476 | */ | ||
| 2477 | public function SimpleArithmeticExpression(): AST\Node|string | ||
| 2478 | { | ||
| 2479 | $terms = []; | ||
| 2480 | $terms[] = $this->ArithmeticTerm(); | ||
| 2481 | |||
| 2482 | while (($isPlus = $this->lexer->isNextToken(TokenType::T_PLUS)) || $this->lexer->isNextToken(TokenType::T_MINUS)) { | ||
| 2483 | $this->match($isPlus ? TokenType::T_PLUS : TokenType::T_MINUS); | ||
| 2484 | |||
| 2485 | assert($this->lexer->token !== null); | ||
| 2486 | $terms[] = $this->lexer->token->value; | ||
| 2487 | $terms[] = $this->ArithmeticTerm(); | ||
| 2488 | } | ||
| 2489 | |||
| 2490 | // Phase 1 AST optimization: Prevent AST\SimpleArithmeticExpression | ||
| 2491 | // if only one AST\ArithmeticTerm is defined | ||
| 2492 | if (count($terms) === 1) { | ||
| 2493 | return $terms[0]; | ||
| 2494 | } | ||
| 2495 | |||
| 2496 | return new AST\SimpleArithmeticExpression($terms); | ||
| 2497 | } | ||
| 2498 | |||
| 2499 | /** | ||
| 2500 | * ArithmeticTerm ::= ArithmeticFactor {("*" | "/") ArithmeticFactor}* | ||
| 2501 | */ | ||
| 2502 | public function ArithmeticTerm(): AST\Node|string | ||
| 2503 | { | ||
| 2504 | $factors = []; | ||
| 2505 | $factors[] = $this->ArithmeticFactor(); | ||
| 2506 | |||
| 2507 | while (($isMult = $this->lexer->isNextToken(TokenType::T_MULTIPLY)) || $this->lexer->isNextToken(TokenType::T_DIVIDE)) { | ||
| 2508 | $this->match($isMult ? TokenType::T_MULTIPLY : TokenType::T_DIVIDE); | ||
| 2509 | |||
| 2510 | assert($this->lexer->token !== null); | ||
| 2511 | $factors[] = $this->lexer->token->value; | ||
| 2512 | $factors[] = $this->ArithmeticFactor(); | ||
| 2513 | } | ||
| 2514 | |||
| 2515 | // Phase 1 AST optimization: Prevent AST\ArithmeticTerm | ||
| 2516 | // if only one AST\ArithmeticFactor is defined | ||
| 2517 | if (count($factors) === 1) { | ||
| 2518 | return $factors[0]; | ||
| 2519 | } | ||
| 2520 | |||
| 2521 | return new AST\ArithmeticTerm($factors); | ||
| 2522 | } | ||
| 2523 | |||
| 2524 | /** | ||
| 2525 | * ArithmeticFactor ::= [("+" | "-")] ArithmeticPrimary | ||
| 2526 | */ | ||
| 2527 | public function ArithmeticFactor(): AST\Node|string|AST\ArithmeticFactor | ||
| 2528 | { | ||
| 2529 | $sign = null; | ||
| 2530 | |||
| 2531 | $isPlus = $this->lexer->isNextToken(TokenType::T_PLUS); | ||
| 2532 | if ($isPlus || $this->lexer->isNextToken(TokenType::T_MINUS)) { | ||
| 2533 | $this->match($isPlus ? TokenType::T_PLUS : TokenType::T_MINUS); | ||
| 2534 | $sign = $isPlus; | ||
| 2535 | } | ||
| 2536 | |||
| 2537 | $primary = $this->ArithmeticPrimary(); | ||
| 2538 | |||
| 2539 | // Phase 1 AST optimization: Prevent AST\ArithmeticFactor | ||
| 2540 | // if only one AST\ArithmeticPrimary is defined | ||
| 2541 | if ($sign === null) { | ||
| 2542 | return $primary; | ||
| 2543 | } | ||
| 2544 | |||
| 2545 | return new AST\ArithmeticFactor($primary, $sign); | ||
| 2546 | } | ||
| 2547 | |||
| 2548 | /** | ||
| 2549 | * ArithmeticPrimary ::= SingleValuedPathExpression | Literal | ParenthesisExpression | ||
| 2550 | * | FunctionsReturningNumerics | AggregateExpression | FunctionsReturningStrings | ||
| 2551 | * | FunctionsReturningDatetime | IdentificationVariable | ResultVariable | ||
| 2552 | * | InputParameter | CaseExpression | ||
| 2553 | */ | ||
| 2554 | public function ArithmeticPrimary(): AST\Node|string | ||
| 2555 | { | ||
| 2556 | if ($this->lexer->isNextToken(TokenType::T_OPEN_PARENTHESIS)) { | ||
| 2557 | $this->match(TokenType::T_OPEN_PARENTHESIS); | ||
| 2558 | |||
| 2559 | $expr = $this->SimpleArithmeticExpression(); | ||
| 2560 | |||
| 2561 | $this->match(TokenType::T_CLOSE_PARENTHESIS); | ||
| 2562 | |||
| 2563 | return new AST\ParenthesisExpression($expr); | ||
| 2564 | } | ||
| 2565 | |||
| 2566 | if ($this->lexer->lookahead === null) { | ||
| 2567 | $this->syntaxError('ArithmeticPrimary'); | ||
| 2568 | } | ||
| 2569 | |||
| 2570 | switch ($this->lexer->lookahead->type) { | ||
| 2571 | case TokenType::T_COALESCE: | ||
| 2572 | case TokenType::T_NULLIF: | ||
| 2573 | case TokenType::T_CASE: | ||
| 2574 | return $this->CaseExpression(); | ||
| 2575 | |||
| 2576 | case TokenType::T_IDENTIFIER: | ||
| 2577 | $peek = $this->lexer->glimpse(); | ||
| 2578 | |||
| 2579 | if ($peek !== null && $peek->value === '(') { | ||
| 2580 | return $this->FunctionDeclaration(); | ||
| 2581 | } | ||
| 2582 | |||
| 2583 | if ($peek !== null && $peek->value === '.') { | ||
| 2584 | return $this->SingleValuedPathExpression(); | ||
| 2585 | } | ||
| 2586 | |||
| 2587 | if (isset($this->queryComponents[$this->lexer->lookahead->value]['resultVariable'])) { | ||
| 2588 | return $this->ResultVariable(); | ||
| 2589 | } | ||
| 2590 | |||
| 2591 | return $this->StateFieldPathExpression(); | ||
| 2592 | |||
| 2593 | case TokenType::T_INPUT_PARAMETER: | ||
| 2594 | return $this->InputParameter(); | ||
| 2595 | |||
| 2596 | default: | ||
| 2597 | $peek = $this->lexer->glimpse(); | ||
| 2598 | |||
| 2599 | if ($peek !== null && $peek->value === '(') { | ||
| 2600 | return $this->FunctionDeclaration(); | ||
| 2601 | } | ||
| 2602 | |||
| 2603 | return $this->Literal(); | ||
| 2604 | } | ||
| 2605 | } | ||
| 2606 | |||
| 2607 | /** | ||
| 2608 | * StringExpression ::= StringPrimary | ResultVariable | "(" Subselect ")" | ||
| 2609 | */ | ||
| 2610 | public function StringExpression(): AST\Subselect|AST\Node|string | ||
| 2611 | { | ||
| 2612 | $peek = $this->lexer->glimpse(); | ||
| 2613 | assert($peek !== null); | ||
| 2614 | |||
| 2615 | // Subselect | ||
| 2616 | if ($this->lexer->isNextToken(TokenType::T_OPEN_PARENTHESIS) && $peek->type === TokenType::T_SELECT) { | ||
| 2617 | $this->match(TokenType::T_OPEN_PARENTHESIS); | ||
| 2618 | $expr = $this->Subselect(); | ||
| 2619 | $this->match(TokenType::T_CLOSE_PARENTHESIS); | ||
| 2620 | |||
| 2621 | return $expr; | ||
| 2622 | } | ||
| 2623 | |||
| 2624 | assert($this->lexer->lookahead !== null); | ||
| 2625 | // ResultVariable (string) | ||
| 2626 | if ( | ||
| 2627 | $this->lexer->isNextToken(TokenType::T_IDENTIFIER) && | ||
| 2628 | isset($this->queryComponents[$this->lexer->lookahead->value]['resultVariable']) | ||
| 2629 | ) { | ||
| 2630 | return $this->ResultVariable(); | ||
| 2631 | } | ||
| 2632 | |||
| 2633 | return $this->StringPrimary(); | ||
| 2634 | } | ||
| 2635 | |||
| 2636 | /** | ||
| 2637 | * StringPrimary ::= StateFieldPathExpression | string | InputParameter | FunctionsReturningStrings | AggregateExpression | CaseExpression | ||
| 2638 | */ | ||
| 2639 | public function StringPrimary(): AST\Node | ||
| 2640 | { | ||
| 2641 | assert($this->lexer->lookahead !== null); | ||
| 2642 | $lookaheadType = $this->lexer->lookahead->type; | ||
| 2643 | |||
| 2644 | switch ($lookaheadType) { | ||
| 2645 | case TokenType::T_IDENTIFIER: | ||
| 2646 | $peek = $this->lexer->glimpse(); | ||
| 2647 | assert($peek !== null); | ||
| 2648 | |||
| 2649 | if ($peek->value === '.') { | ||
| 2650 | return $this->StateFieldPathExpression(); | ||
| 2651 | } | ||
| 2652 | |||
| 2653 | if ($peek->value === '(') { | ||
| 2654 | // do NOT directly go to FunctionsReturningString() because it doesn't check for custom functions. | ||
| 2655 | return $this->FunctionDeclaration(); | ||
| 2656 | } | ||
| 2657 | |||
| 2658 | $this->syntaxError("'.' or '('"); | ||
| 2659 | break; | ||
| 2660 | |||
| 2661 | case TokenType::T_STRING: | ||
| 2662 | $this->match(TokenType::T_STRING); | ||
| 2663 | assert($this->lexer->token !== null); | ||
| 2664 | |||
| 2665 | return new AST\Literal(AST\Literal::STRING, $this->lexer->token->value); | ||
| 2666 | |||
| 2667 | case TokenType::T_INPUT_PARAMETER: | ||
| 2668 | return $this->InputParameter(); | ||
| 2669 | |||
| 2670 | case TokenType::T_CASE: | ||
| 2671 | case TokenType::T_COALESCE: | ||
| 2672 | case TokenType::T_NULLIF: | ||
| 2673 | return $this->CaseExpression(); | ||
| 2674 | |||
| 2675 | default: | ||
| 2676 | assert($lookaheadType !== null); | ||
| 2677 | if ($this->isAggregateFunction($lookaheadType)) { | ||
| 2678 | return $this->AggregateExpression(); | ||
| 2679 | } | ||
| 2680 | } | ||
| 2681 | |||
| 2682 | $this->syntaxError( | ||
| 2683 | 'StateFieldPathExpression | string | InputParameter | FunctionsReturningStrings | AggregateExpression', | ||
| 2684 | ); | ||
| 2685 | } | ||
| 2686 | |||
| 2687 | /** | ||
| 2688 | * EntityExpression ::= SingleValuedAssociationPathExpression | SimpleEntityExpression | ||
| 2689 | */ | ||
| 2690 | public function EntityExpression(): AST\InputParameter|AST\PathExpression | ||
| 2691 | { | ||
| 2692 | $glimpse = $this->lexer->glimpse(); | ||
| 2693 | assert($glimpse !== null); | ||
| 2694 | |||
| 2695 | if ($this->lexer->isNextToken(TokenType::T_IDENTIFIER) && $glimpse->value === '.') { | ||
| 2696 | return $this->SingleValuedAssociationPathExpression(); | ||
| 2697 | } | ||
| 2698 | |||
| 2699 | return $this->SimpleEntityExpression(); | ||
| 2700 | } | ||
| 2701 | |||
| 2702 | /** | ||
| 2703 | * SimpleEntityExpression ::= IdentificationVariable | InputParameter | ||
| 2704 | */ | ||
| 2705 | public function SimpleEntityExpression(): AST\InputParameter|AST\PathExpression | ||
| 2706 | { | ||
| 2707 | if ($this->lexer->isNextToken(TokenType::T_INPUT_PARAMETER)) { | ||
| 2708 | return $this->InputParameter(); | ||
| 2709 | } | ||
| 2710 | |||
| 2711 | return $this->StateFieldPathExpression(); | ||
| 2712 | } | ||
| 2713 | |||
| 2714 | /** | ||
| 2715 | * AggregateExpression ::= | ||
| 2716 | * ("AVG" | "MAX" | "MIN" | "SUM" | "COUNT") "(" ["DISTINCT"] SimpleArithmeticExpression ")" | ||
| 2717 | */ | ||
| 2718 | public function AggregateExpression(): AST\AggregateExpression | ||
| 2719 | { | ||
| 2720 | assert($this->lexer->lookahead !== null); | ||
| 2721 | $lookaheadType = $this->lexer->lookahead->type; | ||
| 2722 | $isDistinct = false; | ||
| 2723 | |||
| 2724 | if (! in_array($lookaheadType, [TokenType::T_COUNT, TokenType::T_AVG, TokenType::T_MAX, TokenType::T_MIN, TokenType::T_SUM], true)) { | ||
| 2725 | $this->syntaxError('One of: MAX, MIN, AVG, SUM, COUNT'); | ||
| 2726 | } | ||
| 2727 | |||
| 2728 | $this->match($lookaheadType); | ||
| 2729 | assert($this->lexer->token !== null); | ||
| 2730 | $functionName = $this->lexer->token->value; | ||
| 2731 | $this->match(TokenType::T_OPEN_PARENTHESIS); | ||
| 2732 | |||
| 2733 | if ($this->lexer->isNextToken(TokenType::T_DISTINCT)) { | ||
| 2734 | $this->match(TokenType::T_DISTINCT); | ||
| 2735 | $isDistinct = true; | ||
| 2736 | } | ||
| 2737 | |||
| 2738 | $pathExp = $this->SimpleArithmeticExpression(); | ||
| 2739 | |||
| 2740 | $this->match(TokenType::T_CLOSE_PARENTHESIS); | ||
| 2741 | |||
| 2742 | return new AST\AggregateExpression($functionName, $pathExp, $isDistinct); | ||
| 2743 | } | ||
| 2744 | |||
| 2745 | /** | ||
| 2746 | * QuantifiedExpression ::= ("ALL" | "ANY" | "SOME") "(" Subselect ")" | ||
| 2747 | */ | ||
| 2748 | public function QuantifiedExpression(): AST\QuantifiedExpression | ||
| 2749 | { | ||
| 2750 | assert($this->lexer->lookahead !== null); | ||
| 2751 | $lookaheadType = $this->lexer->lookahead->type; | ||
| 2752 | $value = $this->lexer->lookahead->value; | ||
| 2753 | |||
| 2754 | if (! in_array($lookaheadType, [TokenType::T_ALL, TokenType::T_ANY, TokenType::T_SOME], true)) { | ||
| 2755 | $this->syntaxError('ALL, ANY or SOME'); | ||
| 2756 | } | ||
| 2757 | |||
| 2758 | $this->match($lookaheadType); | ||
| 2759 | $this->match(TokenType::T_OPEN_PARENTHESIS); | ||
| 2760 | |||
| 2761 | $qExpr = new AST\QuantifiedExpression($this->Subselect()); | ||
| 2762 | $qExpr->type = $value; | ||
| 2763 | |||
| 2764 | $this->match(TokenType::T_CLOSE_PARENTHESIS); | ||
| 2765 | |||
| 2766 | return $qExpr; | ||
| 2767 | } | ||
| 2768 | |||
| 2769 | /** | ||
| 2770 | * BetweenExpression ::= ArithmeticExpression ["NOT"] "BETWEEN" ArithmeticExpression "AND" ArithmeticExpression | ||
| 2771 | */ | ||
| 2772 | public function BetweenExpression(): AST\BetweenExpression | ||
| 2773 | { | ||
| 2774 | $not = false; | ||
| 2775 | $arithExpr1 = $this->ArithmeticExpression(); | ||
| 2776 | |||
| 2777 | if ($this->lexer->isNextToken(TokenType::T_NOT)) { | ||
| 2778 | $this->match(TokenType::T_NOT); | ||
| 2779 | $not = true; | ||
| 2780 | } | ||
| 2781 | |||
| 2782 | $this->match(TokenType::T_BETWEEN); | ||
| 2783 | $arithExpr2 = $this->ArithmeticExpression(); | ||
| 2784 | $this->match(TokenType::T_AND); | ||
| 2785 | $arithExpr3 = $this->ArithmeticExpression(); | ||
| 2786 | |||
| 2787 | return new AST\BetweenExpression($arithExpr1, $arithExpr2, $arithExpr3, $not); | ||
| 2788 | } | ||
| 2789 | |||
| 2790 | /** | ||
| 2791 | * ComparisonExpression ::= ArithmeticExpression ComparisonOperator ( QuantifiedExpression | ArithmeticExpression ) | ||
| 2792 | */ | ||
| 2793 | public function ComparisonExpression(): AST\ComparisonExpression | ||
| 2794 | { | ||
| 2795 | $this->lexer->glimpse(); | ||
| 2796 | |||
| 2797 | $leftExpr = $this->ArithmeticExpression(); | ||
| 2798 | $operator = $this->ComparisonOperator(); | ||
| 2799 | $rightExpr = $this->isNextAllAnySome() | ||
| 2800 | ? $this->QuantifiedExpression() | ||
| 2801 | : $this->ArithmeticExpression(); | ||
| 2802 | |||
| 2803 | return new AST\ComparisonExpression($leftExpr, $operator, $rightExpr); | ||
| 2804 | } | ||
| 2805 | |||
| 2806 | /** | ||
| 2807 | * InExpression ::= SingleValuedPathExpression ["NOT"] "IN" "(" (InParameter {"," InParameter}* | Subselect) ")" | ||
| 2808 | */ | ||
| 2809 | public function InExpression(): AST\InListExpression|AST\InSubselectExpression | ||
| 2810 | { | ||
| 2811 | $expression = $this->ArithmeticExpression(); | ||
| 2812 | |||
| 2813 | $not = false; | ||
| 2814 | if ($this->lexer->isNextToken(TokenType::T_NOT)) { | ||
| 2815 | $this->match(TokenType::T_NOT); | ||
| 2816 | $not = true; | ||
| 2817 | } | ||
| 2818 | |||
| 2819 | $this->match(TokenType::T_IN); | ||
| 2820 | $this->match(TokenType::T_OPEN_PARENTHESIS); | ||
| 2821 | |||
| 2822 | if ($this->lexer->isNextToken(TokenType::T_SELECT)) { | ||
| 2823 | $inExpression = new AST\InSubselectExpression( | ||
| 2824 | $expression, | ||
| 2825 | $this->Subselect(), | ||
| 2826 | $not, | ||
| 2827 | ); | ||
| 2828 | } else { | ||
| 2829 | $literals = [$this->InParameter()]; | ||
| 2830 | |||
| 2831 | while ($this->lexer->isNextToken(TokenType::T_COMMA)) { | ||
| 2832 | $this->match(TokenType::T_COMMA); | ||
| 2833 | $literals[] = $this->InParameter(); | ||
| 2834 | } | ||
| 2835 | |||
| 2836 | $inExpression = new AST\InListExpression( | ||
| 2837 | $expression, | ||
| 2838 | $literals, | ||
| 2839 | $not, | ||
| 2840 | ); | ||
| 2841 | } | ||
| 2842 | |||
| 2843 | $this->match(TokenType::T_CLOSE_PARENTHESIS); | ||
| 2844 | |||
| 2845 | return $inExpression; | ||
| 2846 | } | ||
| 2847 | |||
| 2848 | /** | ||
| 2849 | * InstanceOfExpression ::= IdentificationVariable ["NOT"] "INSTANCE" ["OF"] (InstanceOfParameter | "(" InstanceOfParameter {"," InstanceOfParameter}* ")") | ||
| 2850 | */ | ||
| 2851 | public function InstanceOfExpression(): AST\InstanceOfExpression | ||
| 2852 | { | ||
| 2853 | $identificationVariable = $this->IdentificationVariable(); | ||
| 2854 | |||
| 2855 | $not = false; | ||
| 2856 | if ($this->lexer->isNextToken(TokenType::T_NOT)) { | ||
| 2857 | $this->match(TokenType::T_NOT); | ||
| 2858 | $not = true; | ||
| 2859 | } | ||
| 2860 | |||
| 2861 | $this->match(TokenType::T_INSTANCE); | ||
| 2862 | $this->match(TokenType::T_OF); | ||
| 2863 | |||
| 2864 | $exprValues = $this->lexer->isNextToken(TokenType::T_OPEN_PARENTHESIS) | ||
| 2865 | ? $this->InstanceOfParameterList() | ||
| 2866 | : [$this->InstanceOfParameter()]; | ||
| 2867 | |||
| 2868 | return new AST\InstanceOfExpression( | ||
| 2869 | $identificationVariable, | ||
| 2870 | $exprValues, | ||
| 2871 | $not, | ||
| 2872 | ); | ||
| 2873 | } | ||
| 2874 | |||
| 2875 | /** @return non-empty-list<AST\InputParameter|string> */ | ||
| 2876 | public function InstanceOfParameterList(): array | ||
| 2877 | { | ||
| 2878 | $this->match(TokenType::T_OPEN_PARENTHESIS); | ||
| 2879 | |||
| 2880 | $exprValues = [$this->InstanceOfParameter()]; | ||
| 2881 | |||
| 2882 | while ($this->lexer->isNextToken(TokenType::T_COMMA)) { | ||
| 2883 | $this->match(TokenType::T_COMMA); | ||
| 2884 | |||
| 2885 | $exprValues[] = $this->InstanceOfParameter(); | ||
| 2886 | } | ||
| 2887 | |||
| 2888 | $this->match(TokenType::T_CLOSE_PARENTHESIS); | ||
| 2889 | |||
| 2890 | return $exprValues; | ||
| 2891 | } | ||
| 2892 | |||
| 2893 | /** | ||
| 2894 | * InstanceOfParameter ::= AbstractSchemaName | InputParameter | ||
| 2895 | */ | ||
| 2896 | public function InstanceOfParameter(): AST\InputParameter|string | ||
| 2897 | { | ||
| 2898 | if ($this->lexer->isNextToken(TokenType::T_INPUT_PARAMETER)) { | ||
| 2899 | $this->match(TokenType::T_INPUT_PARAMETER); | ||
| 2900 | assert($this->lexer->token !== null); | ||
| 2901 | |||
| 2902 | return new AST\InputParameter($this->lexer->token->value); | ||
| 2903 | } | ||
| 2904 | |||
| 2905 | $abstractSchemaName = $this->AbstractSchemaName(); | ||
| 2906 | |||
| 2907 | $this->validateAbstractSchemaName($abstractSchemaName); | ||
| 2908 | |||
| 2909 | return $abstractSchemaName; | ||
| 2910 | } | ||
| 2911 | |||
| 2912 | /** | ||
| 2913 | * LikeExpression ::= StringExpression ["NOT"] "LIKE" StringPrimary ["ESCAPE" char] | ||
| 2914 | */ | ||
| 2915 | public function LikeExpression(): AST\LikeExpression | ||
| 2916 | { | ||
| 2917 | $stringExpr = $this->StringExpression(); | ||
| 2918 | $not = false; | ||
| 2919 | |||
| 2920 | if ($this->lexer->isNextToken(TokenType::T_NOT)) { | ||
| 2921 | $this->match(TokenType::T_NOT); | ||
| 2922 | $not = true; | ||
| 2923 | } | ||
| 2924 | |||
| 2925 | $this->match(TokenType::T_LIKE); | ||
| 2926 | |||
| 2927 | if ($this->lexer->isNextToken(TokenType::T_INPUT_PARAMETER)) { | ||
| 2928 | $this->match(TokenType::T_INPUT_PARAMETER); | ||
| 2929 | assert($this->lexer->token !== null); | ||
| 2930 | $stringPattern = new AST\InputParameter($this->lexer->token->value); | ||
| 2931 | } else { | ||
| 2932 | $stringPattern = $this->StringPrimary(); | ||
| 2933 | } | ||
| 2934 | |||
| 2935 | $escapeChar = null; | ||
| 2936 | |||
| 2937 | if ($this->lexer->lookahead !== null && $this->lexer->lookahead->type === TokenType::T_ESCAPE) { | ||
| 2938 | $this->match(TokenType::T_ESCAPE); | ||
| 2939 | $this->match(TokenType::T_STRING); | ||
| 2940 | assert($this->lexer->token !== null); | ||
| 2941 | |||
| 2942 | $escapeChar = new AST\Literal(AST\Literal::STRING, $this->lexer->token->value); | ||
| 2943 | } | ||
| 2944 | |||
| 2945 | return new AST\LikeExpression($stringExpr, $stringPattern, $escapeChar, $not); | ||
| 2946 | } | ||
| 2947 | |||
| 2948 | /** | ||
| 2949 | * NullComparisonExpression ::= (InputParameter | NullIfExpression | CoalesceExpression | AggregateExpression | FunctionDeclaration | IdentificationVariable | SingleValuedPathExpression | ResultVariable) "IS" ["NOT"] "NULL" | ||
| 2950 | */ | ||
| 2951 | public function NullComparisonExpression(): AST\NullComparisonExpression | ||
| 2952 | { | ||
| 2953 | switch (true) { | ||
| 2954 | case $this->lexer->isNextToken(TokenType::T_INPUT_PARAMETER): | ||
| 2955 | $this->match(TokenType::T_INPUT_PARAMETER); | ||
| 2956 | assert($this->lexer->token !== null); | ||
| 2957 | |||
| 2958 | $expr = new AST\InputParameter($this->lexer->token->value); | ||
| 2959 | break; | ||
| 2960 | |||
| 2961 | case $this->lexer->isNextToken(TokenType::T_NULLIF): | ||
| 2962 | $expr = $this->NullIfExpression(); | ||
| 2963 | break; | ||
| 2964 | |||
| 2965 | case $this->lexer->isNextToken(TokenType::T_COALESCE): | ||
| 2966 | $expr = $this->CoalesceExpression(); | ||
| 2967 | break; | ||
| 2968 | |||
| 2969 | case $this->isFunction(): | ||
| 2970 | $expr = $this->FunctionDeclaration(); | ||
| 2971 | break; | ||
| 2972 | |||
| 2973 | default: | ||
| 2974 | // We need to check if we are in a IdentificationVariable or SingleValuedPathExpression | ||
| 2975 | $glimpse = $this->lexer->glimpse(); | ||
| 2976 | assert($glimpse !== null); | ||
| 2977 | |||
| 2978 | if ($glimpse->type === TokenType::T_DOT) { | ||
| 2979 | $expr = $this->SingleValuedPathExpression(); | ||
| 2980 | |||
| 2981 | // Leave switch statement | ||
| 2982 | break; | ||
| 2983 | } | ||
| 2984 | |||
| 2985 | assert($this->lexer->lookahead !== null); | ||
| 2986 | $lookaheadValue = $this->lexer->lookahead->value; | ||
| 2987 | |||
| 2988 | // Validate existing component | ||
| 2989 | if (! isset($this->queryComponents[$lookaheadValue])) { | ||
| 2990 | $this->semanticalError('Cannot add having condition on undefined result variable.'); | ||
| 2991 | } | ||
| 2992 | |||
| 2993 | // Validate SingleValuedPathExpression (ie.: "product") | ||
| 2994 | if (isset($this->queryComponents[$lookaheadValue]['metadata'])) { | ||
| 2995 | $expr = $this->SingleValuedPathExpression(); | ||
| 2996 | break; | ||
| 2997 | } | ||
| 2998 | |||
| 2999 | // Validating ResultVariable | ||
| 3000 | if (! isset($this->queryComponents[$lookaheadValue]['resultVariable'])) { | ||
| 3001 | $this->semanticalError('Cannot add having condition on a non result variable.'); | ||
| 3002 | } | ||
| 3003 | |||
| 3004 | $expr = $this->ResultVariable(); | ||
| 3005 | break; | ||
| 3006 | } | ||
| 3007 | |||
| 3008 | $this->match(TokenType::T_IS); | ||
| 3009 | |||
| 3010 | $not = false; | ||
| 3011 | if ($this->lexer->isNextToken(TokenType::T_NOT)) { | ||
| 3012 | $this->match(TokenType::T_NOT); | ||
| 3013 | |||
| 3014 | $not = true; | ||
| 3015 | } | ||
| 3016 | |||
| 3017 | $this->match(TokenType::T_NULL); | ||
| 3018 | |||
| 3019 | return new AST\NullComparisonExpression($expr, $not); | ||
| 3020 | } | ||
| 3021 | |||
| 3022 | /** | ||
| 3023 | * ExistsExpression ::= ["NOT"] "EXISTS" "(" Subselect ")" | ||
| 3024 | */ | ||
| 3025 | public function ExistsExpression(): AST\ExistsExpression | ||
| 3026 | { | ||
| 3027 | $not = false; | ||
| 3028 | |||
| 3029 | if ($this->lexer->isNextToken(TokenType::T_NOT)) { | ||
| 3030 | $this->match(TokenType::T_NOT); | ||
| 3031 | $not = true; | ||
| 3032 | } | ||
| 3033 | |||
| 3034 | $this->match(TokenType::T_EXISTS); | ||
| 3035 | $this->match(TokenType::T_OPEN_PARENTHESIS); | ||
| 3036 | |||
| 3037 | $subselect = $this->Subselect(); | ||
| 3038 | |||
| 3039 | $this->match(TokenType::T_CLOSE_PARENTHESIS); | ||
| 3040 | |||
| 3041 | return new AST\ExistsExpression($subselect, $not); | ||
| 3042 | } | ||
| 3043 | |||
| 3044 | /** | ||
| 3045 | * ComparisonOperator ::= "=" | "<" | "<=" | "<>" | ">" | ">=" | "!=" | ||
| 3046 | */ | ||
| 3047 | public function ComparisonOperator(): string | ||
| 3048 | { | ||
| 3049 | assert($this->lexer->lookahead !== null); | ||
| 3050 | switch ($this->lexer->lookahead->value) { | ||
| 3051 | case '=': | ||
| 3052 | $this->match(TokenType::T_EQUALS); | ||
| 3053 | |||
| 3054 | return '='; | ||
| 3055 | |||
| 3056 | case '<': | ||
| 3057 | $this->match(TokenType::T_LOWER_THAN); | ||
| 3058 | $operator = '<'; | ||
| 3059 | |||
| 3060 | if ($this->lexer->isNextToken(TokenType::T_EQUALS)) { | ||
| 3061 | $this->match(TokenType::T_EQUALS); | ||
| 3062 | $operator .= '='; | ||
| 3063 | } elseif ($this->lexer->isNextToken(TokenType::T_GREATER_THAN)) { | ||
| 3064 | $this->match(TokenType::T_GREATER_THAN); | ||
| 3065 | $operator .= '>'; | ||
| 3066 | } | ||
| 3067 | |||
| 3068 | return $operator; | ||
| 3069 | |||
| 3070 | case '>': | ||
| 3071 | $this->match(TokenType::T_GREATER_THAN); | ||
| 3072 | $operator = '>'; | ||
| 3073 | |||
| 3074 | if ($this->lexer->isNextToken(TokenType::T_EQUALS)) { | ||
| 3075 | $this->match(TokenType::T_EQUALS); | ||
| 3076 | $operator .= '='; | ||
| 3077 | } | ||
| 3078 | |||
| 3079 | return $operator; | ||
| 3080 | |||
| 3081 | case '!': | ||
| 3082 | $this->match(TokenType::T_NEGATE); | ||
| 3083 | $this->match(TokenType::T_EQUALS); | ||
| 3084 | |||
| 3085 | return '<>'; | ||
| 3086 | |||
| 3087 | default: | ||
| 3088 | $this->syntaxError('=, <, <=, <>, >, >=, !='); | ||
| 3089 | } | ||
| 3090 | } | ||
| 3091 | |||
| 3092 | /** | ||
| 3093 | * FunctionDeclaration ::= FunctionsReturningStrings | FunctionsReturningNumerics | FunctionsReturningDatetime | ||
| 3094 | */ | ||
| 3095 | public function FunctionDeclaration(): Functions\FunctionNode | ||
| 3096 | { | ||
| 3097 | assert($this->lexer->lookahead !== null); | ||
| 3098 | $token = $this->lexer->lookahead; | ||
| 3099 | $funcName = strtolower($token->value); | ||
| 3100 | |||
| 3101 | $customFunctionDeclaration = $this->CustomFunctionDeclaration(); | ||
| 3102 | |||
| 3103 | // Check for custom functions functions first! | ||
| 3104 | switch (true) { | ||
| 3105 | case $customFunctionDeclaration !== null: | ||
| 3106 | return $customFunctionDeclaration; | ||
| 3107 | |||
| 3108 | case isset(self::$stringFunctions[$funcName]): | ||
| 3109 | return $this->FunctionsReturningStrings(); | ||
| 3110 | |||
| 3111 | case isset(self::$numericFunctions[$funcName]): | ||
| 3112 | return $this->FunctionsReturningNumerics(); | ||
| 3113 | |||
| 3114 | case isset(self::$datetimeFunctions[$funcName]): | ||
| 3115 | return $this->FunctionsReturningDatetime(); | ||
| 3116 | |||
| 3117 | default: | ||
| 3118 | $this->syntaxError('known function', $token); | ||
| 3119 | } | ||
| 3120 | } | ||
| 3121 | |||
| 3122 | /** | ||
| 3123 | * Helper function for FunctionDeclaration grammar rule. | ||
| 3124 | */ | ||
| 3125 | private function CustomFunctionDeclaration(): Functions\FunctionNode|null | ||
| 3126 | { | ||
| 3127 | assert($this->lexer->lookahead !== null); | ||
| 3128 | $token = $this->lexer->lookahead; | ||
| 3129 | $funcName = strtolower($token->value); | ||
| 3130 | |||
| 3131 | // Check for custom functions afterwards | ||
| 3132 | $config = $this->em->getConfiguration(); | ||
| 3133 | |||
| 3134 | return match (true) { | ||
| 3135 | $config->getCustomStringFunction($funcName) !== null => $this->CustomFunctionsReturningStrings(), | ||
| 3136 | $config->getCustomNumericFunction($funcName) !== null => $this->CustomFunctionsReturningNumerics(), | ||
| 3137 | $config->getCustomDatetimeFunction($funcName) !== null => $this->CustomFunctionsReturningDatetime(), | ||
| 3138 | default => null, | ||
| 3139 | }; | ||
| 3140 | } | ||
| 3141 | |||
| 3142 | /** | ||
| 3143 | * FunctionsReturningNumerics ::= | ||
| 3144 | * "LENGTH" "(" StringPrimary ")" | | ||
| 3145 | * "LOCATE" "(" StringPrimary "," StringPrimary ["," SimpleArithmeticExpression]")" | | ||
| 3146 | * "ABS" "(" SimpleArithmeticExpression ")" | | ||
| 3147 | * "SQRT" "(" SimpleArithmeticExpression ")" | | ||
| 3148 | * "MOD" "(" SimpleArithmeticExpression "," SimpleArithmeticExpression ")" | | ||
| 3149 | * "SIZE" "(" CollectionValuedPathExpression ")" | | ||
| 3150 | * "DATE_DIFF" "(" ArithmeticPrimary "," ArithmeticPrimary ")" | | ||
| 3151 | * "BIT_AND" "(" ArithmeticPrimary "," ArithmeticPrimary ")" | | ||
| 3152 | * "BIT_OR" "(" ArithmeticPrimary "," ArithmeticPrimary ")" | ||
| 3153 | */ | ||
| 3154 | public function FunctionsReturningNumerics(): AST\Functions\FunctionNode | ||
| 3155 | { | ||
| 3156 | assert($this->lexer->lookahead !== null); | ||
| 3157 | $funcNameLower = strtolower($this->lexer->lookahead->value); | ||
| 3158 | $funcClass = self::$numericFunctions[$funcNameLower]; | ||
| 3159 | |||
| 3160 | $function = new $funcClass($funcNameLower); | ||
| 3161 | $function->parse($this); | ||
| 3162 | |||
| 3163 | return $function; | ||
| 3164 | } | ||
| 3165 | |||
| 3166 | public function CustomFunctionsReturningNumerics(): AST\Functions\FunctionNode | ||
| 3167 | { | ||
| 3168 | assert($this->lexer->lookahead !== null); | ||
| 3169 | // getCustomNumericFunction is case-insensitive | ||
| 3170 | $functionName = strtolower($this->lexer->lookahead->value); | ||
| 3171 | $functionClass = $this->em->getConfiguration()->getCustomNumericFunction($functionName); | ||
| 3172 | |||
| 3173 | assert($functionClass !== null); | ||
| 3174 | |||
| 3175 | $function = is_string($functionClass) | ||
| 3176 | ? new $functionClass($functionName) | ||
| 3177 | : $functionClass($functionName); | ||
| 3178 | |||
| 3179 | $function->parse($this); | ||
| 3180 | |||
| 3181 | return $function; | ||
| 3182 | } | ||
| 3183 | |||
| 3184 | /** | ||
| 3185 | * FunctionsReturningDateTime ::= | ||
| 3186 | * "CURRENT_DATE" | | ||
| 3187 | * "CURRENT_TIME" | | ||
| 3188 | * "CURRENT_TIMESTAMP" | | ||
| 3189 | * "DATE_ADD" "(" ArithmeticPrimary "," ArithmeticPrimary "," StringPrimary ")" | | ||
| 3190 | * "DATE_SUB" "(" ArithmeticPrimary "," ArithmeticPrimary "," StringPrimary ")" | ||
| 3191 | */ | ||
| 3192 | public function FunctionsReturningDatetime(): AST\Functions\FunctionNode | ||
| 3193 | { | ||
| 3194 | assert($this->lexer->lookahead !== null); | ||
| 3195 | $funcNameLower = strtolower($this->lexer->lookahead->value); | ||
| 3196 | $funcClass = self::$datetimeFunctions[$funcNameLower]; | ||
| 3197 | |||
| 3198 | $function = new $funcClass($funcNameLower); | ||
| 3199 | $function->parse($this); | ||
| 3200 | |||
| 3201 | return $function; | ||
| 3202 | } | ||
| 3203 | |||
| 3204 | public function CustomFunctionsReturningDatetime(): AST\Functions\FunctionNode | ||
| 3205 | { | ||
| 3206 | assert($this->lexer->lookahead !== null); | ||
| 3207 | // getCustomDatetimeFunction is case-insensitive | ||
| 3208 | $functionName = $this->lexer->lookahead->value; | ||
| 3209 | $functionClass = $this->em->getConfiguration()->getCustomDatetimeFunction($functionName); | ||
| 3210 | |||
| 3211 | assert($functionClass !== null); | ||
| 3212 | |||
| 3213 | $function = is_string($functionClass) | ||
| 3214 | ? new $functionClass($functionName) | ||
| 3215 | : $functionClass($functionName); | ||
| 3216 | |||
| 3217 | $function->parse($this); | ||
| 3218 | |||
| 3219 | return $function; | ||
| 3220 | } | ||
| 3221 | |||
| 3222 | /** | ||
| 3223 | * FunctionsReturningStrings ::= | ||
| 3224 | * "CONCAT" "(" StringPrimary "," StringPrimary {"," StringPrimary}* ")" | | ||
| 3225 | * "SUBSTRING" "(" StringPrimary "," SimpleArithmeticExpression "," SimpleArithmeticExpression ")" | | ||
| 3226 | * "TRIM" "(" [["LEADING" | "TRAILING" | "BOTH"] [char] "FROM"] StringPrimary ")" | | ||
| 3227 | * "LOWER" "(" StringPrimary ")" | | ||
| 3228 | * "UPPER" "(" StringPrimary ")" | | ||
| 3229 | * "IDENTITY" "(" SingleValuedAssociationPathExpression {"," string} ")" | ||
| 3230 | */ | ||
| 3231 | public function FunctionsReturningStrings(): AST\Functions\FunctionNode | ||
| 3232 | { | ||
| 3233 | assert($this->lexer->lookahead !== null); | ||
| 3234 | $funcNameLower = strtolower($this->lexer->lookahead->value); | ||
| 3235 | $funcClass = self::$stringFunctions[$funcNameLower]; | ||
| 3236 | |||
| 3237 | $function = new $funcClass($funcNameLower); | ||
| 3238 | $function->parse($this); | ||
| 3239 | |||
| 3240 | return $function; | ||
| 3241 | } | ||
| 3242 | |||
| 3243 | public function CustomFunctionsReturningStrings(): Functions\FunctionNode | ||
| 3244 | { | ||
| 3245 | assert($this->lexer->lookahead !== null); | ||
| 3246 | // getCustomStringFunction is case-insensitive | ||
| 3247 | $functionName = $this->lexer->lookahead->value; | ||
| 3248 | $functionClass = $this->em->getConfiguration()->getCustomStringFunction($functionName); | ||
| 3249 | |||
| 3250 | assert($functionClass !== null); | ||
| 3251 | |||
| 3252 | $function = is_string($functionClass) | ||
| 3253 | ? new $functionClass($functionName) | ||
| 3254 | : $functionClass($functionName); | ||
| 3255 | |||
| 3256 | $function->parse($this); | ||
| 3257 | |||
| 3258 | return $function; | ||
| 3259 | } | ||
| 3260 | |||
| 3261 | private function getMetadataForDqlAlias(string $dqlAlias): ClassMetadata | ||
| 3262 | { | ||
| 3263 | if (! isset($this->queryComponents[$dqlAlias]['metadata'])) { | ||
| 3264 | throw new LogicException(sprintf('No metadata for DQL alias: %s', $dqlAlias)); | ||
| 3265 | } | ||
| 3266 | |||
| 3267 | return $this->queryComponents[$dqlAlias]['metadata']; | ||
| 3268 | } | ||
| 3269 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/ParserResult.php b/vendor/doctrine/orm/src/Query/ParserResult.php new file mode 100644 index 0000000..8b5ee1f --- /dev/null +++ b/vendor/doctrine/orm/src/Query/ParserResult.php | |||
| @@ -0,0 +1,118 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Query; | ||
| 6 | |||
| 7 | use Doctrine\ORM\Query\Exec\AbstractSqlExecutor; | ||
| 8 | use LogicException; | ||
| 9 | |||
| 10 | use function sprintf; | ||
| 11 | |||
| 12 | /** | ||
| 13 | * Encapsulates the resulting components from a DQL query parsing process that | ||
| 14 | * can be serialized. | ||
| 15 | * | ||
| 16 | * @link http://www.doctrine-project.org | ||
| 17 | */ | ||
| 18 | class ParserResult | ||
| 19 | { | ||
| 20 | /** | ||
| 21 | * The SQL executor used for executing the SQL. | ||
| 22 | */ | ||
| 23 | private AbstractSqlExecutor|null $sqlExecutor = null; | ||
| 24 | |||
| 25 | /** | ||
| 26 | * The ResultSetMapping that describes how to map the SQL result set. | ||
| 27 | */ | ||
| 28 | private ResultSetMapping $resultSetMapping; | ||
| 29 | |||
| 30 | /** | ||
| 31 | * The mappings of DQL parameter names/positions to SQL parameter positions. | ||
| 32 | * | ||
| 33 | * @psalm-var array<string|int, list<int>> | ||
| 34 | */ | ||
| 35 | private array $parameterMappings = []; | ||
| 36 | |||
| 37 | /** | ||
| 38 | * Initializes a new instance of the <tt>ParserResult</tt> class. | ||
| 39 | * The new instance is initialized with an empty <tt>ResultSetMapping</tt>. | ||
| 40 | */ | ||
| 41 | public function __construct() | ||
| 42 | { | ||
| 43 | $this->resultSetMapping = new ResultSetMapping(); | ||
| 44 | } | ||
| 45 | |||
| 46 | /** | ||
| 47 | * Gets the ResultSetMapping for the parsed query. | ||
| 48 | * | ||
| 49 | * @return ResultSetMapping The result set mapping of the parsed query | ||
| 50 | */ | ||
| 51 | public function getResultSetMapping(): ResultSetMapping | ||
| 52 | { | ||
| 53 | return $this->resultSetMapping; | ||
| 54 | } | ||
| 55 | |||
| 56 | /** | ||
| 57 | * Sets the ResultSetMapping of the parsed query. | ||
| 58 | */ | ||
| 59 | public function setResultSetMapping(ResultSetMapping $rsm): void | ||
| 60 | { | ||
| 61 | $this->resultSetMapping = $rsm; | ||
| 62 | } | ||
| 63 | |||
| 64 | /** | ||
| 65 | * Sets the SQL executor that should be used for this ParserResult. | ||
| 66 | */ | ||
| 67 | public function setSqlExecutor(AbstractSqlExecutor $executor): void | ||
| 68 | { | ||
| 69 | $this->sqlExecutor = $executor; | ||
| 70 | } | ||
| 71 | |||
| 72 | /** | ||
| 73 | * Gets the SQL executor used by this ParserResult. | ||
| 74 | */ | ||
| 75 | public function getSqlExecutor(): AbstractSqlExecutor | ||
| 76 | { | ||
| 77 | if ($this->sqlExecutor === null) { | ||
| 78 | throw new LogicException(sprintf( | ||
| 79 | 'Executor not set yet. Call %s::setSqlExecutor() first.', | ||
| 80 | self::class, | ||
| 81 | )); | ||
| 82 | } | ||
| 83 | |||
| 84 | return $this->sqlExecutor; | ||
| 85 | } | ||
| 86 | |||
| 87 | /** | ||
| 88 | * Adds a DQL to SQL parameter mapping. One DQL parameter name/position can map to | ||
| 89 | * several SQL parameter positions. | ||
| 90 | */ | ||
| 91 | public function addParameterMapping(string|int $dqlPosition, int $sqlPosition): void | ||
| 92 | { | ||
| 93 | $this->parameterMappings[$dqlPosition][] = $sqlPosition; | ||
| 94 | } | ||
| 95 | |||
| 96 | /** | ||
| 97 | * Gets all DQL to SQL parameter mappings. | ||
| 98 | * | ||
| 99 | * @psalm-return array<int|string, list<int>> The parameter mappings. | ||
| 100 | */ | ||
| 101 | public function getParameterMappings(): array | ||
| 102 | { | ||
| 103 | return $this->parameterMappings; | ||
| 104 | } | ||
| 105 | |||
| 106 | /** | ||
| 107 | * Gets the SQL parameter positions for a DQL parameter name/position. | ||
| 108 | * | ||
| 109 | * @param string|int $dqlPosition The name or position of the DQL parameter. | ||
| 110 | * | ||
| 111 | * @return int[] The positions of the corresponding SQL parameters. | ||
| 112 | * @psalm-return list<int> | ||
| 113 | */ | ||
| 114 | public function getSqlParameterPositions(string|int $dqlPosition): array | ||
| 115 | { | ||
| 116 | return $this->parameterMappings[$dqlPosition]; | ||
| 117 | } | ||
| 118 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/Printer.php b/vendor/doctrine/orm/src/Query/Printer.php new file mode 100644 index 0000000..db1f159 --- /dev/null +++ b/vendor/doctrine/orm/src/Query/Printer.php | |||
| @@ -0,0 +1,64 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Query; | ||
| 6 | |||
| 7 | use function str_repeat; | ||
| 8 | |||
| 9 | /** | ||
| 10 | * A parse tree printer for Doctrine Query Language parser. | ||
| 11 | * | ||
| 12 | * @link http://www.phpdoctrine.org | ||
| 13 | */ | ||
| 14 | class Printer | ||
| 15 | { | ||
| 16 | /** Current indentation level */ | ||
| 17 | protected int $indent = 0; | ||
| 18 | |||
| 19 | /** | ||
| 20 | * Constructs a new parse tree printer. | ||
| 21 | * | ||
| 22 | * @param bool $silent Parse tree will not be printed if true. | ||
| 23 | */ | ||
| 24 | public function __construct(protected bool $silent = false) | ||
| 25 | { | ||
| 26 | } | ||
| 27 | |||
| 28 | /** | ||
| 29 | * Prints an opening parenthesis followed by production name and increases | ||
| 30 | * indentation level by one. | ||
| 31 | * | ||
| 32 | * This method is called before executing a production. | ||
| 33 | * | ||
| 34 | * @param string $name Production name. | ||
| 35 | */ | ||
| 36 | public function startProduction(string $name): void | ||
| 37 | { | ||
| 38 | $this->println('(' . $name); | ||
| 39 | $this->indent++; | ||
| 40 | } | ||
| 41 | |||
| 42 | /** | ||
| 43 | * Decreases indentation level by one and prints a closing parenthesis. | ||
| 44 | * | ||
| 45 | * This method is called after executing a production. | ||
| 46 | */ | ||
| 47 | public function endProduction(): void | ||
| 48 | { | ||
| 49 | $this->indent--; | ||
| 50 | $this->println(')'); | ||
| 51 | } | ||
| 52 | |||
| 53 | /** | ||
| 54 | * Prints text indented with spaces depending on current indentation level. | ||
| 55 | * | ||
| 56 | * @param string $str The text. | ||
| 57 | */ | ||
| 58 | public function println(string $str): void | ||
| 59 | { | ||
| 60 | if (! $this->silent) { | ||
| 61 | echo str_repeat(' ', $this->indent), $str, "\n"; | ||
| 62 | } | ||
| 63 | } | ||
| 64 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/QueryException.php b/vendor/doctrine/orm/src/Query/QueryException.php new file mode 100644 index 0000000..ae945b1 --- /dev/null +++ b/vendor/doctrine/orm/src/Query/QueryException.php | |||
| @@ -0,0 +1,155 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Query; | ||
| 6 | |||
| 7 | use Doctrine\ORM\Exception\ORMException; | ||
| 8 | use Doctrine\ORM\Mapping\AssociationMapping; | ||
| 9 | use Doctrine\ORM\Query\AST\PathExpression; | ||
| 10 | use Exception; | ||
| 11 | use Stringable; | ||
| 12 | use Throwable; | ||
| 13 | |||
| 14 | class QueryException extends Exception implements ORMException | ||
| 15 | { | ||
| 16 | public static function dqlError(string $dql): self | ||
| 17 | { | ||
| 18 | return new self($dql); | ||
| 19 | } | ||
| 20 | |||
| 21 | public static function syntaxError(string $message, Throwable|null $previous = null): self | ||
| 22 | { | ||
| 23 | return new self('[Syntax Error] ' . $message, 0, $previous); | ||
| 24 | } | ||
| 25 | |||
| 26 | public static function semanticalError(string $message, Throwable|null $previous = null): self | ||
| 27 | { | ||
| 28 | return new self('[Semantical Error] ' . $message, 0, $previous); | ||
| 29 | } | ||
| 30 | |||
| 31 | public static function invalidLockMode(): self | ||
| 32 | { | ||
| 33 | return new self('Invalid lock mode hint provided.'); | ||
| 34 | } | ||
| 35 | |||
| 36 | public static function invalidParameterType(string $expected, string $received): self | ||
| 37 | { | ||
| 38 | return new self('Invalid parameter type, ' . $received . ' given, but ' . $expected . ' expected.'); | ||
| 39 | } | ||
| 40 | |||
| 41 | public static function invalidParameterPosition(string $pos): self | ||
| 42 | { | ||
| 43 | return new self('Invalid parameter position: ' . $pos); | ||
| 44 | } | ||
| 45 | |||
| 46 | public static function tooManyParameters(int $expected, int $received): self | ||
| 47 | { | ||
| 48 | return new self('Too many parameters: the query defines ' . $expected . ' parameters and you bound ' . $received); | ||
| 49 | } | ||
| 50 | |||
| 51 | public static function tooFewParameters(int $expected, int $received): self | ||
| 52 | { | ||
| 53 | return new self('Too few parameters: the query defines ' . $expected . ' parameters but you only bound ' . $received); | ||
| 54 | } | ||
| 55 | |||
| 56 | public static function invalidParameterFormat(string $value): self | ||
| 57 | { | ||
| 58 | return new self('Invalid parameter format, ' . $value . ' given, but :<name> or ?<num> expected.'); | ||
| 59 | } | ||
| 60 | |||
| 61 | public static function unknownParameter(string $key): self | ||
| 62 | { | ||
| 63 | return new self('Invalid parameter: token ' . $key . ' is not defined in the query.'); | ||
| 64 | } | ||
| 65 | |||
| 66 | public static function parameterTypeMismatch(): self | ||
| 67 | { | ||
| 68 | return new self('DQL Query parameter and type numbers mismatch, but have to be exactly equal.'); | ||
| 69 | } | ||
| 70 | |||
| 71 | public static function invalidPathExpression(PathExpression $pathExpr): self | ||
| 72 | { | ||
| 73 | return new self( | ||
| 74 | "Invalid PathExpression '" . $pathExpr->identificationVariable . '.' . $pathExpr->field . "'.", | ||
| 75 | ); | ||
| 76 | } | ||
| 77 | |||
| 78 | public static function invalidLiteral(string|Stringable $literal): self | ||
| 79 | { | ||
| 80 | return new self("Invalid literal '" . $literal . "'"); | ||
| 81 | } | ||
| 82 | |||
| 83 | public static function iterateWithFetchJoinCollectionNotAllowed(AssociationMapping $assoc): self | ||
| 84 | { | ||
| 85 | return new self( | ||
| 86 | 'Invalid query operation: Not allowed to iterate over fetch join collections ' . | ||
| 87 | 'in class ' . $assoc->sourceEntity . ' association ' . $assoc->fieldName, | ||
| 88 | ); | ||
| 89 | } | ||
| 90 | |||
| 91 | /** | ||
| 92 | * @param string[] $assoc | ||
| 93 | * @psalm-param array<string, string> $assoc | ||
| 94 | */ | ||
| 95 | public static function overwritingJoinConditionsNotYetSupported(array $assoc): self | ||
| 96 | { | ||
| 97 | return new self( | ||
| 98 | 'Unsupported query operation: It is not yet possible to overwrite the join ' . | ||
| 99 | 'conditions in class ' . $assoc['sourceEntityName'] . ' association ' . $assoc['fieldName'] . '. ' . | ||
| 100 | 'Use WITH to append additional join conditions to the association.', | ||
| 101 | ); | ||
| 102 | } | ||
| 103 | |||
| 104 | public static function associationPathInverseSideNotSupported(PathExpression $pathExpr): self | ||
| 105 | { | ||
| 106 | return new self( | ||
| 107 | 'A single-valued association path expression to an inverse side is not supported in DQL queries. ' . | ||
| 108 | 'Instead of "' . $pathExpr->identificationVariable . '.' . $pathExpr->field . '" use an explicit join.', | ||
| 109 | ); | ||
| 110 | } | ||
| 111 | |||
| 112 | public static function iterateWithFetchJoinNotAllowed(AssociationMapping $assoc): self | ||
| 113 | { | ||
| 114 | return new self( | ||
| 115 | 'Iterate with fetch join in class ' . $assoc->sourceEntity . | ||
| 116 | ' using association ' . $assoc->fieldName . ' not allowed.', | ||
| 117 | ); | ||
| 118 | } | ||
| 119 | |||
| 120 | public static function eagerFetchJoinWithNotAllowed(string $sourceEntity, string $fieldName): self | ||
| 121 | { | ||
| 122 | return new self( | ||
| 123 | 'Associations with fetch-mode=EAGER may not be using WITH conditions in | ||
| 124 | "' . $sourceEntity . '#' . $fieldName . '".', | ||
| 125 | ); | ||
| 126 | } | ||
| 127 | |||
| 128 | public static function iterateWithMixedResultNotAllowed(): self | ||
| 129 | { | ||
| 130 | return new self('Iterating a query with mixed results (using scalars) is not supported.'); | ||
| 131 | } | ||
| 132 | |||
| 133 | public static function associationPathCompositeKeyNotSupported(): self | ||
| 134 | { | ||
| 135 | return new self( | ||
| 136 | 'A single-valued association path expression to an entity with a composite primary ' . | ||
| 137 | 'key is not supported. Explicitly name the components of the composite primary key ' . | ||
| 138 | 'in the query.', | ||
| 139 | ); | ||
| 140 | } | ||
| 141 | |||
| 142 | public static function instanceOfUnrelatedClass(string $className, string $rootClass): self | ||
| 143 | { | ||
| 144 | return new self("Cannot check if a child of '" . $rootClass . "' is instanceof '" . $className . "', " . | ||
| 145 | 'inheritance hierarchy does not exists between these two classes.'); | ||
| 146 | } | ||
| 147 | |||
| 148 | public static function invalidQueryComponent(string $dqlAlias): self | ||
| 149 | { | ||
| 150 | return new self( | ||
| 151 | "Invalid query component given for DQL alias '" . $dqlAlias . "', " . | ||
| 152 | "requires 'metadata', 'parent', 'relation', 'map', 'nestingLevel' and 'token' keys.", | ||
| 153 | ); | ||
| 154 | } | ||
| 155 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/QueryExpressionVisitor.php b/vendor/doctrine/orm/src/Query/QueryExpressionVisitor.php new file mode 100644 index 0000000..3e0ec65 --- /dev/null +++ b/vendor/doctrine/orm/src/Query/QueryExpressionVisitor.php | |||
| @@ -0,0 +1,180 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Query; | ||
| 6 | |||
| 7 | use Doctrine\Common\Collections\ArrayCollection; | ||
| 8 | use Doctrine\Common\Collections\Expr\Comparison; | ||
| 9 | use Doctrine\Common\Collections\Expr\CompositeExpression; | ||
| 10 | use Doctrine\Common\Collections\Expr\ExpressionVisitor; | ||
| 11 | use Doctrine\Common\Collections\Expr\Value; | ||
| 12 | use RuntimeException; | ||
| 13 | |||
| 14 | use function count; | ||
| 15 | use function str_replace; | ||
| 16 | use function str_starts_with; | ||
| 17 | |||
| 18 | /** | ||
| 19 | * Converts Collection expressions to Query expressions. | ||
| 20 | */ | ||
| 21 | class QueryExpressionVisitor extends ExpressionVisitor | ||
| 22 | { | ||
| 23 | private const OPERATOR_MAP = [ | ||
| 24 | Comparison::GT => Expr\Comparison::GT, | ||
| 25 | Comparison::GTE => Expr\Comparison::GTE, | ||
| 26 | Comparison::LT => Expr\Comparison::LT, | ||
| 27 | Comparison::LTE => Expr\Comparison::LTE, | ||
| 28 | ]; | ||
| 29 | |||
| 30 | private readonly Expr $expr; | ||
| 31 | |||
| 32 | /** @var list<mixed> */ | ||
| 33 | private array $parameters = []; | ||
| 34 | |||
| 35 | /** @param mixed[] $queryAliases */ | ||
| 36 | public function __construct( | ||
| 37 | private readonly array $queryAliases, | ||
| 38 | ) { | ||
| 39 | $this->expr = new Expr(); | ||
| 40 | } | ||
| 41 | |||
| 42 | /** | ||
| 43 | * Gets bound parameters. | ||
| 44 | * Filled after {@link dispach()}. | ||
| 45 | * | ||
| 46 | * @return ArrayCollection<int, mixed> | ||
| 47 | */ | ||
| 48 | public function getParameters(): ArrayCollection | ||
| 49 | { | ||
| 50 | return new ArrayCollection($this->parameters); | ||
| 51 | } | ||
| 52 | |||
| 53 | public function clearParameters(): void | ||
| 54 | { | ||
| 55 | $this->parameters = []; | ||
| 56 | } | ||
| 57 | |||
| 58 | /** | ||
| 59 | * Converts Criteria expression to Query one based on static map. | ||
| 60 | */ | ||
| 61 | private static function convertComparisonOperator(string $criteriaOperator): string|null | ||
| 62 | { | ||
| 63 | return self::OPERATOR_MAP[$criteriaOperator] ?? null; | ||
| 64 | } | ||
| 65 | |||
| 66 | public function walkCompositeExpression(CompositeExpression $expr): mixed | ||
| 67 | { | ||
| 68 | $expressionList = []; | ||
| 69 | |||
| 70 | foreach ($expr->getExpressionList() as $child) { | ||
| 71 | $expressionList[] = $this->dispatch($child); | ||
| 72 | } | ||
| 73 | |||
| 74 | return match ($expr->getType()) { | ||
| 75 | CompositeExpression::TYPE_AND => new Expr\Andx($expressionList), | ||
| 76 | CompositeExpression::TYPE_OR => new Expr\Orx($expressionList), | ||
| 77 | CompositeExpression::TYPE_NOT => $this->expr->not($expressionList[0]), | ||
| 78 | default => throw new RuntimeException('Unknown composite ' . $expr->getType()), | ||
| 79 | }; | ||
| 80 | } | ||
| 81 | |||
| 82 | public function walkComparison(Comparison $comparison): mixed | ||
| 83 | { | ||
| 84 | if (! isset($this->queryAliases[0])) { | ||
| 85 | throw new QueryException('No aliases are set before invoking walkComparison().'); | ||
| 86 | } | ||
| 87 | |||
| 88 | $field = $this->queryAliases[0] . '.' . $comparison->getField(); | ||
| 89 | |||
| 90 | foreach ($this->queryAliases as $alias) { | ||
| 91 | if (str_starts_with($comparison->getField() . '.', $alias . '.')) { | ||
| 92 | $field = $comparison->getField(); | ||
| 93 | break; | ||
| 94 | } | ||
| 95 | } | ||
| 96 | |||
| 97 | $parameterName = str_replace('.', '_', $comparison->getField()); | ||
| 98 | |||
| 99 | foreach ($this->parameters as $parameter) { | ||
| 100 | if ($parameter->getName() === $parameterName) { | ||
| 101 | $parameterName .= '_' . count($this->parameters); | ||
| 102 | break; | ||
| 103 | } | ||
| 104 | } | ||
| 105 | |||
| 106 | $parameter = new Parameter($parameterName, $this->walkValue($comparison->getValue())); | ||
| 107 | $placeholder = ':' . $parameterName; | ||
| 108 | |||
| 109 | switch ($comparison->getOperator()) { | ||
| 110 | case Comparison::IN: | ||
| 111 | $this->parameters[] = $parameter; | ||
| 112 | |||
| 113 | return $this->expr->in($field, $placeholder); | ||
| 114 | |||
| 115 | case Comparison::NIN: | ||
| 116 | $this->parameters[] = $parameter; | ||
| 117 | |||
| 118 | return $this->expr->notIn($field, $placeholder); | ||
| 119 | |||
| 120 | case Comparison::EQ: | ||
| 121 | case Comparison::IS: | ||
| 122 | if ($this->walkValue($comparison->getValue()) === null) { | ||
| 123 | return $this->expr->isNull($field); | ||
| 124 | } | ||
| 125 | |||
| 126 | $this->parameters[] = $parameter; | ||
| 127 | |||
| 128 | return $this->expr->eq($field, $placeholder); | ||
| 129 | |||
| 130 | case Comparison::NEQ: | ||
| 131 | if ($this->walkValue($comparison->getValue()) === null) { | ||
| 132 | return $this->expr->isNotNull($field); | ||
| 133 | } | ||
| 134 | |||
| 135 | $this->parameters[] = $parameter; | ||
| 136 | |||
| 137 | return $this->expr->neq($field, $placeholder); | ||
| 138 | |||
| 139 | case Comparison::CONTAINS: | ||
| 140 | $parameter->setValue('%' . $parameter->getValue() . '%', $parameter->getType()); | ||
| 141 | $this->parameters[] = $parameter; | ||
| 142 | |||
| 143 | return $this->expr->like($field, $placeholder); | ||
| 144 | |||
| 145 | case Comparison::MEMBER_OF: | ||
| 146 | return $this->expr->isMemberOf($comparison->getField(), $comparison->getValue()->getValue()); | ||
| 147 | |||
| 148 | case Comparison::STARTS_WITH: | ||
| 149 | $parameter->setValue($parameter->getValue() . '%', $parameter->getType()); | ||
| 150 | $this->parameters[] = $parameter; | ||
| 151 | |||
| 152 | return $this->expr->like($field, $placeholder); | ||
| 153 | |||
| 154 | case Comparison::ENDS_WITH: | ||
| 155 | $parameter->setValue('%' . $parameter->getValue(), $parameter->getType()); | ||
| 156 | $this->parameters[] = $parameter; | ||
| 157 | |||
| 158 | return $this->expr->like($field, $placeholder); | ||
| 159 | |||
| 160 | default: | ||
| 161 | $operator = self::convertComparisonOperator($comparison->getOperator()); | ||
| 162 | if ($operator) { | ||
| 163 | $this->parameters[] = $parameter; | ||
| 164 | |||
| 165 | return new Expr\Comparison( | ||
| 166 | $field, | ||
| 167 | $operator, | ||
| 168 | $placeholder, | ||
| 169 | ); | ||
| 170 | } | ||
| 171 | |||
| 172 | throw new RuntimeException('Unknown comparison operator: ' . $comparison->getOperator()); | ||
| 173 | } | ||
| 174 | } | ||
| 175 | |||
| 176 | public function walkValue(Value $value): mixed | ||
| 177 | { | ||
| 178 | return $value->getValue(); | ||
| 179 | } | ||
| 180 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/ResultSetMapping.php b/vendor/doctrine/orm/src/Query/ResultSetMapping.php new file mode 100644 index 0000000..612474d --- /dev/null +++ b/vendor/doctrine/orm/src/Query/ResultSetMapping.php | |||
| @@ -0,0 +1,547 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Query; | ||
| 6 | |||
| 7 | use function count; | ||
| 8 | |||
| 9 | /** | ||
| 10 | * A ResultSetMapping describes how a result set of an SQL query maps to a Doctrine result. | ||
| 11 | * | ||
| 12 | * IMPORTANT NOTE: | ||
| 13 | * The properties of this class are only public for fast internal READ access and to (drastically) | ||
| 14 | * reduce the size of serialized instances for more effective caching due to better (un-)serialization | ||
| 15 | * performance. | ||
| 16 | * | ||
| 17 | * <b>Users should use the public methods.</b> | ||
| 18 | * | ||
| 19 | * @todo Think about whether the number of lookup maps can be reduced. | ||
| 20 | */ | ||
| 21 | class ResultSetMapping | ||
| 22 | { | ||
| 23 | /** | ||
| 24 | * Whether the result is mixed (contains scalar values together with field values). | ||
| 25 | * | ||
| 26 | * @ignore | ||
| 27 | */ | ||
| 28 | public bool $isMixed = false; | ||
| 29 | |||
| 30 | /** | ||
| 31 | * Whether the result is a select statement. | ||
| 32 | * | ||
| 33 | * @ignore | ||
| 34 | */ | ||
| 35 | public bool $isSelect = true; | ||
| 36 | |||
| 37 | /** | ||
| 38 | * Maps alias names to class names. | ||
| 39 | * | ||
| 40 | * @ignore | ||
| 41 | * @psalm-var array<string, class-string> | ||
| 42 | */ | ||
| 43 | public array $aliasMap = []; | ||
| 44 | |||
| 45 | /** | ||
| 46 | * Maps alias names to related association field names. | ||
| 47 | * | ||
| 48 | * @ignore | ||
| 49 | * @psalm-var array<string, string> | ||
| 50 | */ | ||
| 51 | public array $relationMap = []; | ||
| 52 | |||
| 53 | /** | ||
| 54 | * Maps alias names to parent alias names. | ||
| 55 | * | ||
| 56 | * @ignore | ||
| 57 | * @psalm-var array<string, string> | ||
| 58 | */ | ||
| 59 | public array $parentAliasMap = []; | ||
| 60 | |||
| 61 | /** | ||
| 62 | * Maps column names in the result set to field names for each class. | ||
| 63 | * | ||
| 64 | * @ignore | ||
| 65 | * @psalm-var array<string, string> | ||
| 66 | */ | ||
| 67 | public array $fieldMappings = []; | ||
| 68 | |||
| 69 | /** | ||
| 70 | * Maps column names in the result set to the alias/field name to use in the mapped result. | ||
| 71 | * | ||
| 72 | * @ignore | ||
| 73 | * @psalm-var array<string, string|int> | ||
| 74 | */ | ||
| 75 | public array $scalarMappings = []; | ||
| 76 | |||
| 77 | /** | ||
| 78 | * Maps scalar columns to enums | ||
| 79 | * | ||
| 80 | * @ignore | ||
| 81 | * @psalm-var array<string, string> | ||
| 82 | */ | ||
| 83 | public $enumMappings = []; | ||
| 84 | |||
| 85 | /** | ||
| 86 | * Maps column names in the result set to the alias/field type to use in the mapped result. | ||
| 87 | * | ||
| 88 | * @ignore | ||
| 89 | * @psalm-var array<string, string> | ||
| 90 | */ | ||
| 91 | public array $typeMappings = []; | ||
| 92 | |||
| 93 | /** | ||
| 94 | * Maps entities in the result set to the alias name to use in the mapped result. | ||
| 95 | * | ||
| 96 | * @ignore | ||
| 97 | * @psalm-var array<string, string|null> | ||
| 98 | */ | ||
| 99 | public array $entityMappings = []; | ||
| 100 | |||
| 101 | /** | ||
| 102 | * Maps column names of meta columns (foreign keys, discriminator columns, ...) to field names. | ||
| 103 | * | ||
| 104 | * @ignore | ||
| 105 | * @psalm-var array<string, string> | ||
| 106 | */ | ||
| 107 | public array $metaMappings = []; | ||
| 108 | |||
| 109 | /** | ||
| 110 | * Maps column names in the result set to the alias they belong to. | ||
| 111 | * | ||
| 112 | * @ignore | ||
| 113 | * @psalm-var array<string, string> | ||
| 114 | */ | ||
| 115 | public array $columnOwnerMap = []; | ||
| 116 | |||
| 117 | /** | ||
| 118 | * List of columns in the result set that are used as discriminator columns. | ||
| 119 | * | ||
| 120 | * @ignore | ||
| 121 | * @psalm-var array<string, string> | ||
| 122 | */ | ||
| 123 | public array $discriminatorColumns = []; | ||
| 124 | |||
| 125 | /** | ||
| 126 | * Maps alias names to field names that should be used for indexing. | ||
| 127 | * | ||
| 128 | * @ignore | ||
| 129 | * @psalm-var array<string, string> | ||
| 130 | */ | ||
| 131 | public array $indexByMap = []; | ||
| 132 | |||
| 133 | /** | ||
| 134 | * Map from column names to class names that declare the field the column is mapped to. | ||
| 135 | * | ||
| 136 | * @ignore | ||
| 137 | * @psalm-var array<string, class-string> | ||
| 138 | */ | ||
| 139 | public array $declaringClasses = []; | ||
| 140 | |||
| 141 | /** | ||
| 142 | * This is necessary to hydrate derivate foreign keys correctly. | ||
| 143 | * | ||
| 144 | * @psalm-var array<string, array<string, bool>> | ||
| 145 | */ | ||
| 146 | public array $isIdentifierColumn = []; | ||
| 147 | |||
| 148 | /** | ||
| 149 | * Maps column names in the result set to field names for each new object expression. | ||
| 150 | * | ||
| 151 | * @psalm-var array<string, array<string, mixed>> | ||
| 152 | */ | ||
| 153 | public array $newObjectMappings = []; | ||
| 154 | |||
| 155 | /** | ||
| 156 | * Maps metadata parameter names to the metadata attribute. | ||
| 157 | * | ||
| 158 | * @psalm-var array<int|string, string> | ||
| 159 | */ | ||
| 160 | public array $metadataParameterMapping = []; | ||
| 161 | |||
| 162 | /** | ||
| 163 | * Contains query parameter names to be resolved as discriminator values | ||
| 164 | * | ||
| 165 | * @psalm-var array<string, string> | ||
| 166 | */ | ||
| 167 | public array $discriminatorParameters = []; | ||
| 168 | |||
| 169 | /** | ||
| 170 | * Adds an entity result to this ResultSetMapping. | ||
| 171 | * | ||
| 172 | * @param string $class The class name of the entity. | ||
| 173 | * @param string $alias The alias for the class. The alias must be unique among all entity | ||
| 174 | * results or joined entity results within this ResultSetMapping. | ||
| 175 | * @param string|null $resultAlias The result alias with which the entity result should be | ||
| 176 | * placed in the result structure. | ||
| 177 | * @psalm-param class-string $class | ||
| 178 | * | ||
| 179 | * @return $this | ||
| 180 | * | ||
| 181 | * @todo Rename: addRootEntity | ||
| 182 | */ | ||
| 183 | public function addEntityResult(string $class, string $alias, string|null $resultAlias = null): static | ||
| 184 | { | ||
| 185 | $this->aliasMap[$alias] = $class; | ||
| 186 | $this->entityMappings[$alias] = $resultAlias; | ||
| 187 | |||
| 188 | if ($resultAlias !== null) { | ||
| 189 | $this->isMixed = true; | ||
| 190 | } | ||
| 191 | |||
| 192 | return $this; | ||
| 193 | } | ||
| 194 | |||
| 195 | /** | ||
| 196 | * Sets a discriminator column for an entity result or joined entity result. | ||
| 197 | * The discriminator column will be used to determine the concrete class name to | ||
| 198 | * instantiate. | ||
| 199 | * | ||
| 200 | * @param string $alias The alias of the entity result or joined entity result the discriminator | ||
| 201 | * column should be used for. | ||
| 202 | * @param string $discrColumn The name of the discriminator column in the SQL result set. | ||
| 203 | * | ||
| 204 | * @return $this | ||
| 205 | * | ||
| 206 | * @todo Rename: addDiscriminatorColumn | ||
| 207 | */ | ||
| 208 | public function setDiscriminatorColumn(string $alias, string $discrColumn): static | ||
| 209 | { | ||
| 210 | $this->discriminatorColumns[$alias] = $discrColumn; | ||
| 211 | $this->columnOwnerMap[$discrColumn] = $alias; | ||
| 212 | |||
| 213 | return $this; | ||
| 214 | } | ||
| 215 | |||
| 216 | /** | ||
| 217 | * Sets a field to use for indexing an entity result or joined entity result. | ||
| 218 | * | ||
| 219 | * @param string $alias The alias of an entity result or joined entity result. | ||
| 220 | * @param string $fieldName The name of the field to use for indexing. | ||
| 221 | * | ||
| 222 | * @return $this | ||
| 223 | */ | ||
| 224 | public function addIndexBy(string $alias, string $fieldName): static | ||
| 225 | { | ||
| 226 | $found = false; | ||
| 227 | |||
| 228 | foreach ([...$this->metaMappings, ...$this->fieldMappings] as $columnName => $columnFieldName) { | ||
| 229 | if (! ($columnFieldName === $fieldName && $this->columnOwnerMap[$columnName] === $alias)) { | ||
| 230 | continue; | ||
| 231 | } | ||
| 232 | |||
| 233 | $this->addIndexByColumn($alias, $columnName); | ||
| 234 | $found = true; | ||
| 235 | |||
| 236 | break; | ||
| 237 | } | ||
| 238 | |||
| 239 | /* TODO: check if this exception can be put back, for now it's gone because of assumptions made by some ORM internals | ||
| 240 | if ( ! $found) { | ||
| 241 | $message = sprintf( | ||
| 242 | 'Cannot add index by for DQL alias %s and field %s without calling addFieldResult() for them before.', | ||
| 243 | $alias, | ||
| 244 | $fieldName | ||
| 245 | ); | ||
| 246 | |||
| 247 | throw new \LogicException($message); | ||
| 248 | } | ||
| 249 | */ | ||
| 250 | |||
| 251 | return $this; | ||
| 252 | } | ||
| 253 | |||
| 254 | /** | ||
| 255 | * Sets to index by a scalar result column name. | ||
| 256 | * | ||
| 257 | * @return $this | ||
| 258 | */ | ||
| 259 | public function addIndexByScalar(string $resultColumnName): static | ||
| 260 | { | ||
| 261 | $this->indexByMap['scalars'] = $resultColumnName; | ||
| 262 | |||
| 263 | return $this; | ||
| 264 | } | ||
| 265 | |||
| 266 | /** | ||
| 267 | * Sets a column to use for indexing an entity or joined entity result by the given alias name. | ||
| 268 | * | ||
| 269 | * @return $this | ||
| 270 | */ | ||
| 271 | public function addIndexByColumn(string $alias, string $resultColumnName): static | ||
| 272 | { | ||
| 273 | $this->indexByMap[$alias] = $resultColumnName; | ||
| 274 | |||
| 275 | return $this; | ||
| 276 | } | ||
| 277 | |||
| 278 | /** | ||
| 279 | * Checks whether an entity result or joined entity result with a given alias has | ||
| 280 | * a field set for indexing. | ||
| 281 | * | ||
| 282 | * @todo Rename: isIndexed($alias) | ||
| 283 | */ | ||
| 284 | public function hasIndexBy(string $alias): bool | ||
| 285 | { | ||
| 286 | return isset($this->indexByMap[$alias]); | ||
| 287 | } | ||
| 288 | |||
| 289 | /** | ||
| 290 | * Checks whether the column with the given name is mapped as a field result | ||
| 291 | * as part of an entity result or joined entity result. | ||
| 292 | * | ||
| 293 | * @param string $columnName The name of the column in the SQL result set. | ||
| 294 | * | ||
| 295 | * @todo Rename: isField | ||
| 296 | */ | ||
| 297 | public function isFieldResult(string $columnName): bool | ||
| 298 | { | ||
| 299 | return isset($this->fieldMappings[$columnName]); | ||
| 300 | } | ||
| 301 | |||
| 302 | /** | ||
| 303 | * Adds a field to the result that belongs to an entity or joined entity. | ||
| 304 | * | ||
| 305 | * @param string $alias The alias of the root entity or joined entity to which the field belongs. | ||
| 306 | * @param string $columnName The name of the column in the SQL result set. | ||
| 307 | * @param string $fieldName The name of the field on the declaring class. | ||
| 308 | * @param string|null $declaringClass The name of the class that declares/owns the specified field. | ||
| 309 | * When $alias refers to a superclass in a mapped hierarchy but | ||
| 310 | * the field $fieldName is defined on a subclass, specify that here. | ||
| 311 | * If not specified, the field is assumed to belong to the class | ||
| 312 | * designated by $alias. | ||
| 313 | * @psalm-param class-string|null $declaringClass | ||
| 314 | * | ||
| 315 | * @return $this | ||
| 316 | * | ||
| 317 | * @todo Rename: addField | ||
| 318 | */ | ||
| 319 | public function addFieldResult(string $alias, string $columnName, string $fieldName, string|null $declaringClass = null): static | ||
| 320 | { | ||
| 321 | // column name (in result set) => field name | ||
| 322 | $this->fieldMappings[$columnName] = $fieldName; | ||
| 323 | // column name => alias of owner | ||
| 324 | $this->columnOwnerMap[$columnName] = $alias; | ||
| 325 | // field name => class name of declaring class | ||
| 326 | $this->declaringClasses[$columnName] = $declaringClass ?: $this->aliasMap[$alias]; | ||
| 327 | |||
| 328 | if (! $this->isMixed && $this->scalarMappings) { | ||
| 329 | $this->isMixed = true; | ||
| 330 | } | ||
| 331 | |||
| 332 | return $this; | ||
| 333 | } | ||
| 334 | |||
| 335 | /** | ||
| 336 | * Adds a joined entity result. | ||
| 337 | * | ||
| 338 | * @param string $class The class name of the joined entity. | ||
| 339 | * @param string $alias The unique alias to use for the joined entity. | ||
| 340 | * @param string $parentAlias The alias of the entity result that is the parent of this joined result. | ||
| 341 | * @param string $relation The association field that connects the parent entity result | ||
| 342 | * with the joined entity result. | ||
| 343 | * @psalm-param class-string $class | ||
| 344 | * | ||
| 345 | * @return $this | ||
| 346 | * | ||
| 347 | * @todo Rename: addJoinedEntity | ||
| 348 | */ | ||
| 349 | public function addJoinedEntityResult(string $class, string $alias, string $parentAlias, string $relation): static | ||
| 350 | { | ||
| 351 | $this->aliasMap[$alias] = $class; | ||
| 352 | $this->parentAliasMap[$alias] = $parentAlias; | ||
| 353 | $this->relationMap[$alias] = $relation; | ||
| 354 | |||
| 355 | return $this; | ||
| 356 | } | ||
| 357 | |||
| 358 | /** | ||
| 359 | * Adds a scalar result mapping. | ||
| 360 | * | ||
| 361 | * @param string $columnName The name of the column in the SQL result set. | ||
| 362 | * @param string|int $alias The result alias with which the scalar result should be placed in the result structure. | ||
| 363 | * @param string $type The column type | ||
| 364 | * | ||
| 365 | * @return $this | ||
| 366 | * | ||
| 367 | * @todo Rename: addScalar | ||
| 368 | */ | ||
| 369 | public function addScalarResult(string $columnName, string|int $alias, string $type = 'string'): static | ||
| 370 | { | ||
| 371 | $this->scalarMappings[$columnName] = $alias; | ||
| 372 | $this->typeMappings[$columnName] = $type; | ||
| 373 | |||
| 374 | if (! $this->isMixed && $this->fieldMappings) { | ||
| 375 | $this->isMixed = true; | ||
| 376 | } | ||
| 377 | |||
| 378 | return $this; | ||
| 379 | } | ||
| 380 | |||
| 381 | /** | ||
| 382 | * Adds a scalar result mapping. | ||
| 383 | * | ||
| 384 | * @param string $columnName The name of the column in the SQL result set. | ||
| 385 | * @param string $enumType The enum type | ||
| 386 | * | ||
| 387 | * @return $this | ||
| 388 | */ | ||
| 389 | public function addEnumResult(string $columnName, string $enumType): static | ||
| 390 | { | ||
| 391 | $this->enumMappings[$columnName] = $enumType; | ||
| 392 | |||
| 393 | return $this; | ||
| 394 | } | ||
| 395 | |||
| 396 | /** | ||
| 397 | * Adds a metadata parameter mappings. | ||
| 398 | */ | ||
| 399 | public function addMetadataParameterMapping(string|int $parameter, string $attribute): void | ||
| 400 | { | ||
| 401 | $this->metadataParameterMapping[$parameter] = $attribute; | ||
| 402 | } | ||
| 403 | |||
| 404 | /** | ||
| 405 | * Checks whether a column with a given name is mapped as a scalar result. | ||
| 406 | * | ||
| 407 | * @todo Rename: isScalar | ||
| 408 | */ | ||
| 409 | public function isScalarResult(string $columnName): bool | ||
| 410 | { | ||
| 411 | return isset($this->scalarMappings[$columnName]); | ||
| 412 | } | ||
| 413 | |||
| 414 | /** | ||
| 415 | * Gets the name of the class of an entity result or joined entity result, | ||
| 416 | * identified by the given unique alias. | ||
| 417 | * | ||
| 418 | * @psalm-return class-string | ||
| 419 | */ | ||
| 420 | public function getClassName(string $alias): string | ||
| 421 | { | ||
| 422 | return $this->aliasMap[$alias]; | ||
| 423 | } | ||
| 424 | |||
| 425 | /** | ||
| 426 | * Gets the field alias for a column that is mapped as a scalar value. | ||
| 427 | * | ||
| 428 | * @param string $columnName The name of the column in the SQL result set. | ||
| 429 | */ | ||
| 430 | public function getScalarAlias(string $columnName): string|int | ||
| 431 | { | ||
| 432 | return $this->scalarMappings[$columnName]; | ||
| 433 | } | ||
| 434 | |||
| 435 | /** | ||
| 436 | * Gets the name of the class that owns a field mapping for the specified column. | ||
| 437 | * | ||
| 438 | * @psalm-return class-string | ||
| 439 | */ | ||
| 440 | public function getDeclaringClass(string $columnName): string | ||
| 441 | { | ||
| 442 | return $this->declaringClasses[$columnName]; | ||
| 443 | } | ||
| 444 | |||
| 445 | public function getRelation(string $alias): string | ||
| 446 | { | ||
| 447 | return $this->relationMap[$alias]; | ||
| 448 | } | ||
| 449 | |||
| 450 | public function isRelation(string $alias): bool | ||
| 451 | { | ||
| 452 | return isset($this->relationMap[$alias]); | ||
| 453 | } | ||
| 454 | |||
| 455 | /** | ||
| 456 | * Gets the alias of the class that owns a field mapping for the specified column. | ||
| 457 | */ | ||
| 458 | public function getEntityAlias(string $columnName): string | ||
| 459 | { | ||
| 460 | return $this->columnOwnerMap[$columnName]; | ||
| 461 | } | ||
| 462 | |||
| 463 | /** | ||
| 464 | * Gets the parent alias of the given alias. | ||
| 465 | */ | ||
| 466 | public function getParentAlias(string $alias): string | ||
| 467 | { | ||
| 468 | return $this->parentAliasMap[$alias]; | ||
| 469 | } | ||
| 470 | |||
| 471 | /** | ||
| 472 | * Checks whether the given alias has a parent alias. | ||
| 473 | */ | ||
| 474 | public function hasParentAlias(string $alias): bool | ||
| 475 | { | ||
| 476 | return isset($this->parentAliasMap[$alias]); | ||
| 477 | } | ||
| 478 | |||
| 479 | /** | ||
| 480 | * Gets the field name for a column name. | ||
| 481 | */ | ||
| 482 | public function getFieldName(string $columnName): string | ||
| 483 | { | ||
| 484 | return $this->fieldMappings[$columnName]; | ||
| 485 | } | ||
| 486 | |||
| 487 | /** @psalm-return array<string, class-string> */ | ||
| 488 | public function getAliasMap(): array | ||
| 489 | { | ||
| 490 | return $this->aliasMap; | ||
| 491 | } | ||
| 492 | |||
| 493 | /** | ||
| 494 | * Gets the number of different entities that appear in the mapped result. | ||
| 495 | * | ||
| 496 | * @psalm-return 0|positive-int | ||
| 497 | */ | ||
| 498 | public function getEntityResultCount(): int | ||
| 499 | { | ||
| 500 | return count($this->aliasMap); | ||
| 501 | } | ||
| 502 | |||
| 503 | /** | ||
| 504 | * Checks whether this ResultSetMapping defines a mixed result. | ||
| 505 | * | ||
| 506 | * Mixed results can only occur in object and array (graph) hydration. In such a | ||
| 507 | * case a mixed result means that scalar values are mixed with objects/array in | ||
| 508 | * the result. | ||
| 509 | */ | ||
| 510 | public function isMixedResult(): bool | ||
| 511 | { | ||
| 512 | return $this->isMixed; | ||
| 513 | } | ||
| 514 | |||
| 515 | /** | ||
| 516 | * Adds a meta column (foreign key or discriminator column) to the result set. | ||
| 517 | * | ||
| 518 | * @param string $alias The result alias with which the meta result should be placed in the result structure. | ||
| 519 | * @param string $columnName The name of the column in the SQL result set. | ||
| 520 | * @param string $fieldName The name of the field on the declaring class. | ||
| 521 | * @param string|null $type The column type | ||
| 522 | * | ||
| 523 | * @return $this | ||
| 524 | * | ||
| 525 | * @todo Make all methods of this class require all parameters and not infer anything | ||
| 526 | */ | ||
| 527 | public function addMetaResult( | ||
| 528 | string $alias, | ||
| 529 | string $columnName, | ||
| 530 | string $fieldName, | ||
| 531 | bool $isIdentifierColumn = false, | ||
| 532 | string|null $type = null, | ||
| 533 | ): static { | ||
| 534 | $this->metaMappings[$columnName] = $fieldName; | ||
| 535 | $this->columnOwnerMap[$columnName] = $alias; | ||
| 536 | |||
| 537 | if ($isIdentifierColumn) { | ||
| 538 | $this->isIdentifierColumn[$alias][$columnName] = true; | ||
| 539 | } | ||
| 540 | |||
| 541 | if ($type) { | ||
| 542 | $this->typeMappings[$columnName] = $type; | ||
| 543 | } | ||
| 544 | |||
| 545 | return $this; | ||
| 546 | } | ||
| 547 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/ResultSetMappingBuilder.php b/vendor/doctrine/orm/src/Query/ResultSetMappingBuilder.php new file mode 100644 index 0000000..f28f3a9 --- /dev/null +++ b/vendor/doctrine/orm/src/Query/ResultSetMappingBuilder.php | |||
| @@ -0,0 +1,281 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Query; | ||
| 6 | |||
| 7 | use Doctrine\DBAL\Types\Type; | ||
| 8 | use Doctrine\ORM\EntityManagerInterface; | ||
| 9 | use Doctrine\ORM\Internal\SQLResultCasing; | ||
| 10 | use Doctrine\ORM\Mapping\ClassMetadata; | ||
| 11 | use Doctrine\ORM\Utility\PersisterHelper; | ||
| 12 | use InvalidArgumentException; | ||
| 13 | use Stringable; | ||
| 14 | |||
| 15 | use function in_array; | ||
| 16 | use function sprintf; | ||
| 17 | |||
| 18 | /** | ||
| 19 | * A ResultSetMappingBuilder uses the EntityManager to automatically populate entity fields. | ||
| 20 | */ | ||
| 21 | class ResultSetMappingBuilder extends ResultSetMapping implements Stringable | ||
| 22 | { | ||
| 23 | use SQLResultCasing; | ||
| 24 | |||
| 25 | /** | ||
| 26 | * Picking this rename mode will register entity columns as is, | ||
| 27 | * as they are in the database. This can cause clashes when multiple | ||
| 28 | * entities are fetched that have columns with the same name. | ||
| 29 | */ | ||
| 30 | public const COLUMN_RENAMING_NONE = 1; | ||
| 31 | |||
| 32 | /** | ||
| 33 | * Picking custom renaming allows the user to define the renaming | ||
| 34 | * of specific columns with a rename array that contains column names as | ||
| 35 | * keys and result alias as values. | ||
| 36 | */ | ||
| 37 | public const COLUMN_RENAMING_CUSTOM = 2; | ||
| 38 | |||
| 39 | /** | ||
| 40 | * Incremental renaming uses a result set mapping internal counter to add a | ||
| 41 | * number to each column result, leading to uniqueness. This only works if | ||
| 42 | * you use {@see generateSelectClause()} to generate the SELECT clause for | ||
| 43 | * you. | ||
| 44 | */ | ||
| 45 | public const COLUMN_RENAMING_INCREMENT = 3; | ||
| 46 | |||
| 47 | private int $sqlCounter = 0; | ||
| 48 | |||
| 49 | /** @psalm-param self::COLUMN_RENAMING_* $defaultRenameMode */ | ||
| 50 | public function __construct( | ||
| 51 | private readonly EntityManagerInterface $em, | ||
| 52 | private readonly int $defaultRenameMode = self::COLUMN_RENAMING_NONE, | ||
| 53 | ) { | ||
| 54 | } | ||
| 55 | |||
| 56 | /** | ||
| 57 | * Adds a root entity and all of its fields to the result set. | ||
| 58 | * | ||
| 59 | * @param string $class The class name of the root entity. | ||
| 60 | * @param string $alias The unique alias to use for the root entity. | ||
| 61 | * @param string[] $renamedColumns Columns that have been renamed (tableColumnName => queryColumnName). | ||
| 62 | * @psalm-param class-string $class | ||
| 63 | * @psalm-param array<string, string> $renamedColumns | ||
| 64 | * @psalm-param self::COLUMN_RENAMING_*|null $renameMode | ||
| 65 | */ | ||
| 66 | public function addRootEntityFromClassMetadata( | ||
| 67 | string $class, | ||
| 68 | string $alias, | ||
| 69 | array $renamedColumns = [], | ||
| 70 | int|null $renameMode = null, | ||
| 71 | ): void { | ||
| 72 | $renameMode = $renameMode ?: $this->defaultRenameMode; | ||
| 73 | $columnAliasMap = $this->getColumnAliasMap($class, $renameMode, $renamedColumns); | ||
| 74 | |||
| 75 | $this->addEntityResult($class, $alias); | ||
| 76 | $this->addAllClassFields($class, $alias, $columnAliasMap); | ||
| 77 | } | ||
| 78 | |||
| 79 | /** | ||
| 80 | * Adds a joined entity and all of its fields to the result set. | ||
| 81 | * | ||
| 82 | * @param string $class The class name of the joined entity. | ||
| 83 | * @param string $alias The unique alias to use for the joined entity. | ||
| 84 | * @param string $parentAlias The alias of the entity result that is the parent of this joined result. | ||
| 85 | * @param string $relation The association field that connects the parent entity result | ||
| 86 | * with the joined entity result. | ||
| 87 | * @param string[] $renamedColumns Columns that have been renamed (tableColumnName => queryColumnName). | ||
| 88 | * @psalm-param class-string $class | ||
| 89 | * @psalm-param array<string, string> $renamedColumns | ||
| 90 | * @psalm-param self::COLUMN_RENAMING_*|null $renameMode | ||
| 91 | */ | ||
| 92 | public function addJoinedEntityFromClassMetadata( | ||
| 93 | string $class, | ||
| 94 | string $alias, | ||
| 95 | string $parentAlias, | ||
| 96 | string $relation, | ||
| 97 | array $renamedColumns = [], | ||
| 98 | int|null $renameMode = null, | ||
| 99 | ): void { | ||
| 100 | $renameMode = $renameMode ?: $this->defaultRenameMode; | ||
| 101 | $columnAliasMap = $this->getColumnAliasMap($class, $renameMode, $renamedColumns); | ||
| 102 | |||
| 103 | $this->addJoinedEntityResult($class, $alias, $parentAlias, $relation); | ||
| 104 | $this->addAllClassFields($class, $alias, $columnAliasMap); | ||
| 105 | } | ||
| 106 | |||
| 107 | /** | ||
| 108 | * Adds all fields of the given class to the result set mapping (columns and meta fields). | ||
| 109 | * | ||
| 110 | * @param string[] $columnAliasMap | ||
| 111 | * @psalm-param array<string, string> $columnAliasMap | ||
| 112 | * | ||
| 113 | * @throws InvalidArgumentException | ||
| 114 | */ | ||
| 115 | protected function addAllClassFields(string $class, string $alias, array $columnAliasMap = []): void | ||
| 116 | { | ||
| 117 | $classMetadata = $this->em->getClassMetadata($class); | ||
| 118 | $platform = $this->em->getConnection()->getDatabasePlatform(); | ||
| 119 | |||
| 120 | if (! $this->isInheritanceSupported($classMetadata)) { | ||
| 121 | throw new InvalidArgumentException('ResultSetMapping builder does not currently support your inheritance scheme.'); | ||
| 122 | } | ||
| 123 | |||
| 124 | foreach ($classMetadata->getColumnNames() as $columnName) { | ||
| 125 | $propertyName = $classMetadata->getFieldName($columnName); | ||
| 126 | $columnAlias = $this->getSQLResultCasing($platform, $columnAliasMap[$columnName]); | ||
| 127 | |||
| 128 | if (isset($this->fieldMappings[$columnAlias])) { | ||
| 129 | throw new InvalidArgumentException(sprintf( | ||
| 130 | "The column '%s' conflicts with another column in the mapper.", | ||
| 131 | $columnName, | ||
| 132 | )); | ||
| 133 | } | ||
| 134 | |||
| 135 | $this->addFieldResult($alias, $columnAlias, $propertyName); | ||
| 136 | |||
| 137 | $enumType = $classMetadata->getFieldMapping($propertyName)->enumType ?? null; | ||
| 138 | if (! empty($enumType)) { | ||
| 139 | $this->addEnumResult($columnAlias, $enumType); | ||
| 140 | } | ||
| 141 | } | ||
| 142 | |||
| 143 | foreach ($classMetadata->associationMappings as $associationMapping) { | ||
| 144 | if ($associationMapping->isToOneOwningSide()) { | ||
| 145 | $targetClass = $this->em->getClassMetadata($associationMapping->targetEntity); | ||
| 146 | $isIdentifier = isset($associationMapping->id) && $associationMapping->id === true; | ||
| 147 | |||
| 148 | foreach ($associationMapping->joinColumns as $joinColumn) { | ||
| 149 | $columnName = $joinColumn->name; | ||
| 150 | $columnAlias = $this->getSQLResultCasing($platform, $columnAliasMap[$columnName]); | ||
| 151 | $columnType = PersisterHelper::getTypeOfColumn($joinColumn->referencedColumnName, $targetClass, $this->em); | ||
| 152 | |||
| 153 | if (isset($this->metaMappings[$columnAlias])) { | ||
| 154 | throw new InvalidArgumentException(sprintf( | ||
| 155 | "The column '%s' conflicts with another column in the mapper.", | ||
| 156 | $columnAlias, | ||
| 157 | )); | ||
| 158 | } | ||
| 159 | |||
| 160 | $this->addMetaResult($alias, $columnAlias, $columnName, $isIdentifier, $columnType); | ||
| 161 | } | ||
| 162 | } | ||
| 163 | } | ||
| 164 | } | ||
| 165 | |||
| 166 | private function isInheritanceSupported(ClassMetadata $classMetadata): bool | ||
| 167 | { | ||
| 168 | if ( | ||
| 169 | $classMetadata->isInheritanceTypeSingleTable() | ||
| 170 | && in_array($classMetadata->name, $classMetadata->discriminatorMap, true) | ||
| 171 | ) { | ||
| 172 | return true; | ||
| 173 | } | ||
| 174 | |||
| 175 | return ! ($classMetadata->isInheritanceTypeSingleTable() || $classMetadata->isInheritanceTypeJoined()); | ||
| 176 | } | ||
| 177 | |||
| 178 | /** | ||
| 179 | * Gets column alias for a given column. | ||
| 180 | * | ||
| 181 | * @psalm-param array<string, string> $customRenameColumns | ||
| 182 | * | ||
| 183 | * @psalm-assert self::COLUMN_RENAMING_* $mode | ||
| 184 | */ | ||
| 185 | private function getColumnAlias(string $columnName, int $mode, array $customRenameColumns): string | ||
| 186 | { | ||
| 187 | return match ($mode) { | ||
| 188 | self::COLUMN_RENAMING_INCREMENT => $columnName . $this->sqlCounter++, | ||
| 189 | self::COLUMN_RENAMING_CUSTOM => $customRenameColumns[$columnName] ?? $columnName, | ||
| 190 | self::COLUMN_RENAMING_NONE => $columnName, | ||
| 191 | default => throw new InvalidArgumentException(sprintf('%d is not a valid value for $mode', $mode)), | ||
| 192 | }; | ||
| 193 | } | ||
| 194 | |||
| 195 | /** | ||
| 196 | * Retrieves a class columns and join columns aliases that are used in the SELECT clause. | ||
| 197 | * | ||
| 198 | * This depends on the renaming mode selected by the user. | ||
| 199 | * | ||
| 200 | * @psalm-param class-string $className | ||
| 201 | * @psalm-param self::COLUMN_RENAMING_* $mode | ||
| 202 | * @psalm-param array<string, string> $customRenameColumns | ||
| 203 | * | ||
| 204 | * @return string[] | ||
| 205 | * @psalm-return array<array-key, string> | ||
| 206 | */ | ||
| 207 | private function getColumnAliasMap( | ||
| 208 | string $className, | ||
| 209 | int $mode, | ||
| 210 | array $customRenameColumns, | ||
| 211 | ): array { | ||
| 212 | if ($customRenameColumns) { // for BC with 2.2-2.3 API | ||
| 213 | $mode = self::COLUMN_RENAMING_CUSTOM; | ||
| 214 | } | ||
| 215 | |||
| 216 | $columnAlias = []; | ||
| 217 | $class = $this->em->getClassMetadata($className); | ||
| 218 | |||
| 219 | foreach ($class->getColumnNames() as $columnName) { | ||
| 220 | $columnAlias[$columnName] = $this->getColumnAlias($columnName, $mode, $customRenameColumns); | ||
| 221 | } | ||
| 222 | |||
| 223 | foreach ($class->associationMappings as $associationMapping) { | ||
| 224 | if ($associationMapping->isToOneOwningSide()) { | ||
| 225 | foreach ($associationMapping->joinColumns as $joinColumn) { | ||
| 226 | $columnName = $joinColumn->name; | ||
| 227 | $columnAlias[$columnName] = $this->getColumnAlias($columnName, $mode, $customRenameColumns); | ||
| 228 | } | ||
| 229 | } | ||
| 230 | } | ||
| 231 | |||
| 232 | return $columnAlias; | ||
| 233 | } | ||
| 234 | |||
| 235 | /** | ||
| 236 | * Generates the Select clause from this ResultSetMappingBuilder. | ||
| 237 | * | ||
| 238 | * Works only for all the entity results. The select parts for scalar | ||
| 239 | * expressions have to be written manually. | ||
| 240 | * | ||
| 241 | * @param string[] $tableAliases | ||
| 242 | * @psalm-param array<string, string> $tableAliases | ||
| 243 | */ | ||
| 244 | public function generateSelectClause(array $tableAliases = []): string | ||
| 245 | { | ||
| 246 | $sql = ''; | ||
| 247 | |||
| 248 | foreach ($this->columnOwnerMap as $columnName => $dqlAlias) { | ||
| 249 | $tableAlias = $tableAliases[$dqlAlias] ?? $dqlAlias; | ||
| 250 | |||
| 251 | if ($sql !== '') { | ||
| 252 | $sql .= ', '; | ||
| 253 | } | ||
| 254 | |||
| 255 | if (isset($this->fieldMappings[$columnName])) { | ||
| 256 | $class = $this->em->getClassMetadata($this->declaringClasses[$columnName]); | ||
| 257 | $fieldName = $this->fieldMappings[$columnName]; | ||
| 258 | $classFieldMapping = $class->fieldMappings[$fieldName]; | ||
| 259 | $columnSql = $tableAlias . '.' . $classFieldMapping->columnName; | ||
| 260 | |||
| 261 | $type = Type::getType($classFieldMapping->type); | ||
| 262 | $columnSql = $type->convertToPHPValueSQL($columnSql, $this->em->getConnection()->getDatabasePlatform()); | ||
| 263 | |||
| 264 | $sql .= $columnSql; | ||
| 265 | } elseif (isset($this->metaMappings[$columnName])) { | ||
| 266 | $sql .= $tableAlias . '.' . $this->metaMappings[$columnName]; | ||
| 267 | } elseif (isset($this->discriminatorColumns[$dqlAlias])) { | ||
| 268 | $sql .= $tableAlias . '.' . $this->discriminatorColumns[$dqlAlias]; | ||
| 269 | } | ||
| 270 | |||
| 271 | $sql .= ' AS ' . $columnName; | ||
| 272 | } | ||
| 273 | |||
| 274 | return $sql; | ||
| 275 | } | ||
| 276 | |||
| 277 | public function __toString(): string | ||
| 278 | { | ||
| 279 | return $this->generateSelectClause([]); | ||
| 280 | } | ||
| 281 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/SqlWalker.php b/vendor/doctrine/orm/src/Query/SqlWalker.php new file mode 100644 index 0000000..c6f98c1 --- /dev/null +++ b/vendor/doctrine/orm/src/Query/SqlWalker.php | |||
| @@ -0,0 +1,2264 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Query; | ||
| 6 | |||
| 7 | use BadMethodCallException; | ||
| 8 | use Doctrine\DBAL\Connection; | ||
| 9 | use Doctrine\DBAL\LockMode; | ||
| 10 | use Doctrine\DBAL\Platforms\AbstractPlatform; | ||
| 11 | use Doctrine\DBAL\Types\Type; | ||
| 12 | use Doctrine\ORM\EntityManagerInterface; | ||
| 13 | use Doctrine\ORM\Mapping\ClassMetadata; | ||
| 14 | use Doctrine\ORM\Mapping\QuoteStrategy; | ||
| 15 | use Doctrine\ORM\OptimisticLockException; | ||
| 16 | use Doctrine\ORM\Query; | ||
| 17 | use Doctrine\ORM\Utility\HierarchyDiscriminatorResolver; | ||
| 18 | use Doctrine\ORM\Utility\LockSqlHelper; | ||
| 19 | use Doctrine\ORM\Utility\PersisterHelper; | ||
| 20 | use InvalidArgumentException; | ||
| 21 | use LogicException; | ||
| 22 | |||
| 23 | use function array_diff; | ||
| 24 | use function array_filter; | ||
| 25 | use function array_keys; | ||
| 26 | use function array_map; | ||
| 27 | use function array_merge; | ||
| 28 | use function assert; | ||
| 29 | use function count; | ||
| 30 | use function implode; | ||
| 31 | use function is_array; | ||
| 32 | use function is_float; | ||
| 33 | use function is_int; | ||
| 34 | use function is_numeric; | ||
| 35 | use function is_string; | ||
| 36 | use function preg_match; | ||
| 37 | use function reset; | ||
| 38 | use function sprintf; | ||
| 39 | use function strtolower; | ||
| 40 | use function strtoupper; | ||
| 41 | use function trim; | ||
| 42 | |||
| 43 | /** | ||
| 44 | * The SqlWalker walks over a DQL AST and constructs the corresponding SQL. | ||
| 45 | * | ||
| 46 | * @psalm-import-type QueryComponent from Parser | ||
| 47 | * @psalm-consistent-constructor | ||
| 48 | */ | ||
| 49 | class SqlWalker | ||
| 50 | { | ||
| 51 | use LockSqlHelper; | ||
| 52 | |||
| 53 | public const HINT_DISTINCT = 'doctrine.distinct'; | ||
| 54 | |||
| 55 | private readonly ResultSetMapping $rsm; | ||
| 56 | |||
| 57 | /** | ||
| 58 | * Counter for generating unique column aliases. | ||
| 59 | */ | ||
| 60 | private int $aliasCounter = 0; | ||
| 61 | |||
| 62 | /** | ||
| 63 | * Counter for generating unique table aliases. | ||
| 64 | */ | ||
| 65 | private int $tableAliasCounter = 0; | ||
| 66 | |||
| 67 | /** | ||
| 68 | * Counter for generating unique scalar result. | ||
| 69 | */ | ||
| 70 | private int $scalarResultCounter = 1; | ||
| 71 | |||
| 72 | /** | ||
| 73 | * Counter for generating unique parameter indexes. | ||
| 74 | */ | ||
| 75 | private int $sqlParamIndex = 0; | ||
| 76 | |||
| 77 | /** | ||
| 78 | * Counter for generating indexes. | ||
| 79 | */ | ||
| 80 | private int $newObjectCounter = 0; | ||
| 81 | |||
| 82 | private readonly EntityManagerInterface $em; | ||
| 83 | private readonly Connection $conn; | ||
| 84 | |||
| 85 | /** @var mixed[] */ | ||
| 86 | private array $tableAliasMap = []; | ||
| 87 | |||
| 88 | /** | ||
| 89 | * Map from result variable names to their SQL column alias names. | ||
| 90 | * | ||
| 91 | * @psalm-var array<string|int, string|list<string>> | ||
| 92 | */ | ||
| 93 | private array $scalarResultAliasMap = []; | ||
| 94 | |||
| 95 | /** | ||
| 96 | * Map from Table-Alias + Column-Name to OrderBy-Direction. | ||
| 97 | * | ||
| 98 | * @var array<string, string> | ||
| 99 | */ | ||
| 100 | private array $orderedColumnsMap = []; | ||
| 101 | |||
| 102 | /** | ||
| 103 | * Map from DQL-Alias + Field-Name to SQL Column Alias. | ||
| 104 | * | ||
| 105 | * @var array<string, array<string, string>> | ||
| 106 | */ | ||
| 107 | private array $scalarFields = []; | ||
| 108 | |||
| 109 | /** | ||
| 110 | * A list of classes that appear in non-scalar SelectExpressions. | ||
| 111 | * | ||
| 112 | * @psalm-var array<string, array{class: ClassMetadata, dqlAlias: string, resultAlias: string|null}> | ||
| 113 | */ | ||
| 114 | private array $selectedClasses = []; | ||
| 115 | |||
| 116 | /** | ||
| 117 | * The DQL alias of the root class of the currently traversed query. | ||
| 118 | * | ||
| 119 | * @psalm-var list<string> | ||
| 120 | */ | ||
| 121 | private array $rootAliases = []; | ||
| 122 | |||
| 123 | /** | ||
| 124 | * Flag that indicates whether to generate SQL table aliases in the SQL. | ||
| 125 | * These should only be generated for SELECT queries, not for UPDATE/DELETE. | ||
| 126 | */ | ||
| 127 | private bool $useSqlTableAliases = true; | ||
| 128 | |||
| 129 | /** | ||
| 130 | * The database platform abstraction. | ||
| 131 | */ | ||
| 132 | private readonly AbstractPlatform $platform; | ||
| 133 | |||
| 134 | /** | ||
| 135 | * The quote strategy. | ||
| 136 | */ | ||
| 137 | private readonly QuoteStrategy $quoteStrategy; | ||
| 138 | |||
| 139 | /** @psalm-param array<string, QueryComponent> $queryComponents The query components (symbol table). */ | ||
| 140 | public function __construct( | ||
| 141 | private readonly Query $query, | ||
| 142 | private readonly ParserResult $parserResult, | ||
| 143 | private array $queryComponents, | ||
| 144 | ) { | ||
| 145 | $this->rsm = $parserResult->getResultSetMapping(); | ||
| 146 | $this->em = $query->getEntityManager(); | ||
| 147 | $this->conn = $this->em->getConnection(); | ||
| 148 | $this->platform = $this->conn->getDatabasePlatform(); | ||
| 149 | $this->quoteStrategy = $this->em->getConfiguration()->getQuoteStrategy(); | ||
| 150 | } | ||
| 151 | |||
| 152 | /** | ||
| 153 | * Gets the Query instance used by the walker. | ||
| 154 | */ | ||
| 155 | public function getQuery(): Query | ||
| 156 | { | ||
| 157 | return $this->query; | ||
| 158 | } | ||
| 159 | |||
| 160 | /** | ||
| 161 | * Gets the Connection used by the walker. | ||
| 162 | */ | ||
| 163 | public function getConnection(): Connection | ||
| 164 | { | ||
| 165 | return $this->conn; | ||
| 166 | } | ||
| 167 | |||
| 168 | /** | ||
| 169 | * Gets the EntityManager used by the walker. | ||
| 170 | */ | ||
| 171 | public function getEntityManager(): EntityManagerInterface | ||
| 172 | { | ||
| 173 | return $this->em; | ||
| 174 | } | ||
| 175 | |||
| 176 | /** | ||
| 177 | * Gets the information about a single query component. | ||
| 178 | * | ||
| 179 | * @param string $dqlAlias The DQL alias. | ||
| 180 | * | ||
| 181 | * @return mixed[] | ||
| 182 | * @psalm-return QueryComponent | ||
| 183 | */ | ||
| 184 | public function getQueryComponent(string $dqlAlias): array | ||
| 185 | { | ||
| 186 | return $this->queryComponents[$dqlAlias]; | ||
| 187 | } | ||
| 188 | |||
| 189 | public function getMetadataForDqlAlias(string $dqlAlias): ClassMetadata | ||
| 190 | { | ||
| 191 | return $this->queryComponents[$dqlAlias]['metadata'] | ||
| 192 | ?? throw new LogicException(sprintf('No metadata for DQL alias: %s', $dqlAlias)); | ||
| 193 | } | ||
| 194 | |||
| 195 | /** | ||
| 196 | * Returns internal queryComponents array. | ||
| 197 | * | ||
| 198 | * @return array<string, QueryComponent> | ||
| 199 | */ | ||
| 200 | public function getQueryComponents(): array | ||
| 201 | { | ||
| 202 | return $this->queryComponents; | ||
| 203 | } | ||
| 204 | |||
| 205 | /** | ||
| 206 | * Sets or overrides a query component for a given dql alias. | ||
| 207 | * | ||
| 208 | * @psalm-param QueryComponent $queryComponent | ||
| 209 | */ | ||
| 210 | public function setQueryComponent(string $dqlAlias, array $queryComponent): void | ||
| 211 | { | ||
| 212 | $requiredKeys = ['metadata', 'parent', 'relation', 'map', 'nestingLevel', 'token']; | ||
| 213 | |||
| 214 | if (array_diff($requiredKeys, array_keys($queryComponent))) { | ||
| 215 | throw QueryException::invalidQueryComponent($dqlAlias); | ||
| 216 | } | ||
| 217 | |||
| 218 | $this->queryComponents[$dqlAlias] = $queryComponent; | ||
| 219 | } | ||
| 220 | |||
| 221 | /** | ||
| 222 | * Gets an executor that can be used to execute the result of this walker. | ||
| 223 | */ | ||
| 224 | public function getExecutor(AST\SelectStatement|AST\UpdateStatement|AST\DeleteStatement $statement): Exec\AbstractSqlExecutor | ||
| 225 | { | ||
| 226 | return match (true) { | ||
| 227 | $statement instanceof AST\SelectStatement | ||
| 228 | => new Exec\SingleSelectExecutor($statement, $this), | ||
| 229 | $statement instanceof AST\UpdateStatement | ||
| 230 | => $this->em->getClassMetadata($statement->updateClause->abstractSchemaName)->isInheritanceTypeJoined() | ||
| 231 | ? new Exec\MultiTableUpdateExecutor($statement, $this) | ||
| 232 | : new Exec\SingleTableDeleteUpdateExecutor($statement, $this), | ||
| 233 | $statement instanceof AST\DeleteStatement | ||
| 234 | => $this->em->getClassMetadata($statement->deleteClause->abstractSchemaName)->isInheritanceTypeJoined() | ||
| 235 | ? new Exec\MultiTableDeleteExecutor($statement, $this) | ||
| 236 | : new Exec\SingleTableDeleteUpdateExecutor($statement, $this), | ||
| 237 | }; | ||
| 238 | } | ||
| 239 | |||
| 240 | /** | ||
| 241 | * Generates a unique, short SQL table alias. | ||
| 242 | */ | ||
| 243 | public function getSQLTableAlias(string $tableName, string $dqlAlias = ''): string | ||
| 244 | { | ||
| 245 | $tableName .= $dqlAlias ? '@[' . $dqlAlias . ']' : ''; | ||
| 246 | |||
| 247 | if (! isset($this->tableAliasMap[$tableName])) { | ||
| 248 | $this->tableAliasMap[$tableName] = (preg_match('/[a-z]/i', $tableName[0]) ? strtolower($tableName[0]) : 't') | ||
| 249 | . $this->tableAliasCounter++ . '_'; | ||
| 250 | } | ||
| 251 | |||
| 252 | return $this->tableAliasMap[$tableName]; | ||
| 253 | } | ||
| 254 | |||
| 255 | /** | ||
| 256 | * Forces the SqlWalker to use a specific alias for a table name, rather than | ||
| 257 | * generating an alias on its own. | ||
| 258 | */ | ||
| 259 | public function setSQLTableAlias(string $tableName, string $alias, string $dqlAlias = ''): string | ||
| 260 | { | ||
| 261 | $tableName .= $dqlAlias ? '@[' . $dqlAlias . ']' : ''; | ||
| 262 | |||
| 263 | $this->tableAliasMap[$tableName] = $alias; | ||
| 264 | |||
| 265 | return $alias; | ||
| 266 | } | ||
| 267 | |||
| 268 | /** | ||
| 269 | * Gets an SQL column alias for a column name. | ||
| 270 | */ | ||
| 271 | public function getSQLColumnAlias(string $columnName): string | ||
| 272 | { | ||
| 273 | return $this->quoteStrategy->getColumnAlias($columnName, $this->aliasCounter++, $this->platform); | ||
| 274 | } | ||
| 275 | |||
| 276 | /** | ||
| 277 | * Generates the SQL JOINs that are necessary for Class Table Inheritance | ||
| 278 | * for the given class. | ||
| 279 | */ | ||
| 280 | private function generateClassTableInheritanceJoins( | ||
| 281 | ClassMetadata $class, | ||
| 282 | string $dqlAlias, | ||
| 283 | ): string { | ||
| 284 | $sql = ''; | ||
| 285 | |||
| 286 | $baseTableAlias = $this->getSQLTableAlias($class->getTableName(), $dqlAlias); | ||
| 287 | |||
| 288 | // INNER JOIN parent class tables | ||
| 289 | foreach ($class->parentClasses as $parentClassName) { | ||
| 290 | $parentClass = $this->em->getClassMetadata($parentClassName); | ||
| 291 | $tableAlias = $this->getSQLTableAlias($parentClass->getTableName(), $dqlAlias); | ||
| 292 | |||
| 293 | // If this is a joined association we must use left joins to preserve the correct result. | ||
| 294 | $sql .= isset($this->queryComponents[$dqlAlias]['relation']) ? ' LEFT ' : ' INNER '; | ||
| 295 | $sql .= 'JOIN ' . $this->quoteStrategy->getTableName($parentClass, $this->platform) . ' ' . $tableAlias . ' ON '; | ||
| 296 | |||
| 297 | $sqlParts = []; | ||
| 298 | |||
| 299 | foreach ($this->quoteStrategy->getIdentifierColumnNames($class, $this->platform) as $columnName) { | ||
| 300 | $sqlParts[] = $baseTableAlias . '.' . $columnName . ' = ' . $tableAlias . '.' . $columnName; | ||
| 301 | } | ||
| 302 | |||
| 303 | // Add filters on the root class | ||
| 304 | $sqlParts[] = $this->generateFilterConditionSQL($parentClass, $tableAlias); | ||
| 305 | |||
| 306 | $sql .= implode(' AND ', array_filter($sqlParts)); | ||
| 307 | } | ||
| 308 | |||
| 309 | // LEFT JOIN child class tables | ||
| 310 | foreach ($class->subClasses as $subClassName) { | ||
| 311 | $subClass = $this->em->getClassMetadata($subClassName); | ||
| 312 | $tableAlias = $this->getSQLTableAlias($subClass->getTableName(), $dqlAlias); | ||
| 313 | |||
| 314 | $sql .= ' LEFT JOIN ' . $this->quoteStrategy->getTableName($subClass, $this->platform) . ' ' . $tableAlias . ' ON '; | ||
| 315 | |||
| 316 | $sqlParts = []; | ||
| 317 | |||
| 318 | foreach ($this->quoteStrategy->getIdentifierColumnNames($subClass, $this->platform) as $columnName) { | ||
| 319 | $sqlParts[] = $baseTableAlias . '.' . $columnName . ' = ' . $tableAlias . '.' . $columnName; | ||
| 320 | } | ||
| 321 | |||
| 322 | $sql .= implode(' AND ', $sqlParts); | ||
| 323 | } | ||
| 324 | |||
| 325 | return $sql; | ||
| 326 | } | ||
| 327 | |||
| 328 | private function generateOrderedCollectionOrderByItems(): string | ||
| 329 | { | ||
| 330 | $orderedColumns = []; | ||
| 331 | |||
| 332 | foreach ($this->selectedClasses as $selectedClass) { | ||
| 333 | $dqlAlias = $selectedClass['dqlAlias']; | ||
| 334 | $qComp = $this->queryComponents[$dqlAlias]; | ||
| 335 | |||
| 336 | if (! isset($qComp['relation']->orderBy)) { | ||
| 337 | continue; | ||
| 338 | } | ||
| 339 | |||
| 340 | assert(isset($qComp['metadata'])); | ||
| 341 | $persister = $this->em->getUnitOfWork()->getEntityPersister($qComp['metadata']->name); | ||
| 342 | |||
| 343 | foreach ($qComp['relation']->orderBy as $fieldName => $orientation) { | ||
| 344 | $columnName = $this->quoteStrategy->getColumnName($fieldName, $qComp['metadata'], $this->platform); | ||
| 345 | $tableName = $qComp['metadata']->isInheritanceTypeJoined() | ||
| 346 | ? $persister->getOwningTable($fieldName) | ||
| 347 | : $qComp['metadata']->getTableName(); | ||
| 348 | |||
| 349 | $orderedColumn = $this->getSQLTableAlias($tableName, $dqlAlias) . '.' . $columnName; | ||
| 350 | |||
| 351 | // OrderByClause should replace an ordered relation. see - DDC-2475 | ||
| 352 | if (isset($this->orderedColumnsMap[$orderedColumn])) { | ||
| 353 | continue; | ||
| 354 | } | ||
| 355 | |||
| 356 | $this->orderedColumnsMap[$orderedColumn] = $orientation; | ||
| 357 | $orderedColumns[] = $orderedColumn . ' ' . $orientation; | ||
| 358 | } | ||
| 359 | } | ||
| 360 | |||
| 361 | return implode(', ', $orderedColumns); | ||
| 362 | } | ||
| 363 | |||
| 364 | /** | ||
| 365 | * Generates a discriminator column SQL condition for the class with the given DQL alias. | ||
| 366 | * | ||
| 367 | * @psalm-param list<string> $dqlAliases List of root DQL aliases to inspect for discriminator restrictions. | ||
| 368 | */ | ||
| 369 | private function generateDiscriminatorColumnConditionSQL(array $dqlAliases): string | ||
| 370 | { | ||
| 371 | $sqlParts = []; | ||
| 372 | |||
| 373 | foreach ($dqlAliases as $dqlAlias) { | ||
| 374 | $class = $this->getMetadataForDqlAlias($dqlAlias); | ||
| 375 | |||
| 376 | if (! $class->isInheritanceTypeSingleTable()) { | ||
| 377 | continue; | ||
| 378 | } | ||
| 379 | |||
| 380 | $sqlTableAlias = $this->useSqlTableAliases | ||
| 381 | ? $this->getSQLTableAlias($class->getTableName(), $dqlAlias) . '.' | ||
| 382 | : ''; | ||
| 383 | |||
| 384 | $conn = $this->em->getConnection(); | ||
| 385 | $values = []; | ||
| 386 | |||
| 387 | if ($class->discriminatorValue !== null) { // discriminators can be 0 | ||
| 388 | $values[] = $class->getDiscriminatorColumn()->type === 'integer' && is_int($class->discriminatorValue) | ||
| 389 | ? $class->discriminatorValue | ||
| 390 | : $conn->quote((string) $class->discriminatorValue); | ||
| 391 | } | ||
| 392 | |||
| 393 | foreach ($class->subClasses as $subclassName) { | ||
| 394 | $subclassMetadata = $this->em->getClassMetadata($subclassName); | ||
| 395 | |||
| 396 | // Abstract entity classes show up in the list of subClasses, but may be omitted | ||
| 397 | // from the discriminator map. In that case, they have a null discriminator value. | ||
| 398 | if ($subclassMetadata->discriminatorValue === null) { | ||
| 399 | continue; | ||
| 400 | } | ||
| 401 | |||
| 402 | $values[] = $subclassMetadata->getDiscriminatorColumn()->type === 'integer' && is_int($subclassMetadata->discriminatorValue) | ||
| 403 | ? $subclassMetadata->discriminatorValue | ||
| 404 | : $conn->quote((string) $subclassMetadata->discriminatorValue); | ||
| 405 | } | ||
| 406 | |||
| 407 | if ($values !== []) { | ||
| 408 | $sqlParts[] = $sqlTableAlias . $class->getDiscriminatorColumn()->name . ' IN (' . implode(', ', $values) . ')'; | ||
| 409 | } else { | ||
| 410 | $sqlParts[] = '1=0'; // impossible condition | ||
| 411 | } | ||
| 412 | } | ||
| 413 | |||
| 414 | $sql = implode(' AND ', $sqlParts); | ||
| 415 | |||
| 416 | return count($sqlParts) > 1 ? '(' . $sql . ')' : $sql; | ||
| 417 | } | ||
| 418 | |||
| 419 | /** | ||
| 420 | * Generates the filter SQL for a given entity and table alias. | ||
| 421 | */ | ||
| 422 | private function generateFilterConditionSQL( | ||
| 423 | ClassMetadata $targetEntity, | ||
| 424 | string $targetTableAlias, | ||
| 425 | ): string { | ||
| 426 | if (! $this->em->hasFilters()) { | ||
| 427 | return ''; | ||
| 428 | } | ||
| 429 | |||
| 430 | switch ($targetEntity->inheritanceType) { | ||
| 431 | case ClassMetadata::INHERITANCE_TYPE_NONE: | ||
| 432 | break; | ||
| 433 | case ClassMetadata::INHERITANCE_TYPE_JOINED: | ||
| 434 | // The classes in the inheritance will be added to the query one by one, | ||
| 435 | // but only the root node is getting filtered | ||
| 436 | if ($targetEntity->name !== $targetEntity->rootEntityName) { | ||
| 437 | return ''; | ||
| 438 | } | ||
| 439 | |||
| 440 | break; | ||
| 441 | case ClassMetadata::INHERITANCE_TYPE_SINGLE_TABLE: | ||
| 442 | // With STI the table will only be queried once, make sure that the filters | ||
| 443 | // are added to the root entity | ||
| 444 | $targetEntity = $this->em->getClassMetadata($targetEntity->rootEntityName); | ||
| 445 | break; | ||
| 446 | default: | ||
| 447 | //@todo: throw exception? | ||
| 448 | return ''; | ||
| 449 | } | ||
| 450 | |||
| 451 | $filterClauses = []; | ||
| 452 | foreach ($this->em->getFilters()->getEnabledFilters() as $filter) { | ||
| 453 | $filterExpr = $filter->addFilterConstraint($targetEntity, $targetTableAlias); | ||
| 454 | if ($filterExpr !== '') { | ||
| 455 | $filterClauses[] = '(' . $filterExpr . ')'; | ||
| 456 | } | ||
| 457 | } | ||
| 458 | |||
| 459 | return implode(' AND ', $filterClauses); | ||
| 460 | } | ||
| 461 | |||
| 462 | /** | ||
| 463 | * Walks down a SelectStatement AST node, thereby generating the appropriate SQL. | ||
| 464 | */ | ||
| 465 | public function walkSelectStatement(AST\SelectStatement $selectStatement): string | ||
| 466 | { | ||
| 467 | $limit = $this->query->getMaxResults(); | ||
| 468 | $offset = $this->query->getFirstResult(); | ||
| 469 | $lockMode = $this->query->getHint(Query::HINT_LOCK_MODE) ?: LockMode::NONE; | ||
| 470 | $sql = $this->walkSelectClause($selectStatement->selectClause) | ||
| 471 | . $this->walkFromClause($selectStatement->fromClause) | ||
| 472 | . $this->walkWhereClause($selectStatement->whereClause); | ||
| 473 | |||
| 474 | if ($selectStatement->groupByClause) { | ||
| 475 | $sql .= $this->walkGroupByClause($selectStatement->groupByClause); | ||
| 476 | } | ||
| 477 | |||
| 478 | if ($selectStatement->havingClause) { | ||
| 479 | $sql .= $this->walkHavingClause($selectStatement->havingClause); | ||
| 480 | } | ||
| 481 | |||
| 482 | if ($selectStatement->orderByClause) { | ||
| 483 | $sql .= $this->walkOrderByClause($selectStatement->orderByClause); | ||
| 484 | } | ||
| 485 | |||
| 486 | $orderBySql = $this->generateOrderedCollectionOrderByItems(); | ||
| 487 | if (! $selectStatement->orderByClause && $orderBySql) { | ||
| 488 | $sql .= ' ORDER BY ' . $orderBySql; | ||
| 489 | } | ||
| 490 | |||
| 491 | $sql = $this->platform->modifyLimitQuery($sql, $limit, $offset); | ||
| 492 | |||
| 493 | if ($lockMode === LockMode::NONE) { | ||
| 494 | return $sql; | ||
| 495 | } | ||
| 496 | |||
| 497 | if ($lockMode === LockMode::PESSIMISTIC_READ) { | ||
| 498 | return $sql . ' ' . $this->getReadLockSQL($this->platform); | ||
| 499 | } | ||
| 500 | |||
| 501 | if ($lockMode === LockMode::PESSIMISTIC_WRITE) { | ||
| 502 | return $sql . ' ' . $this->getWriteLockSQL($this->platform); | ||
| 503 | } | ||
| 504 | |||
| 505 | if ($lockMode !== LockMode::OPTIMISTIC) { | ||
| 506 | throw QueryException::invalidLockMode(); | ||
| 507 | } | ||
| 508 | |||
| 509 | foreach ($this->selectedClasses as $selectedClass) { | ||
| 510 | if (! $selectedClass['class']->isVersioned) { | ||
| 511 | throw OptimisticLockException::lockFailed($selectedClass['class']->name); | ||
| 512 | } | ||
| 513 | } | ||
| 514 | |||
| 515 | return $sql; | ||
| 516 | } | ||
| 517 | |||
| 518 | /** | ||
| 519 | * Walks down a UpdateStatement AST node, thereby generating the appropriate SQL. | ||
| 520 | */ | ||
| 521 | public function walkUpdateStatement(AST\UpdateStatement $updateStatement): string | ||
| 522 | { | ||
| 523 | $this->useSqlTableAliases = false; | ||
| 524 | $this->rsm->isSelect = false; | ||
| 525 | |||
| 526 | return $this->walkUpdateClause($updateStatement->updateClause) | ||
| 527 | . $this->walkWhereClause($updateStatement->whereClause); | ||
| 528 | } | ||
| 529 | |||
| 530 | /** | ||
| 531 | * Walks down a DeleteStatement AST node, thereby generating the appropriate SQL. | ||
| 532 | */ | ||
| 533 | public function walkDeleteStatement(AST\DeleteStatement $deleteStatement): string | ||
| 534 | { | ||
| 535 | $this->useSqlTableAliases = false; | ||
| 536 | $this->rsm->isSelect = false; | ||
| 537 | |||
| 538 | return $this->walkDeleteClause($deleteStatement->deleteClause) | ||
| 539 | . $this->walkWhereClause($deleteStatement->whereClause); | ||
| 540 | } | ||
| 541 | |||
| 542 | /** | ||
| 543 | * Walks down an IdentificationVariable AST node, thereby generating the appropriate SQL. | ||
| 544 | * This one differs of ->walkIdentificationVariable() because it generates the entity identifiers. | ||
| 545 | */ | ||
| 546 | public function walkEntityIdentificationVariable(string $identVariable): string | ||
| 547 | { | ||
| 548 | $class = $this->getMetadataForDqlAlias($identVariable); | ||
| 549 | $tableAlias = $this->getSQLTableAlias($class->getTableName(), $identVariable); | ||
| 550 | $sqlParts = []; | ||
| 551 | |||
| 552 | foreach ($this->quoteStrategy->getIdentifierColumnNames($class, $this->platform) as $columnName) { | ||
| 553 | $sqlParts[] = $tableAlias . '.' . $columnName; | ||
| 554 | } | ||
| 555 | |||
| 556 | return implode(', ', $sqlParts); | ||
| 557 | } | ||
| 558 | |||
| 559 | /** | ||
| 560 | * Walks down an IdentificationVariable (no AST node associated), thereby generating the SQL. | ||
| 561 | */ | ||
| 562 | public function walkIdentificationVariable(string $identificationVariable, string|null $fieldName = null): string | ||
| 563 | { | ||
| 564 | $class = $this->getMetadataForDqlAlias($identificationVariable); | ||
| 565 | |||
| 566 | if ( | ||
| 567 | $fieldName !== null && $class->isInheritanceTypeJoined() && | ||
| 568 | isset($class->fieldMappings[$fieldName]->inherited) | ||
| 569 | ) { | ||
| 570 | $class = $this->em->getClassMetadata($class->fieldMappings[$fieldName]->inherited); | ||
| 571 | } | ||
| 572 | |||
| 573 | return $this->getSQLTableAlias($class->getTableName(), $identificationVariable); | ||
| 574 | } | ||
| 575 | |||
| 576 | /** | ||
| 577 | * Walks down a PathExpression AST node, thereby generating the appropriate SQL. | ||
| 578 | */ | ||
| 579 | public function walkPathExpression(AST\PathExpression $pathExpr): string | ||
| 580 | { | ||
| 581 | $sql = ''; | ||
| 582 | assert($pathExpr->field !== null); | ||
| 583 | |||
| 584 | switch ($pathExpr->type) { | ||
| 585 | case AST\PathExpression::TYPE_STATE_FIELD: | ||
| 586 | $fieldName = $pathExpr->field; | ||
| 587 | $dqlAlias = $pathExpr->identificationVariable; | ||
| 588 | $class = $this->getMetadataForDqlAlias($dqlAlias); | ||
| 589 | |||
| 590 | if ($this->useSqlTableAliases) { | ||
| 591 | $sql .= $this->walkIdentificationVariable($dqlAlias, $fieldName) . '.'; | ||
| 592 | } | ||
| 593 | |||
| 594 | $sql .= $this->quoteStrategy->getColumnName($fieldName, $class, $this->platform); | ||
| 595 | break; | ||
| 596 | |||
| 597 | case AST\PathExpression::TYPE_SINGLE_VALUED_ASSOCIATION: | ||
| 598 | // 1- the owning side: | ||
| 599 | // Just use the foreign key, i.e. u.group_id | ||
| 600 | $fieldName = $pathExpr->field; | ||
| 601 | $dqlAlias = $pathExpr->identificationVariable; | ||
| 602 | $class = $this->getMetadataForDqlAlias($dqlAlias); | ||
| 603 | |||
| 604 | if (isset($class->associationMappings[$fieldName]->inherited)) { | ||
| 605 | $class = $this->em->getClassMetadata($class->associationMappings[$fieldName]->inherited); | ||
| 606 | } | ||
| 607 | |||
| 608 | $assoc = $class->associationMappings[$fieldName]; | ||
| 609 | |||
| 610 | if (! $assoc->isOwningSide()) { | ||
| 611 | throw QueryException::associationPathInverseSideNotSupported($pathExpr); | ||
| 612 | } | ||
| 613 | |||
| 614 | assert($assoc->isToOneOwningSide()); | ||
| 615 | |||
| 616 | // COMPOSITE KEYS NOT (YET?) SUPPORTED | ||
| 617 | if (count($assoc->sourceToTargetKeyColumns) > 1) { | ||
| 618 | throw QueryException::associationPathCompositeKeyNotSupported(); | ||
| 619 | } | ||
| 620 | |||
| 621 | if ($this->useSqlTableAliases) { | ||
| 622 | $sql .= $this->getSQLTableAlias($class->getTableName(), $dqlAlias) . '.'; | ||
| 623 | } | ||
| 624 | |||
| 625 | $sql .= reset($assoc->targetToSourceKeyColumns); | ||
| 626 | break; | ||
| 627 | |||
| 628 | default: | ||
| 629 | throw QueryException::invalidPathExpression($pathExpr); | ||
| 630 | } | ||
| 631 | |||
| 632 | return $sql; | ||
| 633 | } | ||
| 634 | |||
| 635 | /** | ||
| 636 | * Walks down a SelectClause AST node, thereby generating the appropriate SQL. | ||
| 637 | */ | ||
| 638 | public function walkSelectClause(AST\SelectClause $selectClause): string | ||
| 639 | { | ||
| 640 | $sql = 'SELECT ' . ($selectClause->isDistinct ? 'DISTINCT ' : ''); | ||
| 641 | $sqlSelectExpressions = array_filter(array_map($this->walkSelectExpression(...), $selectClause->selectExpressions)); | ||
| 642 | |||
| 643 | if ($this->query->getHint(Query::HINT_INTERNAL_ITERATION) === true && $selectClause->isDistinct) { | ||
| 644 | $this->query->setHint(self::HINT_DISTINCT, true); | ||
| 645 | } | ||
| 646 | |||
| 647 | $addMetaColumns = $this->query->getHydrationMode() === Query::HYDRATE_OBJECT | ||
| 648 | || $this->query->getHint(Query::HINT_INCLUDE_META_COLUMNS); | ||
| 649 | |||
| 650 | foreach ($this->selectedClasses as $selectedClass) { | ||
| 651 | $class = $selectedClass['class']; | ||
| 652 | $dqlAlias = $selectedClass['dqlAlias']; | ||
| 653 | $resultAlias = $selectedClass['resultAlias']; | ||
| 654 | |||
| 655 | // Register as entity or joined entity result | ||
| 656 | if (! isset($this->queryComponents[$dqlAlias]['relation'])) { | ||
| 657 | $this->rsm->addEntityResult($class->name, $dqlAlias, $resultAlias); | ||
| 658 | } else { | ||
| 659 | assert(isset($this->queryComponents[$dqlAlias]['parent'])); | ||
| 660 | |||
| 661 | $this->rsm->addJoinedEntityResult( | ||
| 662 | $class->name, | ||
| 663 | $dqlAlias, | ||
| 664 | $this->queryComponents[$dqlAlias]['parent'], | ||
| 665 | $this->queryComponents[$dqlAlias]['relation']->fieldName, | ||
| 666 | ); | ||
| 667 | } | ||
| 668 | |||
| 669 | if ($class->isInheritanceTypeSingleTable() || $class->isInheritanceTypeJoined()) { | ||
| 670 | // Add discriminator columns to SQL | ||
| 671 | $rootClass = $this->em->getClassMetadata($class->rootEntityName); | ||
| 672 | $tblAlias = $this->getSQLTableAlias($rootClass->getTableName(), $dqlAlias); | ||
| 673 | $discrColumn = $rootClass->getDiscriminatorColumn(); | ||
| 674 | $columnAlias = $this->getSQLColumnAlias($discrColumn->name); | ||
| 675 | |||
| 676 | $sqlSelectExpressions[] = $tblAlias . '.' . $discrColumn->name . ' AS ' . $columnAlias; | ||
| 677 | |||
| 678 | $this->rsm->setDiscriminatorColumn($dqlAlias, $columnAlias); | ||
| 679 | $this->rsm->addMetaResult($dqlAlias, $columnAlias, $discrColumn->fieldName, false, $discrColumn->type); | ||
| 680 | if (! empty($discrColumn->enumType)) { | ||
| 681 | $this->rsm->addEnumResult($columnAlias, $discrColumn->enumType); | ||
| 682 | } | ||
| 683 | } | ||
| 684 | |||
| 685 | // Add foreign key columns to SQL, if necessary | ||
| 686 | if (! $addMetaColumns && ! $class->containsForeignIdentifier) { | ||
| 687 | continue; | ||
| 688 | } | ||
| 689 | |||
| 690 | // Add foreign key columns of class and also parent classes | ||
| 691 | foreach ($class->associationMappings as $assoc) { | ||
| 692 | if ( | ||
| 693 | ! $assoc->isToOneOwningSide() | ||
| 694 | || ( ! $addMetaColumns && ! isset($assoc->id)) | ||
| 695 | ) { | ||
| 696 | continue; | ||
| 697 | } | ||
| 698 | |||
| 699 | $targetClass = $this->em->getClassMetadata($assoc->targetEntity); | ||
| 700 | $isIdentifier = (isset($assoc->id) && $assoc->id === true); | ||
| 701 | $owningClass = isset($assoc->inherited) ? $this->em->getClassMetadata($assoc->inherited) : $class; | ||
| 702 | $sqlTableAlias = $this->getSQLTableAlias($owningClass->getTableName(), $dqlAlias); | ||
| 703 | |||
| 704 | foreach ($assoc->joinColumns as $joinColumn) { | ||
| 705 | $columnName = $joinColumn->name; | ||
| 706 | $columnAlias = $this->getSQLColumnAlias($columnName); | ||
| 707 | $columnType = PersisterHelper::getTypeOfColumn($joinColumn->referencedColumnName, $targetClass, $this->em); | ||
| 708 | |||
| 709 | $quotedColumnName = $this->quoteStrategy->getJoinColumnName($joinColumn, $class, $this->platform); | ||
| 710 | $sqlSelectExpressions[] = $sqlTableAlias . '.' . $quotedColumnName . ' AS ' . $columnAlias; | ||
| 711 | |||
| 712 | $this->rsm->addMetaResult($dqlAlias, $columnAlias, $columnName, $isIdentifier, $columnType); | ||
| 713 | } | ||
| 714 | } | ||
| 715 | |||
| 716 | // Add foreign key columns to SQL, if necessary | ||
| 717 | if (! $addMetaColumns) { | ||
| 718 | continue; | ||
| 719 | } | ||
| 720 | |||
| 721 | // Add foreign key columns of subclasses | ||
| 722 | foreach ($class->subClasses as $subClassName) { | ||
| 723 | $subClass = $this->em->getClassMetadata($subClassName); | ||
| 724 | $sqlTableAlias = $this->getSQLTableAlias($subClass->getTableName(), $dqlAlias); | ||
| 725 | |||
| 726 | foreach ($subClass->associationMappings as $assoc) { | ||
| 727 | // Skip if association is inherited | ||
| 728 | if (isset($assoc->inherited)) { | ||
| 729 | continue; | ||
| 730 | } | ||
| 731 | |||
| 732 | if ($assoc->isToOneOwningSide()) { | ||
| 733 | $targetClass = $this->em->getClassMetadata($assoc->targetEntity); | ||
| 734 | |||
| 735 | foreach ($assoc->joinColumns as $joinColumn) { | ||
| 736 | $columnName = $joinColumn->name; | ||
| 737 | $columnAlias = $this->getSQLColumnAlias($columnName); | ||
| 738 | $columnType = PersisterHelper::getTypeOfColumn($joinColumn->referencedColumnName, $targetClass, $this->em); | ||
| 739 | |||
| 740 | $quotedColumnName = $this->quoteStrategy->getJoinColumnName($joinColumn, $subClass, $this->platform); | ||
| 741 | $sqlSelectExpressions[] = $sqlTableAlias . '.' . $quotedColumnName . ' AS ' . $columnAlias; | ||
| 742 | |||
| 743 | $this->rsm->addMetaResult($dqlAlias, $columnAlias, $columnName, $subClass->isIdentifier($columnName), $columnType); | ||
| 744 | } | ||
| 745 | } | ||
| 746 | } | ||
| 747 | } | ||
| 748 | } | ||
| 749 | |||
| 750 | return $sql . implode(', ', $sqlSelectExpressions); | ||
| 751 | } | ||
| 752 | |||
| 753 | /** | ||
| 754 | * Walks down a FromClause AST node, thereby generating the appropriate SQL. | ||
| 755 | */ | ||
| 756 | public function walkFromClause(AST\FromClause $fromClause): string | ||
| 757 | { | ||
| 758 | $identificationVarDecls = $fromClause->identificationVariableDeclarations; | ||
| 759 | $sqlParts = []; | ||
| 760 | |||
| 761 | foreach ($identificationVarDecls as $identificationVariableDecl) { | ||
| 762 | $sqlParts[] = $this->walkIdentificationVariableDeclaration($identificationVariableDecl); | ||
| 763 | } | ||
| 764 | |||
| 765 | return ' FROM ' . implode(', ', $sqlParts); | ||
| 766 | } | ||
| 767 | |||
| 768 | /** | ||
| 769 | * Walks down a IdentificationVariableDeclaration AST node, thereby generating the appropriate SQL. | ||
| 770 | */ | ||
| 771 | public function walkIdentificationVariableDeclaration(AST\IdentificationVariableDeclaration $identificationVariableDecl): string | ||
| 772 | { | ||
| 773 | $sql = $this->walkRangeVariableDeclaration($identificationVariableDecl->rangeVariableDeclaration); | ||
| 774 | |||
| 775 | if ($identificationVariableDecl->indexBy) { | ||
| 776 | $this->walkIndexBy($identificationVariableDecl->indexBy); | ||
| 777 | } | ||
| 778 | |||
| 779 | foreach ($identificationVariableDecl->joins as $join) { | ||
| 780 | $sql .= $this->walkJoin($join); | ||
| 781 | } | ||
| 782 | |||
| 783 | return $sql; | ||
| 784 | } | ||
| 785 | |||
| 786 | /** | ||
| 787 | * Walks down a IndexBy AST node. | ||
| 788 | */ | ||
| 789 | public function walkIndexBy(AST\IndexBy $indexBy): void | ||
| 790 | { | ||
| 791 | $pathExpression = $indexBy->singleValuedPathExpression; | ||
| 792 | $alias = $pathExpression->identificationVariable; | ||
| 793 | assert($pathExpression->field !== null); | ||
| 794 | |||
| 795 | switch ($pathExpression->type) { | ||
| 796 | case AST\PathExpression::TYPE_STATE_FIELD: | ||
| 797 | $field = $pathExpression->field; | ||
| 798 | break; | ||
| 799 | |||
| 800 | case AST\PathExpression::TYPE_SINGLE_VALUED_ASSOCIATION: | ||
| 801 | // Just use the foreign key, i.e. u.group_id | ||
| 802 | $fieldName = $pathExpression->field; | ||
| 803 | $class = $this->getMetadataForDqlAlias($alias); | ||
| 804 | |||
| 805 | if (isset($class->associationMappings[$fieldName]->inherited)) { | ||
| 806 | $class = $this->em->getClassMetadata($class->associationMappings[$fieldName]->inherited); | ||
| 807 | } | ||
| 808 | |||
| 809 | $association = $class->associationMappings[$fieldName]; | ||
| 810 | |||
| 811 | if (! $association->isOwningSide()) { | ||
| 812 | throw QueryException::associationPathInverseSideNotSupported($pathExpression); | ||
| 813 | } | ||
| 814 | |||
| 815 | assert($association->isToOneOwningSide()); | ||
| 816 | |||
| 817 | if (count($association->sourceToTargetKeyColumns) > 1) { | ||
| 818 | throw QueryException::associationPathCompositeKeyNotSupported(); | ||
| 819 | } | ||
| 820 | |||
| 821 | $field = reset($association->targetToSourceKeyColumns); | ||
| 822 | break; | ||
| 823 | |||
| 824 | default: | ||
| 825 | throw QueryException::invalidPathExpression($pathExpression); | ||
| 826 | } | ||
| 827 | |||
| 828 | if (isset($this->scalarFields[$alias][$field])) { | ||
| 829 | $this->rsm->addIndexByScalar($this->scalarFields[$alias][$field]); | ||
| 830 | |||
| 831 | return; | ||
| 832 | } | ||
| 833 | |||
| 834 | $this->rsm->addIndexBy($alias, $field); | ||
| 835 | } | ||
| 836 | |||
| 837 | /** | ||
| 838 | * Walks down a RangeVariableDeclaration AST node, thereby generating the appropriate SQL. | ||
| 839 | */ | ||
| 840 | public function walkRangeVariableDeclaration(AST\RangeVariableDeclaration $rangeVariableDeclaration): string | ||
| 841 | { | ||
| 842 | return $this->generateRangeVariableDeclarationSQL($rangeVariableDeclaration, false); | ||
| 843 | } | ||
| 844 | |||
| 845 | /** | ||
| 846 | * Generate appropriate SQL for RangeVariableDeclaration AST node | ||
| 847 | */ | ||
| 848 | private function generateRangeVariableDeclarationSQL( | ||
| 849 | AST\RangeVariableDeclaration $rangeVariableDeclaration, | ||
| 850 | bool $buildNestedJoins, | ||
| 851 | ): string { | ||
| 852 | $class = $this->em->getClassMetadata($rangeVariableDeclaration->abstractSchemaName); | ||
| 853 | $dqlAlias = $rangeVariableDeclaration->aliasIdentificationVariable; | ||
| 854 | |||
| 855 | if ($rangeVariableDeclaration->isRoot) { | ||
| 856 | $this->rootAliases[] = $dqlAlias; | ||
| 857 | } | ||
| 858 | |||
| 859 | $sql = $this->platform->appendLockHint( | ||
| 860 | $this->quoteStrategy->getTableName($class, $this->platform) . ' ' . | ||
| 861 | $this->getSQLTableAlias($class->getTableName(), $dqlAlias), | ||
| 862 | $this->query->getHint(Query::HINT_LOCK_MODE) ?: LockMode::NONE, | ||
| 863 | ); | ||
| 864 | |||
| 865 | if (! $class->isInheritanceTypeJoined()) { | ||
| 866 | return $sql; | ||
| 867 | } | ||
| 868 | |||
| 869 | $classTableInheritanceJoins = $this->generateClassTableInheritanceJoins($class, $dqlAlias); | ||
| 870 | |||
| 871 | if (! $buildNestedJoins) { | ||
| 872 | return $sql . $classTableInheritanceJoins; | ||
| 873 | } | ||
| 874 | |||
| 875 | return $classTableInheritanceJoins === '' ? $sql : '(' . $sql . $classTableInheritanceJoins . ')'; | ||
| 876 | } | ||
| 877 | |||
| 878 | /** | ||
| 879 | * Walks down a JoinAssociationDeclaration AST node, thereby generating the appropriate SQL. | ||
| 880 | * | ||
| 881 | * @psalm-param AST\Join::JOIN_TYPE_* $joinType | ||
| 882 | * | ||
| 883 | * @throws QueryException | ||
| 884 | */ | ||
| 885 | public function walkJoinAssociationDeclaration( | ||
| 886 | AST\JoinAssociationDeclaration $joinAssociationDeclaration, | ||
| 887 | int $joinType = AST\Join::JOIN_TYPE_INNER, | ||
| 888 | AST\ConditionalExpression|AST\Phase2OptimizableConditional|null $condExpr = null, | ||
| 889 | ): string { | ||
| 890 | $sql = ''; | ||
| 891 | |||
| 892 | $associationPathExpression = $joinAssociationDeclaration->joinAssociationPathExpression; | ||
| 893 | $joinedDqlAlias = $joinAssociationDeclaration->aliasIdentificationVariable; | ||
| 894 | $indexBy = $joinAssociationDeclaration->indexBy; | ||
| 895 | |||
| 896 | $relation = $this->queryComponents[$joinedDqlAlias]['relation'] ?? null; | ||
| 897 | assert($relation !== null); | ||
| 898 | $targetClass = $this->em->getClassMetadata($relation->targetEntity); | ||
| 899 | $sourceClass = $this->em->getClassMetadata($relation->sourceEntity); | ||
| 900 | $targetTableName = $this->quoteStrategy->getTableName($targetClass, $this->platform); | ||
| 901 | |||
| 902 | $targetTableAlias = $this->getSQLTableAlias($targetClass->getTableName(), $joinedDqlAlias); | ||
| 903 | $sourceTableAlias = $this->getSQLTableAlias($sourceClass->getTableName(), $associationPathExpression->identificationVariable); | ||
| 904 | |||
| 905 | // Ensure we got the owning side, since it has all mapping info | ||
| 906 | $assoc = $this->em->getMetadataFactory()->getOwningSide($relation); | ||
| 907 | |||
| 908 | if ($this->query->getHint(Query::HINT_INTERNAL_ITERATION) === true && (! $this->query->getHint(self::HINT_DISTINCT) || isset($this->selectedClasses[$joinedDqlAlias]))) { | ||
| 909 | if ($relation->isToMany()) { | ||
| 910 | throw QueryException::iterateWithFetchJoinNotAllowed($assoc); | ||
| 911 | } | ||
| 912 | } | ||
| 913 | |||
| 914 | $fetchMode = $this->query->getHint('fetchMode')[$assoc->sourceEntity][$assoc->fieldName] ?? $relation->fetch; | ||
| 915 | |||
| 916 | if ($fetchMode === ClassMetadata::FETCH_EAGER && $condExpr !== null) { | ||
| 917 | throw QueryException::eagerFetchJoinWithNotAllowed($assoc->sourceEntity, $assoc->fieldName); | ||
| 918 | } | ||
| 919 | |||
| 920 | // This condition is not checking ClassMetadata::MANY_TO_ONE, because by definition it cannot | ||
| 921 | // be the owning side and previously we ensured that $assoc is always the owning side of the associations. | ||
| 922 | // The owning side is necessary at this point because only it contains the JoinColumn information. | ||
| 923 | switch (true) { | ||
| 924 | case $assoc->isToOne(): | ||
| 925 | assert($assoc->isToOneOwningSide()); | ||
| 926 | $conditions = []; | ||
| 927 | |||
| 928 | foreach ($assoc->joinColumns as $joinColumn) { | ||
| 929 | $quotedSourceColumn = $this->quoteStrategy->getJoinColumnName($joinColumn, $targetClass, $this->platform); | ||
| 930 | $quotedTargetColumn = $this->quoteStrategy->getReferencedJoinColumnName($joinColumn, $targetClass, $this->platform); | ||
| 931 | |||
| 932 | if ($relation->isOwningSide()) { | ||
| 933 | $conditions[] = $sourceTableAlias . '.' . $quotedSourceColumn . ' = ' . $targetTableAlias . '.' . $quotedTargetColumn; | ||
| 934 | |||
| 935 | continue; | ||
| 936 | } | ||
| 937 | |||
| 938 | $conditions[] = $sourceTableAlias . '.' . $quotedTargetColumn . ' = ' . $targetTableAlias . '.' . $quotedSourceColumn; | ||
| 939 | } | ||
| 940 | |||
| 941 | // Apply remaining inheritance restrictions | ||
| 942 | $discrSql = $this->generateDiscriminatorColumnConditionSQL([$joinedDqlAlias]); | ||
| 943 | |||
| 944 | if ($discrSql) { | ||
| 945 | $conditions[] = $discrSql; | ||
| 946 | } | ||
| 947 | |||
| 948 | // Apply the filters | ||
| 949 | $filterExpr = $this->generateFilterConditionSQL($targetClass, $targetTableAlias); | ||
| 950 | |||
| 951 | if ($filterExpr) { | ||
| 952 | $conditions[] = $filterExpr; | ||
| 953 | } | ||
| 954 | |||
| 955 | $targetTableJoin = [ | ||
| 956 | 'table' => $targetTableName . ' ' . $targetTableAlias, | ||
| 957 | 'condition' => implode(' AND ', $conditions), | ||
| 958 | ]; | ||
| 959 | break; | ||
| 960 | |||
| 961 | case $assoc->isManyToMany(): | ||
| 962 | // Join relation table | ||
| 963 | $joinTable = $assoc->joinTable; | ||
| 964 | $joinTableAlias = $this->getSQLTableAlias($joinTable->name, $joinedDqlAlias); | ||
| 965 | $joinTableName = $this->quoteStrategy->getJoinTableName($assoc, $sourceClass, $this->platform); | ||
| 966 | |||
| 967 | $conditions = []; | ||
| 968 | $relationColumns = $relation->isOwningSide() | ||
| 969 | ? $assoc->joinTable->joinColumns | ||
| 970 | : $assoc->joinTable->inverseJoinColumns; | ||
| 971 | |||
| 972 | foreach ($relationColumns as $joinColumn) { | ||
| 973 | $quotedSourceColumn = $this->quoteStrategy->getJoinColumnName($joinColumn, $targetClass, $this->platform); | ||
| 974 | $quotedTargetColumn = $this->quoteStrategy->getReferencedJoinColumnName($joinColumn, $targetClass, $this->platform); | ||
| 975 | |||
| 976 | $conditions[] = $sourceTableAlias . '.' . $quotedTargetColumn . ' = ' . $joinTableAlias . '.' . $quotedSourceColumn; | ||
| 977 | } | ||
| 978 | |||
| 979 | $sql .= $joinTableName . ' ' . $joinTableAlias . ' ON ' . implode(' AND ', $conditions); | ||
| 980 | |||
| 981 | // Join target table | ||
| 982 | $sql .= $joinType === AST\Join::JOIN_TYPE_LEFT || $joinType === AST\Join::JOIN_TYPE_LEFTOUTER ? ' LEFT JOIN ' : ' INNER JOIN '; | ||
| 983 | |||
| 984 | $conditions = []; | ||
| 985 | $relationColumns = $relation->isOwningSide() | ||
| 986 | ? $assoc->joinTable->inverseJoinColumns | ||
| 987 | : $assoc->joinTable->joinColumns; | ||
| 988 | |||
| 989 | foreach ($relationColumns as $joinColumn) { | ||
| 990 | $quotedSourceColumn = $this->quoteStrategy->getJoinColumnName($joinColumn, $targetClass, $this->platform); | ||
| 991 | $quotedTargetColumn = $this->quoteStrategy->getReferencedJoinColumnName($joinColumn, $targetClass, $this->platform); | ||
| 992 | |||
| 993 | $conditions[] = $targetTableAlias . '.' . $quotedTargetColumn . ' = ' . $joinTableAlias . '.' . $quotedSourceColumn; | ||
| 994 | } | ||
| 995 | |||
| 996 | // Apply remaining inheritance restrictions | ||
| 997 | $discrSql = $this->generateDiscriminatorColumnConditionSQL([$joinedDqlAlias]); | ||
| 998 | |||
| 999 | if ($discrSql) { | ||
| 1000 | $conditions[] = $discrSql; | ||
| 1001 | } | ||
| 1002 | |||
| 1003 | // Apply the filters | ||
| 1004 | $filterExpr = $this->generateFilterConditionSQL($targetClass, $targetTableAlias); | ||
| 1005 | |||
| 1006 | if ($filterExpr) { | ||
| 1007 | $conditions[] = $filterExpr; | ||
| 1008 | } | ||
| 1009 | |||
| 1010 | $targetTableJoin = [ | ||
| 1011 | 'table' => $targetTableName . ' ' . $targetTableAlias, | ||
| 1012 | 'condition' => implode(' AND ', $conditions), | ||
| 1013 | ]; | ||
| 1014 | break; | ||
| 1015 | |||
| 1016 | default: | ||
| 1017 | throw new BadMethodCallException('Type of association must be one of *_TO_ONE or MANY_TO_MANY'); | ||
| 1018 | } | ||
| 1019 | |||
| 1020 | // Handle WITH clause | ||
| 1021 | $withCondition = $condExpr === null ? '' : ('(' . $this->walkConditionalExpression($condExpr) . ')'); | ||
| 1022 | |||
| 1023 | if ($targetClass->isInheritanceTypeJoined()) { | ||
| 1024 | $ctiJoins = $this->generateClassTableInheritanceJoins($targetClass, $joinedDqlAlias); | ||
| 1025 | // If we have WITH condition, we need to build nested joins for target class table and cti joins | ||
| 1026 | if ($withCondition && $ctiJoins) { | ||
| 1027 | $sql .= '(' . $targetTableJoin['table'] . $ctiJoins . ') ON ' . $targetTableJoin['condition']; | ||
| 1028 | } else { | ||
| 1029 | $sql .= $targetTableJoin['table'] . ' ON ' . $targetTableJoin['condition'] . $ctiJoins; | ||
| 1030 | } | ||
| 1031 | } else { | ||
| 1032 | $sql .= $targetTableJoin['table'] . ' ON ' . $targetTableJoin['condition']; | ||
| 1033 | } | ||
| 1034 | |||
| 1035 | if ($withCondition) { | ||
| 1036 | $sql .= ' AND ' . $withCondition; | ||
| 1037 | } | ||
| 1038 | |||
| 1039 | // Apply the indexes | ||
| 1040 | if ($indexBy) { | ||
| 1041 | // For Many-To-One or One-To-One associations this obviously makes no sense, but is ignored silently. | ||
| 1042 | $this->walkIndexBy($indexBy); | ||
| 1043 | } elseif ($relation->isIndexed()) { | ||
| 1044 | $this->rsm->addIndexBy($joinedDqlAlias, $relation->indexBy()); | ||
| 1045 | } | ||
| 1046 | |||
| 1047 | return $sql; | ||
| 1048 | } | ||
| 1049 | |||
| 1050 | /** | ||
| 1051 | * Walks down a FunctionNode AST node, thereby generating the appropriate SQL. | ||
| 1052 | */ | ||
| 1053 | public function walkFunction(AST\Functions\FunctionNode $function): string | ||
| 1054 | { | ||
| 1055 | return $function->getSql($this); | ||
| 1056 | } | ||
| 1057 | |||
| 1058 | /** | ||
| 1059 | * Walks down an OrderByClause AST node, thereby generating the appropriate SQL. | ||
| 1060 | */ | ||
| 1061 | public function walkOrderByClause(AST\OrderByClause $orderByClause): string | ||
| 1062 | { | ||
| 1063 | $orderByItems = array_map($this->walkOrderByItem(...), $orderByClause->orderByItems); | ||
| 1064 | |||
| 1065 | $collectionOrderByItems = $this->generateOrderedCollectionOrderByItems(); | ||
| 1066 | if ($collectionOrderByItems !== '') { | ||
| 1067 | $orderByItems = array_merge($orderByItems, (array) $collectionOrderByItems); | ||
| 1068 | } | ||
| 1069 | |||
| 1070 | return ' ORDER BY ' . implode(', ', $orderByItems); | ||
| 1071 | } | ||
| 1072 | |||
| 1073 | /** | ||
| 1074 | * Walks down an OrderByItem AST node, thereby generating the appropriate SQL. | ||
| 1075 | */ | ||
| 1076 | public function walkOrderByItem(AST\OrderByItem $orderByItem): string | ||
| 1077 | { | ||
| 1078 | $type = strtoupper($orderByItem->type); | ||
| 1079 | $expr = $orderByItem->expression; | ||
| 1080 | $sql = $expr instanceof AST\Node | ||
| 1081 | ? $expr->dispatch($this) | ||
| 1082 | : $this->walkResultVariable($this->queryComponents[$expr]['token']->value); | ||
| 1083 | |||
| 1084 | $this->orderedColumnsMap[$sql] = $type; | ||
| 1085 | |||
| 1086 | if ($expr instanceof AST\Subselect) { | ||
| 1087 | return '(' . $sql . ') ' . $type; | ||
| 1088 | } | ||
| 1089 | |||
| 1090 | return $sql . ' ' . $type; | ||
| 1091 | } | ||
| 1092 | |||
| 1093 | /** | ||
| 1094 | * Walks down a HavingClause AST node, thereby generating the appropriate SQL. | ||
| 1095 | */ | ||
| 1096 | public function walkHavingClause(AST\HavingClause $havingClause): string | ||
| 1097 | { | ||
| 1098 | return ' HAVING ' . $this->walkConditionalExpression($havingClause->conditionalExpression); | ||
| 1099 | } | ||
| 1100 | |||
| 1101 | /** | ||
| 1102 | * Walks down a Join AST node and creates the corresponding SQL. | ||
| 1103 | */ | ||
| 1104 | public function walkJoin(AST\Join $join): string | ||
| 1105 | { | ||
| 1106 | $joinType = $join->joinType; | ||
| 1107 | $joinDeclaration = $join->joinAssociationDeclaration; | ||
| 1108 | |||
| 1109 | $sql = $joinType === AST\Join::JOIN_TYPE_LEFT || $joinType === AST\Join::JOIN_TYPE_LEFTOUTER | ||
| 1110 | ? ' LEFT JOIN ' | ||
| 1111 | : ' INNER JOIN '; | ||
| 1112 | |||
| 1113 | switch (true) { | ||
| 1114 | case $joinDeclaration instanceof AST\RangeVariableDeclaration: | ||
| 1115 | $class = $this->em->getClassMetadata($joinDeclaration->abstractSchemaName); | ||
| 1116 | $dqlAlias = $joinDeclaration->aliasIdentificationVariable; | ||
| 1117 | $tableAlias = $this->getSQLTableAlias($class->table['name'], $dqlAlias); | ||
| 1118 | $conditions = []; | ||
| 1119 | |||
| 1120 | if ($join->conditionalExpression) { | ||
| 1121 | $conditions[] = '(' . $this->walkConditionalExpression($join->conditionalExpression) . ')'; | ||
| 1122 | } | ||
| 1123 | |||
| 1124 | $isUnconditionalJoin = $conditions === []; | ||
| 1125 | $condExprConjunction = $class->isInheritanceTypeJoined() && $joinType !== AST\Join::JOIN_TYPE_LEFT && $joinType !== AST\Join::JOIN_TYPE_LEFTOUTER && $isUnconditionalJoin | ||
| 1126 | ? ' AND ' | ||
| 1127 | : ' ON '; | ||
| 1128 | |||
| 1129 | $sql .= $this->generateRangeVariableDeclarationSQL($joinDeclaration, ! $isUnconditionalJoin); | ||
| 1130 | |||
| 1131 | // Apply remaining inheritance restrictions | ||
| 1132 | $discrSql = $this->generateDiscriminatorColumnConditionSQL([$dqlAlias]); | ||
| 1133 | |||
| 1134 | if ($discrSql) { | ||
| 1135 | $conditions[] = $discrSql; | ||
| 1136 | } | ||
| 1137 | |||
| 1138 | // Apply the filters | ||
| 1139 | $filterExpr = $this->generateFilterConditionSQL($class, $tableAlias); | ||
| 1140 | |||
| 1141 | if ($filterExpr) { | ||
| 1142 | $conditions[] = $filterExpr; | ||
| 1143 | } | ||
| 1144 | |||
| 1145 | if ($conditions) { | ||
| 1146 | $sql .= $condExprConjunction . implode(' AND ', $conditions); | ||
| 1147 | } | ||
| 1148 | |||
| 1149 | break; | ||
| 1150 | |||
| 1151 | case $joinDeclaration instanceof AST\JoinAssociationDeclaration: | ||
| 1152 | $sql .= $this->walkJoinAssociationDeclaration($joinDeclaration, $joinType, $join->conditionalExpression); | ||
| 1153 | break; | ||
| 1154 | } | ||
| 1155 | |||
| 1156 | return $sql; | ||
| 1157 | } | ||
| 1158 | |||
| 1159 | /** | ||
| 1160 | * Walks down a CoalesceExpression AST node and generates the corresponding SQL. | ||
| 1161 | */ | ||
| 1162 | public function walkCoalesceExpression(AST\CoalesceExpression $coalesceExpression): string | ||
| 1163 | { | ||
| 1164 | $sql = 'COALESCE('; | ||
| 1165 | |||
| 1166 | $scalarExpressions = []; | ||
| 1167 | |||
| 1168 | foreach ($coalesceExpression->scalarExpressions as $scalarExpression) { | ||
| 1169 | $scalarExpressions[] = $this->walkSimpleArithmeticExpression($scalarExpression); | ||
| 1170 | } | ||
| 1171 | |||
| 1172 | return $sql . implode(', ', $scalarExpressions) . ')'; | ||
| 1173 | } | ||
| 1174 | |||
| 1175 | /** | ||
| 1176 | * Walks down a NullIfExpression AST node and generates the corresponding SQL. | ||
| 1177 | */ | ||
| 1178 | public function walkNullIfExpression(AST\NullIfExpression $nullIfExpression): string | ||
| 1179 | { | ||
| 1180 | $firstExpression = is_string($nullIfExpression->firstExpression) | ||
| 1181 | ? $this->conn->quote($nullIfExpression->firstExpression) | ||
| 1182 | : $this->walkSimpleArithmeticExpression($nullIfExpression->firstExpression); | ||
| 1183 | |||
| 1184 | $secondExpression = is_string($nullIfExpression->secondExpression) | ||
| 1185 | ? $this->conn->quote($nullIfExpression->secondExpression) | ||
| 1186 | : $this->walkSimpleArithmeticExpression($nullIfExpression->secondExpression); | ||
| 1187 | |||
| 1188 | return 'NULLIF(' . $firstExpression . ', ' . $secondExpression . ')'; | ||
| 1189 | } | ||
| 1190 | |||
| 1191 | /** | ||
| 1192 | * Walks down a GeneralCaseExpression AST node and generates the corresponding SQL. | ||
| 1193 | */ | ||
| 1194 | public function walkGeneralCaseExpression(AST\GeneralCaseExpression $generalCaseExpression): string | ||
| 1195 | { | ||
| 1196 | $sql = 'CASE'; | ||
| 1197 | |||
| 1198 | foreach ($generalCaseExpression->whenClauses as $whenClause) { | ||
| 1199 | $sql .= ' WHEN ' . $this->walkConditionalExpression($whenClause->caseConditionExpression); | ||
| 1200 | $sql .= ' THEN ' . $this->walkSimpleArithmeticExpression($whenClause->thenScalarExpression); | ||
| 1201 | } | ||
| 1202 | |||
| 1203 | $sql .= ' ELSE ' . $this->walkSimpleArithmeticExpression($generalCaseExpression->elseScalarExpression) . ' END'; | ||
| 1204 | |||
| 1205 | return $sql; | ||
| 1206 | } | ||
| 1207 | |||
| 1208 | /** | ||
| 1209 | * Walks down a SimpleCaseExpression AST node and generates the corresponding SQL. | ||
| 1210 | */ | ||
| 1211 | public function walkSimpleCaseExpression(AST\SimpleCaseExpression $simpleCaseExpression): string | ||
| 1212 | { | ||
| 1213 | $sql = 'CASE ' . $this->walkStateFieldPathExpression($simpleCaseExpression->caseOperand); | ||
| 1214 | |||
| 1215 | foreach ($simpleCaseExpression->simpleWhenClauses as $simpleWhenClause) { | ||
| 1216 | $sql .= ' WHEN ' . $this->walkSimpleArithmeticExpression($simpleWhenClause->caseScalarExpression); | ||
| 1217 | $sql .= ' THEN ' . $this->walkSimpleArithmeticExpression($simpleWhenClause->thenScalarExpression); | ||
| 1218 | } | ||
| 1219 | |||
| 1220 | $sql .= ' ELSE ' . $this->walkSimpleArithmeticExpression($simpleCaseExpression->elseScalarExpression) . ' END'; | ||
| 1221 | |||
| 1222 | return $sql; | ||
| 1223 | } | ||
| 1224 | |||
| 1225 | /** | ||
| 1226 | * Walks down a SelectExpression AST node and generates the corresponding SQL. | ||
| 1227 | */ | ||
| 1228 | public function walkSelectExpression(AST\SelectExpression $selectExpression): string | ||
| 1229 | { | ||
| 1230 | $sql = ''; | ||
| 1231 | $expr = $selectExpression->expression; | ||
| 1232 | $hidden = $selectExpression->hiddenAliasResultVariable; | ||
| 1233 | |||
| 1234 | switch (true) { | ||
| 1235 | case $expr instanceof AST\PathExpression: | ||
| 1236 | if ($expr->type !== AST\PathExpression::TYPE_STATE_FIELD) { | ||
| 1237 | throw QueryException::invalidPathExpression($expr); | ||
| 1238 | } | ||
| 1239 | |||
| 1240 | assert($expr->field !== null); | ||
| 1241 | $fieldName = $expr->field; | ||
| 1242 | $dqlAlias = $expr->identificationVariable; | ||
| 1243 | $class = $this->getMetadataForDqlAlias($dqlAlias); | ||
| 1244 | |||
| 1245 | $resultAlias = $selectExpression->fieldIdentificationVariable ?: $fieldName; | ||
| 1246 | $tableName = $class->isInheritanceTypeJoined() | ||
| 1247 | ? $this->em->getUnitOfWork()->getEntityPersister($class->name)->getOwningTable($fieldName) | ||
| 1248 | : $class->getTableName(); | ||
| 1249 | |||
| 1250 | $sqlTableAlias = $this->getSQLTableAlias($tableName, $dqlAlias); | ||
| 1251 | $fieldMapping = $class->fieldMappings[$fieldName]; | ||
| 1252 | $columnName = $this->quoteStrategy->getColumnName($fieldName, $class, $this->platform); | ||
| 1253 | $columnAlias = $this->getSQLColumnAlias($fieldMapping->columnName); | ||
| 1254 | $col = $sqlTableAlias . '.' . $columnName; | ||
| 1255 | |||
| 1256 | $type = Type::getType($fieldMapping->type); | ||
| 1257 | $col = $type->convertToPHPValueSQL($col, $this->conn->getDatabasePlatform()); | ||
| 1258 | |||
| 1259 | $sql .= $col . ' AS ' . $columnAlias; | ||
| 1260 | |||
| 1261 | $this->scalarResultAliasMap[$resultAlias] = $columnAlias; | ||
| 1262 | |||
| 1263 | if (! $hidden) { | ||
| 1264 | $this->rsm->addScalarResult($columnAlias, $resultAlias, $fieldMapping->type); | ||
| 1265 | $this->scalarFields[$dqlAlias][$fieldName] = $columnAlias; | ||
| 1266 | |||
| 1267 | if (! empty($fieldMapping->enumType)) { | ||
| 1268 | $this->rsm->addEnumResult($columnAlias, $fieldMapping->enumType); | ||
| 1269 | } | ||
| 1270 | } | ||
| 1271 | |||
| 1272 | break; | ||
| 1273 | |||
| 1274 | case $expr instanceof AST\AggregateExpression: | ||
| 1275 | case $expr instanceof AST\Functions\FunctionNode: | ||
| 1276 | case $expr instanceof AST\SimpleArithmeticExpression: | ||
| 1277 | case $expr instanceof AST\ArithmeticTerm: | ||
| 1278 | case $expr instanceof AST\ArithmeticFactor: | ||
| 1279 | case $expr instanceof AST\ParenthesisExpression: | ||
| 1280 | case $expr instanceof AST\Literal: | ||
| 1281 | case $expr instanceof AST\NullIfExpression: | ||
| 1282 | case $expr instanceof AST\CoalesceExpression: | ||
| 1283 | case $expr instanceof AST\GeneralCaseExpression: | ||
| 1284 | case $expr instanceof AST\SimpleCaseExpression: | ||
| 1285 | $columnAlias = $this->getSQLColumnAlias('sclr'); | ||
| 1286 | $resultAlias = $selectExpression->fieldIdentificationVariable ?: $this->scalarResultCounter++; | ||
| 1287 | |||
| 1288 | $sql .= $expr->dispatch($this) . ' AS ' . $columnAlias; | ||
| 1289 | |||
| 1290 | $this->scalarResultAliasMap[$resultAlias] = $columnAlias; | ||
| 1291 | |||
| 1292 | if ($hidden) { | ||
| 1293 | break; | ||
| 1294 | } | ||
| 1295 | |||
| 1296 | if (! $expr instanceof Query\AST\TypedExpression) { | ||
| 1297 | // Conceptually we could resolve field type here by traverse through AST to retrieve field type, | ||
| 1298 | // but this is not a feasible solution; assume 'string'. | ||
| 1299 | $this->rsm->addScalarResult($columnAlias, $resultAlias, 'string'); | ||
| 1300 | |||
| 1301 | break; | ||
| 1302 | } | ||
| 1303 | |||
| 1304 | $this->rsm->addScalarResult($columnAlias, $resultAlias, Type::getTypeRegistry()->lookupName($expr->getReturnType())); | ||
| 1305 | |||
| 1306 | break; | ||
| 1307 | |||
| 1308 | case $expr instanceof AST\Subselect: | ||
| 1309 | $columnAlias = $this->getSQLColumnAlias('sclr'); | ||
| 1310 | $resultAlias = $selectExpression->fieldIdentificationVariable ?: $this->scalarResultCounter++; | ||
| 1311 | |||
| 1312 | $sql .= '(' . $this->walkSubselect($expr) . ') AS ' . $columnAlias; | ||
| 1313 | |||
| 1314 | $this->scalarResultAliasMap[$resultAlias] = $columnAlias; | ||
| 1315 | |||
| 1316 | if (! $hidden) { | ||
| 1317 | // We cannot resolve field type here; assume 'string'. | ||
| 1318 | $this->rsm->addScalarResult($columnAlias, $resultAlias, 'string'); | ||
| 1319 | } | ||
| 1320 | |||
| 1321 | break; | ||
| 1322 | |||
| 1323 | case $expr instanceof AST\NewObjectExpression: | ||
| 1324 | $sql .= $this->walkNewObject($expr, $selectExpression->fieldIdentificationVariable); | ||
| 1325 | break; | ||
| 1326 | |||
| 1327 | default: | ||
| 1328 | $dqlAlias = $expr; | ||
| 1329 | $class = $this->getMetadataForDqlAlias($dqlAlias); | ||
| 1330 | $resultAlias = $selectExpression->fieldIdentificationVariable ?: null; | ||
| 1331 | |||
| 1332 | if (! isset($this->selectedClasses[$dqlAlias])) { | ||
| 1333 | $this->selectedClasses[$dqlAlias] = [ | ||
| 1334 | 'class' => $class, | ||
| 1335 | 'dqlAlias' => $dqlAlias, | ||
| 1336 | 'resultAlias' => $resultAlias, | ||
| 1337 | ]; | ||
| 1338 | } | ||
| 1339 | |||
| 1340 | $sqlParts = []; | ||
| 1341 | |||
| 1342 | // Select all fields from the queried class | ||
| 1343 | foreach ($class->fieldMappings as $fieldName => $mapping) { | ||
| 1344 | $tableName = isset($mapping->inherited) | ||
| 1345 | ? $this->em->getClassMetadata($mapping->inherited)->getTableName() | ||
| 1346 | : $class->getTableName(); | ||
| 1347 | |||
| 1348 | $sqlTableAlias = $this->getSQLTableAlias($tableName, $dqlAlias); | ||
| 1349 | $columnAlias = $this->getSQLColumnAlias($mapping->columnName); | ||
| 1350 | $quotedColumnName = $this->quoteStrategy->getColumnName($fieldName, $class, $this->platform); | ||
| 1351 | |||
| 1352 | $col = $sqlTableAlias . '.' . $quotedColumnName; | ||
| 1353 | |||
| 1354 | $type = Type::getType($mapping->type); | ||
| 1355 | $col = $type->convertToPHPValueSQL($col, $this->platform); | ||
| 1356 | |||
| 1357 | $sqlParts[] = $col . ' AS ' . $columnAlias; | ||
| 1358 | |||
| 1359 | $this->scalarResultAliasMap[$resultAlias][] = $columnAlias; | ||
| 1360 | |||
| 1361 | $this->rsm->addFieldResult($dqlAlias, $columnAlias, $fieldName, $class->name); | ||
| 1362 | |||
| 1363 | if (! empty($mapping->enumType)) { | ||
| 1364 | $this->rsm->addEnumResult($columnAlias, $mapping->enumType); | ||
| 1365 | } | ||
| 1366 | } | ||
| 1367 | |||
| 1368 | // Add any additional fields of subclasses (excluding inherited fields) | ||
| 1369 | // 1) on Single Table Inheritance: always, since its marginal overhead | ||
| 1370 | // 2) on Class Table Inheritance | ||
| 1371 | foreach ($class->subClasses as $subClassName) { | ||
| 1372 | $subClass = $this->em->getClassMetadata($subClassName); | ||
| 1373 | $sqlTableAlias = $this->getSQLTableAlias($subClass->getTableName(), $dqlAlias); | ||
| 1374 | |||
| 1375 | foreach ($subClass->fieldMappings as $fieldName => $mapping) { | ||
| 1376 | if (isset($mapping->inherited)) { | ||
| 1377 | continue; | ||
| 1378 | } | ||
| 1379 | |||
| 1380 | $columnAlias = $this->getSQLColumnAlias($mapping->columnName); | ||
| 1381 | $quotedColumnName = $this->quoteStrategy->getColumnName($fieldName, $subClass, $this->platform); | ||
| 1382 | |||
| 1383 | $col = $sqlTableAlias . '.' . $quotedColumnName; | ||
| 1384 | |||
| 1385 | $type = Type::getType($mapping->type); | ||
| 1386 | $col = $type->convertToPHPValueSQL($col, $this->platform); | ||
| 1387 | |||
| 1388 | $sqlParts[] = $col . ' AS ' . $columnAlias; | ||
| 1389 | |||
| 1390 | $this->scalarResultAliasMap[$resultAlias][] = $columnAlias; | ||
| 1391 | |||
| 1392 | $this->rsm->addFieldResult($dqlAlias, $columnAlias, $fieldName, $subClassName); | ||
| 1393 | } | ||
| 1394 | } | ||
| 1395 | |||
| 1396 | $sql .= implode(', ', $sqlParts); | ||
| 1397 | } | ||
| 1398 | |||
| 1399 | return $sql; | ||
| 1400 | } | ||
| 1401 | |||
| 1402 | public function walkQuantifiedExpression(AST\QuantifiedExpression $qExpr): string | ||
| 1403 | { | ||
| 1404 | return ' ' . strtoupper($qExpr->type) . '(' . $this->walkSubselect($qExpr->subselect) . ')'; | ||
| 1405 | } | ||
| 1406 | |||
| 1407 | /** | ||
| 1408 | * Walks down a Subselect AST node, thereby generating the appropriate SQL. | ||
| 1409 | */ | ||
| 1410 | public function walkSubselect(AST\Subselect $subselect): string | ||
| 1411 | { | ||
| 1412 | $useAliasesBefore = $this->useSqlTableAliases; | ||
| 1413 | $rootAliasesBefore = $this->rootAliases; | ||
| 1414 | |||
| 1415 | $this->rootAliases = []; // reset the rootAliases for the subselect | ||
| 1416 | $this->useSqlTableAliases = true; | ||
| 1417 | |||
| 1418 | $sql = $this->walkSimpleSelectClause($subselect->simpleSelectClause); | ||
| 1419 | $sql .= $this->walkSubselectFromClause($subselect->subselectFromClause); | ||
| 1420 | $sql .= $this->walkWhereClause($subselect->whereClause); | ||
| 1421 | |||
| 1422 | $sql .= $subselect->groupByClause ? $this->walkGroupByClause($subselect->groupByClause) : ''; | ||
| 1423 | $sql .= $subselect->havingClause ? $this->walkHavingClause($subselect->havingClause) : ''; | ||
| 1424 | $sql .= $subselect->orderByClause ? $this->walkOrderByClause($subselect->orderByClause) : ''; | ||
| 1425 | |||
| 1426 | $this->rootAliases = $rootAliasesBefore; // put the main aliases back | ||
| 1427 | $this->useSqlTableAliases = $useAliasesBefore; | ||
| 1428 | |||
| 1429 | return $sql; | ||
| 1430 | } | ||
| 1431 | |||
| 1432 | /** | ||
| 1433 | * Walks down a SubselectFromClause AST node, thereby generating the appropriate SQL. | ||
| 1434 | */ | ||
| 1435 | public function walkSubselectFromClause(AST\SubselectFromClause $subselectFromClause): string | ||
| 1436 | { | ||
| 1437 | $identificationVarDecls = $subselectFromClause->identificationVariableDeclarations; | ||
| 1438 | $sqlParts = []; | ||
| 1439 | |||
| 1440 | foreach ($identificationVarDecls as $subselectIdVarDecl) { | ||
| 1441 | $sqlParts[] = $this->walkIdentificationVariableDeclaration($subselectIdVarDecl); | ||
| 1442 | } | ||
| 1443 | |||
| 1444 | return ' FROM ' . implode(', ', $sqlParts); | ||
| 1445 | } | ||
| 1446 | |||
| 1447 | /** | ||
| 1448 | * Walks down a SimpleSelectClause AST node, thereby generating the appropriate SQL. | ||
| 1449 | */ | ||
| 1450 | public function walkSimpleSelectClause(AST\SimpleSelectClause $simpleSelectClause): string | ||
| 1451 | { | ||
| 1452 | return 'SELECT' . ($simpleSelectClause->isDistinct ? ' DISTINCT' : '') | ||
| 1453 | . $this->walkSimpleSelectExpression($simpleSelectClause->simpleSelectExpression); | ||
| 1454 | } | ||
| 1455 | |||
| 1456 | public function walkParenthesisExpression(AST\ParenthesisExpression $parenthesisExpression): string | ||
| 1457 | { | ||
| 1458 | return sprintf('(%s)', $parenthesisExpression->expression->dispatch($this)); | ||
| 1459 | } | ||
| 1460 | |||
| 1461 | public function walkNewObject(AST\NewObjectExpression $newObjectExpression, string|null $newObjectResultAlias = null): string | ||
| 1462 | { | ||
| 1463 | $sqlSelectExpressions = []; | ||
| 1464 | $objIndex = $newObjectResultAlias ?: $this->newObjectCounter++; | ||
| 1465 | |||
| 1466 | foreach ($newObjectExpression->args as $argIndex => $e) { | ||
| 1467 | $resultAlias = $this->scalarResultCounter++; | ||
| 1468 | $columnAlias = $this->getSQLColumnAlias('sclr'); | ||
| 1469 | $fieldType = 'string'; | ||
| 1470 | |||
| 1471 | switch (true) { | ||
| 1472 | case $e instanceof AST\NewObjectExpression: | ||
| 1473 | $sqlSelectExpressions[] = $e->dispatch($this); | ||
| 1474 | break; | ||
| 1475 | |||
| 1476 | case $e instanceof AST\Subselect: | ||
| 1477 | $sqlSelectExpressions[] = '(' . $e->dispatch($this) . ') AS ' . $columnAlias; | ||
| 1478 | break; | ||
| 1479 | |||
| 1480 | case $e instanceof AST\PathExpression: | ||
| 1481 | assert($e->field !== null); | ||
| 1482 | $dqlAlias = $e->identificationVariable; | ||
| 1483 | $class = $this->getMetadataForDqlAlias($dqlAlias); | ||
| 1484 | $fieldName = $e->field; | ||
| 1485 | $fieldMapping = $class->fieldMappings[$fieldName]; | ||
| 1486 | $fieldType = $fieldMapping->type; | ||
| 1487 | $col = trim($e->dispatch($this)); | ||
| 1488 | |||
| 1489 | $type = Type::getType($fieldType); | ||
| 1490 | $col = $type->convertToPHPValueSQL($col, $this->platform); | ||
| 1491 | |||
| 1492 | $sqlSelectExpressions[] = $col . ' AS ' . $columnAlias; | ||
| 1493 | |||
| 1494 | if (! empty($fieldMapping->enumType)) { | ||
| 1495 | $this->rsm->addEnumResult($columnAlias, $fieldMapping->enumType); | ||
| 1496 | } | ||
| 1497 | |||
| 1498 | break; | ||
| 1499 | |||
| 1500 | case $e instanceof AST\Literal: | ||
| 1501 | switch ($e->type) { | ||
| 1502 | case AST\Literal::BOOLEAN: | ||
| 1503 | $fieldType = 'boolean'; | ||
| 1504 | break; | ||
| 1505 | |||
| 1506 | case AST\Literal::NUMERIC: | ||
| 1507 | $fieldType = is_float($e->value) ? 'float' : 'integer'; | ||
| 1508 | break; | ||
| 1509 | } | ||
| 1510 | |||
| 1511 | $sqlSelectExpressions[] = trim($e->dispatch($this)) . ' AS ' . $columnAlias; | ||
| 1512 | break; | ||
| 1513 | |||
| 1514 | default: | ||
| 1515 | $sqlSelectExpressions[] = trim($e->dispatch($this)) . ' AS ' . $columnAlias; | ||
| 1516 | break; | ||
| 1517 | } | ||
| 1518 | |||
| 1519 | $this->scalarResultAliasMap[$resultAlias] = $columnAlias; | ||
| 1520 | $this->rsm->addScalarResult($columnAlias, $resultAlias, $fieldType); | ||
| 1521 | |||
| 1522 | $this->rsm->newObjectMappings[$columnAlias] = [ | ||
| 1523 | 'className' => $newObjectExpression->className, | ||
| 1524 | 'objIndex' => $objIndex, | ||
| 1525 | 'argIndex' => $argIndex, | ||
| 1526 | ]; | ||
| 1527 | } | ||
| 1528 | |||
| 1529 | return implode(', ', $sqlSelectExpressions); | ||
| 1530 | } | ||
| 1531 | |||
| 1532 | /** | ||
| 1533 | * Walks down a SimpleSelectExpression AST node, thereby generating the appropriate SQL. | ||
| 1534 | */ | ||
| 1535 | public function walkSimpleSelectExpression(AST\SimpleSelectExpression $simpleSelectExpression): string | ||
| 1536 | { | ||
| 1537 | $expr = $simpleSelectExpression->expression; | ||
| 1538 | $sql = ' '; | ||
| 1539 | |||
| 1540 | switch (true) { | ||
| 1541 | case $expr instanceof AST\PathExpression: | ||
| 1542 | $sql .= $this->walkPathExpression($expr); | ||
| 1543 | break; | ||
| 1544 | |||
| 1545 | case $expr instanceof AST\Subselect: | ||
| 1546 | $alias = $simpleSelectExpression->fieldIdentificationVariable ?: $this->scalarResultCounter++; | ||
| 1547 | |||
| 1548 | $columnAlias = 'sclr' . $this->aliasCounter++; | ||
| 1549 | $this->scalarResultAliasMap[$alias] = $columnAlias; | ||
| 1550 | |||
| 1551 | $sql .= '(' . $this->walkSubselect($expr) . ') AS ' . $columnAlias; | ||
| 1552 | break; | ||
| 1553 | |||
| 1554 | case $expr instanceof AST\Functions\FunctionNode: | ||
| 1555 | case $expr instanceof AST\SimpleArithmeticExpression: | ||
| 1556 | case $expr instanceof AST\ArithmeticTerm: | ||
| 1557 | case $expr instanceof AST\ArithmeticFactor: | ||
| 1558 | case $expr instanceof AST\Literal: | ||
| 1559 | case $expr instanceof AST\NullIfExpression: | ||
| 1560 | case $expr instanceof AST\CoalesceExpression: | ||
| 1561 | case $expr instanceof AST\GeneralCaseExpression: | ||
| 1562 | case $expr instanceof AST\SimpleCaseExpression: | ||
| 1563 | $alias = $simpleSelectExpression->fieldIdentificationVariable ?: $this->scalarResultCounter++; | ||
| 1564 | |||
| 1565 | $columnAlias = $this->getSQLColumnAlias('sclr'); | ||
| 1566 | $this->scalarResultAliasMap[$alias] = $columnAlias; | ||
| 1567 | |||
| 1568 | $sql .= $expr->dispatch($this) . ' AS ' . $columnAlias; | ||
| 1569 | break; | ||
| 1570 | |||
| 1571 | case $expr instanceof AST\ParenthesisExpression: | ||
| 1572 | $sql .= $this->walkParenthesisExpression($expr); | ||
| 1573 | break; | ||
| 1574 | |||
| 1575 | default: // IdentificationVariable | ||
| 1576 | $sql .= $this->walkEntityIdentificationVariable($expr); | ||
| 1577 | break; | ||
| 1578 | } | ||
| 1579 | |||
| 1580 | return $sql; | ||
| 1581 | } | ||
| 1582 | |||
| 1583 | /** | ||
| 1584 | * Walks down an AggregateExpression AST node, thereby generating the appropriate SQL. | ||
| 1585 | */ | ||
| 1586 | public function walkAggregateExpression(AST\AggregateExpression $aggExpression): string | ||
| 1587 | { | ||
| 1588 | return $aggExpression->functionName . '(' . ($aggExpression->isDistinct ? 'DISTINCT ' : '') | ||
| 1589 | . $this->walkSimpleArithmeticExpression($aggExpression->pathExpression) . ')'; | ||
| 1590 | } | ||
| 1591 | |||
| 1592 | /** | ||
| 1593 | * Walks down a GroupByClause AST node, thereby generating the appropriate SQL. | ||
| 1594 | */ | ||
| 1595 | public function walkGroupByClause(AST\GroupByClause $groupByClause): string | ||
| 1596 | { | ||
| 1597 | $sqlParts = []; | ||
| 1598 | |||
| 1599 | foreach ($groupByClause->groupByItems as $groupByItem) { | ||
| 1600 | $sqlParts[] = $this->walkGroupByItem($groupByItem); | ||
| 1601 | } | ||
| 1602 | |||
| 1603 | return ' GROUP BY ' . implode(', ', $sqlParts); | ||
| 1604 | } | ||
| 1605 | |||
| 1606 | /** | ||
| 1607 | * Walks down a GroupByItem AST node, thereby generating the appropriate SQL. | ||
| 1608 | */ | ||
| 1609 | public function walkGroupByItem(AST\PathExpression|string $groupByItem): string | ||
| 1610 | { | ||
| 1611 | // StateFieldPathExpression | ||
| 1612 | if (! is_string($groupByItem)) { | ||
| 1613 | return $this->walkPathExpression($groupByItem); | ||
| 1614 | } | ||
| 1615 | |||
| 1616 | // ResultVariable | ||
| 1617 | if (isset($this->queryComponents[$groupByItem]['resultVariable'])) { | ||
| 1618 | $resultVariable = $this->queryComponents[$groupByItem]['resultVariable']; | ||
| 1619 | |||
| 1620 | if ($resultVariable instanceof AST\PathExpression) { | ||
| 1621 | return $this->walkPathExpression($resultVariable); | ||
| 1622 | } | ||
| 1623 | |||
| 1624 | if ($resultVariable instanceof AST\Node && isset($resultVariable->pathExpression)) { | ||
| 1625 | return $this->walkPathExpression($resultVariable->pathExpression); | ||
| 1626 | } | ||
| 1627 | |||
| 1628 | return $this->walkResultVariable($groupByItem); | ||
| 1629 | } | ||
| 1630 | |||
| 1631 | // IdentificationVariable | ||
| 1632 | $sqlParts = []; | ||
| 1633 | |||
| 1634 | foreach ($this->getMetadataForDqlAlias($groupByItem)->fieldNames as $field) { | ||
| 1635 | $item = new AST\PathExpression(AST\PathExpression::TYPE_STATE_FIELD, $groupByItem, $field); | ||
| 1636 | $item->type = AST\PathExpression::TYPE_STATE_FIELD; | ||
| 1637 | |||
| 1638 | $sqlParts[] = $this->walkPathExpression($item); | ||
| 1639 | } | ||
| 1640 | |||
| 1641 | foreach ($this->getMetadataForDqlAlias($groupByItem)->associationMappings as $mapping) { | ||
| 1642 | if ($mapping->isToOneOwningSide()) { | ||
| 1643 | $item = new AST\PathExpression(AST\PathExpression::TYPE_SINGLE_VALUED_ASSOCIATION, $groupByItem, $mapping->fieldName); | ||
| 1644 | $item->type = AST\PathExpression::TYPE_SINGLE_VALUED_ASSOCIATION; | ||
| 1645 | |||
| 1646 | $sqlParts[] = $this->walkPathExpression($item); | ||
| 1647 | } | ||
| 1648 | } | ||
| 1649 | |||
| 1650 | return implode(', ', $sqlParts); | ||
| 1651 | } | ||
| 1652 | |||
| 1653 | /** | ||
| 1654 | * Walks down a DeleteClause AST node, thereby generating the appropriate SQL. | ||
| 1655 | */ | ||
| 1656 | public function walkDeleteClause(AST\DeleteClause $deleteClause): string | ||
| 1657 | { | ||
| 1658 | $class = $this->em->getClassMetadata($deleteClause->abstractSchemaName); | ||
| 1659 | $tableName = $class->getTableName(); | ||
| 1660 | $sql = 'DELETE FROM ' . $this->quoteStrategy->getTableName($class, $this->platform); | ||
| 1661 | |||
| 1662 | $this->setSQLTableAlias($tableName, $tableName, $deleteClause->aliasIdentificationVariable); | ||
| 1663 | $this->rootAliases[] = $deleteClause->aliasIdentificationVariable; | ||
| 1664 | |||
| 1665 | return $sql; | ||
| 1666 | } | ||
| 1667 | |||
| 1668 | /** | ||
| 1669 | * Walks down an UpdateClause AST node, thereby generating the appropriate SQL. | ||
| 1670 | */ | ||
| 1671 | public function walkUpdateClause(AST\UpdateClause $updateClause): string | ||
| 1672 | { | ||
| 1673 | $class = $this->em->getClassMetadata($updateClause->abstractSchemaName); | ||
| 1674 | $tableName = $class->getTableName(); | ||
| 1675 | $sql = 'UPDATE ' . $this->quoteStrategy->getTableName($class, $this->platform); | ||
| 1676 | |||
| 1677 | $this->setSQLTableAlias($tableName, $tableName, $updateClause->aliasIdentificationVariable); | ||
| 1678 | $this->rootAliases[] = $updateClause->aliasIdentificationVariable; | ||
| 1679 | |||
| 1680 | return $sql . ' SET ' . implode(', ', array_map($this->walkUpdateItem(...), $updateClause->updateItems)); | ||
| 1681 | } | ||
| 1682 | |||
| 1683 | /** | ||
| 1684 | * Walks down an UpdateItem AST node, thereby generating the appropriate SQL. | ||
| 1685 | */ | ||
| 1686 | public function walkUpdateItem(AST\UpdateItem $updateItem): string | ||
| 1687 | { | ||
| 1688 | $useTableAliasesBefore = $this->useSqlTableAliases; | ||
| 1689 | $this->useSqlTableAliases = false; | ||
| 1690 | |||
| 1691 | $sql = $this->walkPathExpression($updateItem->pathExpression) . ' = '; | ||
| 1692 | $newValue = $updateItem->newValue; | ||
| 1693 | |||
| 1694 | $sql .= match (true) { | ||
| 1695 | $newValue instanceof AST\Node => $newValue->dispatch($this), | ||
| 1696 | $newValue === null => 'NULL', | ||
| 1697 | }; | ||
| 1698 | |||
| 1699 | $this->useSqlTableAliases = $useTableAliasesBefore; | ||
| 1700 | |||
| 1701 | return $sql; | ||
| 1702 | } | ||
| 1703 | |||
| 1704 | /** | ||
| 1705 | * Walks down a WhereClause AST node, thereby generating the appropriate SQL. | ||
| 1706 | * | ||
| 1707 | * WhereClause or not, the appropriate discriminator sql is added. | ||
| 1708 | */ | ||
| 1709 | public function walkWhereClause(AST\WhereClause|null $whereClause): string | ||
| 1710 | { | ||
| 1711 | $condSql = $whereClause !== null ? $this->walkConditionalExpression($whereClause->conditionalExpression) : ''; | ||
| 1712 | $discrSql = $this->generateDiscriminatorColumnConditionSQL($this->rootAliases); | ||
| 1713 | |||
| 1714 | if ($this->em->hasFilters()) { | ||
| 1715 | $filterClauses = []; | ||
| 1716 | foreach ($this->rootAliases as $dqlAlias) { | ||
| 1717 | $class = $this->getMetadataForDqlAlias($dqlAlias); | ||
| 1718 | $tableAlias = $this->getSQLTableAlias($class->table['name'], $dqlAlias); | ||
| 1719 | |||
| 1720 | $filterExpr = $this->generateFilterConditionSQL($class, $tableAlias); | ||
| 1721 | if ($filterExpr) { | ||
| 1722 | $filterClauses[] = $filterExpr; | ||
| 1723 | } | ||
| 1724 | } | ||
| 1725 | |||
| 1726 | if (count($filterClauses)) { | ||
| 1727 | if ($condSql) { | ||
| 1728 | $condSql = '(' . $condSql . ') AND '; | ||
| 1729 | } | ||
| 1730 | |||
| 1731 | $condSql .= implode(' AND ', $filterClauses); | ||
| 1732 | } | ||
| 1733 | } | ||
| 1734 | |||
| 1735 | if ($condSql) { | ||
| 1736 | return ' WHERE ' . (! $discrSql ? $condSql : '(' . $condSql . ') AND ' . $discrSql); | ||
| 1737 | } | ||
| 1738 | |||
| 1739 | if ($discrSql) { | ||
| 1740 | return ' WHERE ' . $discrSql; | ||
| 1741 | } | ||
| 1742 | |||
| 1743 | return ''; | ||
| 1744 | } | ||
| 1745 | |||
| 1746 | /** | ||
| 1747 | * Walk down a ConditionalExpression AST node, thereby generating the appropriate SQL. | ||
| 1748 | */ | ||
| 1749 | public function walkConditionalExpression( | ||
| 1750 | AST\ConditionalExpression|AST\Phase2OptimizableConditional $condExpr, | ||
| 1751 | ): string { | ||
| 1752 | // Phase 2 AST optimization: Skip processing of ConditionalExpression | ||
| 1753 | // if only one ConditionalTerm is defined | ||
| 1754 | if (! ($condExpr instanceof AST\ConditionalExpression)) { | ||
| 1755 | return $this->walkConditionalTerm($condExpr); | ||
| 1756 | } | ||
| 1757 | |||
| 1758 | return implode(' OR ', array_map($this->walkConditionalTerm(...), $condExpr->conditionalTerms)); | ||
| 1759 | } | ||
| 1760 | |||
| 1761 | /** | ||
| 1762 | * Walks down a ConditionalTerm AST node, thereby generating the appropriate SQL. | ||
| 1763 | */ | ||
| 1764 | public function walkConditionalTerm( | ||
| 1765 | AST\ConditionalTerm|AST\ConditionalPrimary|AST\ConditionalFactor $condTerm, | ||
| 1766 | ): string { | ||
| 1767 | // Phase 2 AST optimization: Skip processing of ConditionalTerm | ||
| 1768 | // if only one ConditionalFactor is defined | ||
| 1769 | if (! ($condTerm instanceof AST\ConditionalTerm)) { | ||
| 1770 | return $this->walkConditionalFactor($condTerm); | ||
| 1771 | } | ||
| 1772 | |||
| 1773 | return implode(' AND ', array_map($this->walkConditionalFactor(...), $condTerm->conditionalFactors)); | ||
| 1774 | } | ||
| 1775 | |||
| 1776 | /** | ||
| 1777 | * Walks down a ConditionalFactor AST node, thereby generating the appropriate SQL. | ||
| 1778 | */ | ||
| 1779 | public function walkConditionalFactor( | ||
| 1780 | AST\ConditionalFactor|AST\ConditionalPrimary $factor, | ||
| 1781 | ): string { | ||
| 1782 | // Phase 2 AST optimization: Skip processing of ConditionalFactor | ||
| 1783 | // if only one ConditionalPrimary is defined | ||
| 1784 | return ! ($factor instanceof AST\ConditionalFactor) | ||
| 1785 | ? $this->walkConditionalPrimary($factor) | ||
| 1786 | : ($factor->not ? 'NOT ' : '') . $this->walkConditionalPrimary($factor->conditionalPrimary); | ||
| 1787 | } | ||
| 1788 | |||
| 1789 | /** | ||
| 1790 | * Walks down a ConditionalPrimary AST node, thereby generating the appropriate SQL. | ||
| 1791 | */ | ||
| 1792 | public function walkConditionalPrimary(AST\ConditionalPrimary $primary): string | ||
| 1793 | { | ||
| 1794 | if ($primary->isSimpleConditionalExpression()) { | ||
| 1795 | return $primary->simpleConditionalExpression->dispatch($this); | ||
| 1796 | } | ||
| 1797 | |||
| 1798 | if ($primary->isConditionalExpression()) { | ||
| 1799 | $condExpr = $primary->conditionalExpression; | ||
| 1800 | |||
| 1801 | return '(' . $this->walkConditionalExpression($condExpr) . ')'; | ||
| 1802 | } | ||
| 1803 | |||
| 1804 | throw new LogicException('Unexpected state of ConditionalPrimary node.'); | ||
| 1805 | } | ||
| 1806 | |||
| 1807 | /** | ||
| 1808 | * Walks down an ExistsExpression AST node, thereby generating the appropriate SQL. | ||
| 1809 | */ | ||
| 1810 | public function walkExistsExpression(AST\ExistsExpression $existsExpr): string | ||
| 1811 | { | ||
| 1812 | $sql = $existsExpr->not ? 'NOT ' : ''; | ||
| 1813 | |||
| 1814 | $sql .= 'EXISTS (' . $this->walkSubselect($existsExpr->subselect) . ')'; | ||
| 1815 | |||
| 1816 | return $sql; | ||
| 1817 | } | ||
| 1818 | |||
| 1819 | /** | ||
| 1820 | * Walks down a CollectionMemberExpression AST node, thereby generating the appropriate SQL. | ||
| 1821 | */ | ||
| 1822 | public function walkCollectionMemberExpression(AST\CollectionMemberExpression $collMemberExpr): string | ||
| 1823 | { | ||
| 1824 | $sql = $collMemberExpr->not ? 'NOT ' : ''; | ||
| 1825 | $sql .= 'EXISTS (SELECT 1 FROM '; | ||
| 1826 | |||
| 1827 | $entityExpr = $collMemberExpr->entityExpression; | ||
| 1828 | $collPathExpr = $collMemberExpr->collectionValuedPathExpression; | ||
| 1829 | assert($collPathExpr->field !== null); | ||
| 1830 | |||
| 1831 | $fieldName = $collPathExpr->field; | ||
| 1832 | $dqlAlias = $collPathExpr->identificationVariable; | ||
| 1833 | |||
| 1834 | $class = $this->getMetadataForDqlAlias($dqlAlias); | ||
| 1835 | |||
| 1836 | switch (true) { | ||
| 1837 | // InputParameter | ||
| 1838 | case $entityExpr instanceof AST\InputParameter: | ||
| 1839 | $dqlParamKey = $entityExpr->name; | ||
| 1840 | $entitySql = '?'; | ||
| 1841 | break; | ||
| 1842 | |||
| 1843 | // SingleValuedAssociationPathExpression | IdentificationVariable | ||
| 1844 | case $entityExpr instanceof AST\PathExpression: | ||
| 1845 | $entitySql = $this->walkPathExpression($entityExpr); | ||
| 1846 | break; | ||
| 1847 | |||
| 1848 | default: | ||
| 1849 | throw new BadMethodCallException('Not implemented'); | ||
| 1850 | } | ||
| 1851 | |||
| 1852 | $assoc = $class->associationMappings[$fieldName]; | ||
| 1853 | |||
| 1854 | if ($assoc->isOneToMany()) { | ||
| 1855 | $targetClass = $this->em->getClassMetadata($assoc->targetEntity); | ||
| 1856 | $targetTableAlias = $this->getSQLTableAlias($targetClass->getTableName()); | ||
| 1857 | $sourceTableAlias = $this->getSQLTableAlias($class->getTableName(), $dqlAlias); | ||
| 1858 | |||
| 1859 | $sql .= $this->quoteStrategy->getTableName($targetClass, $this->platform) . ' ' . $targetTableAlias . ' WHERE '; | ||
| 1860 | |||
| 1861 | $owningAssoc = $targetClass->associationMappings[$assoc->mappedBy]; | ||
| 1862 | assert($owningAssoc->isManyToOne()); | ||
| 1863 | $sqlParts = []; | ||
| 1864 | |||
| 1865 | foreach ($owningAssoc->targetToSourceKeyColumns as $targetColumn => $sourceColumn) { | ||
| 1866 | $targetColumn = $this->quoteStrategy->getColumnName($class->fieldNames[$targetColumn], $class, $this->platform); | ||
| 1867 | |||
| 1868 | $sqlParts[] = $sourceTableAlias . '.' . $targetColumn . ' = ' . $targetTableAlias . '.' . $sourceColumn; | ||
| 1869 | } | ||
| 1870 | |||
| 1871 | foreach ($this->quoteStrategy->getIdentifierColumnNames($targetClass, $this->platform) as $targetColumnName) { | ||
| 1872 | if (isset($dqlParamKey)) { | ||
| 1873 | $this->parserResult->addParameterMapping($dqlParamKey, $this->sqlParamIndex++); | ||
| 1874 | } | ||
| 1875 | |||
| 1876 | $sqlParts[] = $targetTableAlias . '.' . $targetColumnName . ' = ' . $entitySql; | ||
| 1877 | } | ||
| 1878 | |||
| 1879 | $sql .= implode(' AND ', $sqlParts); | ||
| 1880 | } else { // many-to-many | ||
| 1881 | $targetClass = $this->em->getClassMetadata($assoc->targetEntity); | ||
| 1882 | |||
| 1883 | $owningAssoc = $this->em->getMetadataFactory()->getOwningSide($assoc); | ||
| 1884 | assert($owningAssoc->isManyToManyOwningSide()); | ||
| 1885 | $joinTable = $owningAssoc->joinTable; | ||
| 1886 | |||
| 1887 | // SQL table aliases | ||
| 1888 | $joinTableAlias = $this->getSQLTableAlias($joinTable->name); | ||
| 1889 | $sourceTableAlias = $this->getSQLTableAlias($class->getTableName(), $dqlAlias); | ||
| 1890 | |||
| 1891 | $sql .= $this->quoteStrategy->getJoinTableName($owningAssoc, $targetClass, $this->platform) . ' ' . $joinTableAlias . ' WHERE '; | ||
| 1892 | |||
| 1893 | $joinColumns = $assoc->isOwningSide() ? $joinTable->joinColumns : $joinTable->inverseJoinColumns; | ||
| 1894 | $sqlParts = []; | ||
| 1895 | |||
| 1896 | foreach ($joinColumns as $joinColumn) { | ||
| 1897 | $targetColumn = $this->quoteStrategy->getColumnName($class->fieldNames[$joinColumn->referencedColumnName], $class, $this->platform); | ||
| 1898 | |||
| 1899 | $sqlParts[] = $joinTableAlias . '.' . $joinColumn->name . ' = ' . $sourceTableAlias . '.' . $targetColumn; | ||
| 1900 | } | ||
| 1901 | |||
| 1902 | $joinColumns = $assoc->isOwningSide() ? $joinTable->inverseJoinColumns : $joinTable->joinColumns; | ||
| 1903 | |||
| 1904 | foreach ($joinColumns as $joinColumn) { | ||
| 1905 | if (isset($dqlParamKey)) { | ||
| 1906 | $this->parserResult->addParameterMapping($dqlParamKey, $this->sqlParamIndex++); | ||
| 1907 | } | ||
| 1908 | |||
| 1909 | $sqlParts[] = $joinTableAlias . '.' . $joinColumn->name . ' IN (' . $entitySql . ')'; | ||
| 1910 | } | ||
| 1911 | |||
| 1912 | $sql .= implode(' AND ', $sqlParts); | ||
| 1913 | } | ||
| 1914 | |||
| 1915 | return $sql . ')'; | ||
| 1916 | } | ||
| 1917 | |||
| 1918 | /** | ||
| 1919 | * Walks down an EmptyCollectionComparisonExpression AST node, thereby generating the appropriate SQL. | ||
| 1920 | */ | ||
| 1921 | public function walkEmptyCollectionComparisonExpression(AST\EmptyCollectionComparisonExpression $emptyCollCompExpr): string | ||
| 1922 | { | ||
| 1923 | $sizeFunc = new AST\Functions\SizeFunction('size'); | ||
| 1924 | $sizeFunc->collectionPathExpression = $emptyCollCompExpr->expression; | ||
| 1925 | |||
| 1926 | return $sizeFunc->getSql($this) . ($emptyCollCompExpr->not ? ' > 0' : ' = 0'); | ||
| 1927 | } | ||
| 1928 | |||
| 1929 | /** | ||
| 1930 | * Walks down a NullComparisonExpression AST node, thereby generating the appropriate SQL. | ||
| 1931 | */ | ||
| 1932 | public function walkNullComparisonExpression(AST\NullComparisonExpression $nullCompExpr): string | ||
| 1933 | { | ||
| 1934 | $expression = $nullCompExpr->expression; | ||
| 1935 | $comparison = ' IS' . ($nullCompExpr->not ? ' NOT' : '') . ' NULL'; | ||
| 1936 | |||
| 1937 | // Handle ResultVariable | ||
| 1938 | if (is_string($expression) && isset($this->queryComponents[$expression]['resultVariable'])) { | ||
| 1939 | return $this->walkResultVariable($expression) . $comparison; | ||
| 1940 | } | ||
| 1941 | |||
| 1942 | // Handle InputParameter mapping inclusion to ParserResult | ||
| 1943 | if ($expression instanceof AST\InputParameter) { | ||
| 1944 | return $this->walkInputParameter($expression) . $comparison; | ||
| 1945 | } | ||
| 1946 | |||
| 1947 | assert(! is_string($expression)); | ||
| 1948 | |||
| 1949 | return $expression->dispatch($this) . $comparison; | ||
| 1950 | } | ||
| 1951 | |||
| 1952 | /** | ||
| 1953 | * Walks down an InExpression AST node, thereby generating the appropriate SQL. | ||
| 1954 | */ | ||
| 1955 | public function walkInListExpression(AST\InListExpression $inExpr): string | ||
| 1956 | { | ||
| 1957 | return $this->walkArithmeticExpression($inExpr->expression) | ||
| 1958 | . ($inExpr->not ? ' NOT' : '') . ' IN (' | ||
| 1959 | . implode(', ', array_map($this->walkInParameter(...), $inExpr->literals)) | ||
| 1960 | . ')'; | ||
| 1961 | } | ||
| 1962 | |||
| 1963 | /** | ||
| 1964 | * Walks down an InExpression AST node, thereby generating the appropriate SQL. | ||
| 1965 | */ | ||
| 1966 | public function walkInSubselectExpression(AST\InSubselectExpression $inExpr): string | ||
| 1967 | { | ||
| 1968 | return $this->walkArithmeticExpression($inExpr->expression) | ||
| 1969 | . ($inExpr->not ? ' NOT' : '') . ' IN (' | ||
| 1970 | . $this->walkSubselect($inExpr->subselect) | ||
| 1971 | . ')'; | ||
| 1972 | } | ||
| 1973 | |||
| 1974 | /** | ||
| 1975 | * Walks down an InstanceOfExpression AST node, thereby generating the appropriate SQL. | ||
| 1976 | * | ||
| 1977 | * @throws QueryException | ||
| 1978 | */ | ||
| 1979 | public function walkInstanceOfExpression(AST\InstanceOfExpression $instanceOfExpr): string | ||
| 1980 | { | ||
| 1981 | $sql = ''; | ||
| 1982 | |||
| 1983 | $dqlAlias = $instanceOfExpr->identificationVariable; | ||
| 1984 | $discrClass = $class = $this->getMetadataForDqlAlias($dqlAlias); | ||
| 1985 | |||
| 1986 | if ($class->discriminatorColumn) { | ||
| 1987 | $discrClass = $this->em->getClassMetadata($class->rootEntityName); | ||
| 1988 | } | ||
| 1989 | |||
| 1990 | if ($this->useSqlTableAliases) { | ||
| 1991 | $sql .= $this->getSQLTableAlias($discrClass->getTableName(), $dqlAlias) . '.'; | ||
| 1992 | } | ||
| 1993 | |||
| 1994 | $sql .= $class->getDiscriminatorColumn()->name . ($instanceOfExpr->not ? ' NOT IN ' : ' IN '); | ||
| 1995 | $sql .= $this->getChildDiscriminatorsFromClassMetadata($discrClass, $instanceOfExpr); | ||
| 1996 | |||
| 1997 | return $sql; | ||
| 1998 | } | ||
| 1999 | |||
| 2000 | public function walkInParameter(mixed $inParam): string | ||
| 2001 | { | ||
| 2002 | return $inParam instanceof AST\InputParameter | ||
| 2003 | ? $this->walkInputParameter($inParam) | ||
| 2004 | : $this->walkArithmeticExpression($inParam); | ||
| 2005 | } | ||
| 2006 | |||
| 2007 | /** | ||
| 2008 | * Walks down a literal that represents an AST node, thereby generating the appropriate SQL. | ||
| 2009 | */ | ||
| 2010 | public function walkLiteral(AST\Literal $literal): string | ||
| 2011 | { | ||
| 2012 | return match ($literal->type) { | ||
| 2013 | AST\Literal::STRING => $this->conn->quote($literal->value), | ||
| 2014 | AST\Literal::BOOLEAN => (string) $this->conn->getDatabasePlatform()->convertBooleans(strtolower($literal->value) === 'true'), | ||
| 2015 | AST\Literal::NUMERIC => (string) $literal->value, | ||
| 2016 | default => throw QueryException::invalidLiteral($literal), | ||
| 2017 | }; | ||
| 2018 | } | ||
| 2019 | |||
| 2020 | /** | ||
| 2021 | * Walks down a BetweenExpression AST node, thereby generating the appropriate SQL. | ||
| 2022 | */ | ||
| 2023 | public function walkBetweenExpression(AST\BetweenExpression $betweenExpr): string | ||
| 2024 | { | ||
| 2025 | $sql = $this->walkArithmeticExpression($betweenExpr->expression); | ||
| 2026 | |||
| 2027 | if ($betweenExpr->not) { | ||
| 2028 | $sql .= ' NOT'; | ||
| 2029 | } | ||
| 2030 | |||
| 2031 | $sql .= ' BETWEEN ' . $this->walkArithmeticExpression($betweenExpr->leftBetweenExpression) | ||
| 2032 | . ' AND ' . $this->walkArithmeticExpression($betweenExpr->rightBetweenExpression); | ||
| 2033 | |||
| 2034 | return $sql; | ||
| 2035 | } | ||
| 2036 | |||
| 2037 | /** | ||
| 2038 | * Walks down a LikeExpression AST node, thereby generating the appropriate SQL. | ||
| 2039 | */ | ||
| 2040 | public function walkLikeExpression(AST\LikeExpression $likeExpr): string | ||
| 2041 | { | ||
| 2042 | $stringExpr = $likeExpr->stringExpression; | ||
| 2043 | if (is_string($stringExpr)) { | ||
| 2044 | if (! isset($this->queryComponents[$stringExpr]['resultVariable'])) { | ||
| 2045 | throw new LogicException(sprintf('No result variable found for string expression "%s".', $stringExpr)); | ||
| 2046 | } | ||
| 2047 | |||
| 2048 | $leftExpr = $this->walkResultVariable($stringExpr); | ||
| 2049 | } else { | ||
| 2050 | $leftExpr = $stringExpr->dispatch($this); | ||
| 2051 | } | ||
| 2052 | |||
| 2053 | $sql = $leftExpr . ($likeExpr->not ? ' NOT' : '') . ' LIKE '; | ||
| 2054 | |||
| 2055 | if ($likeExpr->stringPattern instanceof AST\InputParameter) { | ||
| 2056 | $sql .= $this->walkInputParameter($likeExpr->stringPattern); | ||
| 2057 | } elseif ($likeExpr->stringPattern instanceof AST\Functions\FunctionNode) { | ||
| 2058 | $sql .= $this->walkFunction($likeExpr->stringPattern); | ||
| 2059 | } elseif ($likeExpr->stringPattern instanceof AST\PathExpression) { | ||
| 2060 | $sql .= $this->walkPathExpression($likeExpr->stringPattern); | ||
| 2061 | } else { | ||
| 2062 | $sql .= $this->walkLiteral($likeExpr->stringPattern); | ||
| 2063 | } | ||
| 2064 | |||
| 2065 | if ($likeExpr->escapeChar) { | ||
| 2066 | $sql .= ' ESCAPE ' . $this->walkLiteral($likeExpr->escapeChar); | ||
| 2067 | } | ||
| 2068 | |||
| 2069 | return $sql; | ||
| 2070 | } | ||
| 2071 | |||
| 2072 | /** | ||
| 2073 | * Walks down a StateFieldPathExpression AST node, thereby generating the appropriate SQL. | ||
| 2074 | */ | ||
| 2075 | public function walkStateFieldPathExpression(AST\PathExpression $stateFieldPathExpression): string | ||
| 2076 | { | ||
| 2077 | return $this->walkPathExpression($stateFieldPathExpression); | ||
| 2078 | } | ||
| 2079 | |||
| 2080 | /** | ||
| 2081 | * Walks down a ComparisonExpression AST node, thereby generating the appropriate SQL. | ||
| 2082 | */ | ||
| 2083 | public function walkComparisonExpression(AST\ComparisonExpression $compExpr): string | ||
| 2084 | { | ||
| 2085 | $leftExpr = $compExpr->leftExpression; | ||
| 2086 | $rightExpr = $compExpr->rightExpression; | ||
| 2087 | $sql = ''; | ||
| 2088 | |||
| 2089 | $sql .= $leftExpr instanceof AST\Node | ||
| 2090 | ? $leftExpr->dispatch($this) | ||
| 2091 | : (is_numeric($leftExpr) ? $leftExpr : $this->conn->quote($leftExpr)); | ||
| 2092 | |||
| 2093 | $sql .= ' ' . $compExpr->operator . ' '; | ||
| 2094 | |||
| 2095 | $sql .= $rightExpr instanceof AST\Node | ||
| 2096 | ? $rightExpr->dispatch($this) | ||
| 2097 | : (is_numeric($rightExpr) ? $rightExpr : $this->conn->quote($rightExpr)); | ||
| 2098 | |||
| 2099 | return $sql; | ||
| 2100 | } | ||
| 2101 | |||
| 2102 | /** | ||
| 2103 | * Walks down an InputParameter AST node, thereby generating the appropriate SQL. | ||
| 2104 | */ | ||
| 2105 | public function walkInputParameter(AST\InputParameter $inputParam): string | ||
| 2106 | { | ||
| 2107 | $this->parserResult->addParameterMapping($inputParam->name, $this->sqlParamIndex++); | ||
| 2108 | |||
| 2109 | $parameter = $this->query->getParameter($inputParam->name); | ||
| 2110 | |||
| 2111 | if ($parameter) { | ||
| 2112 | $type = $parameter->getType(); | ||
| 2113 | if (is_string($type) && Type::hasType($type)) { | ||
| 2114 | return Type::getType($type)->convertToDatabaseValueSQL('?', $this->platform); | ||
| 2115 | } | ||
| 2116 | } | ||
| 2117 | |||
| 2118 | return '?'; | ||
| 2119 | } | ||
| 2120 | |||
| 2121 | /** | ||
| 2122 | * Walks down an ArithmeticExpression AST node, thereby generating the appropriate SQL. | ||
| 2123 | */ | ||
| 2124 | public function walkArithmeticExpression(AST\ArithmeticExpression $arithmeticExpr): string | ||
| 2125 | { | ||
| 2126 | return $arithmeticExpr->isSimpleArithmeticExpression() | ||
| 2127 | ? $this->walkSimpleArithmeticExpression($arithmeticExpr->simpleArithmeticExpression) | ||
| 2128 | : '(' . $this->walkSubselect($arithmeticExpr->subselect) . ')'; | ||
| 2129 | } | ||
| 2130 | |||
| 2131 | /** | ||
| 2132 | * Walks down an SimpleArithmeticExpression AST node, thereby generating the appropriate SQL. | ||
| 2133 | */ | ||
| 2134 | public function walkSimpleArithmeticExpression(AST\Node|string $simpleArithmeticExpr): string | ||
| 2135 | { | ||
| 2136 | if (! ($simpleArithmeticExpr instanceof AST\SimpleArithmeticExpression)) { | ||
| 2137 | return $this->walkArithmeticTerm($simpleArithmeticExpr); | ||
| 2138 | } | ||
| 2139 | |||
| 2140 | return implode(' ', array_map($this->walkArithmeticTerm(...), $simpleArithmeticExpr->arithmeticTerms)); | ||
| 2141 | } | ||
| 2142 | |||
| 2143 | /** | ||
| 2144 | * Walks down an ArithmeticTerm AST node, thereby generating the appropriate SQL. | ||
| 2145 | */ | ||
| 2146 | public function walkArithmeticTerm(AST\Node|string $term): string | ||
| 2147 | { | ||
| 2148 | if (is_string($term)) { | ||
| 2149 | return isset($this->queryComponents[$term]) | ||
| 2150 | ? $this->walkResultVariable($this->queryComponents[$term]['token']->value) | ||
| 2151 | : $term; | ||
| 2152 | } | ||
| 2153 | |||
| 2154 | // Phase 2 AST optimization: Skip processing of ArithmeticTerm | ||
| 2155 | // if only one ArithmeticFactor is defined | ||
| 2156 | if (! ($term instanceof AST\ArithmeticTerm)) { | ||
| 2157 | return $this->walkArithmeticFactor($term); | ||
| 2158 | } | ||
| 2159 | |||
| 2160 | return implode(' ', array_map($this->walkArithmeticFactor(...), $term->arithmeticFactors)); | ||
| 2161 | } | ||
| 2162 | |||
| 2163 | /** | ||
| 2164 | * Walks down an ArithmeticFactor that represents an AST node, thereby generating the appropriate SQL. | ||
| 2165 | */ | ||
| 2166 | public function walkArithmeticFactor(AST\Node|string $factor): string | ||
| 2167 | { | ||
| 2168 | if (is_string($factor)) { | ||
| 2169 | return isset($this->queryComponents[$factor]) | ||
| 2170 | ? $this->walkResultVariable($this->queryComponents[$factor]['token']->value) | ||
| 2171 | : $factor; | ||
| 2172 | } | ||
| 2173 | |||
| 2174 | // Phase 2 AST optimization: Skip processing of ArithmeticFactor | ||
| 2175 | // if only one ArithmeticPrimary is defined | ||
| 2176 | if (! ($factor instanceof AST\ArithmeticFactor)) { | ||
| 2177 | return $this->walkArithmeticPrimary($factor); | ||
| 2178 | } | ||
| 2179 | |||
| 2180 | $sign = $factor->isNegativeSigned() ? '-' : ($factor->isPositiveSigned() ? '+' : ''); | ||
| 2181 | |||
| 2182 | return $sign . $this->walkArithmeticPrimary($factor->arithmeticPrimary); | ||
| 2183 | } | ||
| 2184 | |||
| 2185 | /** | ||
| 2186 | * Walks down an ArithmeticPrimary that represents an AST node, thereby generating the appropriate SQL. | ||
| 2187 | */ | ||
| 2188 | public function walkArithmeticPrimary(AST\Node|string $primary): string | ||
| 2189 | { | ||
| 2190 | if ($primary instanceof AST\SimpleArithmeticExpression) { | ||
| 2191 | return '(' . $this->walkSimpleArithmeticExpression($primary) . ')'; | ||
| 2192 | } | ||
| 2193 | |||
| 2194 | if ($primary instanceof AST\Node) { | ||
| 2195 | return $primary->dispatch($this); | ||
| 2196 | } | ||
| 2197 | |||
| 2198 | return $this->walkEntityIdentificationVariable($primary); | ||
| 2199 | } | ||
| 2200 | |||
| 2201 | /** | ||
| 2202 | * Walks down a StringPrimary that represents an AST node, thereby generating the appropriate SQL. | ||
| 2203 | */ | ||
| 2204 | public function walkStringPrimary(AST\Node|string $stringPrimary): string | ||
| 2205 | { | ||
| 2206 | return is_string($stringPrimary) | ||
| 2207 | ? $this->conn->quote($stringPrimary) | ||
| 2208 | : $stringPrimary->dispatch($this); | ||
| 2209 | } | ||
| 2210 | |||
| 2211 | /** | ||
| 2212 | * Walks down a ResultVariable that represents an AST node, thereby generating the appropriate SQL. | ||
| 2213 | */ | ||
| 2214 | public function walkResultVariable(string $resultVariable): string | ||
| 2215 | { | ||
| 2216 | if (! isset($this->scalarResultAliasMap[$resultVariable])) { | ||
| 2217 | throw new InvalidArgumentException(sprintf('Unknown result variable: %s', $resultVariable)); | ||
| 2218 | } | ||
| 2219 | |||
| 2220 | $resultAlias = $this->scalarResultAliasMap[$resultVariable]; | ||
| 2221 | |||
| 2222 | if (is_array($resultAlias)) { | ||
| 2223 | return implode(', ', $resultAlias); | ||
| 2224 | } | ||
| 2225 | |||
| 2226 | return $resultAlias; | ||
| 2227 | } | ||
| 2228 | |||
| 2229 | /** | ||
| 2230 | * @return string The list in parentheses of valid child discriminators from the given class | ||
| 2231 | * | ||
| 2232 | * @throws QueryException | ||
| 2233 | */ | ||
| 2234 | private function getChildDiscriminatorsFromClassMetadata( | ||
| 2235 | ClassMetadata $rootClass, | ||
| 2236 | AST\InstanceOfExpression $instanceOfExpr, | ||
| 2237 | ): string { | ||
| 2238 | $sqlParameterList = []; | ||
| 2239 | $discriminators = []; | ||
| 2240 | foreach ($instanceOfExpr->value as $parameter) { | ||
| 2241 | if ($parameter instanceof AST\InputParameter) { | ||
| 2242 | $this->rsm->discriminatorParameters[$parameter->name] = $parameter->name; | ||
| 2243 | $sqlParameterList[] = $this->walkInParameter($parameter); | ||
| 2244 | continue; | ||
| 2245 | } | ||
| 2246 | |||
| 2247 | $metadata = $this->em->getClassMetadata($parameter); | ||
| 2248 | |||
| 2249 | if ($metadata->getName() !== $rootClass->name && ! $metadata->getReflectionClass()->isSubclassOf($rootClass->name)) { | ||
| 2250 | throw QueryException::instanceOfUnrelatedClass($parameter, $rootClass->name); | ||
| 2251 | } | ||
| 2252 | |||
| 2253 | $discriminators += HierarchyDiscriminatorResolver::resolveDiscriminatorsForClass($metadata, $this->em); | ||
| 2254 | } | ||
| 2255 | |||
| 2256 | foreach (array_keys($discriminators) as $discriminatorValue) { | ||
| 2257 | $sqlParameterList[] = $rootClass->getDiscriminatorColumn()->type === 'integer' && is_int($discriminatorValue) | ||
| 2258 | ? $discriminatorValue | ||
| 2259 | : $this->conn->quote((string) $discriminatorValue); | ||
| 2260 | } | ||
| 2261 | |||
| 2262 | return '(' . implode(', ', $sqlParameterList) . ')'; | ||
| 2263 | } | ||
| 2264 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/TokenType.php b/vendor/doctrine/orm/src/Query/TokenType.php new file mode 100644 index 0000000..e745e4a --- /dev/null +++ b/vendor/doctrine/orm/src/Query/TokenType.php | |||
| @@ -0,0 +1,91 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Query; | ||
| 6 | |||
| 7 | enum TokenType: int | ||
| 8 | { | ||
| 9 | // All tokens that are not valid identifiers must be < 100 | ||
| 10 | case T_NONE = 1; | ||
| 11 | case T_INTEGER = 2; | ||
| 12 | case T_STRING = 3; | ||
| 13 | case T_INPUT_PARAMETER = 4; | ||
| 14 | case T_FLOAT = 5; | ||
| 15 | case T_CLOSE_PARENTHESIS = 6; | ||
| 16 | case T_OPEN_PARENTHESIS = 7; | ||
| 17 | case T_COMMA = 8; | ||
| 18 | case T_DIVIDE = 9; | ||
| 19 | case T_DOT = 10; | ||
| 20 | case T_EQUALS = 11; | ||
| 21 | case T_GREATER_THAN = 12; | ||
| 22 | case T_LOWER_THAN = 13; | ||
| 23 | case T_MINUS = 14; | ||
| 24 | case T_MULTIPLY = 15; | ||
| 25 | case T_NEGATE = 16; | ||
| 26 | case T_PLUS = 17; | ||
| 27 | case T_OPEN_CURLY_BRACE = 18; | ||
| 28 | case T_CLOSE_CURLY_BRACE = 19; | ||
| 29 | |||
| 30 | // All tokens that are identifiers or keywords that could be considered as identifiers should be >= 100 | ||
| 31 | case T_FULLY_QUALIFIED_NAME = 101; | ||
| 32 | case T_IDENTIFIER = 102; | ||
| 33 | |||
| 34 | // All keyword tokens should be >= 200 | ||
| 35 | case T_ALL = 200; | ||
| 36 | case T_AND = 201; | ||
| 37 | case T_ANY = 202; | ||
| 38 | case T_AS = 203; | ||
| 39 | case T_ASC = 204; | ||
| 40 | case T_AVG = 205; | ||
| 41 | case T_BETWEEN = 206; | ||
| 42 | case T_BOTH = 207; | ||
| 43 | case T_BY = 208; | ||
| 44 | case T_CASE = 209; | ||
| 45 | case T_COALESCE = 210; | ||
| 46 | case T_COUNT = 211; | ||
| 47 | case T_DELETE = 212; | ||
| 48 | case T_DESC = 213; | ||
| 49 | case T_DISTINCT = 214; | ||
| 50 | case T_ELSE = 215; | ||
| 51 | case T_EMPTY = 216; | ||
| 52 | case T_END = 217; | ||
| 53 | case T_ESCAPE = 218; | ||
| 54 | case T_EXISTS = 219; | ||
| 55 | case T_FALSE = 220; | ||
| 56 | case T_FROM = 221; | ||
| 57 | case T_GROUP = 222; | ||
| 58 | case T_HAVING = 223; | ||
| 59 | case T_HIDDEN = 224; | ||
| 60 | case T_IN = 225; | ||
| 61 | case T_INDEX = 226; | ||
| 62 | case T_INNER = 227; | ||
| 63 | case T_INSTANCE = 228; | ||
| 64 | case T_IS = 229; | ||
| 65 | case T_JOIN = 230; | ||
| 66 | case T_LEADING = 231; | ||
| 67 | case T_LEFT = 232; | ||
| 68 | case T_LIKE = 233; | ||
| 69 | case T_MAX = 234; | ||
| 70 | case T_MEMBER = 235; | ||
| 71 | case T_MIN = 236; | ||
| 72 | case T_NEW = 237; | ||
| 73 | case T_NOT = 238; | ||
| 74 | case T_NULL = 239; | ||
| 75 | case T_NULLIF = 240; | ||
| 76 | case T_OF = 241; | ||
| 77 | case T_OR = 242; | ||
| 78 | case T_ORDER = 243; | ||
| 79 | case T_OUTER = 244; | ||
| 80 | case T_SELECT = 246; | ||
| 81 | case T_SET = 247; | ||
| 82 | case T_SOME = 248; | ||
| 83 | case T_SUM = 249; | ||
| 84 | case T_THEN = 250; | ||
| 85 | case T_TRAILING = 251; | ||
| 86 | case T_TRUE = 252; | ||
| 87 | case T_UPDATE = 253; | ||
| 88 | case T_WHEN = 254; | ||
| 89 | case T_WHERE = 255; | ||
| 90 | case T_WITH = 256; | ||
| 91 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/TreeWalker.php b/vendor/doctrine/orm/src/Query/TreeWalker.php new file mode 100644 index 0000000..6c21577 --- /dev/null +++ b/vendor/doctrine/orm/src/Query/TreeWalker.php | |||
| @@ -0,0 +1,44 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Query; | ||
| 6 | |||
| 7 | use Doctrine\ORM\AbstractQuery; | ||
| 8 | |||
| 9 | /** | ||
| 10 | * Interface for walkers of DQL ASTs (abstract syntax trees). | ||
| 11 | * | ||
| 12 | * @psalm-import-type QueryComponent from Parser | ||
| 13 | */ | ||
| 14 | interface TreeWalker | ||
| 15 | { | ||
| 16 | /** | ||
| 17 | * Initializes TreeWalker with important information about the ASTs to be walked. | ||
| 18 | * | ||
| 19 | * @psalm-param array<string, QueryComponent> $queryComponents The query components (symbol table). | ||
| 20 | */ | ||
| 21 | public function __construct(AbstractQuery $query, ParserResult $parserResult, array $queryComponents); | ||
| 22 | |||
| 23 | /** | ||
| 24 | * Returns internal queryComponents array. | ||
| 25 | * | ||
| 26 | * @psalm-return array<string, QueryComponent> | ||
| 27 | */ | ||
| 28 | public function getQueryComponents(): array; | ||
| 29 | |||
| 30 | /** | ||
| 31 | * Walks down a SelectStatement AST node. | ||
| 32 | */ | ||
| 33 | public function walkSelectStatement(AST\SelectStatement $selectStatement): void; | ||
| 34 | |||
| 35 | /** | ||
| 36 | * Walks down an UpdateStatement AST node. | ||
| 37 | */ | ||
| 38 | public function walkUpdateStatement(AST\UpdateStatement $updateStatement): void; | ||
| 39 | |||
| 40 | /** | ||
| 41 | * Walks down a DeleteStatement AST node. | ||
| 42 | */ | ||
| 43 | public function walkDeleteStatement(AST\DeleteStatement $deleteStatement): void; | ||
| 44 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/TreeWalkerAdapter.php b/vendor/doctrine/orm/src/Query/TreeWalkerAdapter.php new file mode 100644 index 0000000..a7948db --- /dev/null +++ b/vendor/doctrine/orm/src/Query/TreeWalkerAdapter.php | |||
| @@ -0,0 +1,90 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Query; | ||
| 6 | |||
| 7 | use Doctrine\ORM\AbstractQuery; | ||
| 8 | use Doctrine\ORM\Mapping\ClassMetadata; | ||
| 9 | use LogicException; | ||
| 10 | |||
| 11 | use function array_diff; | ||
| 12 | use function array_keys; | ||
| 13 | use function sprintf; | ||
| 14 | |||
| 15 | /** | ||
| 16 | * An adapter implementation of the TreeWalker interface. The methods in this class | ||
| 17 | * are empty. This class exists as convenience for creating tree walkers. | ||
| 18 | * | ||
| 19 | * @psalm-import-type QueryComponent from Parser | ||
| 20 | */ | ||
| 21 | abstract class TreeWalkerAdapter implements TreeWalker | ||
| 22 | { | ||
| 23 | /** | ||
| 24 | * {@inheritDoc} | ||
| 25 | */ | ||
| 26 | public function __construct( | ||
| 27 | private readonly AbstractQuery $query, | ||
| 28 | private readonly ParserResult $parserResult, | ||
| 29 | private array $queryComponents, | ||
| 30 | ) { | ||
| 31 | } | ||
| 32 | |||
| 33 | /** | ||
| 34 | * {@inheritDoc} | ||
| 35 | */ | ||
| 36 | public function getQueryComponents(): array | ||
| 37 | { | ||
| 38 | return $this->queryComponents; | ||
| 39 | } | ||
| 40 | |||
| 41 | public function walkSelectStatement(AST\SelectStatement $selectStatement): void | ||
| 42 | { | ||
| 43 | } | ||
| 44 | |||
| 45 | public function walkUpdateStatement(AST\UpdateStatement $updateStatement): void | ||
| 46 | { | ||
| 47 | } | ||
| 48 | |||
| 49 | public function walkDeleteStatement(AST\DeleteStatement $deleteStatement): void | ||
| 50 | { | ||
| 51 | } | ||
| 52 | |||
| 53 | /** | ||
| 54 | * Sets or overrides a query component for a given dql alias. | ||
| 55 | * | ||
| 56 | * @psalm-param QueryComponent $queryComponent | ||
| 57 | */ | ||
| 58 | protected function setQueryComponent(string $dqlAlias, array $queryComponent): void | ||
| 59 | { | ||
| 60 | $requiredKeys = ['metadata', 'parent', 'relation', 'map', 'nestingLevel', 'token']; | ||
| 61 | |||
| 62 | if (array_diff($requiredKeys, array_keys($queryComponent))) { | ||
| 63 | throw QueryException::invalidQueryComponent($dqlAlias); | ||
| 64 | } | ||
| 65 | |||
| 66 | $this->queryComponents[$dqlAlias] = $queryComponent; | ||
| 67 | } | ||
| 68 | |||
| 69 | /** | ||
| 70 | * Retrieves the Query Instance responsible for the current walkers execution. | ||
| 71 | */ | ||
| 72 | protected function _getQuery(): AbstractQuery | ||
| 73 | { | ||
| 74 | return $this->query; | ||
| 75 | } | ||
| 76 | |||
| 77 | /** | ||
| 78 | * Retrieves the ParserResult. | ||
| 79 | */ | ||
| 80 | protected function _getParserResult(): ParserResult | ||
| 81 | { | ||
| 82 | return $this->parserResult; | ||
| 83 | } | ||
| 84 | |||
| 85 | protected function getMetadataForDqlAlias(string $dqlAlias): ClassMetadata | ||
| 86 | { | ||
| 87 | return $this->queryComponents[$dqlAlias]['metadata'] | ||
| 88 | ?? throw new LogicException(sprintf('No metadata for DQL alias: %s', $dqlAlias)); | ||
| 89 | } | ||
| 90 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/TreeWalkerChain.php b/vendor/doctrine/orm/src/Query/TreeWalkerChain.php new file mode 100644 index 0000000..7bb3051 --- /dev/null +++ b/vendor/doctrine/orm/src/Query/TreeWalkerChain.php | |||
| @@ -0,0 +1,88 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Query; | ||
| 6 | |||
| 7 | use Doctrine\ORM\AbstractQuery; | ||
| 8 | use Generator; | ||
| 9 | |||
| 10 | /** | ||
| 11 | * Represents a chain of tree walkers that modify an AST and finally emit output. | ||
| 12 | * Only the last walker in the chain can emit output. Any previous walkers can modify | ||
| 13 | * the AST to influence the final output produced by the last walker. | ||
| 14 | * | ||
| 15 | * @psalm-import-type QueryComponent from Parser | ||
| 16 | */ | ||
| 17 | class TreeWalkerChain implements TreeWalker | ||
| 18 | { | ||
| 19 | /** | ||
| 20 | * The tree walkers. | ||
| 21 | * | ||
| 22 | * @var string[] | ||
| 23 | * @psalm-var list<class-string<TreeWalker>> | ||
| 24 | */ | ||
| 25 | private array $walkers = []; | ||
| 26 | |||
| 27 | /** | ||
| 28 | * {@inheritDoc} | ||
| 29 | */ | ||
| 30 | public function __construct( | ||
| 31 | private readonly AbstractQuery $query, | ||
| 32 | private readonly ParserResult $parserResult, | ||
| 33 | private array $queryComponents, | ||
| 34 | ) { | ||
| 35 | } | ||
| 36 | |||
| 37 | /** | ||
| 38 | * Returns the internal queryComponents array. | ||
| 39 | * | ||
| 40 | * {@inheritDoc} | ||
| 41 | */ | ||
| 42 | public function getQueryComponents(): array | ||
| 43 | { | ||
| 44 | return $this->queryComponents; | ||
| 45 | } | ||
| 46 | |||
| 47 | /** | ||
| 48 | * Adds a tree walker to the chain. | ||
| 49 | * | ||
| 50 | * @param string $walkerClass The class of the walker to instantiate. | ||
| 51 | * @psalm-param class-string<TreeWalker> $walkerClass | ||
| 52 | */ | ||
| 53 | public function addTreeWalker(string $walkerClass): void | ||
| 54 | { | ||
| 55 | $this->walkers[] = $walkerClass; | ||
| 56 | } | ||
| 57 | |||
| 58 | public function walkSelectStatement(AST\SelectStatement $selectStatement): void | ||
| 59 | { | ||
| 60 | foreach ($this->getWalkers() as $walker) { | ||
| 61 | $walker->walkSelectStatement($selectStatement); | ||
| 62 | |||
| 63 | $this->queryComponents = $walker->getQueryComponents(); | ||
| 64 | } | ||
| 65 | } | ||
| 66 | |||
| 67 | public function walkUpdateStatement(AST\UpdateStatement $updateStatement): void | ||
| 68 | { | ||
| 69 | foreach ($this->getWalkers() as $walker) { | ||
| 70 | $walker->walkUpdateStatement($updateStatement); | ||
| 71 | } | ||
| 72 | } | ||
| 73 | |||
| 74 | public function walkDeleteStatement(AST\DeleteStatement $deleteStatement): void | ||
| 75 | { | ||
| 76 | foreach ($this->getWalkers() as $walker) { | ||
| 77 | $walker->walkDeleteStatement($deleteStatement); | ||
| 78 | } | ||
| 79 | } | ||
| 80 | |||
| 81 | /** @psalm-return Generator<int, TreeWalker> */ | ||
| 82 | private function getWalkers(): Generator | ||
| 83 | { | ||
| 84 | foreach ($this->walkers as $walkerClass) { | ||
| 85 | yield new $walkerClass($this->query, $this->parserResult, $this->queryComponents); | ||
| 86 | } | ||
| 87 | } | ||
| 88 | } | ||
diff --git a/vendor/doctrine/orm/src/QueryBuilder.php b/vendor/doctrine/orm/src/QueryBuilder.php new file mode 100644 index 0000000..a6a39a9 --- /dev/null +++ b/vendor/doctrine/orm/src/QueryBuilder.php | |||
| @@ -0,0 +1,1375 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM; | ||
| 6 | |||
| 7 | use Doctrine\Common\Collections\ArrayCollection; | ||
| 8 | use Doctrine\Common\Collections\Criteria; | ||
| 9 | use Doctrine\DBAL\ArrayParameterType; | ||
| 10 | use Doctrine\DBAL\ParameterType; | ||
| 11 | use Doctrine\ORM\Internal\NoUnknownNamedArguments; | ||
| 12 | use Doctrine\ORM\Internal\QueryType; | ||
| 13 | use Doctrine\ORM\Query\Expr; | ||
| 14 | use Doctrine\ORM\Query\Parameter; | ||
| 15 | use Doctrine\ORM\Query\QueryExpressionVisitor; | ||
| 16 | use InvalidArgumentException; | ||
| 17 | use RuntimeException; | ||
| 18 | use Stringable; | ||
| 19 | |||
| 20 | use function array_keys; | ||
| 21 | use function array_unshift; | ||
| 22 | use function assert; | ||
| 23 | use function count; | ||
| 24 | use function implode; | ||
| 25 | use function in_array; | ||
| 26 | use function is_array; | ||
| 27 | use function is_numeric; | ||
| 28 | use function is_object; | ||
| 29 | use function is_string; | ||
| 30 | use function key; | ||
| 31 | use function reset; | ||
| 32 | use function sprintf; | ||
| 33 | use function str_starts_with; | ||
| 34 | use function strpos; | ||
| 35 | use function strrpos; | ||
| 36 | use function substr; | ||
| 37 | |||
| 38 | /** | ||
| 39 | * This class is responsible for building DQL query strings via an object oriented | ||
| 40 | * PHP interface. | ||
| 41 | */ | ||
| 42 | class QueryBuilder implements Stringable | ||
| 43 | { | ||
| 44 | use NoUnknownNamedArguments; | ||
| 45 | |||
| 46 | /** | ||
| 47 | * The array of DQL parts collected. | ||
| 48 | * | ||
| 49 | * @psalm-var array<string, mixed> | ||
| 50 | */ | ||
| 51 | private array $dqlParts = [ | ||
| 52 | 'distinct' => false, | ||
| 53 | 'select' => [], | ||
| 54 | 'from' => [], | ||
| 55 | 'join' => [], | ||
| 56 | 'set' => [], | ||
| 57 | 'where' => null, | ||
| 58 | 'groupBy' => [], | ||
| 59 | 'having' => null, | ||
| 60 | 'orderBy' => [], | ||
| 61 | ]; | ||
| 62 | |||
| 63 | private QueryType $type = QueryType::Select; | ||
| 64 | |||
| 65 | /** | ||
| 66 | * The complete DQL string for this query. | ||
| 67 | */ | ||
| 68 | private string|null $dql = null; | ||
| 69 | |||
| 70 | /** | ||
| 71 | * The query parameters. | ||
| 72 | * | ||
| 73 | * @psalm-var ArrayCollection<int, Parameter> | ||
| 74 | */ | ||
| 75 | private ArrayCollection $parameters; | ||
| 76 | |||
| 77 | /** | ||
| 78 | * The index of the first result to retrieve. | ||
| 79 | */ | ||
| 80 | private int $firstResult = 0; | ||
| 81 | |||
| 82 | /** | ||
| 83 | * The maximum number of results to retrieve. | ||
| 84 | */ | ||
| 85 | private int|null $maxResults = null; | ||
| 86 | |||
| 87 | /** | ||
| 88 | * Keeps root entity alias names for join entities. | ||
| 89 | * | ||
| 90 | * @psalm-var array<string, string> | ||
| 91 | */ | ||
| 92 | private array $joinRootAliases = []; | ||
| 93 | |||
| 94 | /** | ||
| 95 | * Whether to use second level cache, if available. | ||
| 96 | */ | ||
| 97 | protected bool $cacheable = false; | ||
| 98 | |||
| 99 | /** | ||
| 100 | * Second level cache region name. | ||
| 101 | */ | ||
| 102 | protected string|null $cacheRegion = null; | ||
| 103 | |||
| 104 | /** | ||
| 105 | * Second level query cache mode. | ||
| 106 | * | ||
| 107 | * @psalm-var Cache::MODE_*|null | ||
| 108 | */ | ||
| 109 | protected int|null $cacheMode = null; | ||
| 110 | |||
| 111 | protected int $lifetime = 0; | ||
| 112 | |||
| 113 | /** | ||
| 114 | * Initializes a new <tt>QueryBuilder</tt> that uses the given <tt>EntityManager</tt>. | ||
| 115 | * | ||
| 116 | * @param EntityManagerInterface $em The EntityManager to use. | ||
| 117 | */ | ||
| 118 | public function __construct( | ||
| 119 | private readonly EntityManagerInterface $em, | ||
| 120 | ) { | ||
| 121 | $this->parameters = new ArrayCollection(); | ||
| 122 | } | ||
| 123 | |||
| 124 | /** | ||
| 125 | * Gets an ExpressionBuilder used for object-oriented construction of query expressions. | ||
| 126 | * This producer method is intended for convenient inline usage. Example: | ||
| 127 | * | ||
| 128 | * <code> | ||
| 129 | * $qb = $em->createQueryBuilder(); | ||
| 130 | * $qb | ||
| 131 | * ->select('u') | ||
| 132 | * ->from('User', 'u') | ||
| 133 | * ->where($qb->expr()->eq('u.id', 1)); | ||
| 134 | * </code> | ||
| 135 | * | ||
| 136 | * For more complex expression construction, consider storing the expression | ||
| 137 | * builder object in a local variable. | ||
| 138 | */ | ||
| 139 | public function expr(): Expr | ||
| 140 | { | ||
| 141 | return $this->em->getExpressionBuilder(); | ||
| 142 | } | ||
| 143 | |||
| 144 | /** | ||
| 145 | * Enable/disable second level query (result) caching for this query. | ||
| 146 | * | ||
| 147 | * @return $this | ||
| 148 | */ | ||
| 149 | public function setCacheable(bool $cacheable): static | ||
| 150 | { | ||
| 151 | $this->cacheable = $cacheable; | ||
| 152 | |||
| 153 | return $this; | ||
| 154 | } | ||
| 155 | |||
| 156 | /** | ||
| 157 | * Are the query results enabled for second level cache? | ||
| 158 | */ | ||
| 159 | public function isCacheable(): bool | ||
| 160 | { | ||
| 161 | return $this->cacheable; | ||
| 162 | } | ||
| 163 | |||
| 164 | /** @return $this */ | ||
| 165 | public function setCacheRegion(string $cacheRegion): static | ||
| 166 | { | ||
| 167 | $this->cacheRegion = $cacheRegion; | ||
| 168 | |||
| 169 | return $this; | ||
| 170 | } | ||
| 171 | |||
| 172 | /** | ||
| 173 | * Obtain the name of the second level query cache region in which query results will be stored | ||
| 174 | * | ||
| 175 | * @return string|null The cache region name; NULL indicates the default region. | ||
| 176 | */ | ||
| 177 | public function getCacheRegion(): string|null | ||
| 178 | { | ||
| 179 | return $this->cacheRegion; | ||
| 180 | } | ||
| 181 | |||
| 182 | public function getLifetime(): int | ||
| 183 | { | ||
| 184 | return $this->lifetime; | ||
| 185 | } | ||
| 186 | |||
| 187 | /** | ||
| 188 | * Sets the life-time for this query into second level cache. | ||
| 189 | * | ||
| 190 | * @return $this | ||
| 191 | */ | ||
| 192 | public function setLifetime(int $lifetime): static | ||
| 193 | { | ||
| 194 | $this->lifetime = $lifetime; | ||
| 195 | |||
| 196 | return $this; | ||
| 197 | } | ||
| 198 | |||
| 199 | /** @psalm-return Cache::MODE_*|null */ | ||
| 200 | public function getCacheMode(): int|null | ||
| 201 | { | ||
| 202 | return $this->cacheMode; | ||
| 203 | } | ||
| 204 | |||
| 205 | /** | ||
| 206 | * @psalm-param Cache::MODE_* $cacheMode | ||
| 207 | * | ||
| 208 | * @return $this | ||
| 209 | */ | ||
| 210 | public function setCacheMode(int $cacheMode): static | ||
| 211 | { | ||
| 212 | $this->cacheMode = $cacheMode; | ||
| 213 | |||
| 214 | return $this; | ||
| 215 | } | ||
| 216 | |||
| 217 | /** | ||
| 218 | * Gets the associated EntityManager for this query builder. | ||
| 219 | */ | ||
| 220 | public function getEntityManager(): EntityManagerInterface | ||
| 221 | { | ||
| 222 | return $this->em; | ||
| 223 | } | ||
| 224 | |||
| 225 | /** | ||
| 226 | * Gets the complete DQL string formed by the current specifications of this QueryBuilder. | ||
| 227 | * | ||
| 228 | * <code> | ||
| 229 | * $qb = $em->createQueryBuilder() | ||
| 230 | * ->select('u') | ||
| 231 | * ->from('User', 'u'); | ||
| 232 | * echo $qb->getDql(); // SELECT u FROM User u | ||
| 233 | * </code> | ||
| 234 | */ | ||
| 235 | public function getDQL(): string | ||
| 236 | { | ||
| 237 | return $this->dql ??= match ($this->type) { | ||
| 238 | QueryType::Select => $this->getDQLForSelect(), | ||
| 239 | QueryType::Delete => $this->getDQLForDelete(), | ||
| 240 | QueryType::Update => $this->getDQLForUpdate(), | ||
| 241 | }; | ||
| 242 | } | ||
| 243 | |||
| 244 | /** | ||
| 245 | * Constructs a Query instance from the current specifications of the builder. | ||
| 246 | * | ||
| 247 | * <code> | ||
| 248 | * $qb = $em->createQueryBuilder() | ||
| 249 | * ->select('u') | ||
| 250 | * ->from('User', 'u'); | ||
| 251 | * $q = $qb->getQuery(); | ||
| 252 | * $results = $q->execute(); | ||
| 253 | * </code> | ||
| 254 | */ | ||
| 255 | public function getQuery(): Query | ||
| 256 | { | ||
| 257 | $parameters = clone $this->parameters; | ||
| 258 | $query = $this->em->createQuery($this->getDQL()) | ||
| 259 | ->setParameters($parameters) | ||
| 260 | ->setFirstResult($this->firstResult) | ||
| 261 | ->setMaxResults($this->maxResults); | ||
| 262 | |||
| 263 | if ($this->lifetime) { | ||
| 264 | $query->setLifetime($this->lifetime); | ||
| 265 | } | ||
| 266 | |||
| 267 | if ($this->cacheMode) { | ||
| 268 | $query->setCacheMode($this->cacheMode); | ||
| 269 | } | ||
| 270 | |||
| 271 | if ($this->cacheable) { | ||
| 272 | $query->setCacheable($this->cacheable); | ||
| 273 | } | ||
| 274 | |||
| 275 | if ($this->cacheRegion) { | ||
| 276 | $query->setCacheRegion($this->cacheRegion); | ||
| 277 | } | ||
| 278 | |||
| 279 | return $query; | ||
| 280 | } | ||
| 281 | |||
| 282 | /** | ||
| 283 | * Finds the root entity alias of the joined entity. | ||
| 284 | * | ||
| 285 | * @param string $alias The alias of the new join entity | ||
| 286 | * @param string $parentAlias The parent entity alias of the join relationship | ||
| 287 | */ | ||
| 288 | private function findRootAlias(string $alias, string $parentAlias): string | ||
| 289 | { | ||
| 290 | if (in_array($parentAlias, $this->getRootAliases(), true)) { | ||
| 291 | $rootAlias = $parentAlias; | ||
| 292 | } elseif (isset($this->joinRootAliases[$parentAlias])) { | ||
| 293 | $rootAlias = $this->joinRootAliases[$parentAlias]; | ||
| 294 | } else { | ||
| 295 | // Should never happen with correct joining order. Might be | ||
| 296 | // thoughtful to throw exception instead. | ||
| 297 | $rootAlias = $this->getRootAlias(); | ||
| 298 | } | ||
| 299 | |||
| 300 | $this->joinRootAliases[$alias] = $rootAlias; | ||
| 301 | |||
| 302 | return $rootAlias; | ||
| 303 | } | ||
| 304 | |||
| 305 | /** | ||
| 306 | * Gets the FIRST root alias of the query. This is the first entity alias involved | ||
| 307 | * in the construction of the query. | ||
| 308 | * | ||
| 309 | * <code> | ||
| 310 | * $qb = $em->createQueryBuilder() | ||
| 311 | * ->select('u') | ||
| 312 | * ->from('User', 'u'); | ||
| 313 | * | ||
| 314 | * echo $qb->getRootAlias(); // u | ||
| 315 | * </code> | ||
| 316 | * | ||
| 317 | * @deprecated Please use $qb->getRootAliases() instead. | ||
| 318 | * | ||
| 319 | * @throws RuntimeException | ||
| 320 | */ | ||
| 321 | public function getRootAlias(): string | ||
| 322 | { | ||
| 323 | $aliases = $this->getRootAliases(); | ||
| 324 | |||
| 325 | if (! isset($aliases[0])) { | ||
| 326 | throw new RuntimeException('No alias was set before invoking getRootAlias().'); | ||
| 327 | } | ||
| 328 | |||
| 329 | return $aliases[0]; | ||
| 330 | } | ||
| 331 | |||
| 332 | /** | ||
| 333 | * Gets the root aliases of the query. This is the entity aliases involved | ||
| 334 | * in the construction of the query. | ||
| 335 | * | ||
| 336 | * <code> | ||
| 337 | * $qb = $em->createQueryBuilder() | ||
| 338 | * ->select('u') | ||
| 339 | * ->from('User', 'u'); | ||
| 340 | * | ||
| 341 | * $qb->getRootAliases(); // array('u') | ||
| 342 | * </code> | ||
| 343 | * | ||
| 344 | * @return string[] | ||
| 345 | * @psalm-return list<string> | ||
| 346 | */ | ||
| 347 | public function getRootAliases(): array | ||
| 348 | { | ||
| 349 | $aliases = []; | ||
| 350 | |||
| 351 | foreach ($this->dqlParts['from'] as &$fromClause) { | ||
| 352 | if (is_string($fromClause)) { | ||
| 353 | $spacePos = strrpos($fromClause, ' '); | ||
| 354 | |||
| 355 | /** @psalm-var class-string $from */ | ||
| 356 | $from = substr($fromClause, 0, $spacePos); | ||
| 357 | $alias = substr($fromClause, $spacePos + 1); | ||
| 358 | |||
| 359 | $fromClause = new Query\Expr\From($from, $alias); | ||
| 360 | } | ||
| 361 | |||
| 362 | $aliases[] = $fromClause->getAlias(); | ||
| 363 | } | ||
| 364 | |||
| 365 | return $aliases; | ||
| 366 | } | ||
| 367 | |||
| 368 | /** | ||
| 369 | * Gets all the aliases that have been used in the query. | ||
| 370 | * Including all select root aliases and join aliases | ||
| 371 | * | ||
| 372 | * <code> | ||
| 373 | * $qb = $em->createQueryBuilder() | ||
| 374 | * ->select('u') | ||
| 375 | * ->from('User', 'u') | ||
| 376 | * ->join('u.articles','a'); | ||
| 377 | * | ||
| 378 | * $qb->getAllAliases(); // array('u','a') | ||
| 379 | * </code> | ||
| 380 | * | ||
| 381 | * @return string[] | ||
| 382 | * @psalm-return list<string> | ||
| 383 | */ | ||
| 384 | public function getAllAliases(): array | ||
| 385 | { | ||
| 386 | return [...$this->getRootAliases(), ...array_keys($this->joinRootAliases)]; | ||
| 387 | } | ||
| 388 | |||
| 389 | /** | ||
| 390 | * Gets the root entities of the query. This is the entity classes involved | ||
| 391 | * in the construction of the query. | ||
| 392 | * | ||
| 393 | * <code> | ||
| 394 | * $qb = $em->createQueryBuilder() | ||
| 395 | * ->select('u') | ||
| 396 | * ->from('User', 'u'); | ||
| 397 | * | ||
| 398 | * $qb->getRootEntities(); // array('User') | ||
| 399 | * </code> | ||
| 400 | * | ||
| 401 | * @return string[] | ||
| 402 | * @psalm-return list<class-string> | ||
| 403 | */ | ||
| 404 | public function getRootEntities(): array | ||
| 405 | { | ||
| 406 | $entities = []; | ||
| 407 | |||
| 408 | foreach ($this->dqlParts['from'] as &$fromClause) { | ||
| 409 | if (is_string($fromClause)) { | ||
| 410 | $spacePos = strrpos($fromClause, ' '); | ||
| 411 | |||
| 412 | /** @psalm-var class-string $from */ | ||
| 413 | $from = substr($fromClause, 0, $spacePos); | ||
| 414 | $alias = substr($fromClause, $spacePos + 1); | ||
| 415 | |||
| 416 | $fromClause = new Query\Expr\From($from, $alias); | ||
| 417 | } | ||
| 418 | |||
| 419 | $entities[] = $fromClause->getFrom(); | ||
| 420 | } | ||
| 421 | |||
| 422 | return $entities; | ||
| 423 | } | ||
| 424 | |||
| 425 | /** | ||
| 426 | * Sets a query parameter for the query being constructed. | ||
| 427 | * | ||
| 428 | * <code> | ||
| 429 | * $qb = $em->createQueryBuilder() | ||
| 430 | * ->select('u') | ||
| 431 | * ->from('User', 'u') | ||
| 432 | * ->where('u.id = :user_id') | ||
| 433 | * ->setParameter('user_id', 1); | ||
| 434 | * </code> | ||
| 435 | * | ||
| 436 | * @param string|int $key The parameter position or name. | ||
| 437 | * @param ParameterType|ArrayParameterType|string|int|null $type ParameterType::*, ArrayParameterType::* or \Doctrine\DBAL\Types\Type::* constant | ||
| 438 | * | ||
| 439 | * @return $this | ||
| 440 | */ | ||
| 441 | public function setParameter(string|int $key, mixed $value, ParameterType|ArrayParameterType|string|int|null $type = null): static | ||
| 442 | { | ||
| 443 | $existingParameter = $this->getParameter($key); | ||
| 444 | |||
| 445 | if ($existingParameter !== null) { | ||
| 446 | $existingParameter->setValue($value, $type); | ||
| 447 | |||
| 448 | return $this; | ||
| 449 | } | ||
| 450 | |||
| 451 | $this->parameters->add(new Parameter($key, $value, $type)); | ||
| 452 | |||
| 453 | return $this; | ||
| 454 | } | ||
| 455 | |||
| 456 | /** | ||
| 457 | * Sets a collection of query parameters for the query being constructed. | ||
| 458 | * | ||
| 459 | * <code> | ||
| 460 | * $qb = $em->createQueryBuilder() | ||
| 461 | * ->select('u') | ||
| 462 | * ->from('User', 'u') | ||
| 463 | * ->where('u.id = :user_id1 OR u.id = :user_id2') | ||
| 464 | * ->setParameters(new ArrayCollection(array( | ||
| 465 | * new Parameter('user_id1', 1), | ||
| 466 | * new Parameter('user_id2', 2) | ||
| 467 | * ))); | ||
| 468 | * </code> | ||
| 469 | * | ||
| 470 | * @psalm-param ArrayCollection<int, Parameter> $parameters | ||
| 471 | * | ||
| 472 | * @return $this | ||
| 473 | */ | ||
| 474 | public function setParameters(ArrayCollection $parameters): static | ||
| 475 | { | ||
| 476 | $this->parameters = $parameters; | ||
| 477 | |||
| 478 | return $this; | ||
| 479 | } | ||
| 480 | |||
| 481 | /** | ||
| 482 | * Gets all defined query parameters for the query being constructed. | ||
| 483 | * | ||
| 484 | * @psalm-return ArrayCollection<int, Parameter> | ||
| 485 | */ | ||
| 486 | public function getParameters(): ArrayCollection | ||
| 487 | { | ||
| 488 | return $this->parameters; | ||
| 489 | } | ||
| 490 | |||
| 491 | /** | ||
| 492 | * Gets a (previously set) query parameter of the query being constructed. | ||
| 493 | */ | ||
| 494 | public function getParameter(string|int $key): Parameter|null | ||
| 495 | { | ||
| 496 | $key = Parameter::normalizeName($key); | ||
| 497 | |||
| 498 | $filteredParameters = $this->parameters->filter( | ||
| 499 | static fn (Parameter $parameter): bool => $key === $parameter->getName() | ||
| 500 | ); | ||
| 501 | |||
| 502 | return ! $filteredParameters->isEmpty() ? $filteredParameters->first() : null; | ||
| 503 | } | ||
| 504 | |||
| 505 | /** | ||
| 506 | * Sets the position of the first result to retrieve (the "offset"). | ||
| 507 | * | ||
| 508 | * @return $this | ||
| 509 | */ | ||
| 510 | public function setFirstResult(int|null $firstResult): static | ||
| 511 | { | ||
| 512 | $this->firstResult = (int) $firstResult; | ||
| 513 | |||
| 514 | return $this; | ||
| 515 | } | ||
| 516 | |||
| 517 | /** | ||
| 518 | * Gets the position of the first result the query object was set to retrieve (the "offset"). | ||
| 519 | */ | ||
| 520 | public function getFirstResult(): int | ||
| 521 | { | ||
| 522 | return $this->firstResult; | ||
| 523 | } | ||
| 524 | |||
| 525 | /** | ||
| 526 | * Sets the maximum number of results to retrieve (the "limit"). | ||
| 527 | * | ||
| 528 | * @return $this | ||
| 529 | */ | ||
| 530 | public function setMaxResults(int|null $maxResults): static | ||
| 531 | { | ||
| 532 | $this->maxResults = $maxResults; | ||
| 533 | |||
| 534 | return $this; | ||
| 535 | } | ||
| 536 | |||
| 537 | /** | ||
| 538 | * Gets the maximum number of results the query object was set to retrieve (the "limit"). | ||
| 539 | * Returns NULL if {@link setMaxResults} was not applied to this query builder. | ||
| 540 | */ | ||
| 541 | public function getMaxResults(): int|null | ||
| 542 | { | ||
| 543 | return $this->maxResults; | ||
| 544 | } | ||
| 545 | |||
| 546 | /** | ||
| 547 | * Either appends to or replaces a single, generic query part. | ||
| 548 | * | ||
| 549 | * The available parts are: 'select', 'from', 'join', 'set', 'where', | ||
| 550 | * 'groupBy', 'having' and 'orderBy'. | ||
| 551 | * | ||
| 552 | * @psalm-param string|object|list<string>|array{join: array<int|string, object>} $dqlPart | ||
| 553 | * | ||
| 554 | * @return $this | ||
| 555 | */ | ||
| 556 | public function add(string $dqlPartName, string|object|array $dqlPart, bool $append = false): static | ||
| 557 | { | ||
| 558 | if ($append && ($dqlPartName === 'where' || $dqlPartName === 'having')) { | ||
| 559 | throw new InvalidArgumentException( | ||
| 560 | "Using \$append = true does not have an effect with 'where' or 'having' " . | ||
| 561 | 'parts. See QueryBuilder#andWhere() for an example for correct usage.', | ||
| 562 | ); | ||
| 563 | } | ||
| 564 | |||
| 565 | $isMultiple = is_array($this->dqlParts[$dqlPartName]) | ||
| 566 | && ! ($dqlPartName === 'join' && ! $append); | ||
| 567 | |||
| 568 | // Allow adding any part retrieved from self::getDQLParts(). | ||
| 569 | if (is_array($dqlPart) && $dqlPartName !== 'join') { | ||
| 570 | $dqlPart = reset($dqlPart); | ||
| 571 | } | ||
| 572 | |||
| 573 | // This is introduced for backwards compatibility reasons. | ||
| 574 | // TODO: Remove for 3.0 | ||
| 575 | if ($dqlPartName === 'join') { | ||
| 576 | $newDqlPart = []; | ||
| 577 | |||
| 578 | foreach ($dqlPart as $k => $v) { | ||
| 579 | $k = is_numeric($k) ? $this->getRootAlias() : $k; | ||
| 580 | |||
| 581 | $newDqlPart[$k] = $v; | ||
| 582 | } | ||
| 583 | |||
| 584 | $dqlPart = $newDqlPart; | ||
| 585 | } | ||
| 586 | |||
| 587 | if ($append && $isMultiple) { | ||
| 588 | if (is_array($dqlPart)) { | ||
| 589 | $key = key($dqlPart); | ||
| 590 | |||
| 591 | $this->dqlParts[$dqlPartName][$key][] = $dqlPart[$key]; | ||
| 592 | } else { | ||
| 593 | $this->dqlParts[$dqlPartName][] = $dqlPart; | ||
| 594 | } | ||
| 595 | } else { | ||
| 596 | $this->dqlParts[$dqlPartName] = $isMultiple ? [$dqlPart] : $dqlPart; | ||
| 597 | } | ||
| 598 | |||
| 599 | $this->dql = null; | ||
| 600 | |||
| 601 | return $this; | ||
| 602 | } | ||
| 603 | |||
| 604 | /** | ||
| 605 | * Specifies an item that is to be returned in the query result. | ||
| 606 | * Replaces any previously specified selections, if any. | ||
| 607 | * | ||
| 608 | * <code> | ||
| 609 | * $qb = $em->createQueryBuilder() | ||
| 610 | * ->select('u', 'p') | ||
| 611 | * ->from('User', 'u') | ||
| 612 | * ->leftJoin('u.Phonenumbers', 'p'); | ||
| 613 | * </code> | ||
| 614 | * | ||
| 615 | * @return $this | ||
| 616 | */ | ||
| 617 | public function select(mixed ...$select): static | ||
| 618 | { | ||
| 619 | self::validateVariadicParameter($select); | ||
| 620 | |||
| 621 | $this->type = QueryType::Select; | ||
| 622 | |||
| 623 | if ($select === []) { | ||
| 624 | return $this; | ||
| 625 | } | ||
| 626 | |||
| 627 | return $this->add('select', new Expr\Select($select), false); | ||
| 628 | } | ||
| 629 | |||
| 630 | /** | ||
| 631 | * Adds a DISTINCT flag to this query. | ||
| 632 | * | ||
| 633 | * <code> | ||
| 634 | * $qb = $em->createQueryBuilder() | ||
| 635 | * ->select('u') | ||
| 636 | * ->distinct() | ||
| 637 | * ->from('User', 'u'); | ||
| 638 | * </code> | ||
| 639 | * | ||
| 640 | * @return $this | ||
| 641 | */ | ||
| 642 | public function distinct(bool $flag = true): static | ||
| 643 | { | ||
| 644 | if ($this->dqlParts['distinct'] !== $flag) { | ||
| 645 | $this->dqlParts['distinct'] = $flag; | ||
| 646 | $this->dql = null; | ||
| 647 | } | ||
| 648 | |||
| 649 | return $this; | ||
| 650 | } | ||
| 651 | |||
| 652 | /** | ||
| 653 | * Adds an item that is to be returned in the query result. | ||
| 654 | * | ||
| 655 | * <code> | ||
| 656 | * $qb = $em->createQueryBuilder() | ||
| 657 | * ->select('u') | ||
| 658 | * ->addSelect('p') | ||
| 659 | * ->from('User', 'u') | ||
| 660 | * ->leftJoin('u.Phonenumbers', 'p'); | ||
| 661 | * </code> | ||
| 662 | * | ||
| 663 | * @return $this | ||
| 664 | */ | ||
| 665 | public function addSelect(mixed ...$select): static | ||
| 666 | { | ||
| 667 | self::validateVariadicParameter($select); | ||
| 668 | |||
| 669 | $this->type = QueryType::Select; | ||
| 670 | |||
| 671 | if ($select === []) { | ||
| 672 | return $this; | ||
| 673 | } | ||
| 674 | |||
| 675 | return $this->add('select', new Expr\Select($select), true); | ||
| 676 | } | ||
| 677 | |||
| 678 | /** | ||
| 679 | * Turns the query being built into a bulk delete query that ranges over | ||
| 680 | * a certain entity type. | ||
| 681 | * | ||
| 682 | * <code> | ||
| 683 | * $qb = $em->createQueryBuilder() | ||
| 684 | * ->delete('User', 'u') | ||
| 685 | * ->where('u.id = :user_id') | ||
| 686 | * ->setParameter('user_id', 1); | ||
| 687 | * </code> | ||
| 688 | * | ||
| 689 | * @param class-string|null $delete The class/type whose instances are subject to the deletion. | ||
| 690 | * @param string|null $alias The class/type alias used in the constructed query. | ||
| 691 | * | ||
| 692 | * @return $this | ||
| 693 | */ | ||
| 694 | public function delete(string|null $delete = null, string|null $alias = null): static | ||
| 695 | { | ||
| 696 | $this->type = QueryType::Delete; | ||
| 697 | |||
| 698 | if (! $delete) { | ||
| 699 | return $this; | ||
| 700 | } | ||
| 701 | |||
| 702 | if (! $alias) { | ||
| 703 | throw new InvalidArgumentException(sprintf( | ||
| 704 | '%s(): The alias for entity %s must not be omitted.', | ||
| 705 | __METHOD__, | ||
| 706 | $delete, | ||
| 707 | )); | ||
| 708 | } | ||
| 709 | |||
| 710 | return $this->add('from', new Expr\From($delete, $alias)); | ||
| 711 | } | ||
| 712 | |||
| 713 | /** | ||
| 714 | * Turns the query being built into a bulk update query that ranges over | ||
| 715 | * a certain entity type. | ||
| 716 | * | ||
| 717 | * <code> | ||
| 718 | * $qb = $em->createQueryBuilder() | ||
| 719 | * ->update('User', 'u') | ||
| 720 | * ->set('u.password', '?1') | ||
| 721 | * ->where('u.id = ?2'); | ||
| 722 | * </code> | ||
| 723 | * | ||
| 724 | * @param class-string|null $update The class/type whose instances are subject to the update. | ||
| 725 | * @param string|null $alias The class/type alias used in the constructed query. | ||
| 726 | * | ||
| 727 | * @return $this | ||
| 728 | */ | ||
| 729 | public function update(string|null $update = null, string|null $alias = null): static | ||
| 730 | { | ||
| 731 | $this->type = QueryType::Update; | ||
| 732 | |||
| 733 | if (! $update) { | ||
| 734 | return $this; | ||
| 735 | } | ||
| 736 | |||
| 737 | if (! $alias) { | ||
| 738 | throw new InvalidArgumentException(sprintf( | ||
| 739 | '%s(): The alias for entity %s must not be omitted.', | ||
| 740 | __METHOD__, | ||
| 741 | $update, | ||
| 742 | )); | ||
| 743 | } | ||
| 744 | |||
| 745 | return $this->add('from', new Expr\From($update, $alias)); | ||
| 746 | } | ||
| 747 | |||
| 748 | /** | ||
| 749 | * Creates and adds a query root corresponding to the entity identified by the given alias, | ||
| 750 | * forming a cartesian product with any existing query roots. | ||
| 751 | * | ||
| 752 | * <code> | ||
| 753 | * $qb = $em->createQueryBuilder() | ||
| 754 | * ->select('u') | ||
| 755 | * ->from('User', 'u'); | ||
| 756 | * </code> | ||
| 757 | * | ||
| 758 | * @param class-string $from The class name. | ||
| 759 | * @param string $alias The alias of the class. | ||
| 760 | * @param string|null $indexBy The index for the from. | ||
| 761 | * | ||
| 762 | * @return $this | ||
| 763 | */ | ||
| 764 | public function from(string $from, string $alias, string|null $indexBy = null): static | ||
| 765 | { | ||
| 766 | return $this->add('from', new Expr\From($from, $alias, $indexBy), true); | ||
| 767 | } | ||
| 768 | |||
| 769 | /** | ||
| 770 | * Updates a query root corresponding to an entity setting its index by. This method is intended to be used with | ||
| 771 | * EntityRepository->createQueryBuilder(), which creates the initial FROM clause and do not allow you to update it | ||
| 772 | * setting an index by. | ||
| 773 | * | ||
| 774 | * <code> | ||
| 775 | * $qb = $userRepository->createQueryBuilder('u') | ||
| 776 | * ->indexBy('u', 'u.id'); | ||
| 777 | * | ||
| 778 | * // Is equivalent to... | ||
| 779 | * | ||
| 780 | * $qb = $em->createQueryBuilder() | ||
| 781 | * ->select('u') | ||
| 782 | * ->from('User', 'u', 'u.id'); | ||
| 783 | * </code> | ||
| 784 | * | ||
| 785 | * @return $this | ||
| 786 | * | ||
| 787 | * @throws Query\QueryException | ||
| 788 | */ | ||
| 789 | public function indexBy(string $alias, string $indexBy): static | ||
| 790 | { | ||
| 791 | $rootAliases = $this->getRootAliases(); | ||
| 792 | |||
| 793 | if (! in_array($alias, $rootAliases, true)) { | ||
| 794 | throw new Query\QueryException( | ||
| 795 | sprintf('Specified root alias %s must be set before invoking indexBy().', $alias), | ||
| 796 | ); | ||
| 797 | } | ||
| 798 | |||
| 799 | foreach ($this->dqlParts['from'] as &$fromClause) { | ||
| 800 | assert($fromClause instanceof Expr\From); | ||
| 801 | if ($fromClause->getAlias() !== $alias) { | ||
| 802 | continue; | ||
| 803 | } | ||
| 804 | |||
| 805 | $fromClause = new Expr\From($fromClause->getFrom(), $fromClause->getAlias(), $indexBy); | ||
| 806 | } | ||
| 807 | |||
| 808 | return $this; | ||
| 809 | } | ||
| 810 | |||
| 811 | /** | ||
| 812 | * Creates and adds a join over an entity association to the query. | ||
| 813 | * | ||
| 814 | * The entities in the joined association will be fetched as part of the query | ||
| 815 | * result if the alias used for the joined association is placed in the select | ||
| 816 | * expressions. | ||
| 817 | * | ||
| 818 | * <code> | ||
| 819 | * $qb = $em->createQueryBuilder() | ||
| 820 | * ->select('u') | ||
| 821 | * ->from('User', 'u') | ||
| 822 | * ->join('u.Phonenumbers', 'p', Expr\Join::WITH, 'p.is_primary = 1'); | ||
| 823 | * </code> | ||
| 824 | * | ||
| 825 | * @psalm-param Expr\Join::ON|Expr\Join::WITH|null $conditionType | ||
| 826 | * | ||
| 827 | * @return $this | ||
| 828 | */ | ||
| 829 | public function join( | ||
| 830 | string $join, | ||
| 831 | string $alias, | ||
| 832 | string|null $conditionType = null, | ||
| 833 | string|Expr\Composite|Expr\Comparison|Expr\Func|null $condition = null, | ||
| 834 | string|null $indexBy = null, | ||
| 835 | ): static { | ||
| 836 | return $this->innerJoin($join, $alias, $conditionType, $condition, $indexBy); | ||
| 837 | } | ||
| 838 | |||
| 839 | /** | ||
| 840 | * Creates and adds a join over an entity association to the query. | ||
| 841 | * | ||
| 842 | * The entities in the joined association will be fetched as part of the query | ||
| 843 | * result if the alias used for the joined association is placed in the select | ||
| 844 | * expressions. | ||
| 845 | * | ||
| 846 | * [php] | ||
| 847 | * $qb = $em->createQueryBuilder() | ||
| 848 | * ->select('u') | ||
| 849 | * ->from('User', 'u') | ||
| 850 | * ->innerJoin('u.Phonenumbers', 'p', Expr\Join::WITH, 'p.is_primary = 1'); | ||
| 851 | * | ||
| 852 | * @psalm-param Expr\Join::ON|Expr\Join::WITH|null $conditionType | ||
| 853 | * | ||
| 854 | * @return $this | ||
| 855 | */ | ||
| 856 | public function innerJoin( | ||
| 857 | string $join, | ||
| 858 | string $alias, | ||
| 859 | string|null $conditionType = null, | ||
| 860 | string|Expr\Composite|Expr\Comparison|Expr\Func|null $condition = null, | ||
| 861 | string|null $indexBy = null, | ||
| 862 | ): static { | ||
| 863 | $parentAlias = substr($join, 0, (int) strpos($join, '.')); | ||
| 864 | |||
| 865 | $rootAlias = $this->findRootAlias($alias, $parentAlias); | ||
| 866 | |||
| 867 | $join = new Expr\Join( | ||
| 868 | Expr\Join::INNER_JOIN, | ||
| 869 | $join, | ||
| 870 | $alias, | ||
| 871 | $conditionType, | ||
| 872 | $condition, | ||
| 873 | $indexBy, | ||
| 874 | ); | ||
| 875 | |||
| 876 | return $this->add('join', [$rootAlias => $join], true); | ||
| 877 | } | ||
| 878 | |||
| 879 | /** | ||
| 880 | * Creates and adds a left join over an entity association to the query. | ||
| 881 | * | ||
| 882 | * The entities in the joined association will be fetched as part of the query | ||
| 883 | * result if the alias used for the joined association is placed in the select | ||
| 884 | * expressions. | ||
| 885 | * | ||
| 886 | * <code> | ||
| 887 | * $qb = $em->createQueryBuilder() | ||
| 888 | * ->select('u') | ||
| 889 | * ->from('User', 'u') | ||
| 890 | * ->leftJoin('u.Phonenumbers', 'p', Expr\Join::WITH, 'p.is_primary = 1'); | ||
| 891 | * </code> | ||
| 892 | * | ||
| 893 | * @psalm-param Expr\Join::ON|Expr\Join::WITH|null $conditionType | ||
| 894 | * | ||
| 895 | * @return $this | ||
| 896 | */ | ||
| 897 | public function leftJoin( | ||
| 898 | string $join, | ||
| 899 | string $alias, | ||
| 900 | string|null $conditionType = null, | ||
| 901 | string|Expr\Composite|Expr\Comparison|Expr\Func|null $condition = null, | ||
| 902 | string|null $indexBy = null, | ||
| 903 | ): static { | ||
| 904 | $parentAlias = substr($join, 0, (int) strpos($join, '.')); | ||
| 905 | |||
| 906 | $rootAlias = $this->findRootAlias($alias, $parentAlias); | ||
| 907 | |||
| 908 | $join = new Expr\Join( | ||
| 909 | Expr\Join::LEFT_JOIN, | ||
| 910 | $join, | ||
| 911 | $alias, | ||
| 912 | $conditionType, | ||
| 913 | $condition, | ||
| 914 | $indexBy, | ||
| 915 | ); | ||
| 916 | |||
| 917 | return $this->add('join', [$rootAlias => $join], true); | ||
| 918 | } | ||
| 919 | |||
| 920 | /** | ||
| 921 | * Sets a new value for a field in a bulk update query. | ||
| 922 | * | ||
| 923 | * <code> | ||
| 924 | * $qb = $em->createQueryBuilder() | ||
| 925 | * ->update('User', 'u') | ||
| 926 | * ->set('u.password', '?1') | ||
| 927 | * ->where('u.id = ?2'); | ||
| 928 | * </code> | ||
| 929 | * | ||
| 930 | * @return $this | ||
| 931 | */ | ||
| 932 | public function set(string $key, mixed $value): static | ||
| 933 | { | ||
| 934 | return $this->add('set', new Expr\Comparison($key, Expr\Comparison::EQ, $value), true); | ||
| 935 | } | ||
| 936 | |||
| 937 | /** | ||
| 938 | * Specifies one or more restrictions to the query result. | ||
| 939 | * Replaces any previously specified restrictions, if any. | ||
| 940 | * | ||
| 941 | * <code> | ||
| 942 | * $qb = $em->createQueryBuilder() | ||
| 943 | * ->select('u') | ||
| 944 | * ->from('User', 'u') | ||
| 945 | * ->where('u.id = ?'); | ||
| 946 | * | ||
| 947 | * // You can optionally programmatically build and/or expressions | ||
| 948 | * $qb = $em->createQueryBuilder(); | ||
| 949 | * | ||
| 950 | * $or = $qb->expr()->orX(); | ||
| 951 | * $or->add($qb->expr()->eq('u.id', 1)); | ||
| 952 | * $or->add($qb->expr()->eq('u.id', 2)); | ||
| 953 | * | ||
| 954 | * $qb->update('User', 'u') | ||
| 955 | * ->set('u.password', '?') | ||
| 956 | * ->where($or); | ||
| 957 | * </code> | ||
| 958 | * | ||
| 959 | * @return $this | ||
| 960 | */ | ||
| 961 | public function where(mixed ...$predicates): static | ||
| 962 | { | ||
| 963 | self::validateVariadicParameter($predicates); | ||
| 964 | |||
| 965 | if (! (count($predicates) === 1 && $predicates[0] instanceof Expr\Composite)) { | ||
| 966 | $predicates = new Expr\Andx($predicates); | ||
| 967 | } | ||
| 968 | |||
| 969 | return $this->add('where', $predicates); | ||
| 970 | } | ||
| 971 | |||
| 972 | /** | ||
| 973 | * Adds one or more restrictions to the query results, forming a logical | ||
| 974 | * conjunction with any previously specified restrictions. | ||
| 975 | * | ||
| 976 | * <code> | ||
| 977 | * $qb = $em->createQueryBuilder() | ||
| 978 | * ->select('u') | ||
| 979 | * ->from('User', 'u') | ||
| 980 | * ->where('u.username LIKE ?') | ||
| 981 | * ->andWhere('u.is_active = 1'); | ||
| 982 | * </code> | ||
| 983 | * | ||
| 984 | * @see where() | ||
| 985 | * | ||
| 986 | * @return $this | ||
| 987 | */ | ||
| 988 | public function andWhere(mixed ...$where): static | ||
| 989 | { | ||
| 990 | self::validateVariadicParameter($where); | ||
| 991 | |||
| 992 | $dql = $this->getDQLPart('where'); | ||
| 993 | |||
| 994 | if ($dql instanceof Expr\Andx) { | ||
| 995 | $dql->addMultiple($where); | ||
| 996 | } else { | ||
| 997 | array_unshift($where, $dql); | ||
| 998 | $dql = new Expr\Andx($where); | ||
| 999 | } | ||
| 1000 | |||
| 1001 | return $this->add('where', $dql); | ||
| 1002 | } | ||
| 1003 | |||
| 1004 | /** | ||
| 1005 | * Adds one or more restrictions to the query results, forming a logical | ||
| 1006 | * disjunction with any previously specified restrictions. | ||
| 1007 | * | ||
| 1008 | * <code> | ||
| 1009 | * $qb = $em->createQueryBuilder() | ||
| 1010 | * ->select('u') | ||
| 1011 | * ->from('User', 'u') | ||
| 1012 | * ->where('u.id = 1') | ||
| 1013 | * ->orWhere('u.id = 2'); | ||
| 1014 | * </code> | ||
| 1015 | * | ||
| 1016 | * @see where() | ||
| 1017 | * | ||
| 1018 | * @return $this | ||
| 1019 | */ | ||
| 1020 | public function orWhere(mixed ...$where): static | ||
| 1021 | { | ||
| 1022 | self::validateVariadicParameter($where); | ||
| 1023 | |||
| 1024 | $dql = $this->getDQLPart('where'); | ||
| 1025 | |||
| 1026 | if ($dql instanceof Expr\Orx) { | ||
| 1027 | $dql->addMultiple($where); | ||
| 1028 | } else { | ||
| 1029 | array_unshift($where, $dql); | ||
| 1030 | $dql = new Expr\Orx($where); | ||
| 1031 | } | ||
| 1032 | |||
| 1033 | return $this->add('where', $dql); | ||
| 1034 | } | ||
| 1035 | |||
| 1036 | /** | ||
| 1037 | * Specifies a grouping over the results of the query. | ||
| 1038 | * Replaces any previously specified groupings, if any. | ||
| 1039 | * | ||
| 1040 | * <code> | ||
| 1041 | * $qb = $em->createQueryBuilder() | ||
| 1042 | * ->select('u') | ||
| 1043 | * ->from('User', 'u') | ||
| 1044 | * ->groupBy('u.id'); | ||
| 1045 | * </code> | ||
| 1046 | * | ||
| 1047 | * @return $this | ||
| 1048 | */ | ||
| 1049 | public function groupBy(string ...$groupBy): static | ||
| 1050 | { | ||
| 1051 | self::validateVariadicParameter($groupBy); | ||
| 1052 | |||
| 1053 | return $this->add('groupBy', new Expr\GroupBy($groupBy)); | ||
| 1054 | } | ||
| 1055 | |||
| 1056 | /** | ||
| 1057 | * Adds a grouping expression to the query. | ||
| 1058 | * | ||
| 1059 | * <code> | ||
| 1060 | * $qb = $em->createQueryBuilder() | ||
| 1061 | * ->select('u') | ||
| 1062 | * ->from('User', 'u') | ||
| 1063 | * ->groupBy('u.lastLogin') | ||
| 1064 | * ->addGroupBy('u.createdAt'); | ||
| 1065 | * </code> | ||
| 1066 | * | ||
| 1067 | * @return $this | ||
| 1068 | */ | ||
| 1069 | public function addGroupBy(string ...$groupBy): static | ||
| 1070 | { | ||
| 1071 | self::validateVariadicParameter($groupBy); | ||
| 1072 | |||
| 1073 | return $this->add('groupBy', new Expr\GroupBy($groupBy), true); | ||
| 1074 | } | ||
| 1075 | |||
| 1076 | /** | ||
| 1077 | * Specifies a restriction over the groups of the query. | ||
| 1078 | * Replaces any previous having restrictions, if any. | ||
| 1079 | * | ||
| 1080 | * @return $this | ||
| 1081 | */ | ||
| 1082 | public function having(mixed ...$having): static | ||
| 1083 | { | ||
| 1084 | self::validateVariadicParameter($having); | ||
| 1085 | |||
| 1086 | if (! (count($having) === 1 && ($having[0] instanceof Expr\Andx || $having[0] instanceof Expr\Orx))) { | ||
| 1087 | $having = new Expr\Andx($having); | ||
| 1088 | } | ||
| 1089 | |||
| 1090 | return $this->add('having', $having); | ||
| 1091 | } | ||
| 1092 | |||
| 1093 | /** | ||
| 1094 | * Adds a restriction over the groups of the query, forming a logical | ||
| 1095 | * conjunction with any existing having restrictions. | ||
| 1096 | * | ||
| 1097 | * @return $this | ||
| 1098 | */ | ||
| 1099 | public function andHaving(mixed ...$having): static | ||
| 1100 | { | ||
| 1101 | self::validateVariadicParameter($having); | ||
| 1102 | |||
| 1103 | $dql = $this->getDQLPart('having'); | ||
| 1104 | |||
| 1105 | if ($dql instanceof Expr\Andx) { | ||
| 1106 | $dql->addMultiple($having); | ||
| 1107 | } else { | ||
| 1108 | array_unshift($having, $dql); | ||
| 1109 | $dql = new Expr\Andx($having); | ||
| 1110 | } | ||
| 1111 | |||
| 1112 | return $this->add('having', $dql); | ||
| 1113 | } | ||
| 1114 | |||
| 1115 | /** | ||
| 1116 | * Adds a restriction over the groups of the query, forming a logical | ||
| 1117 | * disjunction with any existing having restrictions. | ||
| 1118 | * | ||
| 1119 | * @return $this | ||
| 1120 | */ | ||
| 1121 | public function orHaving(mixed ...$having): static | ||
| 1122 | { | ||
| 1123 | self::validateVariadicParameter($having); | ||
| 1124 | |||
| 1125 | $dql = $this->getDQLPart('having'); | ||
| 1126 | |||
| 1127 | if ($dql instanceof Expr\Orx) { | ||
| 1128 | $dql->addMultiple($having); | ||
| 1129 | } else { | ||
| 1130 | array_unshift($having, $dql); | ||
| 1131 | $dql = new Expr\Orx($having); | ||
| 1132 | } | ||
| 1133 | |||
| 1134 | return $this->add('having', $dql); | ||
| 1135 | } | ||
| 1136 | |||
| 1137 | /** | ||
| 1138 | * Specifies an ordering for the query results. | ||
| 1139 | * Replaces any previously specified orderings, if any. | ||
| 1140 | * | ||
| 1141 | * @return $this | ||
| 1142 | */ | ||
| 1143 | public function orderBy(string|Expr\OrderBy $sort, string|null $order = null): static | ||
| 1144 | { | ||
| 1145 | $orderBy = $sort instanceof Expr\OrderBy ? $sort : new Expr\OrderBy($sort, $order); | ||
| 1146 | |||
| 1147 | return $this->add('orderBy', $orderBy); | ||
| 1148 | } | ||
| 1149 | |||
| 1150 | /** | ||
| 1151 | * Adds an ordering to the query results. | ||
| 1152 | * | ||
| 1153 | * @return $this | ||
| 1154 | */ | ||
| 1155 | public function addOrderBy(string|Expr\OrderBy $sort, string|null $order = null): static | ||
| 1156 | { | ||
| 1157 | $orderBy = $sort instanceof Expr\OrderBy ? $sort : new Expr\OrderBy($sort, $order); | ||
| 1158 | |||
| 1159 | return $this->add('orderBy', $orderBy, true); | ||
| 1160 | } | ||
| 1161 | |||
| 1162 | /** | ||
| 1163 | * Adds criteria to the query. | ||
| 1164 | * | ||
| 1165 | * Adds where expressions with AND operator. | ||
| 1166 | * Adds orderings. | ||
| 1167 | * Overrides firstResult and maxResults if they're set. | ||
| 1168 | * | ||
| 1169 | * @return $this | ||
| 1170 | * | ||
| 1171 | * @throws Query\QueryException | ||
| 1172 | */ | ||
| 1173 | public function addCriteria(Criteria $criteria): static | ||
| 1174 | { | ||
| 1175 | $allAliases = $this->getAllAliases(); | ||
| 1176 | if (! isset($allAliases[0])) { | ||
| 1177 | throw new Query\QueryException('No aliases are set before invoking addCriteria().'); | ||
| 1178 | } | ||
| 1179 | |||
| 1180 | $visitor = new QueryExpressionVisitor($this->getAllAliases()); | ||
| 1181 | |||
| 1182 | $whereExpression = $criteria->getWhereExpression(); | ||
| 1183 | if ($whereExpression) { | ||
| 1184 | $this->andWhere($visitor->dispatch($whereExpression)); | ||
| 1185 | foreach ($visitor->getParameters() as $parameter) { | ||
| 1186 | $this->parameters->add($parameter); | ||
| 1187 | } | ||
| 1188 | } | ||
| 1189 | |||
| 1190 | foreach ($criteria->orderings() as $sort => $order) { | ||
| 1191 | $hasValidAlias = false; | ||
| 1192 | foreach ($allAliases as $alias) { | ||
| 1193 | if (str_starts_with($sort . '.', $alias . '.')) { | ||
| 1194 | $hasValidAlias = true; | ||
| 1195 | break; | ||
| 1196 | } | ||
| 1197 | } | ||
| 1198 | |||
| 1199 | if (! $hasValidAlias) { | ||
| 1200 | $sort = $allAliases[0] . '.' . $sort; | ||
| 1201 | } | ||
| 1202 | |||
| 1203 | $this->addOrderBy($sort, $order->value); | ||
| 1204 | } | ||
| 1205 | |||
| 1206 | // Overwrite limits only if they was set in criteria | ||
| 1207 | $firstResult = $criteria->getFirstResult(); | ||
| 1208 | if ($firstResult > 0) { | ||
| 1209 | $this->setFirstResult($firstResult); | ||
| 1210 | } | ||
| 1211 | |||
| 1212 | $maxResults = $criteria->getMaxResults(); | ||
| 1213 | if ($maxResults !== null) { | ||
| 1214 | $this->setMaxResults($maxResults); | ||
| 1215 | } | ||
| 1216 | |||
| 1217 | return $this; | ||
| 1218 | } | ||
| 1219 | |||
| 1220 | /** | ||
| 1221 | * Gets a query part by its name. | ||
| 1222 | */ | ||
| 1223 | public function getDQLPart(string $queryPartName): mixed | ||
| 1224 | { | ||
| 1225 | return $this->dqlParts[$queryPartName]; | ||
| 1226 | } | ||
| 1227 | |||
| 1228 | /** | ||
| 1229 | * Gets all query parts. | ||
| 1230 | * | ||
| 1231 | * @psalm-return array<string, mixed> $dqlParts | ||
| 1232 | */ | ||
| 1233 | public function getDQLParts(): array | ||
| 1234 | { | ||
| 1235 | return $this->dqlParts; | ||
| 1236 | } | ||
| 1237 | |||
| 1238 | private function getDQLForDelete(): string | ||
| 1239 | { | ||
| 1240 | return 'DELETE' | ||
| 1241 | . $this->getReducedDQLQueryPart('from', ['pre' => ' ', 'separator' => ', ']) | ||
| 1242 | . $this->getReducedDQLQueryPart('where', ['pre' => ' WHERE ']) | ||
| 1243 | . $this->getReducedDQLQueryPart('orderBy', ['pre' => ' ORDER BY ', 'separator' => ', ']); | ||
| 1244 | } | ||
| 1245 | |||
| 1246 | private function getDQLForUpdate(): string | ||
| 1247 | { | ||
| 1248 | return 'UPDATE' | ||
| 1249 | . $this->getReducedDQLQueryPart('from', ['pre' => ' ', 'separator' => ', ']) | ||
| 1250 | . $this->getReducedDQLQueryPart('set', ['pre' => ' SET ', 'separator' => ', ']) | ||
| 1251 | . $this->getReducedDQLQueryPart('where', ['pre' => ' WHERE ']) | ||
| 1252 | . $this->getReducedDQLQueryPart('orderBy', ['pre' => ' ORDER BY ', 'separator' => ', ']); | ||
| 1253 | } | ||
| 1254 | |||
| 1255 | private function getDQLForSelect(): string | ||
| 1256 | { | ||
| 1257 | $dql = 'SELECT' | ||
| 1258 | . ($this->dqlParts['distinct'] === true ? ' DISTINCT' : '') | ||
| 1259 | . $this->getReducedDQLQueryPart('select', ['pre' => ' ', 'separator' => ', ']); | ||
| 1260 | |||
| 1261 | $fromParts = $this->getDQLPart('from'); | ||
| 1262 | $joinParts = $this->getDQLPart('join'); | ||
| 1263 | $fromClauses = []; | ||
| 1264 | |||
| 1265 | // Loop through all FROM clauses | ||
| 1266 | if (! empty($fromParts)) { | ||
| 1267 | $dql .= ' FROM '; | ||
| 1268 | |||
| 1269 | foreach ($fromParts as $from) { | ||
| 1270 | $fromClause = (string) $from; | ||
| 1271 | |||
| 1272 | if ($from instanceof Expr\From && isset($joinParts[$from->getAlias()])) { | ||
| 1273 | foreach ($joinParts[$from->getAlias()] as $join) { | ||
| 1274 | $fromClause .= ' ' . ((string) $join); | ||
| 1275 | } | ||
| 1276 | } | ||
| 1277 | |||
| 1278 | $fromClauses[] = $fromClause; | ||
| 1279 | } | ||
| 1280 | } | ||
| 1281 | |||
| 1282 | $dql .= implode(', ', $fromClauses) | ||
| 1283 | . $this->getReducedDQLQueryPart('where', ['pre' => ' WHERE ']) | ||
| 1284 | . $this->getReducedDQLQueryPart('groupBy', ['pre' => ' GROUP BY ', 'separator' => ', ']) | ||
| 1285 | . $this->getReducedDQLQueryPart('having', ['pre' => ' HAVING ']) | ||
| 1286 | . $this->getReducedDQLQueryPart('orderBy', ['pre' => ' ORDER BY ', 'separator' => ', ']); | ||
| 1287 | |||
| 1288 | return $dql; | ||
| 1289 | } | ||
| 1290 | |||
| 1291 | /** @psalm-param array<string, mixed> $options */ | ||
| 1292 | private function getReducedDQLQueryPart(string $queryPartName, array $options = []): string | ||
| 1293 | { | ||
| 1294 | $queryPart = $this->getDQLPart($queryPartName); | ||
| 1295 | |||
| 1296 | if (empty($queryPart)) { | ||
| 1297 | return $options['empty'] ?? ''; | ||
| 1298 | } | ||
| 1299 | |||
| 1300 | return ($options['pre'] ?? '') | ||
| 1301 | . (is_array($queryPart) ? implode($options['separator'], $queryPart) : $queryPart) | ||
| 1302 | . ($options['post'] ?? ''); | ||
| 1303 | } | ||
| 1304 | |||
| 1305 | /** | ||
| 1306 | * Resets DQL parts. | ||
| 1307 | * | ||
| 1308 | * @param string[]|null $parts | ||
| 1309 | * @psalm-param list<string>|null $parts | ||
| 1310 | * | ||
| 1311 | * @return $this | ||
| 1312 | */ | ||
| 1313 | public function resetDQLParts(array|null $parts = null): static | ||
| 1314 | { | ||
| 1315 | if ($parts === null) { | ||
| 1316 | $parts = array_keys($this->dqlParts); | ||
| 1317 | } | ||
| 1318 | |||
| 1319 | foreach ($parts as $part) { | ||
| 1320 | $this->resetDQLPart($part); | ||
| 1321 | } | ||
| 1322 | |||
| 1323 | return $this; | ||
| 1324 | } | ||
| 1325 | |||
| 1326 | /** | ||
| 1327 | * Resets single DQL part. | ||
| 1328 | * | ||
| 1329 | * @return $this | ||
| 1330 | */ | ||
| 1331 | public function resetDQLPart(string $part): static | ||
| 1332 | { | ||
| 1333 | $this->dqlParts[$part] = is_array($this->dqlParts[$part]) ? [] : null; | ||
| 1334 | $this->dql = null; | ||
| 1335 | |||
| 1336 | return $this; | ||
| 1337 | } | ||
| 1338 | |||
| 1339 | /** | ||
| 1340 | * Gets a string representation of this QueryBuilder which corresponds to | ||
| 1341 | * the final DQL query being constructed. | ||
| 1342 | */ | ||
| 1343 | public function __toString(): string | ||
| 1344 | { | ||
| 1345 | return $this->getDQL(); | ||
| 1346 | } | ||
| 1347 | |||
| 1348 | /** | ||
| 1349 | * Deep clones all expression objects in the DQL parts. | ||
| 1350 | * | ||
| 1351 | * @return void | ||
| 1352 | */ | ||
| 1353 | public function __clone() | ||
| 1354 | { | ||
| 1355 | foreach ($this->dqlParts as $part => $elements) { | ||
| 1356 | if (is_array($this->dqlParts[$part])) { | ||
| 1357 | foreach ($this->dqlParts[$part] as $idx => $element) { | ||
| 1358 | if (is_object($element)) { | ||
| 1359 | $this->dqlParts[$part][$idx] = clone $element; | ||
| 1360 | } | ||
| 1361 | } | ||
| 1362 | } elseif (is_object($elements)) { | ||
| 1363 | $this->dqlParts[$part] = clone $elements; | ||
| 1364 | } | ||
| 1365 | } | ||
| 1366 | |||
| 1367 | $parameters = []; | ||
| 1368 | |||
| 1369 | foreach ($this->parameters as $parameter) { | ||
| 1370 | $parameters[] = clone $parameter; | ||
| 1371 | } | ||
| 1372 | |||
| 1373 | $this->parameters = new ArrayCollection($parameters); | ||
| 1374 | } | ||
| 1375 | } | ||
diff --git a/vendor/doctrine/orm/src/Repository/DefaultRepositoryFactory.php b/vendor/doctrine/orm/src/Repository/DefaultRepositoryFactory.php new file mode 100644 index 0000000..5c408fb --- /dev/null +++ b/vendor/doctrine/orm/src/Repository/DefaultRepositoryFactory.php | |||
| @@ -0,0 +1,49 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Repository; | ||
| 6 | |||
| 7 | use Doctrine\ORM\EntityManagerInterface; | ||
| 8 | use Doctrine\ORM\EntityRepository; | ||
| 9 | use Doctrine\Persistence\ObjectRepository; | ||
| 10 | |||
| 11 | use function spl_object_id; | ||
| 12 | |||
| 13 | /** | ||
| 14 | * This factory is used to create default repository objects for entities at runtime. | ||
| 15 | */ | ||
| 16 | final class DefaultRepositoryFactory implements RepositoryFactory | ||
| 17 | { | ||
| 18 | /** | ||
| 19 | * The list of EntityRepository instances. | ||
| 20 | * | ||
| 21 | * @var ObjectRepository[] | ||
| 22 | * @psalm-var array<string, EntityRepository> | ||
| 23 | */ | ||
| 24 | private array $repositoryList = []; | ||
| 25 | |||
| 26 | public function getRepository(EntityManagerInterface $entityManager, string $entityName): EntityRepository | ||
| 27 | { | ||
| 28 | $repositoryHash = $entityManager->getClassMetadata($entityName)->getName() . spl_object_id($entityManager); | ||
| 29 | |||
| 30 | return $this->repositoryList[$repositoryHash] ??= $this->createRepository($entityManager, $entityName); | ||
| 31 | } | ||
| 32 | |||
| 33 | /** | ||
| 34 | * Create a new repository instance for an entity class. | ||
| 35 | * | ||
| 36 | * @param EntityManagerInterface $entityManager The EntityManager instance. | ||
| 37 | * @param string $entityName The name of the entity. | ||
| 38 | */ | ||
| 39 | private function createRepository( | ||
| 40 | EntityManagerInterface $entityManager, | ||
| 41 | string $entityName, | ||
| 42 | ): EntityRepository { | ||
| 43 | $metadata = $entityManager->getClassMetadata($entityName); | ||
| 44 | $repositoryClassName = $metadata->customRepositoryClassName | ||
| 45 | ?: $entityManager->getConfiguration()->getDefaultRepositoryClassName(); | ||
| 46 | |||
| 47 | return new $repositoryClassName($entityManager, $metadata); | ||
| 48 | } | ||
| 49 | } | ||
diff --git a/vendor/doctrine/orm/src/Repository/Exception/InvalidFindByCall.php b/vendor/doctrine/orm/src/Repository/Exception/InvalidFindByCall.php new file mode 100644 index 0000000..c5dd015 --- /dev/null +++ b/vendor/doctrine/orm/src/Repository/Exception/InvalidFindByCall.php | |||
| @@ -0,0 +1,21 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Repository\Exception; | ||
| 6 | |||
| 7 | use Doctrine\ORM\Exception\RepositoryException; | ||
| 8 | use LogicException; | ||
| 9 | |||
| 10 | final class InvalidFindByCall extends LogicException implements RepositoryException | ||
| 11 | { | ||
| 12 | public static function fromInverseSideUsage( | ||
| 13 | string $entityName, | ||
| 14 | string $associationFieldName, | ||
| 15 | ): self { | ||
| 16 | return new self( | ||
| 17 | "You cannot search for the association field '" . $entityName . '#' . $associationFieldName . "', " . | ||
| 18 | 'because it is the inverse side of an association. Find methods only work on owning side associations.', | ||
| 19 | ); | ||
| 20 | } | ||
| 21 | } | ||
diff --git a/vendor/doctrine/orm/src/Repository/Exception/InvalidMagicMethodCall.php b/vendor/doctrine/orm/src/Repository/Exception/InvalidMagicMethodCall.php new file mode 100644 index 0000000..1da49cb --- /dev/null +++ b/vendor/doctrine/orm/src/Repository/Exception/InvalidMagicMethodCall.php | |||
| @@ -0,0 +1,27 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Repository\Exception; | ||
| 6 | |||
| 7 | use Doctrine\ORM\Exception\RepositoryException; | ||
| 8 | use LogicException; | ||
| 9 | |||
| 10 | final class InvalidMagicMethodCall extends LogicException implements RepositoryException | ||
| 11 | { | ||
| 12 | public static function becauseFieldNotFoundIn( | ||
| 13 | string $entityName, | ||
| 14 | string $fieldName, | ||
| 15 | string $method, | ||
| 16 | ): self { | ||
| 17 | return new self( | ||
| 18 | "Entity '" . $entityName . "' has no field '" . $fieldName . "'. " . | ||
| 19 | "You can therefore not call '" . $method . "' on the entities' repository.", | ||
| 20 | ); | ||
| 21 | } | ||
| 22 | |||
| 23 | public static function onMissingParameter(string $methodName): self | ||
| 24 | { | ||
| 25 | return new self("You need to pass a parameter to '" . $methodName . "'"); | ||
| 26 | } | ||
| 27 | } | ||
diff --git a/vendor/doctrine/orm/src/Repository/RepositoryFactory.php b/vendor/doctrine/orm/src/Repository/RepositoryFactory.php new file mode 100644 index 0000000..e066eae --- /dev/null +++ b/vendor/doctrine/orm/src/Repository/RepositoryFactory.php | |||
| @@ -0,0 +1,26 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Repository; | ||
| 6 | |||
| 7 | use Doctrine\ORM\EntityManagerInterface; | ||
| 8 | use Doctrine\ORM\EntityRepository; | ||
| 9 | |||
| 10 | /** | ||
| 11 | * Interface for entity repository factory. | ||
| 12 | */ | ||
| 13 | interface RepositoryFactory | ||
| 14 | { | ||
| 15 | /** | ||
| 16 | * Gets the repository for an entity class. | ||
| 17 | * | ||
| 18 | * @param EntityManagerInterface $entityManager The EntityManager instance. | ||
| 19 | * @param class-string<T> $entityName The name of the entity. | ||
| 20 | * | ||
| 21 | * @return EntityRepository<T> | ||
| 22 | * | ||
| 23 | * @template T of object | ||
| 24 | */ | ||
| 25 | public function getRepository(EntityManagerInterface $entityManager, string $entityName): EntityRepository; | ||
| 26 | } | ||
diff --git a/vendor/doctrine/orm/src/Tools/AttachEntityListenersListener.php b/vendor/doctrine/orm/src/Tools/AttachEntityListenersListener.php new file mode 100644 index 0000000..9203cfe --- /dev/null +++ b/vendor/doctrine/orm/src/Tools/AttachEntityListenersListener.php | |||
| @@ -0,0 +1,69 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Tools; | ||
| 6 | |||
| 7 | use Doctrine\ORM\Event\LoadClassMetadataEventArgs; | ||
| 8 | use Doctrine\ORM\Events; | ||
| 9 | use Doctrine\ORM\Mapping\Builder\EntityListenerBuilder; | ||
| 10 | |||
| 11 | use function assert; | ||
| 12 | use function ltrim; | ||
| 13 | |||
| 14 | /** | ||
| 15 | * Mechanism to programmatically attach entity listeners. | ||
| 16 | */ | ||
| 17 | class AttachEntityListenersListener | ||
| 18 | { | ||
| 19 | /** | ||
| 20 | * @var array<class-string, list<array{ | ||
| 21 | * event: Events::*|null, | ||
| 22 | * class: class-string, | ||
| 23 | * method: string|null, | ||
| 24 | * }>> | ||
| 25 | */ | ||
| 26 | private array $entityListeners = []; | ||
| 27 | |||
| 28 | /** | ||
| 29 | * Adds an entity listener for a specific entity. | ||
| 30 | * | ||
| 31 | * @param class-string $entityClass The entity to attach the listener. | ||
| 32 | * @param class-string $listenerClass The listener class. | ||
| 33 | * @param Events::*|null $eventName The entity lifecycle event. | ||
| 34 | * @param non-falsy-string|null $listenerCallback The listener callback method or NULL to use $eventName. | ||
| 35 | */ | ||
| 36 | public function addEntityListener( | ||
| 37 | string $entityClass, | ||
| 38 | string $listenerClass, | ||
| 39 | string|null $eventName = null, | ||
| 40 | string|null $listenerCallback = null, | ||
| 41 | ): void { | ||
| 42 | $this->entityListeners[ltrim($entityClass, '\\')][] = [ | ||
| 43 | 'event' => $eventName, | ||
| 44 | 'class' => $listenerClass, | ||
| 45 | 'method' => $listenerCallback ?? $eventName, | ||
| 46 | ]; | ||
| 47 | } | ||
| 48 | |||
| 49 | /** | ||
| 50 | * Processes event and attach the entity listener. | ||
| 51 | */ | ||
| 52 | public function loadClassMetadata(LoadClassMetadataEventArgs $event): void | ||
| 53 | { | ||
| 54 | $metadata = $event->getClassMetadata(); | ||
| 55 | |||
| 56 | if (! isset($this->entityListeners[$metadata->name])) { | ||
| 57 | return; | ||
| 58 | } | ||
| 59 | |||
| 60 | foreach ($this->entityListeners[$metadata->name] as $listener) { | ||
| 61 | if ($listener['event'] === null) { | ||
| 62 | EntityListenerBuilder::bindEntityListener($metadata, $listener['class']); | ||
| 63 | } else { | ||
| 64 | assert($listener['method'] !== null); | ||
| 65 | $metadata->addEntityListener($listener['event'], $listener['class'], $listener['method']); | ||
| 66 | } | ||
| 67 | } | ||
| 68 | } | ||
| 69 | } | ||
diff --git a/vendor/doctrine/orm/src/Tools/Console/Command/AbstractEntityManagerCommand.php b/vendor/doctrine/orm/src/Tools/Console/Command/AbstractEntityManagerCommand.php new file mode 100644 index 0000000..370f4fb --- /dev/null +++ b/vendor/doctrine/orm/src/Tools/Console/Command/AbstractEntityManagerCommand.php | |||
| @@ -0,0 +1,25 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Tools\Console\Command; | ||
| 6 | |||
| 7 | use Doctrine\ORM\EntityManagerInterface; | ||
| 8 | use Doctrine\ORM\Tools\Console\EntityManagerProvider; | ||
| 9 | use Symfony\Component\Console\Command\Command; | ||
| 10 | use Symfony\Component\Console\Input\InputInterface; | ||
| 11 | |||
| 12 | abstract class AbstractEntityManagerCommand extends Command | ||
| 13 | { | ||
| 14 | public function __construct(private readonly EntityManagerProvider $entityManagerProvider) | ||
| 15 | { | ||
| 16 | parent::__construct(); | ||
| 17 | } | ||
| 18 | |||
| 19 | final protected function getEntityManager(InputInterface $input): EntityManagerInterface | ||
| 20 | { | ||
| 21 | return $input->getOption('em') === null | ||
| 22 | ? $this->entityManagerProvider->getDefaultManager() | ||
| 23 | : $this->entityManagerProvider->getManager($input->getOption('em')); | ||
| 24 | } | ||
| 25 | } | ||
diff --git a/vendor/doctrine/orm/src/Tools/Console/Command/ClearCache/CollectionRegionCommand.php b/vendor/doctrine/orm/src/Tools/Console/Command/ClearCache/CollectionRegionCommand.php new file mode 100644 index 0000000..b4c6efa --- /dev/null +++ b/vendor/doctrine/orm/src/Tools/Console/Command/ClearCache/CollectionRegionCommand.php | |||
| @@ -0,0 +1,119 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Tools\Console\Command\ClearCache; | ||
| 6 | |||
| 7 | use Doctrine\ORM\Cache; | ||
| 8 | use Doctrine\ORM\Tools\Console\Command\AbstractEntityManagerCommand; | ||
| 9 | use InvalidArgumentException; | ||
| 10 | use Symfony\Component\Console\Input\InputArgument; | ||
| 11 | use Symfony\Component\Console\Input\InputInterface; | ||
| 12 | use Symfony\Component\Console\Input\InputOption; | ||
| 13 | use Symfony\Component\Console\Output\OutputInterface; | ||
| 14 | use Symfony\Component\Console\Style\SymfonyStyle; | ||
| 15 | |||
| 16 | use function sprintf; | ||
| 17 | |||
| 18 | /** | ||
| 19 | * Command to clear a collection cache region. | ||
| 20 | */ | ||
| 21 | class CollectionRegionCommand extends AbstractEntityManagerCommand | ||
| 22 | { | ||
| 23 | protected function configure(): void | ||
| 24 | { | ||
| 25 | $this->setName('orm:clear-cache:region:collection') | ||
| 26 | ->setDescription('Clear a second-level cache collection region') | ||
| 27 | ->addArgument('owner-class', InputArgument::OPTIONAL, 'The owner entity name.') | ||
| 28 | ->addArgument('association', InputArgument::OPTIONAL, 'The association collection name.') | ||
| 29 | ->addArgument('owner-id', InputArgument::OPTIONAL, 'The owner identifier.') | ||
| 30 | ->addOption('em', null, InputOption::VALUE_REQUIRED, 'Name of the entity manager to operate on') | ||
| 31 | ->addOption('all', null, InputOption::VALUE_NONE, 'If defined, all entity regions will be deleted/invalidated.') | ||
| 32 | ->addOption('flush', null, InputOption::VALUE_NONE, 'If defined, all cache entries will be flushed.') | ||
| 33 | ->setHelp(<<<'EOT' | ||
| 34 | The <info>%command.name%</info> command is meant to clear a second-level cache collection regions for an associated Entity Manager. | ||
| 35 | It is possible to delete/invalidate all collection region, a specific collection region or flushes the cache provider. | ||
| 36 | |||
| 37 | The execution type differ on how you execute the command. | ||
| 38 | If you want to invalidate all entries for an collection region this command would do the work: | ||
| 39 | |||
| 40 | <info>%command.name% 'Entities\MyEntity' 'collectionName'</info> | ||
| 41 | |||
| 42 | To invalidate a specific entry you should use : | ||
| 43 | |||
| 44 | <info>%command.name% 'Entities\MyEntity' 'collectionName' 1</info> | ||
| 45 | |||
| 46 | If you want to invalidate all entries for the all collection regions: | ||
| 47 | |||
| 48 | <info>%command.name% --all</info> | ||
| 49 | |||
| 50 | Alternatively, if you want to flush the configured cache provider for an collection region use this command: | ||
| 51 | |||
| 52 | <info>%command.name% 'Entities\MyEntity' 'collectionName' --flush</info> | ||
| 53 | |||
| 54 | Finally, be aware that if <info>--flush</info> option is passed, | ||
| 55 | not all cache providers are able to flush entries, because of a limitation of its execution nature. | ||
| 56 | EOT); | ||
| 57 | } | ||
| 58 | |||
| 59 | protected function execute(InputInterface $input, OutputInterface $output): int | ||
| 60 | { | ||
| 61 | $ui = (new SymfonyStyle($input, $output))->getErrorStyle(); | ||
| 62 | |||
| 63 | $em = $this->getEntityManager($input); | ||
| 64 | $ownerClass = $input->getArgument('owner-class'); | ||
| 65 | $assoc = $input->getArgument('association'); | ||
| 66 | $ownerId = $input->getArgument('owner-id'); | ||
| 67 | $cache = $em->getCache(); | ||
| 68 | |||
| 69 | if (! $cache instanceof Cache) { | ||
| 70 | throw new InvalidArgumentException('No second-level cache is configured on the given EntityManager.'); | ||
| 71 | } | ||
| 72 | |||
| 73 | if (( ! $ownerClass || ! $assoc) && ! $input->getOption('all')) { | ||
| 74 | throw new InvalidArgumentException('Missing arguments "--owner-class" "--association"'); | ||
| 75 | } | ||
| 76 | |||
| 77 | if ($input->getOption('flush')) { | ||
| 78 | $cache->getCollectionCacheRegion($ownerClass, $assoc) | ||
| 79 | ->evictAll(); | ||
| 80 | |||
| 81 | $ui->comment( | ||
| 82 | sprintf( | ||
| 83 | 'Flushing cache provider configured for <info>"%s#%s"</info>', | ||
| 84 | $ownerClass, | ||
| 85 | $assoc, | ||
| 86 | ), | ||
| 87 | ); | ||
| 88 | |||
| 89 | return 0; | ||
| 90 | } | ||
| 91 | |||
| 92 | if ($input->getOption('all')) { | ||
| 93 | $ui->comment('Clearing <info>all</info> second-level cache collection regions'); | ||
| 94 | |||
| 95 | $cache->evictEntityRegions(); | ||
| 96 | |||
| 97 | return 0; | ||
| 98 | } | ||
| 99 | |||
| 100 | if ($ownerId) { | ||
| 101 | $ui->comment( | ||
| 102 | sprintf( | ||
| 103 | 'Clearing second-level cache entry for collection <info>"%s#%s"</info> owner entity identified by <info>"%s"</info>', | ||
| 104 | $ownerClass, | ||
| 105 | $assoc, | ||
| 106 | $ownerId, | ||
| 107 | ), | ||
| 108 | ); | ||
| 109 | $cache->evictCollection($ownerClass, $assoc, $ownerId); | ||
| 110 | |||
| 111 | return 0; | ||
| 112 | } | ||
| 113 | |||
| 114 | $ui->comment(sprintf('Clearing second-level cache for collection <info>"%s#%s"</info>', $ownerClass, $assoc)); | ||
| 115 | $cache->evictCollectionRegion($ownerClass, $assoc); | ||
| 116 | |||
| 117 | return 0; | ||
| 118 | } | ||
| 119 | } | ||
diff --git a/vendor/doctrine/orm/src/Tools/Console/Command/ClearCache/EntityRegionCommand.php b/vendor/doctrine/orm/src/Tools/Console/Command/ClearCache/EntityRegionCommand.php new file mode 100644 index 0000000..c5f2d65 --- /dev/null +++ b/vendor/doctrine/orm/src/Tools/Console/Command/ClearCache/EntityRegionCommand.php | |||
| @@ -0,0 +1,110 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Tools\Console\Command\ClearCache; | ||
| 6 | |||
| 7 | use Doctrine\ORM\Cache; | ||
| 8 | use Doctrine\ORM\Tools\Console\Command\AbstractEntityManagerCommand; | ||
| 9 | use InvalidArgumentException; | ||
| 10 | use Symfony\Component\Console\Input\InputArgument; | ||
| 11 | use Symfony\Component\Console\Input\InputInterface; | ||
| 12 | use Symfony\Component\Console\Input\InputOption; | ||
| 13 | use Symfony\Component\Console\Output\OutputInterface; | ||
| 14 | use Symfony\Component\Console\Style\SymfonyStyle; | ||
| 15 | |||
| 16 | use function sprintf; | ||
| 17 | |||
| 18 | /** | ||
| 19 | * Command to clear a entity cache region. | ||
| 20 | */ | ||
| 21 | class EntityRegionCommand extends AbstractEntityManagerCommand | ||
| 22 | { | ||
| 23 | protected function configure(): void | ||
| 24 | { | ||
| 25 | $this->setName('orm:clear-cache:region:entity') | ||
| 26 | ->setDescription('Clear a second-level cache entity region') | ||
| 27 | ->addArgument('entity-class', InputArgument::OPTIONAL, 'The entity name.') | ||
| 28 | ->addArgument('entity-id', InputArgument::OPTIONAL, 'The entity identifier.') | ||
| 29 | ->addOption('em', null, InputOption::VALUE_REQUIRED, 'Name of the entity manager to operate on') | ||
| 30 | ->addOption('all', null, InputOption::VALUE_NONE, 'If defined, all entity regions will be deleted/invalidated.') | ||
| 31 | ->addOption('flush', null, InputOption::VALUE_NONE, 'If defined, all cache entries will be flushed.') | ||
| 32 | ->setHelp(<<<'EOT' | ||
| 33 | The <info>%command.name%</info> command is meant to clear a second-level cache entity region for an associated Entity Manager. | ||
| 34 | It is possible to delete/invalidate all entity region, a specific entity region or flushes the cache provider. | ||
| 35 | |||
| 36 | The execution type differ on how you execute the command. | ||
| 37 | If you want to invalidate all entries for an entity region this command would do the work: | ||
| 38 | |||
| 39 | <info>%command.name% 'Entities\MyEntity'</info> | ||
| 40 | |||
| 41 | To invalidate a specific entry you should use : | ||
| 42 | |||
| 43 | <info>%command.name% 'Entities\MyEntity' 1</info> | ||
| 44 | |||
| 45 | If you want to invalidate all entries for the all entity regions: | ||
| 46 | |||
| 47 | <info>%command.name% --all</info> | ||
| 48 | |||
| 49 | Alternatively, if you want to flush the configured cache provider for an entity region use this command: | ||
| 50 | |||
| 51 | <info>%command.name% 'Entities\MyEntity' --flush</info> | ||
| 52 | |||
| 53 | Finally, be aware that if <info>--flush</info> option is passed, | ||
| 54 | not all cache providers are able to flush entries, because of a limitation of its execution nature. | ||
| 55 | EOT); | ||
| 56 | } | ||
| 57 | |||
| 58 | protected function execute(InputInterface $input, OutputInterface $output): int | ||
| 59 | { | ||
| 60 | $ui = (new SymfonyStyle($input, $output))->getErrorStyle(); | ||
| 61 | |||
| 62 | $em = $this->getEntityManager($input); | ||
| 63 | $entityClass = $input->getArgument('entity-class'); | ||
| 64 | $entityId = $input->getArgument('entity-id'); | ||
| 65 | $cache = $em->getCache(); | ||
| 66 | |||
| 67 | if (! $cache instanceof Cache) { | ||
| 68 | throw new InvalidArgumentException('No second-level cache is configured on the given EntityManager.'); | ||
| 69 | } | ||
| 70 | |||
| 71 | if (! $entityClass && ! $input->getOption('all')) { | ||
| 72 | throw new InvalidArgumentException('Invalid argument "--entity-class"'); | ||
| 73 | } | ||
| 74 | |||
| 75 | if ($input->getOption('flush')) { | ||
| 76 | $cache->getEntityCacheRegion($entityClass) | ||
| 77 | ->evictAll(); | ||
| 78 | |||
| 79 | $ui->comment(sprintf('Flushing cache provider configured for entity named <info>"%s"</info>', $entityClass)); | ||
| 80 | |||
| 81 | return 0; | ||
| 82 | } | ||
| 83 | |||
| 84 | if ($input->getOption('all')) { | ||
| 85 | $ui->comment('Clearing <info>all</info> second-level cache entity regions'); | ||
| 86 | |||
| 87 | $cache->evictEntityRegions(); | ||
| 88 | |||
| 89 | return 0; | ||
| 90 | } | ||
| 91 | |||
| 92 | if ($entityId) { | ||
| 93 | $ui->comment( | ||
| 94 | sprintf( | ||
| 95 | 'Clearing second-level cache entry for entity <info>"%s"</info> identified by <info>"%s"</info>', | ||
| 96 | $entityClass, | ||
| 97 | $entityId, | ||
| 98 | ), | ||
| 99 | ); | ||
| 100 | $cache->evictEntity($entityClass, $entityId); | ||
| 101 | |||
| 102 | return 0; | ||
| 103 | } | ||
| 104 | |||
| 105 | $ui->comment(sprintf('Clearing second-level cache for entity <info>"%s"</info>', $entityClass)); | ||
| 106 | $cache->evictEntityRegion($entityClass); | ||
| 107 | |||
| 108 | return 0; | ||
| 109 | } | ||
| 110 | } | ||
diff --git a/vendor/doctrine/orm/src/Tools/Console/Command/ClearCache/MetadataCommand.php b/vendor/doctrine/orm/src/Tools/Console/Command/ClearCache/MetadataCommand.php new file mode 100644 index 0000000..147795b --- /dev/null +++ b/vendor/doctrine/orm/src/Tools/Console/Command/ClearCache/MetadataCommand.php | |||
| @@ -0,0 +1,52 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Tools\Console\Command\ClearCache; | ||
| 6 | |||
| 7 | use Doctrine\ORM\Tools\Console\Command\AbstractEntityManagerCommand; | ||
| 8 | use InvalidArgumentException; | ||
| 9 | use Symfony\Component\Console\Input\InputInterface; | ||
| 10 | use Symfony\Component\Console\Input\InputOption; | ||
| 11 | use Symfony\Component\Console\Output\OutputInterface; | ||
| 12 | use Symfony\Component\Console\Style\SymfonyStyle; | ||
| 13 | |||
| 14 | /** | ||
| 15 | * Command to clear the metadata cache of the various cache drivers. | ||
| 16 | * | ||
| 17 | * @link www.doctrine-project.org | ||
| 18 | */ | ||
| 19 | class MetadataCommand extends AbstractEntityManagerCommand | ||
| 20 | { | ||
| 21 | protected function configure(): void | ||
| 22 | { | ||
| 23 | $this->setName('orm:clear-cache:metadata') | ||
| 24 | ->setDescription('Clear all metadata cache of the various cache drivers') | ||
| 25 | ->addOption('em', null, InputOption::VALUE_REQUIRED, 'Name of the entity manager to operate on') | ||
| 26 | ->addOption('flush', null, InputOption::VALUE_NONE, 'If defined, cache entries will be flushed instead of deleted/invalidated.') | ||
| 27 | ->setHelp(<<<'EOT' | ||
| 28 | The <info>%command.name%</info> command is meant to clear the metadata cache of associated Entity Manager. | ||
| 29 | EOT); | ||
| 30 | } | ||
| 31 | |||
| 32 | protected function execute(InputInterface $input, OutputInterface $output): int | ||
| 33 | { | ||
| 34 | $ui = (new SymfonyStyle($input, $output))->getErrorStyle(); | ||
| 35 | |||
| 36 | $em = $this->getEntityManager($input); | ||
| 37 | $cacheDriver = $em->getConfiguration()->getMetadataCache(); | ||
| 38 | |||
| 39 | if (! $cacheDriver) { | ||
| 40 | throw new InvalidArgumentException('No Metadata cache driver is configured on given EntityManager.'); | ||
| 41 | } | ||
| 42 | |||
| 43 | $ui->comment('Clearing <info>all</info> Metadata cache entries'); | ||
| 44 | |||
| 45 | $result = $cacheDriver->clear(); | ||
| 46 | $message = $result ? 'Successfully deleted cache entries.' : 'No cache entries were deleted.'; | ||
| 47 | |||
| 48 | $ui->success($message); | ||
| 49 | |||
| 50 | return 0; | ||
| 51 | } | ||
| 52 | } | ||
diff --git a/vendor/doctrine/orm/src/Tools/Console/Command/ClearCache/QueryCommand.php b/vendor/doctrine/orm/src/Tools/Console/Command/ClearCache/QueryCommand.php new file mode 100644 index 0000000..83edd7a --- /dev/null +++ b/vendor/doctrine/orm/src/Tools/Console/Command/ClearCache/QueryCommand.php | |||
| @@ -0,0 +1,54 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Tools\Console\Command\ClearCache; | ||
| 6 | |||
| 7 | use Doctrine\ORM\Tools\Console\Command\AbstractEntityManagerCommand; | ||
| 8 | use InvalidArgumentException; | ||
| 9 | use LogicException; | ||
| 10 | use Symfony\Component\Cache\Adapter\ApcuAdapter; | ||
| 11 | use Symfony\Component\Console\Input\InputInterface; | ||
| 12 | use Symfony\Component\Console\Input\InputOption; | ||
| 13 | use Symfony\Component\Console\Output\OutputInterface; | ||
| 14 | use Symfony\Component\Console\Style\SymfonyStyle; | ||
| 15 | |||
| 16 | /** | ||
| 17 | * Command to clear the query cache of the various cache drivers. | ||
| 18 | * | ||
| 19 | * @link www.doctrine-project.org | ||
| 20 | */ | ||
| 21 | class QueryCommand extends AbstractEntityManagerCommand | ||
| 22 | { | ||
| 23 | protected function configure(): void | ||
| 24 | { | ||
| 25 | $this->setName('orm:clear-cache:query') | ||
| 26 | ->setDescription('Clear all query cache of the various cache drivers') | ||
| 27 | ->addOption('em', null, InputOption::VALUE_REQUIRED, 'Name of the entity manager to operate on') | ||
| 28 | ->setHelp('The <info>%command.name%</info> command is meant to clear the query cache of associated Entity Manager.'); | ||
| 29 | } | ||
| 30 | |||
| 31 | protected function execute(InputInterface $input, OutputInterface $output): int | ||
| 32 | { | ||
| 33 | $ui = (new SymfonyStyle($input, $output))->getErrorStyle(); | ||
| 34 | |||
| 35 | $em = $this->getEntityManager($input); | ||
| 36 | $cache = $em->getConfiguration()->getQueryCache(); | ||
| 37 | |||
| 38 | if (! $cache) { | ||
| 39 | throw new InvalidArgumentException('No Query cache driver is configured on given EntityManager.'); | ||
| 40 | } | ||
| 41 | |||
| 42 | if ($cache instanceof ApcuAdapter) { | ||
| 43 | throw new LogicException('Cannot clear APCu Cache from Console, it\'s shared in the Webserver memory and not accessible from the CLI.'); | ||
| 44 | } | ||
| 45 | |||
| 46 | $ui->comment('Clearing <info>all</info> Query cache entries'); | ||
| 47 | |||
| 48 | $message = $cache->clear() ? 'Successfully deleted cache entries.' : 'No cache entries were deleted.'; | ||
| 49 | |||
| 50 | $ui->success($message); | ||
| 51 | |||
| 52 | return 0; | ||
| 53 | } | ||
| 54 | } | ||
diff --git a/vendor/doctrine/orm/src/Tools/Console/Command/ClearCache/QueryRegionCommand.php b/vendor/doctrine/orm/src/Tools/Console/Command/ClearCache/QueryRegionCommand.php new file mode 100644 index 0000000..e80fb90 --- /dev/null +++ b/vendor/doctrine/orm/src/Tools/Console/Command/ClearCache/QueryRegionCommand.php | |||
| @@ -0,0 +1,101 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Tools\Console\Command\ClearCache; | ||
| 6 | |||
| 7 | use Doctrine\ORM\Cache; | ||
| 8 | use Doctrine\ORM\Tools\Console\Command\AbstractEntityManagerCommand; | ||
| 9 | use InvalidArgumentException; | ||
| 10 | use Symfony\Component\Console\Input\InputArgument; | ||
| 11 | use Symfony\Component\Console\Input\InputInterface; | ||
| 12 | use Symfony\Component\Console\Input\InputOption; | ||
| 13 | use Symfony\Component\Console\Output\OutputInterface; | ||
| 14 | use Symfony\Component\Console\Style\SymfonyStyle; | ||
| 15 | |||
| 16 | use function sprintf; | ||
| 17 | |||
| 18 | /** | ||
| 19 | * Command to clear a query cache region. | ||
| 20 | */ | ||
| 21 | class QueryRegionCommand extends AbstractEntityManagerCommand | ||
| 22 | { | ||
| 23 | protected function configure(): void | ||
| 24 | { | ||
| 25 | $this->setName('orm:clear-cache:region:query') | ||
| 26 | ->setDescription('Clear a second-level cache query region') | ||
| 27 | ->addArgument('region-name', InputArgument::OPTIONAL, 'The query region to clear.') | ||
| 28 | ->addOption('em', null, InputOption::VALUE_REQUIRED, 'Name of the entity manager to operate on') | ||
| 29 | ->addOption('all', null, InputOption::VALUE_NONE, 'If defined, all query regions will be deleted/invalidated.') | ||
| 30 | ->addOption('flush', null, InputOption::VALUE_NONE, 'If defined, all cache entries will be flushed.') | ||
| 31 | ->setHelp(<<<'EOT' | ||
| 32 | The <info>%command.name%</info> command is meant to clear a second-level cache query region for an associated Entity Manager. | ||
| 33 | It is possible to delete/invalidate all query region, a specific query region or flushes the cache provider. | ||
| 34 | |||
| 35 | The execution type differ on how you execute the command. | ||
| 36 | If you want to invalidate all entries for the default query region this command would do the work: | ||
| 37 | |||
| 38 | <info>%command.name%</info> | ||
| 39 | |||
| 40 | To invalidate entries for a specific query region you should use : | ||
| 41 | |||
| 42 | <info>%command.name% my_region_name</info> | ||
| 43 | |||
| 44 | If you want to invalidate all entries for the all query region: | ||
| 45 | |||
| 46 | <info>%command.name% --all</info> | ||
| 47 | |||
| 48 | Alternatively, if you want to flush the configured cache provider use this command: | ||
| 49 | |||
| 50 | <info>%command.name% my_region_name --flush</info> | ||
| 51 | |||
| 52 | Finally, be aware that if <info>--flush</info> option is passed, | ||
| 53 | not all cache providers are able to flush entries, because of a limitation of its execution nature. | ||
| 54 | EOT); | ||
| 55 | } | ||
| 56 | |||
| 57 | protected function execute(InputInterface $input, OutputInterface $output): int | ||
| 58 | { | ||
| 59 | $ui = (new SymfonyStyle($input, $output))->getErrorStyle(); | ||
| 60 | |||
| 61 | $em = $this->getEntityManager($input); | ||
| 62 | $name = $input->getArgument('region-name'); | ||
| 63 | $cache = $em->getCache(); | ||
| 64 | |||
| 65 | if ($name === null) { | ||
| 66 | $name = Cache::DEFAULT_QUERY_REGION_NAME; | ||
| 67 | } | ||
| 68 | |||
| 69 | if (! $cache instanceof Cache) { | ||
| 70 | throw new InvalidArgumentException('No second-level cache is configured on the given EntityManager.'); | ||
| 71 | } | ||
| 72 | |||
| 73 | if ($input->getOption('flush')) { | ||
| 74 | $cache->getQueryCache($name) | ||
| 75 | ->getRegion() | ||
| 76 | ->evictAll(); | ||
| 77 | |||
| 78 | $ui->comment( | ||
| 79 | sprintf( | ||
| 80 | 'Flushing cache provider configured for second-level cache query region named <info>"%s"</info>', | ||
| 81 | $name, | ||
| 82 | ), | ||
| 83 | ); | ||
| 84 | |||
| 85 | return 0; | ||
| 86 | } | ||
| 87 | |||
| 88 | if ($input->getOption('all')) { | ||
| 89 | $ui->comment('Clearing <info>all</info> second-level cache query regions'); | ||
| 90 | |||
| 91 | $cache->evictQueryRegions(); | ||
| 92 | |||
| 93 | return 0; | ||
| 94 | } | ||
| 95 | |||
| 96 | $ui->comment(sprintf('Clearing second-level cache query region named <info>"%s"</info>', $name)); | ||
| 97 | $cache->evictQueryRegion($name); | ||
| 98 | |||
| 99 | return 0; | ||
| 100 | } | ||
| 101 | } | ||
diff --git a/vendor/doctrine/orm/src/Tools/Console/Command/ClearCache/ResultCommand.php b/vendor/doctrine/orm/src/Tools/Console/Command/ClearCache/ResultCommand.php new file mode 100644 index 0000000..4f84e0b --- /dev/null +++ b/vendor/doctrine/orm/src/Tools/Console/Command/ClearCache/ResultCommand.php | |||
| @@ -0,0 +1,65 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Tools\Console\Command\ClearCache; | ||
| 6 | |||
| 7 | use Doctrine\ORM\Tools\Console\Command\AbstractEntityManagerCommand; | ||
| 8 | use InvalidArgumentException; | ||
| 9 | use Symfony\Component\Console\Input\InputInterface; | ||
| 10 | use Symfony\Component\Console\Input\InputOption; | ||
| 11 | use Symfony\Component\Console\Output\OutputInterface; | ||
| 12 | use Symfony\Component\Console\Style\SymfonyStyle; | ||
| 13 | |||
| 14 | /** | ||
| 15 | * Command to clear the result cache of the various cache drivers. | ||
| 16 | * | ||
| 17 | * @link www.doctrine-project.org | ||
| 18 | */ | ||
| 19 | class ResultCommand extends AbstractEntityManagerCommand | ||
| 20 | { | ||
| 21 | protected function configure(): void | ||
| 22 | { | ||
| 23 | $this->setName('orm:clear-cache:result') | ||
| 24 | ->setDescription('Clear all result cache of the various cache drivers') | ||
| 25 | ->addOption('em', null, InputOption::VALUE_REQUIRED, 'Name of the entity manager to operate on') | ||
| 26 | ->addOption('flush', null, InputOption::VALUE_NONE, 'If defined, cache entries will be flushed instead of deleted/invalidated.') | ||
| 27 | ->setHelp(<<<'EOT' | ||
| 28 | The <info>%command.name%</info> command is meant to clear the result cache of associated Entity Manager. | ||
| 29 | It is possible to invalidate all cache entries at once - called delete -, or flushes the cache provider | ||
| 30 | instance completely. | ||
| 31 | |||
| 32 | The execution type differ on how you execute the command. | ||
| 33 | If you want to invalidate the entries (and not delete from cache instance), this command would do the work: | ||
| 34 | |||
| 35 | <info>%command.name%</info> | ||
| 36 | |||
| 37 | Alternatively, if you want to flush the cache provider using this command: | ||
| 38 | |||
| 39 | <info>%command.name% --flush</info> | ||
| 40 | |||
| 41 | Finally, be aware that if <info>--flush</info> option is passed, not all cache providers are able to flush entries, | ||
| 42 | because of a limitation of its execution nature. | ||
| 43 | EOT); | ||
| 44 | } | ||
| 45 | |||
| 46 | protected function execute(InputInterface $input, OutputInterface $output): int | ||
| 47 | { | ||
| 48 | $ui = (new SymfonyStyle($input, $output))->getErrorStyle(); | ||
| 49 | |||
| 50 | $em = $this->getEntityManager($input); | ||
| 51 | $cache = $em->getConfiguration()->getResultCache(); | ||
| 52 | |||
| 53 | if (! $cache) { | ||
| 54 | throw new InvalidArgumentException('No Result cache driver is configured on given EntityManager.'); | ||
| 55 | } | ||
| 56 | |||
| 57 | $ui->comment('Clearing <info>all</info> Result cache entries'); | ||
| 58 | |||
| 59 | $message = $cache->clear() ? 'Successfully deleted cache entries.' : 'No cache entries were deleted.'; | ||
| 60 | |||
| 61 | $ui->success($message); | ||
| 62 | |||
| 63 | return 0; | ||
| 64 | } | ||
| 65 | } | ||
diff --git a/vendor/doctrine/orm/src/Tools/Console/Command/GenerateProxiesCommand.php b/vendor/doctrine/orm/src/Tools/Console/Command/GenerateProxiesCommand.php new file mode 100644 index 0000000..5a407de --- /dev/null +++ b/vendor/doctrine/orm/src/Tools/Console/Command/GenerateProxiesCommand.php | |||
| @@ -0,0 +1,96 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Tools\Console\Command; | ||
| 6 | |||
| 7 | use Doctrine\ORM\Tools\Console\MetadataFilter; | ||
| 8 | use InvalidArgumentException; | ||
| 9 | use Symfony\Component\Console\Input\InputArgument; | ||
| 10 | use Symfony\Component\Console\Input\InputInterface; | ||
| 11 | use Symfony\Component\Console\Input\InputOption; | ||
| 12 | use Symfony\Component\Console\Output\OutputInterface; | ||
| 13 | use Symfony\Component\Console\Style\SymfonyStyle; | ||
| 14 | |||
| 15 | use function file_exists; | ||
| 16 | use function is_dir; | ||
| 17 | use function is_writable; | ||
| 18 | use function mkdir; | ||
| 19 | use function realpath; | ||
| 20 | use function sprintf; | ||
| 21 | |||
| 22 | /** | ||
| 23 | * Command to (re)generate the proxy classes used by doctrine. | ||
| 24 | * | ||
| 25 | * @link www.doctrine-project.org | ||
| 26 | */ | ||
| 27 | class GenerateProxiesCommand extends AbstractEntityManagerCommand | ||
| 28 | { | ||
| 29 | protected function configure(): void | ||
| 30 | { | ||
| 31 | $this->setName('orm:generate-proxies') | ||
| 32 | ->setAliases(['orm:generate:proxies']) | ||
| 33 | ->setDescription('Generates proxy classes for entity classes') | ||
| 34 | ->addArgument('dest-path', InputArgument::OPTIONAL, 'The path to generate your proxy classes. If none is provided, it will attempt to grab from configuration.') | ||
| 35 | ->addOption('em', null, InputOption::VALUE_REQUIRED, 'Name of the entity manager to operate on') | ||
| 36 | ->addOption('filter', null, InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'A string pattern used to match entities that should be processed.') | ||
| 37 | ->setHelp('Generates proxy classes for entity classes.'); | ||
| 38 | } | ||
| 39 | |||
| 40 | protected function execute(InputInterface $input, OutputInterface $output): int | ||
| 41 | { | ||
| 42 | $ui = (new SymfonyStyle($input, $output))->getErrorStyle(); | ||
| 43 | |||
| 44 | $em = $this->getEntityManager($input); | ||
| 45 | |||
| 46 | $metadatas = $em->getMetadataFactory()->getAllMetadata(); | ||
| 47 | $metadatas = MetadataFilter::filter($metadatas, $input->getOption('filter')); | ||
| 48 | |||
| 49 | // Process destination directory | ||
| 50 | $destPath = $input->getArgument('dest-path'); | ||
| 51 | if ($destPath === null) { | ||
| 52 | $destPath = $em->getConfiguration()->getProxyDir(); | ||
| 53 | |||
| 54 | if ($destPath === null) { | ||
| 55 | throw new InvalidArgumentException('Proxy directory cannot be null'); | ||
| 56 | } | ||
| 57 | } | ||
| 58 | |||
| 59 | if (! is_dir($destPath)) { | ||
| 60 | mkdir($destPath, 0775, true); | ||
| 61 | } | ||
| 62 | |||
| 63 | $destPath = realpath($destPath); | ||
| 64 | |||
| 65 | if (! file_exists($destPath)) { | ||
| 66 | throw new InvalidArgumentException( | ||
| 67 | sprintf("Proxies destination directory '<info>%s</info>' does not exist.", $em->getConfiguration()->getProxyDir()), | ||
| 68 | ); | ||
| 69 | } | ||
| 70 | |||
| 71 | if (! is_writable($destPath)) { | ||
| 72 | throw new InvalidArgumentException( | ||
| 73 | sprintf("Proxies destination directory '<info>%s</info>' does not have write permissions.", $destPath), | ||
| 74 | ); | ||
| 75 | } | ||
| 76 | |||
| 77 | if (empty($metadatas)) { | ||
| 78 | $ui->success('No Metadata Classes to process.'); | ||
| 79 | |||
| 80 | return 0; | ||
| 81 | } | ||
| 82 | |||
| 83 | foreach ($metadatas as $metadata) { | ||
| 84 | $ui->text(sprintf('Processing entity "<info>%s</info>"', $metadata->name)); | ||
| 85 | } | ||
| 86 | |||
| 87 | // Generating Proxies | ||
| 88 | $em->getProxyFactory()->generateProxyClasses($metadatas, $destPath); | ||
| 89 | |||
| 90 | // Outputting information message | ||
| 91 | $ui->newLine(); | ||
| 92 | $ui->text(sprintf('Proxy classes generated to "<info>%s</info>"', $destPath)); | ||
| 93 | |||
| 94 | return 0; | ||
| 95 | } | ||
| 96 | } | ||
diff --git a/vendor/doctrine/orm/src/Tools/Console/Command/InfoCommand.php b/vendor/doctrine/orm/src/Tools/Console/Command/InfoCommand.php new file mode 100644 index 0000000..deebb58 --- /dev/null +++ b/vendor/doctrine/orm/src/Tools/Console/Command/InfoCommand.php | |||
| @@ -0,0 +1,80 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Tools\Console\Command; | ||
| 6 | |||
| 7 | use Doctrine\ORM\Mapping\MappingException; | ||
| 8 | use Symfony\Component\Console\Input\InputInterface; | ||
| 9 | use Symfony\Component\Console\Input\InputOption; | ||
| 10 | use Symfony\Component\Console\Output\OutputInterface; | ||
| 11 | use Symfony\Component\Console\Style\SymfonyStyle; | ||
| 12 | |||
| 13 | use function count; | ||
| 14 | use function sprintf; | ||
| 15 | |||
| 16 | /** | ||
| 17 | * Show information about mapped entities. | ||
| 18 | * | ||
| 19 | * @link www.doctrine-project.org | ||
| 20 | */ | ||
| 21 | class InfoCommand extends AbstractEntityManagerCommand | ||
| 22 | { | ||
| 23 | protected function configure(): void | ||
| 24 | { | ||
| 25 | $this->setName('orm:info') | ||
| 26 | ->setDescription('Show basic information about all mapped entities') | ||
| 27 | ->addOption('em', null, InputOption::VALUE_REQUIRED, 'Name of the entity manager to operate on') | ||
| 28 | ->setHelp(<<<'EOT' | ||
| 29 | The <info>%command.name%</info> shows basic information about which | ||
| 30 | entities exist and possibly if their mapping information contains errors or | ||
| 31 | not. | ||
| 32 | EOT); | ||
| 33 | } | ||
| 34 | |||
| 35 | protected function execute(InputInterface $input, OutputInterface $output): int | ||
| 36 | { | ||
| 37 | $ui = (new SymfonyStyle($input, $output))->getErrorStyle(); | ||
| 38 | |||
| 39 | $entityManager = $this->getEntityManager($input); | ||
| 40 | |||
| 41 | $entityClassNames = $entityManager->getConfiguration() | ||
| 42 | ->getMetadataDriverImpl() | ||
| 43 | ->getAllClassNames(); | ||
| 44 | |||
| 45 | if (! $entityClassNames) { | ||
| 46 | $ui->caution( | ||
| 47 | [ | ||
| 48 | 'You do not have any mapped Doctrine ORM entities according to the current configuration.', | ||
| 49 | 'If you have entities or mapping files you should check your mapping configuration for errors.', | ||
| 50 | ], | ||
| 51 | ); | ||
| 52 | |||
| 53 | return 1; | ||
| 54 | } | ||
| 55 | |||
| 56 | $ui->text(sprintf('Found <info>%d</info> mapped entities:', count($entityClassNames))); | ||
| 57 | $ui->newLine(); | ||
| 58 | |||
| 59 | $failure = false; | ||
| 60 | |||
| 61 | foreach ($entityClassNames as $entityClassName) { | ||
| 62 | try { | ||
| 63 | $entityManager->getClassMetadata($entityClassName); | ||
| 64 | $ui->text(sprintf('<info>[OK]</info> %s', $entityClassName)); | ||
| 65 | } catch (MappingException $e) { | ||
| 66 | $ui->text( | ||
| 67 | [ | ||
| 68 | sprintf('<error>[FAIL]</error> %s', $entityClassName), | ||
| 69 | sprintf('<comment>%s</comment>', $e->getMessage()), | ||
| 70 | '', | ||
| 71 | ], | ||
| 72 | ); | ||
| 73 | |||
| 74 | $failure = true; | ||
| 75 | } | ||
| 76 | } | ||
| 77 | |||
| 78 | return $failure ? 1 : 0; | ||
| 79 | } | ||
| 80 | } | ||
diff --git a/vendor/doctrine/orm/src/Tools/Console/Command/MappingDescribeCommand.php b/vendor/doctrine/orm/src/Tools/Console/Command/MappingDescribeCommand.php new file mode 100644 index 0000000..41a177d --- /dev/null +++ b/vendor/doctrine/orm/src/Tools/Console/Command/MappingDescribeCommand.php | |||
| @@ -0,0 +1,279 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Tools\Console\Command; | ||
| 6 | |||
| 7 | use Doctrine\ORM\EntityManagerInterface; | ||
| 8 | use Doctrine\ORM\Mapping\AssociationMapping; | ||
| 9 | use Doctrine\ORM\Mapping\ClassMetadata; | ||
| 10 | use Doctrine\ORM\Mapping\FieldMapping; | ||
| 11 | use Doctrine\Persistence\Mapping\MappingException; | ||
| 12 | use InvalidArgumentException; | ||
| 13 | use Symfony\Component\Console\Input\InputArgument; | ||
| 14 | use Symfony\Component\Console\Input\InputInterface; | ||
| 15 | use Symfony\Component\Console\Input\InputOption; | ||
| 16 | use Symfony\Component\Console\Output\OutputInterface; | ||
| 17 | use Symfony\Component\Console\Style\SymfonyStyle; | ||
| 18 | |||
| 19 | use function array_filter; | ||
| 20 | use function array_map; | ||
| 21 | use function array_merge; | ||
| 22 | use function count; | ||
| 23 | use function current; | ||
| 24 | use function get_debug_type; | ||
| 25 | use function implode; | ||
| 26 | use function is_array; | ||
| 27 | use function is_bool; | ||
| 28 | use function is_object; | ||
| 29 | use function is_scalar; | ||
| 30 | use function json_encode; | ||
| 31 | use function preg_match; | ||
| 32 | use function preg_quote; | ||
| 33 | use function print_r; | ||
| 34 | use function sprintf; | ||
| 35 | |||
| 36 | use const JSON_PRETTY_PRINT; | ||
| 37 | use const JSON_THROW_ON_ERROR; | ||
| 38 | use const JSON_UNESCAPED_SLASHES; | ||
| 39 | use const JSON_UNESCAPED_UNICODE; | ||
| 40 | |||
| 41 | /** | ||
| 42 | * Show information about mapped entities. | ||
| 43 | * | ||
| 44 | * @link www.doctrine-project.org | ||
| 45 | */ | ||
| 46 | final class MappingDescribeCommand extends AbstractEntityManagerCommand | ||
| 47 | { | ||
| 48 | protected function configure(): void | ||
| 49 | { | ||
| 50 | $this->setName('orm:mapping:describe') | ||
| 51 | ->addArgument('entityName', InputArgument::REQUIRED, 'Full or partial name of entity') | ||
| 52 | ->setDescription('Display information about mapped objects') | ||
| 53 | ->addOption('em', null, InputOption::VALUE_REQUIRED, 'Name of the entity manager to operate on') | ||
| 54 | ->setHelp(<<<'EOT' | ||
| 55 | The %command.full_name% command describes the metadata for the given full or partial entity class name. | ||
| 56 | |||
| 57 | <info>%command.full_name%</info> My\Namespace\Entity\MyEntity | ||
| 58 | |||
| 59 | Or: | ||
| 60 | |||
| 61 | <info>%command.full_name%</info> MyEntity | ||
| 62 | EOT); | ||
| 63 | } | ||
| 64 | |||
| 65 | protected function execute(InputInterface $input, OutputInterface $output): int | ||
| 66 | { | ||
| 67 | $ui = (new SymfonyStyle($input, $output))->getErrorStyle(); | ||
| 68 | |||
| 69 | $entityManager = $this->getEntityManager($input); | ||
| 70 | |||
| 71 | $this->displayEntity($input->getArgument('entityName'), $entityManager, $ui); | ||
| 72 | |||
| 73 | return 0; | ||
| 74 | } | ||
| 75 | |||
| 76 | /** | ||
| 77 | * Display all the mapping information for a single Entity. | ||
| 78 | * | ||
| 79 | * @param string $entityName Full or partial entity class name | ||
| 80 | */ | ||
| 81 | private function displayEntity( | ||
| 82 | string $entityName, | ||
| 83 | EntityManagerInterface $entityManager, | ||
| 84 | SymfonyStyle $ui, | ||
| 85 | ): void { | ||
| 86 | $metadata = $this->getClassMetadata($entityName, $entityManager); | ||
| 87 | |||
| 88 | $ui->table( | ||
| 89 | ['Field', 'Value'], | ||
| 90 | array_merge( | ||
| 91 | [ | ||
| 92 | $this->formatField('Name', $metadata->name), | ||
| 93 | $this->formatField('Root entity name', $metadata->rootEntityName), | ||
| 94 | $this->formatField('Custom generator definition', $metadata->customGeneratorDefinition), | ||
| 95 | $this->formatField('Custom repository class', $metadata->customRepositoryClassName), | ||
| 96 | $this->formatField('Mapped super class?', $metadata->isMappedSuperclass), | ||
| 97 | $this->formatField('Embedded class?', $metadata->isEmbeddedClass), | ||
| 98 | $this->formatField('Parent classes', $metadata->parentClasses), | ||
| 99 | $this->formatField('Sub classes', $metadata->subClasses), | ||
| 100 | $this->formatField('Embedded classes', $metadata->subClasses), | ||
| 101 | $this->formatField('Identifier', $metadata->identifier), | ||
| 102 | $this->formatField('Inheritance type', $metadata->inheritanceType), | ||
| 103 | $this->formatField('Discriminator column', $metadata->discriminatorColumn), | ||
| 104 | $this->formatField('Discriminator value', $metadata->discriminatorValue), | ||
| 105 | $this->formatField('Discriminator map', $metadata->discriminatorMap), | ||
| 106 | $this->formatField('Generator type', $metadata->generatorType), | ||
| 107 | $this->formatField('Table', $metadata->table), | ||
| 108 | $this->formatField('Composite identifier?', $metadata->isIdentifierComposite), | ||
| 109 | $this->formatField('Foreign identifier?', $metadata->containsForeignIdentifier), | ||
| 110 | $this->formatField('Enum identifier?', $metadata->containsEnumIdentifier), | ||
| 111 | $this->formatField('Sequence generator definition', $metadata->sequenceGeneratorDefinition), | ||
| 112 | $this->formatField('Change tracking policy', $metadata->changeTrackingPolicy), | ||
| 113 | $this->formatField('Versioned?', $metadata->isVersioned), | ||
| 114 | $this->formatField('Version field', $metadata->versionField), | ||
| 115 | $this->formatField('Read only?', $metadata->isReadOnly), | ||
| 116 | |||
| 117 | $this->formatEntityListeners($metadata->entityListeners), | ||
| 118 | ], | ||
| 119 | [$this->formatField('Association mappings:', '')], | ||
| 120 | $this->formatMappings($metadata->associationMappings), | ||
| 121 | [$this->formatField('Field mappings:', '')], | ||
| 122 | $this->formatMappings($metadata->fieldMappings), | ||
| 123 | ), | ||
| 124 | ); | ||
| 125 | } | ||
| 126 | |||
| 127 | /** | ||
| 128 | * Return all mapped entity class names | ||
| 129 | * | ||
| 130 | * @return string[] | ||
| 131 | * @psalm-return class-string[] | ||
| 132 | */ | ||
| 133 | private function getMappedEntities(EntityManagerInterface $entityManager): array | ||
| 134 | { | ||
| 135 | $entityClassNames = $entityManager->getConfiguration() | ||
| 136 | ->getMetadataDriverImpl() | ||
| 137 | ->getAllClassNames(); | ||
| 138 | |||
| 139 | if (! $entityClassNames) { | ||
| 140 | throw new InvalidArgumentException( | ||
| 141 | 'You do not have any mapped Doctrine ORM entities according to the current configuration. ' . | ||
| 142 | 'If you have entities or mapping files you should check your mapping configuration for errors.', | ||
| 143 | ); | ||
| 144 | } | ||
| 145 | |||
| 146 | return $entityClassNames; | ||
| 147 | } | ||
| 148 | |||
| 149 | /** | ||
| 150 | * Return the class metadata for the given entity | ||
| 151 | * name | ||
| 152 | * | ||
| 153 | * @param string $entityName Full or partial entity name | ||
| 154 | */ | ||
| 155 | private function getClassMetadata( | ||
| 156 | string $entityName, | ||
| 157 | EntityManagerInterface $entityManager, | ||
| 158 | ): ClassMetadata { | ||
| 159 | try { | ||
| 160 | return $entityManager->getClassMetadata($entityName); | ||
| 161 | } catch (MappingException) { | ||
| 162 | } | ||
| 163 | |||
| 164 | $matches = array_filter( | ||
| 165 | $this->getMappedEntities($entityManager), | ||
| 166 | static fn ($mappedEntity) => preg_match('{' . preg_quote($entityName) . '}', $mappedEntity) | ||
| 167 | ); | ||
| 168 | |||
| 169 | if (! $matches) { | ||
| 170 | throw new InvalidArgumentException(sprintf( | ||
| 171 | 'Could not find any mapped Entity classes matching "%s"', | ||
| 172 | $entityName, | ||
| 173 | )); | ||
| 174 | } | ||
| 175 | |||
| 176 | if (count($matches) > 1) { | ||
| 177 | throw new InvalidArgumentException(sprintf( | ||
| 178 | 'Entity name "%s" is ambiguous, possible matches: "%s"', | ||
| 179 | $entityName, | ||
| 180 | implode(', ', $matches), | ||
| 181 | )); | ||
| 182 | } | ||
| 183 | |||
| 184 | return $entityManager->getClassMetadata(current($matches)); | ||
| 185 | } | ||
| 186 | |||
| 187 | /** | ||
| 188 | * Format the given value for console output | ||
| 189 | */ | ||
| 190 | private function formatValue(mixed $value): string | ||
| 191 | { | ||
| 192 | if ($value === '') { | ||
| 193 | return ''; | ||
| 194 | } | ||
| 195 | |||
| 196 | if ($value === null) { | ||
| 197 | return '<comment>Null</comment>'; | ||
| 198 | } | ||
| 199 | |||
| 200 | if (is_bool($value)) { | ||
| 201 | return '<comment>' . ($value ? 'True' : 'False') . '</comment>'; | ||
| 202 | } | ||
| 203 | |||
| 204 | if (empty($value)) { | ||
| 205 | return '<comment>Empty</comment>'; | ||
| 206 | } | ||
| 207 | |||
| 208 | if (is_array($value)) { | ||
| 209 | return json_encode( | ||
| 210 | $value, | ||
| 211 | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR, | ||
| 212 | ); | ||
| 213 | } | ||
| 214 | |||
| 215 | if (is_object($value)) { | ||
| 216 | return sprintf('<%s>', get_debug_type($value)); | ||
| 217 | } | ||
| 218 | |||
| 219 | if (is_scalar($value)) { | ||
| 220 | return (string) $value; | ||
| 221 | } | ||
| 222 | |||
| 223 | throw new InvalidArgumentException(sprintf('Do not know how to format value "%s"', print_r($value, true))); | ||
| 224 | } | ||
| 225 | |||
| 226 | /** | ||
| 227 | * Add the given label and value to the two column table output | ||
| 228 | * | ||
| 229 | * @param string $label Label for the value | ||
| 230 | * @param mixed $value A Value to show | ||
| 231 | * | ||
| 232 | * @return string[] | ||
| 233 | * @psalm-return array{0: string, 1: string} | ||
| 234 | */ | ||
| 235 | private function formatField(string $label, mixed $value): array | ||
| 236 | { | ||
| 237 | if ($value === null) { | ||
| 238 | $value = '<comment>None</comment>'; | ||
| 239 | } | ||
| 240 | |||
| 241 | return [sprintf('<info>%s</info>', $label), $this->formatValue($value)]; | ||
| 242 | } | ||
| 243 | |||
| 244 | /** | ||
| 245 | * Format the association mappings | ||
| 246 | * | ||
| 247 | * @psalm-param array<string, FieldMapping|AssociationMapping> $propertyMappings | ||
| 248 | * | ||
| 249 | * @return string[][] | ||
| 250 | * @psalm-return list<array{0: string, 1: string}> | ||
| 251 | */ | ||
| 252 | private function formatMappings(array $propertyMappings): array | ||
| 253 | { | ||
| 254 | $output = []; | ||
| 255 | |||
| 256 | foreach ($propertyMappings as $propertyName => $mapping) { | ||
| 257 | $output[] = $this->formatField(sprintf(' %s', $propertyName), ''); | ||
| 258 | |||
| 259 | foreach ((array) $mapping as $field => $value) { | ||
| 260 | $output[] = $this->formatField(sprintf(' %s', $field), $this->formatValue($value)); | ||
| 261 | } | ||
| 262 | } | ||
| 263 | |||
| 264 | return $output; | ||
| 265 | } | ||
| 266 | |||
| 267 | /** | ||
| 268 | * Format the entity listeners | ||
| 269 | * | ||
| 270 | * @psalm-param list<object> $entityListeners | ||
| 271 | * | ||
| 272 | * @return string[] | ||
| 273 | * @psalm-return array{0: string, 1: string} | ||
| 274 | */ | ||
| 275 | private function formatEntityListeners(array $entityListeners): array | ||
| 276 | { | ||
| 277 | return $this->formatField('Entity listeners', array_map('get_class', $entityListeners)); | ||
| 278 | } | ||
| 279 | } | ||
diff --git a/vendor/doctrine/orm/src/Tools/Console/Command/RunDqlCommand.php b/vendor/doctrine/orm/src/Tools/Console/Command/RunDqlCommand.php new file mode 100644 index 0000000..252151e --- /dev/null +++ b/vendor/doctrine/orm/src/Tools/Console/Command/RunDqlCommand.php | |||
| @@ -0,0 +1,118 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Tools\Console\Command; | ||
| 6 | |||
| 7 | use Doctrine\ORM\Tools\Debug; | ||
| 8 | use LogicException; | ||
| 9 | use RuntimeException; | ||
| 10 | use Symfony\Component\Console\Input\InputArgument; | ||
| 11 | use Symfony\Component\Console\Input\InputInterface; | ||
| 12 | use Symfony\Component\Console\Input\InputOption; | ||
| 13 | use Symfony\Component\Console\Output\OutputInterface; | ||
| 14 | use Symfony\Component\Console\Style\SymfonyStyle; | ||
| 15 | |||
| 16 | use function constant; | ||
| 17 | use function defined; | ||
| 18 | use function is_numeric; | ||
| 19 | use function sprintf; | ||
| 20 | use function str_replace; | ||
| 21 | use function strtoupper; | ||
| 22 | |||
| 23 | /** | ||
| 24 | * Command to execute DQL queries in a given EntityManager. | ||
| 25 | * | ||
| 26 | * @link www.doctrine-project.org | ||
| 27 | */ | ||
| 28 | class RunDqlCommand extends AbstractEntityManagerCommand | ||
| 29 | { | ||
| 30 | protected function configure(): void | ||
| 31 | { | ||
| 32 | $this->setName('orm:run-dql') | ||
| 33 | ->setDescription('Executes arbitrary DQL directly from the command line') | ||
| 34 | ->addArgument('dql', InputArgument::REQUIRED, 'The DQL to execute.') | ||
| 35 | ->addOption('em', null, InputOption::VALUE_REQUIRED, 'Name of the entity manager to operate on') | ||
| 36 | ->addOption('hydrate', null, InputOption::VALUE_REQUIRED, 'Hydration mode of result set. Should be either: object, array, scalar or single-scalar.', 'object') | ||
| 37 | ->addOption('first-result', null, InputOption::VALUE_REQUIRED, 'The first result in the result set.') | ||
| 38 | ->addOption('max-result', null, InputOption::VALUE_REQUIRED, 'The maximum number of results in the result set.') | ||
| 39 | ->addOption('depth', null, InputOption::VALUE_REQUIRED, 'Dumping depth of Entity graph.', 7) | ||
| 40 | ->addOption('show-sql', null, InputOption::VALUE_NONE, 'Dump generated SQL instead of executing query') | ||
| 41 | ->setHelp(<<<'EOT' | ||
| 42 | The <info>%command.name%</info> command executes the given DQL query and | ||
| 43 | outputs the results: | ||
| 44 | |||
| 45 | <info>php %command.full_name% "SELECT u FROM App\Entity\User u"</info> | ||
| 46 | |||
| 47 | You can also optionally specify some additional options like what type of | ||
| 48 | hydration to use when executing the query: | ||
| 49 | |||
| 50 | <info>php %command.full_name% "SELECT u FROM App\Entity\User u" --hydrate=array</info> | ||
| 51 | |||
| 52 | Additionally you can specify the first result and maximum amount of results to | ||
| 53 | show: | ||
| 54 | |||
| 55 | <info>php %command.full_name% "SELECT u FROM App\Entity\User u" --first-result=0 --max-result=30</info> | ||
| 56 | EOT); | ||
| 57 | } | ||
| 58 | |||
| 59 | protected function execute(InputInterface $input, OutputInterface $output): int | ||
| 60 | { | ||
| 61 | $ui = new SymfonyStyle($input, $output); | ||
| 62 | |||
| 63 | $em = $this->getEntityManager($input); | ||
| 64 | |||
| 65 | $dql = $input->getArgument('dql'); | ||
| 66 | if ($dql === null) { | ||
| 67 | throw new RuntimeException("Argument 'dql' is required in order to execute this command correctly."); | ||
| 68 | } | ||
| 69 | |||
| 70 | $depth = $input->getOption('depth'); | ||
| 71 | |||
| 72 | if (! is_numeric($depth)) { | ||
| 73 | throw new LogicException("Option 'depth' must contain an integer value"); | ||
| 74 | } | ||
| 75 | |||
| 76 | $hydrationModeName = (string) $input->getOption('hydrate'); | ||
| 77 | $hydrationMode = 'Doctrine\ORM\Query::HYDRATE_' . strtoupper(str_replace('-', '_', $hydrationModeName)); | ||
| 78 | |||
| 79 | if (! defined($hydrationMode)) { | ||
| 80 | throw new RuntimeException(sprintf( | ||
| 81 | "Hydration mode '%s' does not exist. It should be either: object. array, scalar or single-scalar.", | ||
| 82 | $hydrationModeName, | ||
| 83 | )); | ||
| 84 | } | ||
| 85 | |||
| 86 | $query = $em->createQuery($dql); | ||
| 87 | |||
| 88 | $firstResult = $input->getOption('first-result'); | ||
| 89 | if ($firstResult !== null) { | ||
| 90 | if (! is_numeric($firstResult)) { | ||
| 91 | throw new LogicException("Option 'first-result' must contain an integer value"); | ||
| 92 | } | ||
| 93 | |||
| 94 | $query->setFirstResult((int) $firstResult); | ||
| 95 | } | ||
| 96 | |||
| 97 | $maxResult = $input->getOption('max-result'); | ||
| 98 | if ($maxResult !== null) { | ||
| 99 | if (! is_numeric($maxResult)) { | ||
| 100 | throw new LogicException("Option 'max-result' must contain an integer value"); | ||
| 101 | } | ||
| 102 | |||
| 103 | $query->setMaxResults((int) $maxResult); | ||
| 104 | } | ||
| 105 | |||
| 106 | if ($input->getOption('show-sql')) { | ||
| 107 | $ui->text($query->getSQL()); | ||
| 108 | |||
| 109 | return 0; | ||
| 110 | } | ||
| 111 | |||
| 112 | $resultSet = $query->execute([], constant($hydrationMode)); | ||
| 113 | |||
| 114 | $ui->text(Debug::dump($resultSet, (int) $input->getOption('depth'))); | ||
| 115 | |||
| 116 | return 0; | ||
| 117 | } | ||
| 118 | } | ||
diff --git a/vendor/doctrine/orm/src/Tools/Console/Command/SchemaTool/AbstractCommand.php b/vendor/doctrine/orm/src/Tools/Console/Command/SchemaTool/AbstractCommand.php new file mode 100644 index 0000000..b1e4460 --- /dev/null +++ b/vendor/doctrine/orm/src/Tools/Console/Command/SchemaTool/AbstractCommand.php | |||
| @@ -0,0 +1,39 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Tools\Console\Command\SchemaTool; | ||
| 6 | |||
| 7 | use Doctrine\ORM\Tools\Console\Command\AbstractEntityManagerCommand; | ||
| 8 | use Doctrine\ORM\Tools\SchemaTool; | ||
| 9 | use Symfony\Component\Console\Input\InputInterface; | ||
| 10 | use Symfony\Component\Console\Output\OutputInterface; | ||
| 11 | use Symfony\Component\Console\Style\SymfonyStyle; | ||
| 12 | |||
| 13 | /** | ||
| 14 | * Base class for CreateCommand, DropCommand and UpdateCommand. | ||
| 15 | * | ||
| 16 | * @link www.doctrine-project.org | ||
| 17 | */ | ||
| 18 | abstract class AbstractCommand extends AbstractEntityManagerCommand | ||
| 19 | { | ||
| 20 | /** @param mixed[] $metadatas */ | ||
| 21 | abstract protected function executeSchemaCommand(InputInterface $input, OutputInterface $output, SchemaTool $schemaTool, array $metadatas, SymfonyStyle $ui): int; | ||
| 22 | |||
| 23 | protected function execute(InputInterface $input, OutputInterface $output): int | ||
| 24 | { | ||
| 25 | $ui = new SymfonyStyle($input, $output); | ||
| 26 | |||
| 27 | $em = $this->getEntityManager($input); | ||
| 28 | |||
| 29 | $metadatas = $em->getMetadataFactory()->getAllMetadata(); | ||
| 30 | |||
| 31 | if (empty($metadatas)) { | ||
| 32 | $ui->getErrorStyle()->success('No Metadata Classes to process.'); | ||
| 33 | |||
| 34 | return 0; | ||
| 35 | } | ||
| 36 | |||
| 37 | return $this->executeSchemaCommand($input, $output, new SchemaTool($em), $metadatas, $ui); | ||
| 38 | } | ||
| 39 | } | ||
diff --git a/vendor/doctrine/orm/src/Tools/Console/Command/SchemaTool/CreateCommand.php b/vendor/doctrine/orm/src/Tools/Console/Command/SchemaTool/CreateCommand.php new file mode 100644 index 0000000..69e20c6 --- /dev/null +++ b/vendor/doctrine/orm/src/Tools/Console/Command/SchemaTool/CreateCommand.php | |||
| @@ -0,0 +1,75 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Tools\Console\Command\SchemaTool; | ||
| 6 | |||
| 7 | use Doctrine\ORM\Tools\SchemaTool; | ||
| 8 | use Symfony\Component\Console\Input\InputInterface; | ||
| 9 | use Symfony\Component\Console\Input\InputOption; | ||
| 10 | use Symfony\Component\Console\Output\OutputInterface; | ||
| 11 | use Symfony\Component\Console\Style\SymfonyStyle; | ||
| 12 | |||
| 13 | use function sprintf; | ||
| 14 | |||
| 15 | /** | ||
| 16 | * Command to create the database schema for a set of classes based on their mappings. | ||
| 17 | * | ||
| 18 | * @link www.doctrine-project.org | ||
| 19 | */ | ||
| 20 | class CreateCommand extends AbstractCommand | ||
| 21 | { | ||
| 22 | protected function configure(): void | ||
| 23 | { | ||
| 24 | $this->setName('orm:schema-tool:create') | ||
| 25 | ->setDescription('Processes the schema and either create it directly on EntityManager Storage Connection or generate the SQL output') | ||
| 26 | ->addOption('em', null, InputOption::VALUE_REQUIRED, 'Name of the entity manager to operate on') | ||
| 27 | ->addOption('dump-sql', null, InputOption::VALUE_NONE, 'Instead of trying to apply generated SQLs into EntityManager Storage Connection, output them.') | ||
| 28 | ->setHelp(<<<'EOT' | ||
| 29 | Processes the schema and either create it directly on EntityManager Storage Connection or generate the SQL output. | ||
| 30 | |||
| 31 | <comment>Hint:</comment> If you have a database with tables that should not be managed | ||
| 32 | by the ORM, you can use a DBAL functionality to filter the tables and sequences down | ||
| 33 | on a global level: | ||
| 34 | |||
| 35 | $config->setSchemaAssetsFilter(function (string|AbstractAsset $assetName): bool { | ||
| 36 | if ($assetName instanceof AbstractAsset) { | ||
| 37 | $assetName = $assetName->getName(); | ||
| 38 | } | ||
| 39 | |||
| 40 | return !str_starts_with($assetName, 'audit_'); | ||
| 41 | }); | ||
| 42 | EOT); | ||
| 43 | } | ||
| 44 | |||
| 45 | /** | ||
| 46 | * {@inheritDoc} | ||
| 47 | */ | ||
| 48 | protected function executeSchemaCommand(InputInterface $input, OutputInterface $output, SchemaTool $schemaTool, array $metadatas, SymfonyStyle $ui): int | ||
| 49 | { | ||
| 50 | $dumpSql = $input->getOption('dump-sql') === true; | ||
| 51 | |||
| 52 | if ($dumpSql) { | ||
| 53 | $sqls = $schemaTool->getCreateSchemaSql($metadatas); | ||
| 54 | |||
| 55 | foreach ($sqls as $sql) { | ||
| 56 | $ui->writeln(sprintf('%s;', $sql)); | ||
| 57 | } | ||
| 58 | |||
| 59 | return 0; | ||
| 60 | } | ||
| 61 | |||
| 62 | $notificationUi = $ui->getErrorStyle(); | ||
| 63 | |||
| 64 | $notificationUi->caution('This operation should not be executed in a production environment!'); | ||
| 65 | |||
| 66 | $notificationUi->text('Creating database schema...'); | ||
| 67 | $notificationUi->newLine(); | ||
| 68 | |||
| 69 | $schemaTool->createSchema($metadatas); | ||
| 70 | |||
| 71 | $notificationUi->success('Database schema created successfully!'); | ||
| 72 | |||
| 73 | return 0; | ||
| 74 | } | ||
| 75 | } | ||
diff --git a/vendor/doctrine/orm/src/Tools/Console/Command/SchemaTool/DropCommand.php b/vendor/doctrine/orm/src/Tools/Console/Command/SchemaTool/DropCommand.php new file mode 100644 index 0000000..5c8253b --- /dev/null +++ b/vendor/doctrine/orm/src/Tools/Console/Command/SchemaTool/DropCommand.php | |||
| @@ -0,0 +1,116 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Tools\Console\Command\SchemaTool; | ||
| 6 | |||
| 7 | use Doctrine\ORM\Tools\SchemaTool; | ||
| 8 | use Symfony\Component\Console\Input\InputInterface; | ||
| 9 | use Symfony\Component\Console\Input\InputOption; | ||
| 10 | use Symfony\Component\Console\Output\OutputInterface; | ||
| 11 | use Symfony\Component\Console\Style\SymfonyStyle; | ||
| 12 | |||
| 13 | use function count; | ||
| 14 | use function sprintf; | ||
| 15 | |||
| 16 | /** | ||
| 17 | * Command to drop the database schema for a set of classes based on their mappings. | ||
| 18 | * | ||
| 19 | * @link www.doctrine-project.org | ||
| 20 | */ | ||
| 21 | class DropCommand extends AbstractCommand | ||
| 22 | { | ||
| 23 | protected function configure(): void | ||
| 24 | { | ||
| 25 | $this->setName('orm:schema-tool:drop') | ||
| 26 | ->setDescription('Drop the complete database schema of EntityManager Storage Connection or generate the corresponding SQL output') | ||
| 27 | ->addOption('em', null, InputOption::VALUE_REQUIRED, 'Name of the entity manager to operate on') | ||
| 28 | ->addOption('dump-sql', null, InputOption::VALUE_NONE, 'Instead of trying to apply generated SQLs into EntityManager Storage Connection, output them.') | ||
| 29 | ->addOption('force', 'f', InputOption::VALUE_NONE, "Don't ask for the deletion of the database, but force the operation to run.") | ||
| 30 | ->addOption('full-database', null, InputOption::VALUE_NONE, 'Instead of using the Class Metadata to detect the database table schema, drop ALL assets that the database contains.') | ||
| 31 | ->setHelp(<<<'EOT' | ||
| 32 | Processes the schema and either drop the database schema of EntityManager Storage Connection or generate the SQL output. | ||
| 33 | Beware that the complete database is dropped by this command, even tables that are not relevant to your metadata model. | ||
| 34 | |||
| 35 | <comment>Hint:</comment> If you have a database with tables that should not be managed | ||
| 36 | by the ORM, you can use a DBAL functionality to filter the tables and sequences down | ||
| 37 | on a global level: | ||
| 38 | |||
| 39 | $config->setSchemaAssetsFilter(function (string|AbstractAsset $assetName): bool { | ||
| 40 | if ($assetName instanceof AbstractAsset) { | ||
| 41 | $assetName = $assetName->getName(); | ||
| 42 | } | ||
| 43 | |||
| 44 | return !str_starts_with($assetName, 'audit_'); | ||
| 45 | }); | ||
| 46 | EOT); | ||
| 47 | } | ||
| 48 | |||
| 49 | /** | ||
| 50 | * {@inheritDoc} | ||
| 51 | */ | ||
| 52 | protected function executeSchemaCommand(InputInterface $input, OutputInterface $output, SchemaTool $schemaTool, array $metadatas, SymfonyStyle $ui): int | ||
| 53 | { | ||
| 54 | $isFullDatabaseDrop = $input->getOption('full-database'); | ||
| 55 | $dumpSql = $input->getOption('dump-sql') === true; | ||
| 56 | $force = $input->getOption('force') === true; | ||
| 57 | |||
| 58 | if ($dumpSql) { | ||
| 59 | if ($isFullDatabaseDrop) { | ||
| 60 | $sqls = $schemaTool->getDropDatabaseSQL(); | ||
| 61 | } else { | ||
| 62 | $sqls = $schemaTool->getDropSchemaSQL($metadatas); | ||
| 63 | } | ||
| 64 | |||
| 65 | foreach ($sqls as $sql) { | ||
| 66 | $ui->writeln(sprintf('%s;', $sql)); | ||
| 67 | } | ||
| 68 | |||
| 69 | return 0; | ||
| 70 | } | ||
| 71 | |||
| 72 | $notificationUi = $ui->getErrorStyle(); | ||
| 73 | |||
| 74 | if ($force) { | ||
| 75 | $notificationUi->text('Dropping database schema...'); | ||
| 76 | $notificationUi->newLine(); | ||
| 77 | |||
| 78 | if ($isFullDatabaseDrop) { | ||
| 79 | $schemaTool->dropDatabase(); | ||
| 80 | } else { | ||
| 81 | $schemaTool->dropSchema($metadatas); | ||
| 82 | } | ||
| 83 | |||
| 84 | $notificationUi->success('Database schema dropped successfully!'); | ||
| 85 | |||
| 86 | return 0; | ||
| 87 | } | ||
| 88 | |||
| 89 | $notificationUi->caution('This operation should not be executed in a production environment!'); | ||
| 90 | |||
| 91 | if ($isFullDatabaseDrop) { | ||
| 92 | $sqls = $schemaTool->getDropDatabaseSQL(); | ||
| 93 | } else { | ||
| 94 | $sqls = $schemaTool->getDropSchemaSQL($metadatas); | ||
| 95 | } | ||
| 96 | |||
| 97 | if (empty($sqls)) { | ||
| 98 | $notificationUi->success('Nothing to drop. The database is empty!'); | ||
| 99 | |||
| 100 | return 0; | ||
| 101 | } | ||
| 102 | |||
| 103 | $notificationUi->text( | ||
| 104 | [ | ||
| 105 | sprintf('The Schema-Tool would execute <info>"%s"</info> queries to update the database.', count($sqls)), | ||
| 106 | '', | ||
| 107 | 'Please run the operation by passing one - or both - of the following options:', | ||
| 108 | '', | ||
| 109 | sprintf(' <info>%s --force</info> to execute the command', $this->getName()), | ||
| 110 | sprintf(' <info>%s --dump-sql</info> to dump the SQL statements to the screen', $this->getName()), | ||
| 111 | ], | ||
| 112 | ); | ||
| 113 | |||
| 114 | return 1; | ||
| 115 | } | ||
| 116 | } | ||
diff --git a/vendor/doctrine/orm/src/Tools/Console/Command/SchemaTool/UpdateCommand.php b/vendor/doctrine/orm/src/Tools/Console/Command/SchemaTool/UpdateCommand.php new file mode 100644 index 0000000..f35fc38 --- /dev/null +++ b/vendor/doctrine/orm/src/Tools/Console/Command/SchemaTool/UpdateCommand.php | |||
| @@ -0,0 +1,147 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Tools\Console\Command\SchemaTool; | ||
| 6 | |||
| 7 | use Doctrine\Deprecations\Deprecation; | ||
| 8 | use Doctrine\ORM\Tools\SchemaTool; | ||
| 9 | use Symfony\Component\Console\Input\InputInterface; | ||
| 10 | use Symfony\Component\Console\Input\InputOption; | ||
| 11 | use Symfony\Component\Console\Output\OutputInterface; | ||
| 12 | use Symfony\Component\Console\Style\SymfonyStyle; | ||
| 13 | |||
| 14 | use function count; | ||
| 15 | use function sprintf; | ||
| 16 | |||
| 17 | /** | ||
| 18 | * Command to generate the SQL needed to update the database schema to match | ||
| 19 | * the current mapping information. | ||
| 20 | * | ||
| 21 | * @link www.doctrine-project.org | ||
| 22 | */ | ||
| 23 | class UpdateCommand extends AbstractCommand | ||
| 24 | { | ||
| 25 | protected string $name = 'orm:schema-tool:update'; | ||
| 26 | |||
| 27 | protected function configure(): void | ||
| 28 | { | ||
| 29 | $this->setName($this->name) | ||
| 30 | ->setDescription('Executes (or dumps) the SQL needed to update the database schema to match the current mapping metadata') | ||
| 31 | ->addOption('em', null, InputOption::VALUE_REQUIRED, 'Name of the entity manager to operate on') | ||
| 32 | ->addOption('complete', null, InputOption::VALUE_NONE, 'This option is a no-op, is deprecated and will be removed in 4.0') | ||
| 33 | ->addOption('dump-sql', null, InputOption::VALUE_NONE, 'Dumps the generated SQL statements to the screen (does not execute them).') | ||
| 34 | ->addOption('force', 'f', InputOption::VALUE_NONE, 'Causes the generated SQL statements to be physically executed against your database.') | ||
| 35 | ->setHelp(<<<'EOT' | ||
| 36 | The <info>%command.name%</info> command generates the SQL needed to | ||
| 37 | synchronize the database schema with the current mapping metadata of the | ||
| 38 | default entity manager. | ||
| 39 | |||
| 40 | For example, if you add metadata for a new column to an entity, this command | ||
| 41 | would generate and output the SQL needed to add the new column to the database: | ||
| 42 | |||
| 43 | <info>%command.name% --dump-sql</info> | ||
| 44 | |||
| 45 | Alternatively, you can execute the generated queries: | ||
| 46 | |||
| 47 | <info>%command.name% --force</info> | ||
| 48 | |||
| 49 | If both options are specified, the queries are output and then executed: | ||
| 50 | |||
| 51 | <info>%command.name% --dump-sql --force</info> | ||
| 52 | |||
| 53 | Finally, be aware that this task will drop all database assets (e.g. tables, | ||
| 54 | etc) that are *not* described by the current metadata. In other words, without | ||
| 55 | this option, this task leaves untouched any "extra" tables that exist in the | ||
| 56 | database, but which aren't described by any metadata. | ||
| 57 | |||
| 58 | <comment>Hint:</comment> If you have a database with tables that should not be managed | ||
| 59 | by the ORM, you can use a DBAL functionality to filter the tables and sequences down | ||
| 60 | on a global level: | ||
| 61 | |||
| 62 | $config->setSchemaAssetsFilter(function (string|AbstractAsset $assetName): bool { | ||
| 63 | if ($assetName instanceof AbstractAsset) { | ||
| 64 | $assetName = $assetName->getName(); | ||
| 65 | } | ||
| 66 | |||
| 67 | return !str_starts_with($assetName, 'audit_'); | ||
| 68 | }); | ||
| 69 | EOT); | ||
| 70 | } | ||
| 71 | |||
| 72 | /** | ||
| 73 | * {@inheritDoc} | ||
| 74 | */ | ||
| 75 | protected function executeSchemaCommand(InputInterface $input, OutputInterface $output, SchemaTool $schemaTool, array $metadatas, SymfonyStyle $ui): int | ||
| 76 | { | ||
| 77 | $notificationUi = $ui->getErrorStyle(); | ||
| 78 | |||
| 79 | if ($input->getOption('complete') === true) { | ||
| 80 | Deprecation::trigger( | ||
| 81 | 'doctrine/orm', | ||
| 82 | 'https://github.com/doctrine/orm/pull/11354', | ||
| 83 | 'The --complete option is a no-op, is deprecated and will be removed in Doctrine ORM 4.0.', | ||
| 84 | ); | ||
| 85 | $notificationUi->warning('The --complete option is a no-op, is deprecated and will be removed in Doctrine ORM 4.0.'); | ||
| 86 | } | ||
| 87 | |||
| 88 | $sqls = $schemaTool->getUpdateSchemaSql($metadatas); | ||
| 89 | |||
| 90 | if (empty($sqls)) { | ||
| 91 | $notificationUi->success('Nothing to update - your database is already in sync with the current entity metadata.'); | ||
| 92 | |||
| 93 | return 0; | ||
| 94 | } | ||
| 95 | |||
| 96 | $dumpSql = $input->getOption('dump-sql') === true; | ||
| 97 | $force = $input->getOption('force') === true; | ||
| 98 | |||
| 99 | if ($dumpSql) { | ||
| 100 | foreach ($sqls as $sql) { | ||
| 101 | $ui->writeln(sprintf('%s;', $sql)); | ||
| 102 | } | ||
| 103 | } | ||
| 104 | |||
| 105 | if ($force) { | ||
| 106 | if ($dumpSql) { | ||
| 107 | $notificationUi->newLine(); | ||
| 108 | } | ||
| 109 | |||
| 110 | $notificationUi->text('Updating database schema...'); | ||
| 111 | $notificationUi->newLine(); | ||
| 112 | |||
| 113 | $schemaTool->updateSchema($metadatas); | ||
| 114 | |||
| 115 | $pluralization = count($sqls) === 1 ? 'query was' : 'queries were'; | ||
| 116 | |||
| 117 | $notificationUi->text(sprintf(' <info>%s</info> %s executed', count($sqls), $pluralization)); | ||
| 118 | $notificationUi->success('Database schema updated successfully!'); | ||
| 119 | } | ||
| 120 | |||
| 121 | if ($dumpSql || $force) { | ||
| 122 | return 0; | ||
| 123 | } | ||
| 124 | |||
| 125 | $notificationUi->caution( | ||
| 126 | [ | ||
| 127 | 'This operation should not be executed in a production environment!', | ||
| 128 | '', | ||
| 129 | 'Use the incremental update to detect changes during development and use', | ||
| 130 | 'the SQL DDL provided to manually update your database in production.', | ||
| 131 | ], | ||
| 132 | ); | ||
| 133 | |||
| 134 | $notificationUi->text( | ||
| 135 | [ | ||
| 136 | sprintf('The Schema-Tool would execute <info>"%s"</info> queries to update the database.', count($sqls)), | ||
| 137 | '', | ||
| 138 | 'Please run the operation by passing one - or both - of the following options:', | ||
| 139 | '', | ||
| 140 | sprintf(' <info>%s --force</info> to execute the command', $this->getName()), | ||
| 141 | sprintf(' <info>%s --dump-sql</info> to dump the SQL statements to the screen', $this->getName()), | ||
| 142 | ], | ||
| 143 | ); | ||
| 144 | |||
| 145 | return 1; | ||
| 146 | } | ||
| 147 | } | ||
diff --git a/vendor/doctrine/orm/src/Tools/Console/Command/ValidateSchemaCommand.php b/vendor/doctrine/orm/src/Tools/Console/Command/ValidateSchemaCommand.php new file mode 100644 index 0000000..cffb4ce --- /dev/null +++ b/vendor/doctrine/orm/src/Tools/Console/Command/ValidateSchemaCommand.php | |||
| @@ -0,0 +1,89 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Tools\Console\Command; | ||
| 6 | |||
| 7 | use Doctrine\ORM\Tools\SchemaValidator; | ||
| 8 | use Symfony\Component\Console\Input\InputInterface; | ||
| 9 | use Symfony\Component\Console\Input\InputOption; | ||
| 10 | use Symfony\Component\Console\Output\OutputInterface; | ||
| 11 | use Symfony\Component\Console\Style\SymfonyStyle; | ||
| 12 | |||
| 13 | use function count; | ||
| 14 | use function sprintf; | ||
| 15 | |||
| 16 | /** | ||
| 17 | * Command to validate that the current mapping is valid. | ||
| 18 | * | ||
| 19 | * @link www.doctrine-project.com | ||
| 20 | */ | ||
| 21 | class ValidateSchemaCommand extends AbstractEntityManagerCommand | ||
| 22 | { | ||
| 23 | protected function configure(): void | ||
| 24 | { | ||
| 25 | $this->setName('orm:validate-schema') | ||
| 26 | ->setDescription('Validate the mapping files') | ||
| 27 | ->addOption('em', null, InputOption::VALUE_REQUIRED, 'Name of the entity manager to operate on') | ||
| 28 | ->addOption('skip-mapping', null, InputOption::VALUE_NONE, 'Skip the mapping validation check') | ||
| 29 | ->addOption('skip-sync', null, InputOption::VALUE_NONE, 'Skip checking if the mapping is in sync with the database') | ||
| 30 | ->addOption('skip-property-types', null, InputOption::VALUE_NONE, 'Skip checking if property types match the Doctrine types') | ||
| 31 | ->setHelp('Validate that the mapping files are correct and in sync with the database.'); | ||
| 32 | } | ||
| 33 | |||
| 34 | protected function execute(InputInterface $input, OutputInterface $output): int | ||
| 35 | { | ||
| 36 | $ui = (new SymfonyStyle($input, $output))->getErrorStyle(); | ||
| 37 | |||
| 38 | $em = $this->getEntityManager($input); | ||
| 39 | $validator = new SchemaValidator($em, ! $input->getOption('skip-property-types')); | ||
| 40 | $exit = 0; | ||
| 41 | |||
| 42 | $ui->section('Mapping'); | ||
| 43 | |||
| 44 | if ($input->getOption('skip-mapping')) { | ||
| 45 | $ui->text('<comment>[SKIPPED] The mapping was not checked.</comment>'); | ||
| 46 | } else { | ||
| 47 | $errors = $validator->validateMapping(); | ||
| 48 | if ($errors) { | ||
| 49 | foreach ($errors as $className => $errorMessages) { | ||
| 50 | $ui->text( | ||
| 51 | sprintf( | ||
| 52 | '<error>[FAIL]</error> The entity-class <comment>%s</comment> mapping is invalid:', | ||
| 53 | $className, | ||
| 54 | ), | ||
| 55 | ); | ||
| 56 | |||
| 57 | $ui->listing($errorMessages); | ||
| 58 | $ui->newLine(); | ||
| 59 | } | ||
| 60 | |||
| 61 | ++$exit; | ||
| 62 | } else { | ||
| 63 | $ui->success('The mapping files are correct.'); | ||
| 64 | } | ||
| 65 | } | ||
| 66 | |||
| 67 | $ui->section('Database'); | ||
| 68 | |||
| 69 | if ($input->getOption('skip-sync')) { | ||
| 70 | $ui->text('<comment>[SKIPPED] The database was not checked for synchronicity.</comment>'); | ||
| 71 | } elseif (! $validator->schemaInSyncWithMetadata()) { | ||
| 72 | $ui->error('The database schema is not in sync with the current mapping file.'); | ||
| 73 | |||
| 74 | if ($output->getVerbosity() >= OutputInterface::VERBOSITY_VERBOSE) { | ||
| 75 | $sqls = $validator->getUpdateSchemaList(); | ||
| 76 | $ui->comment(sprintf('<info>%d</info> schema diff(s) detected:', count($sqls))); | ||
| 77 | foreach ($sqls as $sql) { | ||
| 78 | $ui->text(sprintf(' %s;', $sql)); | ||
| 79 | } | ||
| 80 | } | ||
| 81 | |||
| 82 | $exit += 2; | ||
| 83 | } else { | ||
| 84 | $ui->success('The database schema is in sync with the mapping files.'); | ||
| 85 | } | ||
| 86 | |||
| 87 | return $exit; | ||
| 88 | } | ||
| 89 | } | ||
diff --git a/vendor/doctrine/orm/src/Tools/Console/ConsoleRunner.php b/vendor/doctrine/orm/src/Tools/Console/ConsoleRunner.php new file mode 100644 index 0000000..0a00483 --- /dev/null +++ b/vendor/doctrine/orm/src/Tools/Console/ConsoleRunner.php | |||
| @@ -0,0 +1,88 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Tools\Console; | ||
| 6 | |||
| 7 | use Composer\InstalledVersions; | ||
| 8 | use Doctrine\DBAL\Tools\Console as DBALConsole; | ||
| 9 | use Doctrine\ORM\Tools\Console\EntityManagerProvider\ConnectionFromManagerProvider; | ||
| 10 | use OutOfBoundsException; | ||
| 11 | use Symfony\Component\Console\Application; | ||
| 12 | use Symfony\Component\Console\Command\Command as SymfonyCommand; | ||
| 13 | |||
| 14 | use function assert; | ||
| 15 | use function class_exists; | ||
| 16 | |||
| 17 | /** | ||
| 18 | * Handles running the Console Tools inside Symfony Console context. | ||
| 19 | */ | ||
| 20 | final class ConsoleRunner | ||
| 21 | { | ||
| 22 | /** | ||
| 23 | * Runs console with the given helper set. | ||
| 24 | * | ||
| 25 | * @param SymfonyCommand[] $commands | ||
| 26 | */ | ||
| 27 | public static function run(EntityManagerProvider $entityManagerProvider, array $commands = []): void | ||
| 28 | { | ||
| 29 | $cli = self::createApplication($entityManagerProvider, $commands); | ||
| 30 | $cli->run(); | ||
| 31 | } | ||
| 32 | |||
| 33 | /** | ||
| 34 | * Creates a console application with the given helperset and | ||
| 35 | * optional commands. | ||
| 36 | * | ||
| 37 | * @param SymfonyCommand[] $commands | ||
| 38 | * | ||
| 39 | * @throws OutOfBoundsException | ||
| 40 | */ | ||
| 41 | public static function createApplication( | ||
| 42 | EntityManagerProvider $entityManagerProvider, | ||
| 43 | array $commands = [], | ||
| 44 | ): Application { | ||
| 45 | $version = InstalledVersions::getVersion('doctrine/orm'); | ||
| 46 | assert($version !== null); | ||
| 47 | |||
| 48 | $cli = new Application('Doctrine Command Line Interface', $version); | ||
| 49 | $cli->setCatchExceptions(true); | ||
| 50 | |||
| 51 | self::addCommands($cli, $entityManagerProvider); | ||
| 52 | $cli->addCommands($commands); | ||
| 53 | |||
| 54 | return $cli; | ||
| 55 | } | ||
| 56 | |||
| 57 | public static function addCommands(Application $cli, EntityManagerProvider $entityManagerProvider): void | ||
| 58 | { | ||
| 59 | $connectionProvider = new ConnectionFromManagerProvider($entityManagerProvider); | ||
| 60 | |||
| 61 | if (class_exists(DBALConsole\Command\ReservedWordsCommand::class)) { | ||
| 62 | $cli->add(new DBALConsole\Command\ReservedWordsCommand($connectionProvider)); | ||
| 63 | } | ||
| 64 | |||
| 65 | $cli->addCommands( | ||
| 66 | [ | ||
| 67 | // DBAL Commands | ||
| 68 | new DBALConsole\Command\RunSqlCommand($connectionProvider), | ||
| 69 | |||
| 70 | // ORM Commands | ||
| 71 | new Command\ClearCache\CollectionRegionCommand($entityManagerProvider), | ||
| 72 | new Command\ClearCache\EntityRegionCommand($entityManagerProvider), | ||
| 73 | new Command\ClearCache\MetadataCommand($entityManagerProvider), | ||
| 74 | new Command\ClearCache\QueryCommand($entityManagerProvider), | ||
| 75 | new Command\ClearCache\QueryRegionCommand($entityManagerProvider), | ||
| 76 | new Command\ClearCache\ResultCommand($entityManagerProvider), | ||
| 77 | new Command\SchemaTool\CreateCommand($entityManagerProvider), | ||
| 78 | new Command\SchemaTool\UpdateCommand($entityManagerProvider), | ||
| 79 | new Command\SchemaTool\DropCommand($entityManagerProvider), | ||
| 80 | new Command\GenerateProxiesCommand($entityManagerProvider), | ||
| 81 | new Command\RunDqlCommand($entityManagerProvider), | ||
| 82 | new Command\ValidateSchemaCommand($entityManagerProvider), | ||
| 83 | new Command\InfoCommand($entityManagerProvider), | ||
| 84 | new Command\MappingDescribeCommand($entityManagerProvider), | ||
| 85 | ], | ||
| 86 | ); | ||
| 87 | } | ||
| 88 | } | ||
diff --git a/vendor/doctrine/orm/src/Tools/Console/EntityManagerProvider.php b/vendor/doctrine/orm/src/Tools/Console/EntityManagerProvider.php new file mode 100644 index 0000000..866589b --- /dev/null +++ b/vendor/doctrine/orm/src/Tools/Console/EntityManagerProvider.php | |||
| @@ -0,0 +1,14 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Tools\Console; | ||
| 6 | |||
| 7 | use Doctrine\ORM\EntityManagerInterface; | ||
| 8 | |||
| 9 | interface EntityManagerProvider | ||
| 10 | { | ||
| 11 | public function getDefaultManager(): EntityManagerInterface; | ||
| 12 | |||
| 13 | public function getManager(string $name): EntityManagerInterface; | ||
| 14 | } | ||
diff --git a/vendor/doctrine/orm/src/Tools/Console/EntityManagerProvider/ConnectionFromManagerProvider.php b/vendor/doctrine/orm/src/Tools/Console/EntityManagerProvider/ConnectionFromManagerProvider.php new file mode 100644 index 0000000..0776601 --- /dev/null +++ b/vendor/doctrine/orm/src/Tools/Console/EntityManagerProvider/ConnectionFromManagerProvider.php | |||
| @@ -0,0 +1,26 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Tools\Console\EntityManagerProvider; | ||
| 6 | |||
| 7 | use Doctrine\DBAL\Connection; | ||
| 8 | use Doctrine\DBAL\Tools\Console\ConnectionProvider; | ||
| 9 | use Doctrine\ORM\Tools\Console\EntityManagerProvider; | ||
| 10 | |||
| 11 | final class ConnectionFromManagerProvider implements ConnectionProvider | ||
| 12 | { | ||
| 13 | public function __construct(private readonly EntityManagerProvider $entityManagerProvider) | ||
| 14 | { | ||
| 15 | } | ||
| 16 | |||
| 17 | public function getDefaultConnection(): Connection | ||
| 18 | { | ||
| 19 | return $this->entityManagerProvider->getDefaultManager()->getConnection(); | ||
| 20 | } | ||
| 21 | |||
| 22 | public function getConnection(string $name): Connection | ||
| 23 | { | ||
| 24 | return $this->entityManagerProvider->getManager($name)->getConnection(); | ||
| 25 | } | ||
| 26 | } | ||
diff --git a/vendor/doctrine/orm/src/Tools/Console/EntityManagerProvider/SingleManagerProvider.php b/vendor/doctrine/orm/src/Tools/Console/EntityManagerProvider/SingleManagerProvider.php new file mode 100644 index 0000000..ebe60c9 --- /dev/null +++ b/vendor/doctrine/orm/src/Tools/Console/EntityManagerProvider/SingleManagerProvider.php | |||
| @@ -0,0 +1,31 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Tools\Console\EntityManagerProvider; | ||
| 6 | |||
| 7 | use Doctrine\ORM\EntityManagerInterface; | ||
| 8 | use Doctrine\ORM\Tools\Console\EntityManagerProvider; | ||
| 9 | |||
| 10 | final class SingleManagerProvider implements EntityManagerProvider | ||
| 11 | { | ||
| 12 | public function __construct( | ||
| 13 | private readonly EntityManagerInterface $entityManager, | ||
| 14 | private readonly string $defaultManagerName = 'default', | ||
| 15 | ) { | ||
| 16 | } | ||
| 17 | |||
| 18 | public function getDefaultManager(): EntityManagerInterface | ||
| 19 | { | ||
| 20 | return $this->entityManager; | ||
| 21 | } | ||
| 22 | |||
| 23 | public function getManager(string $name): EntityManagerInterface | ||
| 24 | { | ||
| 25 | if ($name !== $this->defaultManagerName) { | ||
| 26 | throw UnknownManagerException::unknownManager($name, [$this->defaultManagerName]); | ||
| 27 | } | ||
| 28 | |||
| 29 | return $this->entityManager; | ||
| 30 | } | ||
| 31 | } | ||
diff --git a/vendor/doctrine/orm/src/Tools/Console/EntityManagerProvider/UnknownManagerException.php b/vendor/doctrine/orm/src/Tools/Console/EntityManagerProvider/UnknownManagerException.php new file mode 100644 index 0000000..583d909 --- /dev/null +++ b/vendor/doctrine/orm/src/Tools/Console/EntityManagerProvider/UnknownManagerException.php | |||
| @@ -0,0 +1,23 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Tools\Console\EntityManagerProvider; | ||
| 6 | |||
| 7 | use OutOfBoundsException; | ||
| 8 | |||
| 9 | use function implode; | ||
| 10 | use function sprintf; | ||
| 11 | |||
| 12 | final class UnknownManagerException extends OutOfBoundsException | ||
| 13 | { | ||
| 14 | /** @psalm-param list<string> $knownManagers */ | ||
| 15 | public static function unknownManager(string $unknownManager, array $knownManagers = []): self | ||
| 16 | { | ||
| 17 | return new self(sprintf( | ||
| 18 | 'Requested unknown entity manager: %s, known managers: %s', | ||
| 19 | $unknownManager, | ||
| 20 | implode(', ', $knownManagers), | ||
| 21 | )); | ||
| 22 | } | ||
| 23 | } | ||
diff --git a/vendor/doctrine/orm/src/Tools/Console/MetadataFilter.php b/vendor/doctrine/orm/src/Tools/Console/MetadataFilter.php new file mode 100644 index 0000000..05e248c --- /dev/null +++ b/vendor/doctrine/orm/src/Tools/Console/MetadataFilter.php | |||
| @@ -0,0 +1,92 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Tools\Console; | ||
| 6 | |||
| 7 | use ArrayIterator; | ||
| 8 | use Countable; | ||
| 9 | use Doctrine\Persistence\Mapping\ClassMetadata; | ||
| 10 | use FilterIterator; | ||
| 11 | use RuntimeException; | ||
| 12 | |||
| 13 | use function assert; | ||
| 14 | use function count; | ||
| 15 | use function iterator_to_array; | ||
| 16 | use function preg_match; | ||
| 17 | use function sprintf; | ||
| 18 | |||
| 19 | /** | ||
| 20 | * Used by CLI Tools to restrict entity-based commands to given patterns. | ||
| 21 | * | ||
| 22 | * @link www.doctrine-project.com | ||
| 23 | */ | ||
| 24 | class MetadataFilter extends FilterIterator implements Countable | ||
| 25 | { | ||
| 26 | /** @var mixed[] */ | ||
| 27 | private array $filter = []; | ||
| 28 | |||
| 29 | /** | ||
| 30 | * Filter Metadatas by one or more filter options. | ||
| 31 | * | ||
| 32 | * @param ClassMetadata[] $metadatas | ||
| 33 | * @param string[]|string $filter | ||
| 34 | * | ||
| 35 | * @return ClassMetadata[] | ||
| 36 | */ | ||
| 37 | public static function filter(array $metadatas, array|string $filter): array | ||
| 38 | { | ||
| 39 | $metadatas = new MetadataFilter(new ArrayIterator($metadatas), $filter); | ||
| 40 | |||
| 41 | return iterator_to_array($metadatas); | ||
| 42 | } | ||
| 43 | |||
| 44 | /** @param mixed[]|string $filter */ | ||
| 45 | public function __construct(ArrayIterator $metadata, array|string $filter) | ||
| 46 | { | ||
| 47 | $this->filter = (array) $filter; | ||
| 48 | |||
| 49 | parent::__construct($metadata); | ||
| 50 | } | ||
| 51 | |||
| 52 | public function accept(): bool | ||
| 53 | { | ||
| 54 | if (count($this->filter) === 0) { | ||
| 55 | return true; | ||
| 56 | } | ||
| 57 | |||
| 58 | $it = $this->getInnerIterator(); | ||
| 59 | $metadata = $it->current(); | ||
| 60 | |||
| 61 | foreach ($this->filter as $filter) { | ||
| 62 | $pregResult = preg_match('/' . $filter . '/', $metadata->getName()); | ||
| 63 | |||
| 64 | if ($pregResult === false) { | ||
| 65 | throw new RuntimeException( | ||
| 66 | sprintf("Error while evaluating regex '/%s/'.", $filter), | ||
| 67 | ); | ||
| 68 | } | ||
| 69 | |||
| 70 | if ($pregResult) { | ||
| 71 | return true; | ||
| 72 | } | ||
| 73 | } | ||
| 74 | |||
| 75 | return false; | ||
| 76 | } | ||
| 77 | |||
| 78 | /** @return ArrayIterator<int, ClassMetadata> */ | ||
| 79 | public function getInnerIterator(): ArrayIterator | ||
| 80 | { | ||
| 81 | $innerIterator = parent::getInnerIterator(); | ||
| 82 | |||
| 83 | assert($innerIterator instanceof ArrayIterator); | ||
| 84 | |||
| 85 | return $innerIterator; | ||
| 86 | } | ||
| 87 | |||
| 88 | public function count(): int | ||
| 89 | { | ||
| 90 | return count($this->getInnerIterator()); | ||
| 91 | } | ||
| 92 | } | ||
diff --git a/vendor/doctrine/orm/src/Tools/Debug.php b/vendor/doctrine/orm/src/Tools/Debug.php new file mode 100644 index 0000000..8521e53 --- /dev/null +++ b/vendor/doctrine/orm/src/Tools/Debug.php | |||
| @@ -0,0 +1,158 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Tools; | ||
| 6 | |||
| 7 | use ArrayIterator; | ||
| 8 | use ArrayObject; | ||
| 9 | use DateTimeInterface; | ||
| 10 | use Doctrine\Common\Collections\Collection; | ||
| 11 | use Doctrine\ORM\Proxy\DefaultProxyClassNameResolver; | ||
| 12 | use Doctrine\Persistence\Proxy; | ||
| 13 | use stdClass; | ||
| 14 | |||
| 15 | use function array_keys; | ||
| 16 | use function count; | ||
| 17 | use function end; | ||
| 18 | use function explode; | ||
| 19 | use function extension_loaded; | ||
| 20 | use function html_entity_decode; | ||
| 21 | use function ini_get; | ||
| 22 | use function ini_set; | ||
| 23 | use function is_array; | ||
| 24 | use function is_object; | ||
| 25 | use function ob_end_clean; | ||
| 26 | use function ob_get_contents; | ||
| 27 | use function ob_start; | ||
| 28 | use function strip_tags; | ||
| 29 | use function var_dump; | ||
| 30 | |||
| 31 | /** | ||
| 32 | * Static class containing most used debug methods. | ||
| 33 | * | ||
| 34 | * @internal | ||
| 35 | * | ||
| 36 | * @link www.doctrine-project.org | ||
| 37 | */ | ||
| 38 | final class Debug | ||
| 39 | { | ||
| 40 | /** | ||
| 41 | * Private constructor (prevents instantiation). | ||
| 42 | */ | ||
| 43 | private function __construct() | ||
| 44 | { | ||
| 45 | } | ||
| 46 | |||
| 47 | /** | ||
| 48 | * Prints a dump of the public, protected and private properties of $var. | ||
| 49 | * | ||
| 50 | * @link https://xdebug.org/ | ||
| 51 | * | ||
| 52 | * @param mixed $var The variable to dump. | ||
| 53 | * @param int $maxDepth The maximum nesting level for object properties. | ||
| 54 | */ | ||
| 55 | public static function dump(mixed $var, int $maxDepth = 2): string | ||
| 56 | { | ||
| 57 | $html = ini_get('html_errors'); | ||
| 58 | |||
| 59 | if ($html !== '1') { | ||
| 60 | ini_set('html_errors', 'on'); | ||
| 61 | } | ||
| 62 | |||
| 63 | if (extension_loaded('xdebug')) { | ||
| 64 | $previousDepth = ini_get('xdebug.var_display_max_depth'); | ||
| 65 | ini_set('xdebug.var_display_max_depth', (string) $maxDepth); | ||
| 66 | } | ||
| 67 | |||
| 68 | try { | ||
| 69 | $var = self::export($var, $maxDepth); | ||
| 70 | |||
| 71 | ob_start(); | ||
| 72 | var_dump($var); | ||
| 73 | |||
| 74 | $dump = ob_get_contents(); | ||
| 75 | |||
| 76 | ob_end_clean(); | ||
| 77 | |||
| 78 | $dumpText = strip_tags(html_entity_decode($dump)); | ||
| 79 | } finally { | ||
| 80 | ini_set('html_errors', $html); | ||
| 81 | |||
| 82 | if (isset($previousDepth)) { | ||
| 83 | ini_set('xdebug.var_display_max_depth', $previousDepth); | ||
| 84 | } | ||
| 85 | } | ||
| 86 | |||
| 87 | return $dumpText; | ||
| 88 | } | ||
| 89 | |||
| 90 | public static function export(mixed $var, int $maxDepth): mixed | ||
| 91 | { | ||
| 92 | if ($var instanceof Collection) { | ||
| 93 | $var = $var->toArray(); | ||
| 94 | } | ||
| 95 | |||
| 96 | if (! $maxDepth) { | ||
| 97 | return is_object($var) ? $var::class | ||
| 98 | : (is_array($var) ? 'Array(' . count($var) . ')' : $var); | ||
| 99 | } | ||
| 100 | |||
| 101 | if (is_array($var)) { | ||
| 102 | $return = []; | ||
| 103 | |||
| 104 | foreach ($var as $k => $v) { | ||
| 105 | $return[$k] = self::export($v, $maxDepth - 1); | ||
| 106 | } | ||
| 107 | |||
| 108 | return $return; | ||
| 109 | } | ||
| 110 | |||
| 111 | if (! is_object($var)) { | ||
| 112 | return $var; | ||
| 113 | } | ||
| 114 | |||
| 115 | $return = new stdClass(); | ||
| 116 | if ($var instanceof DateTimeInterface) { | ||
| 117 | $return->__CLASS__ = $var::class; | ||
| 118 | $return->date = $var->format('c'); | ||
| 119 | $return->timezone = $var->getTimezone()->getName(); | ||
| 120 | |||
| 121 | return $return; | ||
| 122 | } | ||
| 123 | |||
| 124 | $return->__CLASS__ = DefaultProxyClassNameResolver::getClass($var); | ||
| 125 | |||
| 126 | if ($var instanceof Proxy) { | ||
| 127 | $return->__IS_PROXY__ = true; | ||
| 128 | $return->__PROXY_INITIALIZED__ = $var->__isInitialized(); | ||
| 129 | } | ||
| 130 | |||
| 131 | if ($var instanceof ArrayObject || $var instanceof ArrayIterator) { | ||
| 132 | $return->__STORAGE__ = self::export($var->getArrayCopy(), $maxDepth - 1); | ||
| 133 | } | ||
| 134 | |||
| 135 | return self::fillReturnWithClassAttributes($var, $return, $maxDepth); | ||
| 136 | } | ||
| 137 | |||
| 138 | /** | ||
| 139 | * Fill the $return variable with class attributes | ||
| 140 | * Based on obj2array function from {@see https://secure.php.net/manual/en/function.get-object-vars.php#47075} | ||
| 141 | */ | ||
| 142 | private static function fillReturnWithClassAttributes(object $var, stdClass $return, int $maxDepth): stdClass | ||
| 143 | { | ||
| 144 | $clone = (array) $var; | ||
| 145 | |||
| 146 | foreach (array_keys($clone) as $key) { | ||
| 147 | $aux = explode("\0", (string) $key); | ||
| 148 | $name = end($aux); | ||
| 149 | if ($aux[0] === '') { | ||
| 150 | $name .= ':' . ($aux[1] === '*' ? 'protected' : $aux[1] . ':private'); | ||
| 151 | } | ||
| 152 | |||
| 153 | $return->$name = self::export($clone[$key], $maxDepth - 1); | ||
| 154 | } | ||
| 155 | |||
| 156 | return $return; | ||
| 157 | } | ||
| 158 | } | ||
diff --git a/vendor/doctrine/orm/src/Tools/DebugUnitOfWorkListener.php b/vendor/doctrine/orm/src/Tools/DebugUnitOfWorkListener.php new file mode 100644 index 0000000..71059f7 --- /dev/null +++ b/vendor/doctrine/orm/src/Tools/DebugUnitOfWorkListener.php | |||
| @@ -0,0 +1,144 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Tools; | ||
| 6 | |||
| 7 | use Doctrine\ORM\EntityManagerInterface; | ||
| 8 | use Doctrine\ORM\Event\OnFlushEventArgs; | ||
| 9 | use Doctrine\ORM\PersistentCollection; | ||
| 10 | use Doctrine\ORM\UnitOfWork; | ||
| 11 | use ReflectionObject; | ||
| 12 | |||
| 13 | use function count; | ||
| 14 | use function fclose; | ||
| 15 | use function fopen; | ||
| 16 | use function fwrite; | ||
| 17 | use function gettype; | ||
| 18 | use function is_object; | ||
| 19 | use function spl_object_id; | ||
| 20 | |||
| 21 | /** | ||
| 22 | * Use this logger to dump the identity map during the onFlush event. This is useful for debugging | ||
| 23 | * weird UnitOfWork behavior with complex operations. | ||
| 24 | */ | ||
| 25 | class DebugUnitOfWorkListener | ||
| 26 | { | ||
| 27 | /** | ||
| 28 | * Pass a stream and context information for the debugging session. | ||
| 29 | * | ||
| 30 | * The stream can be php://output to print to the screen. | ||
| 31 | */ | ||
| 32 | public function __construct( | ||
| 33 | private readonly string $file = 'php://output', | ||
| 34 | private readonly string $context = '', | ||
| 35 | ) { | ||
| 36 | } | ||
| 37 | |||
| 38 | public function onFlush(OnFlushEventArgs $args): void | ||
| 39 | { | ||
| 40 | $this->dumpIdentityMap($args->getObjectManager()); | ||
| 41 | } | ||
| 42 | |||
| 43 | /** | ||
| 44 | * Dumps the contents of the identity map into a stream. | ||
| 45 | */ | ||
| 46 | public function dumpIdentityMap(EntityManagerInterface $em): void | ||
| 47 | { | ||
| 48 | $uow = $em->getUnitOfWork(); | ||
| 49 | $identityMap = $uow->getIdentityMap(); | ||
| 50 | |||
| 51 | $fh = fopen($this->file, 'xb+'); | ||
| 52 | if (count($identityMap) === 0) { | ||
| 53 | fwrite($fh, 'Flush Operation [' . $this->context . "] - Empty identity map.\n"); | ||
| 54 | |||
| 55 | return; | ||
| 56 | } | ||
| 57 | |||
| 58 | fwrite($fh, 'Flush Operation [' . $this->context . "] - Dumping identity map:\n"); | ||
| 59 | foreach ($identityMap as $className => $map) { | ||
| 60 | fwrite($fh, 'Class: ' . $className . "\n"); | ||
| 61 | |||
| 62 | foreach ($map as $entity) { | ||
| 63 | fwrite($fh, ' Entity: ' . $this->getIdString($entity, $uow) . ' ' . spl_object_id($entity) . "\n"); | ||
| 64 | fwrite($fh, " Associations:\n"); | ||
| 65 | |||
| 66 | $cm = $em->getClassMetadata($className); | ||
| 67 | |||
| 68 | foreach ($cm->associationMappings as $field => $assoc) { | ||
| 69 | fwrite($fh, ' ' . $field . ' '); | ||
| 70 | $value = $cm->getFieldValue($entity, $field); | ||
| 71 | |||
| 72 | if ($assoc->isToOne()) { | ||
| 73 | if ($value === null) { | ||
| 74 | fwrite($fh, " NULL\n"); | ||
| 75 | } else { | ||
| 76 | if ($uow->isUninitializedObject($value)) { | ||
| 77 | fwrite($fh, '[PROXY] '); | ||
| 78 | } | ||
| 79 | |||
| 80 | fwrite($fh, $this->getIdString($value, $uow) . ' ' . spl_object_id($value) . "\n"); | ||
| 81 | } | ||
| 82 | } else { | ||
| 83 | $initialized = ! ($value instanceof PersistentCollection) || $value->isInitialized(); | ||
| 84 | if ($value === null) { | ||
| 85 | fwrite($fh, " NULL\n"); | ||
| 86 | } elseif ($initialized) { | ||
| 87 | fwrite($fh, '[INITIALIZED] ' . $this->getType($value) . ' ' . count($value) . " elements\n"); | ||
| 88 | |||
| 89 | foreach ($value as $obj) { | ||
| 90 | fwrite($fh, ' ' . $this->getIdString($obj, $uow) . ' ' . spl_object_id($obj) . "\n"); | ||
| 91 | } | ||
| 92 | } else { | ||
| 93 | fwrite($fh, '[PROXY] ' . $this->getType($value) . " unknown element size\n"); | ||
| 94 | foreach ($value->unwrap() as $obj) { | ||
| 95 | fwrite($fh, ' ' . $this->getIdString($obj, $uow) . ' ' . spl_object_id($obj) . "\n"); | ||
| 96 | } | ||
| 97 | } | ||
| 98 | } | ||
| 99 | } | ||
| 100 | } | ||
| 101 | } | ||
| 102 | |||
| 103 | fclose($fh); | ||
| 104 | } | ||
| 105 | |||
| 106 | private function getType(mixed $var): string | ||
| 107 | { | ||
| 108 | if (is_object($var)) { | ||
| 109 | $refl = new ReflectionObject($var); | ||
| 110 | |||
| 111 | return $refl->getShortName(); | ||
| 112 | } | ||
| 113 | |||
| 114 | return gettype($var); | ||
| 115 | } | ||
| 116 | |||
| 117 | private function getIdString(object $entity, UnitOfWork $uow): string | ||
| 118 | { | ||
| 119 | if ($uow->isInIdentityMap($entity)) { | ||
| 120 | $ids = $uow->getEntityIdentifier($entity); | ||
| 121 | $idstring = ''; | ||
| 122 | |||
| 123 | foreach ($ids as $k => $v) { | ||
| 124 | $idstring .= $k . '=' . $v; | ||
| 125 | } | ||
| 126 | } else { | ||
| 127 | $idstring = 'NEWOBJECT '; | ||
| 128 | } | ||
| 129 | |||
| 130 | $state = $uow->getEntityState($entity); | ||
| 131 | |||
| 132 | if ($state === UnitOfWork::STATE_NEW) { | ||
| 133 | $idstring .= ' [NEW]'; | ||
| 134 | } elseif ($state === UnitOfWork::STATE_REMOVED) { | ||
| 135 | $idstring .= ' [REMOVED]'; | ||
| 136 | } elseif ($state === UnitOfWork::STATE_MANAGED) { | ||
| 137 | $idstring .= ' [MANAGED]'; | ||
| 138 | } elseif ($state === UnitOfWork::STATE_DETACHED) { | ||
| 139 | $idstring .= ' [DETACHED]'; | ||
| 140 | } | ||
| 141 | |||
| 142 | return $idstring; | ||
| 143 | } | ||
| 144 | } | ||
diff --git a/vendor/doctrine/orm/src/Tools/Event/GenerateSchemaEventArgs.php b/vendor/doctrine/orm/src/Tools/Event/GenerateSchemaEventArgs.php new file mode 100644 index 0000000..3b0993e --- /dev/null +++ b/vendor/doctrine/orm/src/Tools/Event/GenerateSchemaEventArgs.php | |||
| @@ -0,0 +1,33 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Tools\Event; | ||
| 6 | |||
| 7 | use Doctrine\Common\EventArgs; | ||
| 8 | use Doctrine\DBAL\Schema\Schema; | ||
| 9 | use Doctrine\ORM\EntityManagerInterface; | ||
| 10 | |||
| 11 | /** | ||
| 12 | * Event Args used for the Events::postGenerateSchema event. | ||
| 13 | * | ||
| 14 | * @link www.doctrine-project.com | ||
| 15 | */ | ||
| 16 | class GenerateSchemaEventArgs extends EventArgs | ||
| 17 | { | ||
| 18 | public function __construct( | ||
| 19 | private readonly EntityManagerInterface $em, | ||
| 20 | private readonly Schema $schema, | ||
| 21 | ) { | ||
| 22 | } | ||
| 23 | |||
| 24 | public function getEntityManager(): EntityManagerInterface | ||
| 25 | { | ||
| 26 | return $this->em; | ||
| 27 | } | ||
| 28 | |||
| 29 | public function getSchema(): Schema | ||
| 30 | { | ||
| 31 | return $this->schema; | ||
| 32 | } | ||
| 33 | } | ||
diff --git a/vendor/doctrine/orm/src/Tools/Event/GenerateSchemaTableEventArgs.php b/vendor/doctrine/orm/src/Tools/Event/GenerateSchemaTableEventArgs.php new file mode 100644 index 0000000..a09aaae --- /dev/null +++ b/vendor/doctrine/orm/src/Tools/Event/GenerateSchemaTableEventArgs.php | |||
| @@ -0,0 +1,40 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Tools\Event; | ||
| 6 | |||
| 7 | use Doctrine\Common\EventArgs; | ||
| 8 | use Doctrine\DBAL\Schema\Schema; | ||
| 9 | use Doctrine\DBAL\Schema\Table; | ||
| 10 | use Doctrine\ORM\Mapping\ClassMetadata; | ||
| 11 | |||
| 12 | /** | ||
| 13 | * Event Args used for the Events::postGenerateSchemaTable event. | ||
| 14 | * | ||
| 15 | * @link www.doctrine-project.com | ||
| 16 | */ | ||
| 17 | class GenerateSchemaTableEventArgs extends EventArgs | ||
| 18 | { | ||
| 19 | public function __construct( | ||
| 20 | private readonly ClassMetadata $classMetadata, | ||
| 21 | private readonly Schema $schema, | ||
| 22 | private readonly Table $classTable, | ||
| 23 | ) { | ||
| 24 | } | ||
| 25 | |||
| 26 | public function getClassMetadata(): ClassMetadata | ||
| 27 | { | ||
| 28 | return $this->classMetadata; | ||
| 29 | } | ||
| 30 | |||
| 31 | public function getSchema(): Schema | ||
| 32 | { | ||
| 33 | return $this->schema; | ||
| 34 | } | ||
| 35 | |||
| 36 | public function getClassTable(): Table | ||
| 37 | { | ||
| 38 | return $this->classTable; | ||
| 39 | } | ||
| 40 | } | ||
diff --git a/vendor/doctrine/orm/src/Tools/Exception/MissingColumnException.php b/vendor/doctrine/orm/src/Tools/Exception/MissingColumnException.php new file mode 100644 index 0000000..764721e --- /dev/null +++ b/vendor/doctrine/orm/src/Tools/Exception/MissingColumnException.php | |||
| @@ -0,0 +1,23 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Tools\Exception; | ||
| 6 | |||
| 7 | use Doctrine\ORM\Exception\ORMException; | ||
| 8 | use LogicException; | ||
| 9 | |||
| 10 | use function sprintf; | ||
| 11 | |||
| 12 | final class MissingColumnException extends LogicException implements ORMException | ||
| 13 | { | ||
| 14 | public static function fromColumnSourceAndTarget(string $column, string $source, string $target): self | ||
| 15 | { | ||
| 16 | return new self(sprintf( | ||
| 17 | 'Column name "%s" referenced for relation from %s towards %s does not exist.', | ||
| 18 | $column, | ||
| 19 | $source, | ||
| 20 | $target, | ||
| 21 | )); | ||
| 22 | } | ||
| 23 | } | ||
diff --git a/vendor/doctrine/orm/src/Tools/Exception/NotSupported.php b/vendor/doctrine/orm/src/Tools/Exception/NotSupported.php new file mode 100644 index 0000000..af619fd --- /dev/null +++ b/vendor/doctrine/orm/src/Tools/Exception/NotSupported.php | |||
| @@ -0,0 +1,16 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Tools\Exception; | ||
| 6 | |||
| 7 | use Doctrine\ORM\Exception\SchemaToolException; | ||
| 8 | use LogicException; | ||
| 9 | |||
| 10 | final class NotSupported extends LogicException implements SchemaToolException | ||
| 11 | { | ||
| 12 | public static function create(): self | ||
| 13 | { | ||
| 14 | return new self('This behaviour is (currently) not supported by Doctrine 2'); | ||
| 15 | } | ||
| 16 | } | ||
diff --git a/vendor/doctrine/orm/src/Tools/Pagination/CountOutputWalker.php b/vendor/doctrine/orm/src/Tools/Pagination/CountOutputWalker.php new file mode 100644 index 0000000..c7f31db --- /dev/null +++ b/vendor/doctrine/orm/src/Tools/Pagination/CountOutputWalker.php | |||
| @@ -0,0 +1,125 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Tools\Pagination; | ||
| 6 | |||
| 7 | use Doctrine\DBAL\Platforms\AbstractPlatform; | ||
| 8 | use Doctrine\DBAL\Platforms\SQLServerPlatform; | ||
| 9 | use Doctrine\ORM\Query; | ||
| 10 | use Doctrine\ORM\Query\AST\SelectStatement; | ||
| 11 | use Doctrine\ORM\Query\Parser; | ||
| 12 | use Doctrine\ORM\Query\ParserResult; | ||
| 13 | use Doctrine\ORM\Query\ResultSetMapping; | ||
| 14 | use Doctrine\ORM\Query\SqlWalker; | ||
| 15 | use RuntimeException; | ||
| 16 | |||
| 17 | use function array_diff; | ||
| 18 | use function array_keys; | ||
| 19 | use function assert; | ||
| 20 | use function count; | ||
| 21 | use function implode; | ||
| 22 | use function reset; | ||
| 23 | use function sprintf; | ||
| 24 | |||
| 25 | /** | ||
| 26 | * Wraps the query in order to accurately count the root objects. | ||
| 27 | * | ||
| 28 | * Given a DQL like `SELECT u FROM User u` it will generate an SQL query like: | ||
| 29 | * SELECT COUNT(*) (SELECT DISTINCT <id> FROM (<original SQL>)) | ||
| 30 | * | ||
| 31 | * Works with composite keys but cannot deal with queries that have multiple | ||
| 32 | * root entities (e.g. `SELECT f, b from Foo, Bar`) | ||
| 33 | * | ||
| 34 | * Note that the ORDER BY clause is not removed. Many SQL implementations (e.g. MySQL) | ||
| 35 | * are able to cache subqueries. By keeping the ORDER BY clause intact, the limitSubQuery | ||
| 36 | * that will most likely be executed next can be read from the native SQL cache. | ||
| 37 | * | ||
| 38 | * @psalm-import-type QueryComponent from Parser | ||
| 39 | */ | ||
| 40 | class CountOutputWalker extends SqlWalker | ||
| 41 | { | ||
| 42 | private readonly AbstractPlatform $platform; | ||
| 43 | private readonly ResultSetMapping $rsm; | ||
| 44 | |||
| 45 | /** | ||
| 46 | * {@inheritDoc} | ||
| 47 | */ | ||
| 48 | public function __construct(Query $query, ParserResult $parserResult, array $queryComponents) | ||
| 49 | { | ||
| 50 | $this->platform = $query->getEntityManager()->getConnection()->getDatabasePlatform(); | ||
| 51 | $this->rsm = $parserResult->getResultSetMapping(); | ||
| 52 | |||
| 53 | parent::__construct($query, $parserResult, $queryComponents); | ||
| 54 | } | ||
| 55 | |||
| 56 | public function walkSelectStatement(SelectStatement $selectStatement): string | ||
| 57 | { | ||
| 58 | if ($this->platform instanceof SQLServerPlatform) { | ||
| 59 | $selectStatement->orderByClause = null; | ||
| 60 | } | ||
| 61 | |||
| 62 | $sql = parent::walkSelectStatement($selectStatement); | ||
| 63 | |||
| 64 | if ($selectStatement->groupByClause) { | ||
| 65 | return sprintf( | ||
| 66 | 'SELECT COUNT(*) AS dctrn_count FROM (%s) dctrn_table', | ||
| 67 | $sql, | ||
| 68 | ); | ||
| 69 | } | ||
| 70 | |||
| 71 | // Find out the SQL alias of the identifier column of the root entity | ||
| 72 | // It may be possible to make this work with multiple root entities but that | ||
| 73 | // would probably require issuing multiple queries or doing a UNION SELECT | ||
| 74 | // so for now, It's not supported. | ||
| 75 | |||
| 76 | // Get the root entity and alias from the AST fromClause | ||
| 77 | $from = $selectStatement->fromClause->identificationVariableDeclarations; | ||
| 78 | if (count($from) > 1) { | ||
| 79 | throw new RuntimeException('Cannot count query which selects two FROM components, cannot make distinction'); | ||
| 80 | } | ||
| 81 | |||
| 82 | $fromRoot = reset($from); | ||
| 83 | $rootAlias = $fromRoot->rangeVariableDeclaration->aliasIdentificationVariable; | ||
| 84 | $rootClass = $this->getMetadataForDqlAlias($rootAlias); | ||
| 85 | $rootIdentifier = $rootClass->identifier; | ||
| 86 | |||
| 87 | // For every identifier, find out the SQL alias by combing through the ResultSetMapping | ||
| 88 | $sqlIdentifier = []; | ||
| 89 | foreach ($rootIdentifier as $property) { | ||
| 90 | if (isset($rootClass->fieldMappings[$property])) { | ||
| 91 | foreach (array_keys($this->rsm->fieldMappings, $property, true) as $alias) { | ||
| 92 | if ($this->rsm->columnOwnerMap[$alias] === $rootAlias) { | ||
| 93 | $sqlIdentifier[$property] = $alias; | ||
| 94 | } | ||
| 95 | } | ||
| 96 | } | ||
| 97 | |||
| 98 | if (isset($rootClass->associationMappings[$property])) { | ||
| 99 | $association = $rootClass->associationMappings[$property]; | ||
| 100 | assert($association->isToOneOwningSide()); | ||
| 101 | $joinColumn = $association->joinColumns[0]->name; | ||
| 102 | |||
| 103 | foreach (array_keys($this->rsm->metaMappings, $joinColumn, true) as $alias) { | ||
| 104 | if ($this->rsm->columnOwnerMap[$alias] === $rootAlias) { | ||
| 105 | $sqlIdentifier[$property] = $alias; | ||
| 106 | } | ||
| 107 | } | ||
| 108 | } | ||
| 109 | } | ||
| 110 | |||
| 111 | if (count($rootIdentifier) !== count($sqlIdentifier)) { | ||
| 112 | throw new RuntimeException(sprintf( | ||
| 113 | 'Not all identifier properties can be found in the ResultSetMapping: %s', | ||
| 114 | implode(', ', array_diff($rootIdentifier, array_keys($sqlIdentifier))), | ||
| 115 | )); | ||
| 116 | } | ||
| 117 | |||
| 118 | // Build the counter query | ||
| 119 | return sprintf( | ||
| 120 | 'SELECT COUNT(*) AS dctrn_count FROM (SELECT DISTINCT %s FROM (%s) dctrn_result) dctrn_table', | ||
| 121 | implode(', ', $sqlIdentifier), | ||
| 122 | $sql, | ||
| 123 | ); | ||
| 124 | } | ||
| 125 | } | ||
diff --git a/vendor/doctrine/orm/src/Tools/Pagination/CountWalker.php b/vendor/doctrine/orm/src/Tools/Pagination/CountWalker.php new file mode 100644 index 0000000..d212943 --- /dev/null +++ b/vendor/doctrine/orm/src/Tools/Pagination/CountWalker.php | |||
| @@ -0,0 +1,68 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Tools\Pagination; | ||
| 6 | |||
| 7 | use Doctrine\ORM\Query\AST\AggregateExpression; | ||
| 8 | use Doctrine\ORM\Query\AST\PathExpression; | ||
| 9 | use Doctrine\ORM\Query\AST\SelectExpression; | ||
| 10 | use Doctrine\ORM\Query\AST\SelectStatement; | ||
| 11 | use Doctrine\ORM\Query\TreeWalkerAdapter; | ||
| 12 | use RuntimeException; | ||
| 13 | |||
| 14 | use function count; | ||
| 15 | use function reset; | ||
| 16 | |||
| 17 | /** | ||
| 18 | * Replaces the selectClause of the AST with a COUNT statement. | ||
| 19 | */ | ||
| 20 | class CountWalker extends TreeWalkerAdapter | ||
| 21 | { | ||
| 22 | /** | ||
| 23 | * Distinct mode hint name. | ||
| 24 | */ | ||
| 25 | public const HINT_DISTINCT = 'doctrine_paginator.distinct'; | ||
| 26 | |||
| 27 | public function walkSelectStatement(SelectStatement $selectStatement): void | ||
| 28 | { | ||
| 29 | if ($selectStatement->havingClause) { | ||
| 30 | throw new RuntimeException('Cannot count query that uses a HAVING clause. Use the output walkers for pagination'); | ||
| 31 | } | ||
| 32 | |||
| 33 | // Get the root entity and alias from the AST fromClause | ||
| 34 | $from = $selectStatement->fromClause->identificationVariableDeclarations; | ||
| 35 | |||
| 36 | if (count($from) > 1) { | ||
| 37 | throw new RuntimeException('Cannot count query which selects two FROM components, cannot make distinction'); | ||
| 38 | } | ||
| 39 | |||
| 40 | $fromRoot = reset($from); | ||
| 41 | $rootAlias = $fromRoot->rangeVariableDeclaration->aliasIdentificationVariable; | ||
| 42 | $rootClass = $this->getMetadataForDqlAlias($rootAlias); | ||
| 43 | $identifierFieldName = $rootClass->getSingleIdentifierFieldName(); | ||
| 44 | |||
| 45 | $pathType = PathExpression::TYPE_STATE_FIELD; | ||
| 46 | if (isset($rootClass->associationMappings[$identifierFieldName])) { | ||
| 47 | $pathType = PathExpression::TYPE_SINGLE_VALUED_ASSOCIATION; | ||
| 48 | } | ||
| 49 | |||
| 50 | $pathExpression = new PathExpression( | ||
| 51 | PathExpression::TYPE_STATE_FIELD | PathExpression::TYPE_SINGLE_VALUED_ASSOCIATION, | ||
| 52 | $rootAlias, | ||
| 53 | $identifierFieldName, | ||
| 54 | ); | ||
| 55 | $pathExpression->type = $pathType; | ||
| 56 | |||
| 57 | $distinct = $this->_getQuery()->getHint(self::HINT_DISTINCT); | ||
| 58 | $selectStatement->selectClause->selectExpressions = [ | ||
| 59 | new SelectExpression( | ||
| 60 | new AggregateExpression('count', $pathExpression, $distinct), | ||
| 61 | null, | ||
| 62 | ), | ||
| 63 | ]; | ||
| 64 | |||
| 65 | // ORDER BY is not needed, only increases query execution through unnecessary sorting. | ||
| 66 | $selectStatement->orderByClause = null; | ||
| 67 | } | ||
| 68 | } | ||
diff --git a/vendor/doctrine/orm/src/Tools/Pagination/Exception/RowNumberOverFunctionNotEnabled.php b/vendor/doctrine/orm/src/Tools/Pagination/Exception/RowNumberOverFunctionNotEnabled.php new file mode 100644 index 0000000..0e3da93 --- /dev/null +++ b/vendor/doctrine/orm/src/Tools/Pagination/Exception/RowNumberOverFunctionNotEnabled.php | |||
| @@ -0,0 +1,16 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Tools\Pagination\Exception; | ||
| 6 | |||
| 7 | use Doctrine\ORM\Exception\ORMException; | ||
| 8 | use LogicException; | ||
| 9 | |||
| 10 | final class RowNumberOverFunctionNotEnabled extends LogicException implements ORMException | ||
| 11 | { | ||
| 12 | public static function create(): self | ||
| 13 | { | ||
| 14 | return new self('The RowNumberOverFunction is not intended for, nor is it enabled for use in DQL.'); | ||
| 15 | } | ||
| 16 | } | ||
diff --git a/vendor/doctrine/orm/src/Tools/Pagination/LimitSubqueryOutputWalker.php b/vendor/doctrine/orm/src/Tools/Pagination/LimitSubqueryOutputWalker.php new file mode 100644 index 0000000..8bbc44c --- /dev/null +++ b/vendor/doctrine/orm/src/Tools/Pagination/LimitSubqueryOutputWalker.php | |||
| @@ -0,0 +1,544 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Tools\Pagination; | ||
| 6 | |||
| 7 | use Doctrine\DBAL\Platforms\AbstractPlatform; | ||
| 8 | use Doctrine\DBAL\Platforms\DB2Platform; | ||
| 9 | use Doctrine\DBAL\Platforms\OraclePlatform; | ||
| 10 | use Doctrine\DBAL\Platforms\PostgreSQLPlatform; | ||
| 11 | use Doctrine\DBAL\Platforms\SQLServerPlatform; | ||
| 12 | use Doctrine\ORM\EntityManagerInterface; | ||
| 13 | use Doctrine\ORM\Mapping\QuoteStrategy; | ||
| 14 | use Doctrine\ORM\OptimisticLockException; | ||
| 15 | use Doctrine\ORM\Query; | ||
| 16 | use Doctrine\ORM\Query\AST\OrderByClause; | ||
| 17 | use Doctrine\ORM\Query\AST\PathExpression; | ||
| 18 | use Doctrine\ORM\Query\AST\SelectExpression; | ||
| 19 | use Doctrine\ORM\Query\AST\SelectStatement; | ||
| 20 | use Doctrine\ORM\Query\AST\Subselect; | ||
| 21 | use Doctrine\ORM\Query\Parser; | ||
| 22 | use Doctrine\ORM\Query\ParserResult; | ||
| 23 | use Doctrine\ORM\Query\QueryException; | ||
| 24 | use Doctrine\ORM\Query\ResultSetMapping; | ||
| 25 | use Doctrine\ORM\Query\SqlWalker; | ||
| 26 | use RuntimeException; | ||
| 27 | |||
| 28 | use function array_diff; | ||
| 29 | use function array_keys; | ||
| 30 | use function assert; | ||
| 31 | use function count; | ||
| 32 | use function implode; | ||
| 33 | use function in_array; | ||
| 34 | use function is_string; | ||
| 35 | use function method_exists; | ||
| 36 | use function preg_replace; | ||
| 37 | use function reset; | ||
| 38 | use function sprintf; | ||
| 39 | use function strrpos; | ||
| 40 | use function substr; | ||
| 41 | |||
| 42 | /** | ||
| 43 | * Wraps the query in order to select root entity IDs for pagination. | ||
| 44 | * | ||
| 45 | * Given a DQL like `SELECT u FROM User u` it will generate an SQL query like: | ||
| 46 | * SELECT DISTINCT <id> FROM (<original SQL>) LIMIT x OFFSET y | ||
| 47 | * | ||
| 48 | * Works with composite keys but cannot deal with queries that have multiple | ||
| 49 | * root entities (e.g. `SELECT f, b from Foo, Bar`) | ||
| 50 | * | ||
| 51 | * @psalm-import-type QueryComponent from Parser | ||
| 52 | */ | ||
| 53 | class LimitSubqueryOutputWalker extends SqlWalker | ||
| 54 | { | ||
| 55 | private const ORDER_BY_PATH_EXPRESSION = '/(?<![a-z0-9_])%s\.%s(?![a-z0-9_])/i'; | ||
| 56 | |||
| 57 | private readonly AbstractPlatform $platform; | ||
| 58 | private readonly ResultSetMapping $rsm; | ||
| 59 | private readonly int $firstResult; | ||
| 60 | private readonly int|null $maxResults; | ||
| 61 | private readonly EntityManagerInterface $em; | ||
| 62 | private readonly QuoteStrategy $quoteStrategy; | ||
| 63 | |||
| 64 | /** @var list<PathExpression> */ | ||
| 65 | private array $orderByPathExpressions = []; | ||
| 66 | |||
| 67 | /** | ||
| 68 | * We don't want to add path expressions from sub-selects into the select clause of the containing query. | ||
| 69 | * This state flag simply keeps track on whether we are walking on a subquery or not | ||
| 70 | */ | ||
| 71 | private bool $inSubSelect = false; | ||
| 72 | |||
| 73 | /** | ||
| 74 | * Stores various parameters that are otherwise unavailable | ||
| 75 | * because Doctrine\ORM\Query\SqlWalker keeps everything private without | ||
| 76 | * accessors. | ||
| 77 | * | ||
| 78 | * {@inheritDoc} | ||
| 79 | */ | ||
| 80 | public function __construct( | ||
| 81 | Query $query, | ||
| 82 | ParserResult $parserResult, | ||
| 83 | array $queryComponents, | ||
| 84 | ) { | ||
| 85 | $this->platform = $query->getEntityManager()->getConnection()->getDatabasePlatform(); | ||
| 86 | $this->rsm = $parserResult->getResultSetMapping(); | ||
| 87 | |||
| 88 | // Reset limit and offset | ||
| 89 | $this->firstResult = $query->getFirstResult(); | ||
| 90 | $this->maxResults = $query->getMaxResults(); | ||
| 91 | $query->setFirstResult(0)->setMaxResults(null); | ||
| 92 | |||
| 93 | $this->em = $query->getEntityManager(); | ||
| 94 | $this->quoteStrategy = $this->em->getConfiguration()->getQuoteStrategy(); | ||
| 95 | |||
| 96 | parent::__construct($query, $parserResult, $queryComponents); | ||
| 97 | } | ||
| 98 | |||
| 99 | /** | ||
| 100 | * Check if the platform supports the ROW_NUMBER window function. | ||
| 101 | */ | ||
| 102 | private function platformSupportsRowNumber(): bool | ||
| 103 | { | ||
| 104 | return $this->platform instanceof PostgreSQLPlatform | ||
| 105 | || $this->platform instanceof SQLServerPlatform | ||
| 106 | || $this->platform instanceof OraclePlatform | ||
| 107 | || $this->platform instanceof DB2Platform | ||
| 108 | || (method_exists($this->platform, 'supportsRowNumberFunction') | ||
| 109 | && $this->platform->supportsRowNumberFunction()); | ||
| 110 | } | ||
| 111 | |||
| 112 | /** | ||
| 113 | * Rebuilds a select statement's order by clause for use in a | ||
| 114 | * ROW_NUMBER() OVER() expression. | ||
| 115 | */ | ||
| 116 | private function rebuildOrderByForRowNumber(SelectStatement $AST): void | ||
| 117 | { | ||
| 118 | $orderByClause = $AST->orderByClause; | ||
| 119 | $selectAliasToExpressionMap = []; | ||
| 120 | // Get any aliases that are available for select expressions. | ||
| 121 | foreach ($AST->selectClause->selectExpressions as $selectExpression) { | ||
| 122 | $selectAliasToExpressionMap[$selectExpression->fieldIdentificationVariable] = $selectExpression->expression; | ||
| 123 | } | ||
| 124 | |||
| 125 | // Rebuild string orderby expressions to use the select expression they're referencing | ||
| 126 | foreach ($orderByClause->orderByItems as $orderByItem) { | ||
| 127 | if (is_string($orderByItem->expression) && isset($selectAliasToExpressionMap[$orderByItem->expression])) { | ||
| 128 | $orderByItem->expression = $selectAliasToExpressionMap[$orderByItem->expression]; | ||
| 129 | } | ||
| 130 | } | ||
| 131 | |||
| 132 | $func = new RowNumberOverFunction('dctrn_rownum'); | ||
| 133 | $func->orderByClause = $AST->orderByClause; | ||
| 134 | $AST->selectClause->selectExpressions[] = new SelectExpression($func, 'dctrn_rownum', true); | ||
| 135 | |||
| 136 | // No need for an order by clause, we'll order by rownum in the outer query. | ||
| 137 | $AST->orderByClause = null; | ||
| 138 | } | ||
| 139 | |||
| 140 | public function walkSelectStatement(SelectStatement $selectStatement): string | ||
| 141 | { | ||
| 142 | if ($this->platformSupportsRowNumber()) { | ||
| 143 | return $this->walkSelectStatementWithRowNumber($selectStatement); | ||
| 144 | } | ||
| 145 | |||
| 146 | return $this->walkSelectStatementWithoutRowNumber($selectStatement); | ||
| 147 | } | ||
| 148 | |||
| 149 | /** | ||
| 150 | * Walks down a SelectStatement AST node, wrapping it in a SELECT DISTINCT. | ||
| 151 | * This method is for use with platforms which support ROW_NUMBER. | ||
| 152 | * | ||
| 153 | * @throws RuntimeException | ||
| 154 | */ | ||
| 155 | public function walkSelectStatementWithRowNumber(SelectStatement $AST): string | ||
| 156 | { | ||
| 157 | $hasOrderBy = false; | ||
| 158 | $outerOrderBy = ' ORDER BY dctrn_minrownum ASC'; | ||
| 159 | $orderGroupBy = ''; | ||
| 160 | if ($AST->orderByClause instanceof OrderByClause) { | ||
| 161 | $hasOrderBy = true; | ||
| 162 | $this->rebuildOrderByForRowNumber($AST); | ||
| 163 | } | ||
| 164 | |||
| 165 | $innerSql = $this->getInnerSQL($AST); | ||
| 166 | |||
| 167 | $sqlIdentifier = $this->getSQLIdentifier($AST); | ||
| 168 | |||
| 169 | if ($hasOrderBy) { | ||
| 170 | $orderGroupBy = ' GROUP BY ' . implode(', ', $sqlIdentifier); | ||
| 171 | $sqlIdentifier[] = 'MIN(' . $this->walkResultVariable('dctrn_rownum') . ') AS dctrn_minrownum'; | ||
| 172 | } | ||
| 173 | |||
| 174 | // Build the counter query | ||
| 175 | $sql = sprintf( | ||
| 176 | 'SELECT DISTINCT %s FROM (%s) dctrn_result', | ||
| 177 | implode(', ', $sqlIdentifier), | ||
| 178 | $innerSql, | ||
| 179 | ); | ||
| 180 | |||
| 181 | if ($hasOrderBy) { | ||
| 182 | $sql .= $orderGroupBy . $outerOrderBy; | ||
| 183 | } | ||
| 184 | |||
| 185 | // Apply the limit and offset. | ||
| 186 | $sql = $this->platform->modifyLimitQuery( | ||
| 187 | $sql, | ||
| 188 | $this->maxResults, | ||
| 189 | $this->firstResult, | ||
| 190 | ); | ||
| 191 | |||
| 192 | // Add the columns to the ResultSetMapping. It's not really nice but | ||
| 193 | // it works. Preferably I'd clear the RSM or simply create a new one | ||
| 194 | // but that is not possible from inside the output walker, so we dirty | ||
| 195 | // up the one we have. | ||
| 196 | foreach ($sqlIdentifier as $property => $alias) { | ||
| 197 | $this->rsm->addScalarResult($alias, $property); | ||
| 198 | } | ||
| 199 | |||
| 200 | return $sql; | ||
| 201 | } | ||
| 202 | |||
| 203 | /** | ||
| 204 | * Walks down a SelectStatement AST node, wrapping it in a SELECT DISTINCT. | ||
| 205 | * This method is for platforms which DO NOT support ROW_NUMBER. | ||
| 206 | * | ||
| 207 | * @throws RuntimeException | ||
| 208 | */ | ||
| 209 | public function walkSelectStatementWithoutRowNumber(SelectStatement $AST, bool $addMissingItemsFromOrderByToSelect = true): string | ||
| 210 | { | ||
| 211 | // We don't want to call this recursively! | ||
| 212 | if ($AST->orderByClause instanceof OrderByClause && $addMissingItemsFromOrderByToSelect) { | ||
| 213 | // In the case of ordering a query by columns from joined tables, we | ||
| 214 | // must add those columns to the select clause of the query BEFORE | ||
| 215 | // the SQL is generated. | ||
| 216 | $this->addMissingItemsFromOrderByToSelect($AST); | ||
| 217 | } | ||
| 218 | |||
| 219 | // Remove order by clause from the inner query | ||
| 220 | // It will be re-appended in the outer select generated by this method | ||
| 221 | $orderByClause = $AST->orderByClause; | ||
| 222 | $AST->orderByClause = null; | ||
| 223 | |||
| 224 | $innerSql = $this->getInnerSQL($AST); | ||
| 225 | |||
| 226 | $sqlIdentifier = $this->getSQLIdentifier($AST); | ||
| 227 | |||
| 228 | // Build the counter query | ||
| 229 | $sql = sprintf( | ||
| 230 | 'SELECT DISTINCT %s FROM (%s) dctrn_result', | ||
| 231 | implode(', ', $sqlIdentifier), | ||
| 232 | $innerSql, | ||
| 233 | ); | ||
| 234 | |||
| 235 | // https://github.com/doctrine/orm/issues/2630 | ||
| 236 | $sql = $this->preserveSqlOrdering($sqlIdentifier, $innerSql, $sql, $orderByClause); | ||
| 237 | |||
| 238 | // Apply the limit and offset. | ||
| 239 | $sql = $this->platform->modifyLimitQuery( | ||
| 240 | $sql, | ||
| 241 | $this->maxResults, | ||
| 242 | $this->firstResult, | ||
| 243 | ); | ||
| 244 | |||
| 245 | // Add the columns to the ResultSetMapping. It's not really nice but | ||
| 246 | // it works. Preferably I'd clear the RSM or simply create a new one | ||
| 247 | // but that is not possible from inside the output walker, so we dirty | ||
| 248 | // up the one we have. | ||
| 249 | foreach ($sqlIdentifier as $property => $alias) { | ||
| 250 | $this->rsm->addScalarResult($alias, $property); | ||
| 251 | } | ||
| 252 | |||
| 253 | // Restore orderByClause | ||
| 254 | $AST->orderByClause = $orderByClause; | ||
| 255 | |||
| 256 | return $sql; | ||
| 257 | } | ||
| 258 | |||
| 259 | /** | ||
| 260 | * Finds all PathExpressions in an AST's OrderByClause, and ensures that | ||
| 261 | * the referenced fields are present in the SelectClause of the passed AST. | ||
| 262 | */ | ||
| 263 | private function addMissingItemsFromOrderByToSelect(SelectStatement $AST): void | ||
| 264 | { | ||
| 265 | $this->orderByPathExpressions = []; | ||
| 266 | |||
| 267 | // We need to do this in another walker because otherwise we'll end up | ||
| 268 | // polluting the state of this one. | ||
| 269 | $walker = clone $this; | ||
| 270 | |||
| 271 | // This will populate $orderByPathExpressions via | ||
| 272 | // LimitSubqueryOutputWalker::walkPathExpression, which will be called | ||
| 273 | // as the select statement is walked. We'll end up with an array of all | ||
| 274 | // path expressions referenced in the query. | ||
| 275 | $walker->walkSelectStatementWithoutRowNumber($AST, false); | ||
| 276 | $orderByPathExpressions = $walker->getOrderByPathExpressions(); | ||
| 277 | |||
| 278 | // Get a map of referenced identifiers to field names. | ||
| 279 | $selects = []; | ||
| 280 | foreach ($orderByPathExpressions as $pathExpression) { | ||
| 281 | assert($pathExpression->field !== null); | ||
| 282 | $idVar = $pathExpression->identificationVariable; | ||
| 283 | $field = $pathExpression->field; | ||
| 284 | if (! isset($selects[$idVar])) { | ||
| 285 | $selects[$idVar] = []; | ||
| 286 | } | ||
| 287 | |||
| 288 | $selects[$idVar][$field] = true; | ||
| 289 | } | ||
| 290 | |||
| 291 | // Loop the select clause of the AST and exclude items from $select | ||
| 292 | // that are already being selected in the query. | ||
| 293 | foreach ($AST->selectClause->selectExpressions as $selectExpression) { | ||
| 294 | if ($selectExpression instanceof SelectExpression) { | ||
| 295 | $idVar = $selectExpression->expression; | ||
| 296 | if (! is_string($idVar)) { | ||
| 297 | continue; | ||
| 298 | } | ||
| 299 | |||
| 300 | $field = $selectExpression->fieldIdentificationVariable; | ||
| 301 | if ($field === null) { | ||
| 302 | // No need to add this select, as we're already fetching the whole object. | ||
| 303 | unset($selects[$idVar]); | ||
| 304 | } else { | ||
| 305 | unset($selects[$idVar][$field]); | ||
| 306 | } | ||
| 307 | } | ||
| 308 | } | ||
| 309 | |||
| 310 | // Add select items which were not excluded to the AST's select clause. | ||
| 311 | foreach ($selects as $idVar => $fields) { | ||
| 312 | $AST->selectClause->selectExpressions[] = new SelectExpression($idVar, null, true); | ||
| 313 | } | ||
| 314 | } | ||
| 315 | |||
| 316 | /** | ||
| 317 | * Generates new SQL for statements with an order by clause | ||
| 318 | * | ||
| 319 | * @param mixed[] $sqlIdentifier | ||
| 320 | */ | ||
| 321 | private function preserveSqlOrdering( | ||
| 322 | array $sqlIdentifier, | ||
| 323 | string $innerSql, | ||
| 324 | string $sql, | ||
| 325 | OrderByClause|null $orderByClause, | ||
| 326 | ): string { | ||
| 327 | // If the sql statement has an order by clause, we need to wrap it in a new select distinct statement | ||
| 328 | if (! $orderByClause) { | ||
| 329 | return $sql; | ||
| 330 | } | ||
| 331 | |||
| 332 | // now only select distinct identifier | ||
| 333 | return sprintf( | ||
| 334 | 'SELECT DISTINCT %s FROM (%s) dctrn_result', | ||
| 335 | implode(', ', $sqlIdentifier), | ||
| 336 | $this->recreateInnerSql($orderByClause, $sqlIdentifier, $innerSql), | ||
| 337 | ); | ||
| 338 | } | ||
| 339 | |||
| 340 | /** | ||
| 341 | * Generates a new SQL statement for the inner query to keep the correct sorting | ||
| 342 | * | ||
| 343 | * @param mixed[] $identifiers | ||
| 344 | */ | ||
| 345 | private function recreateInnerSql( | ||
| 346 | OrderByClause $orderByClause, | ||
| 347 | array $identifiers, | ||
| 348 | string $innerSql, | ||
| 349 | ): string { | ||
| 350 | [$searchPatterns, $replacements] = $this->generateSqlAliasReplacements(); | ||
| 351 | $orderByItems = []; | ||
| 352 | |||
| 353 | foreach ($orderByClause->orderByItems as $orderByItem) { | ||
| 354 | // Walk order by item to get string representation of it and | ||
| 355 | // replace path expressions in the order by clause with their column alias | ||
| 356 | $orderByItemString = preg_replace( | ||
| 357 | $searchPatterns, | ||
| 358 | $replacements, | ||
| 359 | $this->walkOrderByItem($orderByItem), | ||
| 360 | ); | ||
| 361 | |||
| 362 | $orderByItems[] = $orderByItemString; | ||
| 363 | $identifier = substr($orderByItemString, 0, strrpos($orderByItemString, ' ')); | ||
| 364 | |||
| 365 | if (! in_array($identifier, $identifiers, true)) { | ||
| 366 | $identifiers[] = $identifier; | ||
| 367 | } | ||
| 368 | } | ||
| 369 | |||
| 370 | return $sql = sprintf( | ||
| 371 | 'SELECT DISTINCT %s FROM (%s) dctrn_result_inner ORDER BY %s', | ||
| 372 | implode(', ', $identifiers), | ||
| 373 | $innerSql, | ||
| 374 | implode(', ', $orderByItems), | ||
| 375 | ); | ||
| 376 | } | ||
| 377 | |||
| 378 | /** | ||
| 379 | * @return string[][] | ||
| 380 | * @psalm-return array{0: list<non-empty-string>, 1: list<string>} | ||
| 381 | */ | ||
| 382 | private function generateSqlAliasReplacements(): array | ||
| 383 | { | ||
| 384 | $aliasMap = $searchPatterns = $replacements = $metadataList = []; | ||
| 385 | |||
| 386 | // Generate DQL alias -> SQL table alias mapping | ||
| 387 | foreach (array_keys($this->rsm->aliasMap) as $dqlAlias) { | ||
| 388 | $metadataList[$dqlAlias] = $class = $this->getMetadataForDqlAlias($dqlAlias); | ||
| 389 | $aliasMap[$dqlAlias] = $this->getSQLTableAlias($class->getTableName(), $dqlAlias); | ||
| 390 | } | ||
| 391 | |||
| 392 | // Generate search patterns for each field's path expression in the order by clause | ||
| 393 | foreach ($this->rsm->fieldMappings as $fieldAlias => $fieldName) { | ||
| 394 | $dqlAliasForFieldAlias = $this->rsm->columnOwnerMap[$fieldAlias]; | ||
| 395 | $class = $metadataList[$dqlAliasForFieldAlias]; | ||
| 396 | |||
| 397 | // If the field is from a joined child table, we won't be ordering on it. | ||
| 398 | if (! isset($class->fieldMappings[$fieldName])) { | ||
| 399 | continue; | ||
| 400 | } | ||
| 401 | |||
| 402 | $fieldMapping = $class->fieldMappings[$fieldName]; | ||
| 403 | |||
| 404 | // Get the proper column name as will appear in the select list | ||
| 405 | $columnName = $this->quoteStrategy->getColumnName( | ||
| 406 | $fieldName, | ||
| 407 | $metadataList[$dqlAliasForFieldAlias], | ||
| 408 | $this->em->getConnection()->getDatabasePlatform(), | ||
| 409 | ); | ||
| 410 | |||
| 411 | // Get the SQL table alias for the entity and field | ||
| 412 | $sqlTableAliasForFieldAlias = $aliasMap[$dqlAliasForFieldAlias]; | ||
| 413 | |||
| 414 | if (isset($fieldMapping->declared) && $fieldMapping->declared !== $class->name) { | ||
| 415 | // Field was declared in a parent class, so we need to get the proper SQL table alias | ||
| 416 | // for the joined parent table. | ||
| 417 | $otherClassMetadata = $this->em->getClassMetadata($fieldMapping->declared); | ||
| 418 | |||
| 419 | if (! $otherClassMetadata->isMappedSuperclass) { | ||
| 420 | $sqlTableAliasForFieldAlias = $this->getSQLTableAlias($otherClassMetadata->getTableName(), $dqlAliasForFieldAlias); | ||
| 421 | } | ||
| 422 | } | ||
| 423 | |||
| 424 | // Compose search and replace patterns | ||
| 425 | $searchPatterns[] = sprintf(self::ORDER_BY_PATH_EXPRESSION, $sqlTableAliasForFieldAlias, $columnName); | ||
| 426 | $replacements[] = $fieldAlias; | ||
| 427 | } | ||
| 428 | |||
| 429 | return [$searchPatterns, $replacements]; | ||
| 430 | } | ||
| 431 | |||
| 432 | /** | ||
| 433 | * getter for $orderByPathExpressions | ||
| 434 | * | ||
| 435 | * @return list<PathExpression> | ||
| 436 | */ | ||
| 437 | public function getOrderByPathExpressions(): array | ||
| 438 | { | ||
| 439 | return $this->orderByPathExpressions; | ||
| 440 | } | ||
| 441 | |||
| 442 | /** | ||
| 443 | * @throws OptimisticLockException | ||
| 444 | * @throws QueryException | ||
| 445 | */ | ||
| 446 | private function getInnerSQL(SelectStatement $AST): string | ||
| 447 | { | ||
| 448 | // Set every select expression as visible(hidden = false) to | ||
| 449 | // make $AST have scalar mappings properly - this is relevant for referencing selected | ||
| 450 | // fields from outside the subquery, for example in the ORDER BY segment | ||
| 451 | $hiddens = []; | ||
| 452 | |||
| 453 | foreach ($AST->selectClause->selectExpressions as $idx => $expr) { | ||
| 454 | $hiddens[$idx] = $expr->hiddenAliasResultVariable; | ||
| 455 | $expr->hiddenAliasResultVariable = false; | ||
| 456 | } | ||
| 457 | |||
| 458 | $innerSql = parent::walkSelectStatement($AST); | ||
| 459 | |||
| 460 | // Restore hiddens | ||
| 461 | foreach ($AST->selectClause->selectExpressions as $idx => $expr) { | ||
| 462 | $expr->hiddenAliasResultVariable = $hiddens[$idx]; | ||
| 463 | } | ||
| 464 | |||
| 465 | return $innerSql; | ||
| 466 | } | ||
| 467 | |||
| 468 | /** @return string[] */ | ||
| 469 | private function getSQLIdentifier(SelectStatement $AST): array | ||
| 470 | { | ||
| 471 | // Find out the SQL alias of the identifier column of the root entity. | ||
| 472 | // It may be possible to make this work with multiple root entities but that | ||
| 473 | // would probably require issuing multiple queries or doing a UNION SELECT. | ||
| 474 | // So for now, it's not supported. | ||
| 475 | |||
| 476 | // Get the root entity and alias from the AST fromClause. | ||
| 477 | $from = $AST->fromClause->identificationVariableDeclarations; | ||
| 478 | if (count($from) !== 1) { | ||
| 479 | throw new RuntimeException('Cannot count query which selects two FROM components, cannot make distinction'); | ||
| 480 | } | ||
| 481 | |||
| 482 | $fromRoot = reset($from); | ||
| 483 | $rootAlias = $fromRoot->rangeVariableDeclaration->aliasIdentificationVariable; | ||
| 484 | $rootClass = $this->getMetadataForDqlAlias($rootAlias); | ||
| 485 | $rootIdentifier = $rootClass->identifier; | ||
| 486 | |||
| 487 | // For every identifier, find out the SQL alias by combing through the ResultSetMapping | ||
| 488 | $sqlIdentifier = []; | ||
| 489 | foreach ($rootIdentifier as $property) { | ||
| 490 | if (isset($rootClass->fieldMappings[$property])) { | ||
| 491 | foreach (array_keys($this->rsm->fieldMappings, $property, true) as $alias) { | ||
| 492 | if ($this->rsm->columnOwnerMap[$alias] === $rootAlias) { | ||
| 493 | $sqlIdentifier[$property] = $alias; | ||
| 494 | } | ||
| 495 | } | ||
| 496 | } | ||
| 497 | |||
| 498 | if (isset($rootClass->associationMappings[$property])) { | ||
| 499 | $association = $rootClass->associationMappings[$property]; | ||
| 500 | assert($association->isToOneOwningSide()); | ||
| 501 | $joinColumn = $association->joinColumns[0]->name; | ||
| 502 | |||
| 503 | foreach (array_keys($this->rsm->metaMappings, $joinColumn, true) as $alias) { | ||
| 504 | if ($this->rsm->columnOwnerMap[$alias] === $rootAlias) { | ||
| 505 | $sqlIdentifier[$property] = $alias; | ||
| 506 | } | ||
| 507 | } | ||
| 508 | } | ||
| 509 | } | ||
| 510 | |||
| 511 | if (count($sqlIdentifier) === 0) { | ||
| 512 | throw new RuntimeException('The Paginator does not support Queries which only yield ScalarResults.'); | ||
| 513 | } | ||
| 514 | |||
| 515 | if (count($rootIdentifier) !== count($sqlIdentifier)) { | ||
| 516 | throw new RuntimeException(sprintf( | ||
| 517 | 'Not all identifier properties can be found in the ResultSetMapping: %s', | ||
| 518 | implode(', ', array_diff($rootIdentifier, array_keys($sqlIdentifier))), | ||
| 519 | )); | ||
| 520 | } | ||
| 521 | |||
| 522 | return $sqlIdentifier; | ||
| 523 | } | ||
| 524 | |||
| 525 | public function walkPathExpression(PathExpression $pathExpr): string | ||
| 526 | { | ||
| 527 | if (! $this->inSubSelect && ! $this->platformSupportsRowNumber() && ! in_array($pathExpr, $this->orderByPathExpressions, true)) { | ||
| 528 | $this->orderByPathExpressions[] = $pathExpr; | ||
| 529 | } | ||
| 530 | |||
| 531 | return parent::walkPathExpression($pathExpr); | ||
| 532 | } | ||
| 533 | |||
| 534 | public function walkSubSelect(Subselect $subselect): string | ||
| 535 | { | ||
| 536 | $this->inSubSelect = true; | ||
| 537 | |||
| 538 | $sql = parent::walkSubselect($subselect); | ||
| 539 | |||
| 540 | $this->inSubSelect = false; | ||
| 541 | |||
| 542 | return $sql; | ||
| 543 | } | ||
| 544 | } | ||
diff --git a/vendor/doctrine/orm/src/Tools/Pagination/LimitSubqueryWalker.php b/vendor/doctrine/orm/src/Tools/Pagination/LimitSubqueryWalker.php new file mode 100644 index 0000000..3fb0eee --- /dev/null +++ b/vendor/doctrine/orm/src/Tools/Pagination/LimitSubqueryWalker.php | |||
| @@ -0,0 +1,155 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Tools\Pagination; | ||
| 6 | |||
| 7 | use Doctrine\DBAL\Types\Type; | ||
| 8 | use Doctrine\ORM\Query; | ||
| 9 | use Doctrine\ORM\Query\AST\Functions\IdentityFunction; | ||
| 10 | use Doctrine\ORM\Query\AST\Node; | ||
| 11 | use Doctrine\ORM\Query\AST\PathExpression; | ||
| 12 | use Doctrine\ORM\Query\AST\SelectExpression; | ||
| 13 | use Doctrine\ORM\Query\AST\SelectStatement; | ||
| 14 | use Doctrine\ORM\Query\TreeWalkerAdapter; | ||
| 15 | use RuntimeException; | ||
| 16 | |||
| 17 | use function count; | ||
| 18 | use function is_string; | ||
| 19 | use function reset; | ||
| 20 | |||
| 21 | /** | ||
| 22 | * Replaces the selectClause of the AST with a SELECT DISTINCT root.id equivalent. | ||
| 23 | */ | ||
| 24 | class LimitSubqueryWalker extends TreeWalkerAdapter | ||
| 25 | { | ||
| 26 | public const IDENTIFIER_TYPE = 'doctrine_paginator.id.type'; | ||
| 27 | |||
| 28 | public const FORCE_DBAL_TYPE_CONVERSION = 'doctrine_paginator.scalar_result.force_dbal_type_conversion'; | ||
| 29 | |||
| 30 | /** | ||
| 31 | * Counter for generating unique order column aliases. | ||
| 32 | */ | ||
| 33 | private int $aliasCounter = 0; | ||
| 34 | |||
| 35 | public function walkSelectStatement(SelectStatement $selectStatement): void | ||
| 36 | { | ||
| 37 | // Get the root entity and alias from the AST fromClause | ||
| 38 | $from = $selectStatement->fromClause->identificationVariableDeclarations; | ||
| 39 | $fromRoot = reset($from); | ||
| 40 | $rootAlias = $fromRoot->rangeVariableDeclaration->aliasIdentificationVariable; | ||
| 41 | $rootClass = $this->getMetadataForDqlAlias($rootAlias); | ||
| 42 | |||
| 43 | $this->validate($selectStatement); | ||
| 44 | $identifier = $rootClass->getSingleIdentifierFieldName(); | ||
| 45 | |||
| 46 | if (isset($rootClass->associationMappings[$identifier])) { | ||
| 47 | throw new RuntimeException('Paginating an entity with foreign key as identifier only works when using the Output Walkers. Call Paginator#setUseOutputWalkers(true) before iterating the paginator.'); | ||
| 48 | } | ||
| 49 | |||
| 50 | $query = $this->_getQuery(); | ||
| 51 | |||
| 52 | $query->setHint( | ||
| 53 | self::IDENTIFIER_TYPE, | ||
| 54 | Type::getType($rootClass->fieldMappings[$identifier]->type), | ||
| 55 | ); | ||
| 56 | |||
| 57 | $query->setHint(self::FORCE_DBAL_TYPE_CONVERSION, true); | ||
| 58 | |||
| 59 | $pathExpression = new PathExpression( | ||
| 60 | PathExpression::TYPE_STATE_FIELD | PathExpression::TYPE_SINGLE_VALUED_ASSOCIATION, | ||
| 61 | $rootAlias, | ||
| 62 | $identifier, | ||
| 63 | ); | ||
| 64 | |||
| 65 | $pathExpression->type = PathExpression::TYPE_STATE_FIELD; | ||
| 66 | |||
| 67 | $selectStatement->selectClause->selectExpressions = [new SelectExpression($pathExpression, '_dctrn_id')]; | ||
| 68 | $selectStatement->selectClause->isDistinct = ($query->getHints()[Paginator::HINT_ENABLE_DISTINCT] ?? true) === true; | ||
| 69 | |||
| 70 | if (! isset($selectStatement->orderByClause)) { | ||
| 71 | return; | ||
| 72 | } | ||
| 73 | |||
| 74 | $queryComponents = $this->getQueryComponents(); | ||
| 75 | foreach ($selectStatement->orderByClause->orderByItems as $item) { | ||
| 76 | if ($item->expression instanceof PathExpression) { | ||
| 77 | $selectStatement->selectClause->selectExpressions[] = new SelectExpression( | ||
| 78 | $this->createSelectExpressionItem($item->expression), | ||
| 79 | '_dctrn_ord' . $this->aliasCounter++, | ||
| 80 | ); | ||
| 81 | |||
| 82 | continue; | ||
| 83 | } | ||
| 84 | |||
| 85 | if (is_string($item->expression) && isset($queryComponents[$item->expression])) { | ||
| 86 | $qComp = $queryComponents[$item->expression]; | ||
| 87 | |||
| 88 | if (isset($qComp['resultVariable'])) { | ||
| 89 | $selectStatement->selectClause->selectExpressions[] = new SelectExpression( | ||
| 90 | $qComp['resultVariable'], | ||
| 91 | $item->expression, | ||
| 92 | ); | ||
| 93 | } | ||
| 94 | } | ||
| 95 | } | ||
| 96 | } | ||
| 97 | |||
| 98 | /** | ||
| 99 | * Validate the AST to ensure that this walker is able to properly manipulate it. | ||
| 100 | */ | ||
| 101 | private function validate(SelectStatement $AST): void | ||
| 102 | { | ||
| 103 | // Prevent LimitSubqueryWalker from being used with queries that include | ||
| 104 | // a limit, a fetched to-many join, and an order by condition that | ||
| 105 | // references a column from the fetch joined table. | ||
| 106 | $queryComponents = $this->getQueryComponents(); | ||
| 107 | $query = $this->_getQuery(); | ||
| 108 | $from = $AST->fromClause->identificationVariableDeclarations; | ||
| 109 | $fromRoot = reset($from); | ||
| 110 | |||
| 111 | if ( | ||
| 112 | $query instanceof Query | ||
| 113 | && $query->getMaxResults() !== null | ||
| 114 | && $AST->orderByClause | ||
| 115 | && count($fromRoot->joins) | ||
| 116 | ) { | ||
| 117 | // Check each orderby item. | ||
| 118 | // TODO: check complex orderby items too... | ||
| 119 | foreach ($AST->orderByClause->orderByItems as $orderByItem) { | ||
| 120 | $expression = $orderByItem->expression; | ||
| 121 | if ( | ||
| 122 | $orderByItem->expression instanceof PathExpression | ||
| 123 | && isset($queryComponents[$expression->identificationVariable]) | ||
| 124 | ) { | ||
| 125 | $queryComponent = $queryComponents[$expression->identificationVariable]; | ||
| 126 | if ( | ||
| 127 | isset($queryComponent['parent']) | ||
| 128 | && isset($queryComponent['relation']) | ||
| 129 | && $queryComponent['relation']->isToMany() | ||
| 130 | ) { | ||
| 131 | throw new RuntimeException('Cannot select distinct identifiers from query with LIMIT and ORDER BY on a column from a fetch joined to-many association. Use output walkers.'); | ||
| 132 | } | ||
| 133 | } | ||
| 134 | } | ||
| 135 | } | ||
| 136 | } | ||
| 137 | |||
| 138 | /** | ||
| 139 | * Retrieve either an IdentityFunction (IDENTITY(u.assoc)) or a state field (u.name). | ||
| 140 | * | ||
| 141 | * @return IdentityFunction|PathExpression | ||
| 142 | */ | ||
| 143 | private function createSelectExpressionItem(PathExpression $pathExpression): Node | ||
| 144 | { | ||
| 145 | if ($pathExpression->type === PathExpression::TYPE_SINGLE_VALUED_ASSOCIATION) { | ||
| 146 | $identity = new IdentityFunction('identity'); | ||
| 147 | |||
| 148 | $identity->pathExpression = clone $pathExpression; | ||
| 149 | |||
| 150 | return $identity; | ||
| 151 | } | ||
| 152 | |||
| 153 | return clone $pathExpression; | ||
| 154 | } | ||
| 155 | } | ||
diff --git a/vendor/doctrine/orm/src/Tools/Pagination/Paginator.php b/vendor/doctrine/orm/src/Tools/Pagination/Paginator.php new file mode 100644 index 0000000..db1b34d --- /dev/null +++ b/vendor/doctrine/orm/src/Tools/Pagination/Paginator.php | |||
| @@ -0,0 +1,263 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Tools\Pagination; | ||
| 6 | |||
| 7 | use ArrayIterator; | ||
| 8 | use Countable; | ||
| 9 | use Doctrine\Common\Collections\Collection; | ||
| 10 | use Doctrine\ORM\Internal\SQLResultCasing; | ||
| 11 | use Doctrine\ORM\NoResultException; | ||
| 12 | use Doctrine\ORM\Query; | ||
| 13 | use Doctrine\ORM\Query\Parameter; | ||
| 14 | use Doctrine\ORM\Query\Parser; | ||
| 15 | use Doctrine\ORM\Query\ResultSetMapping; | ||
| 16 | use Doctrine\ORM\QueryBuilder; | ||
| 17 | use IteratorAggregate; | ||
| 18 | use Traversable; | ||
| 19 | |||
| 20 | use function array_key_exists; | ||
| 21 | use function array_map; | ||
| 22 | use function array_sum; | ||
| 23 | use function assert; | ||
| 24 | use function is_string; | ||
| 25 | |||
| 26 | /** | ||
| 27 | * The paginator can handle various complex scenarios with DQL. | ||
| 28 | * | ||
| 29 | * @template-covariant T | ||
| 30 | * @implements IteratorAggregate<array-key, T> | ||
| 31 | */ | ||
| 32 | class Paginator implements Countable, IteratorAggregate | ||
| 33 | { | ||
| 34 | use SQLResultCasing; | ||
| 35 | |||
| 36 | public const HINT_ENABLE_DISTINCT = 'paginator.distinct.enable'; | ||
| 37 | |||
| 38 | private readonly Query $query; | ||
| 39 | private bool|null $useOutputWalkers = null; | ||
| 40 | private int|null $count = null; | ||
| 41 | |||
| 42 | /** @param bool $fetchJoinCollection Whether the query joins a collection (true by default). */ | ||
| 43 | public function __construct( | ||
| 44 | Query|QueryBuilder $query, | ||
| 45 | private readonly bool $fetchJoinCollection = true, | ||
| 46 | ) { | ||
| 47 | if ($query instanceof QueryBuilder) { | ||
| 48 | $query = $query->getQuery(); | ||
| 49 | } | ||
| 50 | |||
| 51 | $this->query = $query; | ||
| 52 | } | ||
| 53 | |||
| 54 | /** | ||
| 55 | * Returns the query. | ||
| 56 | */ | ||
| 57 | public function getQuery(): Query | ||
| 58 | { | ||
| 59 | return $this->query; | ||
| 60 | } | ||
| 61 | |||
| 62 | /** | ||
| 63 | * Returns whether the query joins a collection. | ||
| 64 | * | ||
| 65 | * @return bool Whether the query joins a collection. | ||
| 66 | */ | ||
| 67 | public function getFetchJoinCollection(): bool | ||
| 68 | { | ||
| 69 | return $this->fetchJoinCollection; | ||
| 70 | } | ||
| 71 | |||
| 72 | /** | ||
| 73 | * Returns whether the paginator will use an output walker. | ||
| 74 | */ | ||
| 75 | public function getUseOutputWalkers(): bool|null | ||
| 76 | { | ||
| 77 | return $this->useOutputWalkers; | ||
| 78 | } | ||
| 79 | |||
| 80 | /** | ||
| 81 | * Sets whether the paginator will use an output walker. | ||
| 82 | * | ||
| 83 | * @return $this | ||
| 84 | */ | ||
| 85 | public function setUseOutputWalkers(bool|null $useOutputWalkers): static | ||
| 86 | { | ||
| 87 | $this->useOutputWalkers = $useOutputWalkers; | ||
| 88 | |||
| 89 | return $this; | ||
| 90 | } | ||
| 91 | |||
| 92 | public function count(): int | ||
| 93 | { | ||
| 94 | if ($this->count === null) { | ||
| 95 | try { | ||
| 96 | $this->count = (int) array_sum(array_map('current', $this->getCountQuery()->getScalarResult())); | ||
| 97 | } catch (NoResultException) { | ||
| 98 | $this->count = 0; | ||
| 99 | } | ||
| 100 | } | ||
| 101 | |||
| 102 | return $this->count; | ||
| 103 | } | ||
| 104 | |||
| 105 | /** | ||
| 106 | * {@inheritDoc} | ||
| 107 | * | ||
| 108 | * @psalm-return Traversable<array-key, T> | ||
| 109 | */ | ||
| 110 | public function getIterator(): Traversable | ||
| 111 | { | ||
| 112 | $offset = $this->query->getFirstResult(); | ||
| 113 | $length = $this->query->getMaxResults(); | ||
| 114 | |||
| 115 | if ($this->fetchJoinCollection && $length !== null) { | ||
| 116 | $subQuery = $this->cloneQuery($this->query); | ||
| 117 | |||
| 118 | if ($this->useOutputWalker($subQuery)) { | ||
| 119 | $subQuery->setHint(Query::HINT_CUSTOM_OUTPUT_WALKER, LimitSubqueryOutputWalker::class); | ||
| 120 | } else { | ||
| 121 | $this->appendTreeWalker($subQuery, LimitSubqueryWalker::class); | ||
| 122 | $this->unbindUnusedQueryParams($subQuery); | ||
| 123 | } | ||
| 124 | |||
| 125 | $subQuery->setFirstResult($offset)->setMaxResults($length); | ||
| 126 | |||
| 127 | $foundIdRows = $subQuery->getScalarResult(); | ||
| 128 | |||
| 129 | // don't do this for an empty id array | ||
| 130 | if ($foundIdRows === []) { | ||
| 131 | return new ArrayIterator([]); | ||
| 132 | } | ||
| 133 | |||
| 134 | $whereInQuery = $this->cloneQuery($this->query); | ||
| 135 | $ids = array_map('current', $foundIdRows); | ||
| 136 | |||
| 137 | $this->appendTreeWalker($whereInQuery, WhereInWalker::class); | ||
| 138 | $whereInQuery->setHint(WhereInWalker::HINT_PAGINATOR_HAS_IDS, true); | ||
| 139 | $whereInQuery->setFirstResult(0)->setMaxResults(null); | ||
| 140 | $whereInQuery->setCacheable($this->query->isCacheable()); | ||
| 141 | |||
| 142 | $databaseIds = $this->convertWhereInIdentifiersToDatabaseValues($ids); | ||
| 143 | $whereInQuery->setParameter(WhereInWalker::PAGINATOR_ID_ALIAS, $databaseIds); | ||
| 144 | |||
| 145 | $result = $whereInQuery->getResult($this->query->getHydrationMode()); | ||
| 146 | } else { | ||
| 147 | $result = $this->cloneQuery($this->query) | ||
| 148 | ->setMaxResults($length) | ||
| 149 | ->setFirstResult($offset) | ||
| 150 | ->setCacheable($this->query->isCacheable()) | ||
| 151 | ->getResult($this->query->getHydrationMode()); | ||
| 152 | } | ||
| 153 | |||
| 154 | return new ArrayIterator($result); | ||
| 155 | } | ||
| 156 | |||
| 157 | private function cloneQuery(Query $query): Query | ||
| 158 | { | ||
| 159 | $cloneQuery = clone $query; | ||
| 160 | |||
| 161 | $cloneQuery->setParameters(clone $query->getParameters()); | ||
| 162 | $cloneQuery->setCacheable(false); | ||
| 163 | |||
| 164 | foreach ($query->getHints() as $name => $value) { | ||
| 165 | $cloneQuery->setHint($name, $value); | ||
| 166 | } | ||
| 167 | |||
| 168 | return $cloneQuery; | ||
| 169 | } | ||
| 170 | |||
| 171 | /** | ||
| 172 | * Determines whether to use an output walker for the query. | ||
| 173 | */ | ||
| 174 | private function useOutputWalker(Query $query): bool | ||
| 175 | { | ||
| 176 | if ($this->useOutputWalkers === null) { | ||
| 177 | return (bool) $query->getHint(Query::HINT_CUSTOM_OUTPUT_WALKER) === false; | ||
| 178 | } | ||
| 179 | |||
| 180 | return $this->useOutputWalkers; | ||
| 181 | } | ||
| 182 | |||
| 183 | /** | ||
| 184 | * Appends a custom tree walker to the tree walkers hint. | ||
| 185 | * | ||
| 186 | * @psalm-param class-string $walkerClass | ||
| 187 | */ | ||
| 188 | private function appendTreeWalker(Query $query, string $walkerClass): void | ||
| 189 | { | ||
| 190 | $hints = $query->getHint(Query::HINT_CUSTOM_TREE_WALKERS); | ||
| 191 | |||
| 192 | if ($hints === false) { | ||
| 193 | $hints = []; | ||
| 194 | } | ||
| 195 | |||
| 196 | $hints[] = $walkerClass; | ||
| 197 | $query->setHint(Query::HINT_CUSTOM_TREE_WALKERS, $hints); | ||
| 198 | } | ||
| 199 | |||
| 200 | /** | ||
| 201 | * Returns Query prepared to count. | ||
| 202 | */ | ||
| 203 | private function getCountQuery(): Query | ||
| 204 | { | ||
| 205 | $countQuery = $this->cloneQuery($this->query); | ||
| 206 | |||
| 207 | if (! $countQuery->hasHint(CountWalker::HINT_DISTINCT)) { | ||
| 208 | $countQuery->setHint(CountWalker::HINT_DISTINCT, true); | ||
| 209 | } | ||
| 210 | |||
| 211 | if ($this->useOutputWalker($countQuery)) { | ||
| 212 | $platform = $countQuery->getEntityManager()->getConnection()->getDatabasePlatform(); // law of demeter win | ||
| 213 | |||
| 214 | $rsm = new ResultSetMapping(); | ||
| 215 | $rsm->addScalarResult($this->getSQLResultCasing($platform, 'dctrn_count'), 'count'); | ||
| 216 | |||
| 217 | $countQuery->setHint(Query::HINT_CUSTOM_OUTPUT_WALKER, CountOutputWalker::class); | ||
| 218 | $countQuery->setResultSetMapping($rsm); | ||
| 219 | } else { | ||
| 220 | $this->appendTreeWalker($countQuery, CountWalker::class); | ||
| 221 | $this->unbindUnusedQueryParams($countQuery); | ||
| 222 | } | ||
| 223 | |||
| 224 | $countQuery->setFirstResult(0)->setMaxResults(null); | ||
| 225 | |||
| 226 | return $countQuery; | ||
| 227 | } | ||
| 228 | |||
| 229 | private function unbindUnusedQueryParams(Query $query): void | ||
| 230 | { | ||
| 231 | $parser = new Parser($query); | ||
| 232 | $parameterMappings = $parser->parse()->getParameterMappings(); | ||
| 233 | /** @var Collection|Parameter[] $parameters */ | ||
| 234 | $parameters = $query->getParameters(); | ||
| 235 | |||
| 236 | foreach ($parameters as $key => $parameter) { | ||
| 237 | $parameterName = $parameter->getName(); | ||
| 238 | |||
| 239 | if (! (isset($parameterMappings[$parameterName]) || array_key_exists($parameterName, $parameterMappings))) { | ||
| 240 | unset($parameters[$key]); | ||
| 241 | } | ||
| 242 | } | ||
| 243 | |||
| 244 | $query->setParameters($parameters); | ||
| 245 | } | ||
| 246 | |||
| 247 | /** | ||
| 248 | * @param mixed[] $identifiers | ||
| 249 | * | ||
| 250 | * @return mixed[] | ||
| 251 | */ | ||
| 252 | private function convertWhereInIdentifiersToDatabaseValues(array $identifiers): array | ||
| 253 | { | ||
| 254 | $query = $this->cloneQuery($this->query); | ||
| 255 | $query->setHint(Query::HINT_CUSTOM_OUTPUT_WALKER, RootTypeWalker::class); | ||
| 256 | |||
| 257 | $connection = $this->query->getEntityManager()->getConnection(); | ||
| 258 | $type = $query->getSQL(); | ||
| 259 | assert(is_string($type)); | ||
| 260 | |||
| 261 | return array_map(static fn ($id): mixed => $connection->convertToDatabaseValue($id, $type), $identifiers); | ||
| 262 | } | ||
| 263 | } | ||
diff --git a/vendor/doctrine/orm/src/Tools/Pagination/RootTypeWalker.php b/vendor/doctrine/orm/src/Tools/Pagination/RootTypeWalker.php new file mode 100644 index 0000000..f630ee1 --- /dev/null +++ b/vendor/doctrine/orm/src/Tools/Pagination/RootTypeWalker.php | |||
| @@ -0,0 +1,48 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Tools\Pagination; | ||
| 6 | |||
| 7 | use Doctrine\ORM\Query\AST; | ||
| 8 | use Doctrine\ORM\Query\SqlWalker; | ||
| 9 | use Doctrine\ORM\Utility\PersisterHelper; | ||
| 10 | use RuntimeException; | ||
| 11 | |||
| 12 | use function count; | ||
| 13 | use function reset; | ||
| 14 | |||
| 15 | /** | ||
| 16 | * Infers the DBAL type of the #Id (identifier) column of the given query's root entity, and | ||
| 17 | * returns it in place of a real SQL statement. | ||
| 18 | * | ||
| 19 | * Obtaining this type is a necessary intermediate step for \Doctrine\ORM\Tools\Pagination\Paginator. | ||
| 20 | * We can best do this from a tree walker because it gives us access to the AST. | ||
| 21 | * | ||
| 22 | * Returning the type instead of a "real" SQL statement is a slight hack. However, it has the | ||
| 23 | * benefit that the DQL -> root entity id type resolution can be cached in the query cache. | ||
| 24 | */ | ||
| 25 | final class RootTypeWalker extends SqlWalker | ||
| 26 | { | ||
| 27 | public function walkSelectStatement(AST\SelectStatement $selectStatement): string | ||
| 28 | { | ||
| 29 | // Get the root entity and alias from the AST fromClause | ||
| 30 | $from = $selectStatement->fromClause->identificationVariableDeclarations; | ||
| 31 | |||
| 32 | if (count($from) > 1) { | ||
| 33 | throw new RuntimeException('Can only process queries that select only one FROM component'); | ||
| 34 | } | ||
| 35 | |||
| 36 | $fromRoot = reset($from); | ||
| 37 | $rootAlias = $fromRoot->rangeVariableDeclaration->aliasIdentificationVariable; | ||
| 38 | $rootClass = $this->getMetadataForDqlAlias($rootAlias); | ||
| 39 | $identifierFieldName = $rootClass->getSingleIdentifierFieldName(); | ||
| 40 | |||
| 41 | return PersisterHelper::getTypeOfField( | ||
| 42 | $identifierFieldName, | ||
| 43 | $rootClass, | ||
| 44 | $this->getQuery() | ||
| 45 | ->getEntityManager(), | ||
| 46 | )[0]; | ||
| 47 | } | ||
| 48 | } | ||
diff --git a/vendor/doctrine/orm/src/Tools/Pagination/RowNumberOverFunction.php b/vendor/doctrine/orm/src/Tools/Pagination/RowNumberOverFunction.php new file mode 100644 index 0000000..a0fdd01 --- /dev/null +++ b/vendor/doctrine/orm/src/Tools/Pagination/RowNumberOverFunction.php | |||
| @@ -0,0 +1,40 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Tools\Pagination; | ||
| 6 | |||
| 7 | use Doctrine\ORM\Query\AST\Functions\FunctionNode; | ||
| 8 | use Doctrine\ORM\Query\AST\OrderByClause; | ||
| 9 | use Doctrine\ORM\Query\Parser; | ||
| 10 | use Doctrine\ORM\Query\SqlWalker; | ||
| 11 | use Doctrine\ORM\Tools\Pagination\Exception\RowNumberOverFunctionNotEnabled; | ||
| 12 | |||
| 13 | use function trim; | ||
| 14 | |||
| 15 | /** | ||
| 16 | * RowNumberOverFunction | ||
| 17 | * | ||
| 18 | * Provides ROW_NUMBER() OVER(ORDER BY...) construct for use in LimitSubqueryOutputWalker | ||
| 19 | */ | ||
| 20 | class RowNumberOverFunction extends FunctionNode | ||
| 21 | { | ||
| 22 | public OrderByClause $orderByClause; | ||
| 23 | |||
| 24 | public function getSql(SqlWalker $sqlWalker): string | ||
| 25 | { | ||
| 26 | return 'ROW_NUMBER() OVER(' . trim($sqlWalker->walkOrderByClause( | ||
| 27 | $this->orderByClause, | ||
| 28 | )) . ')'; | ||
| 29 | } | ||
| 30 | |||
| 31 | /** | ||
| 32 | * @throws RowNumberOverFunctionNotEnabled | ||
| 33 | * | ||
| 34 | * @inheritdoc | ||
| 35 | */ | ||
| 36 | public function parse(Parser $parser): void | ||
| 37 | { | ||
| 38 | throw RowNumberOverFunctionNotEnabled::create(); | ||
| 39 | } | ||
| 40 | } | ||
diff --git a/vendor/doctrine/orm/src/Tools/Pagination/WhereInWalker.php b/vendor/doctrine/orm/src/Tools/Pagination/WhereInWalker.php new file mode 100644 index 0000000..01741ca --- /dev/null +++ b/vendor/doctrine/orm/src/Tools/Pagination/WhereInWalker.php | |||
| @@ -0,0 +1,116 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Tools\Pagination; | ||
| 6 | |||
| 7 | use Doctrine\ORM\Query\AST\ArithmeticExpression; | ||
| 8 | use Doctrine\ORM\Query\AST\ConditionalExpression; | ||
| 9 | use Doctrine\ORM\Query\AST\ConditionalPrimary; | ||
| 10 | use Doctrine\ORM\Query\AST\ConditionalTerm; | ||
| 11 | use Doctrine\ORM\Query\AST\InListExpression; | ||
| 12 | use Doctrine\ORM\Query\AST\InputParameter; | ||
| 13 | use Doctrine\ORM\Query\AST\NullComparisonExpression; | ||
| 14 | use Doctrine\ORM\Query\AST\PathExpression; | ||
| 15 | use Doctrine\ORM\Query\AST\SelectStatement; | ||
| 16 | use Doctrine\ORM\Query\AST\SimpleArithmeticExpression; | ||
| 17 | use Doctrine\ORM\Query\AST\WhereClause; | ||
| 18 | use Doctrine\ORM\Query\TreeWalkerAdapter; | ||
| 19 | use RuntimeException; | ||
| 20 | |||
| 21 | use function count; | ||
| 22 | use function reset; | ||
| 23 | |||
| 24 | /** | ||
| 25 | * Appends a condition equivalent to "WHERE IN (:dpid_1, :dpid_2, ...)" to the whereClause of the AST. | ||
| 26 | * | ||
| 27 | * The parameter namespace (dpid) is defined by | ||
| 28 | * the PAGINATOR_ID_ALIAS | ||
| 29 | * | ||
| 30 | * The HINT_PAGINATOR_HAS_IDS query hint indicates whether there are | ||
| 31 | * any ids in the parameter at all. | ||
| 32 | */ | ||
| 33 | class WhereInWalker extends TreeWalkerAdapter | ||
| 34 | { | ||
| 35 | /** | ||
| 36 | * ID Count hint name. | ||
| 37 | */ | ||
| 38 | public const HINT_PAGINATOR_HAS_IDS = 'doctrine.paginator_has_ids'; | ||
| 39 | |||
| 40 | /** | ||
| 41 | * Primary key alias for query. | ||
| 42 | */ | ||
| 43 | public const PAGINATOR_ID_ALIAS = 'dpid'; | ||
| 44 | |||
| 45 | public function walkSelectStatement(SelectStatement $selectStatement): void | ||
| 46 | { | ||
| 47 | // Get the root entity and alias from the AST fromClause | ||
| 48 | $from = $selectStatement->fromClause->identificationVariableDeclarations; | ||
| 49 | |||
| 50 | if (count($from) > 1) { | ||
| 51 | throw new RuntimeException('Cannot count query which selects two FROM components, cannot make distinction'); | ||
| 52 | } | ||
| 53 | |||
| 54 | $fromRoot = reset($from); | ||
| 55 | $rootAlias = $fromRoot->rangeVariableDeclaration->aliasIdentificationVariable; | ||
| 56 | $rootClass = $this->getMetadataForDqlAlias($rootAlias); | ||
| 57 | $identifierFieldName = $rootClass->getSingleIdentifierFieldName(); | ||
| 58 | |||
| 59 | $pathType = PathExpression::TYPE_STATE_FIELD; | ||
| 60 | if (isset($rootClass->associationMappings[$identifierFieldName])) { | ||
| 61 | $pathType = PathExpression::TYPE_SINGLE_VALUED_ASSOCIATION; | ||
| 62 | } | ||
| 63 | |||
| 64 | $pathExpression = new PathExpression(PathExpression::TYPE_STATE_FIELD | PathExpression::TYPE_SINGLE_VALUED_ASSOCIATION, $rootAlias, $identifierFieldName); | ||
| 65 | $pathExpression->type = $pathType; | ||
| 66 | |||
| 67 | $hasIds = $this->_getQuery()->getHint(self::HINT_PAGINATOR_HAS_IDS); | ||
| 68 | |||
| 69 | if ($hasIds) { | ||
| 70 | $arithmeticExpression = new ArithmeticExpression(); | ||
| 71 | $arithmeticExpression->simpleArithmeticExpression = new SimpleArithmeticExpression( | ||
| 72 | [$pathExpression], | ||
| 73 | ); | ||
| 74 | $expression = new InListExpression( | ||
| 75 | $arithmeticExpression, | ||
| 76 | [new InputParameter(':' . self::PAGINATOR_ID_ALIAS)], | ||
| 77 | ); | ||
| 78 | } else { | ||
| 79 | $expression = new NullComparisonExpression($pathExpression); | ||
| 80 | } | ||
| 81 | |||
| 82 | $conditionalPrimary = new ConditionalPrimary(); | ||
| 83 | $conditionalPrimary->simpleConditionalExpression = $expression; | ||
| 84 | if ($selectStatement->whereClause) { | ||
| 85 | if ($selectStatement->whereClause->conditionalExpression instanceof ConditionalTerm) { | ||
| 86 | $selectStatement->whereClause->conditionalExpression->conditionalFactors[] = $conditionalPrimary; | ||
| 87 | } elseif ($selectStatement->whereClause->conditionalExpression instanceof ConditionalPrimary) { | ||
| 88 | $selectStatement->whereClause->conditionalExpression = new ConditionalExpression( | ||
| 89 | [ | ||
| 90 | new ConditionalTerm( | ||
| 91 | [ | ||
| 92 | $selectStatement->whereClause->conditionalExpression, | ||
| 93 | $conditionalPrimary, | ||
| 94 | ], | ||
| 95 | ), | ||
| 96 | ], | ||
| 97 | ); | ||
| 98 | } else { | ||
| 99 | $tmpPrimary = new ConditionalPrimary(); | ||
| 100 | $tmpPrimary->conditionalExpression = $selectStatement->whereClause->conditionalExpression; | ||
| 101 | $selectStatement->whereClause->conditionalExpression = new ConditionalTerm( | ||
| 102 | [ | ||
| 103 | $tmpPrimary, | ||
| 104 | $conditionalPrimary, | ||
| 105 | ], | ||
| 106 | ); | ||
| 107 | } | ||
| 108 | } else { | ||
| 109 | $selectStatement->whereClause = new WhereClause( | ||
| 110 | new ConditionalExpression( | ||
| 111 | [new ConditionalTerm([$conditionalPrimary])], | ||
| 112 | ), | ||
| 113 | ); | ||
| 114 | } | ||
| 115 | } | ||
| 116 | } | ||
diff --git a/vendor/doctrine/orm/src/Tools/ResolveTargetEntityListener.php b/vendor/doctrine/orm/src/Tools/ResolveTargetEntityListener.php new file mode 100644 index 0000000..9e48521 --- /dev/null +++ b/vendor/doctrine/orm/src/Tools/ResolveTargetEntityListener.php | |||
| @@ -0,0 +1,117 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Tools; | ||
| 6 | |||
| 7 | use Doctrine\Common\EventSubscriber; | ||
| 8 | use Doctrine\ORM\Event\LoadClassMetadataEventArgs; | ||
| 9 | use Doctrine\ORM\Event\OnClassMetadataNotFoundEventArgs; | ||
| 10 | use Doctrine\ORM\Events; | ||
| 11 | use Doctrine\ORM\Mapping\AssociationMapping; | ||
| 12 | use Doctrine\ORM\Mapping\ClassMetadata; | ||
| 13 | |||
| 14 | use function array_key_exists; | ||
| 15 | use function array_replace_recursive; | ||
| 16 | use function ltrim; | ||
| 17 | |||
| 18 | /** | ||
| 19 | * ResolveTargetEntityListener | ||
| 20 | * | ||
| 21 | * Mechanism to overwrite interfaces or classes specified as association | ||
| 22 | * targets. | ||
| 23 | */ | ||
| 24 | class ResolveTargetEntityListener implements EventSubscriber | ||
| 25 | { | ||
| 26 | /** @var mixed[][] indexed by original entity name */ | ||
| 27 | private array $resolveTargetEntities = []; | ||
| 28 | |||
| 29 | /** | ||
| 30 | * {@inheritDoc} | ||
| 31 | */ | ||
| 32 | public function getSubscribedEvents(): array | ||
| 33 | { | ||
| 34 | return [ | ||
| 35 | Events::loadClassMetadata, | ||
| 36 | Events::onClassMetadataNotFound, | ||
| 37 | ]; | ||
| 38 | } | ||
| 39 | |||
| 40 | /** | ||
| 41 | * Adds a target-entity class name to resolve to a new class name. | ||
| 42 | * | ||
| 43 | * @psalm-param array<string, mixed> $mapping | ||
| 44 | */ | ||
| 45 | public function addResolveTargetEntity(string $originalEntity, string $newEntity, array $mapping): void | ||
| 46 | { | ||
| 47 | $mapping['targetEntity'] = ltrim($newEntity, '\\'); | ||
| 48 | $this->resolveTargetEntities[ltrim($originalEntity, '\\')] = $mapping; | ||
| 49 | } | ||
| 50 | |||
| 51 | /** @internal this is an event callback, and should not be called directly */ | ||
| 52 | public function onClassMetadataNotFound(OnClassMetadataNotFoundEventArgs $args): void | ||
| 53 | { | ||
| 54 | if (array_key_exists($args->getClassName(), $this->resolveTargetEntities)) { | ||
| 55 | $args->setFoundMetadata( | ||
| 56 | $args | ||
| 57 | ->getObjectManager() | ||
| 58 | ->getClassMetadata($this->resolveTargetEntities[$args->getClassName()]['targetEntity']), | ||
| 59 | ); | ||
| 60 | } | ||
| 61 | } | ||
| 62 | |||
| 63 | /** | ||
| 64 | * Processes event and resolves new target entity names. | ||
| 65 | * | ||
| 66 | * @internal this is an event callback, and should not be called directly | ||
| 67 | */ | ||
| 68 | public function loadClassMetadata(LoadClassMetadataEventArgs $args): void | ||
| 69 | { | ||
| 70 | $cm = $args->getClassMetadata(); | ||
| 71 | |||
| 72 | foreach ($cm->associationMappings as $mapping) { | ||
| 73 | if (isset($this->resolveTargetEntities[$mapping->targetEntity])) { | ||
| 74 | $this->remapAssociation($cm, $mapping); | ||
| 75 | } | ||
| 76 | } | ||
| 77 | |||
| 78 | foreach ($this->resolveTargetEntities as $interface => $data) { | ||
| 79 | if ($data['targetEntity'] === $cm->getName()) { | ||
| 80 | $args->getEntityManager()->getMetadataFactory()->setMetadataFor($interface, $cm); | ||
| 81 | } | ||
| 82 | } | ||
| 83 | |||
| 84 | foreach ($cm->discriminatorMap as $value => $class) { | ||
| 85 | if (isset($this->resolveTargetEntities[$class])) { | ||
| 86 | $cm->addDiscriminatorMapClass($value, $this->resolveTargetEntities[$class]['targetEntity']); | ||
| 87 | } | ||
| 88 | } | ||
| 89 | } | ||
| 90 | |||
| 91 | private function remapAssociation(ClassMetadata $classMetadata, AssociationMapping $mapping): void | ||
| 92 | { | ||
| 93 | $newMapping = $this->resolveTargetEntities[$mapping->targetEntity]; | ||
| 94 | $newMapping = array_replace_recursive( | ||
| 95 | $mapping->toArray(), | ||
| 96 | $newMapping, | ||
| 97 | ); | ||
| 98 | $newMapping['fieldName'] = $mapping->fieldName; | ||
| 99 | |||
| 100 | unset($classMetadata->associationMappings[$mapping->fieldName]); | ||
| 101 | |||
| 102 | switch ($mapping->type()) { | ||
| 103 | case ClassMetadata::MANY_TO_MANY: | ||
| 104 | $classMetadata->mapManyToMany($newMapping); | ||
| 105 | break; | ||
| 106 | case ClassMetadata::MANY_TO_ONE: | ||
| 107 | $classMetadata->mapManyToOne($newMapping); | ||
| 108 | break; | ||
| 109 | case ClassMetadata::ONE_TO_MANY: | ||
| 110 | $classMetadata->mapOneToMany($newMapping); | ||
| 111 | break; | ||
| 112 | case ClassMetadata::ONE_TO_ONE: | ||
| 113 | $classMetadata->mapOneToOne($newMapping); | ||
| 114 | break; | ||
| 115 | } | ||
| 116 | } | ||
| 117 | } | ||
diff --git a/vendor/doctrine/orm/src/Tools/SchemaTool.php b/vendor/doctrine/orm/src/Tools/SchemaTool.php new file mode 100644 index 0000000..42b52df --- /dev/null +++ b/vendor/doctrine/orm/src/Tools/SchemaTool.php | |||
| @@ -0,0 +1,932 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Tools; | ||
| 6 | |||
| 7 | use BackedEnum; | ||
| 8 | use Doctrine\DBAL\Platforms\AbstractPlatform; | ||
| 9 | use Doctrine\DBAL\Schema\AbstractAsset; | ||
| 10 | use Doctrine\DBAL\Schema\AbstractSchemaManager; | ||
| 11 | use Doctrine\DBAL\Schema\Index; | ||
| 12 | use Doctrine\DBAL\Schema\Schema; | ||
| 13 | use Doctrine\DBAL\Schema\Table; | ||
| 14 | use Doctrine\ORM\EntityManagerInterface; | ||
| 15 | use Doctrine\ORM\Mapping\AssociationMapping; | ||
| 16 | use Doctrine\ORM\Mapping\ClassMetadata; | ||
| 17 | use Doctrine\ORM\Mapping\DiscriminatorColumnMapping; | ||
| 18 | use Doctrine\ORM\Mapping\FieldMapping; | ||
| 19 | use Doctrine\ORM\Mapping\JoinColumnMapping; | ||
| 20 | use Doctrine\ORM\Mapping\ManyToManyOwningSideMapping; | ||
| 21 | use Doctrine\ORM\Mapping\MappingException; | ||
| 22 | use Doctrine\ORM\Mapping\QuoteStrategy; | ||
| 23 | use Doctrine\ORM\Tools\Event\GenerateSchemaEventArgs; | ||
| 24 | use Doctrine\ORM\Tools\Event\GenerateSchemaTableEventArgs; | ||
| 25 | use Doctrine\ORM\Tools\Exception\MissingColumnException; | ||
| 26 | use Doctrine\ORM\Tools\Exception\NotSupported; | ||
| 27 | use Throwable; | ||
| 28 | |||
| 29 | use function array_diff; | ||
| 30 | use function array_diff_key; | ||
| 31 | use function array_filter; | ||
| 32 | use function array_flip; | ||
| 33 | use function array_intersect_key; | ||
| 34 | use function assert; | ||
| 35 | use function count; | ||
| 36 | use function current; | ||
| 37 | use function implode; | ||
| 38 | use function in_array; | ||
| 39 | use function is_numeric; | ||
| 40 | use function strtolower; | ||
| 41 | |||
| 42 | /** | ||
| 43 | * The SchemaTool is a tool to create/drop/update database schemas based on | ||
| 44 | * <tt>ClassMetadata</tt> class descriptors. | ||
| 45 | * | ||
| 46 | * @link www.doctrine-project.org | ||
| 47 | */ | ||
| 48 | class SchemaTool | ||
| 49 | { | ||
| 50 | private const KNOWN_COLUMN_OPTIONS = ['comment', 'unsigned', 'fixed', 'default']; | ||
| 51 | |||
| 52 | private readonly AbstractPlatform $platform; | ||
| 53 | private readonly QuoteStrategy $quoteStrategy; | ||
| 54 | private readonly AbstractSchemaManager $schemaManager; | ||
| 55 | |||
| 56 | /** | ||
| 57 | * Initializes a new SchemaTool instance that uses the connection of the | ||
| 58 | * provided EntityManager. | ||
| 59 | */ | ||
| 60 | public function __construct(private readonly EntityManagerInterface $em) | ||
| 61 | { | ||
| 62 | $this->platform = $em->getConnection()->getDatabasePlatform(); | ||
| 63 | $this->quoteStrategy = $em->getConfiguration()->getQuoteStrategy(); | ||
| 64 | $this->schemaManager = $em->getConnection()->createSchemaManager(); | ||
| 65 | } | ||
| 66 | |||
| 67 | /** | ||
| 68 | * Creates the database schema for the given array of ClassMetadata instances. | ||
| 69 | * | ||
| 70 | * @psalm-param list<ClassMetadata> $classes | ||
| 71 | * | ||
| 72 | * @throws ToolsException | ||
| 73 | */ | ||
| 74 | public function createSchema(array $classes): void | ||
| 75 | { | ||
| 76 | $createSchemaSql = $this->getCreateSchemaSql($classes); | ||
| 77 | $conn = $this->em->getConnection(); | ||
| 78 | |||
| 79 | foreach ($createSchemaSql as $sql) { | ||
| 80 | try { | ||
| 81 | $conn->executeStatement($sql); | ||
| 82 | } catch (Throwable $e) { | ||
| 83 | throw ToolsException::schemaToolFailure($sql, $e); | ||
| 84 | } | ||
| 85 | } | ||
| 86 | } | ||
| 87 | |||
| 88 | /** | ||
| 89 | * Gets the list of DDL statements that are required to create the database schema for | ||
| 90 | * the given list of ClassMetadata instances. | ||
| 91 | * | ||
| 92 | * @psalm-param list<ClassMetadata> $classes | ||
| 93 | * | ||
| 94 | * @return list<string> The SQL statements needed to create the schema for the classes. | ||
| 95 | */ | ||
| 96 | public function getCreateSchemaSql(array $classes): array | ||
| 97 | { | ||
| 98 | $schema = $this->getSchemaFromMetadata($classes); | ||
| 99 | |||
| 100 | return $schema->toSql($this->platform); | ||
| 101 | } | ||
| 102 | |||
| 103 | /** | ||
| 104 | * Detects instances of ClassMetadata that don't need to be processed in the SchemaTool context. | ||
| 105 | * | ||
| 106 | * @psalm-param array<string, bool> $processedClasses | ||
| 107 | */ | ||
| 108 | private function processingNotRequired( | ||
| 109 | ClassMetadata $class, | ||
| 110 | array $processedClasses, | ||
| 111 | ): bool { | ||
| 112 | return isset($processedClasses[$class->name]) || | ||
| 113 | $class->isMappedSuperclass || | ||
| 114 | $class->isEmbeddedClass || | ||
| 115 | ($class->isInheritanceTypeSingleTable() && $class->name !== $class->rootEntityName) || | ||
| 116 | in_array($class->name, $this->em->getConfiguration()->getSchemaIgnoreClasses()); | ||
| 117 | } | ||
| 118 | |||
| 119 | /** | ||
| 120 | * Resolves fields in index mapping to column names | ||
| 121 | * | ||
| 122 | * @param mixed[] $indexData index or unique constraint data | ||
| 123 | * | ||
| 124 | * @return list<string> Column names from combined fields and columns mappings | ||
| 125 | */ | ||
| 126 | private function getIndexColumns(ClassMetadata $class, array $indexData): array | ||
| 127 | { | ||
| 128 | $columns = []; | ||
| 129 | |||
| 130 | if ( | ||
| 131 | isset($indexData['columns'], $indexData['fields']) | ||
| 132 | || ( | ||
| 133 | ! isset($indexData['columns']) | ||
| 134 | && ! isset($indexData['fields']) | ||
| 135 | ) | ||
| 136 | ) { | ||
| 137 | throw MappingException::invalidIndexConfiguration( | ||
| 138 | (string) $class, | ||
| 139 | $indexData['name'] ?? 'unnamed', | ||
| 140 | ); | ||
| 141 | } | ||
| 142 | |||
| 143 | if (isset($indexData['columns'])) { | ||
| 144 | $columns = $indexData['columns']; | ||
| 145 | } | ||
| 146 | |||
| 147 | if (isset($indexData['fields'])) { | ||
| 148 | foreach ($indexData['fields'] as $fieldName) { | ||
| 149 | if ($class->hasField($fieldName)) { | ||
| 150 | $columns[] = $this->quoteStrategy->getColumnName($fieldName, $class, $this->platform); | ||
| 151 | } elseif ($class->hasAssociation($fieldName)) { | ||
| 152 | $assoc = $class->getAssociationMapping($fieldName); | ||
| 153 | assert($assoc->isToOneOwningSide()); | ||
| 154 | foreach ($assoc->joinColumns as $joinColumn) { | ||
| 155 | $columns[] = $this->quoteStrategy->getJoinColumnName($joinColumn, $class, $this->platform); | ||
| 156 | } | ||
| 157 | } | ||
| 158 | } | ||
| 159 | } | ||
| 160 | |||
| 161 | return $columns; | ||
| 162 | } | ||
| 163 | |||
| 164 | /** | ||
| 165 | * Creates a Schema instance from a given set of metadata classes. | ||
| 166 | * | ||
| 167 | * @psalm-param list<ClassMetadata> $classes | ||
| 168 | * | ||
| 169 | * @throws NotSupported | ||
| 170 | */ | ||
| 171 | public function getSchemaFromMetadata(array $classes): Schema | ||
| 172 | { | ||
| 173 | // Reminder for processed classes, used for hierarchies | ||
| 174 | $processedClasses = []; | ||
| 175 | $eventManager = $this->em->getEventManager(); | ||
| 176 | $metadataSchemaConfig = $this->schemaManager->createSchemaConfig(); | ||
| 177 | |||
| 178 | $schema = new Schema([], [], $metadataSchemaConfig); | ||
| 179 | |||
| 180 | $addedFks = []; | ||
| 181 | $blacklistedFks = []; | ||
| 182 | |||
| 183 | foreach ($classes as $class) { | ||
| 184 | if ($this->processingNotRequired($class, $processedClasses)) { | ||
| 185 | continue; | ||
| 186 | } | ||
| 187 | |||
| 188 | $table = $schema->createTable($this->quoteStrategy->getTableName($class, $this->platform)); | ||
| 189 | |||
| 190 | if ($class->isInheritanceTypeSingleTable()) { | ||
| 191 | $this->gatherColumns($class, $table); | ||
| 192 | $this->gatherRelationsSql($class, $table, $schema, $addedFks, $blacklistedFks); | ||
| 193 | |||
| 194 | // Add the discriminator column | ||
| 195 | $this->addDiscriminatorColumnDefinition($class, $table); | ||
| 196 | |||
| 197 | // Aggregate all the information from all classes in the hierarchy | ||
| 198 | foreach ($class->parentClasses as $parentClassName) { | ||
| 199 | // Parent class information is already contained in this class | ||
| 200 | $processedClasses[$parentClassName] = true; | ||
| 201 | } | ||
| 202 | |||
| 203 | foreach ($class->subClasses as $subClassName) { | ||
| 204 | $subClass = $this->em->getClassMetadata($subClassName); | ||
| 205 | $this->gatherColumns($subClass, $table); | ||
| 206 | $this->gatherRelationsSql($subClass, $table, $schema, $addedFks, $blacklistedFks); | ||
| 207 | $processedClasses[$subClassName] = true; | ||
| 208 | } | ||
| 209 | } elseif ($class->isInheritanceTypeJoined()) { | ||
| 210 | // Add all non-inherited fields as columns | ||
| 211 | foreach ($class->fieldMappings as $fieldName => $mapping) { | ||
| 212 | if (! isset($mapping->inherited)) { | ||
| 213 | $this->gatherColumn($class, $mapping, $table); | ||
| 214 | } | ||
| 215 | } | ||
| 216 | |||
| 217 | $this->gatherRelationsSql($class, $table, $schema, $addedFks, $blacklistedFks); | ||
| 218 | |||
| 219 | // Add the discriminator column only to the root table | ||
| 220 | if ($class->name === $class->rootEntityName) { | ||
| 221 | $this->addDiscriminatorColumnDefinition($class, $table); | ||
| 222 | } else { | ||
| 223 | // Add an ID FK column to child tables | ||
| 224 | $pkColumns = []; | ||
| 225 | $inheritedKeyColumns = []; | ||
| 226 | |||
| 227 | foreach ($class->identifier as $identifierField) { | ||
| 228 | if (isset($class->fieldMappings[$identifierField]->inherited)) { | ||
| 229 | $idMapping = $class->fieldMappings[$identifierField]; | ||
| 230 | $this->gatherColumn($class, $idMapping, $table); | ||
| 231 | $columnName = $this->quoteStrategy->getColumnName( | ||
| 232 | $identifierField, | ||
| 233 | $class, | ||
| 234 | $this->platform, | ||
| 235 | ); | ||
| 236 | // TODO: This seems rather hackish, can we optimize it? | ||
| 237 | $table->getColumn($columnName)->setAutoincrement(false); | ||
| 238 | |||
| 239 | $pkColumns[] = $columnName; | ||
| 240 | $inheritedKeyColumns[] = $columnName; | ||
| 241 | |||
| 242 | continue; | ||
| 243 | } | ||
| 244 | |||
| 245 | if (isset($class->associationMappings[$identifierField]->inherited)) { | ||
| 246 | $idMapping = $class->associationMappings[$identifierField]; | ||
| 247 | assert($idMapping->isToOneOwningSide()); | ||
| 248 | |||
| 249 | $targetEntity = current( | ||
| 250 | array_filter( | ||
| 251 | $classes, | ||
| 252 | static fn (ClassMetadata $class): bool => $class->name === $idMapping->targetEntity, | ||
| 253 | ), | ||
| 254 | ); | ||
| 255 | |||
| 256 | foreach ($idMapping->joinColumns as $joinColumn) { | ||
| 257 | if (isset($targetEntity->fieldMappings[$joinColumn->referencedColumnName])) { | ||
| 258 | $columnName = $this->quoteStrategy->getJoinColumnName( | ||
| 259 | $joinColumn, | ||
| 260 | $class, | ||
| 261 | $this->platform, | ||
| 262 | ); | ||
| 263 | |||
| 264 | $pkColumns[] = $columnName; | ||
| 265 | $inheritedKeyColumns[] = $columnName; | ||
| 266 | } | ||
| 267 | } | ||
| 268 | } | ||
| 269 | } | ||
| 270 | |||
| 271 | if ($inheritedKeyColumns !== []) { | ||
| 272 | // Add a FK constraint on the ID column | ||
| 273 | $table->addForeignKeyConstraint( | ||
| 274 | $this->quoteStrategy->getTableName( | ||
| 275 | $this->em->getClassMetadata($class->rootEntityName), | ||
| 276 | $this->platform, | ||
| 277 | ), | ||
| 278 | $inheritedKeyColumns, | ||
| 279 | $inheritedKeyColumns, | ||
| 280 | ['onDelete' => 'CASCADE'], | ||
| 281 | ); | ||
| 282 | } | ||
| 283 | |||
| 284 | if ($pkColumns !== []) { | ||
| 285 | $table->setPrimaryKey($pkColumns); | ||
| 286 | } | ||
| 287 | } | ||
| 288 | } else { | ||
| 289 | $this->gatherColumns($class, $table); | ||
| 290 | $this->gatherRelationsSql($class, $table, $schema, $addedFks, $blacklistedFks); | ||
| 291 | } | ||
| 292 | |||
| 293 | $pkColumns = []; | ||
| 294 | |||
| 295 | foreach ($class->identifier as $identifierField) { | ||
| 296 | if (isset($class->fieldMappings[$identifierField])) { | ||
| 297 | $pkColumns[] = $this->quoteStrategy->getColumnName($identifierField, $class, $this->platform); | ||
| 298 | } elseif (isset($class->associationMappings[$identifierField])) { | ||
| 299 | $assoc = $class->associationMappings[$identifierField]; | ||
| 300 | assert($assoc->isToOneOwningSide()); | ||
| 301 | |||
| 302 | foreach ($assoc->joinColumns as $joinColumn) { | ||
| 303 | $pkColumns[] = $this->quoteStrategy->getJoinColumnName($joinColumn, $class, $this->platform); | ||
| 304 | } | ||
| 305 | } | ||
| 306 | } | ||
| 307 | |||
| 308 | if (! $table->hasIndex('primary')) { | ||
| 309 | $table->setPrimaryKey($pkColumns); | ||
| 310 | } | ||
| 311 | |||
| 312 | // there can be unique indexes automatically created for join column | ||
| 313 | // if join column is also primary key we should keep only primary key on this column | ||
| 314 | // so, remove indexes overruled by primary key | ||
| 315 | $primaryKey = $table->getIndex('primary'); | ||
| 316 | |||
| 317 | foreach ($table->getIndexes() as $idxKey => $existingIndex) { | ||
| 318 | if ($primaryKey->overrules($existingIndex)) { | ||
| 319 | $table->dropIndex($idxKey); | ||
| 320 | } | ||
| 321 | } | ||
| 322 | |||
| 323 | if (isset($class->table['indexes'])) { | ||
| 324 | foreach ($class->table['indexes'] as $indexName => $indexData) { | ||
| 325 | if (! isset($indexData['flags'])) { | ||
| 326 | $indexData['flags'] = []; | ||
| 327 | } | ||
| 328 | |||
| 329 | $table->addIndex( | ||
| 330 | $this->getIndexColumns($class, $indexData), | ||
| 331 | is_numeric($indexName) ? null : $indexName, | ||
| 332 | (array) $indexData['flags'], | ||
| 333 | $indexData['options'] ?? [], | ||
| 334 | ); | ||
| 335 | } | ||
| 336 | } | ||
| 337 | |||
| 338 | if (isset($class->table['uniqueConstraints'])) { | ||
| 339 | foreach ($class->table['uniqueConstraints'] as $indexName => $indexData) { | ||
| 340 | $uniqIndex = new Index('tmp__' . $indexName, $this->getIndexColumns($class, $indexData), true, false, [], $indexData['options'] ?? []); | ||
| 341 | |||
| 342 | foreach ($table->getIndexes() as $tableIndexName => $tableIndex) { | ||
| 343 | if ($tableIndex->isFulfilledBy($uniqIndex)) { | ||
| 344 | $table->dropIndex($tableIndexName); | ||
| 345 | break; | ||
| 346 | } | ||
| 347 | } | ||
| 348 | |||
| 349 | $table->addUniqueIndex($uniqIndex->getColumns(), is_numeric($indexName) ? null : $indexName, $indexData['options'] ?? []); | ||
| 350 | } | ||
| 351 | } | ||
| 352 | |||
| 353 | if (isset($class->table['options'])) { | ||
| 354 | foreach ($class->table['options'] as $key => $val) { | ||
| 355 | $table->addOption($key, $val); | ||
| 356 | } | ||
| 357 | } | ||
| 358 | |||
| 359 | $processedClasses[$class->name] = true; | ||
| 360 | |||
| 361 | if ($class->isIdGeneratorSequence() && $class->name === $class->rootEntityName) { | ||
| 362 | $seqDef = $class->sequenceGeneratorDefinition; | ||
| 363 | $quotedName = $this->quoteStrategy->getSequenceName($seqDef, $class, $this->platform); | ||
| 364 | if (! $schema->hasSequence($quotedName)) { | ||
| 365 | $schema->createSequence( | ||
| 366 | $quotedName, | ||
| 367 | (int) $seqDef['allocationSize'], | ||
| 368 | (int) $seqDef['initialValue'], | ||
| 369 | ); | ||
| 370 | } | ||
| 371 | } | ||
| 372 | |||
| 373 | if ($eventManager->hasListeners(ToolEvents::postGenerateSchemaTable)) { | ||
| 374 | $eventManager->dispatchEvent( | ||
| 375 | ToolEvents::postGenerateSchemaTable, | ||
| 376 | new GenerateSchemaTableEventArgs($class, $schema, $table), | ||
| 377 | ); | ||
| 378 | } | ||
| 379 | } | ||
| 380 | |||
| 381 | if ($eventManager->hasListeners(ToolEvents::postGenerateSchema)) { | ||
| 382 | $eventManager->dispatchEvent( | ||
| 383 | ToolEvents::postGenerateSchema, | ||
| 384 | new GenerateSchemaEventArgs($this->em, $schema), | ||
| 385 | ); | ||
| 386 | } | ||
| 387 | |||
| 388 | return $schema; | ||
| 389 | } | ||
| 390 | |||
| 391 | /** | ||
| 392 | * Gets a portable column definition as required by the DBAL for the discriminator | ||
| 393 | * column of a class. | ||
| 394 | */ | ||
| 395 | private function addDiscriminatorColumnDefinition(ClassMetadata $class, Table $table): void | ||
| 396 | { | ||
| 397 | $discrColumn = $class->discriminatorColumn; | ||
| 398 | assert($discrColumn !== null); | ||
| 399 | |||
| 400 | if (strtolower($discrColumn->type) === 'string' && ! isset($discrColumn->length)) { | ||
| 401 | $discrColumn->type = 'string'; | ||
| 402 | $discrColumn->length = 255; | ||
| 403 | } | ||
| 404 | |||
| 405 | $options = [ | ||
| 406 | 'length' => $discrColumn->length ?? null, | ||
| 407 | 'notnull' => true, | ||
| 408 | ]; | ||
| 409 | |||
| 410 | if (isset($discrColumn->columnDefinition)) { | ||
| 411 | $options['columnDefinition'] = $discrColumn->columnDefinition; | ||
| 412 | } | ||
| 413 | |||
| 414 | $options = $this->gatherColumnOptions($discrColumn) + $options; | ||
| 415 | $table->addColumn($discrColumn->name, $discrColumn->type, $options); | ||
| 416 | } | ||
| 417 | |||
| 418 | /** | ||
| 419 | * Gathers the column definitions as required by the DBAL of all field mappings | ||
| 420 | * found in the given class. | ||
| 421 | */ | ||
| 422 | private function gatherColumns(ClassMetadata $class, Table $table): void | ||
| 423 | { | ||
| 424 | $pkColumns = []; | ||
| 425 | |||
| 426 | foreach ($class->fieldMappings as $mapping) { | ||
| 427 | if ($class->isInheritanceTypeSingleTable() && isset($mapping->inherited)) { | ||
| 428 | continue; | ||
| 429 | } | ||
| 430 | |||
| 431 | $this->gatherColumn($class, $mapping, $table); | ||
| 432 | |||
| 433 | if ($class->isIdentifier($mapping->fieldName)) { | ||
| 434 | $pkColumns[] = $this->quoteStrategy->getColumnName($mapping->fieldName, $class, $this->platform); | ||
| 435 | } | ||
| 436 | } | ||
| 437 | } | ||
| 438 | |||
| 439 | /** | ||
| 440 | * Creates a column definition as required by the DBAL from an ORM field mapping definition. | ||
| 441 | * | ||
| 442 | * @param ClassMetadata $class The class that owns the field mapping. | ||
| 443 | * @psalm-param FieldMapping $mapping The field mapping. | ||
| 444 | */ | ||
| 445 | private function gatherColumn( | ||
| 446 | ClassMetadata $class, | ||
| 447 | FieldMapping $mapping, | ||
| 448 | Table $table, | ||
| 449 | ): void { | ||
| 450 | $columnName = $this->quoteStrategy->getColumnName($mapping->fieldName, $class, $this->platform); | ||
| 451 | $columnType = $mapping->type; | ||
| 452 | |||
| 453 | $options = []; | ||
| 454 | $options['length'] = $mapping->length ?? null; | ||
| 455 | $options['notnull'] = isset($mapping->nullable) ? ! $mapping->nullable : true; | ||
| 456 | if ($class->isInheritanceTypeSingleTable() && $class->parentClasses) { | ||
| 457 | $options['notnull'] = false; | ||
| 458 | } | ||
| 459 | |||
| 460 | $options['platformOptions'] = []; | ||
| 461 | $options['platformOptions']['version'] = $class->isVersioned && $class->versionField === $mapping->fieldName; | ||
| 462 | |||
| 463 | if (strtolower($columnType) === 'string' && $options['length'] === null) { | ||
| 464 | $options['length'] = 255; | ||
| 465 | } | ||
| 466 | |||
| 467 | if (isset($mapping->precision)) { | ||
| 468 | $options['precision'] = $mapping->precision; | ||
| 469 | } | ||
| 470 | |||
| 471 | if (isset($mapping->scale)) { | ||
| 472 | $options['scale'] = $mapping->scale; | ||
| 473 | } | ||
| 474 | |||
| 475 | if (isset($mapping->default)) { | ||
| 476 | $options['default'] = $mapping->default; | ||
| 477 | } | ||
| 478 | |||
| 479 | if (isset($mapping->columnDefinition)) { | ||
| 480 | $options['columnDefinition'] = $mapping->columnDefinition; | ||
| 481 | } | ||
| 482 | |||
| 483 | // the 'default' option can be overwritten here | ||
| 484 | $options = $this->gatherColumnOptions($mapping) + $options; | ||
| 485 | |||
| 486 | if ($class->isIdGeneratorIdentity() && $class->getIdentifierFieldNames() === [$mapping->fieldName]) { | ||
| 487 | $options['autoincrement'] = true; | ||
| 488 | } | ||
| 489 | |||
| 490 | if ($class->isInheritanceTypeJoined() && $class->name !== $class->rootEntityName) { | ||
| 491 | $options['autoincrement'] = false; | ||
| 492 | } | ||
| 493 | |||
| 494 | if ($table->hasColumn($columnName)) { | ||
| 495 | // required in some inheritance scenarios | ||
| 496 | $table->modifyColumn($columnName, $options); | ||
| 497 | } else { | ||
| 498 | $table->addColumn($columnName, $columnType, $options); | ||
| 499 | } | ||
| 500 | |||
| 501 | $isUnique = $mapping->unique ?? false; | ||
| 502 | if ($isUnique) { | ||
| 503 | $table->addUniqueIndex([$columnName]); | ||
| 504 | } | ||
| 505 | } | ||
| 506 | |||
| 507 | /** | ||
| 508 | * Gathers the SQL for properly setting up the relations of the given class. | ||
| 509 | * This includes the SQL for foreign key constraints and join tables. | ||
| 510 | * | ||
| 511 | * @psalm-param array<string, array{ | ||
| 512 | * foreignTableName: string, | ||
| 513 | * foreignColumns: list<string> | ||
| 514 | * }> $addedFks | ||
| 515 | * @psalm-param array<string, bool> $blacklistedFks | ||
| 516 | * | ||
| 517 | * @throws NotSupported | ||
| 518 | */ | ||
| 519 | private function gatherRelationsSql( | ||
| 520 | ClassMetadata $class, | ||
| 521 | Table $table, | ||
| 522 | Schema $schema, | ||
| 523 | array &$addedFks, | ||
| 524 | array &$blacklistedFks, | ||
| 525 | ): void { | ||
| 526 | foreach ($class->associationMappings as $id => $mapping) { | ||
| 527 | if (isset($mapping->inherited) && ! in_array($id, $class->identifier, true)) { | ||
| 528 | continue; | ||
| 529 | } | ||
| 530 | |||
| 531 | $foreignClass = $this->em->getClassMetadata($mapping->targetEntity); | ||
| 532 | |||
| 533 | if ($mapping->isToOneOwningSide()) { | ||
| 534 | $primaryKeyColumns = []; // PK is unnecessary for this relation-type | ||
| 535 | |||
| 536 | $this->gatherRelationJoinColumns( | ||
| 537 | $mapping->joinColumns, | ||
| 538 | $table, | ||
| 539 | $foreignClass, | ||
| 540 | $mapping, | ||
| 541 | $primaryKeyColumns, | ||
| 542 | $addedFks, | ||
| 543 | $blacklistedFks, | ||
| 544 | ); | ||
| 545 | } elseif ($mapping instanceof ManyToManyOwningSideMapping) { | ||
| 546 | // create join table | ||
| 547 | $joinTable = $mapping->joinTable; | ||
| 548 | |||
| 549 | $theJoinTable = $schema->createTable( | ||
| 550 | $this->quoteStrategy->getJoinTableName($mapping, $foreignClass, $this->platform), | ||
| 551 | ); | ||
| 552 | |||
| 553 | foreach ($joinTable->options as $key => $val) { | ||
| 554 | $theJoinTable->addOption($key, $val); | ||
| 555 | } | ||
| 556 | |||
| 557 | $primaryKeyColumns = []; | ||
| 558 | |||
| 559 | // Build first FK constraint (relation table => source table) | ||
| 560 | $this->gatherRelationJoinColumns( | ||
| 561 | $joinTable->joinColumns, | ||
| 562 | $theJoinTable, | ||
| 563 | $class, | ||
| 564 | $mapping, | ||
| 565 | $primaryKeyColumns, | ||
| 566 | $addedFks, | ||
| 567 | $blacklistedFks, | ||
| 568 | ); | ||
| 569 | |||
| 570 | // Build second FK constraint (relation table => target table) | ||
| 571 | $this->gatherRelationJoinColumns( | ||
| 572 | $joinTable->inverseJoinColumns, | ||
| 573 | $theJoinTable, | ||
| 574 | $foreignClass, | ||
| 575 | $mapping, | ||
| 576 | $primaryKeyColumns, | ||
| 577 | $addedFks, | ||
| 578 | $blacklistedFks, | ||
| 579 | ); | ||
| 580 | |||
| 581 | $theJoinTable->setPrimaryKey($primaryKeyColumns); | ||
| 582 | } | ||
| 583 | } | ||
| 584 | } | ||
| 585 | |||
| 586 | /** | ||
| 587 | * Gets the class metadata that is responsible for the definition of the referenced column name. | ||
| 588 | * | ||
| 589 | * Previously this was a simple task, but with DDC-117 this problem is actually recursive. If its | ||
| 590 | * not a simple field, go through all identifier field names that are associations recursively and | ||
| 591 | * find that referenced column name. | ||
| 592 | * | ||
| 593 | * TODO: Is there any way to make this code more pleasing? | ||
| 594 | * | ||
| 595 | * @psalm-return array{ClassMetadata, string}|null | ||
| 596 | */ | ||
| 597 | private function getDefiningClass(ClassMetadata $class, string $referencedColumnName): array|null | ||
| 598 | { | ||
| 599 | $referencedFieldName = $class->getFieldName($referencedColumnName); | ||
| 600 | |||
| 601 | if ($class->hasField($referencedFieldName)) { | ||
| 602 | return [$class, $referencedFieldName]; | ||
| 603 | } | ||
| 604 | |||
| 605 | if (in_array($referencedColumnName, $class->getIdentifierColumnNames(), true)) { | ||
| 606 | // it seems to be an entity as foreign key | ||
| 607 | foreach ($class->getIdentifierFieldNames() as $fieldName) { | ||
| 608 | if ( | ||
| 609 | $class->hasAssociation($fieldName) | ||
| 610 | && $class->getSingleAssociationJoinColumnName($fieldName) === $referencedColumnName | ||
| 611 | ) { | ||
| 612 | return $this->getDefiningClass( | ||
| 613 | $this->em->getClassMetadata($class->associationMappings[$fieldName]->targetEntity), | ||
| 614 | $class->getSingleAssociationReferencedJoinColumnName($fieldName), | ||
| 615 | ); | ||
| 616 | } | ||
| 617 | } | ||
| 618 | } | ||
| 619 | |||
| 620 | return null; | ||
| 621 | } | ||
| 622 | |||
| 623 | /** | ||
| 624 | * Gathers columns and fk constraints that are required for one part of relationship. | ||
| 625 | * | ||
| 626 | * @psalm-param list<JoinColumnMapping> $joinColumns | ||
| 627 | * @psalm-param list<string> $primaryKeyColumns | ||
| 628 | * @psalm-param array<string, array{ | ||
| 629 | * foreignTableName: string, | ||
| 630 | * foreignColumns: list<string> | ||
| 631 | * }> $addedFks | ||
| 632 | * @psalm-param array<string,bool> $blacklistedFks | ||
| 633 | * | ||
| 634 | * @throws MissingColumnException | ||
| 635 | */ | ||
| 636 | private function gatherRelationJoinColumns( | ||
| 637 | array $joinColumns, | ||
| 638 | Table $theJoinTable, | ||
| 639 | ClassMetadata $class, | ||
| 640 | AssociationMapping $mapping, | ||
| 641 | array &$primaryKeyColumns, | ||
| 642 | array &$addedFks, | ||
| 643 | array &$blacklistedFks, | ||
| 644 | ): void { | ||
| 645 | $localColumns = []; | ||
| 646 | $foreignColumns = []; | ||
| 647 | $fkOptions = []; | ||
| 648 | $foreignTableName = $this->quoteStrategy->getTableName($class, $this->platform); | ||
| 649 | $uniqueConstraints = []; | ||
| 650 | |||
| 651 | foreach ($joinColumns as $joinColumn) { | ||
| 652 | [$definingClass, $referencedFieldName] = $this->getDefiningClass( | ||
| 653 | $class, | ||
| 654 | $joinColumn->referencedColumnName, | ||
| 655 | ); | ||
| 656 | |||
| 657 | if (! $definingClass) { | ||
| 658 | throw MissingColumnException::fromColumnSourceAndTarget( | ||
| 659 | $joinColumn->referencedColumnName, | ||
| 660 | $mapping->sourceEntity, | ||
| 661 | $mapping->targetEntity, | ||
| 662 | ); | ||
| 663 | } | ||
| 664 | |||
| 665 | $quotedColumnName = $this->quoteStrategy->getJoinColumnName($joinColumn, $class, $this->platform); | ||
| 666 | $quotedRefColumnName = $this->quoteStrategy->getReferencedJoinColumnName( | ||
| 667 | $joinColumn, | ||
| 668 | $class, | ||
| 669 | $this->platform, | ||
| 670 | ); | ||
| 671 | |||
| 672 | $primaryKeyColumns[] = $quotedColumnName; | ||
| 673 | $localColumns[] = $quotedColumnName; | ||
| 674 | $foreignColumns[] = $quotedRefColumnName; | ||
| 675 | |||
| 676 | if (! $theJoinTable->hasColumn($quotedColumnName)) { | ||
| 677 | // Only add the column to the table if it does not exist already. | ||
| 678 | // It might exist already if the foreign key is mapped into a regular | ||
| 679 | // property as well. | ||
| 680 | |||
| 681 | $fieldMapping = $definingClass->getFieldMapping($referencedFieldName); | ||
| 682 | |||
| 683 | $columnOptions = ['notnull' => false]; | ||
| 684 | |||
| 685 | if (isset($joinColumn->columnDefinition)) { | ||
| 686 | $columnOptions['columnDefinition'] = $joinColumn->columnDefinition; | ||
| 687 | } elseif (isset($fieldMapping->columnDefinition)) { | ||
| 688 | $columnOptions['columnDefinition'] = $fieldMapping->columnDefinition; | ||
| 689 | } | ||
| 690 | |||
| 691 | if (isset($joinColumn->nullable)) { | ||
| 692 | $columnOptions['notnull'] = ! $joinColumn->nullable; | ||
| 693 | } | ||
| 694 | |||
| 695 | $columnOptions += $this->gatherColumnOptions($fieldMapping); | ||
| 696 | |||
| 697 | if (isset($fieldMapping->length)) { | ||
| 698 | $columnOptions['length'] = $fieldMapping->length; | ||
| 699 | } | ||
| 700 | |||
| 701 | if ($fieldMapping->type === 'decimal') { | ||
| 702 | $columnOptions['scale'] = $fieldMapping->scale; | ||
| 703 | $columnOptions['precision'] = $fieldMapping->precision; | ||
| 704 | } | ||
| 705 | |||
| 706 | $columnOptions = $this->gatherColumnOptions($joinColumn) + $columnOptions; | ||
| 707 | |||
| 708 | $theJoinTable->addColumn($quotedColumnName, $fieldMapping->type, $columnOptions); | ||
| 709 | } | ||
| 710 | |||
| 711 | if (isset($joinColumn->unique) && $joinColumn->unique === true) { | ||
| 712 | $uniqueConstraints[] = ['columns' => [$quotedColumnName]]; | ||
| 713 | } | ||
| 714 | |||
| 715 | if (isset($joinColumn->onDelete)) { | ||
| 716 | $fkOptions['onDelete'] = $joinColumn->onDelete; | ||
| 717 | } | ||
| 718 | } | ||
| 719 | |||
| 720 | // Prefer unique constraints over implicit simple indexes created for foreign keys. | ||
| 721 | // Also avoids index duplication. | ||
| 722 | foreach ($uniqueConstraints as $indexName => $unique) { | ||
| 723 | $theJoinTable->addUniqueIndex($unique['columns'], is_numeric($indexName) ? null : $indexName); | ||
| 724 | } | ||
| 725 | |||
| 726 | $compositeName = $theJoinTable->getName() . '.' . implode('', $localColumns); | ||
| 727 | if ( | ||
| 728 | isset($addedFks[$compositeName]) | ||
| 729 | && ($foreignTableName !== $addedFks[$compositeName]['foreignTableName'] | ||
| 730 | || 0 < count(array_diff($foreignColumns, $addedFks[$compositeName]['foreignColumns']))) | ||
| 731 | ) { | ||
| 732 | foreach ($theJoinTable->getForeignKeys() as $fkName => $key) { | ||
| 733 | if ( | ||
| 734 | count(array_diff($key->getLocalColumns(), $localColumns)) === 0 | ||
| 735 | && (($key->getForeignTableName() !== $foreignTableName) | ||
| 736 | || 0 < count(array_diff($key->getForeignColumns(), $foreignColumns))) | ||
| 737 | ) { | ||
| 738 | $theJoinTable->removeForeignKey($fkName); | ||
| 739 | break; | ||
| 740 | } | ||
| 741 | } | ||
| 742 | |||
| 743 | $blacklistedFks[$compositeName] = true; | ||
| 744 | } elseif (! isset($blacklistedFks[$compositeName])) { | ||
| 745 | $addedFks[$compositeName] = ['foreignTableName' => $foreignTableName, 'foreignColumns' => $foreignColumns]; | ||
| 746 | $theJoinTable->addForeignKeyConstraint( | ||
| 747 | $foreignTableName, | ||
| 748 | $localColumns, | ||
| 749 | $foreignColumns, | ||
| 750 | $fkOptions, | ||
| 751 | ); | ||
| 752 | } | ||
| 753 | } | ||
| 754 | |||
| 755 | /** @return mixed[] */ | ||
| 756 | private function gatherColumnOptions(JoinColumnMapping|FieldMapping|DiscriminatorColumnMapping $mapping): array | ||
| 757 | { | ||
| 758 | $mappingOptions = $mapping->options ?? []; | ||
| 759 | |||
| 760 | if (isset($mapping->enumType)) { | ||
| 761 | $mappingOptions['enumType'] = $mapping->enumType; | ||
| 762 | } | ||
| 763 | |||
| 764 | if (($mappingOptions['default'] ?? null) instanceof BackedEnum) { | ||
| 765 | $mappingOptions['default'] = $mappingOptions['default']->value; | ||
| 766 | } | ||
| 767 | |||
| 768 | if (empty($mappingOptions)) { | ||
| 769 | return []; | ||
| 770 | } | ||
| 771 | |||
| 772 | $options = array_intersect_key($mappingOptions, array_flip(self::KNOWN_COLUMN_OPTIONS)); | ||
| 773 | $options['platformOptions'] = array_diff_key($mappingOptions, $options); | ||
| 774 | |||
| 775 | return $options; | ||
| 776 | } | ||
| 777 | |||
| 778 | /** | ||
| 779 | * Drops the database schema for the given classes. | ||
| 780 | * | ||
| 781 | * In any way when an exception is thrown it is suppressed since drop was | ||
| 782 | * issued for all classes of the schema and some probably just don't exist. | ||
| 783 | * | ||
| 784 | * @psalm-param list<ClassMetadata> $classes | ||
| 785 | */ | ||
| 786 | public function dropSchema(array $classes): void | ||
| 787 | { | ||
| 788 | $dropSchemaSql = $this->getDropSchemaSQL($classes); | ||
| 789 | $conn = $this->em->getConnection(); | ||
| 790 | |||
| 791 | foreach ($dropSchemaSql as $sql) { | ||
| 792 | try { | ||
| 793 | $conn->executeStatement($sql); | ||
| 794 | } catch (Throwable) { | ||
| 795 | // ignored | ||
| 796 | } | ||
| 797 | } | ||
| 798 | } | ||
| 799 | |||
| 800 | /** | ||
| 801 | * Drops all elements in the database of the current connection. | ||
| 802 | */ | ||
| 803 | public function dropDatabase(): void | ||
| 804 | { | ||
| 805 | $dropSchemaSql = $this->getDropDatabaseSQL(); | ||
| 806 | $conn = $this->em->getConnection(); | ||
| 807 | |||
| 808 | foreach ($dropSchemaSql as $sql) { | ||
| 809 | $conn->executeStatement($sql); | ||
| 810 | } | ||
| 811 | } | ||
| 812 | |||
| 813 | /** | ||
| 814 | * Gets the SQL needed to drop the database schema for the connections database. | ||
| 815 | * | ||
| 816 | * @return list<string> | ||
| 817 | */ | ||
| 818 | public function getDropDatabaseSQL(): array | ||
| 819 | { | ||
| 820 | return $this->schemaManager | ||
| 821 | ->introspectSchema() | ||
| 822 | ->toDropSql($this->platform); | ||
| 823 | } | ||
| 824 | |||
| 825 | /** | ||
| 826 | * Gets SQL to drop the tables defined by the passed classes. | ||
| 827 | * | ||
| 828 | * @psalm-param list<ClassMetadata> $classes | ||
| 829 | * | ||
| 830 | * @return list<string> | ||
| 831 | */ | ||
| 832 | public function getDropSchemaSQL(array $classes): array | ||
| 833 | { | ||
| 834 | $schema = $this->getSchemaFromMetadata($classes); | ||
| 835 | |||
| 836 | $deployedSchema = $this->schemaManager->introspectSchema(); | ||
| 837 | |||
| 838 | foreach ($schema->getTables() as $table) { | ||
| 839 | if (! $deployedSchema->hasTable($table->getName())) { | ||
| 840 | $schema->dropTable($table->getName()); | ||
| 841 | } | ||
| 842 | } | ||
| 843 | |||
| 844 | if ($this->platform->supportsSequences()) { | ||
| 845 | foreach ($schema->getSequences() as $sequence) { | ||
| 846 | if (! $deployedSchema->hasSequence($sequence->getName())) { | ||
| 847 | $schema->dropSequence($sequence->getName()); | ||
| 848 | } | ||
| 849 | } | ||
| 850 | |||
| 851 | foreach ($schema->getTables() as $table) { | ||
| 852 | $primaryKey = $table->getPrimaryKey(); | ||
| 853 | if ($primaryKey === null) { | ||
| 854 | continue; | ||
| 855 | } | ||
| 856 | |||
| 857 | $columns = $primaryKey->getColumns(); | ||
| 858 | if (count($columns) === 1) { | ||
| 859 | $checkSequence = $table->getName() . '_' . $columns[0] . '_seq'; | ||
| 860 | if ($deployedSchema->hasSequence($checkSequence) && ! $schema->hasSequence($checkSequence)) { | ||
| 861 | $schema->createSequence($checkSequence); | ||
| 862 | } | ||
| 863 | } | ||
| 864 | } | ||
| 865 | } | ||
| 866 | |||
| 867 | return $schema->toDropSql($this->platform); | ||
| 868 | } | ||
| 869 | |||
| 870 | /** | ||
| 871 | * Updates the database schema of the given classes by comparing the ClassMetadata | ||
| 872 | * instances to the current database schema that is inspected. | ||
| 873 | * | ||
| 874 | * @param mixed[] $classes | ||
| 875 | */ | ||
| 876 | public function updateSchema(array $classes): void | ||
| 877 | { | ||
| 878 | $conn = $this->em->getConnection(); | ||
| 879 | |||
| 880 | foreach ($this->getUpdateSchemaSql($classes) as $sql) { | ||
| 881 | $conn->executeStatement($sql); | ||
| 882 | } | ||
| 883 | } | ||
| 884 | |||
| 885 | /** | ||
| 886 | * Gets the sequence of SQL statements that need to be performed in order | ||
| 887 | * to bring the given class mappings in-synch with the relational schema. | ||
| 888 | * | ||
| 889 | * @param list<ClassMetadata> $classes The classes to consider. | ||
| 890 | * | ||
| 891 | * @return list<string> The sequence of SQL statements. | ||
| 892 | */ | ||
| 893 | public function getUpdateSchemaSql(array $classes): array | ||
| 894 | { | ||
| 895 | $toSchema = $this->getSchemaFromMetadata($classes); | ||
| 896 | $fromSchema = $this->createSchemaForComparison($toSchema); | ||
| 897 | $comparator = $this->schemaManager->createComparator(); | ||
| 898 | $schemaDiff = $comparator->compareSchemas($fromSchema, $toSchema); | ||
| 899 | |||
| 900 | return $this->platform->getAlterSchemaSQL($schemaDiff); | ||
| 901 | } | ||
| 902 | |||
| 903 | /** | ||
| 904 | * Creates the schema from the database, ensuring tables from the target schema are whitelisted for comparison. | ||
| 905 | */ | ||
| 906 | private function createSchemaForComparison(Schema $toSchema): Schema | ||
| 907 | { | ||
| 908 | $connection = $this->em->getConnection(); | ||
| 909 | |||
| 910 | // backup schema assets filter | ||
| 911 | $config = $connection->getConfiguration(); | ||
| 912 | $previousFilter = $config->getSchemaAssetsFilter(); | ||
| 913 | |||
| 914 | if ($previousFilter === null) { | ||
| 915 | return $this->schemaManager->introspectSchema(); | ||
| 916 | } | ||
| 917 | |||
| 918 | // whitelist assets we already know about in $toSchema, use the existing filter otherwise | ||
| 919 | $config->setSchemaAssetsFilter(static function ($asset) use ($previousFilter, $toSchema): bool { | ||
| 920 | $assetName = $asset instanceof AbstractAsset ? $asset->getName() : $asset; | ||
| 921 | |||
| 922 | return $toSchema->hasTable($assetName) || $toSchema->hasSequence($assetName) || $previousFilter($asset); | ||
| 923 | }); | ||
| 924 | |||
| 925 | try { | ||
| 926 | return $this->schemaManager->introspectSchema(); | ||
| 927 | } finally { | ||
| 928 | // restore schema assets filter | ||
| 929 | $config->setSchemaAssetsFilter($previousFilter); | ||
| 930 | } | ||
| 931 | } | ||
| 932 | } | ||
diff --git a/vendor/doctrine/orm/src/Tools/SchemaValidator.php b/vendor/doctrine/orm/src/Tools/SchemaValidator.php new file mode 100644 index 0000000..fdfc003 --- /dev/null +++ b/vendor/doctrine/orm/src/Tools/SchemaValidator.php | |||
| @@ -0,0 +1,443 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Tools; | ||
| 6 | |||
| 7 | use BackedEnum; | ||
| 8 | use Doctrine\DBAL\Types\AsciiStringType; | ||
| 9 | use Doctrine\DBAL\Types\BigIntType; | ||
| 10 | use Doctrine\DBAL\Types\BooleanType; | ||
| 11 | use Doctrine\DBAL\Types\DecimalType; | ||
| 12 | use Doctrine\DBAL\Types\FloatType; | ||
| 13 | use Doctrine\DBAL\Types\GuidType; | ||
| 14 | use Doctrine\DBAL\Types\IntegerType; | ||
| 15 | use Doctrine\DBAL\Types\JsonType; | ||
| 16 | use Doctrine\DBAL\Types\SimpleArrayType; | ||
| 17 | use Doctrine\DBAL\Types\SmallIntType; | ||
| 18 | use Doctrine\DBAL\Types\StringType; | ||
| 19 | use Doctrine\DBAL\Types\TextType; | ||
| 20 | use Doctrine\DBAL\Types\Type; | ||
| 21 | use Doctrine\ORM\EntityManagerInterface; | ||
| 22 | use Doctrine\ORM\Mapping\ClassMetadata; | ||
| 23 | use Doctrine\ORM\Mapping\FieldMapping; | ||
| 24 | use ReflectionEnum; | ||
| 25 | use ReflectionNamedType; | ||
| 26 | |||
| 27 | use function array_diff; | ||
| 28 | use function array_filter; | ||
| 29 | use function array_key_exists; | ||
| 30 | use function array_map; | ||
| 31 | use function array_push; | ||
| 32 | use function array_search; | ||
| 33 | use function array_values; | ||
| 34 | use function assert; | ||
| 35 | use function class_exists; | ||
| 36 | use function class_parents; | ||
| 37 | use function count; | ||
| 38 | use function implode; | ||
| 39 | use function in_array; | ||
| 40 | use function interface_exists; | ||
| 41 | use function is_a; | ||
| 42 | use function sprintf; | ||
| 43 | |||
| 44 | /** | ||
| 45 | * Performs strict validation of the mapping schema | ||
| 46 | * | ||
| 47 | * @link www.doctrine-project.com | ||
| 48 | */ | ||
| 49 | class SchemaValidator | ||
| 50 | { | ||
| 51 | /** | ||
| 52 | * It maps built-in Doctrine types to PHP types | ||
| 53 | */ | ||
| 54 | private const BUILTIN_TYPES_MAP = [ | ||
| 55 | AsciiStringType::class => ['string'], | ||
| 56 | BigIntType::class => ['int', 'string'], | ||
| 57 | BooleanType::class => ['bool'], | ||
| 58 | DecimalType::class => ['string'], | ||
| 59 | FloatType::class => ['float'], | ||
| 60 | GuidType::class => ['string'], | ||
| 61 | IntegerType::class => ['int'], | ||
| 62 | JsonType::class => ['array'], | ||
| 63 | SimpleArrayType::class => ['array'], | ||
| 64 | SmallIntType::class => ['int'], | ||
| 65 | StringType::class => ['string'], | ||
| 66 | TextType::class => ['string'], | ||
| 67 | ]; | ||
| 68 | |||
| 69 | public function __construct( | ||
| 70 | private readonly EntityManagerInterface $em, | ||
| 71 | private readonly bool $validatePropertyTypes = true, | ||
| 72 | ) { | ||
| 73 | } | ||
| 74 | |||
| 75 | /** | ||
| 76 | * Checks the internal consistency of all mapping files. | ||
| 77 | * | ||
| 78 | * There are several checks that can't be done at runtime or are too expensive, which can be verified | ||
| 79 | * with this command. For example: | ||
| 80 | * | ||
| 81 | * 1. Check if a relation with "mappedBy" is actually connected to that specified field. | ||
| 82 | * 2. Check if "mappedBy" and "inversedBy" are consistent to each other. | ||
| 83 | * 3. Check if "referencedColumnName" attributes are really pointing to primary key columns. | ||
| 84 | * | ||
| 85 | * @psalm-return array<string, list<string>> | ||
| 86 | */ | ||
| 87 | public function validateMapping(): array | ||
| 88 | { | ||
| 89 | $errors = []; | ||
| 90 | $cmf = $this->em->getMetadataFactory(); | ||
| 91 | $classes = $cmf->getAllMetadata(); | ||
| 92 | |||
| 93 | foreach ($classes as $class) { | ||
| 94 | $ce = $this->validateClass($class); | ||
| 95 | if ($ce) { | ||
| 96 | $errors[$class->name] = $ce; | ||
| 97 | } | ||
| 98 | } | ||
| 99 | |||
| 100 | return $errors; | ||
| 101 | } | ||
| 102 | |||
| 103 | /** | ||
| 104 | * Validates a single class of the current. | ||
| 105 | * | ||
| 106 | * @return string[] | ||
| 107 | * @psalm-return list<string> | ||
| 108 | */ | ||
| 109 | public function validateClass(ClassMetadata $class): array | ||
| 110 | { | ||
| 111 | $ce = []; | ||
| 112 | $cmf = $this->em->getMetadataFactory(); | ||
| 113 | |||
| 114 | foreach ($class->fieldMappings as $fieldName => $mapping) { | ||
| 115 | if (! Type::hasType($mapping->type)) { | ||
| 116 | $ce[] = "The field '" . $class->name . '#' . $fieldName . "' uses a non-existent type '" . $mapping->type . "'."; | ||
| 117 | } | ||
| 118 | } | ||
| 119 | |||
| 120 | if ($this->validatePropertyTypes) { | ||
| 121 | array_push($ce, ...$this->validatePropertiesTypes($class)); | ||
| 122 | } | ||
| 123 | |||
| 124 | foreach ($class->associationMappings as $fieldName => $assoc) { | ||
| 125 | if (! class_exists($assoc->targetEntity) || $cmf->isTransient($assoc->targetEntity)) { | ||
| 126 | $ce[] = "The target entity '" . $assoc->targetEntity . "' specified on " . $class->name . '#' . $fieldName . ' is unknown or not an entity.'; | ||
| 127 | |||
| 128 | return $ce; | ||
| 129 | } | ||
| 130 | |||
| 131 | $targetMetadata = $cmf->getMetadataFor($assoc->targetEntity); | ||
| 132 | |||
| 133 | if ($targetMetadata->isMappedSuperclass) { | ||
| 134 | $ce[] = "The target entity '" . $assoc->targetEntity . "' specified on " . $class->name . '#' . $fieldName . ' is a mapped superclass. This is not possible since there is no table that a foreign key could refer to.'; | ||
| 135 | |||
| 136 | return $ce; | ||
| 137 | } | ||
| 138 | |||
| 139 | if (isset($assoc->id) && $targetMetadata->containsForeignIdentifier) { | ||
| 140 | $ce[] = "Cannot map association '" . $class->name . '#' . $fieldName . ' as identifier, because ' . | ||
| 141 | "the target entity '" . $targetMetadata->name . "' also maps an association as identifier."; | ||
| 142 | } | ||
| 143 | |||
| 144 | if (! $assoc->isOwningSide()) { | ||
| 145 | if ($targetMetadata->hasField($assoc->mappedBy)) { | ||
| 146 | $ce[] = 'The association ' . $class->name . '#' . $fieldName . ' refers to the owning side ' . | ||
| 147 | 'field ' . $assoc->targetEntity . '#' . $assoc->mappedBy . ' which is not defined as association, but as field.'; | ||
| 148 | } | ||
| 149 | |||
| 150 | if (! $targetMetadata->hasAssociation($assoc->mappedBy)) { | ||
| 151 | $ce[] = 'The association ' . $class->name . '#' . $fieldName . ' refers to the owning side ' . | ||
| 152 | 'field ' . $assoc->targetEntity . '#' . $assoc->mappedBy . ' which does not exist.'; | ||
| 153 | } elseif ($targetMetadata->associationMappings[$assoc->mappedBy]->inversedBy === null) { | ||
| 154 | $ce[] = 'The field ' . $class->name . '#' . $fieldName . ' is on the inverse side of a ' . | ||
| 155 | 'bi-directional relationship, but the specified mappedBy association on the target-entity ' . | ||
| 156 | $assoc->targetEntity . '#' . $assoc->mappedBy . ' does not contain the required ' . | ||
| 157 | "'inversedBy=\"" . $fieldName . "\"' attribute."; | ||
| 158 | } elseif ($targetMetadata->associationMappings[$assoc->mappedBy]->inversedBy !== $fieldName) { | ||
| 159 | $ce[] = 'The mappings ' . $class->name . '#' . $fieldName . ' and ' . | ||
| 160 | $assoc->targetEntity . '#' . $assoc->mappedBy . ' are ' . | ||
| 161 | 'inconsistent with each other.'; | ||
| 162 | } | ||
| 163 | } | ||
| 164 | |||
| 165 | if ($assoc->isOwningSide() && $assoc->inversedBy !== null) { | ||
| 166 | if ($targetMetadata->hasField($assoc->inversedBy)) { | ||
| 167 | $ce[] = 'The association ' . $class->name . '#' . $fieldName . ' refers to the inverse side ' . | ||
| 168 | 'field ' . $assoc->targetEntity . '#' . $assoc->inversedBy . ' which is not defined as association.'; | ||
| 169 | } | ||
| 170 | |||
| 171 | if (! $targetMetadata->hasAssociation($assoc->inversedBy)) { | ||
| 172 | $ce[] = 'The association ' . $class->name . '#' . $fieldName . ' refers to the inverse side ' . | ||
| 173 | 'field ' . $assoc->targetEntity . '#' . $assoc->inversedBy . ' which does not exist.'; | ||
| 174 | } elseif ($targetMetadata->associationMappings[$assoc->inversedBy]->isOwningSide()) { | ||
| 175 | $ce[] = 'The field ' . $class->name . '#' . $fieldName . ' is on the owning side of a ' . | ||
| 176 | 'bi-directional relationship, but the specified inversedBy association on the target-entity ' . | ||
| 177 | $assoc->targetEntity . '#' . $assoc->inversedBy . ' does not contain the required ' . | ||
| 178 | "'mappedBy=\"" . $fieldName . "\"' attribute."; | ||
| 179 | } elseif ($targetMetadata->associationMappings[$assoc->inversedBy]->mappedBy !== $fieldName) { | ||
| 180 | $ce[] = 'The mappings ' . $class->name . '#' . $fieldName . ' and ' . | ||
| 181 | $assoc->targetEntity . '#' . $assoc->inversedBy . ' are ' . | ||
| 182 | 'inconsistent with each other.'; | ||
| 183 | } | ||
| 184 | |||
| 185 | // Verify inverse side/owning side match each other | ||
| 186 | if (array_key_exists($assoc->inversedBy, $targetMetadata->associationMappings)) { | ||
| 187 | $targetAssoc = $targetMetadata->associationMappings[$assoc->inversedBy]; | ||
| 188 | if ($assoc->isOneToOne() && ! $targetAssoc->isOneToOne()) { | ||
| 189 | $ce[] = 'If association ' . $class->name . '#' . $fieldName . ' is one-to-one, then the inversed ' . | ||
| 190 | 'side ' . $targetMetadata->name . '#' . $assoc->inversedBy . ' has to be one-to-one as well.'; | ||
| 191 | } elseif ($assoc->isManyToOne() && ! $targetAssoc->isOneToMany()) { | ||
| 192 | $ce[] = 'If association ' . $class->name . '#' . $fieldName . ' is many-to-one, then the inversed ' . | ||
| 193 | 'side ' . $targetMetadata->name . '#' . $assoc->inversedBy . ' has to be one-to-many.'; | ||
| 194 | } elseif ($assoc->isManyToMany() && ! $targetAssoc->isManyToMany()) { | ||
| 195 | $ce[] = 'If association ' . $class->name . '#' . $fieldName . ' is many-to-many, then the inversed ' . | ||
| 196 | 'side ' . $targetMetadata->name . '#' . $assoc->inversedBy . ' has to be many-to-many as well.'; | ||
| 197 | } | ||
| 198 | } | ||
| 199 | } | ||
| 200 | |||
| 201 | if ($assoc->isOwningSide()) { | ||
| 202 | if ($assoc->isManyToManyOwningSide()) { | ||
| 203 | $identifierColumns = $class->getIdentifierColumnNames(); | ||
| 204 | foreach ($assoc->joinTable->joinColumns as $joinColumn) { | ||
| 205 | if (! in_array($joinColumn->referencedColumnName, $identifierColumns, true)) { | ||
| 206 | $ce[] = "The referenced column name '" . $joinColumn->referencedColumnName . "' " . | ||
| 207 | "has to be a primary key column on the target entity class '" . $class->name . "'."; | ||
| 208 | break; | ||
| 209 | } | ||
| 210 | } | ||
| 211 | |||
| 212 | $identifierColumns = $targetMetadata->getIdentifierColumnNames(); | ||
| 213 | foreach ($assoc->joinTable->inverseJoinColumns as $inverseJoinColumn) { | ||
| 214 | if (! in_array($inverseJoinColumn->referencedColumnName, $identifierColumns, true)) { | ||
| 215 | $ce[] = "The referenced column name '" . $inverseJoinColumn->referencedColumnName . "' " . | ||
| 216 | "has to be a primary key column on the target entity class '" . $targetMetadata->name . "'."; | ||
| 217 | break; | ||
| 218 | } | ||
| 219 | } | ||
| 220 | |||
| 221 | if (count($targetMetadata->getIdentifierColumnNames()) !== count($assoc->joinTable->inverseJoinColumns)) { | ||
| 222 | $ce[] = "The inverse join columns of the many-to-many table '" . $assoc->joinTable->name . "' " . | ||
| 223 | "have to contain to ALL identifier columns of the target entity '" . $targetMetadata->name . "', " . | ||
| 224 | "however '" . implode(', ', array_diff($targetMetadata->getIdentifierColumnNames(), array_values($assoc->relationToTargetKeyColumns))) . | ||
| 225 | "' are missing."; | ||
| 226 | } | ||
| 227 | |||
| 228 | if (count($class->getIdentifierColumnNames()) !== count($assoc->joinTable->joinColumns)) { | ||
| 229 | $ce[] = "The join columns of the many-to-many table '" . $assoc->joinTable->name . "' " . | ||
| 230 | "have to contain to ALL identifier columns of the source entity '" . $class->name . "', " . | ||
| 231 | "however '" . implode(', ', array_diff($class->getIdentifierColumnNames(), array_values($assoc->relationToSourceKeyColumns))) . | ||
| 232 | "' are missing."; | ||
| 233 | } | ||
| 234 | } elseif ($assoc->isToOneOwningSide()) { | ||
| 235 | $identifierColumns = $targetMetadata->getIdentifierColumnNames(); | ||
| 236 | foreach ($assoc->joinColumns as $joinColumn) { | ||
| 237 | if (! in_array($joinColumn->referencedColumnName, $identifierColumns, true)) { | ||
| 238 | $ce[] = "The referenced column name '" . $joinColumn->referencedColumnName . "' " . | ||
| 239 | "has to be a primary key column on the target entity class '" . $targetMetadata->name . "'."; | ||
| 240 | } | ||
| 241 | } | ||
| 242 | |||
| 243 | if (count($identifierColumns) !== count($assoc->joinColumns)) { | ||
| 244 | $ids = []; | ||
| 245 | |||
| 246 | foreach ($assoc->joinColumns as $joinColumn) { | ||
| 247 | $ids[] = $joinColumn->name; | ||
| 248 | } | ||
| 249 | |||
| 250 | $ce[] = "The join columns of the association '" . $assoc->fieldName . "' " . | ||
| 251 | "have to match to ALL identifier columns of the target entity '" . $targetMetadata->name . "', " . | ||
| 252 | "however '" . implode(', ', array_diff($targetMetadata->getIdentifierColumnNames(), $ids)) . | ||
| 253 | "' are missing."; | ||
| 254 | } | ||
| 255 | } | ||
| 256 | } | ||
| 257 | |||
| 258 | if ($assoc->isOrdered()) { | ||
| 259 | foreach ($assoc->orderBy() as $orderField => $orientation) { | ||
| 260 | if (! $targetMetadata->hasField($orderField) && ! $targetMetadata->hasAssociation($orderField)) { | ||
| 261 | $ce[] = 'The association ' . $class->name . '#' . $fieldName . ' is ordered by a foreign field ' . | ||
| 262 | $orderField . ' that is not a field on the target entity ' . $targetMetadata->name . '.'; | ||
| 263 | continue; | ||
| 264 | } | ||
| 265 | |||
| 266 | if ($targetMetadata->isCollectionValuedAssociation($orderField)) { | ||
| 267 | $ce[] = 'The association ' . $class->name . '#' . $fieldName . ' is ordered by a field ' . | ||
| 268 | $orderField . ' on ' . $targetMetadata->name . ' that is a collection-valued association.'; | ||
| 269 | continue; | ||
| 270 | } | ||
| 271 | |||
| 272 | if ($targetMetadata->isAssociationInverseSide($orderField)) { | ||
| 273 | $ce[] = 'The association ' . $class->name . '#' . $fieldName . ' is ordered by a field ' . | ||
| 274 | $orderField . ' on ' . $targetMetadata->name . ' that is the inverse side of an association.'; | ||
| 275 | continue; | ||
| 276 | } | ||
| 277 | } | ||
| 278 | } | ||
| 279 | } | ||
| 280 | |||
| 281 | if ( | ||
| 282 | ! $class->isInheritanceTypeNone() | ||
| 283 | && ! $class->isRootEntity() | ||
| 284 | && ($class->reflClass !== null && ! $class->reflClass->isAbstract()) | ||
| 285 | && ! $class->isMappedSuperclass | ||
| 286 | && array_search($class->name, $class->discriminatorMap, true) === false | ||
| 287 | ) { | ||
| 288 | $ce[] = "Entity class '" . $class->name . "' is part of inheritance hierarchy, but is " . | ||
| 289 | "not mapped in the root entity '" . $class->rootEntityName . "' discriminator map. " . | ||
| 290 | 'All subclasses must be listed in the discriminator map.'; | ||
| 291 | } | ||
| 292 | |||
| 293 | foreach ($class->subClasses as $subClass) { | ||
| 294 | if (! in_array($class->name, class_parents($subClass), true)) { | ||
| 295 | $ce[] = "According to the discriminator map class '" . $subClass . "' has to be a child " . | ||
| 296 | "of '" . $class->name . "' but these entities are not related through inheritance."; | ||
| 297 | } | ||
| 298 | } | ||
| 299 | |||
| 300 | return $ce; | ||
| 301 | } | ||
| 302 | |||
| 303 | /** | ||
| 304 | * Checks if the Database Schema is in sync with the current metadata state. | ||
| 305 | */ | ||
| 306 | public function schemaInSyncWithMetadata(): bool | ||
| 307 | { | ||
| 308 | return count($this->getUpdateSchemaList()) === 0; | ||
| 309 | } | ||
| 310 | |||
| 311 | /** | ||
| 312 | * Returns the list of missing Database Schema updates. | ||
| 313 | * | ||
| 314 | * @return array<string> | ||
| 315 | */ | ||
| 316 | public function getUpdateSchemaList(): array | ||
| 317 | { | ||
| 318 | $schemaTool = new SchemaTool($this->em); | ||
| 319 | |||
| 320 | $allMetadata = $this->em->getMetadataFactory()->getAllMetadata(); | ||
| 321 | |||
| 322 | return $schemaTool->getUpdateSchemaSql($allMetadata); | ||
| 323 | } | ||
| 324 | |||
| 325 | /** @return list<string> containing the found issues */ | ||
| 326 | private function validatePropertiesTypes(ClassMetadata $class): array | ||
| 327 | { | ||
| 328 | return array_values( | ||
| 329 | array_filter( | ||
| 330 | array_map( | ||
| 331 | function (FieldMapping $fieldMapping) use ($class): string|null { | ||
| 332 | $fieldName = $fieldMapping->fieldName; | ||
| 333 | assert(isset($class->reflFields[$fieldName])); | ||
| 334 | $propertyType = $class->reflFields[$fieldName]->getType(); | ||
| 335 | |||
| 336 | // If the field type is not a built-in type, we cannot check it | ||
| 337 | if (! Type::hasType($fieldMapping->type)) { | ||
| 338 | return null; | ||
| 339 | } | ||
| 340 | |||
| 341 | // If the property type is not a named type, we cannot check it | ||
| 342 | if (! ($propertyType instanceof ReflectionNamedType) || $propertyType->getName() === 'mixed') { | ||
| 343 | return null; | ||
| 344 | } | ||
| 345 | |||
| 346 | $metadataFieldType = $this->findBuiltInType(Type::getType($fieldMapping->type)); | ||
| 347 | |||
| 348 | //If the metadata field type is not a mapped built-in type, we cannot check it | ||
| 349 | if ($metadataFieldType === null) { | ||
| 350 | return null; | ||
| 351 | } | ||
| 352 | |||
| 353 | $propertyType = $propertyType->getName(); | ||
| 354 | |||
| 355 | // If the property type is the same as the metadata field type, we are ok | ||
| 356 | if (in_array($propertyType, $metadataFieldType, true)) { | ||
| 357 | return null; | ||
| 358 | } | ||
| 359 | |||
| 360 | if (is_a($propertyType, BackedEnum::class, true)) { | ||
| 361 | $backingType = (string) (new ReflectionEnum($propertyType))->getBackingType(); | ||
| 362 | |||
| 363 | if (! in_array($backingType, $metadataFieldType, true)) { | ||
| 364 | return sprintf( | ||
| 365 | "The field '%s#%s' has the property type '%s' with a backing type of '%s' that differs from the metadata field type '%s'.", | ||
| 366 | $class->name, | ||
| 367 | $fieldName, | ||
| 368 | $propertyType, | ||
| 369 | $backingType, | ||
| 370 | implode('|', $metadataFieldType), | ||
| 371 | ); | ||
| 372 | } | ||
| 373 | |||
| 374 | if (! isset($fieldMapping->enumType) || $propertyType === $fieldMapping->enumType) { | ||
| 375 | return null; | ||
| 376 | } | ||
| 377 | |||
| 378 | return sprintf( | ||
| 379 | "The field '%s#%s' has the property type '%s' that differs from the metadata enumType '%s'.", | ||
| 380 | $class->name, | ||
| 381 | $fieldName, | ||
| 382 | $propertyType, | ||
| 383 | $fieldMapping->enumType, | ||
| 384 | ); | ||
| 385 | } | ||
| 386 | |||
| 387 | if ( | ||
| 388 | isset($fieldMapping->enumType) | ||
| 389 | && $propertyType !== $fieldMapping->enumType | ||
| 390 | && interface_exists($propertyType) | ||
| 391 | && is_a($fieldMapping->enumType, $propertyType, true) | ||
| 392 | ) { | ||
| 393 | $backingType = (string) (new ReflectionEnum($fieldMapping->enumType))->getBackingType(); | ||
| 394 | |||
| 395 | if (in_array($backingType, $metadataFieldType, true)) { | ||
| 396 | return null; | ||
| 397 | } | ||
| 398 | |||
| 399 | return sprintf( | ||
| 400 | "The field '%s#%s' has the metadata enumType '%s' with a backing type of '%s' that differs from the metadata field type '%s'.", | ||
| 401 | $class->name, | ||
| 402 | $fieldName, | ||
| 403 | $fieldMapping->enumType, | ||
| 404 | $backingType, | ||
| 405 | implode('|', $metadataFieldType), | ||
| 406 | ); | ||
| 407 | } | ||
| 408 | |||
| 409 | if ( | ||
| 410 | $fieldMapping->type === 'json' | ||
| 411 | && in_array($propertyType, ['string', 'int', 'float', 'bool', 'true', 'false', 'null'], true) | ||
| 412 | ) { | ||
| 413 | return null; | ||
| 414 | } | ||
| 415 | |||
| 416 | return sprintf( | ||
| 417 | "The field '%s#%s' has the property type '%s' that differs from the metadata field type '%s' returned by the '%s' DBAL type.", | ||
| 418 | $class->name, | ||
| 419 | $fieldName, | ||
| 420 | $propertyType, | ||
| 421 | implode('|', $metadataFieldType), | ||
| 422 | $fieldMapping->type, | ||
| 423 | ); | ||
| 424 | }, | ||
| 425 | $class->fieldMappings, | ||
| 426 | ), | ||
| 427 | ), | ||
| 428 | ); | ||
| 429 | } | ||
| 430 | |||
| 431 | /** | ||
| 432 | * The exact DBAL type must be used (no subclasses), since consumers of doctrine/orm may have their own | ||
| 433 | * customization around field types. | ||
| 434 | * | ||
| 435 | * @return list<string>|null | ||
| 436 | */ | ||
| 437 | private function findBuiltInType(Type $type): array|null | ||
| 438 | { | ||
| 439 | $typeName = $type::class; | ||
| 440 | |||
| 441 | return self::BUILTIN_TYPES_MAP[$typeName] ?? null; | ||
| 442 | } | ||
| 443 | } | ||
diff --git a/vendor/doctrine/orm/src/Tools/ToolEvents.php b/vendor/doctrine/orm/src/Tools/ToolEvents.php new file mode 100644 index 0000000..fac37fa --- /dev/null +++ b/vendor/doctrine/orm/src/Tools/ToolEvents.php | |||
| @@ -0,0 +1,23 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Tools; | ||
| 6 | |||
| 7 | class ToolEvents | ||
| 8 | { | ||
| 9 | /** | ||
| 10 | * The postGenerateSchemaTable event occurs in SchemaTool#getSchemaFromMetadata() | ||
| 11 | * whenever an entity class is transformed into its table representation. It receives | ||
| 12 | * the current non-complete Schema instance, the Entity Metadata Class instance and | ||
| 13 | * the Schema Table instance of this entity. | ||
| 14 | */ | ||
| 15 | public const postGenerateSchemaTable = 'postGenerateSchemaTable'; | ||
| 16 | |||
| 17 | /** | ||
| 18 | * The postGenerateSchema event is triggered in SchemaTool#getSchemaFromMetadata() | ||
| 19 | * after all entity classes have been transformed into the related Schema structure. | ||
| 20 | * The EventArgs contain the EntityManager and the created Schema instance. | ||
| 21 | */ | ||
| 22 | public const postGenerateSchema = 'postGenerateSchema'; | ||
| 23 | } | ||
diff --git a/vendor/doctrine/orm/src/Tools/ToolsException.php b/vendor/doctrine/orm/src/Tools/ToolsException.php new file mode 100644 index 0000000..e5cb973 --- /dev/null +++ b/vendor/doctrine/orm/src/Tools/ToolsException.php | |||
| @@ -0,0 +1,24 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Tools; | ||
| 6 | |||
| 7 | use Doctrine\ORM\Exception\ORMException; | ||
| 8 | use RuntimeException; | ||
| 9 | use Throwable; | ||
| 10 | |||
| 11 | /** | ||
| 12 | * Tools related Exceptions. | ||
| 13 | */ | ||
| 14 | class ToolsException extends RuntimeException implements ORMException | ||
| 15 | { | ||
| 16 | public static function schemaToolFailure(string $sql, Throwable $e): self | ||
| 17 | { | ||
| 18 | return new self( | ||
| 19 | "Schema-Tool failed with Error '" . $e->getMessage() . "' while executing DDL: " . $sql, | ||
| 20 | 0, | ||
| 21 | $e, | ||
| 22 | ); | ||
| 23 | } | ||
| 24 | } | ||
diff --git a/vendor/doctrine/orm/src/TransactionRequiredException.php b/vendor/doctrine/orm/src/TransactionRequiredException.php new file mode 100644 index 0000000..6114544 --- /dev/null +++ b/vendor/doctrine/orm/src/TransactionRequiredException.php | |||
| @@ -0,0 +1,21 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM; | ||
| 6 | |||
| 7 | use Doctrine\ORM\Exception\ORMException; | ||
| 8 | use LogicException; | ||
| 9 | |||
| 10 | /** | ||
| 11 | * Is thrown when a transaction is required for the current operation, but there is none open. | ||
| 12 | * | ||
| 13 | * @link www.doctrine-project.com | ||
| 14 | */ | ||
| 15 | class TransactionRequiredException extends LogicException implements ORMException | ||
| 16 | { | ||
| 17 | public static function transactionRequired(): self | ||
| 18 | { | ||
| 19 | return new self('An open transaction is required for this operation.'); | ||
| 20 | } | ||
| 21 | } | ||
diff --git a/vendor/doctrine/orm/src/UnexpectedResultException.php b/vendor/doctrine/orm/src/UnexpectedResultException.php new file mode 100644 index 0000000..8d8c966 --- /dev/null +++ b/vendor/doctrine/orm/src/UnexpectedResultException.php | |||
| @@ -0,0 +1,15 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM; | ||
| 6 | |||
| 7 | use Doctrine\ORM\Exception\ORMException; | ||
| 8 | use RuntimeException; | ||
| 9 | |||
| 10 | /** | ||
| 11 | * Exception for a unexpected query result. | ||
| 12 | */ | ||
| 13 | class UnexpectedResultException extends RuntimeException implements ORMException | ||
| 14 | { | ||
| 15 | } | ||
diff --git a/vendor/doctrine/orm/src/UnitOfWork.php b/vendor/doctrine/orm/src/UnitOfWork.php new file mode 100644 index 0000000..26f17d2 --- /dev/null +++ b/vendor/doctrine/orm/src/UnitOfWork.php | |||
| @@ -0,0 +1,3252 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM; | ||
| 6 | |||
| 7 | use BackedEnum; | ||
| 8 | use DateTimeInterface; | ||
| 9 | use Doctrine\Common\Collections\ArrayCollection; | ||
| 10 | use Doctrine\Common\Collections\Collection; | ||
| 11 | use Doctrine\Common\EventManager; | ||
| 12 | use Doctrine\DBAL; | ||
| 13 | use Doctrine\DBAL\Connections\PrimaryReadReplicaConnection; | ||
| 14 | use Doctrine\DBAL\LockMode; | ||
| 15 | use Doctrine\ORM\Cache\Persister\CachedPersister; | ||
| 16 | use Doctrine\ORM\Event\ListenersInvoker; | ||
| 17 | use Doctrine\ORM\Event\OnClearEventArgs; | ||
| 18 | use Doctrine\ORM\Event\OnFlushEventArgs; | ||
| 19 | use Doctrine\ORM\Event\PostFlushEventArgs; | ||
| 20 | use Doctrine\ORM\Event\PostPersistEventArgs; | ||
| 21 | use Doctrine\ORM\Event\PostRemoveEventArgs; | ||
| 22 | use Doctrine\ORM\Event\PostUpdateEventArgs; | ||
| 23 | use Doctrine\ORM\Event\PreFlushEventArgs; | ||
| 24 | use Doctrine\ORM\Event\PrePersistEventArgs; | ||
| 25 | use Doctrine\ORM\Event\PreRemoveEventArgs; | ||
| 26 | use Doctrine\ORM\Event\PreUpdateEventArgs; | ||
| 27 | use Doctrine\ORM\Exception\EntityIdentityCollisionException; | ||
| 28 | use Doctrine\ORM\Exception\ORMException; | ||
| 29 | use Doctrine\ORM\Exception\UnexpectedAssociationValue; | ||
| 30 | use Doctrine\ORM\Id\AssignedGenerator; | ||
| 31 | use Doctrine\ORM\Internal\HydrationCompleteHandler; | ||
| 32 | use Doctrine\ORM\Internal\StronglyConnectedComponents; | ||
| 33 | use Doctrine\ORM\Internal\TopologicalSort; | ||
| 34 | use Doctrine\ORM\Mapping\AssociationMapping; | ||
| 35 | use Doctrine\ORM\Mapping\ClassMetadata; | ||
| 36 | use Doctrine\ORM\Mapping\MappingException; | ||
| 37 | use Doctrine\ORM\Mapping\ToManyInverseSideMapping; | ||
| 38 | use Doctrine\ORM\Persisters\Collection\CollectionPersister; | ||
| 39 | use Doctrine\ORM\Persisters\Collection\ManyToManyPersister; | ||
| 40 | use Doctrine\ORM\Persisters\Collection\OneToManyPersister; | ||
| 41 | use Doctrine\ORM\Persisters\Entity\BasicEntityPersister; | ||
| 42 | use Doctrine\ORM\Persisters\Entity\EntityPersister; | ||
| 43 | use Doctrine\ORM\Persisters\Entity\JoinedSubclassPersister; | ||
| 44 | use Doctrine\ORM\Persisters\Entity\SingleTablePersister; | ||
| 45 | use Doctrine\ORM\Proxy\InternalProxy; | ||
| 46 | use Doctrine\ORM\Utility\IdentifierFlattener; | ||
| 47 | use Doctrine\Persistence\PropertyChangedListener; | ||
| 48 | use Exception; | ||
| 49 | use InvalidArgumentException; | ||
| 50 | use RuntimeException; | ||
| 51 | use Stringable; | ||
| 52 | use Throwable; | ||
| 53 | use UnexpectedValueException; | ||
| 54 | |||
| 55 | use function array_chunk; | ||
| 56 | use function array_combine; | ||
| 57 | use function array_diff_key; | ||
| 58 | use function array_filter; | ||
| 59 | use function array_key_exists; | ||
| 60 | use function array_map; | ||
| 61 | use function array_sum; | ||
| 62 | use function array_values; | ||
| 63 | use function assert; | ||
| 64 | use function current; | ||
| 65 | use function get_debug_type; | ||
| 66 | use function implode; | ||
| 67 | use function in_array; | ||
| 68 | use function is_array; | ||
| 69 | use function is_object; | ||
| 70 | use function reset; | ||
| 71 | use function spl_object_id; | ||
| 72 | use function sprintf; | ||
| 73 | use function strtolower; | ||
| 74 | |||
| 75 | /** | ||
| 76 | * The UnitOfWork is responsible for tracking changes to objects during an | ||
| 77 | * "object-level" transaction and for writing out changes to the database | ||
| 78 | * in the correct order. | ||
| 79 | * | ||
| 80 | * Internal note: This class contains highly performance-sensitive code. | ||
| 81 | */ | ||
| 82 | class UnitOfWork implements PropertyChangedListener | ||
| 83 | { | ||
| 84 | /** | ||
| 85 | * An entity is in MANAGED state when its persistence is managed by an EntityManager. | ||
| 86 | */ | ||
| 87 | public const STATE_MANAGED = 1; | ||
| 88 | |||
| 89 | /** | ||
| 90 | * An entity is new if it has just been instantiated (i.e. using the "new" operator) | ||
| 91 | * and is not (yet) managed by an EntityManager. | ||
| 92 | */ | ||
| 93 | public const STATE_NEW = 2; | ||
| 94 | |||
| 95 | /** | ||
| 96 | * A detached entity is an instance with persistent state and identity that is not | ||
| 97 | * (or no longer) associated with an EntityManager (and a UnitOfWork). | ||
| 98 | */ | ||
| 99 | public const STATE_DETACHED = 3; | ||
| 100 | |||
| 101 | /** | ||
| 102 | * A removed entity instance is an instance with a persistent identity, | ||
| 103 | * associated with an EntityManager, whose persistent state will be deleted | ||
| 104 | * on commit. | ||
| 105 | */ | ||
| 106 | public const STATE_REMOVED = 4; | ||
| 107 | |||
| 108 | /** | ||
| 109 | * Hint used to collect all primary keys of associated entities during hydration | ||
| 110 | * and execute it in a dedicated query afterwards | ||
| 111 | * | ||
| 112 | * @see https://www.doctrine-project.org/projects/doctrine-orm/en/stable/reference/dql-doctrine-query-language.html#temporarily-change-fetch-mode-in-dql | ||
| 113 | */ | ||
| 114 | public const HINT_DEFEREAGERLOAD = 'deferEagerLoad'; | ||
| 115 | |||
| 116 | /** | ||
| 117 | * The identity map that holds references to all managed entities that have | ||
| 118 | * an identity. The entities are grouped by their class name. | ||
| 119 | * Since all classes in a hierarchy must share the same identifier set, | ||
| 120 | * we always take the root class name of the hierarchy. | ||
| 121 | * | ||
| 122 | * @psalm-var array<class-string, array<string, object>> | ||
| 123 | */ | ||
| 124 | private array $identityMap = []; | ||
| 125 | |||
| 126 | /** | ||
| 127 | * Map of all identifiers of managed entities. | ||
| 128 | * Keys are object ids (spl_object_id). | ||
| 129 | * | ||
| 130 | * @psalm-var array<int, array<string, mixed>> | ||
| 131 | */ | ||
| 132 | private array $entityIdentifiers = []; | ||
| 133 | |||
| 134 | /** | ||
| 135 | * Map of the original entity data of managed entities. | ||
| 136 | * Keys are object ids (spl_object_id). This is used for calculating changesets | ||
| 137 | * at commit time. | ||
| 138 | * | ||
| 139 | * Internal note: Note that PHPs "copy-on-write" behavior helps a lot with memory usage. | ||
| 140 | * A value will only really be copied if the value in the entity is modified | ||
| 141 | * by the user. | ||
| 142 | * | ||
| 143 | * @psalm-var array<int, array<string, mixed>> | ||
| 144 | */ | ||
| 145 | private array $originalEntityData = []; | ||
| 146 | |||
| 147 | /** | ||
| 148 | * Map of entity changes. Keys are object ids (spl_object_id). | ||
| 149 | * Filled at the beginning of a commit of the UnitOfWork and cleaned at the end. | ||
| 150 | * | ||
| 151 | * @psalm-var array<int, array<string, array{mixed, mixed}>> | ||
| 152 | */ | ||
| 153 | private array $entityChangeSets = []; | ||
| 154 | |||
| 155 | /** | ||
| 156 | * The (cached) states of any known entities. | ||
| 157 | * Keys are object ids (spl_object_id). | ||
| 158 | * | ||
| 159 | * @psalm-var array<int, self::STATE_*> | ||
| 160 | */ | ||
| 161 | private array $entityStates = []; | ||
| 162 | |||
| 163 | /** | ||
| 164 | * Map of entities that are scheduled for dirty checking at commit time. | ||
| 165 | * This is only used for entities with a change tracking policy of DEFERRED_EXPLICIT. | ||
| 166 | * Keys are object ids (spl_object_id). | ||
| 167 | * | ||
| 168 | * @psalm-var array<class-string, array<int, mixed>> | ||
| 169 | */ | ||
| 170 | private array $scheduledForSynchronization = []; | ||
| 171 | |||
| 172 | /** | ||
| 173 | * A list of all pending entity insertions. | ||
| 174 | * | ||
| 175 | * @psalm-var array<int, object> | ||
| 176 | */ | ||
| 177 | private array $entityInsertions = []; | ||
| 178 | |||
| 179 | /** | ||
| 180 | * A list of all pending entity updates. | ||
| 181 | * | ||
| 182 | * @psalm-var array<int, object> | ||
| 183 | */ | ||
| 184 | private array $entityUpdates = []; | ||
| 185 | |||
| 186 | /** | ||
| 187 | * Any pending extra updates that have been scheduled by persisters. | ||
| 188 | * | ||
| 189 | * @psalm-var array<int, array{object, array<string, array{mixed, mixed}>}> | ||
| 190 | */ | ||
| 191 | private array $extraUpdates = []; | ||
| 192 | |||
| 193 | /** | ||
| 194 | * A list of all pending entity deletions. | ||
| 195 | * | ||
| 196 | * @psalm-var array<int, object> | ||
| 197 | */ | ||
| 198 | private array $entityDeletions = []; | ||
| 199 | |||
| 200 | /** | ||
| 201 | * New entities that were discovered through relationships that were not | ||
| 202 | * marked as cascade-persist. During flush, this array is populated and | ||
| 203 | * then pruned of any entities that were discovered through a valid | ||
| 204 | * cascade-persist path. (Leftovers cause an error.) | ||
| 205 | * | ||
| 206 | * Keys are OIDs, payload is a two-item array describing the association | ||
| 207 | * and the entity. | ||
| 208 | * | ||
| 209 | * @var array<int, array{AssociationMapping, object}> indexed by respective object spl_object_id() | ||
| 210 | */ | ||
| 211 | private array $nonCascadedNewDetectedEntities = []; | ||
| 212 | |||
| 213 | /** | ||
| 214 | * All pending collection deletions. | ||
| 215 | * | ||
| 216 | * @psalm-var array<int, PersistentCollection<array-key, object>> | ||
| 217 | */ | ||
| 218 | private array $collectionDeletions = []; | ||
| 219 | |||
| 220 | /** | ||
| 221 | * All pending collection updates. | ||
| 222 | * | ||
| 223 | * @psalm-var array<int, PersistentCollection<array-key, object>> | ||
| 224 | */ | ||
| 225 | private array $collectionUpdates = []; | ||
| 226 | |||
| 227 | /** | ||
| 228 | * List of collections visited during changeset calculation on a commit-phase of a UnitOfWork. | ||
| 229 | * At the end of the UnitOfWork all these collections will make new snapshots | ||
| 230 | * of their data. | ||
| 231 | * | ||
| 232 | * @psalm-var array<int, PersistentCollection<array-key, object>> | ||
| 233 | */ | ||
| 234 | private array $visitedCollections = []; | ||
| 235 | |||
| 236 | /** | ||
| 237 | * List of collections visited during the changeset calculation that contain to-be-removed | ||
| 238 | * entities and need to have keys removed post commit. | ||
| 239 | * | ||
| 240 | * Indexed by Collection object ID, which also serves as the key in self::$visitedCollections; | ||
| 241 | * values are the key names that need to be removed. | ||
| 242 | * | ||
| 243 | * @psalm-var array<int, array<array-key, true>> | ||
| 244 | */ | ||
| 245 | private array $pendingCollectionElementRemovals = []; | ||
| 246 | |||
| 247 | /** | ||
| 248 | * The entity persister instances used to persist entity instances. | ||
| 249 | * | ||
| 250 | * @psalm-var array<string, EntityPersister> | ||
| 251 | */ | ||
| 252 | private array $persisters = []; | ||
| 253 | |||
| 254 | /** | ||
| 255 | * The collection persister instances used to persist collections. | ||
| 256 | * | ||
| 257 | * @psalm-var array<array-key, CollectionPersister> | ||
| 258 | */ | ||
| 259 | private array $collectionPersisters = []; | ||
| 260 | |||
| 261 | /** | ||
| 262 | * The EventManager used for dispatching events. | ||
| 263 | */ | ||
| 264 | private readonly EventManager $evm; | ||
| 265 | |||
| 266 | /** | ||
| 267 | * The ListenersInvoker used for dispatching events. | ||
| 268 | */ | ||
| 269 | private readonly ListenersInvoker $listenersInvoker; | ||
| 270 | |||
| 271 | /** | ||
| 272 | * The IdentifierFlattener used for manipulating identifiers | ||
| 273 | */ | ||
| 274 | private readonly IdentifierFlattener $identifierFlattener; | ||
| 275 | |||
| 276 | /** | ||
| 277 | * Orphaned entities that are scheduled for removal. | ||
| 278 | * | ||
| 279 | * @psalm-var array<int, object> | ||
| 280 | */ | ||
| 281 | private array $orphanRemovals = []; | ||
| 282 | |||
| 283 | /** | ||
| 284 | * Read-Only objects are never evaluated | ||
| 285 | * | ||
| 286 | * @var array<int, true> | ||
| 287 | */ | ||
| 288 | private array $readOnlyObjects = []; | ||
| 289 | |||
| 290 | /** | ||
| 291 | * Map of Entity Class-Names and corresponding IDs that should eager loaded when requested. | ||
| 292 | * | ||
| 293 | * @psalm-var array<class-string, array<string, mixed>> | ||
| 294 | */ | ||
| 295 | private array $eagerLoadingEntities = []; | ||
| 296 | |||
| 297 | /** @var array<string, array<string, mixed>> */ | ||
| 298 | private array $eagerLoadingCollections = []; | ||
| 299 | |||
| 300 | protected bool $hasCache = false; | ||
| 301 | |||
| 302 | /** | ||
| 303 | * Helper for handling completion of hydration | ||
| 304 | */ | ||
| 305 | private readonly HydrationCompleteHandler $hydrationCompleteHandler; | ||
| 306 | |||
| 307 | /** | ||
| 308 | * Initializes a new UnitOfWork instance, bound to the given EntityManager. | ||
| 309 | * | ||
| 310 | * @param EntityManagerInterface $em The EntityManager that "owns" this UnitOfWork instance. | ||
| 311 | */ | ||
| 312 | public function __construct( | ||
| 313 | private readonly EntityManagerInterface $em, | ||
| 314 | ) { | ||
| 315 | $this->evm = $em->getEventManager(); | ||
| 316 | $this->listenersInvoker = new ListenersInvoker($em); | ||
| 317 | $this->hasCache = $em->getConfiguration()->isSecondLevelCacheEnabled(); | ||
| 318 | $this->identifierFlattener = new IdentifierFlattener($this, $em->getMetadataFactory()); | ||
| 319 | $this->hydrationCompleteHandler = new HydrationCompleteHandler($this->listenersInvoker, $em); | ||
| 320 | } | ||
| 321 | |||
| 322 | /** | ||
| 323 | * Commits the UnitOfWork, executing all operations that have been postponed | ||
| 324 | * up to this point. The state of all managed entities will be synchronized with | ||
| 325 | * the database. | ||
| 326 | * | ||
| 327 | * The operations are executed in the following order: | ||
| 328 | * | ||
| 329 | * 1) All entity insertions | ||
| 330 | * 2) All entity updates | ||
| 331 | * 3) All collection deletions | ||
| 332 | * 4) All collection updates | ||
| 333 | * 5) All entity deletions | ||
| 334 | * | ||
| 335 | * @throws Exception | ||
| 336 | */ | ||
| 337 | public function commit(): void | ||
| 338 | { | ||
| 339 | $connection = $this->em->getConnection(); | ||
| 340 | |||
| 341 | if ($connection instanceof PrimaryReadReplicaConnection) { | ||
| 342 | $connection->ensureConnectedToPrimary(); | ||
| 343 | } | ||
| 344 | |||
| 345 | // Raise preFlush | ||
| 346 | if ($this->evm->hasListeners(Events::preFlush)) { | ||
| 347 | $this->evm->dispatchEvent(Events::preFlush, new PreFlushEventArgs($this->em)); | ||
| 348 | } | ||
| 349 | |||
| 350 | // Compute changes done since last commit. | ||
| 351 | $this->computeChangeSets(); | ||
| 352 | |||
| 353 | if ( | ||
| 354 | ! ($this->entityInsertions || | ||
| 355 | $this->entityDeletions || | ||
| 356 | $this->entityUpdates || | ||
| 357 | $this->collectionUpdates || | ||
| 358 | $this->collectionDeletions || | ||
| 359 | $this->orphanRemovals) | ||
| 360 | ) { | ||
| 361 | $this->dispatchOnFlushEvent(); | ||
| 362 | $this->dispatchPostFlushEvent(); | ||
| 363 | |||
| 364 | $this->postCommitCleanup(); | ||
| 365 | |||
| 366 | return; // Nothing to do. | ||
| 367 | } | ||
| 368 | |||
| 369 | $this->assertThatThereAreNoUnintentionallyNonPersistedAssociations(); | ||
| 370 | |||
| 371 | if ($this->orphanRemovals) { | ||
| 372 | foreach ($this->orphanRemovals as $orphan) { | ||
| 373 | $this->remove($orphan); | ||
| 374 | } | ||
| 375 | } | ||
| 376 | |||
| 377 | $this->dispatchOnFlushEvent(); | ||
| 378 | |||
| 379 | $conn = $this->em->getConnection(); | ||
| 380 | $conn->beginTransaction(); | ||
| 381 | |||
| 382 | try { | ||
| 383 | // Collection deletions (deletions of complete collections) | ||
| 384 | foreach ($this->collectionDeletions as $collectionToDelete) { | ||
| 385 | // Deferred explicit tracked collections can be removed only when owning relation was persisted | ||
| 386 | $owner = $collectionToDelete->getOwner(); | ||
| 387 | |||
| 388 | if ($this->em->getClassMetadata($owner::class)->isChangeTrackingDeferredImplicit() || $this->isScheduledForDirtyCheck($owner)) { | ||
| 389 | $this->getCollectionPersister($collectionToDelete->getMapping())->delete($collectionToDelete); | ||
| 390 | } | ||
| 391 | } | ||
| 392 | |||
| 393 | if ($this->entityInsertions) { | ||
| 394 | // Perform entity insertions first, so that all new entities have their rows in the database | ||
| 395 | // and can be referred to by foreign keys. The commit order only needs to take new entities | ||
| 396 | // into account (new entities referring to other new entities), since all other types (entities | ||
| 397 | // with updates or scheduled deletions) are currently not a problem, since they are already | ||
| 398 | // in the database. | ||
| 399 | $this->executeInserts(); | ||
| 400 | } | ||
| 401 | |||
| 402 | if ($this->entityUpdates) { | ||
| 403 | // Updates do not need to follow a particular order | ||
| 404 | $this->executeUpdates(); | ||
| 405 | } | ||
| 406 | |||
| 407 | // Extra updates that were requested by persisters. | ||
| 408 | // This may include foreign keys that could not be set when an entity was inserted, | ||
| 409 | // which may happen in the case of circular foreign key relationships. | ||
| 410 | if ($this->extraUpdates) { | ||
| 411 | $this->executeExtraUpdates(); | ||
| 412 | } | ||
| 413 | |||
| 414 | // Collection updates (deleteRows, updateRows, insertRows) | ||
| 415 | // No particular order is necessary, since all entities themselves are already | ||
| 416 | // in the database | ||
| 417 | foreach ($this->collectionUpdates as $collectionToUpdate) { | ||
| 418 | $this->getCollectionPersister($collectionToUpdate->getMapping())->update($collectionToUpdate); | ||
| 419 | } | ||
| 420 | |||
| 421 | // Entity deletions come last. Their order only needs to take care of other deletions | ||
| 422 | // (first delete entities depending upon others, before deleting depended-upon entities). | ||
| 423 | if ($this->entityDeletions) { | ||
| 424 | $this->executeDeletions(); | ||
| 425 | } | ||
| 426 | |||
| 427 | $commitFailed = false; | ||
| 428 | try { | ||
| 429 | if ($conn->commit() === false) { | ||
| 430 | $commitFailed = true; | ||
| 431 | } | ||
| 432 | } catch (DBAL\Exception $e) { | ||
| 433 | $commitFailed = true; | ||
| 434 | } | ||
| 435 | |||
| 436 | if ($commitFailed) { | ||
| 437 | throw new OptimisticLockException('Commit failed', null, $e ?? null); | ||
| 438 | } | ||
| 439 | } catch (Throwable $e) { | ||
| 440 | $this->em->close(); | ||
| 441 | |||
| 442 | if ($conn->isTransactionActive()) { | ||
| 443 | $conn->rollBack(); | ||
| 444 | } | ||
| 445 | |||
| 446 | $this->afterTransactionRolledBack(); | ||
| 447 | |||
| 448 | throw $e; | ||
| 449 | } | ||
| 450 | |||
| 451 | $this->afterTransactionComplete(); | ||
| 452 | |||
| 453 | // Unset removed entities from collections, and take new snapshots from | ||
| 454 | // all visited collections. | ||
| 455 | foreach ($this->visitedCollections as $coid => $coll) { | ||
| 456 | if (isset($this->pendingCollectionElementRemovals[$coid])) { | ||
| 457 | foreach ($this->pendingCollectionElementRemovals[$coid] as $key => $valueIgnored) { | ||
| 458 | unset($coll[$key]); | ||
| 459 | } | ||
| 460 | } | ||
| 461 | |||
| 462 | $coll->takeSnapshot(); | ||
| 463 | } | ||
| 464 | |||
| 465 | $this->dispatchPostFlushEvent(); | ||
| 466 | |||
| 467 | $this->postCommitCleanup(); | ||
| 468 | } | ||
| 469 | |||
| 470 | private function postCommitCleanup(): void | ||
| 471 | { | ||
| 472 | $this->entityInsertions = | ||
| 473 | $this->entityUpdates = | ||
| 474 | $this->entityDeletions = | ||
| 475 | $this->extraUpdates = | ||
| 476 | $this->collectionUpdates = | ||
| 477 | $this->nonCascadedNewDetectedEntities = | ||
| 478 | $this->collectionDeletions = | ||
| 479 | $this->pendingCollectionElementRemovals = | ||
| 480 | $this->visitedCollections = | ||
| 481 | $this->orphanRemovals = | ||
| 482 | $this->entityChangeSets = | ||
| 483 | $this->scheduledForSynchronization = []; | ||
| 484 | } | ||
| 485 | |||
| 486 | /** | ||
| 487 | * Computes the changesets of all entities scheduled for insertion. | ||
| 488 | */ | ||
| 489 | private function computeScheduleInsertsChangeSets(): void | ||
| 490 | { | ||
| 491 | foreach ($this->entityInsertions as $entity) { | ||
| 492 | $class = $this->em->getClassMetadata($entity::class); | ||
| 493 | |||
| 494 | $this->computeChangeSet($class, $entity); | ||
| 495 | } | ||
| 496 | } | ||
| 497 | |||
| 498 | /** | ||
| 499 | * Executes any extra updates that have been scheduled. | ||
| 500 | */ | ||
| 501 | private function executeExtraUpdates(): void | ||
| 502 | { | ||
| 503 | foreach ($this->extraUpdates as $oid => $update) { | ||
| 504 | [$entity, $changeset] = $update; | ||
| 505 | |||
| 506 | $this->entityChangeSets[$oid] = $changeset; | ||
| 507 | $this->getEntityPersister($entity::class)->update($entity); | ||
| 508 | } | ||
| 509 | |||
| 510 | $this->extraUpdates = []; | ||
| 511 | } | ||
| 512 | |||
| 513 | /** | ||
| 514 | * Gets the changeset for an entity. | ||
| 515 | * | ||
| 516 | * @return mixed[][] | ||
| 517 | * @psalm-return array<string, array{mixed, mixed}|PersistentCollection> | ||
| 518 | */ | ||
| 519 | public function & getEntityChangeSet(object $entity): array | ||
| 520 | { | ||
| 521 | $oid = spl_object_id($entity); | ||
| 522 | $data = []; | ||
| 523 | |||
| 524 | if (! isset($this->entityChangeSets[$oid])) { | ||
| 525 | return $data; | ||
| 526 | } | ||
| 527 | |||
| 528 | return $this->entityChangeSets[$oid]; | ||
| 529 | } | ||
| 530 | |||
| 531 | /** | ||
| 532 | * Computes the changes that happened to a single entity. | ||
| 533 | * | ||
| 534 | * Modifies/populates the following properties: | ||
| 535 | * | ||
| 536 | * {@link _originalEntityData} | ||
| 537 | * If the entity is NEW or MANAGED but not yet fully persisted (only has an id) | ||
| 538 | * then it was not fetched from the database and therefore we have no original | ||
| 539 | * entity data yet. All of the current entity data is stored as the original entity data. | ||
| 540 | * | ||
| 541 | * {@link _entityChangeSets} | ||
| 542 | * The changes detected on all properties of the entity are stored there. | ||
| 543 | * A change is a tuple array where the first entry is the old value and the second | ||
| 544 | * entry is the new value of the property. Changesets are used by persisters | ||
| 545 | * to INSERT/UPDATE the persistent entity state. | ||
| 546 | * | ||
| 547 | * {@link _entityUpdates} | ||
| 548 | * If the entity is already fully MANAGED (has been fetched from the database before) | ||
| 549 | * and any changes to its properties are detected, then a reference to the entity is stored | ||
| 550 | * there to mark it for an update. | ||
| 551 | * | ||
| 552 | * {@link _collectionDeletions} | ||
| 553 | * If a PersistentCollection has been de-referenced in a fully MANAGED entity, | ||
| 554 | * then this collection is marked for deletion. | ||
| 555 | * | ||
| 556 | * @param ClassMetadata $class The class descriptor of the entity. | ||
| 557 | * @param object $entity The entity for which to compute the changes. | ||
| 558 | * @psalm-param ClassMetadata<T> $class | ||
| 559 | * @psalm-param T $entity | ||
| 560 | * | ||
| 561 | * @template T of object | ||
| 562 | * | ||
| 563 | * @ignore | ||
| 564 | */ | ||
| 565 | public function computeChangeSet(ClassMetadata $class, object $entity): void | ||
| 566 | { | ||
| 567 | $oid = spl_object_id($entity); | ||
| 568 | |||
| 569 | if (isset($this->readOnlyObjects[$oid])) { | ||
| 570 | return; | ||
| 571 | } | ||
| 572 | |||
| 573 | if (! $class->isInheritanceTypeNone()) { | ||
| 574 | $class = $this->em->getClassMetadata($entity::class); | ||
| 575 | } | ||
| 576 | |||
| 577 | $invoke = $this->listenersInvoker->getSubscribedSystems($class, Events::preFlush) & ~ListenersInvoker::INVOKE_MANAGER; | ||
| 578 | |||
| 579 | if ($invoke !== ListenersInvoker::INVOKE_NONE) { | ||
| 580 | $this->listenersInvoker->invoke($class, Events::preFlush, $entity, new PreFlushEventArgs($this->em), $invoke); | ||
| 581 | } | ||
| 582 | |||
| 583 | $actualData = []; | ||
| 584 | |||
| 585 | foreach ($class->reflFields as $name => $refProp) { | ||
| 586 | $value = $refProp->getValue($entity); | ||
| 587 | |||
| 588 | if ($class->isCollectionValuedAssociation($name) && $value !== null) { | ||
| 589 | if ($value instanceof PersistentCollection) { | ||
| 590 | if ($value->getOwner() === $entity) { | ||
| 591 | $actualData[$name] = $value; | ||
| 592 | continue; | ||
| 593 | } | ||
| 594 | |||
| 595 | $value = new ArrayCollection($value->getValues()); | ||
| 596 | } | ||
| 597 | |||
| 598 | // If $value is not a Collection then use an ArrayCollection. | ||
| 599 | if (! $value instanceof Collection) { | ||
| 600 | $value = new ArrayCollection($value); | ||
| 601 | } | ||
| 602 | |||
| 603 | $assoc = $class->associationMappings[$name]; | ||
| 604 | assert($assoc->isToMany()); | ||
| 605 | |||
| 606 | // Inject PersistentCollection | ||
| 607 | $value = new PersistentCollection( | ||
| 608 | $this->em, | ||
| 609 | $this->em->getClassMetadata($assoc->targetEntity), | ||
| 610 | $value, | ||
| 611 | ); | ||
| 612 | $value->setOwner($entity, $assoc); | ||
| 613 | $value->setDirty(! $value->isEmpty()); | ||
| 614 | |||
| 615 | $refProp->setValue($entity, $value); | ||
| 616 | |||
| 617 | $actualData[$name] = $value; | ||
| 618 | |||
| 619 | continue; | ||
| 620 | } | ||
| 621 | |||
| 622 | if (( ! $class->isIdentifier($name) || ! $class->isIdGeneratorIdentity()) && ($name !== $class->versionField)) { | ||
| 623 | $actualData[$name] = $value; | ||
| 624 | } | ||
| 625 | } | ||
| 626 | |||
| 627 | if (! isset($this->originalEntityData[$oid])) { | ||
| 628 | // Entity is either NEW or MANAGED but not yet fully persisted (only has an id). | ||
| 629 | // These result in an INSERT. | ||
| 630 | $this->originalEntityData[$oid] = $actualData; | ||
| 631 | $changeSet = []; | ||
| 632 | |||
| 633 | foreach ($actualData as $propName => $actualValue) { | ||
| 634 | if (! isset($class->associationMappings[$propName])) { | ||
| 635 | $changeSet[$propName] = [null, $actualValue]; | ||
| 636 | |||
| 637 | continue; | ||
| 638 | } | ||
| 639 | |||
| 640 | $assoc = $class->associationMappings[$propName]; | ||
| 641 | |||
| 642 | if ($assoc->isToOneOwningSide()) { | ||
| 643 | $changeSet[$propName] = [null, $actualValue]; | ||
| 644 | } | ||
| 645 | } | ||
| 646 | |||
| 647 | $this->entityChangeSets[$oid] = $changeSet; | ||
| 648 | } else { | ||
| 649 | // Entity is "fully" MANAGED: it was already fully persisted before | ||
| 650 | // and we have a copy of the original data | ||
| 651 | $originalData = $this->originalEntityData[$oid]; | ||
| 652 | $changeSet = []; | ||
| 653 | |||
| 654 | foreach ($actualData as $propName => $actualValue) { | ||
| 655 | // skip field, its a partially omitted one! | ||
| 656 | if (! (isset($originalData[$propName]) || array_key_exists($propName, $originalData))) { | ||
| 657 | continue; | ||
| 658 | } | ||
| 659 | |||
| 660 | $orgValue = $originalData[$propName]; | ||
| 661 | |||
| 662 | if (! empty($class->fieldMappings[$propName]->enumType)) { | ||
| 663 | if (is_array($orgValue)) { | ||
| 664 | foreach ($orgValue as $id => $val) { | ||
| 665 | if ($val instanceof BackedEnum) { | ||
| 666 | $orgValue[$id] = $val->value; | ||
| 667 | } | ||
| 668 | } | ||
| 669 | } else { | ||
| 670 | if ($orgValue instanceof BackedEnum) { | ||
| 671 | $orgValue = $orgValue->value; | ||
| 672 | } | ||
| 673 | } | ||
| 674 | } | ||
| 675 | |||
| 676 | // skip if value haven't changed | ||
| 677 | if ($orgValue === $actualValue) { | ||
| 678 | continue; | ||
| 679 | } | ||
| 680 | |||
| 681 | // if regular field | ||
| 682 | if (! isset($class->associationMappings[$propName])) { | ||
| 683 | $changeSet[$propName] = [$orgValue, $actualValue]; | ||
| 684 | |||
| 685 | continue; | ||
| 686 | } | ||
| 687 | |||
| 688 | $assoc = $class->associationMappings[$propName]; | ||
| 689 | |||
| 690 | // Persistent collection was exchanged with the "originally" | ||
| 691 | // created one. This can only mean it was cloned and replaced | ||
| 692 | // on another entity. | ||
| 693 | if ($actualValue instanceof PersistentCollection) { | ||
| 694 | assert($assoc->isToMany()); | ||
| 695 | $owner = $actualValue->getOwner(); | ||
| 696 | if ($owner === null) { // cloned | ||
| 697 | $actualValue->setOwner($entity, $assoc); | ||
| 698 | } elseif ($owner !== $entity) { // no clone, we have to fix | ||
| 699 | if (! $actualValue->isInitialized()) { | ||
| 700 | $actualValue->initialize(); // we have to do this otherwise the cols share state | ||
| 701 | } | ||
| 702 | |||
| 703 | $newValue = clone $actualValue; | ||
| 704 | $newValue->setOwner($entity, $assoc); | ||
| 705 | $class->reflFields[$propName]->setValue($entity, $newValue); | ||
| 706 | } | ||
| 707 | } | ||
| 708 | |||
| 709 | if ($orgValue instanceof PersistentCollection) { | ||
| 710 | // A PersistentCollection was de-referenced, so delete it. | ||
| 711 | $coid = spl_object_id($orgValue); | ||
| 712 | |||
| 713 | if (isset($this->collectionDeletions[$coid])) { | ||
| 714 | continue; | ||
| 715 | } | ||
| 716 | |||
| 717 | $this->collectionDeletions[$coid] = $orgValue; | ||
| 718 | $changeSet[$propName] = $orgValue; // Signal changeset, to-many assocs will be ignored. | ||
| 719 | |||
| 720 | continue; | ||
| 721 | } | ||
| 722 | |||
| 723 | if ($assoc->isToOne()) { | ||
| 724 | if ($assoc->isOwningSide()) { | ||
| 725 | $changeSet[$propName] = [$orgValue, $actualValue]; | ||
| 726 | } | ||
| 727 | |||
| 728 | if ($orgValue !== null && $assoc->orphanRemoval) { | ||
| 729 | assert(is_object($orgValue)); | ||
| 730 | $this->scheduleOrphanRemoval($orgValue); | ||
| 731 | } | ||
| 732 | } | ||
| 733 | } | ||
| 734 | |||
| 735 | if ($changeSet) { | ||
| 736 | $this->entityChangeSets[$oid] = $changeSet; | ||
| 737 | $this->originalEntityData[$oid] = $actualData; | ||
| 738 | $this->entityUpdates[$oid] = $entity; | ||
| 739 | } | ||
| 740 | } | ||
| 741 | |||
| 742 | // Look for changes in associations of the entity | ||
| 743 | foreach ($class->associationMappings as $field => $assoc) { | ||
| 744 | $val = $class->reflFields[$field]->getValue($entity); | ||
| 745 | if ($val === null) { | ||
| 746 | continue; | ||
| 747 | } | ||
| 748 | |||
| 749 | $this->computeAssociationChanges($assoc, $val); | ||
| 750 | |||
| 751 | if ( | ||
| 752 | ! isset($this->entityChangeSets[$oid]) && | ||
| 753 | $assoc->isManyToManyOwningSide() && | ||
| 754 | $val instanceof PersistentCollection && | ||
| 755 | $val->isDirty() | ||
| 756 | ) { | ||
| 757 | $this->entityChangeSets[$oid] = []; | ||
| 758 | $this->originalEntityData[$oid] = $actualData; | ||
| 759 | $this->entityUpdates[$oid] = $entity; | ||
| 760 | } | ||
| 761 | } | ||
| 762 | } | ||
| 763 | |||
| 764 | /** | ||
| 765 | * Computes all the changes that have been done to entities and collections | ||
| 766 | * since the last commit and stores these changes in the _entityChangeSet map | ||
| 767 | * temporarily for access by the persisters, until the UoW commit is finished. | ||
| 768 | */ | ||
| 769 | public function computeChangeSets(): void | ||
| 770 | { | ||
| 771 | // Compute changes for INSERTed entities first. This must always happen. | ||
| 772 | $this->computeScheduleInsertsChangeSets(); | ||
| 773 | |||
| 774 | // Compute changes for other MANAGED entities. Change tracking policies take effect here. | ||
| 775 | foreach ($this->identityMap as $className => $entities) { | ||
| 776 | $class = $this->em->getClassMetadata($className); | ||
| 777 | |||
| 778 | // Skip class if instances are read-only | ||
| 779 | if ($class->isReadOnly) { | ||
| 780 | continue; | ||
| 781 | } | ||
| 782 | |||
| 783 | $entitiesToProcess = match (true) { | ||
| 784 | $class->isChangeTrackingDeferredImplicit() => $entities, | ||
| 785 | isset($this->scheduledForSynchronization[$className]) => $this->scheduledForSynchronization[$className], | ||
| 786 | default => [], | ||
| 787 | }; | ||
| 788 | |||
| 789 | foreach ($entitiesToProcess as $entity) { | ||
| 790 | // Ignore uninitialized proxy objects | ||
| 791 | if ($this->isUninitializedObject($entity)) { | ||
| 792 | continue; | ||
| 793 | } | ||
| 794 | |||
| 795 | // Only MANAGED entities that are NOT SCHEDULED FOR INSERTION OR DELETION are processed here. | ||
| 796 | $oid = spl_object_id($entity); | ||
| 797 | |||
| 798 | if (! isset($this->entityInsertions[$oid]) && ! isset($this->entityDeletions[$oid]) && isset($this->entityStates[$oid])) { | ||
| 799 | $this->computeChangeSet($class, $entity); | ||
| 800 | } | ||
| 801 | } | ||
| 802 | } | ||
| 803 | } | ||
| 804 | |||
| 805 | /** | ||
| 806 | * Computes the changes of an association. | ||
| 807 | * | ||
| 808 | * @param mixed $value The value of the association. | ||
| 809 | * | ||
| 810 | * @throws ORMInvalidArgumentException | ||
| 811 | * @throws ORMException | ||
| 812 | */ | ||
| 813 | private function computeAssociationChanges(AssociationMapping $assoc, mixed $value): void | ||
| 814 | { | ||
| 815 | if ($this->isUninitializedObject($value)) { | ||
| 816 | return; | ||
| 817 | } | ||
| 818 | |||
| 819 | // If this collection is dirty, schedule it for updates | ||
| 820 | if ($value instanceof PersistentCollection && $value->isDirty()) { | ||
| 821 | $coid = spl_object_id($value); | ||
| 822 | |||
| 823 | $this->collectionUpdates[$coid] = $value; | ||
| 824 | $this->visitedCollections[$coid] = $value; | ||
| 825 | } | ||
| 826 | |||
| 827 | // Look through the entities, and in any of their associations, | ||
| 828 | // for transient (new) entities, recursively. ("Persistence by reachability") | ||
| 829 | // Unwrap. Uninitialized collections will simply be empty. | ||
| 830 | $unwrappedValue = $assoc->isToOne() ? [$value] : $value->unwrap(); | ||
| 831 | $targetClass = $this->em->getClassMetadata($assoc->targetEntity); | ||
| 832 | |||
| 833 | foreach ($unwrappedValue as $key => $entry) { | ||
| 834 | if (! ($entry instanceof $targetClass->name)) { | ||
| 835 | throw ORMInvalidArgumentException::invalidAssociation($targetClass, $assoc, $entry); | ||
| 836 | } | ||
| 837 | |||
| 838 | $state = $this->getEntityState($entry, self::STATE_NEW); | ||
| 839 | |||
| 840 | if (! ($entry instanceof $assoc->targetEntity)) { | ||
| 841 | throw UnexpectedAssociationValue::create( | ||
| 842 | $assoc->sourceEntity, | ||
| 843 | $assoc->fieldName, | ||
| 844 | get_debug_type($entry), | ||
| 845 | $assoc->targetEntity, | ||
| 846 | ); | ||
| 847 | } | ||
| 848 | |||
| 849 | switch ($state) { | ||
| 850 | case self::STATE_NEW: | ||
| 851 | if (! $assoc->isCascadePersist()) { | ||
| 852 | /* | ||
| 853 | * For now just record the details, because this may | ||
| 854 | * not be an issue if we later discover another pathway | ||
| 855 | * through the object-graph where cascade-persistence | ||
| 856 | * is enabled for this object. | ||
| 857 | */ | ||
| 858 | $this->nonCascadedNewDetectedEntities[spl_object_id($entry)] = [$assoc, $entry]; | ||
| 859 | |||
| 860 | break; | ||
| 861 | } | ||
| 862 | |||
| 863 | $this->persistNew($targetClass, $entry); | ||
| 864 | $this->computeChangeSet($targetClass, $entry); | ||
| 865 | |||
| 866 | break; | ||
| 867 | |||
| 868 | case self::STATE_REMOVED: | ||
| 869 | // Consume the $value as array (it's either an array or an ArrayAccess) | ||
| 870 | // and remove the element from Collection. | ||
| 871 | if (! $assoc->isToMany()) { | ||
| 872 | break; | ||
| 873 | } | ||
| 874 | |||
| 875 | $coid = spl_object_id($value); | ||
| 876 | $this->visitedCollections[$coid] = $value; | ||
| 877 | |||
| 878 | if (! isset($this->pendingCollectionElementRemovals[$coid])) { | ||
| 879 | $this->pendingCollectionElementRemovals[$coid] = []; | ||
| 880 | } | ||
| 881 | |||
| 882 | $this->pendingCollectionElementRemovals[$coid][$key] = true; | ||
| 883 | break; | ||
| 884 | |||
| 885 | case self::STATE_DETACHED: | ||
| 886 | // Can actually not happen right now as we assume STATE_NEW, | ||
| 887 | // so the exception will be raised from the DBAL layer (constraint violation). | ||
| 888 | throw ORMInvalidArgumentException::detachedEntityFoundThroughRelationship($assoc, $entry); | ||
| 889 | |||
| 890 | default: | ||
| 891 | // MANAGED associated entities are already taken into account | ||
| 892 | // during changeset calculation anyway, since they are in the identity map. | ||
| 893 | } | ||
| 894 | } | ||
| 895 | } | ||
| 896 | |||
| 897 | /** | ||
| 898 | * @psalm-param ClassMetadata<T> $class | ||
| 899 | * @psalm-param T $entity | ||
| 900 | * | ||
| 901 | * @template T of object | ||
| 902 | */ | ||
| 903 | private function persistNew(ClassMetadata $class, object $entity): void | ||
| 904 | { | ||
| 905 | $oid = spl_object_id($entity); | ||
| 906 | $invoke = $this->listenersInvoker->getSubscribedSystems($class, Events::prePersist); | ||
| 907 | |||
| 908 | if ($invoke !== ListenersInvoker::INVOKE_NONE) { | ||
| 909 | $this->listenersInvoker->invoke($class, Events::prePersist, $entity, new PrePersistEventArgs($entity, $this->em), $invoke); | ||
| 910 | } | ||
| 911 | |||
| 912 | $idGen = $class->idGenerator; | ||
| 913 | |||
| 914 | if (! $idGen->isPostInsertGenerator()) { | ||
| 915 | $idValue = $idGen->generateId($this->em, $entity); | ||
| 916 | |||
| 917 | if (! $idGen instanceof AssignedGenerator) { | ||
| 918 | $idValue = [$class->getSingleIdentifierFieldName() => $this->convertSingleFieldIdentifierToPHPValue($class, $idValue)]; | ||
| 919 | |||
| 920 | $class->setIdentifierValues($entity, $idValue); | ||
| 921 | } | ||
| 922 | |||
| 923 | // Some identifiers may be foreign keys to new entities. | ||
| 924 | // In this case, we don't have the value yet and should treat it as if we have a post-insert generator | ||
| 925 | if (! $this->hasMissingIdsWhichAreForeignKeys($class, $idValue)) { | ||
| 926 | $this->entityIdentifiers[$oid] = $idValue; | ||
| 927 | } | ||
| 928 | } | ||
| 929 | |||
| 930 | $this->entityStates[$oid] = self::STATE_MANAGED; | ||
| 931 | |||
| 932 | $this->scheduleForInsert($entity); | ||
| 933 | } | ||
| 934 | |||
| 935 | /** @param mixed[] $idValue */ | ||
| 936 | private function hasMissingIdsWhichAreForeignKeys(ClassMetadata $class, array $idValue): bool | ||
| 937 | { | ||
| 938 | foreach ($idValue as $idField => $idFieldValue) { | ||
| 939 | if ($idFieldValue === null && isset($class->associationMappings[$idField])) { | ||
| 940 | return true; | ||
| 941 | } | ||
| 942 | } | ||
| 943 | |||
| 944 | return false; | ||
| 945 | } | ||
| 946 | |||
| 947 | /** | ||
| 948 | * INTERNAL: | ||
| 949 | * Computes the changeset of an individual entity, independently of the | ||
| 950 | * computeChangeSets() routine that is used at the beginning of a UnitOfWork#commit(). | ||
| 951 | * | ||
| 952 | * The passed entity must be a managed entity. If the entity already has a change set | ||
| 953 | * because this method is invoked during a commit cycle then the change sets are added. | ||
| 954 | * whereby changes detected in this method prevail. | ||
| 955 | * | ||
| 956 | * @param ClassMetadata $class The class descriptor of the entity. | ||
| 957 | * @param object $entity The entity for which to (re)calculate the change set. | ||
| 958 | * @psalm-param ClassMetadata<T> $class | ||
| 959 | * @psalm-param T $entity | ||
| 960 | * | ||
| 961 | * @throws ORMInvalidArgumentException If the passed entity is not MANAGED. | ||
| 962 | * | ||
| 963 | * @template T of object | ||
| 964 | * @ignore | ||
| 965 | */ | ||
| 966 | public function recomputeSingleEntityChangeSet(ClassMetadata $class, object $entity): void | ||
| 967 | { | ||
| 968 | $oid = spl_object_id($entity); | ||
| 969 | |||
| 970 | if (! isset($this->entityStates[$oid]) || $this->entityStates[$oid] !== self::STATE_MANAGED) { | ||
| 971 | throw ORMInvalidArgumentException::entityNotManaged($entity); | ||
| 972 | } | ||
| 973 | |||
| 974 | if (! $class->isInheritanceTypeNone()) { | ||
| 975 | $class = $this->em->getClassMetadata($entity::class); | ||
| 976 | } | ||
| 977 | |||
| 978 | $actualData = []; | ||
| 979 | |||
| 980 | foreach ($class->reflFields as $name => $refProp) { | ||
| 981 | if ( | ||
| 982 | ( ! $class->isIdentifier($name) || ! $class->isIdGeneratorIdentity()) | ||
| 983 | && ($name !== $class->versionField) | ||
| 984 | && ! $class->isCollectionValuedAssociation($name) | ||
| 985 | ) { | ||
| 986 | $actualData[$name] = $refProp->getValue($entity); | ||
| 987 | } | ||
| 988 | } | ||
| 989 | |||
| 990 | if (! isset($this->originalEntityData[$oid])) { | ||
| 991 | throw new RuntimeException('Cannot call recomputeSingleEntityChangeSet before computeChangeSet on an entity.'); | ||
| 992 | } | ||
| 993 | |||
| 994 | $originalData = $this->originalEntityData[$oid]; | ||
| 995 | $changeSet = []; | ||
| 996 | |||
| 997 | foreach ($actualData as $propName => $actualValue) { | ||
| 998 | $orgValue = $originalData[$propName] ?? null; | ||
| 999 | |||
| 1000 | if (isset($class->fieldMappings[$propName]->enumType)) { | ||
| 1001 | if (is_array($orgValue)) { | ||
| 1002 | foreach ($orgValue as $id => $val) { | ||
| 1003 | if ($val instanceof BackedEnum) { | ||
| 1004 | $orgValue[$id] = $val->value; | ||
| 1005 | } | ||
| 1006 | } | ||
| 1007 | } else { | ||
| 1008 | if ($orgValue instanceof BackedEnum) { | ||
| 1009 | $orgValue = $orgValue->value; | ||
| 1010 | } | ||
| 1011 | } | ||
| 1012 | } | ||
| 1013 | |||
| 1014 | if ($orgValue !== $actualValue) { | ||
| 1015 | $changeSet[$propName] = [$orgValue, $actualValue]; | ||
| 1016 | } | ||
| 1017 | } | ||
| 1018 | |||
| 1019 | if ($changeSet) { | ||
| 1020 | if (isset($this->entityChangeSets[$oid])) { | ||
| 1021 | $this->entityChangeSets[$oid] = [...$this->entityChangeSets[$oid], ...$changeSet]; | ||
| 1022 | } elseif (! isset($this->entityInsertions[$oid])) { | ||
| 1023 | $this->entityChangeSets[$oid] = $changeSet; | ||
| 1024 | $this->entityUpdates[$oid] = $entity; | ||
| 1025 | } | ||
| 1026 | |||
| 1027 | $this->originalEntityData[$oid] = $actualData; | ||
| 1028 | } | ||
| 1029 | } | ||
| 1030 | |||
| 1031 | /** | ||
| 1032 | * Executes entity insertions | ||
| 1033 | */ | ||
| 1034 | private function executeInserts(): void | ||
| 1035 | { | ||
| 1036 | $entities = $this->computeInsertExecutionOrder(); | ||
| 1037 | $eventsToDispatch = []; | ||
| 1038 | |||
| 1039 | foreach ($entities as $entity) { | ||
| 1040 | $oid = spl_object_id($entity); | ||
| 1041 | $class = $this->em->getClassMetadata($entity::class); | ||
| 1042 | $persister = $this->getEntityPersister($class->name); | ||
| 1043 | |||
| 1044 | $persister->addInsert($entity); | ||
| 1045 | |||
| 1046 | unset($this->entityInsertions[$oid]); | ||
| 1047 | |||
| 1048 | $persister->executeInserts(); | ||
| 1049 | |||
| 1050 | if (! isset($this->entityIdentifiers[$oid])) { | ||
| 1051 | //entity was not added to identity map because some identifiers are foreign keys to new entities. | ||
| 1052 | //add it now | ||
| 1053 | $this->addToEntityIdentifiersAndEntityMap($class, $oid, $entity); | ||
| 1054 | } | ||
| 1055 | |||
| 1056 | $invoke = $this->listenersInvoker->getSubscribedSystems($class, Events::postPersist); | ||
| 1057 | |||
| 1058 | if ($invoke !== ListenersInvoker::INVOKE_NONE) { | ||
| 1059 | $eventsToDispatch[] = ['class' => $class, 'entity' => $entity, 'invoke' => $invoke]; | ||
| 1060 | } | ||
| 1061 | } | ||
| 1062 | |||
| 1063 | // Defer dispatching `postPersist` events to until all entities have been inserted and post-insert | ||
| 1064 | // IDs have been assigned. | ||
| 1065 | foreach ($eventsToDispatch as $event) { | ||
| 1066 | $this->listenersInvoker->invoke( | ||
| 1067 | $event['class'], | ||
| 1068 | Events::postPersist, | ||
| 1069 | $event['entity'], | ||
| 1070 | new PostPersistEventArgs($event['entity'], $this->em), | ||
| 1071 | $event['invoke'], | ||
| 1072 | ); | ||
| 1073 | } | ||
| 1074 | } | ||
| 1075 | |||
| 1076 | /** | ||
| 1077 | * @psalm-param ClassMetadata<T> $class | ||
| 1078 | * @psalm-param T $entity | ||
| 1079 | * | ||
| 1080 | * @template T of object | ||
| 1081 | */ | ||
| 1082 | private function addToEntityIdentifiersAndEntityMap( | ||
| 1083 | ClassMetadata $class, | ||
| 1084 | int $oid, | ||
| 1085 | object $entity, | ||
| 1086 | ): void { | ||
| 1087 | $identifier = []; | ||
| 1088 | |||
| 1089 | foreach ($class->getIdentifierFieldNames() as $idField) { | ||
| 1090 | $origValue = $class->getFieldValue($entity, $idField); | ||
| 1091 | |||
| 1092 | $value = null; | ||
| 1093 | if (isset($class->associationMappings[$idField])) { | ||
| 1094 | // NOTE: Single Columns as associated identifiers only allowed - this constraint it is enforced. | ||
| 1095 | $value = $this->getSingleIdentifierValue($origValue); | ||
| 1096 | } | ||
| 1097 | |||
| 1098 | $identifier[$idField] = $value ?? $origValue; | ||
| 1099 | $this->originalEntityData[$oid][$idField] = $origValue; | ||
| 1100 | } | ||
| 1101 | |||
| 1102 | $this->entityStates[$oid] = self::STATE_MANAGED; | ||
| 1103 | $this->entityIdentifiers[$oid] = $identifier; | ||
| 1104 | |||
| 1105 | $this->addToIdentityMap($entity); | ||
| 1106 | } | ||
| 1107 | |||
| 1108 | /** | ||
| 1109 | * Executes all entity updates | ||
| 1110 | */ | ||
| 1111 | private function executeUpdates(): void | ||
| 1112 | { | ||
| 1113 | foreach ($this->entityUpdates as $oid => $entity) { | ||
| 1114 | $class = $this->em->getClassMetadata($entity::class); | ||
| 1115 | $persister = $this->getEntityPersister($class->name); | ||
| 1116 | $preUpdateInvoke = $this->listenersInvoker->getSubscribedSystems($class, Events::preUpdate); | ||
| 1117 | $postUpdateInvoke = $this->listenersInvoker->getSubscribedSystems($class, Events::postUpdate); | ||
| 1118 | |||
| 1119 | if ($preUpdateInvoke !== ListenersInvoker::INVOKE_NONE) { | ||
| 1120 | $this->listenersInvoker->invoke($class, Events::preUpdate, $entity, new PreUpdateEventArgs($entity, $this->em, $this->getEntityChangeSet($entity)), $preUpdateInvoke); | ||
| 1121 | |||
| 1122 | $this->recomputeSingleEntityChangeSet($class, $entity); | ||
| 1123 | } | ||
| 1124 | |||
| 1125 | if (! empty($this->entityChangeSets[$oid])) { | ||
| 1126 | $persister->update($entity); | ||
| 1127 | } | ||
| 1128 | |||
| 1129 | unset($this->entityUpdates[$oid]); | ||
| 1130 | |||
| 1131 | if ($postUpdateInvoke !== ListenersInvoker::INVOKE_NONE) { | ||
| 1132 | $this->listenersInvoker->invoke($class, Events::postUpdate, $entity, new PostUpdateEventArgs($entity, $this->em), $postUpdateInvoke); | ||
| 1133 | } | ||
| 1134 | } | ||
| 1135 | } | ||
| 1136 | |||
| 1137 | /** | ||
| 1138 | * Executes all entity deletions | ||
| 1139 | */ | ||
| 1140 | private function executeDeletions(): void | ||
| 1141 | { | ||
| 1142 | $entities = $this->computeDeleteExecutionOrder(); | ||
| 1143 | $eventsToDispatch = []; | ||
| 1144 | |||
| 1145 | foreach ($entities as $entity) { | ||
| 1146 | $this->removeFromIdentityMap($entity); | ||
| 1147 | |||
| 1148 | $oid = spl_object_id($entity); | ||
| 1149 | $class = $this->em->getClassMetadata($entity::class); | ||
| 1150 | $persister = $this->getEntityPersister($class->name); | ||
| 1151 | $invoke = $this->listenersInvoker->getSubscribedSystems($class, Events::postRemove); | ||
| 1152 | |||
| 1153 | $persister->delete($entity); | ||
| 1154 | |||
| 1155 | unset( | ||
| 1156 | $this->entityDeletions[$oid], | ||
| 1157 | $this->entityIdentifiers[$oid], | ||
| 1158 | $this->originalEntityData[$oid], | ||
| 1159 | $this->entityStates[$oid], | ||
| 1160 | ); | ||
| 1161 | |||
| 1162 | // Entity with this $oid after deletion treated as NEW, even if the $oid | ||
| 1163 | // is obtained by a new entity because the old one went out of scope. | ||
| 1164 | //$this->entityStates[$oid] = self::STATE_NEW; | ||
| 1165 | if (! $class->isIdentifierNatural()) { | ||
| 1166 | $class->reflFields[$class->identifier[0]]->setValue($entity, null); | ||
| 1167 | } | ||
| 1168 | |||
| 1169 | if ($invoke !== ListenersInvoker::INVOKE_NONE) { | ||
| 1170 | $eventsToDispatch[] = ['class' => $class, 'entity' => $entity, 'invoke' => $invoke]; | ||
| 1171 | } | ||
| 1172 | } | ||
| 1173 | |||
| 1174 | // Defer dispatching `postRemove` events to until all entities have been removed. | ||
| 1175 | foreach ($eventsToDispatch as $event) { | ||
| 1176 | $this->listenersInvoker->invoke( | ||
| 1177 | $event['class'], | ||
| 1178 | Events::postRemove, | ||
| 1179 | $event['entity'], | ||
| 1180 | new PostRemoveEventArgs($event['entity'], $this->em), | ||
| 1181 | $event['invoke'], | ||
| 1182 | ); | ||
| 1183 | } | ||
| 1184 | } | ||
| 1185 | |||
| 1186 | /** @return list<object> */ | ||
| 1187 | private function computeInsertExecutionOrder(): array | ||
| 1188 | { | ||
| 1189 | $sort = new TopologicalSort(); | ||
| 1190 | |||
| 1191 | // First make sure we have all the nodes | ||
| 1192 | foreach ($this->entityInsertions as $entity) { | ||
| 1193 | $sort->addNode($entity); | ||
| 1194 | } | ||
| 1195 | |||
| 1196 | // Now add edges | ||
| 1197 | foreach ($this->entityInsertions as $entity) { | ||
| 1198 | $class = $this->em->getClassMetadata($entity::class); | ||
| 1199 | |||
| 1200 | foreach ($class->associationMappings as $assoc) { | ||
| 1201 | // We only need to consider the owning sides of to-one associations, | ||
| 1202 | // since many-to-many associations are persisted at a later step and | ||
| 1203 | // have no insertion order problems (all entities already in the database | ||
| 1204 | // at that time). | ||
| 1205 | if (! $assoc->isToOneOwningSide()) { | ||
| 1206 | continue; | ||
| 1207 | } | ||
| 1208 | |||
| 1209 | $targetEntity = $class->getFieldValue($entity, $assoc->fieldName); | ||
| 1210 | |||
| 1211 | // If there is no entity that we need to refer to, or it is already in the | ||
| 1212 | // database (i. e. does not have to be inserted), no need to consider it. | ||
| 1213 | if ($targetEntity === null || ! $sort->hasNode($targetEntity)) { | ||
| 1214 | continue; | ||
| 1215 | } | ||
| 1216 | |||
| 1217 | // An entity that references back to itself _and_ uses an application-provided ID | ||
| 1218 | // (the "NONE" generator strategy) can be exempted from commit order computation. | ||
| 1219 | // See https://github.com/doctrine/orm/pull/10735/ for more details on this edge case. | ||
| 1220 | // A non-NULLable self-reference would be a cycle in the graph. | ||
| 1221 | if ($targetEntity === $entity && $class->isIdentifierNatural()) { | ||
| 1222 | continue; | ||
| 1223 | } | ||
| 1224 | |||
| 1225 | // According to https://www.doctrine-project.org/projects/doctrine-orm/en/2.14/reference/annotations-reference.html#annref_joincolumn, | ||
| 1226 | // the default for "nullable" is true. Unfortunately, it seems this default is not applied at the metadata driver, factory or other | ||
| 1227 | // level, but in fact we may have an undefined 'nullable' key here, so we must assume that default here as well. | ||
| 1228 | // | ||
| 1229 | // Same in \Doctrine\ORM\Tools\EntityGenerator::isAssociationIsNullable or \Doctrine\ORM\Persisters\Entity\BasicEntityPersister::getJoinSQLForJoinColumns, | ||
| 1230 | // to give two examples. | ||
| 1231 | $joinColumns = reset($assoc->joinColumns); | ||
| 1232 | $isNullable = ! isset($joinColumns->nullable) || $joinColumns->nullable; | ||
| 1233 | |||
| 1234 | // Add dependency. The dependency direction implies that "$entity depends on $targetEntity". The | ||
| 1235 | // topological sort result will output the depended-upon nodes first, which means we can insert | ||
| 1236 | // entities in that order. | ||
| 1237 | $sort->addEdge($entity, $targetEntity, $isNullable); | ||
| 1238 | } | ||
| 1239 | } | ||
| 1240 | |||
| 1241 | return $sort->sort(); | ||
| 1242 | } | ||
| 1243 | |||
| 1244 | /** @return list<object> */ | ||
| 1245 | private function computeDeleteExecutionOrder(): array | ||
| 1246 | { | ||
| 1247 | $stronglyConnectedComponents = new StronglyConnectedComponents(); | ||
| 1248 | $sort = new TopologicalSort(); | ||
| 1249 | |||
| 1250 | foreach ($this->entityDeletions as $entity) { | ||
| 1251 | $stronglyConnectedComponents->addNode($entity); | ||
| 1252 | $sort->addNode($entity); | ||
| 1253 | } | ||
| 1254 | |||
| 1255 | // First, consider only "on delete cascade" associations between entities | ||
| 1256 | // and find strongly connected groups. Once we delete any one of the entities | ||
| 1257 | // in such a group, _all_ of the other entities will be removed as well. So, | ||
| 1258 | // we need to treat those groups like a single entity when performing delete | ||
| 1259 | // order topological sorting. | ||
| 1260 | foreach ($this->entityDeletions as $entity) { | ||
| 1261 | $class = $this->em->getClassMetadata($entity::class); | ||
| 1262 | |||
| 1263 | foreach ($class->associationMappings as $assoc) { | ||
| 1264 | // We only need to consider the owning sides of to-one associations, | ||
| 1265 | // since many-to-many associations can always be (and have already been) | ||
| 1266 | // deleted in a preceding step. | ||
| 1267 | if (! $assoc->isToOneOwningSide()) { | ||
| 1268 | continue; | ||
| 1269 | } | ||
| 1270 | |||
| 1271 | $joinColumns = reset($assoc->joinColumns); | ||
| 1272 | if (! isset($joinColumns->onDelete)) { | ||
| 1273 | continue; | ||
| 1274 | } | ||
| 1275 | |||
| 1276 | $onDeleteOption = strtolower($joinColumns->onDelete); | ||
| 1277 | if ($onDeleteOption !== 'cascade') { | ||
| 1278 | continue; | ||
| 1279 | } | ||
| 1280 | |||
| 1281 | $targetEntity = $class->getFieldValue($entity, $assoc->fieldName); | ||
| 1282 | |||
| 1283 | // If the association does not refer to another entity or that entity | ||
| 1284 | // is not to be deleted, there is no ordering problem and we can | ||
| 1285 | // skip this particular association. | ||
| 1286 | if ($targetEntity === null || ! $stronglyConnectedComponents->hasNode($targetEntity)) { | ||
| 1287 | continue; | ||
| 1288 | } | ||
| 1289 | |||
| 1290 | $stronglyConnectedComponents->addEdge($entity, $targetEntity); | ||
| 1291 | } | ||
| 1292 | } | ||
| 1293 | |||
| 1294 | $stronglyConnectedComponents->findStronglyConnectedComponents(); | ||
| 1295 | |||
| 1296 | // Now do the actual topological sorting to find the delete order. | ||
| 1297 | foreach ($this->entityDeletions as $entity) { | ||
| 1298 | $class = $this->em->getClassMetadata($entity::class); | ||
| 1299 | |||
| 1300 | // Get the entities representing the SCC | ||
| 1301 | $entityComponent = $stronglyConnectedComponents->getNodeRepresentingStronglyConnectedComponent($entity); | ||
| 1302 | |||
| 1303 | // When $entity is part of a non-trivial strongly connected component group | ||
| 1304 | // (a group containing not only those entities alone), make sure we process it _after_ the | ||
| 1305 | // entity representing the group. | ||
| 1306 | // The dependency direction implies that "$entity depends on $entityComponent | ||
| 1307 | // being deleted first". The topological sort will output the depended-upon nodes first. | ||
| 1308 | if ($entityComponent !== $entity) { | ||
| 1309 | $sort->addEdge($entity, $entityComponent, false); | ||
| 1310 | } | ||
| 1311 | |||
| 1312 | foreach ($class->associationMappings as $assoc) { | ||
| 1313 | // We only need to consider the owning sides of to-one associations, | ||
| 1314 | // since many-to-many associations can always be (and have already been) | ||
| 1315 | // deleted in a preceding step. | ||
| 1316 | if (! $assoc->isToOneOwningSide()) { | ||
| 1317 | continue; | ||
| 1318 | } | ||
| 1319 | |||
| 1320 | // For associations that implement a database-level set null operation, | ||
| 1321 | // we do not have to follow a particular order: If the referred-to entity is | ||
| 1322 | // deleted first, the DBMS will temporarily set the foreign key to NULL (SET NULL). | ||
| 1323 | // So, we can skip it in the computation. | ||
| 1324 | $joinColumns = reset($assoc->joinColumns); | ||
| 1325 | if (isset($joinColumns->onDelete)) { | ||
| 1326 | $onDeleteOption = strtolower($joinColumns->onDelete); | ||
| 1327 | if ($onDeleteOption === 'set null') { | ||
| 1328 | continue; | ||
| 1329 | } | ||
| 1330 | } | ||
| 1331 | |||
| 1332 | $targetEntity = $class->getFieldValue($entity, $assoc->fieldName); | ||
| 1333 | |||
| 1334 | // If the association does not refer to another entity or that entity | ||
| 1335 | // is not to be deleted, there is no ordering problem and we can | ||
| 1336 | // skip this particular association. | ||
| 1337 | if ($targetEntity === null || ! $sort->hasNode($targetEntity)) { | ||
| 1338 | continue; | ||
| 1339 | } | ||
| 1340 | |||
| 1341 | // Get the entities representing the SCC | ||
| 1342 | $targetEntityComponent = $stronglyConnectedComponents->getNodeRepresentingStronglyConnectedComponent($targetEntity); | ||
| 1343 | |||
| 1344 | // When we have a dependency between two different groups of strongly connected nodes, | ||
| 1345 | // add it to the computation. | ||
| 1346 | // The dependency direction implies that "$targetEntityComponent depends on $entityComponent | ||
| 1347 | // being deleted first". The topological sort will output the depended-upon nodes first, | ||
| 1348 | // so we can work through the result in the returned order. | ||
| 1349 | if ($targetEntityComponent !== $entityComponent) { | ||
| 1350 | $sort->addEdge($targetEntityComponent, $entityComponent, false); | ||
| 1351 | } | ||
| 1352 | } | ||
| 1353 | } | ||
| 1354 | |||
| 1355 | return $sort->sort(); | ||
| 1356 | } | ||
| 1357 | |||
| 1358 | /** | ||
| 1359 | * Schedules an entity for insertion into the database. | ||
| 1360 | * If the entity already has an identifier, it will be added to the identity map. | ||
| 1361 | * | ||
| 1362 | * @throws ORMInvalidArgumentException | ||
| 1363 | * @throws InvalidArgumentException | ||
| 1364 | */ | ||
| 1365 | public function scheduleForInsert(object $entity): void | ||
| 1366 | { | ||
| 1367 | $oid = spl_object_id($entity); | ||
| 1368 | |||
| 1369 | if (isset($this->entityUpdates[$oid])) { | ||
| 1370 | throw new InvalidArgumentException('Dirty entity can not be scheduled for insertion.'); | ||
| 1371 | } | ||
| 1372 | |||
| 1373 | if (isset($this->entityDeletions[$oid])) { | ||
| 1374 | throw ORMInvalidArgumentException::scheduleInsertForRemovedEntity($entity); | ||
| 1375 | } | ||
| 1376 | |||
| 1377 | if (isset($this->originalEntityData[$oid]) && ! isset($this->entityInsertions[$oid])) { | ||
| 1378 | throw ORMInvalidArgumentException::scheduleInsertForManagedEntity($entity); | ||
| 1379 | } | ||
| 1380 | |||
| 1381 | if (isset($this->entityInsertions[$oid])) { | ||
| 1382 | throw ORMInvalidArgumentException::scheduleInsertTwice($entity); | ||
| 1383 | } | ||
| 1384 | |||
| 1385 | $this->entityInsertions[$oid] = $entity; | ||
| 1386 | |||
| 1387 | if (isset($this->entityIdentifiers[$oid])) { | ||
| 1388 | $this->addToIdentityMap($entity); | ||
| 1389 | } | ||
| 1390 | } | ||
| 1391 | |||
| 1392 | /** | ||
| 1393 | * Checks whether an entity is scheduled for insertion. | ||
| 1394 | */ | ||
| 1395 | public function isScheduledForInsert(object $entity): bool | ||
| 1396 | { | ||
| 1397 | return isset($this->entityInsertions[spl_object_id($entity)]); | ||
| 1398 | } | ||
| 1399 | |||
| 1400 | /** | ||
| 1401 | * Schedules an entity for being updated. | ||
| 1402 | * | ||
| 1403 | * @throws ORMInvalidArgumentException | ||
| 1404 | */ | ||
| 1405 | public function scheduleForUpdate(object $entity): void | ||
| 1406 | { | ||
| 1407 | $oid = spl_object_id($entity); | ||
| 1408 | |||
| 1409 | if (! isset($this->entityIdentifiers[$oid])) { | ||
| 1410 | throw ORMInvalidArgumentException::entityHasNoIdentity($entity, 'scheduling for update'); | ||
| 1411 | } | ||
| 1412 | |||
| 1413 | if (isset($this->entityDeletions[$oid])) { | ||
| 1414 | throw ORMInvalidArgumentException::entityIsRemoved($entity, 'schedule for update'); | ||
| 1415 | } | ||
| 1416 | |||
| 1417 | if (! isset($this->entityUpdates[$oid]) && ! isset($this->entityInsertions[$oid])) { | ||
| 1418 | $this->entityUpdates[$oid] = $entity; | ||
| 1419 | } | ||
| 1420 | } | ||
| 1421 | |||
| 1422 | /** | ||
| 1423 | * INTERNAL: | ||
| 1424 | * Schedules an extra update that will be executed immediately after the | ||
| 1425 | * regular entity updates within the currently running commit cycle. | ||
| 1426 | * | ||
| 1427 | * Extra updates for entities are stored as (entity, changeset) tuples. | ||
| 1428 | * | ||
| 1429 | * @psalm-param array<string, array{mixed, mixed}> $changeset The changeset of the entity (what to update). | ||
| 1430 | * | ||
| 1431 | * @ignore | ||
| 1432 | */ | ||
| 1433 | public function scheduleExtraUpdate(object $entity, array $changeset): void | ||
| 1434 | { | ||
| 1435 | $oid = spl_object_id($entity); | ||
| 1436 | $extraUpdate = [$entity, $changeset]; | ||
| 1437 | |||
| 1438 | if (isset($this->extraUpdates[$oid])) { | ||
| 1439 | [, $changeset2] = $this->extraUpdates[$oid]; | ||
| 1440 | |||
| 1441 | $extraUpdate = [$entity, $changeset + $changeset2]; | ||
| 1442 | } | ||
| 1443 | |||
| 1444 | $this->extraUpdates[$oid] = $extraUpdate; | ||
| 1445 | } | ||
| 1446 | |||
| 1447 | /** | ||
| 1448 | * Checks whether an entity is registered as dirty in the unit of work. | ||
| 1449 | * Note: Is not very useful currently as dirty entities are only registered | ||
| 1450 | * at commit time. | ||
| 1451 | */ | ||
| 1452 | public function isScheduledForUpdate(object $entity): bool | ||
| 1453 | { | ||
| 1454 | return isset($this->entityUpdates[spl_object_id($entity)]); | ||
| 1455 | } | ||
| 1456 | |||
| 1457 | /** | ||
| 1458 | * Checks whether an entity is registered to be checked in the unit of work. | ||
| 1459 | */ | ||
| 1460 | public function isScheduledForDirtyCheck(object $entity): bool | ||
| 1461 | { | ||
| 1462 | $rootEntityName = $this->em->getClassMetadata($entity::class)->rootEntityName; | ||
| 1463 | |||
| 1464 | return isset($this->scheduledForSynchronization[$rootEntityName][spl_object_id($entity)]); | ||
| 1465 | } | ||
| 1466 | |||
| 1467 | /** | ||
| 1468 | * INTERNAL: | ||
| 1469 | * Schedules an entity for deletion. | ||
| 1470 | */ | ||
| 1471 | public function scheduleForDelete(object $entity): void | ||
| 1472 | { | ||
| 1473 | $oid = spl_object_id($entity); | ||
| 1474 | |||
| 1475 | if (isset($this->entityInsertions[$oid])) { | ||
| 1476 | if ($this->isInIdentityMap($entity)) { | ||
| 1477 | $this->removeFromIdentityMap($entity); | ||
| 1478 | } | ||
| 1479 | |||
| 1480 | unset($this->entityInsertions[$oid], $this->entityStates[$oid]); | ||
| 1481 | |||
| 1482 | return; // entity has not been persisted yet, so nothing more to do. | ||
| 1483 | } | ||
| 1484 | |||
| 1485 | if (! $this->isInIdentityMap($entity)) { | ||
| 1486 | return; | ||
| 1487 | } | ||
| 1488 | |||
| 1489 | unset($this->entityUpdates[$oid]); | ||
| 1490 | |||
| 1491 | if (! isset($this->entityDeletions[$oid])) { | ||
| 1492 | $this->entityDeletions[$oid] = $entity; | ||
| 1493 | $this->entityStates[$oid] = self::STATE_REMOVED; | ||
| 1494 | } | ||
| 1495 | } | ||
| 1496 | |||
| 1497 | /** | ||
| 1498 | * Checks whether an entity is registered as removed/deleted with the unit | ||
| 1499 | * of work. | ||
| 1500 | */ | ||
| 1501 | public function isScheduledForDelete(object $entity): bool | ||
| 1502 | { | ||
| 1503 | return isset($this->entityDeletions[spl_object_id($entity)]); | ||
| 1504 | } | ||
| 1505 | |||
| 1506 | /** | ||
| 1507 | * Checks whether an entity is scheduled for insertion, update or deletion. | ||
| 1508 | */ | ||
| 1509 | public function isEntityScheduled(object $entity): bool | ||
| 1510 | { | ||
| 1511 | $oid = spl_object_id($entity); | ||
| 1512 | |||
| 1513 | return isset($this->entityInsertions[$oid]) | ||
| 1514 | || isset($this->entityUpdates[$oid]) | ||
| 1515 | || isset($this->entityDeletions[$oid]); | ||
| 1516 | } | ||
| 1517 | |||
| 1518 | /** | ||
| 1519 | * INTERNAL: | ||
| 1520 | * Registers an entity in the identity map. | ||
| 1521 | * Note that entities in a hierarchy are registered with the class name of | ||
| 1522 | * the root entity. | ||
| 1523 | * | ||
| 1524 | * @return bool TRUE if the registration was successful, FALSE if the identity of | ||
| 1525 | * the entity in question is already managed. | ||
| 1526 | * | ||
| 1527 | * @throws ORMInvalidArgumentException | ||
| 1528 | * @throws EntityIdentityCollisionException | ||
| 1529 | * | ||
| 1530 | * @ignore | ||
| 1531 | */ | ||
| 1532 | public function addToIdentityMap(object $entity): bool | ||
| 1533 | { | ||
| 1534 | $classMetadata = $this->em->getClassMetadata($entity::class); | ||
| 1535 | $idHash = $this->getIdHashByEntity($entity); | ||
| 1536 | $className = $classMetadata->rootEntityName; | ||
| 1537 | |||
| 1538 | if (isset($this->identityMap[$className][$idHash])) { | ||
| 1539 | if ($this->identityMap[$className][$idHash] !== $entity) { | ||
| 1540 | throw EntityIdentityCollisionException::create($this->identityMap[$className][$idHash], $entity, $idHash); | ||
| 1541 | } | ||
| 1542 | |||
| 1543 | return false; | ||
| 1544 | } | ||
| 1545 | |||
| 1546 | $this->identityMap[$className][$idHash] = $entity; | ||
| 1547 | |||
| 1548 | return true; | ||
| 1549 | } | ||
| 1550 | |||
| 1551 | /** | ||
| 1552 | * Gets the id hash of an entity by its identifier. | ||
| 1553 | * | ||
| 1554 | * @param array<string|int, mixed> $identifier The identifier of an entity | ||
| 1555 | * | ||
| 1556 | * @return string The entity id hash. | ||
| 1557 | */ | ||
| 1558 | final public static function getIdHashByIdentifier(array $identifier): string | ||
| 1559 | { | ||
| 1560 | foreach ($identifier as $k => $value) { | ||
| 1561 | if ($value instanceof BackedEnum) { | ||
| 1562 | $identifier[$k] = $value->value; | ||
| 1563 | } | ||
| 1564 | } | ||
| 1565 | |||
| 1566 | return implode( | ||
| 1567 | ' ', | ||
| 1568 | $identifier, | ||
| 1569 | ); | ||
| 1570 | } | ||
| 1571 | |||
| 1572 | /** | ||
| 1573 | * Gets the id hash of an entity. | ||
| 1574 | * | ||
| 1575 | * @param object $entity The entity managed by Unit Of Work | ||
| 1576 | * | ||
| 1577 | * @return string The entity id hash. | ||
| 1578 | */ | ||
| 1579 | public function getIdHashByEntity(object $entity): string | ||
| 1580 | { | ||
| 1581 | $identifier = $this->entityIdentifiers[spl_object_id($entity)]; | ||
| 1582 | |||
| 1583 | if (empty($identifier) || in_array(null, $identifier, true)) { | ||
| 1584 | $classMetadata = $this->em->getClassMetadata($entity::class); | ||
| 1585 | |||
| 1586 | throw ORMInvalidArgumentException::entityWithoutIdentity($classMetadata->name, $entity); | ||
| 1587 | } | ||
| 1588 | |||
| 1589 | return self::getIdHashByIdentifier($identifier); | ||
| 1590 | } | ||
| 1591 | |||
| 1592 | /** | ||
| 1593 | * Gets the state of an entity with regard to the current unit of work. | ||
| 1594 | * | ||
| 1595 | * @param int|null $assume The state to assume if the state is not yet known (not MANAGED or REMOVED). | ||
| 1596 | * This parameter can be set to improve performance of entity state detection | ||
| 1597 | * by potentially avoiding a database lookup if the distinction between NEW and DETACHED | ||
| 1598 | * is either known or does not matter for the caller of the method. | ||
| 1599 | * @psalm-param self::STATE_*|null $assume | ||
| 1600 | * | ||
| 1601 | * @psalm-return self::STATE_* | ||
| 1602 | */ | ||
| 1603 | public function getEntityState(object $entity, int|null $assume = null): int | ||
| 1604 | { | ||
| 1605 | $oid = spl_object_id($entity); | ||
| 1606 | |||
| 1607 | if (isset($this->entityStates[$oid])) { | ||
| 1608 | return $this->entityStates[$oid]; | ||
| 1609 | } | ||
| 1610 | |||
| 1611 | if ($assume !== null) { | ||
| 1612 | return $assume; | ||
| 1613 | } | ||
| 1614 | |||
| 1615 | // State can only be NEW or DETACHED, because MANAGED/REMOVED states are known. | ||
| 1616 | // Note that you can not remember the NEW or DETACHED state in _entityStates since | ||
| 1617 | // the UoW does not hold references to such objects and the object hash can be reused. | ||
| 1618 | // More generally because the state may "change" between NEW/DETACHED without the UoW being aware of it. | ||
| 1619 | $class = $this->em->getClassMetadata($entity::class); | ||
| 1620 | $id = $class->getIdentifierValues($entity); | ||
| 1621 | |||
| 1622 | if (! $id) { | ||
| 1623 | return self::STATE_NEW; | ||
| 1624 | } | ||
| 1625 | |||
| 1626 | if ($class->containsForeignIdentifier || $class->containsEnumIdentifier) { | ||
| 1627 | $id = $this->identifierFlattener->flattenIdentifier($class, $id); | ||
| 1628 | } | ||
| 1629 | |||
| 1630 | switch (true) { | ||
| 1631 | case $class->isIdentifierNatural(): | ||
| 1632 | // Check for a version field, if available, to avoid a db lookup. | ||
| 1633 | if ($class->isVersioned) { | ||
| 1634 | assert($class->versionField !== null); | ||
| 1635 | |||
| 1636 | return $class->getFieldValue($entity, $class->versionField) | ||
| 1637 | ? self::STATE_DETACHED | ||
| 1638 | : self::STATE_NEW; | ||
| 1639 | } | ||
| 1640 | |||
| 1641 | // Last try before db lookup: check the identity map. | ||
| 1642 | if ($this->tryGetById($id, $class->rootEntityName)) { | ||
| 1643 | return self::STATE_DETACHED; | ||
| 1644 | } | ||
| 1645 | |||
| 1646 | // db lookup | ||
| 1647 | if ($this->getEntityPersister($class->name)->exists($entity)) { | ||
| 1648 | return self::STATE_DETACHED; | ||
| 1649 | } | ||
| 1650 | |||
| 1651 | return self::STATE_NEW; | ||
| 1652 | |||
| 1653 | case ! $class->idGenerator->isPostInsertGenerator(): | ||
| 1654 | // if we have a pre insert generator we can't be sure that having an id | ||
| 1655 | // really means that the entity exists. We have to verify this through | ||
| 1656 | // the last resort: a db lookup | ||
| 1657 | |||
| 1658 | // Last try before db lookup: check the identity map. | ||
| 1659 | if ($this->tryGetById($id, $class->rootEntityName)) { | ||
| 1660 | return self::STATE_DETACHED; | ||
| 1661 | } | ||
| 1662 | |||
| 1663 | // db lookup | ||
| 1664 | if ($this->getEntityPersister($class->name)->exists($entity)) { | ||
| 1665 | return self::STATE_DETACHED; | ||
| 1666 | } | ||
| 1667 | |||
| 1668 | return self::STATE_NEW; | ||
| 1669 | |||
| 1670 | default: | ||
| 1671 | return self::STATE_DETACHED; | ||
| 1672 | } | ||
| 1673 | } | ||
| 1674 | |||
| 1675 | /** | ||
| 1676 | * INTERNAL: | ||
| 1677 | * Removes an entity from the identity map. This effectively detaches the | ||
| 1678 | * entity from the persistence management of Doctrine. | ||
| 1679 | * | ||
| 1680 | * @throws ORMInvalidArgumentException | ||
| 1681 | * | ||
| 1682 | * @ignore | ||
| 1683 | */ | ||
| 1684 | public function removeFromIdentityMap(object $entity): bool | ||
| 1685 | { | ||
| 1686 | $oid = spl_object_id($entity); | ||
| 1687 | $classMetadata = $this->em->getClassMetadata($entity::class); | ||
| 1688 | $idHash = self::getIdHashByIdentifier($this->entityIdentifiers[$oid]); | ||
| 1689 | |||
| 1690 | if ($idHash === '') { | ||
| 1691 | throw ORMInvalidArgumentException::entityHasNoIdentity($entity, 'remove from identity map'); | ||
| 1692 | } | ||
| 1693 | |||
| 1694 | $className = $classMetadata->rootEntityName; | ||
| 1695 | |||
| 1696 | if (isset($this->identityMap[$className][$idHash])) { | ||
| 1697 | unset($this->identityMap[$className][$idHash], $this->readOnlyObjects[$oid]); | ||
| 1698 | |||
| 1699 | //$this->entityStates[$oid] = self::STATE_DETACHED; | ||
| 1700 | |||
| 1701 | return true; | ||
| 1702 | } | ||
| 1703 | |||
| 1704 | return false; | ||
| 1705 | } | ||
| 1706 | |||
| 1707 | /** | ||
| 1708 | * INTERNAL: | ||
| 1709 | * Gets an entity in the identity map by its identifier hash. | ||
| 1710 | * | ||
| 1711 | * @ignore | ||
| 1712 | */ | ||
| 1713 | public function getByIdHash(string $idHash, string $rootClassName): object|null | ||
| 1714 | { | ||
| 1715 | return $this->identityMap[$rootClassName][$idHash]; | ||
| 1716 | } | ||
| 1717 | |||
| 1718 | /** | ||
| 1719 | * INTERNAL: | ||
| 1720 | * Tries to get an entity by its identifier hash. If no entity is found for | ||
| 1721 | * the given hash, FALSE is returned. | ||
| 1722 | * | ||
| 1723 | * @param mixed $idHash (must be possible to cast it to string) | ||
| 1724 | * | ||
| 1725 | * @return false|object The found entity or FALSE. | ||
| 1726 | * | ||
| 1727 | * @ignore | ||
| 1728 | */ | ||
| 1729 | public function tryGetByIdHash(mixed $idHash, string $rootClassName): object|false | ||
| 1730 | { | ||
| 1731 | $stringIdHash = (string) $idHash; | ||
| 1732 | |||
| 1733 | return $this->identityMap[$rootClassName][$stringIdHash] ?? false; | ||
| 1734 | } | ||
| 1735 | |||
| 1736 | /** | ||
| 1737 | * Checks whether an entity is registered in the identity map of this UnitOfWork. | ||
| 1738 | */ | ||
| 1739 | public function isInIdentityMap(object $entity): bool | ||
| 1740 | { | ||
| 1741 | $oid = spl_object_id($entity); | ||
| 1742 | |||
| 1743 | if (empty($this->entityIdentifiers[$oid])) { | ||
| 1744 | return false; | ||
| 1745 | } | ||
| 1746 | |||
| 1747 | $classMetadata = $this->em->getClassMetadata($entity::class); | ||
| 1748 | $idHash = self::getIdHashByIdentifier($this->entityIdentifiers[$oid]); | ||
| 1749 | |||
| 1750 | return isset($this->identityMap[$classMetadata->rootEntityName][$idHash]); | ||
| 1751 | } | ||
| 1752 | |||
| 1753 | /** | ||
| 1754 | * Persists an entity as part of the current unit of work. | ||
| 1755 | */ | ||
| 1756 | public function persist(object $entity): void | ||
| 1757 | { | ||
| 1758 | $visited = []; | ||
| 1759 | |||
| 1760 | $this->doPersist($entity, $visited); | ||
| 1761 | } | ||
| 1762 | |||
| 1763 | /** | ||
| 1764 | * Persists an entity as part of the current unit of work. | ||
| 1765 | * | ||
| 1766 | * This method is internally called during persist() cascades as it tracks | ||
| 1767 | * the already visited entities to prevent infinite recursions. | ||
| 1768 | * | ||
| 1769 | * @psalm-param array<int, object> $visited The already visited entities. | ||
| 1770 | * | ||
| 1771 | * @throws ORMInvalidArgumentException | ||
| 1772 | * @throws UnexpectedValueException | ||
| 1773 | */ | ||
| 1774 | private function doPersist(object $entity, array &$visited): void | ||
| 1775 | { | ||
| 1776 | $oid = spl_object_id($entity); | ||
| 1777 | |||
| 1778 | if (isset($visited[$oid])) { | ||
| 1779 | return; // Prevent infinite recursion | ||
| 1780 | } | ||
| 1781 | |||
| 1782 | $visited[$oid] = $entity; // Mark visited | ||
| 1783 | |||
| 1784 | $class = $this->em->getClassMetadata($entity::class); | ||
| 1785 | |||
| 1786 | // We assume NEW, so DETACHED entities result in an exception on flush (constraint violation). | ||
| 1787 | // If we would detect DETACHED here we would throw an exception anyway with the same | ||
| 1788 | // consequences (not recoverable/programming error), so just assuming NEW here | ||
| 1789 | // lets us avoid some database lookups for entities with natural identifiers. | ||
| 1790 | $entityState = $this->getEntityState($entity, self::STATE_NEW); | ||
| 1791 | |||
| 1792 | switch ($entityState) { | ||
| 1793 | case self::STATE_MANAGED: | ||
| 1794 | // Nothing to do, except if policy is "deferred explicit" | ||
| 1795 | if ($class->isChangeTrackingDeferredExplicit()) { | ||
| 1796 | $this->scheduleForDirtyCheck($entity); | ||
| 1797 | } | ||
| 1798 | |||
| 1799 | break; | ||
| 1800 | |||
| 1801 | case self::STATE_NEW: | ||
| 1802 | $this->persistNew($class, $entity); | ||
| 1803 | break; | ||
| 1804 | |||
| 1805 | case self::STATE_REMOVED: | ||
| 1806 | // Entity becomes managed again | ||
| 1807 | unset($this->entityDeletions[$oid]); | ||
| 1808 | $this->addToIdentityMap($entity); | ||
| 1809 | |||
| 1810 | $this->entityStates[$oid] = self::STATE_MANAGED; | ||
| 1811 | |||
| 1812 | if ($class->isChangeTrackingDeferredExplicit()) { | ||
| 1813 | $this->scheduleForDirtyCheck($entity); | ||
| 1814 | } | ||
| 1815 | |||
| 1816 | break; | ||
| 1817 | |||
| 1818 | case self::STATE_DETACHED: | ||
| 1819 | // Can actually not happen right now since we assume STATE_NEW. | ||
| 1820 | throw ORMInvalidArgumentException::detachedEntityCannot($entity, 'persisted'); | ||
| 1821 | |||
| 1822 | default: | ||
| 1823 | throw new UnexpectedValueException(sprintf( | ||
| 1824 | 'Unexpected entity state: %s. %s', | ||
| 1825 | $entityState, | ||
| 1826 | self::objToStr($entity), | ||
| 1827 | )); | ||
| 1828 | } | ||
| 1829 | |||
| 1830 | $this->cascadePersist($entity, $visited); | ||
| 1831 | } | ||
| 1832 | |||
| 1833 | /** | ||
| 1834 | * Deletes an entity as part of the current unit of work. | ||
| 1835 | */ | ||
| 1836 | public function remove(object $entity): void | ||
| 1837 | { | ||
| 1838 | $visited = []; | ||
| 1839 | |||
| 1840 | $this->doRemove($entity, $visited); | ||
| 1841 | } | ||
| 1842 | |||
| 1843 | /** | ||
| 1844 | * Deletes an entity as part of the current unit of work. | ||
| 1845 | * | ||
| 1846 | * This method is internally called during delete() cascades as it tracks | ||
| 1847 | * the already visited entities to prevent infinite recursions. | ||
| 1848 | * | ||
| 1849 | * @psalm-param array<int, object> $visited The map of the already visited entities. | ||
| 1850 | * | ||
| 1851 | * @throws ORMInvalidArgumentException If the instance is a detached entity. | ||
| 1852 | * @throws UnexpectedValueException | ||
| 1853 | */ | ||
| 1854 | private function doRemove(object $entity, array &$visited): void | ||
| 1855 | { | ||
| 1856 | $oid = spl_object_id($entity); | ||
| 1857 | |||
| 1858 | if (isset($visited[$oid])) { | ||
| 1859 | return; // Prevent infinite recursion | ||
| 1860 | } | ||
| 1861 | |||
| 1862 | $visited[$oid] = $entity; // mark visited | ||
| 1863 | |||
| 1864 | // Cascade first, because scheduleForDelete() removes the entity from the identity map, which | ||
| 1865 | // can cause problems when a lazy proxy has to be initialized for the cascade operation. | ||
| 1866 | $this->cascadeRemove($entity, $visited); | ||
| 1867 | |||
| 1868 | $class = $this->em->getClassMetadata($entity::class); | ||
| 1869 | $entityState = $this->getEntityState($entity); | ||
| 1870 | |||
| 1871 | switch ($entityState) { | ||
| 1872 | case self::STATE_NEW: | ||
| 1873 | case self::STATE_REMOVED: | ||
| 1874 | // nothing to do | ||
| 1875 | break; | ||
| 1876 | |||
| 1877 | case self::STATE_MANAGED: | ||
| 1878 | $invoke = $this->listenersInvoker->getSubscribedSystems($class, Events::preRemove); | ||
| 1879 | |||
| 1880 | if ($invoke !== ListenersInvoker::INVOKE_NONE) { | ||
| 1881 | $this->listenersInvoker->invoke($class, Events::preRemove, $entity, new PreRemoveEventArgs($entity, $this->em), $invoke); | ||
| 1882 | } | ||
| 1883 | |||
| 1884 | $this->scheduleForDelete($entity); | ||
| 1885 | break; | ||
| 1886 | |||
| 1887 | case self::STATE_DETACHED: | ||
| 1888 | throw ORMInvalidArgumentException::detachedEntityCannot($entity, 'removed'); | ||
| 1889 | |||
| 1890 | default: | ||
| 1891 | throw new UnexpectedValueException(sprintf( | ||
| 1892 | 'Unexpected entity state: %s. %s', | ||
| 1893 | $entityState, | ||
| 1894 | self::objToStr($entity), | ||
| 1895 | )); | ||
| 1896 | } | ||
| 1897 | } | ||
| 1898 | |||
| 1899 | /** | ||
| 1900 | * Detaches an entity from the persistence management. It's persistence will | ||
| 1901 | * no longer be managed by Doctrine. | ||
| 1902 | */ | ||
| 1903 | public function detach(object $entity): void | ||
| 1904 | { | ||
| 1905 | $visited = []; | ||
| 1906 | |||
| 1907 | $this->doDetach($entity, $visited); | ||
| 1908 | } | ||
| 1909 | |||
| 1910 | /** | ||
| 1911 | * Executes a detach operation on the given entity. | ||
| 1912 | * | ||
| 1913 | * @param mixed[] $visited | ||
| 1914 | * @param bool $noCascade if true, don't cascade detach operation. | ||
| 1915 | */ | ||
| 1916 | private function doDetach( | ||
| 1917 | object $entity, | ||
| 1918 | array &$visited, | ||
| 1919 | bool $noCascade = false, | ||
| 1920 | ): void { | ||
| 1921 | $oid = spl_object_id($entity); | ||
| 1922 | |||
| 1923 | if (isset($visited[$oid])) { | ||
| 1924 | return; // Prevent infinite recursion | ||
| 1925 | } | ||
| 1926 | |||
| 1927 | $visited[$oid] = $entity; // mark visited | ||
| 1928 | |||
| 1929 | switch ($this->getEntityState($entity, self::STATE_DETACHED)) { | ||
| 1930 | case self::STATE_MANAGED: | ||
| 1931 | if ($this->isInIdentityMap($entity)) { | ||
| 1932 | $this->removeFromIdentityMap($entity); | ||
| 1933 | } | ||
| 1934 | |||
| 1935 | unset( | ||
| 1936 | $this->entityInsertions[$oid], | ||
| 1937 | $this->entityUpdates[$oid], | ||
| 1938 | $this->entityDeletions[$oid], | ||
| 1939 | $this->entityIdentifiers[$oid], | ||
| 1940 | $this->entityStates[$oid], | ||
| 1941 | $this->originalEntityData[$oid], | ||
| 1942 | ); | ||
| 1943 | break; | ||
| 1944 | case self::STATE_NEW: | ||
| 1945 | case self::STATE_DETACHED: | ||
| 1946 | return; | ||
| 1947 | } | ||
| 1948 | |||
| 1949 | if (! $noCascade) { | ||
| 1950 | $this->cascadeDetach($entity, $visited); | ||
| 1951 | } | ||
| 1952 | } | ||
| 1953 | |||
| 1954 | /** | ||
| 1955 | * Refreshes the state of the given entity from the database, overwriting | ||
| 1956 | * any local, unpersisted changes. | ||
| 1957 | * | ||
| 1958 | * @psalm-param LockMode::*|null $lockMode | ||
| 1959 | * | ||
| 1960 | * @throws InvalidArgumentException If the entity is not MANAGED. | ||
| 1961 | * @throws TransactionRequiredException | ||
| 1962 | */ | ||
| 1963 | public function refresh(object $entity, LockMode|int|null $lockMode = null): void | ||
| 1964 | { | ||
| 1965 | $visited = []; | ||
| 1966 | |||
| 1967 | $this->doRefresh($entity, $visited, $lockMode); | ||
| 1968 | } | ||
| 1969 | |||
| 1970 | /** | ||
| 1971 | * Executes a refresh operation on an entity. | ||
| 1972 | * | ||
| 1973 | * @psalm-param array<int, object> $visited The already visited entities during cascades. | ||
| 1974 | * @psalm-param LockMode::*|null $lockMode | ||
| 1975 | * | ||
| 1976 | * @throws ORMInvalidArgumentException If the entity is not MANAGED. | ||
| 1977 | * @throws TransactionRequiredException | ||
| 1978 | */ | ||
| 1979 | private function doRefresh(object $entity, array &$visited, LockMode|int|null $lockMode = null): void | ||
| 1980 | { | ||
| 1981 | switch (true) { | ||
| 1982 | case $lockMode === LockMode::PESSIMISTIC_READ: | ||
| 1983 | case $lockMode === LockMode::PESSIMISTIC_WRITE: | ||
| 1984 | if (! $this->em->getConnection()->isTransactionActive()) { | ||
| 1985 | throw TransactionRequiredException::transactionRequired(); | ||
| 1986 | } | ||
| 1987 | } | ||
| 1988 | |||
| 1989 | $oid = spl_object_id($entity); | ||
| 1990 | |||
| 1991 | if (isset($visited[$oid])) { | ||
| 1992 | return; // Prevent infinite recursion | ||
| 1993 | } | ||
| 1994 | |||
| 1995 | $visited[$oid] = $entity; // mark visited | ||
| 1996 | |||
| 1997 | $class = $this->em->getClassMetadata($entity::class); | ||
| 1998 | |||
| 1999 | if ($this->getEntityState($entity) !== self::STATE_MANAGED) { | ||
| 2000 | throw ORMInvalidArgumentException::entityNotManaged($entity); | ||
| 2001 | } | ||
| 2002 | |||
| 2003 | $this->getEntityPersister($class->name)->refresh( | ||
| 2004 | array_combine($class->getIdentifierFieldNames(), $this->entityIdentifiers[$oid]), | ||
| 2005 | $entity, | ||
| 2006 | $lockMode, | ||
| 2007 | ); | ||
| 2008 | |||
| 2009 | $this->cascadeRefresh($entity, $visited, $lockMode); | ||
| 2010 | } | ||
| 2011 | |||
| 2012 | /** | ||
| 2013 | * Cascades a refresh operation to associated entities. | ||
| 2014 | * | ||
| 2015 | * @psalm-param array<int, object> $visited | ||
| 2016 | * @psalm-param LockMode::*|null $lockMode | ||
| 2017 | */ | ||
| 2018 | private function cascadeRefresh(object $entity, array &$visited, LockMode|int|null $lockMode = null): void | ||
| 2019 | { | ||
| 2020 | $class = $this->em->getClassMetadata($entity::class); | ||
| 2021 | |||
| 2022 | $associationMappings = array_filter( | ||
| 2023 | $class->associationMappings, | ||
| 2024 | static fn (AssociationMapping $assoc): bool => $assoc->isCascadeRefresh() | ||
| 2025 | ); | ||
| 2026 | |||
| 2027 | foreach ($associationMappings as $assoc) { | ||
| 2028 | $relatedEntities = $class->reflFields[$assoc->fieldName]->getValue($entity); | ||
| 2029 | |||
| 2030 | switch (true) { | ||
| 2031 | case $relatedEntities instanceof PersistentCollection: | ||
| 2032 | // Unwrap so that foreach() does not initialize | ||
| 2033 | $relatedEntities = $relatedEntities->unwrap(); | ||
| 2034 | // break; is commented intentionally! | ||
| 2035 | |||
| 2036 | case $relatedEntities instanceof Collection: | ||
| 2037 | case is_array($relatedEntities): | ||
| 2038 | foreach ($relatedEntities as $relatedEntity) { | ||
| 2039 | $this->doRefresh($relatedEntity, $visited, $lockMode); | ||
| 2040 | } | ||
| 2041 | |||
| 2042 | break; | ||
| 2043 | |||
| 2044 | case $relatedEntities !== null: | ||
| 2045 | $this->doRefresh($relatedEntities, $visited, $lockMode); | ||
| 2046 | break; | ||
| 2047 | |||
| 2048 | default: | ||
| 2049 | // Do nothing | ||
| 2050 | } | ||
| 2051 | } | ||
| 2052 | } | ||
| 2053 | |||
| 2054 | /** | ||
| 2055 | * Cascades a detach operation to associated entities. | ||
| 2056 | * | ||
| 2057 | * @param array<int, object> $visited | ||
| 2058 | */ | ||
| 2059 | private function cascadeDetach(object $entity, array &$visited): void | ||
| 2060 | { | ||
| 2061 | $class = $this->em->getClassMetadata($entity::class); | ||
| 2062 | |||
| 2063 | $associationMappings = array_filter( | ||
| 2064 | $class->associationMappings, | ||
| 2065 | static fn (AssociationMapping $assoc): bool => $assoc->isCascadeDetach() | ||
| 2066 | ); | ||
| 2067 | |||
| 2068 | foreach ($associationMappings as $assoc) { | ||
| 2069 | $relatedEntities = $class->reflFields[$assoc->fieldName]->getValue($entity); | ||
| 2070 | |||
| 2071 | switch (true) { | ||
| 2072 | case $relatedEntities instanceof PersistentCollection: | ||
| 2073 | // Unwrap so that foreach() does not initialize | ||
| 2074 | $relatedEntities = $relatedEntities->unwrap(); | ||
| 2075 | // break; is commented intentionally! | ||
| 2076 | |||
| 2077 | case $relatedEntities instanceof Collection: | ||
| 2078 | case is_array($relatedEntities): | ||
| 2079 | foreach ($relatedEntities as $relatedEntity) { | ||
| 2080 | $this->doDetach($relatedEntity, $visited); | ||
| 2081 | } | ||
| 2082 | |||
| 2083 | break; | ||
| 2084 | |||
| 2085 | case $relatedEntities !== null: | ||
| 2086 | $this->doDetach($relatedEntities, $visited); | ||
| 2087 | break; | ||
| 2088 | |||
| 2089 | default: | ||
| 2090 | // Do nothing | ||
| 2091 | } | ||
| 2092 | } | ||
| 2093 | } | ||
| 2094 | |||
| 2095 | /** | ||
| 2096 | * Cascades the save operation to associated entities. | ||
| 2097 | * | ||
| 2098 | * @psalm-param array<int, object> $visited | ||
| 2099 | */ | ||
| 2100 | private function cascadePersist(object $entity, array &$visited): void | ||
| 2101 | { | ||
| 2102 | if ($this->isUninitializedObject($entity)) { | ||
| 2103 | // nothing to do - proxy is not initialized, therefore we don't do anything with it | ||
| 2104 | return; | ||
| 2105 | } | ||
| 2106 | |||
| 2107 | $class = $this->em->getClassMetadata($entity::class); | ||
| 2108 | |||
| 2109 | $associationMappings = array_filter( | ||
| 2110 | $class->associationMappings, | ||
| 2111 | static fn (AssociationMapping $assoc): bool => $assoc->isCascadePersist() | ||
| 2112 | ); | ||
| 2113 | |||
| 2114 | foreach ($associationMappings as $assoc) { | ||
| 2115 | $relatedEntities = $class->reflFields[$assoc->fieldName]->getValue($entity); | ||
| 2116 | |||
| 2117 | switch (true) { | ||
| 2118 | case $relatedEntities instanceof PersistentCollection: | ||
| 2119 | // Unwrap so that foreach() does not initialize | ||
| 2120 | $relatedEntities = $relatedEntities->unwrap(); | ||
| 2121 | // break; is commented intentionally! | ||
| 2122 | |||
| 2123 | case $relatedEntities instanceof Collection: | ||
| 2124 | case is_array($relatedEntities): | ||
| 2125 | if ($assoc->isToMany() <= 0) { | ||
| 2126 | throw ORMInvalidArgumentException::invalidAssociation( | ||
| 2127 | $this->em->getClassMetadata($assoc->targetEntity), | ||
| 2128 | $assoc, | ||
| 2129 | $relatedEntities, | ||
| 2130 | ); | ||
| 2131 | } | ||
| 2132 | |||
| 2133 | foreach ($relatedEntities as $relatedEntity) { | ||
| 2134 | $this->doPersist($relatedEntity, $visited); | ||
| 2135 | } | ||
| 2136 | |||
| 2137 | break; | ||
| 2138 | |||
| 2139 | case $relatedEntities !== null: | ||
| 2140 | if (! $relatedEntities instanceof $assoc->targetEntity) { | ||
| 2141 | throw ORMInvalidArgumentException::invalidAssociation( | ||
| 2142 | $this->em->getClassMetadata($assoc->targetEntity), | ||
| 2143 | $assoc, | ||
| 2144 | $relatedEntities, | ||
| 2145 | ); | ||
| 2146 | } | ||
| 2147 | |||
| 2148 | $this->doPersist($relatedEntities, $visited); | ||
| 2149 | break; | ||
| 2150 | |||
| 2151 | default: | ||
| 2152 | // Do nothing | ||
| 2153 | } | ||
| 2154 | } | ||
| 2155 | } | ||
| 2156 | |||
| 2157 | /** | ||
| 2158 | * Cascades the delete operation to associated entities. | ||
| 2159 | * | ||
| 2160 | * @psalm-param array<int, object> $visited | ||
| 2161 | */ | ||
| 2162 | private function cascadeRemove(object $entity, array &$visited): void | ||
| 2163 | { | ||
| 2164 | $class = $this->em->getClassMetadata($entity::class); | ||
| 2165 | |||
| 2166 | $associationMappings = array_filter( | ||
| 2167 | $class->associationMappings, | ||
| 2168 | static fn (AssociationMapping $assoc): bool => $assoc->isCascadeRemove() | ||
| 2169 | ); | ||
| 2170 | |||
| 2171 | if ($associationMappings) { | ||
| 2172 | $this->initializeObject($entity); | ||
| 2173 | } | ||
| 2174 | |||
| 2175 | $entitiesToCascade = []; | ||
| 2176 | |||
| 2177 | foreach ($associationMappings as $assoc) { | ||
| 2178 | $relatedEntities = $class->reflFields[$assoc->fieldName]->getValue($entity); | ||
| 2179 | |||
| 2180 | switch (true) { | ||
| 2181 | case $relatedEntities instanceof Collection: | ||
| 2182 | case is_array($relatedEntities): | ||
| 2183 | // If its a PersistentCollection initialization is intended! No unwrap! | ||
| 2184 | foreach ($relatedEntities as $relatedEntity) { | ||
| 2185 | $entitiesToCascade[] = $relatedEntity; | ||
| 2186 | } | ||
| 2187 | |||
| 2188 | break; | ||
| 2189 | |||
| 2190 | case $relatedEntities !== null: | ||
| 2191 | $entitiesToCascade[] = $relatedEntities; | ||
| 2192 | break; | ||
| 2193 | |||
| 2194 | default: | ||
| 2195 | // Do nothing | ||
| 2196 | } | ||
| 2197 | } | ||
| 2198 | |||
| 2199 | foreach ($entitiesToCascade as $relatedEntity) { | ||
| 2200 | $this->doRemove($relatedEntity, $visited); | ||
| 2201 | } | ||
| 2202 | } | ||
| 2203 | |||
| 2204 | /** | ||
| 2205 | * Acquire a lock on the given entity. | ||
| 2206 | * | ||
| 2207 | * @psalm-param LockMode::* $lockMode | ||
| 2208 | * | ||
| 2209 | * @throws ORMInvalidArgumentException | ||
| 2210 | * @throws TransactionRequiredException | ||
| 2211 | * @throws OptimisticLockException | ||
| 2212 | */ | ||
| 2213 | public function lock(object $entity, LockMode|int $lockMode, DateTimeInterface|int|null $lockVersion = null): void | ||
| 2214 | { | ||
| 2215 | if ($this->getEntityState($entity, self::STATE_DETACHED) !== self::STATE_MANAGED) { | ||
| 2216 | throw ORMInvalidArgumentException::entityNotManaged($entity); | ||
| 2217 | } | ||
| 2218 | |||
| 2219 | $class = $this->em->getClassMetadata($entity::class); | ||
| 2220 | |||
| 2221 | switch (true) { | ||
| 2222 | case $lockMode === LockMode::OPTIMISTIC: | ||
| 2223 | if (! $class->isVersioned) { | ||
| 2224 | throw OptimisticLockException::notVersioned($class->name); | ||
| 2225 | } | ||
| 2226 | |||
| 2227 | if ($lockVersion === null) { | ||
| 2228 | return; | ||
| 2229 | } | ||
| 2230 | |||
| 2231 | $this->initializeObject($entity); | ||
| 2232 | |||
| 2233 | assert($class->versionField !== null); | ||
| 2234 | $entityVersion = $class->reflFields[$class->versionField]->getValue($entity); | ||
| 2235 | |||
| 2236 | // phpcs:ignore SlevomatCodingStandard.Operators.DisallowEqualOperators.DisallowedNotEqualOperator | ||
| 2237 | if ($entityVersion != $lockVersion) { | ||
| 2238 | throw OptimisticLockException::lockFailedVersionMismatch($entity, $lockVersion, $entityVersion); | ||
| 2239 | } | ||
| 2240 | |||
| 2241 | break; | ||
| 2242 | |||
| 2243 | case $lockMode === LockMode::NONE: | ||
| 2244 | case $lockMode === LockMode::PESSIMISTIC_READ: | ||
| 2245 | case $lockMode === LockMode::PESSIMISTIC_WRITE: | ||
| 2246 | if (! $this->em->getConnection()->isTransactionActive()) { | ||
| 2247 | throw TransactionRequiredException::transactionRequired(); | ||
| 2248 | } | ||
| 2249 | |||
| 2250 | $oid = spl_object_id($entity); | ||
| 2251 | |||
| 2252 | $this->getEntityPersister($class->name)->lock( | ||
| 2253 | array_combine($class->getIdentifierFieldNames(), $this->entityIdentifiers[$oid]), | ||
| 2254 | $lockMode, | ||
| 2255 | ); | ||
| 2256 | break; | ||
| 2257 | |||
| 2258 | default: | ||
| 2259 | // Do nothing | ||
| 2260 | } | ||
| 2261 | } | ||
| 2262 | |||
| 2263 | /** | ||
| 2264 | * Clears the UnitOfWork. | ||
| 2265 | */ | ||
| 2266 | public function clear(): void | ||
| 2267 | { | ||
| 2268 | $this->identityMap = | ||
| 2269 | $this->entityIdentifiers = | ||
| 2270 | $this->originalEntityData = | ||
| 2271 | $this->entityChangeSets = | ||
| 2272 | $this->entityStates = | ||
| 2273 | $this->scheduledForSynchronization = | ||
| 2274 | $this->entityInsertions = | ||
| 2275 | $this->entityUpdates = | ||
| 2276 | $this->entityDeletions = | ||
| 2277 | $this->nonCascadedNewDetectedEntities = | ||
| 2278 | $this->collectionDeletions = | ||
| 2279 | $this->collectionUpdates = | ||
| 2280 | $this->extraUpdates = | ||
| 2281 | $this->readOnlyObjects = | ||
| 2282 | $this->pendingCollectionElementRemovals = | ||
| 2283 | $this->visitedCollections = | ||
| 2284 | $this->eagerLoadingEntities = | ||
| 2285 | $this->eagerLoadingCollections = | ||
| 2286 | $this->orphanRemovals = []; | ||
| 2287 | |||
| 2288 | if ($this->evm->hasListeners(Events::onClear)) { | ||
| 2289 | $this->evm->dispatchEvent(Events::onClear, new OnClearEventArgs($this->em)); | ||
| 2290 | } | ||
| 2291 | } | ||
| 2292 | |||
| 2293 | /** | ||
| 2294 | * INTERNAL: | ||
| 2295 | * Schedules an orphaned entity for removal. The remove() operation will be | ||
| 2296 | * invoked on that entity at the beginning of the next commit of this | ||
| 2297 | * UnitOfWork. | ||
| 2298 | * | ||
| 2299 | * @ignore | ||
| 2300 | */ | ||
| 2301 | public function scheduleOrphanRemoval(object $entity): void | ||
| 2302 | { | ||
| 2303 | $this->orphanRemovals[spl_object_id($entity)] = $entity; | ||
| 2304 | } | ||
| 2305 | |||
| 2306 | /** | ||
| 2307 | * INTERNAL: | ||
| 2308 | * Cancels a previously scheduled orphan removal. | ||
| 2309 | * | ||
| 2310 | * @ignore | ||
| 2311 | */ | ||
| 2312 | public function cancelOrphanRemoval(object $entity): void | ||
| 2313 | { | ||
| 2314 | unset($this->orphanRemovals[spl_object_id($entity)]); | ||
| 2315 | } | ||
| 2316 | |||
| 2317 | /** | ||
| 2318 | * INTERNAL: | ||
| 2319 | * Schedules a complete collection for removal when this UnitOfWork commits. | ||
| 2320 | */ | ||
| 2321 | public function scheduleCollectionDeletion(PersistentCollection $coll): void | ||
| 2322 | { | ||
| 2323 | $coid = spl_object_id($coll); | ||
| 2324 | |||
| 2325 | // TODO: if $coll is already scheduled for recreation ... what to do? | ||
| 2326 | // Just remove $coll from the scheduled recreations? | ||
| 2327 | unset($this->collectionUpdates[$coid]); | ||
| 2328 | |||
| 2329 | $this->collectionDeletions[$coid] = $coll; | ||
| 2330 | } | ||
| 2331 | |||
| 2332 | public function isCollectionScheduledForDeletion(PersistentCollection $coll): bool | ||
| 2333 | { | ||
| 2334 | return isset($this->collectionDeletions[spl_object_id($coll)]); | ||
| 2335 | } | ||
| 2336 | |||
| 2337 | /** | ||
| 2338 | * INTERNAL: | ||
| 2339 | * Creates an entity. Used for reconstitution of persistent entities. | ||
| 2340 | * | ||
| 2341 | * Internal note: Highly performance-sensitive method. | ||
| 2342 | * | ||
| 2343 | * @param string $className The name of the entity class. | ||
| 2344 | * @param mixed[] $data The data for the entity. | ||
| 2345 | * @param mixed[] $hints Any hints to account for during reconstitution/lookup of the entity. | ||
| 2346 | * @psalm-param class-string $className | ||
| 2347 | * @psalm-param array<string, mixed> $hints | ||
| 2348 | * | ||
| 2349 | * @return object The managed entity instance. | ||
| 2350 | * | ||
| 2351 | * @ignore | ||
| 2352 | * @todo Rename: getOrCreateEntity | ||
| 2353 | */ | ||
| 2354 | public function createEntity(string $className, array $data, array &$hints = []): object | ||
| 2355 | { | ||
| 2356 | $class = $this->em->getClassMetadata($className); | ||
| 2357 | |||
| 2358 | $id = $this->identifierFlattener->flattenIdentifier($class, $data); | ||
| 2359 | $idHash = self::getIdHashByIdentifier($id); | ||
| 2360 | |||
| 2361 | if (isset($this->identityMap[$class->rootEntityName][$idHash])) { | ||
| 2362 | $entity = $this->identityMap[$class->rootEntityName][$idHash]; | ||
| 2363 | $oid = spl_object_id($entity); | ||
| 2364 | |||
| 2365 | if ( | ||
| 2366 | isset($hints[Query::HINT_REFRESH], $hints[Query::HINT_REFRESH_ENTITY]) | ||
| 2367 | ) { | ||
| 2368 | $unmanagedProxy = $hints[Query::HINT_REFRESH_ENTITY]; | ||
| 2369 | if ( | ||
| 2370 | $unmanagedProxy !== $entity | ||
| 2371 | && $this->isIdentifierEquals($unmanagedProxy, $entity) | ||
| 2372 | ) { | ||
| 2373 | // We will hydrate the given un-managed proxy anyway: | ||
| 2374 | // continue work, but consider it the entity from now on | ||
| 2375 | $entity = $unmanagedProxy; | ||
| 2376 | } | ||
| 2377 | } | ||
| 2378 | |||
| 2379 | if ($this->isUninitializedObject($entity)) { | ||
| 2380 | $entity->__setInitialized(true); | ||
| 2381 | } else { | ||
| 2382 | if ( | ||
| 2383 | ! isset($hints[Query::HINT_REFRESH]) | ||
| 2384 | || (isset($hints[Query::HINT_REFRESH_ENTITY]) && $hints[Query::HINT_REFRESH_ENTITY] !== $entity) | ||
| 2385 | ) { | ||
| 2386 | return $entity; | ||
| 2387 | } | ||
| 2388 | } | ||
| 2389 | |||
| 2390 | $this->originalEntityData[$oid] = $data; | ||
| 2391 | } else { | ||
| 2392 | $entity = $class->newInstance(); | ||
| 2393 | $oid = spl_object_id($entity); | ||
| 2394 | $this->registerManaged($entity, $id, $data); | ||
| 2395 | |||
| 2396 | if (isset($hints[Query::HINT_READ_ONLY])) { | ||
| 2397 | $this->readOnlyObjects[$oid] = true; | ||
| 2398 | } | ||
| 2399 | } | ||
| 2400 | |||
| 2401 | foreach ($data as $field => $value) { | ||
| 2402 | if (isset($class->fieldMappings[$field])) { | ||
| 2403 | $class->reflFields[$field]->setValue($entity, $value); | ||
| 2404 | } | ||
| 2405 | } | ||
| 2406 | |||
| 2407 | // Loading the entity right here, if its in the eager loading map get rid of it there. | ||
| 2408 | unset($this->eagerLoadingEntities[$class->rootEntityName][$idHash]); | ||
| 2409 | |||
| 2410 | if (isset($this->eagerLoadingEntities[$class->rootEntityName]) && ! $this->eagerLoadingEntities[$class->rootEntityName]) { | ||
| 2411 | unset($this->eagerLoadingEntities[$class->rootEntityName]); | ||
| 2412 | } | ||
| 2413 | |||
| 2414 | foreach ($class->associationMappings as $field => $assoc) { | ||
| 2415 | // Check if the association is not among the fetch-joined associations already. | ||
| 2416 | if (isset($hints['fetchAlias'], $hints['fetched'][$hints['fetchAlias']][$field])) { | ||
| 2417 | continue; | ||
| 2418 | } | ||
| 2419 | |||
| 2420 | if (! isset($hints['fetchMode'][$class->name][$field])) { | ||
| 2421 | $hints['fetchMode'][$class->name][$field] = $assoc->fetch; | ||
| 2422 | } | ||
| 2423 | |||
| 2424 | $targetClass = $this->em->getClassMetadata($assoc->targetEntity); | ||
| 2425 | |||
| 2426 | switch (true) { | ||
| 2427 | case $assoc->isToOne(): | ||
| 2428 | if (! $assoc->isOwningSide()) { | ||
| 2429 | // use the given entity association | ||
| 2430 | if (isset($data[$field]) && is_object($data[$field]) && isset($this->entityStates[spl_object_id($data[$field])])) { | ||
| 2431 | $this->originalEntityData[$oid][$field] = $data[$field]; | ||
| 2432 | |||
| 2433 | $class->reflFields[$field]->setValue($entity, $data[$field]); | ||
| 2434 | $targetClass->reflFields[$assoc->mappedBy]->setValue($data[$field], $entity); | ||
| 2435 | |||
| 2436 | continue 2; | ||
| 2437 | } | ||
| 2438 | |||
| 2439 | // Inverse side of x-to-one can never be lazy | ||
| 2440 | $class->reflFields[$field]->setValue($entity, $this->getEntityPersister($assoc->targetEntity)->loadOneToOneEntity($assoc, $entity)); | ||
| 2441 | |||
| 2442 | continue 2; | ||
| 2443 | } | ||
| 2444 | |||
| 2445 | // use the entity association | ||
| 2446 | if (isset($data[$field]) && is_object($data[$field]) && isset($this->entityStates[spl_object_id($data[$field])])) { | ||
| 2447 | $class->reflFields[$field]->setValue($entity, $data[$field]); | ||
| 2448 | $this->originalEntityData[$oid][$field] = $data[$field]; | ||
| 2449 | |||
| 2450 | break; | ||
| 2451 | } | ||
| 2452 | |||
| 2453 | $associatedId = []; | ||
| 2454 | |||
| 2455 | assert($assoc->isToOneOwningSide()); | ||
| 2456 | // TODO: Is this even computed right in all cases of composite keys? | ||
| 2457 | foreach ($assoc->targetToSourceKeyColumns as $targetColumn => $srcColumn) { | ||
| 2458 | $joinColumnValue = $data[$srcColumn] ?? null; | ||
| 2459 | |||
| 2460 | if ($joinColumnValue !== null) { | ||
| 2461 | if ($joinColumnValue instanceof BackedEnum) { | ||
| 2462 | $joinColumnValue = $joinColumnValue->value; | ||
| 2463 | } | ||
| 2464 | |||
| 2465 | if ($targetClass->containsForeignIdentifier) { | ||
| 2466 | $associatedId[$targetClass->getFieldForColumn($targetColumn)] = $joinColumnValue; | ||
| 2467 | } else { | ||
| 2468 | $associatedId[$targetClass->fieldNames[$targetColumn]] = $joinColumnValue; | ||
| 2469 | } | ||
| 2470 | } elseif (in_array($targetClass->getFieldForColumn($targetColumn), $targetClass->identifier, true)) { | ||
| 2471 | // the missing key is part of target's entity primary key | ||
| 2472 | $associatedId = []; | ||
| 2473 | break; | ||
| 2474 | } | ||
| 2475 | } | ||
| 2476 | |||
| 2477 | if (! $associatedId) { | ||
| 2478 | // Foreign key is NULL | ||
| 2479 | $class->reflFields[$field]->setValue($entity, null); | ||
| 2480 | $this->originalEntityData[$oid][$field] = null; | ||
| 2481 | |||
| 2482 | break; | ||
| 2483 | } | ||
| 2484 | |||
| 2485 | // Foreign key is set | ||
| 2486 | // Check identity map first | ||
| 2487 | // FIXME: Can break easily with composite keys if join column values are in | ||
| 2488 | // wrong order. The correct order is the one in ClassMetadata#identifier. | ||
| 2489 | $relatedIdHash = self::getIdHashByIdentifier($associatedId); | ||
| 2490 | |||
| 2491 | switch (true) { | ||
| 2492 | case isset($this->identityMap[$targetClass->rootEntityName][$relatedIdHash]): | ||
| 2493 | $newValue = $this->identityMap[$targetClass->rootEntityName][$relatedIdHash]; | ||
| 2494 | |||
| 2495 | // If this is an uninitialized proxy, we are deferring eager loads, | ||
| 2496 | // this association is marked as eager fetch, and its an uninitialized proxy (wtf!) | ||
| 2497 | // then we can append this entity for eager loading! | ||
| 2498 | if ( | ||
| 2499 | $hints['fetchMode'][$class->name][$field] === ClassMetadata::FETCH_EAGER && | ||
| 2500 | isset($hints[self::HINT_DEFEREAGERLOAD]) && | ||
| 2501 | ! $targetClass->isIdentifierComposite && | ||
| 2502 | $this->isUninitializedObject($newValue) | ||
| 2503 | ) { | ||
| 2504 | $this->eagerLoadingEntities[$targetClass->rootEntityName][$relatedIdHash] = current($associatedId); | ||
| 2505 | } | ||
| 2506 | |||
| 2507 | break; | ||
| 2508 | |||
| 2509 | case $targetClass->subClasses: | ||
| 2510 | // If it might be a subtype, it can not be lazy. There isn't even | ||
| 2511 | // a way to solve this with deferred eager loading, which means putting | ||
| 2512 | // an entity with subclasses at a *-to-one location is really bad! (performance-wise) | ||
| 2513 | $newValue = $this->getEntityPersister($assoc->targetEntity)->loadOneToOneEntity($assoc, $entity, $associatedId); | ||
| 2514 | break; | ||
| 2515 | |||
| 2516 | default: | ||
| 2517 | $normalizedAssociatedId = $this->normalizeIdentifier($targetClass, $associatedId); | ||
| 2518 | |||
| 2519 | switch (true) { | ||
| 2520 | // We are negating the condition here. Other cases will assume it is valid! | ||
| 2521 | case $hints['fetchMode'][$class->name][$field] !== ClassMetadata::FETCH_EAGER: | ||
| 2522 | $newValue = $this->em->getProxyFactory()->getProxy($assoc->targetEntity, $normalizedAssociatedId); | ||
| 2523 | $this->registerManaged($newValue, $associatedId, []); | ||
| 2524 | break; | ||
| 2525 | |||
| 2526 | // Deferred eager load only works for single identifier classes | ||
| 2527 | case isset($hints[self::HINT_DEFEREAGERLOAD]) && | ||
| 2528 | $hints[self::HINT_DEFEREAGERLOAD] && | ||
| 2529 | ! $targetClass->isIdentifierComposite: | ||
| 2530 | // TODO: Is there a faster approach? | ||
| 2531 | $this->eagerLoadingEntities[$targetClass->rootEntityName][$relatedIdHash] = current($normalizedAssociatedId); | ||
| 2532 | |||
| 2533 | $newValue = $this->em->getProxyFactory()->getProxy($assoc->targetEntity, $normalizedAssociatedId); | ||
| 2534 | $this->registerManaged($newValue, $associatedId, []); | ||
| 2535 | break; | ||
| 2536 | |||
| 2537 | default: | ||
| 2538 | // TODO: This is very imperformant, ignore it? | ||
| 2539 | $newValue = $this->em->find($assoc->targetEntity, $normalizedAssociatedId); | ||
| 2540 | break; | ||
| 2541 | } | ||
| 2542 | } | ||
| 2543 | |||
| 2544 | $this->originalEntityData[$oid][$field] = $newValue; | ||
| 2545 | $class->reflFields[$field]->setValue($entity, $newValue); | ||
| 2546 | |||
| 2547 | if ($assoc->inversedBy !== null && $assoc->isOneToOne() && $newValue !== null) { | ||
| 2548 | $inverseAssoc = $targetClass->associationMappings[$assoc->inversedBy]; | ||
| 2549 | $targetClass->reflFields[$inverseAssoc->fieldName]->setValue($newValue, $entity); | ||
| 2550 | } | ||
| 2551 | |||
| 2552 | break; | ||
| 2553 | |||
| 2554 | default: | ||
| 2555 | assert($assoc->isToMany()); | ||
| 2556 | // Ignore if its a cached collection | ||
| 2557 | if (isset($hints[Query::HINT_CACHE_ENABLED]) && $class->getFieldValue($entity, $field) instanceof PersistentCollection) { | ||
| 2558 | break; | ||
| 2559 | } | ||
| 2560 | |||
| 2561 | // use the given collection | ||
| 2562 | if (isset($data[$field]) && $data[$field] instanceof PersistentCollection) { | ||
| 2563 | $data[$field]->setOwner($entity, $assoc); | ||
| 2564 | |||
| 2565 | $class->reflFields[$field]->setValue($entity, $data[$field]); | ||
| 2566 | $this->originalEntityData[$oid][$field] = $data[$field]; | ||
| 2567 | |||
| 2568 | break; | ||
| 2569 | } | ||
| 2570 | |||
| 2571 | // Inject collection | ||
| 2572 | $pColl = new PersistentCollection($this->em, $targetClass, new ArrayCollection()); | ||
| 2573 | $pColl->setOwner($entity, $assoc); | ||
| 2574 | $pColl->setInitialized(false); | ||
| 2575 | |||
| 2576 | $reflField = $class->reflFields[$field]; | ||
| 2577 | $reflField->setValue($entity, $pColl); | ||
| 2578 | |||
| 2579 | if ($hints['fetchMode'][$class->name][$field] === ClassMetadata::FETCH_EAGER) { | ||
| 2580 | $isIteration = isset($hints[Query::HINT_INTERNAL_ITERATION]) && $hints[Query::HINT_INTERNAL_ITERATION]; | ||
| 2581 | if (! $isIteration && $assoc->isOneToMany() && ! $targetClass->isIdentifierComposite && ! $assoc->isIndexed()) { | ||
| 2582 | $this->scheduleCollectionForBatchLoading($pColl, $class); | ||
| 2583 | } else { | ||
| 2584 | $this->loadCollection($pColl); | ||
| 2585 | $pColl->takeSnapshot(); | ||
| 2586 | } | ||
| 2587 | } | ||
| 2588 | |||
| 2589 | $this->originalEntityData[$oid][$field] = $pColl; | ||
| 2590 | break; | ||
| 2591 | } | ||
| 2592 | } | ||
| 2593 | |||
| 2594 | // defer invoking of postLoad event to hydration complete step | ||
| 2595 | $this->hydrationCompleteHandler->deferPostLoadInvoking($class, $entity); | ||
| 2596 | |||
| 2597 | return $entity; | ||
| 2598 | } | ||
| 2599 | |||
| 2600 | public function triggerEagerLoads(): void | ||
| 2601 | { | ||
| 2602 | if (! $this->eagerLoadingEntities && ! $this->eagerLoadingCollections) { | ||
| 2603 | return; | ||
| 2604 | } | ||
| 2605 | |||
| 2606 | // avoid infinite recursion | ||
| 2607 | $eagerLoadingEntities = $this->eagerLoadingEntities; | ||
| 2608 | $this->eagerLoadingEntities = []; | ||
| 2609 | |||
| 2610 | foreach ($eagerLoadingEntities as $entityName => $ids) { | ||
| 2611 | if (! $ids) { | ||
| 2612 | continue; | ||
| 2613 | } | ||
| 2614 | |||
| 2615 | $class = $this->em->getClassMetadata($entityName); | ||
| 2616 | $batches = array_chunk($ids, $this->em->getConfiguration()->getEagerFetchBatchSize()); | ||
| 2617 | |||
| 2618 | foreach ($batches as $batchedIds) { | ||
| 2619 | $this->getEntityPersister($entityName)->loadAll( | ||
| 2620 | array_combine($class->identifier, [$batchedIds]), | ||
| 2621 | ); | ||
| 2622 | } | ||
| 2623 | } | ||
| 2624 | |||
| 2625 | $eagerLoadingCollections = $this->eagerLoadingCollections; // avoid recursion | ||
| 2626 | $this->eagerLoadingCollections = []; | ||
| 2627 | |||
| 2628 | foreach ($eagerLoadingCollections as $group) { | ||
| 2629 | $this->eagerLoadCollections($group['items'], $group['mapping']); | ||
| 2630 | } | ||
| 2631 | } | ||
| 2632 | |||
| 2633 | /** | ||
| 2634 | * Load all data into the given collections, according to the specified mapping | ||
| 2635 | * | ||
| 2636 | * @param PersistentCollection[] $collections | ||
| 2637 | */ | ||
| 2638 | private function eagerLoadCollections(array $collections, ToManyInverseSideMapping $mapping): void | ||
| 2639 | { | ||
| 2640 | $targetEntity = $mapping->targetEntity; | ||
| 2641 | $class = $this->em->getClassMetadata($mapping->sourceEntity); | ||
| 2642 | $mappedBy = $mapping->mappedBy; | ||
| 2643 | |||
| 2644 | $batches = array_chunk($collections, $this->em->getConfiguration()->getEagerFetchBatchSize(), true); | ||
| 2645 | |||
| 2646 | foreach ($batches as $collectionBatch) { | ||
| 2647 | $entities = []; | ||
| 2648 | |||
| 2649 | foreach ($collectionBatch as $collection) { | ||
| 2650 | $entities[] = $collection->getOwner(); | ||
| 2651 | } | ||
| 2652 | |||
| 2653 | $found = $this->getEntityPersister($targetEntity)->loadAll([$mappedBy => $entities], $mapping->orderBy); | ||
| 2654 | |||
| 2655 | $targetClass = $this->em->getClassMetadata($targetEntity); | ||
| 2656 | $targetProperty = $targetClass->getReflectionProperty($mappedBy); | ||
| 2657 | assert($targetProperty !== null); | ||
| 2658 | |||
| 2659 | foreach ($found as $targetValue) { | ||
| 2660 | $sourceEntity = $targetProperty->getValue($targetValue); | ||
| 2661 | |||
| 2662 | if ($sourceEntity === null && isset($targetClass->associationMappings[$mappedBy]->joinColumns)) { | ||
| 2663 | // case where the hydration $targetValue itself has not yet fully completed, for example | ||
| 2664 | // in case a bi-directional association is being hydrated and deferring eager loading is | ||
| 2665 | // not possible due to subclassing. | ||
| 2666 | $data = $this->getOriginalEntityData($targetValue); | ||
| 2667 | $id = []; | ||
| 2668 | foreach ($targetClass->associationMappings[$mappedBy]->joinColumns as $joinColumn) { | ||
| 2669 | $id[] = $data[$joinColumn->name]; | ||
| 2670 | } | ||
| 2671 | } else { | ||
| 2672 | $id = $this->identifierFlattener->flattenIdentifier($class, $class->getIdentifierValues($sourceEntity)); | ||
| 2673 | } | ||
| 2674 | |||
| 2675 | $idHash = implode(' ', $id); | ||
| 2676 | |||
| 2677 | if ($mapping->indexBy !== null) { | ||
| 2678 | $indexByProperty = $targetClass->getReflectionProperty($mapping->indexBy); | ||
| 2679 | assert($indexByProperty !== null); | ||
| 2680 | $collectionBatch[$idHash]->hydrateSet($indexByProperty->getValue($targetValue), $targetValue); | ||
| 2681 | } else { | ||
| 2682 | $collectionBatch[$idHash]->add($targetValue); | ||
| 2683 | } | ||
| 2684 | } | ||
| 2685 | } | ||
| 2686 | |||
| 2687 | foreach ($collections as $association) { | ||
| 2688 | $association->setInitialized(true); | ||
| 2689 | $association->takeSnapshot(); | ||
| 2690 | } | ||
| 2691 | } | ||
| 2692 | |||
| 2693 | /** | ||
| 2694 | * Initializes (loads) an uninitialized persistent collection of an entity. | ||
| 2695 | * | ||
| 2696 | * @todo Maybe later move to EntityManager#initialize($proxyOrCollection). See DDC-733. | ||
| 2697 | */ | ||
| 2698 | public function loadCollection(PersistentCollection $collection): void | ||
| 2699 | { | ||
| 2700 | $assoc = $collection->getMapping(); | ||
| 2701 | $persister = $this->getEntityPersister($assoc->targetEntity); | ||
| 2702 | |||
| 2703 | switch ($assoc->type()) { | ||
| 2704 | case ClassMetadata::ONE_TO_MANY: | ||
| 2705 | $persister->loadOneToManyCollection($assoc, $collection->getOwner(), $collection); | ||
| 2706 | break; | ||
| 2707 | |||
| 2708 | case ClassMetadata::MANY_TO_MANY: | ||
| 2709 | $persister->loadManyToManyCollection($assoc, $collection->getOwner(), $collection); | ||
| 2710 | break; | ||
| 2711 | } | ||
| 2712 | |||
| 2713 | $collection->setInitialized(true); | ||
| 2714 | } | ||
| 2715 | |||
| 2716 | /** | ||
| 2717 | * Schedule this collection for batch loading at the end of the UnitOfWork | ||
| 2718 | */ | ||
| 2719 | private function scheduleCollectionForBatchLoading(PersistentCollection $collection, ClassMetadata $sourceClass): void | ||
| 2720 | { | ||
| 2721 | $mapping = $collection->getMapping(); | ||
| 2722 | $name = $mapping->sourceEntity . '#' . $mapping->fieldName; | ||
| 2723 | |||
| 2724 | if (! isset($this->eagerLoadingCollections[$name])) { | ||
| 2725 | $this->eagerLoadingCollections[$name] = [ | ||
| 2726 | 'items' => [], | ||
| 2727 | 'mapping' => $mapping, | ||
| 2728 | ]; | ||
| 2729 | } | ||
| 2730 | |||
| 2731 | $owner = $collection->getOwner(); | ||
| 2732 | assert($owner !== null); | ||
| 2733 | |||
| 2734 | $id = $this->identifierFlattener->flattenIdentifier( | ||
| 2735 | $sourceClass, | ||
| 2736 | $sourceClass->getIdentifierValues($owner), | ||
| 2737 | ); | ||
| 2738 | $idHash = implode(' ', $id); | ||
| 2739 | |||
| 2740 | $this->eagerLoadingCollections[$name]['items'][$idHash] = $collection; | ||
| 2741 | } | ||
| 2742 | |||
| 2743 | /** | ||
| 2744 | * Gets the identity map of the UnitOfWork. | ||
| 2745 | * | ||
| 2746 | * @psalm-return array<class-string, array<string, object>> | ||
| 2747 | */ | ||
| 2748 | public function getIdentityMap(): array | ||
| 2749 | { | ||
| 2750 | return $this->identityMap; | ||
| 2751 | } | ||
| 2752 | |||
| 2753 | /** | ||
| 2754 | * Gets the original data of an entity. The original data is the data that was | ||
| 2755 | * present at the time the entity was reconstituted from the database. | ||
| 2756 | * | ||
| 2757 | * @psalm-return array<string, mixed> | ||
| 2758 | */ | ||
| 2759 | public function getOriginalEntityData(object $entity): array | ||
| 2760 | { | ||
| 2761 | $oid = spl_object_id($entity); | ||
| 2762 | |||
| 2763 | return $this->originalEntityData[$oid] ?? []; | ||
| 2764 | } | ||
| 2765 | |||
| 2766 | /** | ||
| 2767 | * @param mixed[] $data | ||
| 2768 | * | ||
| 2769 | * @ignore | ||
| 2770 | */ | ||
| 2771 | public function setOriginalEntityData(object $entity, array $data): void | ||
| 2772 | { | ||
| 2773 | $this->originalEntityData[spl_object_id($entity)] = $data; | ||
| 2774 | } | ||
| 2775 | |||
| 2776 | /** | ||
| 2777 | * INTERNAL: | ||
| 2778 | * Sets a property value of the original data array of an entity. | ||
| 2779 | * | ||
| 2780 | * @ignore | ||
| 2781 | */ | ||
| 2782 | public function setOriginalEntityProperty(int $oid, string $property, mixed $value): void | ||
| 2783 | { | ||
| 2784 | $this->originalEntityData[$oid][$property] = $value; | ||
| 2785 | } | ||
| 2786 | |||
| 2787 | /** | ||
| 2788 | * Gets the identifier of an entity. | ||
| 2789 | * The returned value is always an array of identifier values. If the entity | ||
| 2790 | * has a composite identifier then the identifier values are in the same | ||
| 2791 | * order as the identifier field names as returned by ClassMetadata#getIdentifierFieldNames(). | ||
| 2792 | * | ||
| 2793 | * @return mixed[] The identifier values. | ||
| 2794 | */ | ||
| 2795 | public function getEntityIdentifier(object $entity): array | ||
| 2796 | { | ||
| 2797 | return $this->entityIdentifiers[spl_object_id($entity)] | ||
| 2798 | ?? throw EntityNotFoundException::noIdentifierFound(get_debug_type($entity)); | ||
| 2799 | } | ||
| 2800 | |||
| 2801 | /** | ||
| 2802 | * Processes an entity instance to extract their identifier values. | ||
| 2803 | * | ||
| 2804 | * @return mixed A scalar value. | ||
| 2805 | * | ||
| 2806 | * @throws ORMInvalidArgumentException | ||
| 2807 | */ | ||
| 2808 | public function getSingleIdentifierValue(object $entity): mixed | ||
| 2809 | { | ||
| 2810 | $class = $this->em->getClassMetadata($entity::class); | ||
| 2811 | |||
| 2812 | if ($class->isIdentifierComposite) { | ||
| 2813 | throw ORMInvalidArgumentException::invalidCompositeIdentifier(); | ||
| 2814 | } | ||
| 2815 | |||
| 2816 | $values = $this->isInIdentityMap($entity) | ||
| 2817 | ? $this->getEntityIdentifier($entity) | ||
| 2818 | : $class->getIdentifierValues($entity); | ||
| 2819 | |||
| 2820 | return $values[$class->identifier[0]] ?? null; | ||
| 2821 | } | ||
| 2822 | |||
| 2823 | /** | ||
| 2824 | * Tries to find an entity with the given identifier in the identity map of | ||
| 2825 | * this UnitOfWork. | ||
| 2826 | * | ||
| 2827 | * @param mixed $id The entity identifier to look for. | ||
| 2828 | * @param string $rootClassName The name of the root class of the mapped entity hierarchy. | ||
| 2829 | * @psalm-param class-string $rootClassName | ||
| 2830 | * | ||
| 2831 | * @return object|false Returns the entity with the specified identifier if it exists in | ||
| 2832 | * this UnitOfWork, FALSE otherwise. | ||
| 2833 | */ | ||
| 2834 | public function tryGetById(mixed $id, string $rootClassName): object|false | ||
| 2835 | { | ||
| 2836 | $idHash = self::getIdHashByIdentifier((array) $id); | ||
| 2837 | |||
| 2838 | return $this->identityMap[$rootClassName][$idHash] ?? false; | ||
| 2839 | } | ||
| 2840 | |||
| 2841 | /** | ||
| 2842 | * Schedules an entity for dirty-checking at commit-time. | ||
| 2843 | * | ||
| 2844 | * @todo Rename: scheduleForSynchronization | ||
| 2845 | */ | ||
| 2846 | public function scheduleForDirtyCheck(object $entity): void | ||
| 2847 | { | ||
| 2848 | $rootClassName = $this->em->getClassMetadata($entity::class)->rootEntityName; | ||
| 2849 | |||
| 2850 | $this->scheduledForSynchronization[$rootClassName][spl_object_id($entity)] = $entity; | ||
| 2851 | } | ||
| 2852 | |||
| 2853 | /** | ||
| 2854 | * Checks whether the UnitOfWork has any pending insertions. | ||
| 2855 | */ | ||
| 2856 | public function hasPendingInsertions(): bool | ||
| 2857 | { | ||
| 2858 | return ! empty($this->entityInsertions); | ||
| 2859 | } | ||
| 2860 | |||
| 2861 | /** | ||
| 2862 | * Calculates the size of the UnitOfWork. The size of the UnitOfWork is the | ||
| 2863 | * number of entities in the identity map. | ||
| 2864 | */ | ||
| 2865 | public function size(): int | ||
| 2866 | { | ||
| 2867 | return array_sum(array_map('count', $this->identityMap)); | ||
| 2868 | } | ||
| 2869 | |||
| 2870 | /** | ||
| 2871 | * Gets the EntityPersister for an Entity. | ||
| 2872 | * | ||
| 2873 | * @psalm-param class-string $entityName | ||
| 2874 | */ | ||
| 2875 | public function getEntityPersister(string $entityName): EntityPersister | ||
| 2876 | { | ||
| 2877 | if (isset($this->persisters[$entityName])) { | ||
| 2878 | return $this->persisters[$entityName]; | ||
| 2879 | } | ||
| 2880 | |||
| 2881 | $class = $this->em->getClassMetadata($entityName); | ||
| 2882 | |||
| 2883 | $persister = match (true) { | ||
| 2884 | $class->isInheritanceTypeNone() => new BasicEntityPersister($this->em, $class), | ||
| 2885 | $class->isInheritanceTypeSingleTable() => new SingleTablePersister($this->em, $class), | ||
| 2886 | $class->isInheritanceTypeJoined() => new JoinedSubclassPersister($this->em, $class), | ||
| 2887 | default => throw new RuntimeException('No persister found for entity.'), | ||
| 2888 | }; | ||
| 2889 | |||
| 2890 | if ($this->hasCache && $class->cache !== null) { | ||
| 2891 | $persister = $this->em->getConfiguration() | ||
| 2892 | ->getSecondLevelCacheConfiguration() | ||
| 2893 | ->getCacheFactory() | ||
| 2894 | ->buildCachedEntityPersister($this->em, $persister, $class); | ||
| 2895 | } | ||
| 2896 | |||
| 2897 | $this->persisters[$entityName] = $persister; | ||
| 2898 | |||
| 2899 | return $this->persisters[$entityName]; | ||
| 2900 | } | ||
| 2901 | |||
| 2902 | /** Gets a collection persister for a collection-valued association. */ | ||
| 2903 | public function getCollectionPersister(AssociationMapping $association): CollectionPersister | ||
| 2904 | { | ||
| 2905 | $role = isset($association->cache) | ||
| 2906 | ? $association->sourceEntity . '::' . $association->fieldName | ||
| 2907 | : $association->type(); | ||
| 2908 | |||
| 2909 | if (isset($this->collectionPersisters[$role])) { | ||
| 2910 | return $this->collectionPersisters[$role]; | ||
| 2911 | } | ||
| 2912 | |||
| 2913 | $persister = $association->type() === ClassMetadata::ONE_TO_MANY | ||
| 2914 | ? new OneToManyPersister($this->em) | ||
| 2915 | : new ManyToManyPersister($this->em); | ||
| 2916 | |||
| 2917 | if ($this->hasCache && isset($association->cache)) { | ||
| 2918 | $persister = $this->em->getConfiguration() | ||
| 2919 | ->getSecondLevelCacheConfiguration() | ||
| 2920 | ->getCacheFactory() | ||
| 2921 | ->buildCachedCollectionPersister($this->em, $persister, $association); | ||
| 2922 | } | ||
| 2923 | |||
| 2924 | $this->collectionPersisters[$role] = $persister; | ||
| 2925 | |||
| 2926 | return $this->collectionPersisters[$role]; | ||
| 2927 | } | ||
| 2928 | |||
| 2929 | /** | ||
| 2930 | * INTERNAL: | ||
| 2931 | * Registers an entity as managed. | ||
| 2932 | * | ||
| 2933 | * @param mixed[] $id The identifier values. | ||
| 2934 | * @param mixed[] $data The original entity data. | ||
| 2935 | */ | ||
| 2936 | public function registerManaged(object $entity, array $id, array $data): void | ||
| 2937 | { | ||
| 2938 | $oid = spl_object_id($entity); | ||
| 2939 | |||
| 2940 | $this->entityIdentifiers[$oid] = $id; | ||
| 2941 | $this->entityStates[$oid] = self::STATE_MANAGED; | ||
| 2942 | $this->originalEntityData[$oid] = $data; | ||
| 2943 | |||
| 2944 | $this->addToIdentityMap($entity); | ||
| 2945 | } | ||
| 2946 | |||
| 2947 | /* PropertyChangedListener implementation */ | ||
| 2948 | |||
| 2949 | /** | ||
| 2950 | * Notifies this UnitOfWork of a property change in an entity. | ||
| 2951 | * | ||
| 2952 | * {@inheritDoc} | ||
| 2953 | */ | ||
| 2954 | public function propertyChanged(object $sender, string $propertyName, mixed $oldValue, mixed $newValue): void | ||
| 2955 | { | ||
| 2956 | $oid = spl_object_id($sender); | ||
| 2957 | $class = $this->em->getClassMetadata($sender::class); | ||
| 2958 | |||
| 2959 | $isAssocField = isset($class->associationMappings[$propertyName]); | ||
| 2960 | |||
| 2961 | if (! $isAssocField && ! isset($class->fieldMappings[$propertyName])) { | ||
| 2962 | return; // ignore non-persistent fields | ||
| 2963 | } | ||
| 2964 | |||
| 2965 | // Update changeset and mark entity for synchronization | ||
| 2966 | $this->entityChangeSets[$oid][$propertyName] = [$oldValue, $newValue]; | ||
| 2967 | |||
| 2968 | if (! isset($this->scheduledForSynchronization[$class->rootEntityName][$oid])) { | ||
| 2969 | $this->scheduleForDirtyCheck($sender); | ||
| 2970 | } | ||
| 2971 | } | ||
| 2972 | |||
| 2973 | /** | ||
| 2974 | * Gets the currently scheduled entity insertions in this UnitOfWork. | ||
| 2975 | * | ||
| 2976 | * @psalm-return array<int, object> | ||
| 2977 | */ | ||
| 2978 | public function getScheduledEntityInsertions(): array | ||
| 2979 | { | ||
| 2980 | return $this->entityInsertions; | ||
| 2981 | } | ||
| 2982 | |||
| 2983 | /** | ||
| 2984 | * Gets the currently scheduled entity updates in this UnitOfWork. | ||
| 2985 | * | ||
| 2986 | * @psalm-return array<int, object> | ||
| 2987 | */ | ||
| 2988 | public function getScheduledEntityUpdates(): array | ||
| 2989 | { | ||
| 2990 | return $this->entityUpdates; | ||
| 2991 | } | ||
| 2992 | |||
| 2993 | /** | ||
| 2994 | * Gets the currently scheduled entity deletions in this UnitOfWork. | ||
| 2995 | * | ||
| 2996 | * @psalm-return array<int, object> | ||
| 2997 | */ | ||
| 2998 | public function getScheduledEntityDeletions(): array | ||
| 2999 | { | ||
| 3000 | return $this->entityDeletions; | ||
| 3001 | } | ||
| 3002 | |||
| 3003 | /** | ||
| 3004 | * Gets the currently scheduled complete collection deletions | ||
| 3005 | * | ||
| 3006 | * @psalm-return array<int, PersistentCollection<array-key, object>> | ||
| 3007 | */ | ||
| 3008 | public function getScheduledCollectionDeletions(): array | ||
| 3009 | { | ||
| 3010 | return $this->collectionDeletions; | ||
| 3011 | } | ||
| 3012 | |||
| 3013 | /** | ||
| 3014 | * Gets the currently scheduled collection inserts, updates and deletes. | ||
| 3015 | * | ||
| 3016 | * @psalm-return array<int, PersistentCollection<array-key, object>> | ||
| 3017 | */ | ||
| 3018 | public function getScheduledCollectionUpdates(): array | ||
| 3019 | { | ||
| 3020 | return $this->collectionUpdates; | ||
| 3021 | } | ||
| 3022 | |||
| 3023 | /** | ||
| 3024 | * Helper method to initialize a lazy loading proxy or persistent collection. | ||
| 3025 | */ | ||
| 3026 | public function initializeObject(object $obj): void | ||
| 3027 | { | ||
| 3028 | if ($obj instanceof InternalProxy) { | ||
| 3029 | $obj->__load(); | ||
| 3030 | |||
| 3031 | return; | ||
| 3032 | } | ||
| 3033 | |||
| 3034 | if ($obj instanceof PersistentCollection) { | ||
| 3035 | $obj->initialize(); | ||
| 3036 | } | ||
| 3037 | } | ||
| 3038 | |||
| 3039 | /** | ||
| 3040 | * Tests if a value is an uninitialized entity. | ||
| 3041 | * | ||
| 3042 | * @psalm-assert-if-true InternalProxy $obj | ||
| 3043 | */ | ||
| 3044 | public function isUninitializedObject(mixed $obj): bool | ||
| 3045 | { | ||
| 3046 | return $obj instanceof InternalProxy && ! $obj->__isInitialized(); | ||
| 3047 | } | ||
| 3048 | |||
| 3049 | /** | ||
| 3050 | * Helper method to show an object as string. | ||
| 3051 | */ | ||
| 3052 | private static function objToStr(object $obj): string | ||
| 3053 | { | ||
| 3054 | return $obj instanceof Stringable ? (string) $obj : get_debug_type($obj) . '@' . spl_object_id($obj); | ||
| 3055 | } | ||
| 3056 | |||
| 3057 | /** | ||
| 3058 | * Marks an entity as read-only so that it will not be considered for updates during UnitOfWork#commit(). | ||
| 3059 | * | ||
| 3060 | * This operation cannot be undone as some parts of the UnitOfWork now keep gathering information | ||
| 3061 | * on this object that might be necessary to perform a correct update. | ||
| 3062 | * | ||
| 3063 | * @throws ORMInvalidArgumentException | ||
| 3064 | */ | ||
| 3065 | public function markReadOnly(object $object): void | ||
| 3066 | { | ||
| 3067 | if (! $this->isInIdentityMap($object)) { | ||
| 3068 | throw ORMInvalidArgumentException::readOnlyRequiresManagedEntity($object); | ||
| 3069 | } | ||
| 3070 | |||
| 3071 | $this->readOnlyObjects[spl_object_id($object)] = true; | ||
| 3072 | } | ||
| 3073 | |||
| 3074 | /** | ||
| 3075 | * Is this entity read only? | ||
| 3076 | * | ||
| 3077 | * @throws ORMInvalidArgumentException | ||
| 3078 | */ | ||
| 3079 | public function isReadOnly(object $object): bool | ||
| 3080 | { | ||
| 3081 | return isset($this->readOnlyObjects[spl_object_id($object)]); | ||
| 3082 | } | ||
| 3083 | |||
| 3084 | /** | ||
| 3085 | * Perform whatever processing is encapsulated here after completion of the transaction. | ||
| 3086 | */ | ||
| 3087 | private function afterTransactionComplete(): void | ||
| 3088 | { | ||
| 3089 | $this->performCallbackOnCachedPersister(static function (CachedPersister $persister): void { | ||
| 3090 | $persister->afterTransactionComplete(); | ||
| 3091 | }); | ||
| 3092 | } | ||
| 3093 | |||
| 3094 | /** | ||
| 3095 | * Perform whatever processing is encapsulated here after completion of the rolled-back. | ||
| 3096 | */ | ||
| 3097 | private function afterTransactionRolledBack(): void | ||
| 3098 | { | ||
| 3099 | $this->performCallbackOnCachedPersister(static function (CachedPersister $persister): void { | ||
| 3100 | $persister->afterTransactionRolledBack(); | ||
| 3101 | }); | ||
| 3102 | } | ||
| 3103 | |||
| 3104 | /** | ||
| 3105 | * Performs an action after the transaction. | ||
| 3106 | */ | ||
| 3107 | private function performCallbackOnCachedPersister(callable $callback): void | ||
| 3108 | { | ||
| 3109 | if (! $this->hasCache) { | ||
| 3110 | return; | ||
| 3111 | } | ||
| 3112 | |||
| 3113 | foreach ([...$this->persisters, ...$this->collectionPersisters] as $persister) { | ||
| 3114 | if ($persister instanceof CachedPersister) { | ||
| 3115 | $callback($persister); | ||
| 3116 | } | ||
| 3117 | } | ||
| 3118 | } | ||
| 3119 | |||
| 3120 | private function dispatchOnFlushEvent(): void | ||
| 3121 | { | ||
| 3122 | if ($this->evm->hasListeners(Events::onFlush)) { | ||
| 3123 | $this->evm->dispatchEvent(Events::onFlush, new OnFlushEventArgs($this->em)); | ||
| 3124 | } | ||
| 3125 | } | ||
| 3126 | |||
| 3127 | private function dispatchPostFlushEvent(): void | ||
| 3128 | { | ||
| 3129 | if ($this->evm->hasListeners(Events::postFlush)) { | ||
| 3130 | $this->evm->dispatchEvent(Events::postFlush, new PostFlushEventArgs($this->em)); | ||
| 3131 | } | ||
| 3132 | } | ||
| 3133 | |||
| 3134 | /** | ||
| 3135 | * Verifies if two given entities actually are the same based on identifier comparison | ||
| 3136 | */ | ||
| 3137 | private function isIdentifierEquals(object $entity1, object $entity2): bool | ||
| 3138 | { | ||
| 3139 | if ($entity1 === $entity2) { | ||
| 3140 | return true; | ||
| 3141 | } | ||
| 3142 | |||
| 3143 | $class = $this->em->getClassMetadata($entity1::class); | ||
| 3144 | |||
| 3145 | if ($class !== $this->em->getClassMetadata($entity2::class)) { | ||
| 3146 | return false; | ||
| 3147 | } | ||
| 3148 | |||
| 3149 | $oid1 = spl_object_id($entity1); | ||
| 3150 | $oid2 = spl_object_id($entity2); | ||
| 3151 | |||
| 3152 | $id1 = $this->entityIdentifiers[$oid1] ?? $this->identifierFlattener->flattenIdentifier($class, $class->getIdentifierValues($entity1)); | ||
| 3153 | $id2 = $this->entityIdentifiers[$oid2] ?? $this->identifierFlattener->flattenIdentifier($class, $class->getIdentifierValues($entity2)); | ||
| 3154 | |||
| 3155 | return $id1 === $id2 || self::getIdHashByIdentifier($id1) === self::getIdHashByIdentifier($id2); | ||
| 3156 | } | ||
| 3157 | |||
| 3158 | /** @throws ORMInvalidArgumentException */ | ||
| 3159 | private function assertThatThereAreNoUnintentionallyNonPersistedAssociations(): void | ||
| 3160 | { | ||
| 3161 | $entitiesNeedingCascadePersist = array_diff_key($this->nonCascadedNewDetectedEntities, $this->entityInsertions); | ||
| 3162 | |||
| 3163 | $this->nonCascadedNewDetectedEntities = []; | ||
| 3164 | |||
| 3165 | if ($entitiesNeedingCascadePersist) { | ||
| 3166 | throw ORMInvalidArgumentException::newEntitiesFoundThroughRelationships( | ||
| 3167 | array_values($entitiesNeedingCascadePersist), | ||
| 3168 | ); | ||
| 3169 | } | ||
| 3170 | } | ||
| 3171 | |||
| 3172 | /** | ||
| 3173 | * This method called by hydrators, and indicates that hydrator totally completed current hydration cycle. | ||
| 3174 | * Unit of work able to fire deferred events, related to loading events here. | ||
| 3175 | * | ||
| 3176 | * @internal should be called internally from object hydrators | ||
| 3177 | */ | ||
| 3178 | public function hydrationComplete(): void | ||
| 3179 | { | ||
| 3180 | $this->hydrationCompleteHandler->hydrationComplete(); | ||
| 3181 | } | ||
| 3182 | |||
| 3183 | /** @throws MappingException if the entity has more than a single identifier. */ | ||
| 3184 | private function convertSingleFieldIdentifierToPHPValue(ClassMetadata $class, mixed $identifierValue): mixed | ||
| 3185 | { | ||
| 3186 | return $this->em->getConnection()->convertToPHPValue( | ||
| 3187 | $identifierValue, | ||
| 3188 | $class->getTypeOfField($class->getSingleIdentifierFieldName()), | ||
| 3189 | ); | ||
| 3190 | } | ||
| 3191 | |||
| 3192 | /** | ||
| 3193 | * Given a flat identifier, this method will produce another flat identifier, but with all | ||
| 3194 | * association fields that are mapped as identifiers replaced by entity references, recursively. | ||
| 3195 | * | ||
| 3196 | * @param mixed[] $flatIdentifier | ||
| 3197 | * | ||
| 3198 | * @return array<string, mixed> | ||
| 3199 | */ | ||
| 3200 | private function normalizeIdentifier(ClassMetadata $targetClass, array $flatIdentifier): array | ||
| 3201 | { | ||
| 3202 | $normalizedAssociatedId = []; | ||
| 3203 | |||
| 3204 | foreach ($targetClass->getIdentifierFieldNames() as $name) { | ||
| 3205 | if (! array_key_exists($name, $flatIdentifier)) { | ||
| 3206 | continue; | ||
| 3207 | } | ||
| 3208 | |||
| 3209 | if (! $targetClass->isSingleValuedAssociation($name)) { | ||
| 3210 | $normalizedAssociatedId[$name] = $flatIdentifier[$name]; | ||
| 3211 | continue; | ||
| 3212 | } | ||
| 3213 | |||
| 3214 | $targetIdMetadata = $this->em->getClassMetadata($targetClass->getAssociationTargetClass($name)); | ||
| 3215 | |||
| 3216 | // Note: the ORM prevents using an entity with a composite identifier as an identifier association | ||
| 3217 | // therefore, reset($targetIdMetadata->identifier) is always correct | ||
| 3218 | $normalizedAssociatedId[$name] = $this->em->getReference( | ||
| 3219 | $targetIdMetadata->getName(), | ||
| 3220 | $this->normalizeIdentifier( | ||
| 3221 | $targetIdMetadata, | ||
| 3222 | [(string) reset($targetIdMetadata->identifier) => $flatIdentifier[$name]], | ||
| 3223 | ), | ||
| 3224 | ); | ||
| 3225 | } | ||
| 3226 | |||
| 3227 | return $normalizedAssociatedId; | ||
| 3228 | } | ||
| 3229 | |||
| 3230 | /** | ||
| 3231 | * Assign a post-insert generated ID to an entity | ||
| 3232 | * | ||
| 3233 | * This is used by EntityPersisters after they inserted entities into the database. | ||
| 3234 | * It will place the assigned ID values in the entity's fields and start tracking | ||
| 3235 | * the entity in the identity map. | ||
| 3236 | */ | ||
| 3237 | final public function assignPostInsertId(object $entity, mixed $generatedId): void | ||
| 3238 | { | ||
| 3239 | $class = $this->em->getClassMetadata($entity::class); | ||
| 3240 | $idField = $class->getSingleIdentifierFieldName(); | ||
| 3241 | $idValue = $this->convertSingleFieldIdentifierToPHPValue($class, $generatedId); | ||
| 3242 | $oid = spl_object_id($entity); | ||
| 3243 | |||
| 3244 | $class->reflFields[$idField]->setValue($entity, $idValue); | ||
| 3245 | |||
| 3246 | $this->entityIdentifiers[$oid] = [$idField => $idValue]; | ||
| 3247 | $this->entityStates[$oid] = self::STATE_MANAGED; | ||
| 3248 | $this->originalEntityData[$oid][$idField] = $idValue; | ||
| 3249 | |||
| 3250 | $this->addToIdentityMap($entity); | ||
| 3251 | } | ||
| 3252 | } | ||
diff --git a/vendor/doctrine/orm/src/Utility/HierarchyDiscriminatorResolver.php b/vendor/doctrine/orm/src/Utility/HierarchyDiscriminatorResolver.php new file mode 100644 index 0000000..b682125 --- /dev/null +++ b/vendor/doctrine/orm/src/Utility/HierarchyDiscriminatorResolver.php | |||
| @@ -0,0 +1,44 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Utility; | ||
| 6 | |||
| 7 | use Doctrine\ORM\EntityManagerInterface; | ||
| 8 | use Doctrine\Persistence\Mapping\ClassMetadata; | ||
| 9 | |||
| 10 | /** @internal This class exists only to avoid code duplication, do not reuse it externally */ | ||
| 11 | final class HierarchyDiscriminatorResolver | ||
| 12 | { | ||
| 13 | private function __construct() | ||
| 14 | { | ||
| 15 | } | ||
| 16 | |||
| 17 | /** | ||
| 18 | * This method is needed to make INSTANCEOF work correctly with inheritance: if the class at hand has inheritance, | ||
| 19 | * it extracts all the discriminators from the child classes and returns them | ||
| 20 | * | ||
| 21 | * @return null[] | ||
| 22 | * @psalm-return array<array-key, null> | ||
| 23 | */ | ||
| 24 | public static function resolveDiscriminatorsForClass( | ||
| 25 | ClassMetadata $rootClassMetadata, | ||
| 26 | EntityManagerInterface $entityManager, | ||
| 27 | ): array { | ||
| 28 | $hierarchyClasses = $rootClassMetadata->subClasses; | ||
| 29 | $hierarchyClasses[] = $rootClassMetadata->name; | ||
| 30 | |||
| 31 | $discriminators = []; | ||
| 32 | |||
| 33 | foreach ($hierarchyClasses as $class) { | ||
| 34 | $currentMetadata = $entityManager->getClassMetadata($class); | ||
| 35 | $currentDiscriminator = $currentMetadata->discriminatorValue; | ||
| 36 | |||
| 37 | if ($currentDiscriminator !== null) { | ||
| 38 | $discriminators[$currentDiscriminator] = null; | ||
| 39 | } | ||
| 40 | } | ||
| 41 | |||
| 42 | return $discriminators; | ||
| 43 | } | ||
| 44 | } | ||
diff --git a/vendor/doctrine/orm/src/Utility/IdentifierFlattener.php b/vendor/doctrine/orm/src/Utility/IdentifierFlattener.php new file mode 100644 index 0000000..3792d33 --- /dev/null +++ b/vendor/doctrine/orm/src/Utility/IdentifierFlattener.php | |||
| @@ -0,0 +1,83 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Utility; | ||
| 6 | |||
| 7 | use BackedEnum; | ||
| 8 | use Doctrine\ORM\Mapping\ClassMetadata; | ||
| 9 | use Doctrine\ORM\UnitOfWork; | ||
| 10 | use Doctrine\Persistence\Mapping\ClassMetadataFactory; | ||
| 11 | |||
| 12 | use function assert; | ||
| 13 | use function implode; | ||
| 14 | use function is_a; | ||
| 15 | |||
| 16 | /** | ||
| 17 | * The IdentifierFlattener utility now houses some of the identifier manipulation logic from unit of work, so that it | ||
| 18 | * can be re-used elsewhere. | ||
| 19 | */ | ||
| 20 | final class IdentifierFlattener | ||
| 21 | { | ||
| 22 | /** | ||
| 23 | * Initializes a new IdentifierFlattener instance, bound to the given EntityManager. | ||
| 24 | */ | ||
| 25 | public function __construct( | ||
| 26 | /** | ||
| 27 | * The UnitOfWork used to coordinate object-level transactions. | ||
| 28 | */ | ||
| 29 | private readonly UnitOfWork $unitOfWork, | ||
| 30 | /** | ||
| 31 | * The metadata factory, used to retrieve the ORM metadata of entity classes. | ||
| 32 | */ | ||
| 33 | private readonly ClassMetadataFactory $metadataFactory, | ||
| 34 | ) { | ||
| 35 | } | ||
| 36 | |||
| 37 | /** | ||
| 38 | * convert foreign identifiers into scalar foreign key values to avoid object to string conversion failures. | ||
| 39 | * | ||
| 40 | * @param mixed[] $id | ||
| 41 | * | ||
| 42 | * @return mixed[] | ||
| 43 | * @psalm-return array<string, mixed> | ||
| 44 | */ | ||
| 45 | public function flattenIdentifier(ClassMetadata $class, array $id): array | ||
| 46 | { | ||
| 47 | $flatId = []; | ||
| 48 | |||
| 49 | foreach ($class->identifier as $field) { | ||
| 50 | if (isset($class->associationMappings[$field]) && isset($id[$field]) && is_a($id[$field], $class->associationMappings[$field]->targetEntity)) { | ||
| 51 | $targetClassMetadata = $this->metadataFactory->getMetadataFor( | ||
| 52 | $class->associationMappings[$field]->targetEntity, | ||
| 53 | ); | ||
| 54 | assert($targetClassMetadata instanceof ClassMetadata); | ||
| 55 | |||
| 56 | if ($this->unitOfWork->isInIdentityMap($id[$field])) { | ||
| 57 | $associatedId = $this->flattenIdentifier($targetClassMetadata, $this->unitOfWork->getEntityIdentifier($id[$field])); | ||
| 58 | } else { | ||
| 59 | $associatedId = $this->flattenIdentifier($targetClassMetadata, $targetClassMetadata->getIdentifierValues($id[$field])); | ||
| 60 | } | ||
| 61 | |||
| 62 | $flatId[$field] = implode(' ', $associatedId); | ||
| 63 | } elseif (isset($class->associationMappings[$field])) { | ||
| 64 | assert($class->associationMappings[$field]->isToOneOwningSide()); | ||
| 65 | $associatedId = []; | ||
| 66 | |||
| 67 | foreach ($class->associationMappings[$field]->joinColumns as $joinColumn) { | ||
| 68 | $associatedId[] = $id[$joinColumn->name]; | ||
| 69 | } | ||
| 70 | |||
| 71 | $flatId[$field] = implode(' ', $associatedId); | ||
| 72 | } else { | ||
| 73 | if ($id[$field] instanceof BackedEnum) { | ||
| 74 | $flatId[$field] = $id[$field]->value; | ||
| 75 | } else { | ||
| 76 | $flatId[$field] = $id[$field]; | ||
| 77 | } | ||
| 78 | } | ||
| 79 | } | ||
| 80 | |||
| 81 | return $flatId; | ||
| 82 | } | ||
| 83 | } | ||
diff --git a/vendor/doctrine/orm/src/Utility/LockSqlHelper.php b/vendor/doctrine/orm/src/Utility/LockSqlHelper.php new file mode 100644 index 0000000..7d135eb --- /dev/null +++ b/vendor/doctrine/orm/src/Utility/LockSqlHelper.php | |||
| @@ -0,0 +1,35 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Utility; | ||
| 6 | |||
| 7 | use Doctrine\DBAL\Platforms\AbstractMySQLPlatform; | ||
| 8 | use Doctrine\DBAL\Platforms\AbstractPlatform; | ||
| 9 | use Doctrine\DBAL\Platforms\DB2Platform; | ||
| 10 | use Doctrine\DBAL\Platforms\PostgreSQLPlatform; | ||
| 11 | use Doctrine\DBAL\Platforms\SQLitePlatform; | ||
| 12 | use Doctrine\DBAL\Platforms\SQLServerPlatform; | ||
| 13 | |||
| 14 | /** @internal */ | ||
| 15 | trait LockSqlHelper | ||
| 16 | { | ||
| 17 | private function getReadLockSQL(AbstractPlatform $platform): string | ||
| 18 | { | ||
| 19 | return match (true) { | ||
| 20 | $platform instanceof AbstractMySQLPlatform => 'LOCK IN SHARE MODE', | ||
| 21 | $platform instanceof PostgreSQLPlatform => 'FOR SHARE', | ||
| 22 | default => $this->getWriteLockSQL($platform), | ||
| 23 | }; | ||
| 24 | } | ||
| 25 | |||
| 26 | private function getWriteLockSQL(AbstractPlatform $platform): string | ||
| 27 | { | ||
| 28 | return match (true) { | ||
| 29 | $platform instanceof DB2Platform => 'WITH RR USE AND KEEP UPDATE LOCKS', | ||
| 30 | $platform instanceof SQLitePlatform, | ||
| 31 | $platform instanceof SQLServerPlatform => '', | ||
| 32 | default => 'FOR UPDATE', | ||
| 33 | }; | ||
| 34 | } | ||
| 35 | } | ||
diff --git a/vendor/doctrine/orm/src/Utility/PersisterHelper.php b/vendor/doctrine/orm/src/Utility/PersisterHelper.php new file mode 100644 index 0000000..76e9242 --- /dev/null +++ b/vendor/doctrine/orm/src/Utility/PersisterHelper.php | |||
| @@ -0,0 +1,108 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Utility; | ||
| 6 | |||
| 7 | use Doctrine\ORM\EntityManagerInterface; | ||
| 8 | use Doctrine\ORM\Mapping\ClassMetadata; | ||
| 9 | use Doctrine\ORM\Query\QueryException; | ||
| 10 | use RuntimeException; | ||
| 11 | |||
| 12 | use function sprintf; | ||
| 13 | |||
| 14 | /** | ||
| 15 | * The PersisterHelper contains logic to infer binding types which is used in | ||
| 16 | * several persisters. | ||
| 17 | * | ||
| 18 | * @link www.doctrine-project.org | ||
| 19 | */ | ||
| 20 | class PersisterHelper | ||
| 21 | { | ||
| 22 | /** | ||
| 23 | * @return list<string> | ||
| 24 | * | ||
| 25 | * @throws QueryException | ||
| 26 | */ | ||
| 27 | public static function getTypeOfField(string $fieldName, ClassMetadata $class, EntityManagerInterface $em): array | ||
| 28 | { | ||
| 29 | if (isset($class->fieldMappings[$fieldName])) { | ||
| 30 | return [$class->fieldMappings[$fieldName]->type]; | ||
| 31 | } | ||
| 32 | |||
| 33 | if (! isset($class->associationMappings[$fieldName])) { | ||
| 34 | return []; | ||
| 35 | } | ||
| 36 | |||
| 37 | $assoc = $class->associationMappings[$fieldName]; | ||
| 38 | |||
| 39 | if (! $assoc->isOwningSide()) { | ||
| 40 | return self::getTypeOfField($assoc->mappedBy, $em->getClassMetadata($assoc->targetEntity), $em); | ||
| 41 | } | ||
| 42 | |||
| 43 | if ($assoc->isManyToManyOwningSide()) { | ||
| 44 | $joinData = $assoc->joinTable; | ||
| 45 | } else { | ||
| 46 | $joinData = $assoc; | ||
| 47 | } | ||
| 48 | |||
| 49 | $types = []; | ||
| 50 | $targetClass = $em->getClassMetadata($assoc->targetEntity); | ||
| 51 | |||
| 52 | foreach ($joinData->joinColumns as $joinColumn) { | ||
| 53 | $types[] = self::getTypeOfColumn($joinColumn->referencedColumnName, $targetClass, $em); | ||
| 54 | } | ||
| 55 | |||
| 56 | return $types; | ||
| 57 | } | ||
| 58 | |||
| 59 | /** @throws RuntimeException */ | ||
| 60 | public static function getTypeOfColumn(string $columnName, ClassMetadata $class, EntityManagerInterface $em): string | ||
| 61 | { | ||
| 62 | if (isset($class->fieldNames[$columnName])) { | ||
| 63 | $fieldName = $class->fieldNames[$columnName]; | ||
| 64 | |||
| 65 | if (isset($class->fieldMappings[$fieldName])) { | ||
| 66 | return $class->fieldMappings[$fieldName]->type; | ||
| 67 | } | ||
| 68 | } | ||
| 69 | |||
| 70 | // iterate over to-one association mappings | ||
| 71 | foreach ($class->associationMappings as $assoc) { | ||
| 72 | if (! $assoc->isToOneOwningSide()) { | ||
| 73 | continue; | ||
| 74 | } | ||
| 75 | |||
| 76 | foreach ($assoc->joinColumns as $joinColumn) { | ||
| 77 | if ($joinColumn->name === $columnName) { | ||
| 78 | $targetColumnName = $joinColumn->referencedColumnName; | ||
| 79 | $targetClass = $em->getClassMetadata($assoc->targetEntity); | ||
| 80 | |||
| 81 | return self::getTypeOfColumn($targetColumnName, $targetClass, $em); | ||
| 82 | } | ||
| 83 | } | ||
| 84 | } | ||
| 85 | |||
| 86 | // iterate over to-many association mappings | ||
| 87 | foreach ($class->associationMappings as $assoc) { | ||
| 88 | if (! $assoc->isManyToManyOwningSide()) { | ||
| 89 | continue; | ||
| 90 | } | ||
| 91 | |||
| 92 | foreach ($assoc->joinTable->joinColumns as $joinColumn) { | ||
| 93 | if ($joinColumn->name === $columnName) { | ||
| 94 | $targetColumnName = $joinColumn->referencedColumnName; | ||
| 95 | $targetClass = $em->getClassMetadata($assoc->targetEntity); | ||
| 96 | |||
| 97 | return self::getTypeOfColumn($targetColumnName, $targetClass, $em); | ||
| 98 | } | ||
| 99 | } | ||
| 100 | } | ||
| 101 | |||
| 102 | throw new RuntimeException(sprintf( | ||
| 103 | 'Could not resolve type of column "%s" of class "%s"', | ||
| 104 | $columnName, | ||
| 105 | $class->getName(), | ||
| 106 | )); | ||
| 107 | } | ||
| 108 | } | ||
