<?php
namespace App\Uplifted\BaseBundle\Service;
use App\AdminBundle\Service\AiService;
use App\Uplifted\BaseBundle\EventListener\AfterSavingEntityCreationListener;
use App\Uplifted\BaseBundle\EventListener\AfterSavingEntityDeletionListener;
use App\Uplifted\BaseBundle\EventListener\AfterSavingEntityListener;
use App\Uplifted\BaseBundle\EventListener\AfterSavingEntityUpdateListener;
use App\Uplifted\BaseBundle\Exception\AccessDeniedException;
use App\Uplifted\BaseBundle\Exception\EntityValidationException;
use App\Uplifted\BaseBundle\Exception\MissingRequiredFieldsException;
use Doctrine\ORM\EntityManager as DoctrineEntityManager;
use Doctrine\ORM\Events;
use Doctrine\ORM\Exception\ORMException;
use Doctrine\ORM\UnitOfWork;
use Doctrine\ORM\Mapping\ClassMetadata;
use Exception;
use Symfony\Contracts\Translation\TranslatorInterface;
abstract class BaseEntityManager extends BaseService
{
const DATETIME_ZERO_TIMESTAMP = 1; // 1970-01-01 0:00:01
const DATETIME_INIFINTY_TIMESTAMP = 4102444800; // 2100-01-01 0:00:00
protected DoctrineEntityManager $doctrineEntityManager;
protected $validationService;
protected $translatorService;
protected $securityTokenStorage;
protected $eventManager;
protected $flushManager;
protected AiService $aiCompletionService;
public function __construct(
DoctrineEntityManager $doctrineEntityManager,
ValidationService $validationService,
TranslatorInterface $translatorService,
$securityTokenStorage
)
{
$this->doctrineEntityManager = $doctrineEntityManager;
$this->validationService = $validationService;
$this->translatorService = $translatorService;
$this->securityTokenStorage = $securityTokenStorage;
}
public function setEventManager(EventManager $eventManager)
{
if ($this->eventManager === null) {
$this->eventManager = $eventManager;
}
}
public function setFlushManager(FlushManager $flushManager)
{
if ($this->flushManager === null) {
$this->flushManager = $flushManager;
}
}
public function setAiCompletionService(AiService $aiCompletionService)
{
$this->aiCompletionService = $aiCompletionService;
}
/**
* Returns an array containing the names of the fields that are required to create an entity
* This is used by the isValidRequest method
*
* @param array $values A key value pair array of the field names and their values.
* Which fields are required could be influenced or determined by some value in the
* $values array.
*
* @return array
*/
abstract public function getCreateRequiredFieldsNames(array $values = array());
/**
* Main attributes validation function. Must be a field by field validation
* Invoke parent function first
*
* @param array $values A key value pair array of the field names and their values
* @param object $entity The entity to validate the values upon (if exists). Defaults to null
*
* @return array An array with all errors found or empty if none
*/
abstract public function validateValues($values, $entity = null);
/**
* Function to set values to an entity
* Invoke parent function first
*
* @param object $entity The entity being set
* @param array $values A key value pair array of the field names and their values
*/
abstract public function setValues(&$entity, $values);
/**
* Function to get the managed entity's class
*
* @return string The fully qualified class identifier for the managed entity
*/
abstract public function getManagedEntityClass();
/**
* Function to get a new entity instance from the entity manager
*
* @return object A new entity instance of the managed type
*/
public function getNewEntityInstance()
{
$fullyQualifiedClassName = $this->getManagedEntityClass();
return new $fullyQualifiedClassName();
}
/**
* Function to get the handling repository for the entity type
*
* @return Doctrine\ORM\EntityRepository The repository
*/
public function getEntityRepo()
{
try {
// Explode the fully qualified entity manager class name
$parts = explode('\\', get_class($this));
// Get the entity name from the last item
$entityName = str_replace('Manager', '', array_pop($parts));
// Remove 'Service' from the array
array_pop($parts);
$bundleName = implode('\\', $parts);
// Form the entity class
$entityClass = $bundleName . '\\Entity\\' . $entityName;
// And build the repository name with that
$repo = $this->getDoctrineEntityManager()->getRepository($entityClass);
} catch (Exception $ex) {
throw new Exception($this->translatorService
->trans('uplifted.base_bundle.entity.get_entity_repo_not_implemented_in_manager',
array('%searchedTerm%' => $bundleName . ':' . $entityName)
) . ' ' . $ex->getMessage()
);
}
return $repo;
}
/**
* Validation of provided fields names per action type
*
* @param array $providedFieldsNames Submitted values fields names
* @param 'create'|'update'|null $actionName The action for which we
* need to validate the required fields names. Defaults to
* 'create'
*
* @return bool
*/
public function areRequiredFieldsNamesMetForAction($providedFieldsNames, $actionName = 'create')
{
$requiredFieldsNames = $this->getRequiredFieldsNamesForAction(
$providedFieldsNames, $actionName
);
// Perform the actual check (intersect keys and count the result set) and return the result
return $this->areRequiredFieldsNamesMet($providedFieldsNames, $requiredFieldsNames);
}
public function getRequiredFieldsNamesForAction($providedFieldsNames, $actionName = 'create')
{
// Get the required fields names, given the action name
switch ($actionName) {
case 'update':
return $this->getUpdateRequiredFieldsNames($providedFieldsNames);
case 'create':
default:
return $this->getCreateRequiredFieldsNames($providedFieldsNames);
}
}
/**
* Verification of provided vs required fields names
*
* @param array $providedFieldsNames Provided fields names
* @param array $requiredFieldsNames Required fields names
*
* @return boolean Whether if the required fields are met or not
*/
public function areRequiredFieldsNamesMet($providedFieldsNames, $requiredFieldsNames)
{
return count(array_intersect_key(array_flip($requiredFieldsNames),
$providedFieldsNames)) === count($requiredFieldsNames);
}
/**
* Returns an array containing the names of the fields that are required to update an entity
* This is used by the isValidRequest method. Defaults to just the 'id'
* Override as needed
*
* @param array $values A key value pair array of the field names and their values.
* Which fields are required could be influenced or determined by some value in the
* $values array.
*
* @return array
*/
public function getUpdateRequiredFieldsNames(array $values = array())
{
return array();
}
/**
* Entity creation based on provided values, conditioned to validity of them
*
* @param array $values Key-value map of properties and their values
* @param boolean $save Whether to trigger a save operation or not. Defaults to true
* @param array $additionalData Array to hold any additional data used for general purposes
* @param bool $persistCreated Whether to persist the newly created entity
*
* @return Object The created entity
*
* @throws MissingRequiredFieldsException, EntityValidationException
*/
public function safeCreate($values, $save = true, array $additionalData = array(), $persistCreated = true)
{
// Set default values
$values = $this->setCreateDefaultValues($values, $additionalData);
// Make sure all field requirements are met
if (!$this->areRequiredFieldsNamesMetForAction($values, 'create')) {
$missingFieldsException = new MissingRequiredFieldsException();
$missingFieldsException->setRequiredFieldNames(
$this->getRequiredFieldsNamesForAction($values, 'create')
);
$missingFieldsException->setProvidedFieldNames(array_keys($values));
throw $missingFieldsException;
}
// Do all the fields values validations
$errors = $this->validateValues($values);
if (!is_array($errors)) {
// Add a check to not allow omitting returning the $errors array
throw new Exception($this->translatorService->trans(
'uplifted.base_bundle.dev_error.omitted_return_value_of_validate_values_method',
array('%calledClass%' => get_called_class())
));
}
if (!empty($errors)) {
$this->appLogger->info('BEM____401 - Entity validation error', array(
'calledClass' => get_called_class(),
'errors' => $errors,
'values' => $values,
));
throw (new EntityValidationException())->setErrors($errors);
}
// Finally build the new entity from the values
return $this->createFromValues($values, $save, $additionalData, $persistCreated);
}
/**
* Entity creation based on provided values
*
* @param array $values Key-value map of properties and their values
* @param boolean $save Whether to trigger a save operation or not. Defaults to true
* @param array $additionalData Array to hold any additional data used for general purposes
* @param bool $persistCreated Whether to persist the newly created entity
*
* @return Object The created entity
*/
protected function createFromValues($values, $save = true, array $additionalData = array(), $persistCreated = true)
{
// Atomize action into a transaction
$transactionBegunNow = $this->beginTransaction();
// Include $transactionBegunNow flag in the additionalData array
$additionalData['transactionBegunNow'] = $transactionBegunNow;
try {
// Flag we will need to flush changes
$this->requireFlush();
// Get a new entity instance
$entity = $this->getNewEntityInstance();
// Persist the new entity in the entity manager, if needed
if ($persistCreated === true) {
$this->getDoctrineEntityManager()->persist($entity);
}
// Perform any actions needed before setting the provided values
$beforeSettingValuesReturnData = $this->beforeSettingValues($entity, $values);
// Set the values
$this->setValues($entity, $values);
// Treat any related entities as 'add' action since on creation we would not be removing any
$this->addRelatedEntities($entity, $values);
// Perform any required actions before saving the new entity
$this->beforeSavingEntityCreation(
$entity, $values, $beforeSettingValuesReturnData, $additionalData
);
// Perform any required actions before saving the entity
$this->beforeSavingEntity(
$entity, $values, $beforeSettingValuesReturnData, 'add', $additionalData
);
// Queue an event listener on the postFlush event for AfterSavingEntityCreationListener
$this->eventManager->addEventListener(
Events::postFlush,
new AfterSavingEntityCreationListener(
$this->eventManager, $this, $entity, $values,
$beforeSettingValuesReturnData, $additionalData
)
);
// Queue an event listener on the postFlush event for AfterSavingEntityListener
$this->eventManager->addEventListener(
Events::postFlush,
new AfterSavingEntityListener(
$this->eventManager, $this, $entity, $values, $additionalData
)
);
if ($save) {
// Save everything
$this->flush();
// Commit the transaction, if corresponds
if ($transactionBegunNow) {
$this->commitTransaction();
}
}
return $entity;
} catch (Exception $ex) {
$this->rollbackTransactionAndReThrowException($ex);
}
}
/**
* Entity update based on provided values, conditioned to validity of them
*
* @param Object $entity The entity to update
* @param array $values Key-value map of properties and their values
* @param boolean $save Whether to trigger a save operation or not. Defaults to true
* @param null $addRemoveRelatedEntities Values: 'add'|'remove'|null. Flag to add or remove related entities
* (passed in the $values array too)
* @param array $additionalData Array to hold any additional data used for general purposes
*
* @return Object The updated entity
* @throws EntityValidationException
* @throws MissingRequiredFieldsException
* @throws Exception
*/
public function safeUpdate($entity, $values, $save = true, $addRemoveRelatedEntities = null, array $additionalData = array())
{
// Set default values
$values = $this->setUpdateDefaultValues($entity, $values);
// Make sure all field requirements are met
if (!$this->areRequiredFieldsNamesMetForAction($values, 'update')) {
$missingFieldsException = new MissingRequiredFieldsException();
$missingFieldsException->setRequiredFieldNames(
$this->getRequiredFieldsNamesForAction($values, 'update')
);
$missingFieldsException->setProvidedFieldNames(array_keys($values));
throw $missingFieldsException;
}
// Do all the fields values validations
$errors = $this->validateValues($values, $entity);
if (!is_array($errors)) {
// Add a check to not allow omitting returning the $errors array
throw new Exception($this->translatorService->trans(
'uplifted.base_bundle.dev_error.omitted_return_value_of_validate_values_method',
array('%calledClass%' => get_called_class())
));
}
if (!empty($errors)) {
$this->appLogger->info('BEM____402 - Entity validation error', array(
'calledClass' => get_called_class(),
'errors' => $errors,
'values' => $values,
));
throw (new EntityValidationException())->setErrors($errors);
}
// Update the entity from the values
return $this->updateFromValues($entity, $values, $save, $addRemoveRelatedEntities, $additionalData);
}
/**
* Entity update based on provided values
*
* @param Object $entity The entity to update
* @param array $values Key-value map of properties and their values
* @param boolean $save Whether to trigger a save operation or not. Defaults to
* true
* @param null $addRemoveRelatedEntities Values: 'add'|'remove'|null. Flag to add or remove
* related entities (passed in the $values array too)
* @param array $additionalData Array to hold any additional data used for general
* purposes
*
* @return Object The created entity
* @throws Exception
*/
protected function updateFromValues($entity, $values, $save = true, $addRemoveRelatedEntities = null, array $additionalData = array())
{
// Atomize action into a transaction
$transactionBegunNow = $this->beginTransaction();
// Include $transactionBegunNow flag in the additionalData array
$additionalData['transactionBegunNow'] = $transactionBegunNow;
try {
// Flag we will need to flush changes
$this->requireFlush();
// If we are dealing with a new (not managed) entity, then persist it
if ($this->getDoctrineEntityManager()->getUnitOfWork()->getEntityState($entity) == UnitOfWork::STATE_NEW) {
try {
$this->getDoctrineEntityManager()->persist($entity);
} catch (ORMException $ex) {
throw new Exception($ex->getMessage());
}
}
// Perform any actions needed before setting the provided values
$beforeSettingValuesReturnData = $this->beforeSettingValues($entity, $values);
// Set the values
$this->setValues($entity, $values);
// Add or remove related entities
switch ($addRemoveRelatedEntities) {
case 'add':
$this->addRelatedEntities($entity, $values);
break;
case 'remove':
$this->removeRelatedEntities($entity, $values);
break;
}
// Perform any required actions before saving the updated entity
$this->beforeSavingEntityUpdate(
$entity, $values, $beforeSettingValuesReturnData,
$addRemoveRelatedEntities, $additionalData
);
// Perform any required actions before saving the entity
$this->beforeSavingEntity(
$entity, $values, $beforeSettingValuesReturnData,
$addRemoveRelatedEntities, $additionalData
);
// Queue an event listener on the postFlush event for AfterSavingEntityUpdateListener
$this->eventManager->addEventListener(
Events::postFlush,
new AfterSavingEntityUpdateListener(
$this->eventManager, $this, $entity, $values,
$beforeSettingValuesReturnData, $addRemoveRelatedEntities,
$additionalData
)
);
// Queue an event listener on the postFlush event for AfterSavingEntityListener
$this->eventManager->addEventListener(
Events::postFlush,
new AfterSavingEntityListener(
$this->eventManager, $this, $entity, $values, $additionalData
)
);
if ($save) {
// Save everything
$this->flush();
// Commit the transaction, if corresponds
if ($transactionBegunNow) {
$this->commitTransaction();
}
}
return $entity;
} catch (Exception $ex) {
$this->rollbackTransactionAndReThrowException($ex);
}
}
/**
* Delete an entity
*
* @param object $entity The entity to be deleted
* @param boolean $save Whether to trigger a save operation or not. Defaults to true
* @param array $requestParams Parameters provided in the delete request
* @param array $additionalData Array to hold any additional data used for general purposes
*
* @throws ORMException
* @throws AccessDeniedException
* @throws Exception
*/
public function delete(object $entity, bool $save = true, array $requestParams = array(), array $additionalData = array()): void
{
// The logged-in user must have access to the entity. The 'find' operation will not find the entity if the
// logged-in user does not have access to it
if (
$entity->getId() !== null &&
$this->find($entity->getId()) === null
) {
throw new AccessDeniedException();
}
// Flag we will need to flush changes
$this->requireFlush();
// Perform any required actions before removing the entity from the entity manager
$this->beforeRemovingEntityFromEntityManager($entity, $requestParams);
// Remove from persistence entity manager's scope
$this->getDoctrineEntityManager()->remove($entity);
// Perform any actions after removing the entity but before saving it
$this->beforeSavingEntityDeletion($entity, $requestParams);
// Add the deleted entity id and class as additionalData
$additionalData['deletedEntityId'] = $entity->getId();
$additionalData['deletedEntityClass'] = get_class($entity);
// Queue an event listener on the postFlush event for AfterDeletingEntityListener
$this->eventManager->addEventListener(
Events::postFlush,
new AfterSavingEntityDeletionListener(
$this->eventManager, $this, $requestParams, $additionalData
)
);
// Conditional save of the operation
if ($save === true) {
$this->flush();
}
}
/**
* Add a related entity into a one-to-many or many-to-many relationship collection
* Shortcut to addRemoveRelatedEntities invoked with 'add' value for $addRemove
* Called from the createFromValues and updateFromValues action (optional in updateFromValues)
*
* @param Object $entity The entity to update the relationship from
* @param array $values Key-value map of properties and their values
*/
protected function addRelatedEntities(&$entity, $values)
{
$this->addRemoveRelatedEntities($entity, $values, 'add');
}
/**
* Remove a related entity from a one-to-many or many-to-many relationship collection
* Shortcut to addRemoveRelatedEntities invoked with 'remove' value for $addRemove
*
* @param Object $entity The entity to update the relationship from
* @param array $values Key-value map of properties and their values
*/
protected function removeRelatedEntities(&$entity, $values)
{
$this->addRemoveRelatedEntities($entity, $values, 'remove');
}
/**
* Add or remove a related entity to/from a one-to-many or many-to-many relationship collection
*
* @param Object $entity The entity to update the relationship from
* @param array $values Key-value map of properties and their values
* @param 'add'|'remove' $addRemove Flag to add or remove related entities (passed in the $values array too)
*/
protected function addRemoveRelatedEntities(&$entity, $values, $addRemove)
{
}
/**
* Shortcut to save and commit all pending unflushed data
*
* @param boolean $andCommit whether to commit or not a possible ongoing transaction
*/
public function save(bool $andCommit = true)
{
// Save
$this->flush();
if ($andCommit) {
$this->commitTransaction();
}
}
/**
* Shortcut to mark flush as required
*/
public function requireFlush()
{
$this->flushManager->markFlushAsRequired(
spl_object_id($this->doctrineEntityManager)
);
}
/**
* Shortcut to refresh an entity with the doctrine entity manager
*
* @param object $entity The persisted object we want to refresh
*/
public function refresh($entity)
{
$this->getDoctrineEntityManager()->refresh($entity);
}
/**
* Shortcut to find on the entity repository
*
* @param int $entityId
*
* @return object|null
* @throws Exception
*/
public function find(int $entityId)
{
// If the entity manager adds compulsory filters, or it defines it must find using enforced filters then run the
// 'find' operation as a 'findBy id' one
if (
count($this->addCompulsoryFilters(array())) > 0 ||
$this->mustFindUsingEnforcedFilters()
) {
$results = $this->findBy(array('id' => $entityId));
if (count($results) > 0) {
return reset($results);
} else {
return null;
}
} else {
return $this->getEntityRepo()->find($entityId);
}
}
/**
* Shortcut to findBy on the entity repository
*
* @see Uplifted\BaseBundle\Repository\BaseEntityRepository::findBy()
*/
public function findBy(array $filters, array|null $orderBy = null, int|null $limit = null, int|null $offset = null, $distinct = null, $count = false, $export = false, array $modifiers = array()): array
{
// If required, then filter by custom enforced criteria
if ($this->mustFindUsingEnforcedFilters()) {
return $this->getEntityRepo()->findByUsingEnforcedFilters(
$this->addCompulsoryFilters($filters), $orderBy, $limit, $offset, $distinct, $count, $export, $modifiers,
$this->getEnforcedFiltersAdditionalData()
);
} else {
return $this->getEntityRepo()->findBy(
$this->addCompulsoryFilters($filters), $orderBy, $limit, $offset, $distinct, $count, $export, $modifiers
);
}
}
/**
* Shortcut to findOneBy on the entity repository
*
* @see Uplifted\BaseBundle\Repository\BaseEntityRepository::findOneBy()
*/
public function findOneBy(array $filters)
{
if ($this->mustFindUsingEnforcedFilters()) {
$results = $this->findBy($filters);
if (count($results) > 0) {
if (count($results) == 1) {
return reset($results);
} else {
throw new \Exception('Trying to find one, found more. Make sure you are providing a unique criteria');
}
}
} else {
return $this->getEntityRepo()->findOneBy($this->addCompulsoryFilters($filters));
}
}
/**
* Shortcut to findAll on the entity repository
*
* @see Uplifted\BaseBundle\Repository\BaseEntityRepository::findAll()
*/
public function findAll()
{
if ($this->mustFindUsingEnforcedFilters()) {
return $this->findBy(array());
} else {
return $this->getEntityRepo()->findAll();
}
}
/**
* Function to make a hybrid search for an entity based on a single attribute and a text search term
*
* @param string $searchValue
* @param string $attribute
* @param array|null $entities
*
* @return mixed
*/
public function hybridFindOneByAttribute(string $searchValue, string $attribute = 'name', ?array $entities = null)
{
// First look for an exact match
$matchingEntities = $this->findBy(array(
$attribute => $searchValue
));
if (!empty($matchingEntities)) {
return $matchingEntities[0];
}
// If failed, then build a list of entity options and fallback to AI search
// Use provided entities list or fetch all if not provided
$entityOptions = array_map(fn($entity) => [
'searchTerm' => $entity->{'get' . ucfirst($attribute)}(),
'entity' => $entity,
], $entities ?? $this->findAll());
return $this->aiCompletionService->matchSearchTermToOption($searchValue, $entityOptions, true);
}
/**
* Shortcut to exists on the entity repository
*
* @param $filters array The filters to use to find the entity to check existence of
*
* @throws Exception
* @see Uplifted\BaseBundle\Repository\BaseEntityRepository::exists()
*/
public function exists(array $filters)
{
if ($this->mustFindUsingEnforcedFilters()) {
$result = $this->findBy($filters, null, null, null, null, true);
return $result['count'] && $result['count'] > 0;
} else {
return $this->getEntityRepo()->exists($this->addCompulsoryFilters($filters));
}
}
/**
* Convenience function to add special filters required on every query to the entity
* repository
*
* @param array $filters Key-value pairs with keys being the fields names to apply 'where'
* conditions and the values are the values to filter on
*
* @return array
*/
public function addCompulsoryFilters(array $filters)
{
return $filters;
}
/* Protected functions */
/* ------------------- */
/**
* Modules that require enforced filters must override this method
*
* @return bool
*/
protected function mustFindUsingEnforcedFilters(): bool
{
return false;
}
protected function getEnforcedFiltersAdditionalData()
{
return array();
}
/**
* Shortcut for Doctrine\ORM\EntityManagerInterface::getReference()
*
* Gets a reference to the entity identified by the given type and identifier
* without actually loading it, if the entity is not yet loaded.
*
* @param string $entityName The name of the entity type.
* @param mixed $id The entity identifier.
*/
protected function getEntityReference($entityName, $id)
{
return $this->getDoctrineEntityManager()->getReference($entityName, $id);
}
/**
* Function to get access to the persistence entity manager
*
* @return DoctrineEntityManager
*/
public function getDoctrineEntityManager()
{
return $this->doctrineEntityManager;
}
public function persist($entity)
{
return $this->getDoctrineEntityManager()->persist($entity);
}
/**
* Performs a loop of doctrine entity manager's flush operations to save everything
* and guarantee the postFlush event queue is completely emptied.
* Runs flush operation between 1 and 3 times.
*
* Finally runs flush() one last time in case changes made during the last series of
* afterSaving...() methods were made and still need saving.
*
* NOTE: this repeated saving is not doing what expected because after the flush operation
* the entityManager clears all the UnitOfWork insertions, deletions, updates, etc, so
* adding to those in the afterSavingEntity... functions is futile, if not expressly
* calling the flush method again. Right now for the AfterSavingEntityCreation one we
* are calling the flush operation explicitly after executing the custom function (in
* the AfterSavingEntityCreationListener)
*/
public function flush()
{
if ($this->flushManager->isFlushInProgress(
spl_object_id($this->doctrineEntityManager)
)) {
return;
}
// Consider the flush process started
$this->flushManager
->markFlushInProgress(spl_object_id($this->doctrineEntityManager))
;
$i = 1;
do {
// Always flush if reaching this point, not explicitly checking if required
$this->doctrineEntityManager->flush();
$this->flushManager
->markFlushAsNotRequired(spl_object_id($this->doctrineEntityManager))
;
$i++;
if ($this->eventManager->hasListeners(Events::postFlush)) {
$this->eventManager->dispatchEvent(Events::postFlush);
}
} while (
$i <= 3 &&
$this->flushManager->isFlushRequired(spl_object_id($this->doctrineEntityManager)) === true
);
// Consider the flush process ended
$this->flushManager
->markFlushAsNotInProgress(spl_object_id($this->doctrineEntityManager))
;
}
/**
* Function to allow the setup of default values on creation time
* Invoke parent function first
*
* @param array $values Key-value map of properties and their values
* @param array $additionalData Array to hold any additional data used for general purposes
*
* @return array The resulting key-value map of properties and their values
*/
protected function setCreateDefaultValues(array $values, array $additionalData = array())
{
return $values;
}
/**
* Function to allow the setup of default values on update time
* Invoke parent function first
*
* @param Object $entity The entity being updated
* @param array $values Key-value map of properties and their values
*
* @return array The resulting key-value map of properties and their values
*/
protected function setUpdateDefaultValues($entity, array $values)
{
return $values;
}
/**
* Method called before setting the entities values (calling the 'setValues' method)
* for both create and update actions
*
* @param Object $entity The entity being created/updated
* @param array $values The values being set
*
* @return array An array with the values before setting them
*/
protected function beforeSettingValues(&$entity, array &$values): array
{
$beforeValues = array();
foreach ($values as $fieldName => $value) {
if (method_exists($entity, $this->getFieldGetterName($fieldName))) {
$beforeValues[$fieldName] = $entity->{$this->getFieldGetterName($fieldName)}();
}
}
return $beforeValues;
}
/**
* Method called before saving the entity creation
*
* @param Object $entity The entity being created
* @param array $values The values used to create the entity
* @param mixed $beforeSettingValuesReturnData Any data returned by function beforeSettingValues()
* @param array $additionalData Array to hold any additional data used for general purposes
*/
protected function beforeSavingEntityCreation(&$entity, $values, $beforeSettingValuesReturnData = null, array $additionalData = null)
{
}
/**
* Method called before saving the entity update action
*
* @param Object $entity The entity being updated
* @param array $values The values used to update the entity
* @param mixed $beforeSettingValuesReturnData Any data returned by function beforeSettingValues()
* @param 'add'|'remove'|null $addRemoveRelatedEntities Flag to add or remove related entities (passed in
* the $values array too)
* @param array $additionalData Array to hold any additional data used for general
* purposes
*/
protected function beforeSavingEntityUpdate(&$entity, $values, $beforeSettingValuesReturnData = null, $addRemoveRelatedEntities = null, array $additionalData = null)
{
}
/**
* Method called before saving the entity for both create and update actions
*
* @param Object $entity The entity being saved
* @param array $values The values used to create/update the entity
* @param mixed $beforeSettingValuesReturnData Any data returned by function beforeSettingValues()
* @param 'add'|'remove'|null $addRemoveRelatedEntities Flag to add or remove related entities (passed in
* the $values array too)
* @param array $additionalData Array to hold any additional data used for general
* purposes
*/
protected function beforeSavingEntity(&$entity, $values, $beforeSettingValuesReturnData = null, $addRemoveRelatedEntities = null, array $additionalData = null)
{
}
/**
* Method called after saving the entity creation
*
* @param Object $entity The entity being created
* @param array $values The values used to create the entity
* @param mixed $beforeSettingValuesReturnData Any data returned by function beforeSettingValues()
* @param array $additionalData Array to hold any additional data used for general purposes
*/
public function afterSavingEntityCreation(&$entity, $values, $beforeSettingValuesReturnData = null, array $additionalData = array())
{
}
/**
* Method called after the entity update action
*
* @param Object $entity The entity being updated
* @param array $values The values used to update the entity
* @param mixed $beforeSettingValuesReturnData Any data returned by function beforeSettingValues()
* @param 'add'|'remove'|null $addRemoveRelatedEntities Flag to add or remove related entities (passed in
* the $values array too)
* @param array $additionalData Array to hold any additional data used for general
* purposes
*/
public function afterSavingEntityUpdate(&$entity, $values, $beforeSettingValuesReturnData = null, $addRemoveRelatedEntities = null, array $additionalData = array())
{
}
/**
* Method called after saving the entity for both create and update actions
*
* @param Object $entity The entity being saved
* @param array $values The values used to create/update the entity
* @param array $additionalData Array to hold any additional data used for general purposes
*/
public function afterSavingEntity(&$entity, $values, array $additionalData = array())
{
}
/**
* Method called before removing an entity being deleted
*
* @param object $entity The entity to be deleted
* @param array $requestParams Parameters provided in the delete request
*/
protected function beforeRemovingEntityFromEntityManager($entity, $requestParams)
{
}
/**
* Method called before saving the deletion of an entity
*
* @param object $entity The entity to be deleted
* @param array $requestParams Parameters provided in the delete request
*/
protected function beforeSavingEntityDeletion($entity, $requestParams)
{
}
/**
* Method called after saving the deletion of an entity
*
* @param array $requestParams Parameters provided in the delete request
* @param array $additionalData Array to hold any additional data used for general purposes
*/
public function afterSavingEntityDeletion(array $requestParams = array(), array $additionalData = array())
{
}
/**
* Shortcut function to perform simple/standard validations of fields via a
* configuration array.
*
* @param array $values The values used to create/update the entity
* @param array $errors The array carrying all validation errors
* @param array $config An array with fieldNames as keys and
* $fieldValidations as values. The fieldValidations is also an array which can either
* be:
* 1) a non-associative array with just a list of validations (if none of the
* validations require extra options),
* or
* 2) an associative array with validation type as keys and an array of options as
* values.
*
* @return array An array with the fieldName as keys and the entities as values for
* the validatedEntites
*/
protected function simpleValidateValues($values, &$errors, array $config)
{
$returnEntities = array();
foreach ($config as $fieldName => $fieldValidations) {
foreach ($fieldValidations as $validationIndexOrType => $validationTypeOrConfig) {
// Setup validationType and validationConfig from the config element and key
if (is_numeric($validationIndexOrType)) {
$validationType = $validationTypeOrConfig;
$validationConfig = null;
} else {
$validationType = $validationIndexOrType;
$validationConfig = $validationTypeOrConfig;
}
// Assess validation type 'required' separately
if ($validationType == 'required') {
if (isset($values[$fieldName])) {
$this->validationService->validateNotNull($errors, $fieldName,
$values[$fieldName]);
$this->validationService->validateNotBlank($errors, $fieldName,
$values[$fieldName]);
} else {
$errors[$fieldName] = $this->translatorService->trans('uplifted.base_bundle.validator.required');
}
} else {
if (isset($values[$fieldName])) {
switch ($validationType) {
case 'length':
if (!isset($validationConfig['min'])) {
$validationConfig['min'] = null;
}
if (!isset($validationConfig['max'])) {
$validationConfig['max'] = null;
}
$this->validationService->validateLength($errors,
$fieldName, $values[$fieldName],
$validationConfig['min'], $validationConfig['max']);
break;
case 'email':
$this->validationService->validateEmail($errors,
$fieldName, $values[$fieldName]);
break;
case 'range':
if (!isset($validationConfig['min'])) {
$validationConfig['min'] = null;
}
if (!isset($validationConfig['max'])) {
$validationConfig['max'] = null;
}
$this->validationService->validateRange($errors,
$fieldName, $values[$fieldName],
$validationConfig['min'], $validationConfig['max']);
break;
case 'number':
case 'float':
$this->validationService->validateNumber($errors,
$fieldName, $values[$fieldName]);
break;
case 'int':
case 'integer':
$this->validationService->validateInt($errors, $fieldName,
$values[$fieldName]);
break;
case 'false':
$this->validationService->validateFalse($errors,
$fieldName, $values[$fieldName]);
break;
case 'true':
$this->validationService->validateTrue($errors,
$fieldName, $values[$fieldName]);
break;
case 'bool':
case 'boolean':
$this->validationService->validateBoolean($errors,
$fieldName, $values[$fieldName]);
break;
case 'date':
case 'dateTime':
if (!isset($validationConfig['min'])) {
$validationConfig['min'] = null;
}
if (!isset($validationConfig['max'])) {
$validationConfig['max'] = null;
}
$this->validationService->validateDate($errors,
$fieldName, $values[$fieldName], false,
$validationConfig['min'], $validationConfig['max']);
break;
case 'time':
$this->validationService->validateDate($errors,
$fieldName, $values[$fieldName], true);
break;
case 'choice':
$this->validationService->validateChoice($errors,
$fieldName, $values[$fieldName],
$validationConfig['options'],
'The value "' . $values[$fieldName] . '" is not a valid choice. Choose one of: ' . implode(', ', $validationConfig['options'])
);
break;
case 'image':
$this->validationService->validateImageUpload(
$errors, $fieldName, $values[$fieldName],
isset($validationConfig['limitDimensions']) ? $validationConfig['limitDimensions'] : null,
isset($validationConfig['allowedMimeTypes']) ? $validationConfig['allowedMimeTypes'] : array(
'image/gif',
'image/jpeg',
'image/png'
)
);
break;
case 'url':
if (trim($values[$fieldName]) != '') {
$this->validationService->validateUrl($errors,
$fieldName, $values[$fieldName],
isset($validationConfig['options']) ? $validationConfig['options'] : array()
);
} else {
$this->validationService->validateNotBlank($errors, $fieldName, $values[$fieldName]);
}
break;
case 'entity':
case 'entityOrNull':
case 'entityOrEntities':
if (
!isset($validationConfig['entityClassName'])
) {
throw new Exception('Dev exception: required options for simpleValidateValues on "entity" type '
. 'are "repositoryName" and "entityClassName" when dealing with "' . $fieldName . '" field');
}
if ($validationType == 'entity') {
$entity = $this->validateEntity(
$errors, $values, $fieldName, $validationConfig['entityClassName']
);
if ($entity !== null) {
$returnEntities[$fieldName] = $entity;
}
} elseif ($validationType == 'entityOrNull') {
// 'null' value validates correctly in this case
if ($values[$fieldName] !== null) {
$entity = $this->validateEntity(
$errors, $values, $fieldName, $validationConfig['entityClassName']
);
if ($entity !== null) {
$returnEntities[$fieldName] = $entity;
}
}
} elseif ($validationType == 'entities') {
$entities = $this->validateEntities(
$errors, $values, $fieldName, $validationConfig['entityClassName']
);
if (count($entities) > 0) {
$returnEntities[$fieldName] = $entities;
}
} elseif ($validationType == 'entityOrEntities') {
$entities = $this->validateEntityOrEntities(
$errors, $values, $fieldName, $validationConfig['entityClassName']
);
if (count($entities) > 0) {
$returnEntities[$fieldName] = $entities;
}
}
break;
}
}
}
}
}
return $returnEntities;
}
/**
* Shortcut function to validate 2 dates are chronological in the order given,
* bearing in mind the 2 dates existed or not in the entity and any or both of the
* dates is/are being updated
*
* @param array $values The values used to create/update the entity
* @param object $entity The entity being created/updated
* @param array $errors The array carrying all validation errors
* @param string $dateFromFieldName The field name for the earliest date
* @param string $dateToFieldName The field name for the latest date
*/
public function validateDatesOrder($values, $entity, $errors, string $dateFromFieldName, string $dateToFieldName)
{
if (
isset($values[$dateFromFieldName]) ||
isset($values[$dateToFieldName])
) {
$this->validationService->validateStartDateTimePriorToEndDateTime(
$errors, $dateFromFieldName,
$values[$dateFromFieldName] ?? null,
$values[$dateToFieldName] ?? null,
$entity?->${$this->getFieldGetterName($dateFromFieldName)}(),
$entity?->${$this->getFieldGetterName($dateToFieldName)}()
);
}
}
/**
* Shortcut function to validate an entity and write to $errors array any errors found
* Combines the validationService->validateEntity() function with the entity retrieval
* callback and the translation for the entity/relation name
*
* @param array $errors The array carrying all validation errors
* @param array $values The values used to create/update the entity
* @param string $fieldName The name of the property to validate
* @param string $entityClassName The class name for entity we are validating
*
* @return Object The passed/retrieved entity
*/
public function validateEntity(&$errors, $values, $fieldName, $entityClassName, $allowNull = false)
{
if (array_key_exists($fieldName, $values)) {
if ($allowNull === true) {
if ($values[$fieldName] === null) {
// Value is null and is allowed to be null. No further validation is required
return null;
}
} else {
// Validate not-null if not allowed to be null
$this->validationService->validateNotNull(
$errors, $fieldName, $values[$fieldName]
);
if (
isset($errors[$fieldName]) &&
count($errors[$fieldName]) > 0
) {
return;
}
}
// Validate miss-config. Array not expected here
if (is_array($values[$fieldName])) {
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.');
}
$entities = $this->validateEntities($errors,
array(
$fieldName => array(
$values[$fieldName]
)
), $fieldName, $entityClassName);
if (count($entities) > 0) {
return reset($entities);
}
}
}
/**
* Same as above but for multiple entities inside an array
*
* @see Uplifted\BaseBundle\Service\BaseEntityManager::validateEntity()
*/
public function validateEntities(&$errors, $values, $fieldName, $entityClassName)
{
$return = array();
if (isset($values[$fieldName])) {
foreach ($values[$fieldName] as $entityOrEntityId) {
$return[] = $this->validationService->validateEntity(
$errors, $fieldName, $entityOrEntityId,
function ($id, $params) {
if (is_numeric($id) && $id > 0) {
return $this->doctrineEntityManager->getRepository($params['entityClassName'])->find($id);
} else {
return null;
}
},
$this->translatorService->trans('app_bundle.entity.name.' . $fieldName),
$entityClassName,
array(
'entityClassName' => $entityClassName
)
);
}
}
return $return;
}
/**
* Shortcut method to validate a single entity or multiple ones without knowing in
* advance what your value will be
*
* @return array An array with the validated entity/entities
* @see self::validateEntity()
*
*/
public function validateEntityOrEntities(&$errors, $values, $fieldName, $entityClassName)
{
if (isset($values[$fieldName])) {
if (is_array($values[$fieldName])) {
return $this->validateEntities($errors, $values, $fieldName, $entityClassName);
} else {
return array(
$this->validateEntity($errors, $values, $fieldName, $entityClassName)
);
}
}
return array();
}
/**
* Shortcut function to set multiple fields using the setFieldValue function, via a
* configuration array. Currently supports the string, boolean and date types of
* values.
*
* @param Object $entity The entity being modified
* @param array $values The values to use for the 'set' operation, using
* the $fieldName as key
* @param array $config An array with fieldNames as keys and fieldTypes as values
*/
protected function simpleSetValues(&$entity, $values, array $config)
{
foreach ($config as $fieldName => $fieldType) {
if (array_key_exists($fieldName, $values)) {
switch ($fieldType) {
case 'string':
case 'unformatted':
$this->setFieldValue($entity, $fieldName, $values);
break;
case 'bool':
case 'boolean':
$this->setFieldValue($entity, $fieldName,
$this->mixedToBool($values[$fieldName]));
break;
case 'dateTime':
case 'date':
case 'time':
$this->setFieldValue($entity, $fieldName,
$this->mixedToDateTime($values[$fieldName]));
break;
}
}
}
}
/**
* Shortcut function to set multiple string-type fields using the setFieldValue function
*
* @param Object $entity The entity being modified
* @param array $fieldNames An array with the property names to set
* @param array $values The values to use for the 'set' operation, using
* the $fieldName as key
*/
protected function setFieldsValue($entity, $fieldNames, $values)
{
foreach ($fieldNames as $fieldName) {
$this->setFieldValue($entity, $fieldName, $values);
}
}
/**
* Shortcut function used to set a property value, used when the property setter name is
* exactly the property name ($fieldName) prepended with 'set'
*
* @param Object $entity The entity being modified
* @param string $fieldName The name of the property to set
* @param mixed $valueOrValuesArray Either the values to use for the 'set' operation, using
* the $fieldName as key, or the actual value of the property to be set
*/
protected function setFieldValue($entity, $fieldName, $valueOrValuesArray)
{
$valueIsSet = false;
if (is_array($valueOrValuesArray)) {
if (array_key_exists($fieldName, $valueOrValuesArray)) {
$valueIsSet = true;
$value = $valueOrValuesArray[$fieldName];
}
} else {
$valueIsSet = true;
$value = $valueOrValuesArray;
}
if ($valueIsSet === true) {
$setterName = $this->getFieldSetterName($fieldName);
$entity->$setterName($value);
}
}
/**
* Returns the camel-case getter name for the given fieldName
*
* @param string $fieldName The field to get the getter name for
*
* @return string The getter function name
*/
protected function getFieldGetterName(string $fieldName): string
{
return 'get' . ucfirst($fieldName);
}
/**
* Returns the camel-case setter name for the given fieldName
*
* @param string $fieldName The field to get the setter name for
*
* @return string The setter function name
*/
protected function getFieldSetterName(string $fieldName): string
{
return 'set' . ucfirst($fieldName);
}
protected function getCompoundFieldValue($entity, string $compoundFieldName)
{
$fieldNames = explode('.', $compoundFieldName);
$value = $entity;
foreach ($fieldNames as $fieldName) {
$getterName = $this->getFieldGetterName($fieldName);
if ($value !== null) {
$value = $value->$getterName();
}
}
return $value;
}
/**
* Shortcut function that combines setFieldValue and getEntityReferenceOrObject
* used to conveniently set an entity into a relationship field
*
* @param Object $entity The entity being modified
* @param string $fieldName The name of the property to set
* @param array $values The values to use for the 'set' operation, using
* the $fieldName as key
* @param string $className The entity's class name as returned from Entity::class
*/
protected function setEntityReferenceFieldValue($entity, string $fieldName, array $values, string $className)
{
if (array_key_exists($fieldName, $values)) {
$this->setFieldValue($entity, $fieldName,
$this->getEntityReferenceOrObject($values[$fieldName], $className));
}
}
/**
* Shortcut to add or remove a standard-named add/remove methods property
*
* @param 'add'|'remove' $addRemove Flag to add or remove related entities (passed in the $values array too)
* @param object $entity The entity you are adding or removing from
* @param string $fieldName The name of the relationship to set
* @param array $values The values to use for the 'set' operation, using the $fieldName as key. The
* value referenced by the key can be a single entity/entityReference or an
* array of entities/entityReferences
* @param string $className The entity's class name as returned from Entity::class
*/
protected function addRemoveRelatedEntity(string $addRemove, $entity, string $fieldName, array $values, string $className)
{
if (isset($values[$fieldName])) {
$methodName = $addRemove . ucfirst($fieldName);
if (!is_array($values[$fieldName])) {
$valuesArray = array($values[$fieldName]);
} else {
$valuesArray = $values[$fieldName];
}
foreach ($valuesArray as $value) {
$entity->$methodName($this->getEntityReferenceOrObject($value, $className));
}
}
}
/**
* Function to completely setup an associated set of entities given a complete list of
* them. This will remove and add the needed ones to exactly match the list.
*
* This function should be used when the relationship is a simple 1-1, 1-m, m-1 or m-m
* relationship without using a relationship class (so no creation and deletion of
* objects is involved)
*
* @param object $entity The entity being created or
* updated
* @param string $fieldName The name of the relationship to
* setup
* @param array $values The values to use for the 'set'
* operation, using the $fieldName
* as key
* @param \Uplifted\BaseBundle\Service\BaseEntityManager $associatedEntityManager The entityManager for the
* associated entity
* @param function $itemReferenceGetter Function to get a reference to
* the associated entity
* @param array $lifeCycleFunctionNames A map of method names to use on
* the main entity to get the list
* of, add and remove associated
* entities
*/
protected function normalizeAssociatedSimpleEntity($entity, string $fieldName, array $values, BaseEntityManager $associatedEntityManager, $itemReferenceGetter = null, array $lifeCycleFunctionNames = array(), $itemFinderFromManager = null)
{
if (isset($values[$fieldName])) {
// Set default lifeCycle names if not set
if (!isset($lifeCycleFunctionNames['getter'])) {
$lifeCycleFunctionNames['getter'] = 'get' . ucfirst($fieldName);
}
if (!isset($lifeCycleFunctionNames['remover'])) {
$lifeCycleFunctionNames['remover'] = 'remove' . ucfirst(rtrim($fieldName, 's'));
}
if (!isset($lifeCycleFunctionNames['adder'])) {
$lifeCycleFunctionNames['adder'] = 'add' . ucfirst(rtrim($fieldName, 's'));
}
if ($itemReferenceGetter === null) {
$itemReferenceGetter = function ($relatedEntity) {
return $relatedEntity->getId();
};
}
$newItemsReference = $values[$fieldName];
$currentItemsReference = array();
$currentItemsIndexedByReference = array();
// Index current items
foreach ($entity->{$lifeCycleFunctionNames['getter']}() as $item) {
$itemReference = $itemReferenceGetter($item);
if (!in_array($itemReference, $currentItemsReference)) {
$currentItemsReference[] = $itemReference;
}
$currentItemsIndexedByReference[$itemReference] = $item;
}
// Spot itemsReference to add and to remove
$itemsReferenceToRemove = array_diff($currentItemsReference,
$newItemsReference);
$itemsReferenceToAdd = array_diff($newItemsReference, $currentItemsReference);
// Remove not needed anymore (using the reference created above)
foreach ($itemsReferenceToRemove as $itemReferenceToRemove) {
// Remove from entity
$entity->{$lifeCycleFunctionNames['remover']}(
$currentItemsIndexedByReference[$itemReferenceToRemove]
);
}
// Add the new ones
if ($itemFinderFromManager === null) {
$itemFinderFromManager = function ($entityManager, $itemReference) {
return $entityManager->find($itemReference);
};
}
foreach ($itemsReferenceToAdd as $itemReferenceToAdd) {
$newItem = $itemFinderFromManager($associatedEntityManager, $itemReferenceToAdd);
if ($newItem !== null) {
$entity->{$lifeCycleFunctionNames['adder']}($newItem);
}
}
}
}
protected function normalizeAssociatedModelEntity($entity, string $fieldName, array $values, $associatedEntityManager, $itemReferenceGetter = null, array $lifeCycleFunctionNames = array(), $itemFinderFromManager = null)
{
if (isset($values[$fieldName])) {
// Set default lifeCycle names if not set
if (!isset($lifeCycleFunctionNames['getter'])) {
$lifeCycleFunctionNames['getter'] = 'get' . ucfirst($fieldName);
}
if (!isset($lifeCycleFunctionNames['remover'])) {
$lifeCycleFunctionNames['remover'] = 'remove' . ucfirst(rtrim($fieldName,
's'));
}
if (!isset($lifeCycleFunctionNames['adder'])) {
$lifeCycleFunctionNames['adder'] = 'add' . ucfirst(rtrim($fieldName, 's'));
}
if ($itemReferenceGetter === null) {
$itemReferenceGetter = function ($relatedEntity) {
return $relatedEntity->getId();
};
}
$newItemsReference = $values[$fieldName];
$currentItemsReference = array();
$currentItemsIndexedByReference = array();
// Index current items
foreach ($entity->{$lifeCycleFunctionNames['getter']}() as $item) {
$itemReference = $itemReferenceGetter($item);
if (!in_array($itemReference, $currentItemsReference)) {
$currentItemsReference[] = $itemReference;
}
$currentItemsIndexedByReference[$itemReference] = $item;
}
// Spot itemsReference to add and to remove
$itemsReferenceToRemove = array_diff($currentItemsReference,
$newItemsReference);
$itemsReferenceToAdd = array_diff($newItemsReference, $currentItemsReference);
// Remove not needed anymore (using the reference created above)
foreach ($itemsReferenceToRemove as $itemReferenceToRemove) {
// Remove from entity
$entity->{$lifeCycleFunctionNames['remover']}(
$currentItemsIndexedByReference[$itemReferenceToRemove]
);
}
// Add the new ones
if ($itemFinderFromManager === null) {
$itemFinderFromManager = function ($entityManager, $itemReference) {
return $entityManager->find($itemReference);
};
}
foreach ($itemsReferenceToAdd as $itemReferenceToAdd) {
$newItem = $itemFinderFromManager($associatedEntityManager, $itemReferenceToAdd);
$entity->{$lifeCycleFunctionNames['adder']}($newItem);
}
}
}
/**
* Function to completely setup a related set of entities given a complete list of
* them. This will remove and add the needed ones to exactly match the list.
*
* This function should be used when the relationship is through a relationship class
*
* @param array $normalizationValues Required keys are
* :entity - The entity being created or updated
* :fieldName - The name of the relationship to setup
* :values - The values array containing the related entities to be
* created/updated
* :relatedEntityManager - The entityManager for the related entity
* :relatedEntityClassName - The complete class name of the related entity
* :newRelatedEntitySafeCreateArrayArgumentGetter - Function which returns the
* array used to create a new related entity
*
* @return Array An array with the created and updated related entities
* @throws Exception
*/
protected function normalizeRelatedEntity(array $normalizationValues = array())
{
$processedEntities = array(
'created' => array(),
'updated' => array(),
);
// Check existance of required parameters
if (count(array_diff(array(
'entity',
'fieldName',
'values',
'relatedEntityManager',
'relatedEntityClassName',
'newRelatedEntitySafeCreateArrayArgumentGetter',
), array_keys($normalizationValues))) > 0) {
throw new Exception('Dev config: missing compulsory parameters');
}
// Extract all parameter values
extract($normalizationValues);
if (isset($values[$fieldName])) {
// Set default lifeCycle names if not set
if (!isset($lifeCycleFunctionNames)) {
$lifeCycleFunctionNames = array();
}
if (!isset($lifeCycleFunctionNames['getter'])) {
$lifeCycleFunctionNames['getter'] = 'get' . ucfirst($fieldName);
}
if (!isset($lifeCycleFunctionNames['remover'])) {
$lifeCycleFunctionNames['remover'] = 'remove' . ucfirst(rtrim($fieldName,
's'));
}
if (!isset($lifeCycleFunctionNames['adder'])) {
$lifeCycleFunctionNames['adder'] = 'add' . ucfirst(rtrim($fieldName, 's'));
}
// Spot related entities to be kept
$existingRelatedEntityIdsToBeKept = array();
foreach ($values[$fieldName] as $key => $relatedEntityData) {
if (isset($relatedEntityData['id'])) {
$existingRelatedEntityIdsToBeKept[] = $relatedEntityData['id'];
}
}
// Remove related entities to be removed
foreach ($entity->{$lifeCycleFunctionNames['getter']}() as $existingRelatedEntityToBeRemoved) {
if (!in_array($existingRelatedEntityToBeRemoved->getId(),
$existingRelatedEntityIdsToBeKept)) {
$entity->{$lifeCycleFunctionNames['remover']}($existingRelatedEntityToBeRemoved);
$relatedEntityManager->delete($existingRelatedEntityToBeRemoved);
}
}
// Update existing related entities and create new ones
foreach ($values[$fieldName] as $key => $relatedEntityData) {
if (isset($relatedEntityData['id'])) {
$processedEntities['updated'][] = $relatedEntityManager
->safeUpdate($this->getEntityReference($relatedEntityClassName,
$relatedEntityData['id']), $relatedEntityData, false)
;
} else {
$newEntity = $relatedEntityManager->safeCreate(
$newRelatedEntitySafeCreateArrayArgumentGetter($entity,
$relatedEntityData), false
);
$processedEntities['created'][] = $newEntity;
$entity->{$lifeCycleFunctionNames['adder']}($newEntity);
}
}
}
return $processedEntities;
}
/**
* Shortcut function to get either an entity reference or the entity object itself
*
* @param int|object|null $value Either the entity id, the entity itself or null
* @param string|null $className The entity's class name as returned from Entity::class
* If null then the entity repository is used and
* the fully loaded entity
*
* @return object The entity itself, a reference to it or null
*/
public function getEntityReferenceOrObject($value, $className = null)
{
if (is_numeric($value)) {
if ($className !== null) {
return $this->doctrineEntityManager->getReference($className, $value);
} else {
return $this->getEntityRepo()->find($value);
}
} else {
return $value;
}
}
/**
* Shortcut function to get a \DateTime object from either a string or a \DateTime object
*
* @param \DateTime|string|null $mixedDateTime The value you want to get returned as \DateTime.
* Defaults to current date and time if null
*
* @return \DateTime
*/
protected function mixedToDateTime($mixedDateTime = null)
{
if ($mixedDateTime === null) {
return new \DateTime();
}
if ($mixedDateTime instanceof \DateTime) {
return $mixedDateTime;
}
if ($this->validationService->isValidDate($mixedDateTime)) {
return new \DateTime($mixedDateTime);
}
return null;
}
/**
* Shortcut function to get a boolean from either a string or a boolean
* If the input value is invalid it will return null
*
* @param bool|string|(int)0|(int)1 $mixedBool The value you want to get returned as boolean
*
* @return boolean|null
*/
protected function mixedToBool($mixedBool)
{
if ($mixedBool instanceof bool) {
return $mixedBool;
}
if ($mixedBool === 1 || $mixedBool == '1' || $mixedBool == 'true' || $mixedBool == 'TRUE') {
return true;
}
if ($mixedBool === 0 || $mixedBool == '0' || $mixedBool == 'false' || $mixedBool == 'FALSE') {
return false;
}
}
/**
* Shortcut method to begin a transaction in doctrine's entity manager
*
* @return bool true if the transaction was actually begun now, false if not
*/
protected function beginTransaction()
{
if (!$this->doctrineEntityManager->getConnection()->isTransactionActive()) {
$this->doctrineEntityManager->beginTransaction();
return true;
}
return false;
}
/**
* Method to request the beginning of a transaction from an external class.
* Further actions/checks can be performed here or even denying the request.
*
* @return bool true if the transaction was actually begun now, false if not
*/
public function requestTransactionBegin()
{
return $this->beginTransaction();
}
/**
* Shortcut method to commit a transaction in doctrine's entity manager, if there is
* and active one
*/
protected function commitTransaction()
{
if ($this->doctrineEntityManager->getConnection()->isTransactionActive()) {
$this->doctrineEntityManager->commit();
}
}
/**
* Method to request the end of a transaction from an external class.
* Further actions/checks can be performed here or even denying the request.
*/
public function requestTransactionCommit()
{
return $this->commitTransaction();
}
/**
* Shortcut method to rollback a transaction if there is an active one, and re-throw
* an exception if passed one
*
* @param Exception $ex
*
* @throws Exception
*/
protected function rollbackTransactionAndReThrowException(Exception $ex = null)
{
// If we had an exception, and still haven't committed the transaction, rollback the whole transaction
if ($this->doctrineEntityManager->getConnection()->isTransactionActive()) {
$this->doctrineEntityManager->rollback();
}
// And rethrow the exception for external catching, if any
if ($ex !== null) {
throw $ex;
}
}
/**
* Method to request the rollback of a transaction and re-throw of an exception from
* an external class.
* Further actions/checks can be performed here or even denying the request.
*/
public function requestRollbackTransactionAndReThrowException(Exception $ex = null)
{
$this->rollbackTransactionAndReThrowException($ex);
}
/**
* Shortcut to get the logged user
*
* @return User|null The logged user or null if not logged in
*/
protected function getLoggedUser()
{
$loggedUser = $this->securityTokenStorage->getToken()?->getUser();
if (is_string($loggedUser) || $loggedUser === null) {
return null;
} else {
return $loggedUser;
}
}
/**
* Shortcut to get the logged user, also checking the logged user exists and
* throwing an access denied exception if none found
*
* @return User The logged user
* @throws AccessDeniedException
*
*/
protected function getRequiredLoggedUser()
{
$loggedUser = $this->getLoggedUser();
if ($loggedUser == null) {
throw new AccessDeniedException();
}
return $loggedUser;
}
/**
* Function to determine if the "userless cli command" flag has been raised. This can
* be used to avoid requesting and using the loggedUser given there will be none. It
* might not even make sense to do this step in the calling function if running from
* the command line.
*
* @return bool True if we are running a userless cli command which raised the
* executing_userless_cli_command custom flag. False otherwise
*/
public function runningUserlessCliCommandOrAnonymousRequest()
{
return isset($_SERVER['executing_userless_cli_command_or_anonymous_request']) &&
$_SERVER['executing_userless_cli_command_or_anonymous_request'] === true;
}
/**
* Function to get an attribute value from the entity or from the $values array if it
* has been overridden
*
* @param object $entity The entity to default to
* @param string $fieldName The name of the attribute/key in $values array
* @param array $values Usually the key/value pair used to create/update
* an entity
* @param 'bool'|'date'|'time'|'entity' $type A type hint to allow formatting the
* value if retrieved from the $values
* array
* @param string $className Used when type == 'entity'. The class name of
* the entity you want to build from the $values
* value
* @param array $getterParams Any parameters needed when invoking the getter
* function on the entity
* @param string $customGetterName The name of the getter function, if not the
* standard one
*
* @return mixed The value retrieved from the $values array, formatted as needed or
* the attribute value from the entity provided, if not defined in $values array
*/
protected function getAttributeOrValue($entity, $fieldName, $values, $type = null, $className = null, $getterParams = array(), $customGetterName = null)
{
// First try to get the value from the values array (which overrides the one in
// the entity)
if (array_key_exists($fieldName, $values)) {
switch ($type) {
case 'bool':
return $this->mixedToBool($values[$fieldName]);
case 'int':
return intval($values[$fieldName]);
case 'dateTime':
case 'date':
case 'time':
if ($this->validationService->isValidDate($values[$fieldName])) {
return $this->mixedToDateTime($values[$fieldName]);
}
case 'entity':
return $this->getEntityReferenceOrObject(
$values[$fieldName], $className
);
default:
return $values[$fieldName];
}
}
// Else get it from the entity
if ($entity !== null) {
if ($customGetterName !== null) {
$getterName = $customGetterName;
} else {
$getterName = $this->getFieldGetterName($fieldName);
}
return call_user_func_array(array($entity, $getterName), $getterParams);
}
return null;
}
/**
* Function to populate a $values array from an entity based on a list of fields,
* using the common getFieldGetterName function to pick the values
*
* @param object $entity The entity to pick values from
* @param array $fieldNames The list of fields to attempt to set up
* @param array $values The values array to fill in
*
* @return array The values array with all possible values set
*/
public function extractValuesOfFields($entity, array $fieldNames, array $values = null)
{
if ($values === null) {
$values = array();
}
foreach ($fieldNames as $fieldName) {
$getterName = $this->getFieldGetterName($fieldName);
if ($entity->$getterName() !== null) {
$values[$fieldName] = $entity->$getterName();
}
}
return $values;
}
/**
* @param object $entity
*
* @return ClassMetadata|null
*/
public function getEntityMetadata(object $entity): ClassMetadata|null
{
return $this->getEntityClassMetadata(get_class($entity));
}
/**
* @param string $entityClass
*
* @return ClassMetadata|null
*/
public function getEntityClassMetadata(string $entityClass): ClassMetadata|null
{
return $this->getDoctrineEntityManager()->getClassMetadata($entityClass);
}
/**
* Function to get the field descriptions for a given entity class based on its metadata
*
* @param string|null $entityClass
* @param array|null $fieldsNames
*
* @return array
*/
public function getEntityFieldsDescriptions(string $entityClass = null, array $fieldsNames = null): array
{
if (is_array($fieldsNames) && count($fieldsNames) == 0) {
return array();
}
if ($entityClass === null) {
$entityClass = $this->getManagedEntityClass();
}
// Retrieve the metadata for the entity class
$metadata = $this->getEntityClassMetadata($entityClass);
if (!$metadata) {
return array();
}
$fieldsDescription = array();
foreach ($metadata->fieldMappings as $fieldName => $mapping) {
// Skip if certain fields were requested and this one was not
if (
$fieldsNames !== null &&
!in_array($fieldName, $fieldsNames)
) {
continue;
}
$fieldsDescription[$fieldName] = array(
'type' => $mapping['type'],
'nullable' => $mapping['nullable'] ?? false,
'columnName' => $mapping['columnName'],
);
}
return $fieldsDescription;
}
protected function getAiUpdatableFields()
{
return array();
}
protected function getAiUpdatableFieldsDescriptions(string $entityClass = null)
{
return $this->getEntityFieldsDescriptions($entityClass, $this->getAiUpdatableFields());
}
}