commit
f90897465c
@ -89,7 +89,7 @@ class DefaultCollectionHydrator implements CollectionHydrator
|
||||
|
||||
/* @var $entityEntries \Doctrine\ORM\Cache\EntityCacheEntry[] */
|
||||
foreach ($entityEntries as $index => $entityEntry) {
|
||||
$list[$index] = $this->uow->createEntity($entityEntry->class, $entityEntry->data, self::$hints);
|
||||
$list[$index] = $this->uow->createEntity($entityEntry->class, $entityEntry->resolveAssociationEntries($this->em), self::$hints);
|
||||
}
|
||||
|
||||
array_walk($list, function($entity, $index) use ($collection) {
|
||||
|
@ -27,6 +27,7 @@ use Doctrine\ORM\Cache\CollectionCacheKey;
|
||||
use Doctrine\ORM\Cache\TimestampCacheKey;
|
||||
use Doctrine\ORM\Cache\QueryCacheKey;
|
||||
use Doctrine\ORM\Cache\Persister\CachedPersister;
|
||||
use Doctrine\ORM\Cache\CacheException;
|
||||
use Doctrine\ORM\Mapping\ClassMetadata;
|
||||
use Doctrine\ORM\PersistentCollection;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
@ -233,6 +234,14 @@ abstract class AbstractEntityPersister implements CachedEntityPersister
|
||||
$class = $this->metadataFactory->getMetadataFor($className);
|
||||
}
|
||||
|
||||
if ($class->containsForeignIdentifier) {
|
||||
foreach ($class->associationMappings as $name => $assoc) {
|
||||
if (!empty($assoc['id']) && !isset($assoc['cache'])) {
|
||||
throw CacheException::nonCacheableEntityAssociation($class->name, $name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$entry = $this->hydrator->buildCacheEntry($class, $key, $entity);
|
||||
$cached = $this->region->put($key, $entry);
|
||||
|
||||
|
@ -317,4 +317,12 @@ class ORMException extends Exception
|
||||
{
|
||||
return new self("It is not allowed to overwrite internal function '$functionName' in the DQL parser through user-defined functions.");
|
||||
}
|
||||
|
||||
/**
|
||||
* @return ORMException
|
||||
*/
|
||||
public static function cantUseInOperatorOnCompositeKeys()
|
||||
{
|
||||
return new self("Can't use IN operator on entities that have composite keys.");
|
||||
}
|
||||
}
|
||||
|
@ -281,8 +281,11 @@ class BasicEntityPersister implements EntityPersister
|
||||
$stmt->execute();
|
||||
|
||||
if ($isPostInsertId) {
|
||||
$id = $idGenerator->generate($this->em, $entity);
|
||||
$postInsertIds[$id] = $entity;
|
||||
$generatedId = $idGenerator->generate($this->em, $entity);
|
||||
$id = array(
|
||||
$this->class->identifier[0] => $generatedId
|
||||
);
|
||||
$postInsertIds[$generatedId] = $entity;
|
||||
} else {
|
||||
$id = $this->class->getIdentifierValues($entity);
|
||||
}
|
||||
@ -304,11 +307,11 @@ class BasicEntityPersister implements EntityPersister
|
||||
* entities version field.
|
||||
*
|
||||
* @param object $entity
|
||||
* @param mixed $id
|
||||
* @param array $id
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function assignDefaultVersionValue($entity, $id)
|
||||
protected function assignDefaultVersionValue($entity, array $id)
|
||||
{
|
||||
$value = $this->fetchVersionValue($this->class, $id);
|
||||
|
||||
@ -319,11 +322,11 @@ class BasicEntityPersister implements EntityPersister
|
||||
* Fetches the current version value of a versioned entity.
|
||||
*
|
||||
* @param \Doctrine\ORM\Mapping\ClassMetadata $versionedClass
|
||||
* @param mixed $id
|
||||
* @param array $id
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
protected function fetchVersionValue($versionedClass, $id)
|
||||
protected function fetchVersionValue($versionedClass, array $id)
|
||||
{
|
||||
$versionField = $versionedClass->versionField;
|
||||
$tableName = $this->quoteStrategy->getTableName($versionedClass, $this->platform);
|
||||
@ -335,7 +338,7 @@ class BasicEntityPersister implements EntityPersister
|
||||
. ' FROM ' . $tableName
|
||||
. ' WHERE ' . implode(' = ? AND ', $identifier) . ' = ?';
|
||||
|
||||
$flatId = $this->identifierFlattener->flattenIdentifier($versionedClass, (array) $id);
|
||||
$flatId = $this->identifierFlattener->flattenIdentifier($versionedClass, $id);
|
||||
|
||||
$value = $this->conn->fetchColumn($sql, array_values($flatId));
|
||||
|
||||
@ -852,12 +855,12 @@ class BasicEntityPersister implements EntityPersister
|
||||
list($params, $types) = $valueVisitor->getParamsAndTypes();
|
||||
|
||||
foreach ($params as $param) {
|
||||
$sqlParams[] = PersisterHelper::getIdentifierValues($param, $this->em);
|
||||
$sqlParams = array_merge($sqlParams, $this->getValues($param));
|
||||
}
|
||||
|
||||
foreach ($types as $type) {
|
||||
list($field, $value) = $type;
|
||||
$sqlTypes[] = $this->getType($field, $value, $this->class);
|
||||
list ($field, $value) = $type;
|
||||
$sqlTypes = array_merge($sqlTypes, $this->getTypes($field, $value, $this->class));
|
||||
}
|
||||
|
||||
return array($sqlParams, $sqlTypes);
|
||||
@ -1565,40 +1568,61 @@ class BasicEntityPersister implements EntityPersister
|
||||
*/
|
||||
public function getSelectConditionStatementSQL($field, $value, $assoc = null, $comparison = null)
|
||||
{
|
||||
$placeholder = '?';
|
||||
$condition = $this->getSelectConditionStatementColumnSQL($field, $assoc);
|
||||
$selectedColumns = array();
|
||||
$columns = $this->getSelectConditionStatementColumnSQL($field, $assoc);
|
||||
|
||||
if (isset($this->class->fieldMappings[$field]['requireSQLConversion'])) {
|
||||
$placeholder = Type::getType($this->class->getTypeOfField($field))->convertToDatabaseValueSQL($placeholder, $this->platform);
|
||||
if (count($columns) > 1 && $comparison === Comparison::IN) {
|
||||
/*
|
||||
* @todo try to support multi-column IN expressions.
|
||||
* Example: (col1, col2) IN (('val1A', 'val2A'), ('val1B', 'val2B'))
|
||||
*/
|
||||
throw ORMException::cantUseInOperatorOnCompositeKeys();
|
||||
}
|
||||
|
||||
if ($comparison !== null) {
|
||||
foreach ($columns as $column) {
|
||||
$placeholder = '?';
|
||||
|
||||
// special case null value handling
|
||||
if (($comparison === Comparison::EQ || $comparison === Comparison::IS) && $value === null) {
|
||||
return $condition . ' IS NULL';
|
||||
} else if ($comparison === Comparison::NEQ && $value === null) {
|
||||
return $condition . ' IS NOT NULL';
|
||||
if (isset($this->class->fieldMappings[$field]['requireSQLConversion'])) {
|
||||
$placeholder = Type::getType($this->class->getTypeOfField($field))->convertToDatabaseValueSQL($placeholder, $this->platform);
|
||||
}
|
||||
|
||||
return $condition . ' ' . sprintf(self::$comparisonMap[$comparison], $placeholder);
|
||||
}
|
||||
if (null !== $comparison) {
|
||||
// special case null value handling
|
||||
if (($comparison === Comparison::EQ || $comparison === Comparison::IS) && null ===$value) {
|
||||
$selectedColumns[] = $column . ' IS NULL';
|
||||
continue;
|
||||
}
|
||||
|
||||
if (is_array($value)) {
|
||||
$in = sprintf('%s IN (%s)' , $condition, $placeholder);
|
||||
if ($comparison === Comparison::NEQ && null === $value) {
|
||||
$selectedColumns[] = $column . ' IS NOT NULL';
|
||||
continue;
|
||||
}
|
||||
|
||||
if (false !== array_search(null, $value, true)) {
|
||||
return sprintf('(%s OR %s IS NULL)' , $in, $condition);
|
||||
$selectedColumns[] = $column . ' ' . sprintf(self::$comparisonMap[$comparison], $placeholder);
|
||||
continue;
|
||||
}
|
||||
|
||||
return $in;
|
||||
if (is_array($value)) {
|
||||
$in = sprintf('%s IN (%s)', $column, $placeholder);
|
||||
|
||||
if (false !== array_search(null, $value, true)) {
|
||||
$selectedColumns[] = sprintf('(%s OR %s IS NULL)', $in, $column);
|
||||
continue;
|
||||
}
|
||||
|
||||
$selectedColumns[] = $in;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (null === $value) {
|
||||
$selectedColumns[] = sprintf('%s IS NULL', $column);
|
||||
continue;
|
||||
}
|
||||
|
||||
$selectedColumns[] = sprintf('%s = %s', $column, $placeholder);
|
||||
}
|
||||
|
||||
if ($value === null) {
|
||||
return sprintf('%s IS NULL' , $condition);
|
||||
}
|
||||
|
||||
return sprintf('%s = %s' , $condition, $placeholder);
|
||||
return implode(' AND ', $selectedColumns);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -1607,7 +1631,7 @@ class BasicEntityPersister implements EntityPersister
|
||||
* @param string $field
|
||||
* @param array|null $assoc
|
||||
*
|
||||
* @return string
|
||||
* @return string[]
|
||||
*
|
||||
* @throws \Doctrine\ORM\ORMException
|
||||
*/
|
||||
@ -1618,36 +1642,45 @@ class BasicEntityPersister implements EntityPersister
|
||||
? $this->class->fieldMappings[$field]['inherited']
|
||||
: $this->class->name;
|
||||
|
||||
return $this->getSQLTableAlias($className) . '.' . $this->quoteStrategy->getColumnName($field, $this->class, $this->platform);
|
||||
return array($this->getSQLTableAlias($className) . '.' . $this->quoteStrategy->getColumnName($field, $this->class, $this->platform));
|
||||
}
|
||||
|
||||
if (isset($this->class->associationMappings[$field])) {
|
||||
$association = $this->class->associationMappings[$field];
|
||||
|
||||
// Many-To-Many requires join table check for joinColumn
|
||||
$columns = array();
|
||||
$class = $this->class;
|
||||
|
||||
if ($association['type'] === ClassMetadata::MANY_TO_MANY) {
|
||||
if ( ! $association['isOwningSide']) {
|
||||
$association = $assoc;
|
||||
}
|
||||
|
||||
$joinTableName = $this->quoteStrategy->getJoinTableName($association, $this->class, $this->platform);
|
||||
$joinColumn = $assoc['isOwningSide']
|
||||
? $association['joinTable']['joinColumns'][0]
|
||||
: $association['joinTable']['inverseJoinColumns'][0];
|
||||
$joinTableName = $this->quoteStrategy->getJoinTableName($association, $class, $this->platform);
|
||||
$joinColumns = $assoc['isOwningSide']
|
||||
? $association['joinTable']['joinColumns']
|
||||
: $association['joinTable']['inverseJoinColumns'];
|
||||
|
||||
return $joinTableName . '.' . $this->quoteStrategy->getJoinColumnName($joinColumn, $this->class, $this->platform);
|
||||
|
||||
foreach ($joinColumns as $joinColumn) {
|
||||
$columns[] = $joinTableName . '.' . $this->quoteStrategy->getJoinColumnName($joinColumn, $class, $this->platform);
|
||||
}
|
||||
|
||||
} else {
|
||||
|
||||
if ( ! $association['isOwningSide']) {
|
||||
throw ORMException::invalidFindByInverseAssociation($this->class->name, $field);
|
||||
}
|
||||
|
||||
$className = (isset($association['inherited']))
|
||||
? $association['inherited']
|
||||
: $this->class->name;
|
||||
|
||||
foreach ($association['joinColumns'] as $joinColumn) {
|
||||
$columns[] = $this->getSQLTableAlias($className) . '.' . $this->quoteStrategy->getJoinColumnName($joinColumn, $this->class, $this->platform);
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! $association['isOwningSide']) {
|
||||
throw ORMException::invalidFindByInverseAssociation($this->class->name, $field);
|
||||
}
|
||||
|
||||
$joinColumn = $association['joinColumns'][0];
|
||||
$className = (isset($association['inherited']))
|
||||
? $association['inherited']
|
||||
: $this->class->name;
|
||||
|
||||
return $this->getSQLTableAlias($className) . '.' . $this->quoteStrategy->getJoinColumnName($joinColumn, $this->class, $this->platform);
|
||||
return $columns;
|
||||
}
|
||||
|
||||
if ($assoc !== null && strpos($field, " ") === false && strpos($field, "(") === false) {
|
||||
@ -1655,7 +1688,7 @@ class BasicEntityPersister implements EntityPersister
|
||||
// therefore checking for spaces and function calls which are not allowed.
|
||||
|
||||
// found a join column condition, not really a "field"
|
||||
return $field;
|
||||
return array($field);
|
||||
}
|
||||
|
||||
throw ORMException::unrecognizedField($field);
|
||||
@ -1778,8 +1811,8 @@ class BasicEntityPersister implements EntityPersister
|
||||
continue; // skip null values.
|
||||
}
|
||||
|
||||
$types[] = $this->getType($field, $value, $this->class);
|
||||
$params[] = $this->getValue($value);
|
||||
$types = array_merge($types, $this->getTypes($field, $value, $this->class));
|
||||
$params = array_merge($params, $this->getValues($value));
|
||||
}
|
||||
|
||||
return array($params, $types);
|
||||
@ -1807,56 +1840,100 @@ class BasicEntityPersister implements EntityPersister
|
||||
continue; // skip null values.
|
||||
}
|
||||
|
||||
$types[] = $this->getType($criterion['field'], $criterion['value'], $criterion['class']);
|
||||
$params[] = PersisterHelper::getIdentifierValues($criterion['value'], $this->em);
|
||||
$types = array_merge($types, $this->getTypes($criterion['field'], $criterion['value'], $criterion['class']));
|
||||
$params = array_merge($params, $this->getValues($criterion['value']));
|
||||
}
|
||||
|
||||
return array($params, $types);
|
||||
}
|
||||
|
||||
/**
|
||||
* Infers field type to be used by parameter type casting.
|
||||
* Infers field types to be used by parameter type casting.
|
||||
*
|
||||
* @param string $fieldName
|
||||
* @param mixed $value
|
||||
* @param ClassMetadata $class
|
||||
* @param string $field
|
||||
* @param mixed $value
|
||||
*
|
||||
* @return integer
|
||||
* @return array
|
||||
*
|
||||
* @throws \Doctrine\ORM\Query\QueryException
|
||||
*/
|
||||
private function getType($fieldName, $value, ClassMetadata $class)
|
||||
private function getTypes($field, $value, ClassMetadata $class)
|
||||
{
|
||||
$type = PersisterHelper::getTypeOfField($fieldName, $class, $this->em);
|
||||
$types = array();
|
||||
|
||||
if (is_array($value)) {
|
||||
$type = Type::getType($type)->getBindingType();
|
||||
$type += Connection::ARRAY_PARAM_OFFSET;
|
||||
switch (true) {
|
||||
case (isset($class->fieldMappings[$field])):
|
||||
$types = array_merge($types, PersisterHelper::getTypeOfField($field, $class, $this->em));
|
||||
break;
|
||||
|
||||
case (isset($class->associationMappings[$field])):
|
||||
$assoc = $class->associationMappings[$field];
|
||||
$class = $this->em->getClassMetadata($assoc['targetEntity']);
|
||||
|
||||
if (! $assoc['isOwningSide']) {
|
||||
$assoc = $class->associationMappings[$assoc['mappedBy']];
|
||||
$class = $this->em->getClassMetadata($assoc['targetEntity']);
|
||||
}
|
||||
|
||||
$columns = $assoc['type'] === ClassMetadata::MANY_TO_MANY
|
||||
? $assoc['relationToTargetKeyColumns']
|
||||
: $assoc['sourceToTargetKeyColumns'];
|
||||
|
||||
foreach ($columns as $column){
|
||||
$types[] = PersisterHelper::getTypeOfColumn($column, $class, $this->em);
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
$types[] = null;
|
||||
break;
|
||||
}
|
||||
|
||||
return $type;
|
||||
if (is_array($value)) {
|
||||
return array_map(
|
||||
function ($type) {
|
||||
return Type::getType($type)->getBindingType() + Connection::ARRAY_PARAM_OFFSET;
|
||||
},
|
||||
$types
|
||||
);
|
||||
}
|
||||
|
||||
return $types;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves parameter value.
|
||||
* Retrieves the parameters that identifies a value.
|
||||
*
|
||||
* @param mixed $value
|
||||
*
|
||||
* @return mixed
|
||||
* @return array
|
||||
*/
|
||||
private function getValue($value)
|
||||
private function getValues($value)
|
||||
{
|
||||
if ( ! is_array($value)) {
|
||||
return $this->getIndividualValue($value);
|
||||
if (is_array($value)) {
|
||||
$newValue = array();
|
||||
|
||||
foreach ($value as $itemValue) {
|
||||
$newValue = array_merge($newValue, $this->getValues($itemValue));
|
||||
}
|
||||
|
||||
return array($newValue);
|
||||
}
|
||||
|
||||
$newValue = array();
|
||||
if (is_object($value) && $this->em->getMetadataFactory()->hasMetadataFor(ClassUtils::getClass($value))) {
|
||||
$class = $this->em->getClassMetadata(get_class($value));
|
||||
if ($class->isIdentifierComposite) {
|
||||
$newValue = array();
|
||||
|
||||
foreach ($value as $itemValue) {
|
||||
$newValue[] = $this->getIndividualValue($itemValue);
|
||||
foreach ($class->getIdentifierValues($value) as $innerValue) {
|
||||
$newValue = array_merge($newValue, $this->getValues($innerValue));
|
||||
}
|
||||
|
||||
return $newValue;
|
||||
}
|
||||
}
|
||||
|
||||
return $newValue;
|
||||
return array($this->getIndividualValue($value));
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -176,8 +176,11 @@ class JoinedSubclassPersister extends AbstractEntityInheritancePersister
|
||||
$rootTableStmt->execute();
|
||||
|
||||
if ($isPostInsertId) {
|
||||
$id = $idGenerator->generate($this->em, $entity);
|
||||
$postInsertIds[$id] = $entity;
|
||||
$generatedId = $idGenerator->generate($this->em, $entity);
|
||||
$id = array(
|
||||
$this->class->identifier[0] => $generatedId
|
||||
);
|
||||
$postInsertIds[$generatedId] = $entity;
|
||||
} else {
|
||||
$id = $this->em->getUnitOfWork()->getEntityIdentifier($entity);
|
||||
}
|
||||
@ -572,7 +575,7 @@ class JoinedSubclassPersister extends AbstractEntityInheritancePersister
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function assignDefaultVersionValue($entity, $id)
|
||||
protected function assignDefaultVersionValue($entity, array $id)
|
||||
{
|
||||
$value = $this->fetchVersionValue($this->getVersionedClassMetadata(), $id);
|
||||
$this->class->setFieldValue($entity, $this->class->versionField, $value);
|
||||
|
@ -49,6 +49,7 @@ use Doctrine\ORM\Persisters\Entity\JoinedSubclassPersister;
|
||||
use Doctrine\ORM\Persisters\Collection\OneToManyPersister;
|
||||
use Doctrine\ORM\Persisters\Collection\ManyToManyPersister;
|
||||
use Doctrine\ORM\Utility\IdentifierFlattener;
|
||||
use Doctrine\ORM\Cache\AssociationCacheEntry;
|
||||
|
||||
/**
|
||||
* The UnitOfWork is responsible for tracking changes to objects during an
|
||||
@ -2490,22 +2491,7 @@ class UnitOfWork implements PropertyChangedListener
|
||||
$class = $this->em->getClassMetadata($className);
|
||||
//$isReadOnly = isset($hints[Query::HINT_READ_ONLY]);
|
||||
|
||||
if ($class->isIdentifierComposite) {
|
||||
$id = array();
|
||||
|
||||
foreach ($class->identifier as $fieldName) {
|
||||
$id[$fieldName] = isset($class->associationMappings[$fieldName])
|
||||
? $data[$class->associationMappings[$fieldName]['joinColumns'][0]['name']]
|
||||
: $data[$fieldName];
|
||||
}
|
||||
} else {
|
||||
$id = isset($class->associationMappings[$class->identifier[0]])
|
||||
? $data[$class->associationMappings[$class->identifier[0]]['joinColumns'][0]['name']]
|
||||
: $data[$class->identifier[0]];
|
||||
|
||||
$id = array($class->identifier[0] => $id);
|
||||
}
|
||||
|
||||
$id = $this->identifierFlattener->flattenIdentifier($class, $data);
|
||||
$idHash = implode(' ', $id);
|
||||
|
||||
if (isset($this->identityMap[$class->rootEntityName][$idHash])) {
|
||||
@ -2643,6 +2629,12 @@ class UnitOfWork implements PropertyChangedListener
|
||||
} else {
|
||||
$associatedId[$targetClass->fieldNames[$targetColumn]] = $joinColumnValue;
|
||||
}
|
||||
} elseif ($targetClass->containsForeignIdentifier
|
||||
&& in_array($targetClass->getFieldForColumn($targetColumn), $targetClass->identifier, true)
|
||||
) {
|
||||
// the missing key is part of target's entity primary key
|
||||
$associatedId = array();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -71,20 +71,30 @@ final class IdentifierFlattener
|
||||
{
|
||||
$flatId = array();
|
||||
|
||||
foreach ($id as $idField => $idValue) {
|
||||
if (isset($class->associationMappings[$idField]) && is_object($idValue)) {
|
||||
foreach ($class->identifier as $field) {
|
||||
if (isset($class->associationMappings[$field]) && isset($id[$field]) && is_object($id[$field])) {
|
||||
/* @var $targetClassMetadata ClassMetadata */
|
||||
$targetClassMetadata = $this->metadataFactory->getMetadataFor(
|
||||
$class->associationMappings[$idField]['targetEntity']
|
||||
$class->associationMappings[$field]['targetEntity']
|
||||
);
|
||||
|
||||
$associatedId = $this->unitOfWork->isInIdentityMap($idValue)
|
||||
? $this->unitOfWork->getEntityIdentifier($idValue)
|
||||
: $targetClassMetadata->getIdentifierValues($idValue);
|
||||
if ($this->unitOfWork->isInIdentityMap($id[$field])) {
|
||||
$associatedId = $this->flattenIdentifier($targetClassMetadata, $this->unitOfWork->getEntityIdentifier($id[$field]));
|
||||
} else {
|
||||
$associatedId = $this->flattenIdentifier($targetClassMetadata, $targetClassMetadata->getIdentifierValues($id[$field]));
|
||||
}
|
||||
|
||||
$flatId[$idField] = $associatedId[$targetClassMetadata->identifier[0]];
|
||||
$flatId[$field] = implode(' ', $associatedId);
|
||||
} elseif (isset($class->associationMappings[$field])) {
|
||||
$associatedId = array();
|
||||
|
||||
foreach ($class->associationMappings[$field]['joinColumns'] as $joinColumn) {
|
||||
$associatedId[] = $id[$joinColumn['name']];
|
||||
}
|
||||
|
||||
$flatId[$field] = implode(' ', $associatedId);
|
||||
} else {
|
||||
$flatId[$idField] = $idValue;
|
||||
$flatId[$field] = $id[$field];
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -40,18 +40,18 @@ class PersisterHelper
|
||||
* @param ClassMetadata $class
|
||||
* @param EntityManagerInterface $em
|
||||
*
|
||||
* @return string|null
|
||||
* @return array
|
||||
*
|
||||
* @throws QueryException
|
||||
*/
|
||||
public static function getTypeOfField($fieldName, ClassMetadata $class, EntityManagerInterface $em)
|
||||
{
|
||||
if (isset($class->fieldMappings[$fieldName])) {
|
||||
return $class->fieldMappings[$fieldName]['type'];
|
||||
return array($class->fieldMappings[$fieldName]['type']);
|
||||
}
|
||||
|
||||
if ( ! isset($class->associationMappings[$fieldName])) {
|
||||
return null;
|
||||
return array();
|
||||
}
|
||||
|
||||
$assoc = $class->associationMappings[$fieldName];
|
||||
@ -60,20 +60,20 @@ class PersisterHelper
|
||||
return self::getTypeOfField($assoc['mappedBy'], $em->getClassMetadata($assoc['targetEntity']), $em);
|
||||
}
|
||||
|
||||
if (($assoc['type'] & ClassMetadata::MANY_TO_MANY) > 0) {
|
||||
if ($assoc['type'] & ClassMetadata::MANY_TO_MANY) {
|
||||
$joinData = $assoc['joinTable'];
|
||||
} else {
|
||||
$joinData = $assoc;
|
||||
}
|
||||
|
||||
if (count($joinData['joinColumns']) > 1) {
|
||||
throw QueryException::associationPathCompositeKeyNotSupported();
|
||||
$types = array();
|
||||
$targetClass = $em->getClassMetadata($assoc['targetEntity']);
|
||||
|
||||
foreach ($joinData['joinColumns'] as $joinColumn) {
|
||||
$types[] = self::getTypeOfColumn($joinColumn['referencedColumnName'], $targetClass, $em);
|
||||
}
|
||||
|
||||
$targetColumnName = $joinData['joinColumns'][0]['referencedColumnName'];
|
||||
$targetClass = $em->getClassMetadata($assoc['targetEntity']);
|
||||
|
||||
return self::getTypeOfColumn($targetColumnName, $targetClass, $em);
|
||||
return $types;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -133,40 +133,4 @@ class PersisterHelper
|
||||
$class->getName()
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $value
|
||||
* @param EntityManagerInterface $em
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public static function getIdentifierValues($value, EntityManagerInterface $em)
|
||||
{
|
||||
if ( ! is_array($value)) {
|
||||
return self::getIndividualValue($value, $em);
|
||||
}
|
||||
|
||||
$newValue = array();
|
||||
|
||||
foreach ($value as $fieldName => $fieldValue) {
|
||||
$newValue[$fieldName] = self::getIndividualValue($fieldValue, $em);
|
||||
}
|
||||
|
||||
return $newValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $value
|
||||
* @param EntityManagerInterface $em
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
private static function getIndividualValue($value, EntityManagerInterface $em)
|
||||
{
|
||||
if ( ! is_object($value) || ! $em->getMetadataFactory()->hasMetadataFor(ClassUtils::getClass($value))) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
return $em->getUnitOfWork()->getSingleIdentifierValue($value);
|
||||
}
|
||||
}
|
||||
|
44
tests/Doctrine/Tests/Models/GeoNames/Admin1.php
Normal file
44
tests/Doctrine/Tests/Models/GeoNames/Admin1.php
Normal file
@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
namespace Doctrine\Tests\Models\GeoNames;
|
||||
|
||||
/**
|
||||
* @Entity
|
||||
* @Table(name="geonames_admin1")
|
||||
* @Cache
|
||||
*/
|
||||
class Admin1
|
||||
{
|
||||
/**
|
||||
* @Id
|
||||
* @Column(type="integer", length=25)
|
||||
* @GeneratedValue(strategy="NONE")
|
||||
*/
|
||||
public $id;
|
||||
|
||||
/**
|
||||
* @Id
|
||||
* @ManyToOne(targetEntity="Country")
|
||||
* @JoinColumn(name="country", referencedColumnName="id")
|
||||
* @Cache
|
||||
*/
|
||||
public $country;
|
||||
|
||||
/**
|
||||
* @OneToMany(targetEntity="Admin1AlternateName", mappedBy="admin1")
|
||||
* @Cache
|
||||
*/
|
||||
public $names = array();
|
||||
|
||||
/**
|
||||
* @Column(type="string", length=255);
|
||||
*/
|
||||
public $name;
|
||||
|
||||
public function __construct($id, $name, Country $country)
|
||||
{
|
||||
$this->id = $id;
|
||||
$this->name = $name;
|
||||
$this->country = $country;
|
||||
}
|
||||
}
|
41
tests/Doctrine/Tests/Models/GeoNames/Admin1AlternateName.php
Normal file
41
tests/Doctrine/Tests/Models/GeoNames/Admin1AlternateName.php
Normal file
@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
namespace Doctrine\Tests\Models\GeoNames;
|
||||
|
||||
/**
|
||||
* @Entity
|
||||
* @Table(name="geonames_admin1_alternate_name")
|
||||
* @Cache
|
||||
*/
|
||||
class Admin1AlternateName
|
||||
{
|
||||
/**
|
||||
* @Id
|
||||
* @Column(type="string", length=25)
|
||||
* @GeneratedValue(strategy="NONE")
|
||||
*/
|
||||
public $id;
|
||||
|
||||
/**
|
||||
* @ManyToOne(targetEntity="Admin1", inversedBy="names")
|
||||
* @JoinColumns({
|
||||
* @JoinColumn(name="admin1", referencedColumnName="id"),
|
||||
* @JoinColumn(name="country", referencedColumnName="country")
|
||||
* })
|
||||
* @Cache
|
||||
*/
|
||||
public $admin1;
|
||||
|
||||
/**
|
||||
* @Column(type="string", length=255);
|
||||
*/
|
||||
public $name;
|
||||
|
||||
|
||||
public function __construct($id, $name, Admin1 $admin1)
|
||||
{
|
||||
$this->id = $id;
|
||||
$this->name = $name;
|
||||
$this->admin1 = $admin1;
|
||||
}
|
||||
}
|
47
tests/Doctrine/Tests/Models/GeoNames/City.php
Normal file
47
tests/Doctrine/Tests/Models/GeoNames/City.php
Normal file
@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
namespace Doctrine\Tests\Models\GeoNames;
|
||||
|
||||
/**
|
||||
* @Entity
|
||||
* @Table(name="geonames_city")
|
||||
* @Cache
|
||||
*/
|
||||
class City
|
||||
{
|
||||
/**
|
||||
* @Id
|
||||
* @Column(type="string", length=25)
|
||||
* @GeneratedValue(strategy="NONE")
|
||||
*/
|
||||
public $id;
|
||||
|
||||
/**
|
||||
* @ManyToOne(targetEntity="Country")
|
||||
* @JoinColumn(name="country", referencedColumnName="id")
|
||||
* @Cache
|
||||
*/
|
||||
public $country;
|
||||
|
||||
/**
|
||||
* @ManyToOne(targetEntity="Admin1")
|
||||
* @JoinColumns({
|
||||
* @JoinColumn(name="admin1", referencedColumnName="id"),
|
||||
* @JoinColumn(name="country", referencedColumnName="country")
|
||||
* })
|
||||
* @Cache
|
||||
*/
|
||||
public $admin1;
|
||||
|
||||
/**
|
||||
* @Column(type="string", length=255);
|
||||
*/
|
||||
public $name;
|
||||
|
||||
|
||||
public function __construct($id, $name)
|
||||
{
|
||||
$this->id = $id;
|
||||
$this->name = $name;
|
||||
}
|
||||
}
|
29
tests/Doctrine/Tests/Models/GeoNames/Country.php
Normal file
29
tests/Doctrine/Tests/Models/GeoNames/Country.php
Normal file
@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace Doctrine\Tests\Models\GeoNames;
|
||||
|
||||
/**
|
||||
* @Entity
|
||||
* @Table(name="geonames_country")
|
||||
* @Cache
|
||||
*/
|
||||
class Country
|
||||
{
|
||||
/**
|
||||
* @Id
|
||||
* @Column(type="string", length=2)
|
||||
* @GeneratedValue(strategy="NONE")
|
||||
*/
|
||||
public $id;
|
||||
|
||||
/**
|
||||
* @Column(type="string", length=255);
|
||||
*/
|
||||
public $name;
|
||||
|
||||
public function __construct($id, $name)
|
||||
{
|
||||
$this->id = $id;
|
||||
$this->name = $name;
|
||||
}
|
||||
}
|
@ -0,0 +1,60 @@
|
||||
<?php
|
||||
|
||||
namespace Doctrine\Tests\ORM\Functional;
|
||||
|
||||
use Doctrine\Tests\Models\GeoNames\Country;
|
||||
use Doctrine\Tests\Models\GeoNames\Admin1;
|
||||
use Doctrine\Tests\Models\GeoNames\Admin1AlternateName;
|
||||
|
||||
class CompositePrimaryKeyWithAssociationsTest extends \Doctrine\Tests\OrmFunctionalTestCase
|
||||
{
|
||||
public function setUp()
|
||||
{
|
||||
$this->useModelSet('geonames');
|
||||
parent::setUp();
|
||||
|
||||
$it = new Country("IT", "Italy");
|
||||
|
||||
$this->_em->persist($it);
|
||||
$this->_em->flush();
|
||||
|
||||
$admin1 = new Admin1(1, "Rome", $it);
|
||||
|
||||
$this->_em->persist($admin1);
|
||||
$this->_em->flush();
|
||||
|
||||
$name1 = new Admin1AlternateName(1, "Roma", $admin1);
|
||||
$name2 = new Admin1AlternateName(2, "Rome", $admin1);
|
||||
|
||||
$admin1->names[] = $name1;
|
||||
$admin1->names[] = $name2;
|
||||
|
||||
$this->_em->persist($admin1);
|
||||
$this->_em->persist($name1);
|
||||
$this->_em->persist($name2);
|
||||
|
||||
$this->_em->flush();
|
||||
|
||||
$this->_em->clear();
|
||||
}
|
||||
|
||||
public function testFindByAbleToGetCompositeEntitiesWithMixedTypeIdentifiers()
|
||||
{
|
||||
$admin1Repo = $this->_em->getRepository('Doctrine\Tests\Models\GeoNames\Admin1');
|
||||
$admin1NamesRepo = $this->_em->getRepository('Doctrine\Tests\Models\GeoNames\Admin1AlternateName');
|
||||
|
||||
$admin1Rome = $admin1Repo->findOneBy(array('country' => 'IT', 'id' => 1));
|
||||
|
||||
$names = $admin1NamesRepo->findBy(array('admin1' => $admin1Rome));
|
||||
$this->assertCount(2, $names);
|
||||
|
||||
$name1 = $admin1NamesRepo->findOneBy(array('admin1' => $admin1Rome, 'id' => 1));
|
||||
$name2 = $admin1NamesRepo->findOneBy(array('admin1' => $admin1Rome, 'id' => 2));
|
||||
|
||||
$this->assertEquals(1, $name1->id);
|
||||
$this->assertEquals("Roma", $name1->name);
|
||||
|
||||
$this->assertEquals(2, $name2->id);
|
||||
$this->assertEquals("Rome", $name2->name);
|
||||
}
|
||||
}
|
@ -0,0 +1,81 @@
|
||||
<?php
|
||||
|
||||
namespace Doctrine\Tests\ORM\Functional;
|
||||
|
||||
use Doctrine\Tests\OrmFunctionalTestCase;
|
||||
use Doctrine\Tests\Models\GeoNames\Country;
|
||||
use Doctrine\Tests\Models\GeoNames\Admin1;
|
||||
use Doctrine\Tests\Models\GeoNames\Admin1AlternateName;
|
||||
|
||||
class SecondLevelCacheCompositePrimaryKeyWithAssociationsTest extends OrmFunctionalTestCase
|
||||
{
|
||||
|
||||
/**
|
||||
* @var \Doctrine\ORM\Cache
|
||||
*/
|
||||
protected $cache;
|
||||
|
||||
public function setUp()
|
||||
{
|
||||
$this->enableSecondLevelCache();
|
||||
$this->useModelSet('geonames');
|
||||
parent::setUp();
|
||||
|
||||
$this->cache = $this->_em->getCache();
|
||||
|
||||
$it = new Country("IT", "Italy");
|
||||
|
||||
$this->_em->persist($it);
|
||||
$this->_em->flush();
|
||||
|
||||
$admin1 = new Admin1(1, "Rome", $it);
|
||||
|
||||
$this->_em->persist($admin1);
|
||||
$this->_em->flush();
|
||||
|
||||
$name1 = new Admin1AlternateName(1, "Roma", $admin1);
|
||||
$name2 = new Admin1AlternateName(2, "Rome", $admin1);
|
||||
|
||||
$admin1->names[] = $name1;
|
||||
$admin1->names[] = $name2;
|
||||
|
||||
$this->_em->persist($admin1);
|
||||
$this->_em->persist($name1);
|
||||
$this->_em->persist($name2);
|
||||
|
||||
$this->_em->flush();
|
||||
$this->_em->clear();
|
||||
$this->evictRegions();
|
||||
|
||||
}
|
||||
|
||||
public function testFindByReturnsCachedEntity()
|
||||
{
|
||||
$admin1Repo = $this->_em->getRepository('Doctrine\Tests\Models\GeoNames\Admin1');
|
||||
|
||||
$queries = $this->getCurrentQueryCount();
|
||||
|
||||
$admin1Rome = $admin1Repo->findOneBy(array('country' => 'IT', 'id' => 1));
|
||||
|
||||
$this->assertEquals("Italy", $admin1Rome->country->name);
|
||||
$this->assertEquals(2, count($admin1Rome->names));
|
||||
$this->assertEquals($queries + 3, $this->getCurrentQueryCount());
|
||||
|
||||
$this->_em->clear();
|
||||
|
||||
$queries = $this->getCurrentQueryCount();
|
||||
|
||||
$admin1Rome = $admin1Repo->findOneBy(array('country' => 'IT', 'id' => 1));
|
||||
|
||||
$this->assertEquals("Italy", $admin1Rome->country->name);
|
||||
$this->assertEquals(2, count($admin1Rome->names));
|
||||
$this->assertEquals($queries, $this->getCurrentQueryCount());
|
||||
}
|
||||
|
||||
private function evictRegions()
|
||||
{
|
||||
$this->cache->evictQueryRegions();
|
||||
$this->cache->evictEntityRegions();
|
||||
$this->cache->evictCollectionRegions();
|
||||
}
|
||||
}
|
@ -0,0 +1,67 @@
|
||||
<?php
|
||||
|
||||
namespace Doctrine\Tests\ORM\Persisters;
|
||||
|
||||
use Doctrine\Common\Collections\Criteria;
|
||||
use Doctrine\ORM\Persisters\Entity\BasicEntityPersister;
|
||||
use Doctrine\Tests\Models\GeoNames\Admin1;
|
||||
use Doctrine\Tests\Models\GeoNames\Country;
|
||||
|
||||
class BasicEntityPersisterCompositeTypeParametersTest extends \Doctrine\Tests\OrmTestCase
|
||||
{
|
||||
/**
|
||||
* @var BasicEntityPersister
|
||||
*/
|
||||
protected $_persister;
|
||||
|
||||
/**
|
||||
* @var \Doctrine\ORM\EntityManager
|
||||
*/
|
||||
protected $_em;
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
protected function setUp()
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->_em = $this->_getTestEntityManager();
|
||||
|
||||
$this->_em->getClassMetadata('Doctrine\Tests\Models\GeoNames\Country');
|
||||
$this->_em->getClassMetadata('Doctrine\Tests\Models\GeoNames\Admin1');
|
||||
$this->_em->getClassMetadata('Doctrine\Tests\Models\GeoNames\Admin1AlternateName');
|
||||
|
||||
$this->_persister = new BasicEntityPersister($this->_em, $this->_em->getClassMetadata('Doctrine\Tests\Models\GeoNames\Admin1AlternateName'));
|
||||
|
||||
}
|
||||
|
||||
public function testExpandParametersWillExpandCompositeEntityKeys()
|
||||
{
|
||||
$country = new Country("IT", "Italy");
|
||||
$admin1 = new Admin1(10, "Rome", $country);
|
||||
|
||||
|
||||
list ($values, $types) = $this->_persister->expandParameters(array(
|
||||
'admin1' => $admin1
|
||||
));
|
||||
|
||||
$this->assertEquals(array('integer', 'string'), $types);
|
||||
$this->assertEquals(array(10, 'IT'), $values);
|
||||
}
|
||||
|
||||
public function testExpandCriteriaParametersWillExpandCompositeEntityKeys()
|
||||
{
|
||||
$country = new Country("IT", "Italy");
|
||||
$admin1 = new Admin1(10, "Rome", $country);
|
||||
|
||||
$criteria = Criteria::create();
|
||||
|
||||
$criteria->andWhere(Criteria::expr()->eq("admin1", $admin1));
|
||||
|
||||
list ($values, $types) = $this->_persister->expandCriteriaParameters($criteria);
|
||||
|
||||
$this->assertEquals(array('integer', 'string'), $types);
|
||||
$this->assertEquals(array(10, 'IT'), $values);
|
||||
}
|
||||
}
|
@ -0,0 +1,62 @@
|
||||
<?php
|
||||
|
||||
namespace Doctrine\Tests\ORM\Persisters;
|
||||
|
||||
use Doctrine\Common\Collections\Criteria;
|
||||
use Doctrine\DBAL\Types\Type as DBALType;
|
||||
use Doctrine\ORM\EntityManager;
|
||||
use Doctrine\ORM\Persisters\Entity\BasicEntityPersister;
|
||||
use Doctrine\Tests\Models\CustomType\CustomTypeParent;
|
||||
use Doctrine\Tests\Models\CustomType\CustomTypeChild;
|
||||
use Doctrine\Common\Collections\Expr\Comparison;
|
||||
|
||||
class BasicEntityPersisterCompositeTypeSqlTest extends \Doctrine\Tests\OrmTestCase
|
||||
{
|
||||
/**
|
||||
* @var BasicEntityPersister
|
||||
*/
|
||||
protected $_persister;
|
||||
|
||||
/**
|
||||
* @var \Doctrine\ORM\EntityManager
|
||||
*/
|
||||
protected $_em;
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
protected function setUp()
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->_em = $this->_getTestEntityManager();
|
||||
|
||||
$this->_persister = new BasicEntityPersister($this->_em, $this->_em->getClassMetadata('Doctrine\Tests\Models\GeoNames\Admin1AlternateName'));
|
||||
}
|
||||
|
||||
public function testSelectConditionStatementEq()
|
||||
{
|
||||
$statement = $this->_persister->getSelectConditionStatementSQL('admin1', 1, array(), Comparison::EQ);
|
||||
$this->assertEquals('t0.admin1 = ? AND t0.country = ?', $statement);
|
||||
}
|
||||
|
||||
public function testSelectConditionStatementEqNull()
|
||||
{
|
||||
$statement = $this->_persister->getSelectConditionStatementSQL('admin1', null, array(), Comparison::IS);
|
||||
$this->assertEquals('t0.admin1 IS NULL AND t0.country IS NULL', $statement);
|
||||
}
|
||||
|
||||
public function testSelectConditionStatementNeqNull()
|
||||
{
|
||||
$statement = $this->_persister->getSelectConditionStatementSQL('admin1', null, array(), Comparison::NEQ);
|
||||
$this->assertEquals('t0.admin1 IS NOT NULL AND t0.country IS NOT NULL', $statement);
|
||||
}
|
||||
|
||||
/**
|
||||
* @expectedException Doctrine\ORM\ORMException
|
||||
*/
|
||||
public function testSelectConditionStatementIn()
|
||||
{
|
||||
$this->_persister->getSelectConditionStatementSQL('admin1', array(), array(), Comparison::IN);
|
||||
}
|
||||
}
|
@ -9,6 +9,7 @@ use Doctrine\Tests\Models\Cache\City;
|
||||
use Doctrine\Tests\Models\Cache\Flight;
|
||||
use Doctrine\ORM\ORMException;
|
||||
use Doctrine\ORM\Utility\IdentifierFlattener;
|
||||
|
||||
/**
|
||||
* Test the IdentifierFlattener utility class
|
||||
*
|
||||
@ -87,7 +88,7 @@ class IdentifierFlattenerTest extends OrmFunctionalTestCase
|
||||
|
||||
$this->assertArrayHasKey('secondEntity', $flatIds, 'It should be called secondEntity');
|
||||
|
||||
$this->assertSame($id['secondEntity']->id, $flatIds['secondEntity']);
|
||||
$this->assertEquals($id['secondEntity']->id, $flatIds['secondEntity']);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -115,8 +116,8 @@ class IdentifierFlattenerTest extends OrmFunctionalTestCase
|
||||
$this->assertArrayHasKey('leavingFrom', $id);
|
||||
$this->assertArrayHasKey('goingTo', $id);
|
||||
|
||||
$this->assertSame($leeds, $id['leavingFrom']);
|
||||
$this->assertSame($london, $id['goingTo']);
|
||||
$this->assertEquals($leeds, $id['leavingFrom']);
|
||||
$this->assertEquals($london, $id['goingTo']);
|
||||
|
||||
$flatIds = $this->identifierFlattener->flattenIdentifier($class, $id);
|
||||
|
||||
@ -125,7 +126,7 @@ class IdentifierFlattenerTest extends OrmFunctionalTestCase
|
||||
$this->assertArrayHasKey('leavingFrom', $flatIds);
|
||||
$this->assertArrayHasKey('goingTo', $flatIds);
|
||||
|
||||
$this->assertSame($id['leavingFrom']->getId(), $flatIds['leavingFrom']);
|
||||
$this->assertSame($id['goingTo']->getId(), $flatIds['goingTo']);
|
||||
$this->assertEquals($id['leavingFrom']->getId(), $flatIds['leavingFrom']);
|
||||
$this->assertEquals($id['goingTo']->getId(), $flatIds['goingTo']);
|
||||
}
|
||||
}
|
||||
|
@ -252,6 +252,12 @@ abstract class OrmFunctionalTestCase extends OrmTestCase
|
||||
'Doctrine\Tests\Models\ValueConversionType\InversedManyToManyExtraLazyEntity',
|
||||
'Doctrine\Tests\Models\ValueConversionType\OwningManyToManyExtraLazyEntity'
|
||||
),
|
||||
'geonames' => array(
|
||||
'Doctrine\Tests\Models\GeoNames\Country',
|
||||
'Doctrine\Tests\Models\GeoNames\Admin1',
|
||||
'Doctrine\Tests\Models\GeoNames\Admin1AlternateName',
|
||||
'Doctrine\Tests\Models\GeoNames\City'
|
||||
)
|
||||
);
|
||||
|
||||
/**
|
||||
@ -483,6 +489,12 @@ abstract class OrmFunctionalTestCase extends OrmTestCase
|
||||
$conn->executeUpdate('DELETE FROM vct_owning_manytomany_extralazy');
|
||||
$conn->executeUpdate('DELETE FROM vct_inversed_manytomany_extralazy');
|
||||
}
|
||||
if (isset($this->_usedModelSets['geonames'])) {
|
||||
$conn->executeUpdate('DELETE FROM geonames_admin1_alternate_name');
|
||||
$conn->executeUpdate('DELETE FROM geonames_admin1');
|
||||
$conn->executeUpdate('DELETE FROM geonames_city');
|
||||
$conn->executeUpdate('DELETE FROM geonames_country');
|
||||
}
|
||||
|
||||
$this->_em->clear();
|
||||
}
|
||||
@ -623,7 +635,8 @@ abstract class OrmFunctionalTestCase extends OrmTestCase
|
||||
}
|
||||
|
||||
$config->setMetadataDriverImpl($config->newDefaultAnnotationDriver(array(
|
||||
realpath(__DIR__ . '/Models/Cache')
|
||||
realpath(__DIR__ . '/Models/Cache'),
|
||||
realpath(__DIR__ . '/Models/GeoNames')
|
||||
), true));
|
||||
|
||||
$conn = static::$_sharedConn;
|
||||
|
Loading…
Reference in New Issue
Block a user