src/Uplifted/BaseBundle/Service/BaseEntityManager.php line 440

Open in your IDE?
  1. <?php
  2. namespace App\Uplifted\BaseBundle\Service;
  3. use App\AdminBundle\Service\AiService;
  4. use App\Uplifted\BaseBundle\EventListener\AfterSavingEntityCreationListener;
  5. use App\Uplifted\BaseBundle\EventListener\AfterSavingEntityDeletionListener;
  6. use App\Uplifted\BaseBundle\EventListener\AfterSavingEntityListener;
  7. use App\Uplifted\BaseBundle\EventListener\AfterSavingEntityUpdateListener;
  8. use App\Uplifted\BaseBundle\Exception\AccessDeniedException;
  9. use App\Uplifted\BaseBundle\Exception\EntityValidationException;
  10. use App\Uplifted\BaseBundle\Exception\MissingRequiredFieldsException;
  11. use Doctrine\ORM\EntityManager as DoctrineEntityManager;
  12. use Doctrine\ORM\Events;
  13. use Doctrine\ORM\Exception\ORMException;
  14. use Doctrine\ORM\UnitOfWork;
  15. use Doctrine\ORM\Mapping\ClassMetadata;
  16. use Exception;
  17. use Symfony\Contracts\Translation\TranslatorInterface;
  18. abstract class BaseEntityManager extends BaseService
  19. {
  20.     const DATETIME_ZERO_TIMESTAMP 1;              // 1970-01-01 0:00:01
  21.     const DATETIME_INIFINTY_TIMESTAMP 4102444800// 2100-01-01 0:00:00
  22.     protected DoctrineEntityManager $doctrineEntityManager;
  23.     protected $validationService;
  24.     protected $translatorService;
  25.     protected $securityTokenStorage;
  26.     protected $eventManager;
  27.     protected $flushManager;
  28.     protected AiService $aiCompletionService;
  29.     public function __construct(
  30.         DoctrineEntityManager $doctrineEntityManager,
  31.         ValidationService     $validationService,
  32.         TranslatorInterface   $translatorService,
  33.                               $securityTokenStorage
  34.     )
  35.     {
  36.         $this->doctrineEntityManager $doctrineEntityManager;
  37.         $this->validationService $validationService;
  38.         $this->translatorService $translatorService;
  39.         $this->securityTokenStorage $securityTokenStorage;
  40.     }
  41.     public function setEventManager(EventManager $eventManager)
  42.     {
  43.         if ($this->eventManager === null) {
  44.             $this->eventManager $eventManager;
  45.         }
  46.     }
  47.     public function setFlushManager(FlushManager $flushManager)
  48.     {
  49.         if ($this->flushManager === null) {
  50.             $this->flushManager $flushManager;
  51.         }
  52.     }
  53.     public function setAiCompletionService(AiService $aiCompletionService)
  54.     {
  55.         $this->aiCompletionService $aiCompletionService;
  56.     }
  57.     /**
  58.      * Returns an array containing the names of the fields that are required to create an entity
  59.      * This is used by the isValidRequest method
  60.      *
  61.      * @param array  $values  A key value pair array of the field names and their values.
  62.      *                        Which fields are required could be influenced or determined by some value in the
  63.      *                        $values array.
  64.      *
  65.      * @return array
  66.      */
  67.     abstract public function getCreateRequiredFieldsNames(array $values = array());
  68.     /**
  69.      * Main attributes validation function. Must be a field by field validation
  70.      * Invoke parent function first
  71.      *
  72.      * @param array   $values  A key value pair array of the field names and their values
  73.      * @param object  $entity  The entity to validate the values upon (if exists). Defaults to null
  74.      *
  75.      * @return array    An array with all errors found or empty if none
  76.      */
  77.     abstract public function validateValues($values$entity null);
  78.     /**
  79.      * Function to set values to an entity
  80.      * Invoke parent function first
  81.      *
  82.      * @param object  $entity  The entity being set
  83.      * @param array   $values  A key value pair array of the field names and their values
  84.      */
  85.     abstract public function setValues(&$entity$values);
  86.     /**
  87.      * Function to get the managed entity's class
  88.      *
  89.      * @return string   The fully qualified class identifier for the managed entity
  90.      */
  91.     abstract public function getManagedEntityClass();
  92.     /**
  93.      * Function to get a new entity instance from the entity manager
  94.      *
  95.      * @return object A new entity instance of the managed type
  96.      */
  97.     public function getNewEntityInstance()
  98.     {
  99.         $fullyQualifiedClassName $this->getManagedEntityClass();
  100.         return new $fullyQualifiedClassName();
  101.     }
  102.     /**
  103.      * Function to get the handling repository for the entity type
  104.      *
  105.      * @return Doctrine\ORM\EntityRepository The repository
  106.      */
  107.     public function getEntityRepo()
  108.     {
  109.         try {
  110.             // Explode the fully qualified entity manager class name
  111.             $parts explode('\\'get_class($this));
  112.             // Get the entity name from the last item
  113.             $entityName str_replace('Manager'''array_pop($parts));
  114.             // Remove 'Service' from the array
  115.             array_pop($parts);
  116.             $bundleName implode('\\'$parts);
  117.             // Form the entity class
  118.             $entityClass $bundleName '\\Entity\\' $entityName;
  119.             // And build the repository name with that
  120.             $repo $this->getDoctrineEntityManager()->getRepository($entityClass);
  121.         } catch (Exception $ex) {
  122.             throw new Exception($this->translatorService
  123.                     ->trans('uplifted.base_bundle.entity.get_entity_repo_not_implemented_in_manager',
  124.                         array('%searchedTerm%' => $bundleName ':' $entityName)
  125.                     ) . ' ' $ex->getMessage()
  126.             );
  127.         }
  128.         return $repo;
  129.     }
  130.     /**
  131.      * Validation of provided fields names per action type
  132.      *
  133.      * @param array                   $providedFieldsNames  Submitted values fields names
  134.      * @param 'create'|'update'|null  $actionName           The action for which we
  135.      *                                                      need to validate the required fields names. Defaults to
  136.      *                                                      'create'
  137.      *
  138.      * @return bool
  139.      */
  140.     public function areRequiredFieldsNamesMetForAction($providedFieldsNames$actionName 'create')
  141.     {
  142.         $requiredFieldsNames $this->getRequiredFieldsNamesForAction(
  143.             $providedFieldsNames$actionName
  144.         );
  145.         // Perform the actual check (intersect keys and count the result set) and return the result
  146.         return $this->areRequiredFieldsNamesMet($providedFieldsNames$requiredFieldsNames);
  147.     }
  148.     public function getRequiredFieldsNamesForAction($providedFieldsNames$actionName 'create')
  149.     {
  150.         // Get the required fields names, given the action name
  151.         switch ($actionName) {
  152.             case 'update':
  153.                 return $this->getUpdateRequiredFieldsNames($providedFieldsNames);
  154.             case 'create':
  155.             default:
  156.                 return $this->getCreateRequiredFieldsNames($providedFieldsNames);
  157.         }
  158.     }
  159.     /**
  160.      * Verification of provided vs required fields names
  161.      *
  162.      * @param array  $providedFieldsNames  Provided fields names
  163.      * @param array  $requiredFieldsNames  Required fields names
  164.      *
  165.      * @return boolean  Whether if the required fields are met or not
  166.      */
  167.     public function areRequiredFieldsNamesMet($providedFieldsNames$requiredFieldsNames)
  168.     {
  169.         return count(array_intersect_key(array_flip($requiredFieldsNames),
  170.                 $providedFieldsNames)) === count($requiredFieldsNames);
  171.     }
  172.     /**
  173.      * Returns an array containing the names of the fields that are required to update an entity
  174.      * This is used by the isValidRequest method. Defaults to just the 'id'
  175.      * Override as needed
  176.      *
  177.      * @param array  $values  A key value pair array of the field names and their values.
  178.      *                        Which fields are required could be influenced or determined by some value in the
  179.      *                        $values array.
  180.      *
  181.      * @return array
  182.      */
  183.     public function getUpdateRequiredFieldsNames(array $values = array())
  184.     {
  185.         return array();
  186.     }
  187.     /**
  188.      * Entity creation based on provided values, conditioned to validity of them
  189.      *
  190.      * @param array    $values          Key-value map of properties and their values
  191.      * @param boolean  $save            Whether to trigger a save operation or not. Defaults to true
  192.      * @param array    $additionalData  Array to hold any additional data used for general purposes
  193.      * @param bool     $persistCreated  Whether to persist the newly created entity
  194.      *
  195.      * @return Object   The created entity
  196.      *
  197.      * @throws MissingRequiredFieldsException, EntityValidationException
  198.      */
  199.     public function safeCreate($values$save true, array $additionalData = array(), $persistCreated true)
  200.     {
  201.         // Set default values
  202.         $values $this->setCreateDefaultValues($values$additionalData);
  203.         // Make sure all field requirements are met
  204.         if (!$this->areRequiredFieldsNamesMetForAction($values'create')) {
  205.             $missingFieldsException = new MissingRequiredFieldsException();
  206.             $missingFieldsException->setRequiredFieldNames(
  207.                 $this->getRequiredFieldsNamesForAction($values'create')
  208.             );
  209.             $missingFieldsException->setProvidedFieldNames(array_keys($values));
  210.             throw $missingFieldsException;
  211.         }
  212.         // Do all the fields values validations
  213.         $errors $this->validateValues($values);
  214.         if (!is_array($errors)) {
  215.             // Add a check to not allow omitting returning the $errors array
  216.             throw new Exception($this->translatorService->trans(
  217.                 'uplifted.base_bundle.dev_error.omitted_return_value_of_validate_values_method',
  218.                 array('%calledClass%' => get_called_class())
  219.             ));
  220.         }
  221.         if (!empty($errors)) {
  222.             $this->appLogger->info('BEM____401 - Entity validation error', array(
  223.                 'calledClass' => get_called_class(),
  224.                 'errors' => $errors,
  225.                 'values' => $values,
  226.             ));
  227.             throw (new EntityValidationException())->setErrors($errors);
  228.         }
  229.         // Finally build the new entity from the values
  230.         return $this->createFromValues($values$save$additionalData$persistCreated);
  231.     }
  232.     /**
  233.      * Entity creation based on provided values
  234.      *
  235.      * @param array    $values          Key-value map of properties and their values
  236.      * @param boolean  $save            Whether to trigger a save operation or not. Defaults to true
  237.      * @param array    $additionalData  Array to hold any additional data used for general purposes
  238.      * @param bool     $persistCreated  Whether to persist the newly created entity
  239.      *
  240.      * @return Object   The created entity
  241.      */
  242.     protected function createFromValues($values$save true, array $additionalData = array(), $persistCreated true)
  243.     {
  244.         // Atomize action into a transaction
  245.         $transactionBegunNow $this->beginTransaction();
  246.         // Include $transactionBegunNow flag in the additionalData array
  247.         $additionalData['transactionBegunNow'] = $transactionBegunNow;
  248.         try {
  249.             // Flag we will need to flush changes
  250.             $this->requireFlush();
  251.             // Get a new entity instance
  252.             $entity $this->getNewEntityInstance();
  253.             // Persist the new entity in the entity manager, if needed
  254.             if ($persistCreated === true) {
  255.                 $this->getDoctrineEntityManager()->persist($entity);
  256.             }
  257.             // Perform any actions needed before setting the provided values
  258.             $beforeSettingValuesReturnData $this->beforeSettingValues($entity$values);
  259.             // Set the values
  260.             $this->setValues($entity$values);
  261.             // Treat any related entities as 'add' action since on creation we would not be removing any
  262.             $this->addRelatedEntities($entity$values);
  263.             // Perform any required actions before saving the new entity
  264.             $this->beforeSavingEntityCreation(
  265.                 $entity$values$beforeSettingValuesReturnData$additionalData
  266.             );
  267.             // Perform any required actions before saving the entity
  268.             $this->beforeSavingEntity(
  269.                 $entity$values$beforeSettingValuesReturnData'add'$additionalData
  270.             );
  271.             // Queue an event listener on the postFlush event for AfterSavingEntityCreationListener
  272.             $this->eventManager->addEventListener(
  273.                 Events::postFlush,
  274.                 new AfterSavingEntityCreationListener(
  275.                     $this->eventManager$this$entity$values,
  276.                     $beforeSettingValuesReturnData$additionalData
  277.                 )
  278.             );
  279.             // Queue an event listener on the postFlush event for AfterSavingEntityListener
  280.             $this->eventManager->addEventListener(
  281.                 Events::postFlush,
  282.                 new AfterSavingEntityListener(
  283.                     $this->eventManager$this$entity$values$additionalData
  284.                 )
  285.             );
  286.             if ($save) {
  287.                 // Save everything
  288.                 $this->flush();
  289.                 // Commit the transaction, if corresponds
  290.                 if ($transactionBegunNow) {
  291.                     $this->commitTransaction();
  292.                 }
  293.             }
  294.             return $entity;
  295.         } catch (Exception $ex) {
  296.             $this->rollbackTransactionAndReThrowException($ex);
  297.         }
  298.     }
  299.     /**
  300.      * Entity update based on provided values, conditioned to validity of them
  301.      *
  302.      * @param Object   $entity                    The entity to update
  303.      * @param array    $values                    Key-value map of properties and their values
  304.      * @param boolean  $save                      Whether to trigger a save operation or not. Defaults to true
  305.      * @param null     $addRemoveRelatedEntities  Values: 'add'|'remove'|null. Flag to add or remove related entities
  306.      *                                            (passed in the $values array too)
  307.      * @param array    $additionalData            Array to hold any additional data used for general purposes
  308.      *
  309.      * @return Object                             The updated entity
  310.      * @throws EntityValidationException
  311.      * @throws MissingRequiredFieldsException
  312.      * @throws Exception
  313.      */
  314.     public function safeUpdate($entity$values$save true$addRemoveRelatedEntities null, array $additionalData = array())
  315.     {
  316.         // Set default values
  317.         $values $this->setUpdateDefaultValues($entity$values);
  318.         // Make sure all field requirements are met
  319.         if (!$this->areRequiredFieldsNamesMetForAction($values'update')) {
  320.             $missingFieldsException = new MissingRequiredFieldsException();
  321.             $missingFieldsException->setRequiredFieldNames(
  322.                 $this->getRequiredFieldsNamesForAction($values'update')
  323.             );
  324.             $missingFieldsException->setProvidedFieldNames(array_keys($values));
  325.             throw $missingFieldsException;
  326.         }
  327.         // Do all the fields values validations
  328.         $errors $this->validateValues($values$entity);
  329.         if (!is_array($errors)) {
  330.             // Add a check to not allow omitting returning the $errors array
  331.             throw new Exception($this->translatorService->trans(
  332.                 'uplifted.base_bundle.dev_error.omitted_return_value_of_validate_values_method',
  333.                 array('%calledClass%' => get_called_class())
  334.             ));
  335.         }
  336.         if (!empty($errors)) {
  337.             $this->appLogger->info('BEM____402 - Entity validation error', array(
  338.                 'calledClass' => get_called_class(),
  339.                 'errors' => $errors,
  340.                 'values' => $values,
  341.             ));
  342.             throw (new EntityValidationException())->setErrors($errors);
  343.         }
  344.         // Update the entity from the values
  345.         return $this->updateFromValues($entity$values$save$addRemoveRelatedEntities$additionalData);
  346.     }
  347.     /**
  348.      * Entity update based on provided values
  349.      *
  350.      * @param Object   $entity                                The entity to update
  351.      * @param array    $values                                Key-value map of properties and their values
  352.      * @param boolean  $save                                  Whether to trigger a save operation or not. Defaults to
  353.      *                                                        true
  354.      * @param null     $addRemoveRelatedEntities              Values: 'add'|'remove'|null. Flag to add or remove
  355.      *                                                        related entities (passed in the $values array too)
  356.      * @param array    $additionalData                        Array to hold any additional data used for general
  357.      *                                                        purposes
  358.      *
  359.      * @return Object               The created entity
  360.      * @throws Exception
  361.      */
  362.     protected function updateFromValues($entity$values$save true$addRemoveRelatedEntities null, array $additionalData = array())
  363.     {
  364.         // Atomize action into a transaction
  365.         $transactionBegunNow $this->beginTransaction();
  366.         // Include $transactionBegunNow flag in the additionalData array
  367.         $additionalData['transactionBegunNow'] = $transactionBegunNow;
  368.         try {
  369.             // Flag we will need to flush changes
  370.             $this->requireFlush();
  371.             // If we are dealing with a new (not managed) entity, then persist it
  372.             if ($this->getDoctrineEntityManager()->getUnitOfWork()->getEntityState($entity) == UnitOfWork::STATE_NEW) {
  373.                 try {
  374.                     $this->getDoctrineEntityManager()->persist($entity);
  375.                 } catch (ORMException $ex) {
  376.                     throw new Exception($ex->getMessage());
  377.                 }
  378.             }
  379.             // Perform any actions needed before setting the provided values
  380.             $beforeSettingValuesReturnData $this->beforeSettingValues($entity$values);
  381.             // Set the values
  382.             $this->setValues($entity$values);
  383.             // Add or remove related entities
  384.             switch ($addRemoveRelatedEntities) {
  385.                 case 'add':
  386.                     $this->addRelatedEntities($entity$values);
  387.                     break;
  388.                 case 'remove':
  389.                     $this->removeRelatedEntities($entity$values);
  390.                     break;
  391.             }
  392.             // Perform any required actions before saving the updated entity
  393.             $this->beforeSavingEntityUpdate(
  394.                 $entity$values$beforeSettingValuesReturnData,
  395.                 $addRemoveRelatedEntities$additionalData
  396.             );
  397.             // Perform any required actions before saving the entity
  398.             $this->beforeSavingEntity(
  399.                 $entity$values$beforeSettingValuesReturnData,
  400.                 $addRemoveRelatedEntities$additionalData
  401.             );
  402.             // Queue an event listener on the postFlush event for AfterSavingEntityUpdateListener
  403.             $this->eventManager->addEventListener(
  404.                 Events::postFlush,
  405.                 new AfterSavingEntityUpdateListener(
  406.                     $this->eventManager$this$entity$values,
  407.                     $beforeSettingValuesReturnData$addRemoveRelatedEntities,
  408.                     $additionalData
  409.                 )
  410.             );
  411.             // Queue an event listener on the postFlush event for AfterSavingEntityListener
  412.             $this->eventManager->addEventListener(
  413.                 Events::postFlush,
  414.                 new AfterSavingEntityListener(
  415.                     $this->eventManager$this$entity$values$additionalData
  416.                 )
  417.             );
  418.             if ($save) {
  419.                 // Save everything
  420.                 $this->flush();
  421.                 // Commit the transaction, if corresponds
  422.                 if ($transactionBegunNow) {
  423.                     $this->commitTransaction();
  424.                 }
  425.             }
  426.             return $entity;
  427.         } catch (Exception $ex) {
  428.             $this->rollbackTransactionAndReThrowException($ex);
  429.         }
  430.     }
  431.     /**
  432.      * Delete an entity
  433.      *
  434.      * @param object   $entity          The entity to be deleted
  435.      * @param boolean  $save            Whether to trigger a save operation or not. Defaults to true
  436.      * @param array    $requestParams   Parameters provided in the delete request
  437.      * @param array    $additionalData  Array to hold any additional data used for general purposes
  438.      *
  439.      * @throws ORMException
  440.      * @throws AccessDeniedException
  441.      * @throws Exception
  442.      */
  443.     public function delete(object $entitybool $save true, array $requestParams = array(), array $additionalData = array()): void
  444.     {
  445.         // The logged-in user must have access to the entity. The 'find' operation will not find the entity if the
  446.         // logged-in user does not have access to it
  447.         if (
  448.             $entity->getId() !== null &&
  449.             $this->find($entity->getId()) === null
  450.         ) {
  451.             throw new AccessDeniedException();
  452.         }
  453.         // Flag we will need to flush changes
  454.         $this->requireFlush();
  455.         // Perform any required actions before removing the entity from the entity manager
  456.         $this->beforeRemovingEntityFromEntityManager($entity$requestParams);
  457.         // Remove from persistence entity manager's scope
  458.         $this->getDoctrineEntityManager()->remove($entity);
  459.         // Perform any actions after removing the entity but before saving it
  460.         $this->beforeSavingEntityDeletion($entity$requestParams);
  461.         // Add the deleted entity id and class as additionalData
  462.         $additionalData['deletedEntityId'] = $entity->getId();
  463.         $additionalData['deletedEntityClass'] = get_class($entity);
  464.         // Queue an event listener on the postFlush event for AfterDeletingEntityListener
  465.         $this->eventManager->addEventListener(
  466.             Events::postFlush,
  467.             new AfterSavingEntityDeletionListener(
  468.                 $this->eventManager$this$requestParams$additionalData
  469.             )
  470.         );
  471.         // Conditional save of the operation
  472.         if ($save === true) {
  473.             $this->flush();
  474.         }
  475.     }
  476.     /**
  477.      * Add a related entity into a one-to-many or many-to-many relationship collection
  478.      * Shortcut to addRemoveRelatedEntities invoked with 'add' value for $addRemove
  479.      * Called from the createFromValues and updateFromValues action (optional in updateFromValues)
  480.      *
  481.      * @param Object  $entity  The entity to update the relationship from
  482.      * @param array   $values  Key-value map of properties and their values
  483.      */
  484.     protected function addRelatedEntities(&$entity$values)
  485.     {
  486.         $this->addRemoveRelatedEntities($entity$values'add');
  487.     }
  488.     /**
  489.      * Remove a related entity from a one-to-many or many-to-many relationship collection
  490.      * Shortcut to addRemoveRelatedEntities invoked with 'remove' value for $addRemove
  491.      *
  492.      * @param Object  $entity  The entity to update the relationship from
  493.      * @param array   $values  Key-value map of properties and their values
  494.      */
  495.     protected function removeRelatedEntities(&$entity$values)
  496.     {
  497.         $this->addRemoveRelatedEntities($entity$values'remove');
  498.     }
  499.     /**
  500.      * Add or remove a related entity to/from a one-to-many or many-to-many relationship collection
  501.      *
  502.      * @param Object          $entity     The entity to update the relationship from
  503.      * @param array           $values     Key-value map of properties and their values
  504.      * @param 'add'|'remove'  $addRemove  Flag to add or remove related entities (passed in the $values array too)
  505.      */
  506.     protected function addRemoveRelatedEntities(&$entity$values$addRemove)
  507.     {
  508.     }
  509.     /**
  510.      * Shortcut to save and commit all pending unflushed data
  511.      *
  512.      * @param boolean  $andCommit  whether to commit or not a possible ongoing transaction
  513.      */
  514.     public function save(bool $andCommit true)
  515.     {
  516.         // Save
  517.         $this->flush();
  518.         if ($andCommit) {
  519.             $this->commitTransaction();
  520.         }
  521.     }
  522.     /**
  523.      * Shortcut to mark flush as required
  524.      */
  525.     public function requireFlush()
  526.     {
  527.         $this->flushManager->markFlushAsRequired(
  528.             spl_object_id($this->doctrineEntityManager)
  529.         );
  530.     }
  531.     /**
  532.      * Shortcut to refresh an entity with the doctrine entity manager
  533.      *
  534.      * @param object  $entity  The persisted object we want to refresh
  535.      */
  536.     public function refresh($entity)
  537.     {
  538.         $this->getDoctrineEntityManager()->refresh($entity);
  539.     }
  540.     /**
  541.      * Shortcut to find on the entity repository
  542.      *
  543.      * @param int  $entityId
  544.      *
  545.      * @return object|null
  546.      * @throws Exception
  547.      */
  548.     public function find(int $entityId)
  549.     {
  550.         // If the entity manager adds compulsory filters, or it defines it must find using enforced filters then run the
  551.         // 'find' operation as a 'findBy id' one
  552.         if (
  553.             count($this->addCompulsoryFilters(array())) > ||
  554.             $this->mustFindUsingEnforcedFilters()
  555.         ) {
  556.             $results $this->findBy(array('id' => $entityId));
  557.             if (count($results) > 0) {
  558.                 return reset($results);
  559.             } else {
  560.                 return null;
  561.             }
  562.         } else {
  563.             return $this->getEntityRepo()->find($entityId);
  564.         }
  565.     }
  566.     /**
  567.      * Shortcut to findBy on the entity repository
  568.      *
  569.      * @see Uplifted\BaseBundle\Repository\BaseEntityRepository::findBy()
  570.      */
  571.     public function findBy(array $filters, array|null $orderBy nullint|null $limit nullint|null $offset null$distinct null$count false$export false, array $modifiers = array()): array
  572.     {
  573.         // If required, then filter by custom enforced criteria
  574.         if ($this->mustFindUsingEnforcedFilters()) {
  575.             return $this->getEntityRepo()->findByUsingEnforcedFilters(
  576.                 $this->addCompulsoryFilters($filters), $orderBy$limit$offset$distinct$count$export$modifiers,
  577.                 $this->getEnforcedFiltersAdditionalData()
  578.             );
  579.         } else {
  580.             return $this->getEntityRepo()->findBy(
  581.                 $this->addCompulsoryFilters($filters), $orderBy$limit$offset$distinct$count$export$modifiers
  582.             );
  583.         }
  584.     }
  585.     /**
  586.      * Shortcut to findOneBy on the entity repository
  587.      *
  588.      * @see Uplifted\BaseBundle\Repository\BaseEntityRepository::findOneBy()
  589.      */
  590.     public function findOneBy(array $filters)
  591.     {
  592.         if ($this->mustFindUsingEnforcedFilters()) {
  593.             $results $this->findBy($filters);
  594.             if (count($results) > 0) {
  595.                 if (count($results) == 1) {
  596.                     return reset($results);
  597.                 } else {
  598.                     throw new \Exception('Trying to find one, found more. Make sure you are providing a unique criteria');
  599.                 }
  600.             }
  601.         } else {
  602.             return $this->getEntityRepo()->findOneBy($this->addCompulsoryFilters($filters));
  603.         }
  604.     }
  605.     /**
  606.      * Shortcut to findAll on the entity repository
  607.      *
  608.      * @see Uplifted\BaseBundle\Repository\BaseEntityRepository::findAll()
  609.      */
  610.     public function findAll()
  611.     {
  612.         if ($this->mustFindUsingEnforcedFilters()) {
  613.             return $this->findBy(array());
  614.         } else {
  615.             return $this->getEntityRepo()->findAll();
  616.         }
  617.     }
  618.     /**
  619.      * Function to make a hybrid search for an entity based on a single attribute and a text search term
  620.      *
  621.      * @param string      $searchValue
  622.      * @param string      $attribute
  623.      * @param array|null  $entities
  624.      *
  625.      * @return mixed
  626.      */
  627.     public function hybridFindOneByAttribute(string $searchValuestring $attribute 'name', ?array $entities null)
  628.     {
  629.         // First look for an exact match
  630.         $matchingEntities $this->findBy(array(
  631.             $attribute => $searchValue
  632.         ));
  633.         if (!empty($matchingEntities)) {
  634.             return $matchingEntities[0];
  635.         }
  636.         // If failed, then build a list of entity options and fallback to AI search
  637.         // Use provided entities list or fetch all if not provided
  638.         $entityOptions array_map(fn($entity) => [
  639.             'searchTerm' => $entity->{'get' ucfirst($attribute)}(),
  640.             'entity' => $entity,
  641.         ], $entities ?? $this->findAll());
  642.         return $this->aiCompletionService->matchSearchTermToOption($searchValue$entityOptionstrue);
  643.     }
  644.     /**
  645.      * Shortcut to exists on the entity repository
  646.      *
  647.      * @param $filters array The filters to use to find the entity to check existence of
  648.      *
  649.      * @throws Exception
  650.      * @see Uplifted\BaseBundle\Repository\BaseEntityRepository::exists()
  651.      */
  652.     public function exists(array $filters)
  653.     {
  654.         if ($this->mustFindUsingEnforcedFilters()) {
  655.             $result $this->findBy($filtersnullnullnullnulltrue);
  656.             return $result['count'] && $result['count'] > 0;
  657.         } else {
  658.             return $this->getEntityRepo()->exists($this->addCompulsoryFilters($filters));
  659.         }
  660.     }
  661.     /**
  662.      * Convenience function to add special filters required on every query to the entity
  663.      * repository
  664.      *
  665.      * @param array  $filters  Key-value pairs with keys being the fields names to apply 'where'
  666.      *                         conditions and the values are the values to filter on
  667.      *
  668.      * @return array
  669.      */
  670.     public function addCompulsoryFilters(array $filters)
  671.     {
  672.         return $filters;
  673.     }
  674.     /* Protected functions */
  675.     /* ------------------- */
  676.     /**
  677.      * Modules that require enforced filters must override this method
  678.      *
  679.      * @return bool
  680.      */
  681.     protected function mustFindUsingEnforcedFilters(): bool
  682.     {
  683.         return false;
  684.     }
  685.     protected function getEnforcedFiltersAdditionalData()
  686.     {
  687.         return array();
  688.     }
  689.     /**
  690.      * Shortcut for Doctrine\ORM\EntityManagerInterface::getReference()
  691.      *
  692.      * Gets a reference to the entity identified by the given type and identifier
  693.      * without actually loading it, if the entity is not yet loaded.
  694.      *
  695.      * @param string  $entityName  The name of the entity type.
  696.      * @param mixed   $id          The entity identifier.
  697.      */
  698.     protected function getEntityReference($entityName$id)
  699.     {
  700.         return $this->getDoctrineEntityManager()->getReference($entityName$id);
  701.     }
  702.     /**
  703.      * Function to get access to the persistence entity manager
  704.      *
  705.      * @return DoctrineEntityManager
  706.      */
  707.     public function getDoctrineEntityManager()
  708.     {
  709.         return $this->doctrineEntityManager;
  710.     }
  711.     public function persist($entity)
  712.     {
  713.         return $this->getDoctrineEntityManager()->persist($entity);
  714.     }
  715.     /**
  716.      * Performs a loop of doctrine entity manager's flush operations to save everything
  717.      * and guarantee the postFlush event queue is completely emptied.
  718.      * Runs flush operation between 1 and 3 times.
  719.      *
  720.      * Finally runs flush() one last time in case changes made during the last series of
  721.      * afterSaving...() methods were made and still need saving.
  722.      *
  723.      * NOTE: this repeated saving is not doing what expected because after the flush operation
  724.      * the entityManager clears all the UnitOfWork insertions, deletions, updates, etc, so
  725.      * adding to those in the afterSavingEntity... functions is futile, if not expressly
  726.      * calling the flush method again. Right now for the AfterSavingEntityCreation one we
  727.      * are calling the flush operation explicitly after executing the custom function (in
  728.      * the AfterSavingEntityCreationListener)
  729.      */
  730.     public function flush()
  731.     {
  732.         if ($this->flushManager->isFlushInProgress(
  733.             spl_object_id($this->doctrineEntityManager)
  734.         )) {
  735.             return;
  736.         }
  737.         // Consider the flush process started
  738.         $this->flushManager
  739.             ->markFlushInProgress(spl_object_id($this->doctrineEntityManager))
  740.         ;
  741.         $i 1;
  742.         do {
  743.             // Always flush if reaching this point, not explicitly checking if required
  744.             $this->doctrineEntityManager->flush();
  745.             $this->flushManager
  746.                 ->markFlushAsNotRequired(spl_object_id($this->doctrineEntityManager))
  747.             ;
  748.             $i++;
  749.             if ($this->eventManager->hasListeners(Events::postFlush)) {
  750.                 $this->eventManager->dispatchEvent(Events::postFlush);
  751.             }
  752.         } while (
  753.             $i <= &&
  754.             $this->flushManager->isFlushRequired(spl_object_id($this->doctrineEntityManager)) === true
  755.         );
  756.         // Consider the flush process ended
  757.         $this->flushManager
  758.             ->markFlushAsNotInProgress(spl_object_id($this->doctrineEntityManager))
  759.         ;
  760.     }
  761.     /**
  762.      * Function to allow the setup of default values on creation time
  763.      * Invoke parent function first
  764.      *
  765.      * @param array  $values          Key-value map of properties and their values
  766.      * @param array  $additionalData  Array to hold any additional data used for general purposes
  767.      *
  768.      * @return array            The resulting key-value map of properties and their values
  769.      */
  770.     protected function setCreateDefaultValues(array $values, array $additionalData = array())
  771.     {
  772.         return $values;
  773.     }
  774.     /**
  775.      * Function to allow the setup of default values on update time
  776.      * Invoke parent function first
  777.      *
  778.      * @param Object  $entity  The entity being updated
  779.      * @param array   $values  Key-value map of properties and their values
  780.      *
  781.      * @return array            The resulting key-value map of properties and their values
  782.      */
  783.     protected function setUpdateDefaultValues($entity, array $values)
  784.     {
  785.         return $values;
  786.     }
  787.     /**
  788.      * Method called before setting the entities values (calling the 'setValues' method)
  789.      * for both create and update actions
  790.      *
  791.      * @param Object  $entity  The entity being created/updated
  792.      * @param array   $values  The values being set
  793.      *
  794.      * @return array  An array with the values before setting them
  795.      */
  796.     protected function beforeSettingValues(&$entity, array &$values): array
  797.     {
  798.         $beforeValues = array();
  799.         foreach ($values as $fieldName => $value) {
  800.             if (method_exists($entity$this->getFieldGetterName($fieldName))) {
  801.                 $beforeValues[$fieldName] = $entity->{$this->getFieldGetterName($fieldName)}();
  802.             }
  803.         }
  804.         return $beforeValues;
  805.     }
  806.     /**
  807.      * Method called before saving the entity creation
  808.      *
  809.      * @param Object  $entity                         The entity being created
  810.      * @param array   $values                         The values used to create the entity
  811.      * @param mixed   $beforeSettingValuesReturnData  Any data returned by function beforeSettingValues()
  812.      * @param array   $additionalData                 Array to hold any additional data used for general purposes
  813.      */
  814.     protected function beforeSavingEntityCreation(&$entity$values$beforeSettingValuesReturnData null, array $additionalData null)
  815.     {
  816.     }
  817.     /**
  818.      * Method called before saving the entity update action
  819.      *
  820.      * @param Object               $entity                         The entity being updated
  821.      * @param array                $values                         The values used to update the entity
  822.      * @param mixed                $beforeSettingValuesReturnData  Any data returned by function beforeSettingValues()
  823.      * @param 'add'|'remove'|null  $addRemoveRelatedEntities       Flag to add or remove related entities (passed in
  824.      *                                                             the $values array too)
  825.      * @param array                $additionalData                 Array to hold any additional data used for general
  826.      *                                                             purposes
  827.      */
  828.     protected function beforeSavingEntityUpdate(&$entity$values$beforeSettingValuesReturnData null$addRemoveRelatedEntities null, array $additionalData null)
  829.     {
  830.     }
  831.     /**
  832.      * Method called before saving the entity for both create and update actions
  833.      *
  834.      * @param Object               $entity                         The entity being saved
  835.      * @param array                $values                         The values used to create/update the entity
  836.      * @param mixed                $beforeSettingValuesReturnData  Any data returned by function beforeSettingValues()
  837.      * @param 'add'|'remove'|null  $addRemoveRelatedEntities       Flag to add or remove related entities (passed in
  838.      *                                                             the $values array too)
  839.      * @param array                $additionalData                 Array to hold any additional data used for general
  840.      *                                                             purposes
  841.      */
  842.     protected function beforeSavingEntity(&$entity$values$beforeSettingValuesReturnData null$addRemoveRelatedEntities null, array $additionalData null)
  843.     {
  844.     }
  845.     /**
  846.      * Method called after saving the entity creation
  847.      *
  848.      * @param Object  $entity                         The entity being created
  849.      * @param array   $values                         The values used to create the entity
  850.      * @param mixed   $beforeSettingValuesReturnData  Any data returned by function beforeSettingValues()
  851.      * @param array   $additionalData                 Array to hold any additional data used for general purposes
  852.      */
  853.     public function afterSavingEntityCreation(&$entity$values$beforeSettingValuesReturnData null, array $additionalData = array())
  854.     {
  855.     }
  856.     /**
  857.      * Method called after the entity update action
  858.      *
  859.      * @param Object               $entity                         The entity being updated
  860.      * @param array                $values                         The values used to update the entity
  861.      * @param mixed                $beforeSettingValuesReturnData  Any data returned by function beforeSettingValues()
  862.      * @param 'add'|'remove'|null  $addRemoveRelatedEntities       Flag to add or remove related entities (passed in
  863.      *                                                             the $values array too)
  864.      * @param array                $additionalData                 Array to hold any additional data used for general
  865.      *                                                             purposes
  866.      */
  867.     public function afterSavingEntityUpdate(&$entity$values$beforeSettingValuesReturnData null$addRemoveRelatedEntities null, array $additionalData = array())
  868.     {
  869.     }
  870.     /**
  871.      * Method called after saving the entity for both create and update actions
  872.      *
  873.      * @param Object  $entity          The entity being saved
  874.      * @param array   $values          The values used to create/update the entity
  875.      * @param array   $additionalData  Array to hold any additional data used for general purposes
  876.      */
  877.     public function afterSavingEntity(&$entity$values, array $additionalData = array())
  878.     {
  879.     }
  880.     /**
  881.      * Method called before removing an entity being deleted
  882.      *
  883.      * @param object  $entity         The entity to be deleted
  884.      * @param array   $requestParams  Parameters provided in the delete request
  885.      */
  886.     protected function beforeRemovingEntityFromEntityManager($entity$requestParams)
  887.     {
  888.     }
  889.     /**
  890.      * Method called before saving the deletion of an entity
  891.      *
  892.      * @param object  $entity         The entity to be deleted
  893.      * @param array   $requestParams  Parameters provided in the delete request
  894.      */
  895.     protected function beforeSavingEntityDeletion($entity$requestParams)
  896.     {
  897.     }
  898.     /**
  899.      * Method called after saving the deletion of an entity
  900.      *
  901.      * @param array  $requestParams   Parameters provided in the delete request
  902.      * @param array  $additionalData  Array to hold any additional data used for general purposes
  903.      */
  904.     public function afterSavingEntityDeletion(array $requestParams = array(), array $additionalData = array())
  905.     {
  906.     }
  907.     /**
  908.      * Shortcut function to perform simple/standard validations of fields via a
  909.      * configuration array.
  910.      *
  911.      * @param array  $values  The values used to create/update the entity
  912.      * @param array  $errors  The array carrying all validation errors
  913.      * @param array  $config  An array with fieldNames as keys and
  914.      *                        $fieldValidations as values. The fieldValidations is also an array which can either
  915.      *                        be:
  916.      *                        1) a non-associative array with just a list of validations (if none of the
  917.      *                        validations require extra options),
  918.      *                        or
  919.      *                        2) an associative array with validation type as keys and an array of options as
  920.      *                        values.
  921.      *
  922.      * @return array    An array with the fieldName as keys and the entities as values for
  923.      * the validatedEntites
  924.      */
  925.     protected function simpleValidateValues($values, &$errors, array $config)
  926.     {
  927.         $returnEntities = array();
  928.         foreach ($config as $fieldName => $fieldValidations) {
  929.             foreach ($fieldValidations as $validationIndexOrType => $validationTypeOrConfig) {
  930.                 // Setup validationType and validationConfig from the config element and key
  931.                 if (is_numeric($validationIndexOrType)) {
  932.                     $validationType $validationTypeOrConfig;
  933.                     $validationConfig null;
  934.                 } else {
  935.                     $validationType $validationIndexOrType;
  936.                     $validationConfig $validationTypeOrConfig;
  937.                 }
  938.                 // Assess validation type 'required' separately
  939.                 if ($validationType == 'required') {
  940.                     if (isset($values[$fieldName])) {
  941.                         $this->validationService->validateNotNull($errors$fieldName,
  942.                             $values[$fieldName]);
  943.                         $this->validationService->validateNotBlank($errors$fieldName,
  944.                             $values[$fieldName]);
  945.                     } else {
  946.                         $errors[$fieldName] = $this->translatorService->trans('uplifted.base_bundle.validator.required');
  947.                     }
  948.                 } else {
  949.                     if (isset($values[$fieldName])) {
  950.                         switch ($validationType) {
  951.                             case 'length':
  952.                                 if (!isset($validationConfig['min'])) {
  953.                                     $validationConfig['min'] = null;
  954.                                 }
  955.                                 if (!isset($validationConfig['max'])) {
  956.                                     $validationConfig['max'] = null;
  957.                                 }
  958.                                 $this->validationService->validateLength($errors,
  959.                                     $fieldName$values[$fieldName],
  960.                                     $validationConfig['min'], $validationConfig['max']);
  961.                                 break;
  962.                             case 'email':
  963.                                 $this->validationService->validateEmail($errors,
  964.                                     $fieldName$values[$fieldName]);
  965.                                 break;
  966.                             case 'range':
  967.                                 if (!isset($validationConfig['min'])) {
  968.                                     $validationConfig['min'] = null;
  969.                                 }
  970.                                 if (!isset($validationConfig['max'])) {
  971.                                     $validationConfig['max'] = null;
  972.                                 }
  973.                                 $this->validationService->validateRange($errors,
  974.                                     $fieldName$values[$fieldName],
  975.                                     $validationConfig['min'], $validationConfig['max']);
  976.                                 break;
  977.                             case 'number':
  978.                             case 'float':
  979.                                 $this->validationService->validateNumber($errors,
  980.                                     $fieldName$values[$fieldName]);
  981.                                 break;
  982.                             case 'int':
  983.                             case 'integer':
  984.                                 $this->validationService->validateInt($errors$fieldName,
  985.                                     $values[$fieldName]);
  986.                                 break;
  987.                             case 'false':
  988.                                 $this->validationService->validateFalse($errors,
  989.                                     $fieldName$values[$fieldName]);
  990.                                 break;
  991.                             case 'true':
  992.                                 $this->validationService->validateTrue($errors,
  993.                                     $fieldName$values[$fieldName]);
  994.                                 break;
  995.                             case 'bool':
  996.                             case 'boolean':
  997.                                 $this->validationService->validateBoolean($errors,
  998.                                     $fieldName$values[$fieldName]);
  999.                                 break;
  1000.                             case 'date':
  1001.                             case 'dateTime':
  1002.                                 if (!isset($validationConfig['min'])) {
  1003.                                     $validationConfig['min'] = null;
  1004.                                 }
  1005.                                 if (!isset($validationConfig['max'])) {
  1006.                                     $validationConfig['max'] = null;
  1007.                                 }
  1008.                                 $this->validationService->validateDate($errors,
  1009.                                     $fieldName$values[$fieldName], false,
  1010.                                     $validationConfig['min'], $validationConfig['max']);
  1011.                                 break;
  1012.                             case 'time':
  1013.                                 $this->validationService->validateDate($errors,
  1014.                                     $fieldName$values[$fieldName], true);
  1015.                                 break;
  1016.                             case 'choice':
  1017.                                 $this->validationService->validateChoice($errors,
  1018.                                     $fieldName$values[$fieldName],
  1019.                                     $validationConfig['options'],
  1020.                                     'The value "' $values[$fieldName] . '" is not a valid choice. Choose one of: ' implode(', '$validationConfig['options'])
  1021.                                 );
  1022.                                 break;
  1023.                             case 'image':
  1024.                                 $this->validationService->validateImageUpload(
  1025.                                     $errors$fieldName$values[$fieldName],
  1026.                                     isset($validationConfig['limitDimensions']) ? $validationConfig['limitDimensions'] : null,
  1027.                                     isset($validationConfig['allowedMimeTypes']) ? $validationConfig['allowedMimeTypes'] : array(
  1028.                                         'image/gif',
  1029.                                         'image/jpeg',
  1030.                                         'image/png'
  1031.                                     )
  1032.                                 );
  1033.                                 break;
  1034.                             case 'url':
  1035.                                 if (trim($values[$fieldName]) != '') {
  1036.                                     $this->validationService->validateUrl($errors,
  1037.                                         $fieldName$values[$fieldName],
  1038.                                         isset($validationConfig['options']) ? $validationConfig['options'] : array()
  1039.                                     );
  1040.                                 } else {
  1041.                                     $this->validationService->validateNotBlank($errors$fieldName$values[$fieldName]);
  1042.                                 }
  1043.                                 break;
  1044.                             case 'entity':
  1045.                             case 'entityOrNull':
  1046.                             case 'entityOrEntities':
  1047.                                 if (
  1048.                                     !isset($validationConfig['entityClassName'])
  1049.                                 ) {
  1050.                                     throw new Exception('Dev exception: required options for simpleValidateValues on "entity" type '
  1051.                                         'are "repositoryName" and "entityClassName" when dealing with "' $fieldName '" field');
  1052.                                 }
  1053.                                 if ($validationType == 'entity') {
  1054.                                     $entity $this->validateEntity(
  1055.                                         $errors$values$fieldName$validationConfig['entityClassName']
  1056.                                     );
  1057.                                     if ($entity !== null) {
  1058.                                         $returnEntities[$fieldName] = $entity;
  1059.                                     }
  1060.                                 } elseif ($validationType == 'entityOrNull') {
  1061.                                     // 'null' value validates correctly in this case
  1062.                                     if ($values[$fieldName] !== null) {
  1063.                                         $entity $this->validateEntity(
  1064.                                             $errors$values$fieldName$validationConfig['entityClassName']
  1065.                                         );
  1066.                                         if ($entity !== null) {
  1067.                                             $returnEntities[$fieldName] = $entity;
  1068.                                         }
  1069.                                     }
  1070.                                 } elseif ($validationType == 'entities') {
  1071.                                     $entities $this->validateEntities(
  1072.                                         $errors$values$fieldName$validationConfig['entityClassName']
  1073.                                     );
  1074.                                     if (count($entities) > 0) {
  1075.                                         $returnEntities[$fieldName] = $entities;
  1076.                                     }
  1077.                                 } elseif ($validationType == 'entityOrEntities') {
  1078.                                     $entities $this->validateEntityOrEntities(
  1079.                                         $errors$values$fieldName$validationConfig['entityClassName']
  1080.                                     );
  1081.                                     if (count($entities) > 0) {
  1082.                                         $returnEntities[$fieldName] = $entities;
  1083.                                     }
  1084.                                 }
  1085.                                 break;
  1086.                         }
  1087.                     }
  1088.                 }
  1089.             }
  1090.         }
  1091.         return $returnEntities;
  1092.     }
  1093.     /**
  1094.      * Shortcut function to validate 2 dates are chronological in the order given,
  1095.      * bearing in mind the 2 dates existed or not in the entity and any or both of the
  1096.      * dates is/are being updated
  1097.      *
  1098.      * @param array   $values             The values used to create/update the entity
  1099.      * @param object  $entity             The entity being created/updated
  1100.      * @param array   $errors             The array carrying all validation errors
  1101.      * @param string  $dateFromFieldName  The field name for the earliest date
  1102.      * @param string  $dateToFieldName    The field name for the latest date
  1103.      */
  1104.     public function validateDatesOrder($values$entity$errorsstring $dateFromFieldNamestring $dateToFieldName)
  1105.     {
  1106.         if (
  1107.             isset($values[$dateFromFieldName]) ||
  1108.             isset($values[$dateToFieldName])
  1109.         ) {
  1110.             $this->validationService->validateStartDateTimePriorToEndDateTime(
  1111.                 $errors$dateFromFieldName,
  1112.                 $values[$dateFromFieldName] ?? null,
  1113.                 $values[$dateToFieldName] ?? null,
  1114.                 $entity?->${$this->getFieldGetterName($dateFromFieldName)}(),
  1115.                 $entity?->${$this->getFieldGetterName($dateToFieldName)}()
  1116.             );
  1117.         }
  1118.     }
  1119.     /**
  1120.      * Shortcut function to validate an entity and write to $errors array any errors found
  1121.      * Combines the validationService->validateEntity() function with the entity retrieval
  1122.      * callback and the translation for the entity/relation name
  1123.      *
  1124.      * @param array   $errors           The array carrying all validation errors
  1125.      * @param array   $values           The values used to create/update the entity
  1126.      * @param string  $fieldName        The name of the property to validate
  1127.      * @param string  $entityClassName  The class name for entity we are validating
  1128.      *
  1129.      * @return  Object  The passed/retrieved entity
  1130.      */
  1131.     public function validateEntity(&$errors$values$fieldName$entityClassName$allowNull false)
  1132.     {
  1133.         if (array_key_exists($fieldName$values)) {
  1134.             if ($allowNull === true) {
  1135.                 if ($values[$fieldName] === null) {
  1136.                     // Value is null and is allowed to be null. No further validation is required
  1137.                     return null;
  1138.                 }
  1139.             } else {
  1140.                 // Validate not-null if not allowed to be null
  1141.                 $this->validationService->validateNotNull(
  1142.                     $errors$fieldName$values[$fieldName]
  1143.                 );
  1144.                 if (
  1145.                     isset($errors[$fieldName]) &&
  1146.                     count($errors[$fieldName]) > 0
  1147.                 ) {
  1148.                     return;
  1149.                 }
  1150.             }
  1151.             // Validate miss-config. Array not expected here
  1152.             if (is_array($values[$fieldName])) {
  1153.                 throw new Exception('Dev miss config: did you forget to set validation for "' $fieldName '" to "entityOrEntities" instead? Or maybe you are not replacing an entity by its id in the UI before sending the request.');
  1154.             }
  1155.             $entities $this->validateEntities($errors,
  1156.                 array(
  1157.                     $fieldName => array(
  1158.                         $values[$fieldName]
  1159.                     )
  1160.                 ), $fieldName$entityClassName);
  1161.             if (count($entities) > 0) {
  1162.                 return reset($entities);
  1163.             }
  1164.         }
  1165.     }
  1166.     /**
  1167.      * Same as above but for multiple entities inside an array
  1168.      *
  1169.      * @see Uplifted\BaseBundle\Service\BaseEntityManager::validateEntity()
  1170.      */
  1171.     public function validateEntities(&$errors$values$fieldName$entityClassName)
  1172.     {
  1173.         $return = array();
  1174.         if (isset($values[$fieldName])) {
  1175.             foreach ($values[$fieldName] as $entityOrEntityId) {
  1176.                 $return[] = $this->validationService->validateEntity(
  1177.                     $errors$fieldName$entityOrEntityId,
  1178.                     function ($id$params) {
  1179.                         if (is_numeric($id) && $id 0) {
  1180.                             return $this->doctrineEntityManager->getRepository($params['entityClassName'])->find($id);
  1181.                         } else {
  1182.                             return null;
  1183.                         }
  1184.                     },
  1185.                     $this->translatorService->trans('app_bundle.entity.name.' $fieldName),
  1186.                     $entityClassName,
  1187.                     array(
  1188.                         'entityClassName' => $entityClassName
  1189.                     )
  1190.                 );
  1191.             }
  1192.         }
  1193.         return $return;
  1194.     }
  1195.     /**
  1196.      * Shortcut method to validate a single entity or multiple ones without knowing in
  1197.      * advance what your value will be
  1198.      *
  1199.      * @return  array   An array with the validated entity/entities
  1200.      * @see self::validateEntity()
  1201.      *
  1202.      */
  1203.     public function validateEntityOrEntities(&$errors$values$fieldName$entityClassName)
  1204.     {
  1205.         if (isset($values[$fieldName])) {
  1206.             if (is_array($values[$fieldName])) {
  1207.                 return $this->validateEntities($errors$values$fieldName$entityClassName);
  1208.             } else {
  1209.                 return array(
  1210.                     $this->validateEntity($errors$values$fieldName$entityClassName)
  1211.                 );
  1212.             }
  1213.         }
  1214.         return array();
  1215.     }
  1216.     /**
  1217.      * Shortcut function to set multiple fields using the setFieldValue function, via a
  1218.      * configuration array. Currently supports the string, boolean and date types of
  1219.      * values.
  1220.      *
  1221.      * @param Object  $entity  The entity being modified
  1222.      * @param array   $values  The values to use for the 'set' operation, using
  1223.      *                         the $fieldName as key
  1224.      * @param array   $config  An array with fieldNames as keys and fieldTypes as values
  1225.      */
  1226.     protected function simpleSetValues(&$entity$values, array $config)
  1227.     {
  1228.         foreach ($config as $fieldName => $fieldType) {
  1229.             if (array_key_exists($fieldName$values)) {
  1230.                 switch ($fieldType) {
  1231.                     case 'string':
  1232.                     case 'unformatted':
  1233.                         $this->setFieldValue($entity$fieldName$values);
  1234.                         break;
  1235.                     case 'bool':
  1236.                     case 'boolean':
  1237.                         $this->setFieldValue($entity$fieldName,
  1238.                             $this->mixedToBool($values[$fieldName]));
  1239.                         break;
  1240.                     case 'dateTime':
  1241.                     case 'date':
  1242.                     case 'time':
  1243.                         $this->setFieldValue($entity$fieldName,
  1244.                             $this->mixedToDateTime($values[$fieldName]));
  1245.                         break;
  1246.                 }
  1247.             }
  1248.         }
  1249.     }
  1250.     /**
  1251.      * Shortcut function to set multiple string-type fields using the setFieldValue function
  1252.      *
  1253.      * @param Object  $entity      The entity being modified
  1254.      * @param array   $fieldNames  An array with the property names to set
  1255.      * @param array   $values      The values to use for the 'set' operation, using
  1256.      *                             the $fieldName as key
  1257.      */
  1258.     protected function setFieldsValue($entity$fieldNames$values)
  1259.     {
  1260.         foreach ($fieldNames as $fieldName) {
  1261.             $this->setFieldValue($entity$fieldName$values);
  1262.         }
  1263.     }
  1264.     /**
  1265.      * Shortcut function used to set a property value, used when the property setter name is
  1266.      * exactly the property name ($fieldName) prepended with 'set'
  1267.      *
  1268.      * @param Object  $entity              The entity being modified
  1269.      * @param string  $fieldName           The name of the property to set
  1270.      * @param mixed   $valueOrValuesArray  Either the values to use for the 'set' operation, using
  1271.      *                                     the $fieldName as key, or the actual value of the property to be set
  1272.      */
  1273.     protected function setFieldValue($entity$fieldName$valueOrValuesArray)
  1274.     {
  1275.         $valueIsSet false;
  1276.         if (is_array($valueOrValuesArray)) {
  1277.             if (array_key_exists($fieldName$valueOrValuesArray)) {
  1278.                 $valueIsSet true;
  1279.                 $value $valueOrValuesArray[$fieldName];
  1280.             }
  1281.         } else {
  1282.             $valueIsSet true;
  1283.             $value $valueOrValuesArray;
  1284.         }
  1285.         if ($valueIsSet === true) {
  1286.             $setterName $this->getFieldSetterName($fieldName);
  1287.             $entity->$setterName($value);
  1288.         }
  1289.     }
  1290.     /**
  1291.      * Returns the camel-case getter name for the given fieldName
  1292.      *
  1293.      * @param string  $fieldName  The field to get the getter name for
  1294.      *
  1295.      * @return string   The getter function name
  1296.      */
  1297.     protected function getFieldGetterName(string $fieldName): string
  1298.     {
  1299.         return 'get' ucfirst($fieldName);
  1300.     }
  1301.     /**
  1302.      * Returns the camel-case setter name for the given fieldName
  1303.      *
  1304.      * @param string  $fieldName  The field to get the setter name for
  1305.      *
  1306.      * @return string   The setter function name
  1307.      */
  1308.     protected function getFieldSetterName(string $fieldName): string
  1309.     {
  1310.         return 'set' ucfirst($fieldName);
  1311.     }
  1312.     protected function getCompoundFieldValue($entitystring $compoundFieldName)
  1313.     {
  1314.         $fieldNames explode('.'$compoundFieldName);
  1315.         $value $entity;
  1316.         foreach ($fieldNames as $fieldName) {
  1317.             $getterName $this->getFieldGetterName($fieldName);
  1318.             if ($value !== null) {
  1319.                 $value $value->$getterName();
  1320.             }
  1321.         }
  1322.         return $value;
  1323.     }
  1324.     /**
  1325.      * Shortcut function that combines setFieldValue and getEntityReferenceOrObject
  1326.      * used to conveniently set an entity into a relationship field
  1327.      *
  1328.      * @param Object  $entity                   The entity being modified
  1329.      * @param string  $fieldName                The name of the property to set
  1330.      * @param array   $values                   The values to use for the 'set' operation, using
  1331.      *                                          the $fieldName as key
  1332.      * @param string  $className                The entity's class name as returned from Entity::class
  1333.      */
  1334.     protected function setEntityReferenceFieldValue($entitystring $fieldName, array $valuesstring $className)
  1335.     {
  1336.         if (array_key_exists($fieldName$values)) {
  1337.             $this->setFieldValue($entity$fieldName,
  1338.                 $this->getEntityReferenceOrObject($values[$fieldName], $className));
  1339.         }
  1340.     }
  1341.     /**
  1342.      * Shortcut to add or remove a standard-named add/remove methods property
  1343.      *
  1344.      * @param 'add'|'remove'  $addRemove  Flag to add or remove related entities (passed in the $values array too)
  1345.      * @param object          $entity     The entity you are adding or removing from
  1346.      * @param string          $fieldName  The name of the relationship to set
  1347.      * @param array           $values     The values to use for the 'set' operation, using the $fieldName as key. The
  1348.      *                                    value referenced by the key can be a single entity/entityReference or an
  1349.      *                                    array of entities/entityReferences
  1350.      * @param string          $className  The entity's class name as returned from Entity::class
  1351.      */
  1352.     protected function addRemoveRelatedEntity(string $addRemove$entitystring $fieldName, array $valuesstring $className)
  1353.     {
  1354.         if (isset($values[$fieldName])) {
  1355.             $methodName $addRemove ucfirst($fieldName);
  1356.             if (!is_array($values[$fieldName])) {
  1357.                 $valuesArray = array($values[$fieldName]);
  1358.             } else {
  1359.                 $valuesArray $values[$fieldName];
  1360.             }
  1361.             foreach ($valuesArray as $value) {
  1362.                 $entity->$methodName($this->getEntityReferenceOrObject($value$className));
  1363.             }
  1364.         }
  1365.     }
  1366.     /**
  1367.      * Function to completely setup an associated set of entities given a complete list of
  1368.      * them. This will remove and add the needed ones to exactly match the list.
  1369.      *
  1370.      * This function should be used when the relationship is a simple 1-1, 1-m, m-1 or m-m
  1371.      * relationship without using a relationship class (so no creation and deletion of
  1372.      * objects is involved)
  1373.      *
  1374.      * @param object                                          $entity                   The entity being created or
  1375.      *                                                                                  updated
  1376.      * @param string                                          $fieldName                The name of the relationship to
  1377.      *                                                                                  setup
  1378.      * @param array                                           $values                   The values to use for the 'set'
  1379.      *                                                                                  operation, using the $fieldName
  1380.      *                                                                                  as key
  1381.      * @param \Uplifted\BaseBundle\Service\BaseEntityManager  $associatedEntityManager  The entityManager for the
  1382.      *                                                                                  associated entity
  1383.      * @param function                                        $itemReferenceGetter      Function to get a reference to
  1384.      *                                                                                  the associated entity
  1385.      * @param array                                           $lifeCycleFunctionNames   A map of method names to use on
  1386.      *                                                                                  the main entity to get the list
  1387.      *                                                                                  of, add and remove associated
  1388.      *                                                                                  entities
  1389.      */
  1390.     protected function normalizeAssociatedSimpleEntity($entitystring $fieldName, array $valuesBaseEntityManager $associatedEntityManager$itemReferenceGetter null, array $lifeCycleFunctionNames = array(), $itemFinderFromManager null)
  1391.     {
  1392.         if (isset($values[$fieldName])) {
  1393.             // Set default lifeCycle names if not set
  1394.             if (!isset($lifeCycleFunctionNames['getter'])) {
  1395.                 $lifeCycleFunctionNames['getter'] = 'get' ucfirst($fieldName);
  1396.             }
  1397.             if (!isset($lifeCycleFunctionNames['remover'])) {
  1398.                 $lifeCycleFunctionNames['remover'] = 'remove' ucfirst(rtrim($fieldName's'));
  1399.             }
  1400.             if (!isset($lifeCycleFunctionNames['adder'])) {
  1401.                 $lifeCycleFunctionNames['adder'] = 'add' ucfirst(rtrim($fieldName's'));
  1402.             }
  1403.             if ($itemReferenceGetter === null) {
  1404.                 $itemReferenceGetter = function ($relatedEntity) {
  1405.                     return $relatedEntity->getId();
  1406.                 };
  1407.             }
  1408.             $newItemsReference $values[$fieldName];
  1409.             $currentItemsReference = array();
  1410.             $currentItemsIndexedByReference = array();
  1411.             // Index current items
  1412.             foreach ($entity->{$lifeCycleFunctionNames['getter']}() as $item) {
  1413.                 $itemReference $itemReferenceGetter($item);
  1414.                 if (!in_array($itemReference$currentItemsReference)) {
  1415.                     $currentItemsReference[] = $itemReference;
  1416.                 }
  1417.                 $currentItemsIndexedByReference[$itemReference] = $item;
  1418.             }
  1419.             // Spot itemsReference to add and to remove
  1420.             $itemsReferenceToRemove array_diff($currentItemsReference,
  1421.                 $newItemsReference);
  1422.             $itemsReferenceToAdd array_diff($newItemsReference$currentItemsReference);
  1423.             // Remove not needed anymore (using the reference created above)
  1424.             foreach ($itemsReferenceToRemove as $itemReferenceToRemove) {
  1425.                 // Remove from entity
  1426.                 $entity->{$lifeCycleFunctionNames['remover']}(
  1427.                     $currentItemsIndexedByReference[$itemReferenceToRemove]
  1428.                 );
  1429.             }
  1430.             // Add the new ones
  1431.             if ($itemFinderFromManager === null) {
  1432.                 $itemFinderFromManager = function ($entityManager$itemReference) {
  1433.                     return $entityManager->find($itemReference);
  1434.                 };
  1435.             }
  1436.             foreach ($itemsReferenceToAdd as $itemReferenceToAdd) {
  1437.                 $newItem $itemFinderFromManager($associatedEntityManager$itemReferenceToAdd);
  1438.                 if ($newItem !== null) {
  1439.                     $entity->{$lifeCycleFunctionNames['adder']}($newItem);
  1440.                 }
  1441.             }
  1442.         }
  1443.     }
  1444.     protected function normalizeAssociatedModelEntity($entitystring $fieldName, array $values$associatedEntityManager$itemReferenceGetter null, array $lifeCycleFunctionNames = array(), $itemFinderFromManager null)
  1445.     {
  1446.         if (isset($values[$fieldName])) {
  1447.             // Set default lifeCycle names if not set
  1448.             if (!isset($lifeCycleFunctionNames['getter'])) {
  1449.                 $lifeCycleFunctionNames['getter'] = 'get' ucfirst($fieldName);
  1450.             }
  1451.             if (!isset($lifeCycleFunctionNames['remover'])) {
  1452.                 $lifeCycleFunctionNames['remover'] = 'remove' ucfirst(rtrim($fieldName,
  1453.                         's'));
  1454.             }
  1455.             if (!isset($lifeCycleFunctionNames['adder'])) {
  1456.                 $lifeCycleFunctionNames['adder'] = 'add' ucfirst(rtrim($fieldName's'));
  1457.             }
  1458.             if ($itemReferenceGetter === null) {
  1459.                 $itemReferenceGetter = function ($relatedEntity) {
  1460.                     return $relatedEntity->getId();
  1461.                 };
  1462.             }
  1463.             $newItemsReference $values[$fieldName];
  1464.             $currentItemsReference = array();
  1465.             $currentItemsIndexedByReference = array();
  1466.             // Index current items
  1467.             foreach ($entity->{$lifeCycleFunctionNames['getter']}() as $item) {
  1468.                 $itemReference $itemReferenceGetter($item);
  1469.                 if (!in_array($itemReference$currentItemsReference)) {
  1470.                     $currentItemsReference[] = $itemReference;
  1471.                 }
  1472.                 $currentItemsIndexedByReference[$itemReference] = $item;
  1473.             }
  1474.             // Spot itemsReference to add and to remove
  1475.             $itemsReferenceToRemove array_diff($currentItemsReference,
  1476.                 $newItemsReference);
  1477.             $itemsReferenceToAdd array_diff($newItemsReference$currentItemsReference);
  1478.             // Remove not needed anymore (using the reference created above)
  1479.             foreach ($itemsReferenceToRemove as $itemReferenceToRemove) {
  1480.                 // Remove from entity
  1481.                 $entity->{$lifeCycleFunctionNames['remover']}(
  1482.                     $currentItemsIndexedByReference[$itemReferenceToRemove]
  1483.                 );
  1484.             }
  1485.             // Add the new ones
  1486.             if ($itemFinderFromManager === null) {
  1487.                 $itemFinderFromManager = function ($entityManager$itemReference) {
  1488.                     return $entityManager->find($itemReference);
  1489.                 };
  1490.             }
  1491.             foreach ($itemsReferenceToAdd as $itemReferenceToAdd) {
  1492.                 $newItem $itemFinderFromManager($associatedEntityManager$itemReferenceToAdd);
  1493.                 $entity->{$lifeCycleFunctionNames['adder']}($newItem);
  1494.             }
  1495.         }
  1496.     }
  1497.     /**
  1498.      * Function to completely setup a related set of entities given a complete list of
  1499.      * them. This will remove and add the needed ones to exactly match the list.
  1500.      *
  1501.      * This function should be used when the relationship is through a relationship class
  1502.      *
  1503.      * @param array  $normalizationValues  Required keys are
  1504.      *                                     :entity - The entity being created or updated
  1505.      *                                     :fieldName - The name of the relationship to setup
  1506.      *                                     :values - The values array containing the related entities to be
  1507.      *                                     created/updated
  1508.      *                                     :relatedEntityManager - The entityManager for the related entity
  1509.      *                                     :relatedEntityClassName - The complete class name of the related entity
  1510.      *                                     :newRelatedEntitySafeCreateArrayArgumentGetter - Function which returns the
  1511.      *                                     array used to create a new related entity
  1512.      *
  1513.      * @return  Array   An array with the created and updated related entities
  1514.      * @throws Exception
  1515.      */
  1516.     protected function normalizeRelatedEntity(array $normalizationValues = array())
  1517.     {
  1518.         $processedEntities = array(
  1519.             'created' => array(),
  1520.             'updated' => array(),
  1521.         );
  1522.         // Check existance of required parameters
  1523.         if (count(array_diff(array(
  1524.                 'entity',
  1525.                 'fieldName',
  1526.                 'values',
  1527.                 'relatedEntityManager',
  1528.                 'relatedEntityClassName',
  1529.                 'newRelatedEntitySafeCreateArrayArgumentGetter',
  1530.             ), array_keys($normalizationValues))) > 0) {
  1531.             throw new Exception('Dev config: missing compulsory parameters');
  1532.         }
  1533.         // Extract all parameter values
  1534.         extract($normalizationValues);
  1535.         if (isset($values[$fieldName])) {
  1536.             // Set default lifeCycle names if not set
  1537.             if (!isset($lifeCycleFunctionNames)) {
  1538.                 $lifeCycleFunctionNames = array();
  1539.             }
  1540.             if (!isset($lifeCycleFunctionNames['getter'])) {
  1541.                 $lifeCycleFunctionNames['getter'] = 'get' ucfirst($fieldName);
  1542.             }
  1543.             if (!isset($lifeCycleFunctionNames['remover'])) {
  1544.                 $lifeCycleFunctionNames['remover'] = 'remove' ucfirst(rtrim($fieldName,
  1545.                         's'));
  1546.             }
  1547.             if (!isset($lifeCycleFunctionNames['adder'])) {
  1548.                 $lifeCycleFunctionNames['adder'] = 'add' ucfirst(rtrim($fieldName's'));
  1549.             }
  1550.             // Spot related entities to be kept
  1551.             $existingRelatedEntityIdsToBeKept = array();
  1552.             foreach ($values[$fieldName] as $key => $relatedEntityData) {
  1553.                 if (isset($relatedEntityData['id'])) {
  1554.                     $existingRelatedEntityIdsToBeKept[] = $relatedEntityData['id'];
  1555.                 }
  1556.             }
  1557.             // Remove related entities to be removed
  1558.             foreach ($entity->{$lifeCycleFunctionNames['getter']}() as $existingRelatedEntityToBeRemoved) {
  1559.                 if (!in_array($existingRelatedEntityToBeRemoved->getId(),
  1560.                     $existingRelatedEntityIdsToBeKept)) {
  1561.                     $entity->{$lifeCycleFunctionNames['remover']}($existingRelatedEntityToBeRemoved);
  1562.                     $relatedEntityManager->delete($existingRelatedEntityToBeRemoved);
  1563.                 }
  1564.             }
  1565.             // Update existing related entities and create new ones
  1566.             foreach ($values[$fieldName] as $key => $relatedEntityData) {
  1567.                 if (isset($relatedEntityData['id'])) {
  1568.                     $processedEntities['updated'][] = $relatedEntityManager
  1569.                         ->safeUpdate($this->getEntityReference($relatedEntityClassName,
  1570.                             $relatedEntityData['id']), $relatedEntityDatafalse)
  1571.                     ;
  1572.                 } else {
  1573.                     $newEntity $relatedEntityManager->safeCreate(
  1574.                         $newRelatedEntitySafeCreateArrayArgumentGetter($entity,
  1575.                             $relatedEntityData), false
  1576.                     );
  1577.                     $processedEntities['created'][] = $newEntity;
  1578.                     $entity->{$lifeCycleFunctionNames['adder']}($newEntity);
  1579.                 }
  1580.             }
  1581.         }
  1582.         return $processedEntities;
  1583.     }
  1584.     /**
  1585.      * Shortcut function to get either an entity reference or the entity object itself
  1586.      *
  1587.      * @param int|object|null  $value       Either the entity id, the entity itself or null
  1588.      * @param string|null      $className   The entity's class name as returned from Entity::class
  1589.      *                                      If null then the entity repository is used and
  1590.      *                                      the fully loaded entity
  1591.      *
  1592.      * @return object                       The entity itself, a reference to it or null
  1593.      */
  1594.     public function getEntityReferenceOrObject($value$className null)
  1595.     {
  1596.         if (is_numeric($value)) {
  1597.             if ($className !== null) {
  1598.                 return $this->doctrineEntityManager->getReference($className$value);
  1599.             } else {
  1600.                 return $this->getEntityRepo()->find($value);
  1601.             }
  1602.         } else {
  1603.             return $value;
  1604.         }
  1605.     }
  1606.     /**
  1607.      * Shortcut function to get a \DateTime object from either a string or a \DateTime object
  1608.      *
  1609.      * @param \DateTime|string|null  $mixedDateTime  The value you want to get returned as \DateTime.
  1610.      *                                               Defaults to current date and time if null
  1611.      *
  1612.      * @return \DateTime
  1613.      */
  1614.     protected function mixedToDateTime($mixedDateTime null)
  1615.     {
  1616.         if ($mixedDateTime === null) {
  1617.             return new \DateTime();
  1618.         }
  1619.         if ($mixedDateTime instanceof \DateTime) {
  1620.             return $mixedDateTime;
  1621.         }
  1622.         if ($this->validationService->isValidDate($mixedDateTime)) {
  1623.             return new \DateTime($mixedDateTime);
  1624.         }
  1625.         return null;
  1626.     }
  1627.     /**
  1628.      * Shortcut function to get a boolean from either a string or a boolean
  1629.      * If the input value is invalid it will return null
  1630.      *
  1631.      * @param bool|string|(int)0|(int)1     $mixedBool    The value you want to get returned as boolean
  1632.      *
  1633.      * @return boolean|null
  1634.      */
  1635.     protected function mixedToBool($mixedBool)
  1636.     {
  1637.         if ($mixedBool instanceof bool) {
  1638.             return $mixedBool;
  1639.         }
  1640.         if ($mixedBool === || $mixedBool == '1' || $mixedBool == 'true' || $mixedBool == 'TRUE') {
  1641.             return true;
  1642.         }
  1643.         if ($mixedBool === || $mixedBool == '0' || $mixedBool == 'false' || $mixedBool == 'FALSE') {
  1644.             return false;
  1645.         }
  1646.     }
  1647.     /**
  1648.      * Shortcut method to begin a transaction in doctrine's entity manager
  1649.      *
  1650.      * @return bool     true if the transaction was actually begun now, false if not
  1651.      */
  1652.     protected function beginTransaction()
  1653.     {
  1654.         if (!$this->doctrineEntityManager->getConnection()->isTransactionActive()) {
  1655.             $this->doctrineEntityManager->beginTransaction();
  1656.             return true;
  1657.         }
  1658.         return false;
  1659.     }
  1660.     /**
  1661.      * Method to request the beginning of a transaction from an external class.
  1662.      * Further actions/checks can be performed here or even denying the request.
  1663.      *
  1664.      * @return bool     true if the transaction was actually begun now, false if not
  1665.      */
  1666.     public function requestTransactionBegin()
  1667.     {
  1668.         return $this->beginTransaction();
  1669.     }
  1670.     /**
  1671.      * Shortcut method to commit a transaction in doctrine's entity manager, if there is
  1672.      * and active one
  1673.      */
  1674.     protected function commitTransaction()
  1675.     {
  1676.         if ($this->doctrineEntityManager->getConnection()->isTransactionActive()) {
  1677.             $this->doctrineEntityManager->commit();
  1678.         }
  1679.     }
  1680.     /**
  1681.      * Method to request the end of a transaction from an external class.
  1682.      * Further actions/checks can be performed here or even denying the request.
  1683.      */
  1684.     public function requestTransactionCommit()
  1685.     {
  1686.         return $this->commitTransaction();
  1687.     }
  1688.     /**
  1689.      * Shortcut method to rollback a transaction if there is an active one, and re-throw
  1690.      * an exception if passed one
  1691.      *
  1692.      * @param Exception  $ex
  1693.      *
  1694.      * @throws Exception
  1695.      */
  1696.     protected function rollbackTransactionAndReThrowException(Exception $ex null)
  1697.     {
  1698.         // If we had an exception, and still haven't committed the transaction, rollback the whole transaction
  1699.         if ($this->doctrineEntityManager->getConnection()->isTransactionActive()) {
  1700.             $this->doctrineEntityManager->rollback();
  1701.         }
  1702.         // And rethrow the exception for external catching, if any
  1703.         if ($ex !== null) {
  1704.             throw $ex;
  1705.         }
  1706.     }
  1707.     /**
  1708.      * Method to request the rollback of a transaction and re-throw of an exception from
  1709.      * an external class.
  1710.      * Further actions/checks can be performed here or even denying the request.
  1711.      */
  1712.     public function requestRollbackTransactionAndReThrowException(Exception $ex null)
  1713.     {
  1714.         $this->rollbackTransactionAndReThrowException($ex);
  1715.     }
  1716.     /**
  1717.      * Shortcut to get the logged user
  1718.      *
  1719.      * @return User|null    The logged user or null if not logged in
  1720.      */
  1721.     protected function getLoggedUser()
  1722.     {
  1723.         $loggedUser $this->securityTokenStorage->getToken()?->getUser();
  1724.         if (is_string($loggedUser) || $loggedUser === null) {
  1725.             return null;
  1726.         } else {
  1727.             return $loggedUser;
  1728.         }
  1729.     }
  1730.     /**
  1731.      * Shortcut to get the logged user, also checking the logged user exists and
  1732.      * throwing an access denied exception if none found
  1733.      *
  1734.      * @return User    The logged user
  1735.      * @throws AccessDeniedException
  1736.      *
  1737.      */
  1738.     protected function getRequiredLoggedUser()
  1739.     {
  1740.         $loggedUser $this->getLoggedUser();
  1741.         if ($loggedUser == null) {
  1742.             throw new AccessDeniedException();
  1743.         }
  1744.         return $loggedUser;
  1745.     }
  1746.     /**
  1747.      * Function to determine if the "userless cli command" flag has been raised. This can
  1748.      * be used to avoid requesting and using the loggedUser given there will be none. It
  1749.      * might not even make sense to do this step in the calling function if running from
  1750.      * the command line.
  1751.      *
  1752.      * @return bool     True if we are running a userless cli command which raised the
  1753.      * executing_userless_cli_command custom flag. False otherwise
  1754.      */
  1755.     public function runningUserlessCliCommandOrAnonymousRequest()
  1756.     {
  1757.         return isset($_SERVER['executing_userless_cli_command_or_anonymous_request']) &&
  1758.             $_SERVER['executing_userless_cli_command_or_anonymous_request'] === true;
  1759.     }
  1760.     /**
  1761.      * Function to get an attribute value from the entity or from the $values array if it
  1762.      * has been overridden
  1763.      *
  1764.      * @param object                         $entity            The entity to default to
  1765.      * @param string                         $fieldName         The name of the attribute/key in $values array
  1766.      * @param array                          $values            Usually the key/value pair used to create/update
  1767.      *                                                          an entity
  1768.      * @param 'bool'|'date'|'time'|'entity'  $type              A type hint to allow formatting the
  1769.      *                                                          value if retrieved from the $values
  1770.      *                                                          array
  1771.      * @param string                         $className         Used when type == 'entity'. The class name of
  1772.      *                                                          the entity you want to build from the $values
  1773.      *                                                          value
  1774.      * @param array                          $getterParams      Any parameters needed when invoking the getter
  1775.      *                                                          function on the entity
  1776.      * @param string                         $customGetterName  The name of the getter function, if not the
  1777.      *                                                          standard one
  1778.      *
  1779.      * @return mixed    The value retrieved from the $values array, formatted as needed or
  1780.      * the attribute value from the entity provided, if not defined in $values array
  1781.      */
  1782.     protected function getAttributeOrValue($entity$fieldName$values$type null$className null$getterParams = array(), $customGetterName null)
  1783.     {
  1784.         // First try to get the value from the values array (which overrides the one in
  1785.         //  the entity)
  1786.         if (array_key_exists($fieldName$values)) {
  1787.             switch ($type) {
  1788.                 case 'bool':
  1789.                     return $this->mixedToBool($values[$fieldName]);
  1790.                 case 'int':
  1791.                     return intval($values[$fieldName]);
  1792.                 case 'dateTime':
  1793.                 case 'date':
  1794.                 case 'time':
  1795.                     if ($this->validationService->isValidDate($values[$fieldName])) {
  1796.                         return $this->mixedToDateTime($values[$fieldName]);
  1797.                     }
  1798.                 case 'entity':
  1799.                     return $this->getEntityReferenceOrObject(
  1800.                         $values[$fieldName], $className
  1801.                     );
  1802.                 default:
  1803.                     return $values[$fieldName];
  1804.             }
  1805.         }
  1806.         // Else get it from the entity
  1807.         if ($entity !== null) {
  1808.             if ($customGetterName !== null) {
  1809.                 $getterName $customGetterName;
  1810.             } else {
  1811.                 $getterName $this->getFieldGetterName($fieldName);
  1812.             }
  1813.             return call_user_func_array(array($entity$getterName), $getterParams);
  1814.         }
  1815.         return null;
  1816.     }
  1817.     /**
  1818.      * Function to populate a $values array from an entity based on a list of fields,
  1819.      * using the common getFieldGetterName function to pick the values
  1820.      *
  1821.      * @param object  $entity      The entity to pick values from
  1822.      * @param array   $fieldNames  The list of fields to attempt to set up
  1823.      * @param array   $values      The values array to fill in
  1824.      *
  1825.      * @return array                The values array with all possible values set
  1826.      */
  1827.     public function extractValuesOfFields($entity, array $fieldNames, array $values null)
  1828.     {
  1829.         if ($values === null) {
  1830.             $values = array();
  1831.         }
  1832.         foreach ($fieldNames as $fieldName) {
  1833.             $getterName $this->getFieldGetterName($fieldName);
  1834.             if ($entity->$getterName() !== null) {
  1835.                 $values[$fieldName] = $entity->$getterName();
  1836.             }
  1837.         }
  1838.         return $values;
  1839.     }
  1840.     /**
  1841.      * @param object  $entity
  1842.      *
  1843.      * @return ClassMetadata|null
  1844.      */
  1845.     public function getEntityMetadata(object $entity): ClassMetadata|null
  1846.     {
  1847.         return $this->getEntityClassMetadata(get_class($entity));
  1848.     }
  1849.     /**
  1850.      * @param string  $entityClass
  1851.      *
  1852.      * @return ClassMetadata|null
  1853.      */
  1854.     public function getEntityClassMetadata(string $entityClass): ClassMetadata|null
  1855.     {
  1856.         return $this->getDoctrineEntityManager()->getClassMetadata($entityClass);
  1857.     }
  1858.     /**
  1859.      * Function to get the field descriptions for a given entity class based on its metadata
  1860.      *
  1861.      * @param string|null  $entityClass
  1862.      * @param array|null   $fieldsNames
  1863.      *
  1864.      * @return array
  1865.      */
  1866.     public function getEntityFieldsDescriptions(string $entityClass null, array $fieldsNames null): array
  1867.     {
  1868.         if (is_array($fieldsNames) && count($fieldsNames) == 0) {
  1869.             return array();
  1870.         }
  1871.         if ($entityClass === null) {
  1872.             $entityClass $this->getManagedEntityClass();
  1873.         }
  1874.         // Retrieve the metadata for the entity class
  1875.         $metadata $this->getEntityClassMetadata($entityClass);
  1876.         if (!$metadata) {
  1877.             return array();
  1878.         }
  1879.         $fieldsDescription = array();
  1880.         foreach ($metadata->fieldMappings as $fieldName => $mapping) {
  1881.             // Skip if certain fields were requested and this one was not
  1882.             if (
  1883.                 $fieldsNames !== null &&
  1884.                 !in_array($fieldName$fieldsNames)
  1885.             ) {
  1886.                 continue;
  1887.             }
  1888.             $fieldsDescription[$fieldName] = array(
  1889.                 'type' => $mapping['type'],
  1890.                 'nullable' => $mapping['nullable'] ?? false,
  1891.                 'columnName' => $mapping['columnName'],
  1892.             );
  1893.         }
  1894.         return $fieldsDescription;
  1895.     }
  1896.     protected function getAiUpdatableFields()
  1897.     {
  1898.         return array();
  1899.     }
  1900.     protected function getAiUpdatableFieldsDescriptions(string $entityClass null)
  1901.     {
  1902.         return $this->getEntityFieldsDescriptions($entityClass$this->getAiUpdatableFields());
  1903.     }
  1904. }