vendor/doctrine/mongodb-odm/lib/Doctrine/ODM/MongoDB/Hydrator/HydratorFactory.php line 507

Open in your IDE?
  1. <?php
  2. declare(strict_types=1);
  3. namespace Doctrine\ODM\MongoDB\Hydrator;
  4. use Doctrine\Common\EventManager;
  5. use Doctrine\ODM\MongoDB\Configuration;
  6. use Doctrine\ODM\MongoDB\DocumentManager;
  7. use Doctrine\ODM\MongoDB\Event\LifecycleEventArgs;
  8. use Doctrine\ODM\MongoDB\Event\PreLoadEventArgs;
  9. use Doctrine\ODM\MongoDB\Events;
  10. use Doctrine\ODM\MongoDB\Mapping\ClassMetadata;
  11. use Doctrine\ODM\MongoDB\Types\Type;
  12. use Doctrine\ODM\MongoDB\UnitOfWork;
  13. use ProxyManager\Proxy\GhostObjectInterface;
  14. use function array_key_exists;
  15. use function chmod;
  16. use function class_exists;
  17. use function dirname;
  18. use function file_exists;
  19. use function file_put_contents;
  20. use function get_class;
  21. use function is_dir;
  22. use function is_writable;
  23. use function mkdir;
  24. use function rename;
  25. use function rtrim;
  26. use function sprintf;
  27. use function str_replace;
  28. use function substr;
  29. use function uniqid;
  30. use const DIRECTORY_SEPARATOR;
  31. /**
  32.  * The HydratorFactory class is responsible for instantiating a correct hydrator
  33.  * type based on document's ClassMetadata
  34.  *
  35.  * @psalm-import-type Hints from UnitOfWork
  36.  */
  37. final class HydratorFactory
  38. {
  39.     /**
  40.      * The DocumentManager this factory is bound to.
  41.      *
  42.      * @var DocumentManager
  43.      */
  44.     private $dm;
  45.     /**
  46.      * The UnitOfWork used to coordinate object-level transactions.
  47.      *
  48.      * @var UnitOfWork
  49.      */
  50.     private $unitOfWork;
  51.     /**
  52.      * The EventManager associated with this Hydrator
  53.      *
  54.      * @var EventManager
  55.      */
  56.     private $evm;
  57.     /**
  58.      * Which algorithm to use to automatically (re)generate hydrator classes.
  59.      *
  60.      * @var int
  61.      */
  62.     private $autoGenerate;
  63.     /**
  64.      * The namespace that contains all hydrator classes.
  65.      *
  66.      * @var string|null
  67.      */
  68.     private $hydratorNamespace;
  69.     /**
  70.      * The directory that contains all hydrator classes.
  71.      *
  72.      * @var string
  73.      */
  74.     private $hydratorDir;
  75.     /**
  76.      * Array of instantiated document hydrators.
  77.      *
  78.      * @var array
  79.      * @psalm-var array<class-string, HydratorInterface>
  80.      */
  81.     private $hydrators = [];
  82.     /**
  83.      * @throws HydratorException
  84.      */
  85.     public function __construct(DocumentManager $dmEventManager $evm, ?string $hydratorDir, ?string $hydratorNsint $autoGenerate)
  86.     {
  87.         if (! $hydratorDir) {
  88.             throw HydratorException::hydratorDirectoryRequired();
  89.         }
  90.         if (! $hydratorNs) {
  91.             throw HydratorException::hydratorNamespaceRequired();
  92.         }
  93.         $this->dm                $dm;
  94.         $this->evm               $evm;
  95.         $this->hydratorDir       $hydratorDir;
  96.         $this->hydratorNamespace $hydratorNs;
  97.         $this->autoGenerate      $autoGenerate;
  98.     }
  99.     /**
  100.      * Sets the UnitOfWork instance.
  101.      *
  102.      * @internal
  103.      */
  104.     public function setUnitOfWork(UnitOfWork $uow): void
  105.     {
  106.         $this->unitOfWork $uow;
  107.     }
  108.     /**
  109.      * Gets the hydrator object for the given document class.
  110.      *
  111.      * @psalm-param class-string $className
  112.      */
  113.     public function getHydratorFor(string $className): HydratorInterface
  114.     {
  115.         if (isset($this->hydrators[$className])) {
  116.             return $this->hydrators[$className];
  117.         }
  118.         $hydratorClassName str_replace('\\'''$className) . 'Hydrator';
  119.         $fqn               $this->hydratorNamespace '\\' $hydratorClassName;
  120.         $class             $this->dm->getClassMetadata($className);
  121.         if (! class_exists($fqnfalse)) {
  122.             $fileName $this->hydratorDir DIRECTORY_SEPARATOR $hydratorClassName '.php';
  123.             switch ($this->autoGenerate) {
  124.                 case Configuration::AUTOGENERATE_NEVER:
  125.                     require $fileName;
  126.                     break;
  127.                 case Configuration::AUTOGENERATE_ALWAYS:
  128.                     $this->generateHydratorClass($class$hydratorClassName$fileName);
  129.                     require $fileName;
  130.                     break;
  131.                 case Configuration::AUTOGENERATE_FILE_NOT_EXISTS:
  132.                     if (! file_exists($fileName)) {
  133.                         $this->generateHydratorClass($class$hydratorClassName$fileName);
  134.                     }
  135.                     require $fileName;
  136.                     break;
  137.                 case Configuration::AUTOGENERATE_EVAL:
  138.                     $this->generateHydratorClass($class$hydratorClassNamenull);
  139.                     break;
  140.             }
  141.         }
  142.         $this->hydrators[$className] = new $fqn($this->dm$this->unitOfWork$class);
  143.         return $this->hydrators[$className];
  144.     }
  145.     /**
  146.      * Generates hydrator classes for all given classes.
  147.      *
  148.      * @param ClassMetadata<object>[] $classes The classes (ClassMetadata instances) for which to generate hydrators.
  149.      * @param string|null             $toDir   The target directory of the hydrator classes. If not specified, the
  150.      *                                    directory configured on the Configuration of the DocumentManager used
  151.      *                                    by this factory is used.
  152.      */
  153.     public function generateHydratorClasses(array $classes, ?string $toDir null): void
  154.     {
  155.         $hydratorDir $toDir ?: $this->hydratorDir;
  156.         $hydratorDir rtrim($hydratorDirDIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR;
  157.         foreach ($classes as $class) {
  158.             $hydratorClassName str_replace('\\'''$class->name) . 'Hydrator';
  159.             $hydratorFileName  $hydratorDir $hydratorClassName '.php';
  160.             $this->generateHydratorClass($class$hydratorClassName$hydratorFileName);
  161.         }
  162.     }
  163.     /**
  164.      * @param ClassMetadata<object> $class
  165.      */
  166.     private function generateHydratorClass(ClassMetadata $classstring $hydratorClassName, ?string $fileName): void
  167.     {
  168.         $code '';
  169.         foreach ($class->fieldMappings as $fieldName => $mapping) {
  170.             if (isset($mapping['alsoLoadFields'])) {
  171.                 foreach ($mapping['alsoLoadFields'] as $name) {
  172.                     $code .= sprintf(
  173.                         <<<EOF
  174.         /** @AlsoLoad("$name") */
  175.         if (!array_key_exists('%1\$s', \$data) && array_key_exists('$name', \$data)) {
  176.             \$data['%1\$s'] = \$data['$name'];
  177.         }
  178. EOF
  179.                         ,
  180.                         $mapping['name']
  181.                     );
  182.                 }
  183.             }
  184.             if ($mapping['type'] === 'date') {
  185.                 $code .= sprintf(
  186.                     <<<EOF
  187.         /** @Field(type="date") */
  188.         if (isset(\$data['%1\$s'])) {
  189.             \$value = \$data['%1\$s'];
  190.             %3\$s
  191.             \$this->class->reflFields['%2\$s']->setValue(\$document, clone \$return);
  192.             \$hydratedData['%2\$s'] = \$return;
  193.         }
  194. EOF
  195.                     ,
  196.                     $mapping['name'],
  197.                     $mapping['fieldName'],
  198.                     Type::getType($mapping['type'])->closureToPHP()
  199.                 );
  200.             } elseif (! isset($mapping['association'])) {
  201.                 $code .= sprintf(
  202.                     <<<EOF
  203.         /** @Field(type="{$mapping['type']}") */
  204.         if (isset(\$data['%1\$s']) || (! empty(\$this->class->fieldMappings['%2\$s']['nullable']) && array_key_exists('%1\$s', \$data))) {
  205.             \$value = \$data['%1\$s'];
  206.             if (\$value !== null) {
  207.                 \$typeIdentifier = \$this->class->fieldMappings['%2\$s']['type'];
  208.                 %3\$s
  209.             } else {
  210.                 \$return = null;
  211.             }
  212.             \$this->class->reflFields['%2\$s']->setValue(\$document, \$return);
  213.             \$hydratedData['%2\$s'] = \$return;
  214.         }
  215. EOF
  216.                     ,
  217.                     $mapping['name'],
  218.                     $mapping['fieldName'],
  219.                     Type::getType($mapping['type'])->closureToPHP()
  220.                 );
  221.             } elseif ($mapping['association'] === ClassMetadata::REFERENCE_ONE && $mapping['isOwningSide']) {
  222.                 $code .= sprintf(
  223.                     <<<EOF
  224.         /** @ReferenceOne */
  225.         if (isset(\$data['%1\$s']) || (! empty(\$this->class->fieldMappings['%2\$s']['nullable']) && array_key_exists('%1\$s', \$data))) {
  226.             \$return = \$data['%1\$s'];
  227.             if (\$return !== null) {
  228.                 if (\$this->class->fieldMappings['%2\$s']['storeAs'] !== ClassMetadata::REFERENCE_STORE_AS_ID && ! is_array(\$return)) {
  229.                     throw HydratorException::associationTypeMismatch('%3\$s', '%1\$s', 'array', gettype(\$return));
  230.                 }
  231.                 \$className = \$this->unitOfWork->getClassNameForAssociation(\$this->class->fieldMappings['%2\$s'], \$return);
  232.                 \$identifier = ClassMetadata::getReferenceId(\$return, \$this->class->fieldMappings['%2\$s']['storeAs']);
  233.                 \$targetMetadata = \$this->dm->getClassMetadata(\$className);
  234.                 \$id = \$targetMetadata->getPHPIdentifierValue(\$identifier);
  235.                 \$return = \$this->dm->getReference(\$className, \$id);
  236.             }
  237.             \$this->class->reflFields['%2\$s']->setValue(\$document, \$return);
  238.             \$hydratedData['%2\$s'] = \$return;
  239.         }
  240. EOF
  241.                     ,
  242.                     $mapping['name'],
  243.                     $mapping['fieldName'],
  244.                     $class->getName()
  245.                 );
  246.             } elseif ($mapping['association'] === ClassMetadata::REFERENCE_ONE && $mapping['isInverseSide']) {
  247.                 if (isset($mapping['repositoryMethod']) && $mapping['repositoryMethod']) {
  248.                     $code .= sprintf(
  249.                         <<<EOF
  250.         \$className = \$this->class->fieldMappings['%2\$s']['targetDocument'];
  251.         \$return = \$this->dm->getRepository(\$className)->%3\$s(\$document);
  252.         \$this->class->reflFields['%2\$s']->setValue(\$document, \$return);
  253.         \$hydratedData['%2\$s'] = \$return;
  254. EOF
  255.                         ,
  256.                         $mapping['name'],
  257.                         $mapping['fieldName'],
  258.                         $mapping['repositoryMethod']
  259.                     );
  260.                 } else {
  261.                     $code .= sprintf(
  262.                         <<<EOF
  263.         \$mapping = \$this->class->fieldMappings['%2\$s'];
  264.         \$className = \$mapping['targetDocument'];
  265.         \$targetClass = \$this->dm->getClassMetadata(\$mapping['targetDocument']);
  266.         \$mappedByMapping = \$targetClass->fieldMappings[\$mapping['mappedBy']];
  267.         \$mappedByFieldName = ClassMetadata::getReferenceFieldName(\$mappedByMapping['storeAs'], \$mapping['mappedBy']);
  268.         \$criteria = array_merge(
  269.             array(\$mappedByFieldName => \$data['_id']),
  270.             isset(\$this->class->fieldMappings['%2\$s']['criteria']) ? \$this->class->fieldMappings['%2\$s']['criteria'] : array()
  271.         );
  272.         \$sort = isset(\$this->class->fieldMappings['%2\$s']['sort']) ? \$this->class->fieldMappings['%2\$s']['sort'] : array();
  273.         \$return = \$this->unitOfWork->getDocumentPersister(\$className)->load(\$criteria, null, array(), 0, \$sort);
  274.         \$this->class->reflFields['%2\$s']->setValue(\$document, \$return);
  275.         \$hydratedData['%2\$s'] = \$return;
  276. EOF
  277.                         ,
  278.                         $mapping['name'],
  279.                         $mapping['fieldName']
  280.                     );
  281.                 }
  282.             } elseif ($mapping['association'] === ClassMetadata::REFERENCE_MANY || $mapping['association'] === ClassMetadata::EMBED_MANY) {
  283.                 $code .= sprintf(
  284.                     <<<EOF
  285.         /** @Many */
  286.         \$mongoData = isset(\$data['%1\$s']) ? \$data['%1\$s'] : null;
  287.         if (\$mongoData !== null && ! is_array(\$mongoData)) {
  288.             throw HydratorException::associationTypeMismatch('%3\$s', '%1\$s', 'array', gettype(\$mongoData));
  289.         }
  290.         \$return = \$this->dm->getConfiguration()->getPersistentCollectionFactory()->create(\$this->dm, \$this->class->fieldMappings['%2\$s']);
  291.         \$return->setHints(\$hints);
  292.         \$return->setOwner(\$document, \$this->class->fieldMappings['%2\$s']);
  293.         \$return->setInitialized(false);
  294.         if (\$mongoData) {
  295.             \$return->setMongoData(\$mongoData);
  296.         }
  297.         \$this->class->reflFields['%2\$s']->setValue(\$document, \$return);
  298.         \$hydratedData['%2\$s'] = \$return;
  299. EOF
  300.                     ,
  301.                     $mapping['name'],
  302.                     $mapping['fieldName'],
  303.                     $class->getName()
  304.                 );
  305.             } elseif ($mapping['association'] === ClassMetadata::EMBED_ONE) {
  306.                 $code .= sprintf(
  307.                     <<<EOF
  308.         /** @EmbedOne */
  309.         if (isset(\$data['%1\$s']) || (! empty(\$this->class->fieldMappings['%2\$s']['nullable']) && array_key_exists('%1\$s', \$data))) {
  310.             \$return = \$data['%1\$s'];
  311.             if (\$return !== null) {
  312.                 \$embeddedDocument = \$return;
  313.                 if (! is_array(\$embeddedDocument)) {
  314.                     throw HydratorException::associationTypeMismatch('%3\$s', '%1\$s', 'array', gettype(\$embeddedDocument));
  315.                 }
  316.         
  317.                 \$className = \$this->unitOfWork->getClassNameForAssociation(\$this->class->fieldMappings['%2\$s'], \$embeddedDocument);
  318.                 \$embeddedMetadata = \$this->dm->getClassMetadata(\$className);
  319.                 \$return = \$embeddedMetadata->newInstance();
  320.                 \$this->unitOfWork->setParentAssociation(\$return, \$this->class->fieldMappings['%2\$s'], \$document, '%1\$s');
  321.                 \$embeddedData = \$this->dm->getHydratorFactory()->hydrate(\$return, \$embeddedDocument, \$hints);
  322.                 \$embeddedId = \$embeddedMetadata->identifier && isset(\$embeddedData[\$embeddedMetadata->identifier]) ? \$embeddedData[\$embeddedMetadata->identifier] : null;
  323.                 if (empty(\$hints[Query::HINT_READ_ONLY])) {
  324.                     \$this->unitOfWork->registerManaged(\$return, \$embeddedId, \$embeddedData);
  325.                 }
  326.             }
  327.             \$this->class->reflFields['%2\$s']->setValue(\$document, \$return);
  328.             \$hydratedData['%2\$s'] = \$return;
  329.         }
  330. EOF
  331.                     ,
  332.                     $mapping['name'],
  333.                     $mapping['fieldName'],
  334.                     $class->getName()
  335.                 );
  336.             }
  337.         }
  338.         $namespace $this->hydratorNamespace;
  339.         $code      sprintf(
  340.             <<<EOF
  341. <?php
  342. namespace $namespace;
  343. use Doctrine\ODM\MongoDB\DocumentManager;
  344. use Doctrine\ODM\MongoDB\Hydrator\HydratorException;
  345. use Doctrine\ODM\MongoDB\Hydrator\HydratorInterface;
  346. use Doctrine\ODM\MongoDB\Query\Query;
  347. use Doctrine\ODM\MongoDB\UnitOfWork;
  348. use Doctrine\ODM\MongoDB\Mapping\ClassMetadata;
  349. /**
  350.  * THIS CLASS WAS GENERATED BY THE DOCTRINE ODM. DO NOT EDIT THIS FILE.
  351.  */
  352. class $hydratorClassName implements HydratorInterface
  353. {
  354.     private \$dm;
  355.     private \$unitOfWork;
  356.     private \$class;
  357.     public function __construct(DocumentManager \$dm, UnitOfWork \$uow, ClassMetadata \$class)
  358.     {
  359.         \$this->dm = \$dm;
  360.         \$this->unitOfWork = \$uow;
  361.         \$this->class = \$class;
  362.     }
  363.     public function hydrate(object \$document, array \$data, array \$hints = array()): array
  364.     {
  365.         \$hydratedData = array();
  366. %s        return \$hydratedData;
  367.     }
  368. }
  369. EOF
  370.             ,
  371.             $code
  372.         );
  373.         if ($fileName === null) {
  374.             if (! class_exists($namespace '\\' $hydratorClassName)) {
  375.                 eval(substr($code5));
  376.             }
  377.             return;
  378.         }
  379.         $parentDirectory dirname($fileName);
  380.         if (! is_dir($parentDirectory) && (@mkdir($parentDirectory0775true) === false)) {
  381.             throw HydratorException::hydratorDirectoryNotWritable();
  382.         }
  383.         if (! is_writable($parentDirectory)) {
  384.             throw HydratorException::hydratorDirectoryNotWritable();
  385.         }
  386.         $tmpFileName $fileName '.' uniqid(''true);
  387.         file_put_contents($tmpFileName$code);
  388.         rename($tmpFileName$fileName);
  389.         chmod($fileName0664);
  390.     }
  391.     /**
  392.      * Hydrate array of MongoDB document data into the given document object.
  393.      *
  394.      * @param array<string, mixed> $data
  395.      * @psalm-param Hints $hints Any hints to account for during reconstitution/lookup of the document.
  396.      *
  397.      * @return array<string, mixed>
  398.      */
  399.     public function hydrate(object $document, array $data, array $hints = []): array
  400.     {
  401.         $metadata $this->dm->getClassMetadata(get_class($document));
  402.         // Invoke preLoad lifecycle events and listeners
  403.         if (! empty($metadata->lifecycleCallbacks[Events::preLoad])) {
  404.             $args = [new PreLoadEventArgs($document$this->dm$data)];
  405.             $metadata->invokeLifecycleCallbacks(Events::preLoad$document$args);
  406.         }
  407.         if ($this->evm->hasListeners(Events::preLoad)) {
  408.             $this->evm->dispatchEvent(Events::preLoad, new PreLoadEventArgs($document$this->dm$data));
  409.         }
  410.         // alsoLoadMethods may transform the document before hydration
  411.         if (! empty($metadata->alsoLoadMethods)) {
  412.             foreach ($metadata->alsoLoadMethods as $method => $fieldNames) {
  413.                 foreach ($fieldNames as $fieldName) {
  414.                     // Invoke the method only once for the first field we find
  415.                     if (array_key_exists($fieldName$data)) {
  416.                         $document->$method($data[$fieldName]);
  417.                         continue 2;
  418.                     }
  419.                 }
  420.             }
  421.         }
  422.         if ($document instanceof GhostObjectInterface && $document->getProxyInitializer() !== null) {
  423.             // Inject an empty initialiser to not load any object data
  424.             $document->setProxyInitializer(static function (
  425.                 GhostObjectInterface $ghostObject,
  426.                 string $method// we don't care
  427.                 array $parameters// we don't care
  428.                 &$initializer,
  429.                 array $properties // we currently do not use this
  430.             ): bool {
  431.                 $initializer null;
  432.                 return true;
  433.             });
  434.         }
  435.         $data $this->getHydratorFor($metadata->name)->hydrate($document$data$hints);
  436.         // Invoke the postLoad lifecycle callbacks and listeners
  437.         if (! empty($metadata->lifecycleCallbacks[Events::postLoad])) {
  438.             $metadata->invokeLifecycleCallbacks(Events::postLoad$document, [new LifecycleEventArgs($document$this->dm)]);
  439.         }
  440.         if ($this->evm->hasListeners(Events::postLoad)) {
  441.             $this->evm->dispatchEvent(Events::postLoad, new LifecycleEventArgs($document$this->dm));
  442.         }
  443.         return $data;
  444.     }
  445. }