소스 검색

improve hydration modes and schema quoting

phpdevcommunity 1 개월 전
부모
커밋
e194294b40
35개의 변경된 파일564개의 추가작업 그리고 322개의 파일을 삭제
  1. 1 1
      src/Command/DatabaseCreateCommand.php
  2. 1 1
      src/Command/DatabaseDropCommand.php
  3. 1 1
      src/Command/Migration/MigrationDiffCommand.php
  4. 1 1
      src/Command/Migration/MigrationMigrateCommand.php
  5. 1 1
      src/Command/QueryExecuteCommand.php
  6. 1 1
      src/Command/ShowTablesCommand.php
  7. 2 0
      src/Driver/MariaDBDriver.php
  8. 7 3
      src/EntityManager.php
  9. 1 1
      src/EntityManagerInterface.php
  10. 0 5
      src/Expression/Expr.php
  11. 93 0
      src/Hydrator/AbstractEntityHydrator.php
  12. 16 82
      src/Hydrator/EntityHydrator.php
  13. 11 0
      src/Hydrator/ReadOnlyEntityHydrator.php
  14. 1 1
      src/Mapping/Entity.php
  15. 1 1
      src/Migration/PaperMigration.php
  16. 0 1
      src/Parser/DSNParser.php
  17. 148 0
      src/Persistence/EntityPersistence.php
  18. 7 2
      src/Platform/MariaDBPlatform.php
  19. 3 0
      src/Platform/PlatformInterface.php
  20. 5 0
      src/Platform/SqlitePlatform.php
  21. 11 2
      src/Query/Fetcher.php
  22. 30 16
      src/Query/QueryBuilder.php
  23. 17 105
      src/Repository/Repository.php
  24. 36 30
      src/Schema/MariaDBSchema.php
  25. 31 0
      src/Schema/SchemaInterface.php
  26. 25 9
      src/Schema/SqliteSchema.php
  27. 26 0
      src/Schema/Traits/IdentifierQuotingTrait.php
  28. 1 1
      src/Serializer/SerializerToDb.php
  29. 1 1
      tests/DatabaseShowTablesCommandTest.php
  30. 2 2
      tests/Factory/DatabaseConnectionFactory.php
  31. 1 1
      tests/Helper/DataBaseHelperTest.php
  32. 35 43
      tests/MigrationTest.php
  33. 1 1
      tests/PlatformDiffTest.php
  34. 9 9
      tests/PlatformTest.php
  35. 37 0
      tests/RepositoryTest.php

+ 1 - 1
src/Command/DatabaseCreateCommand.php

@@ -44,7 +44,7 @@ class DatabaseCreateCommand implements CommandInterface
     public function execute(InputInterface $input, OutputInterface $output): void
     {
         $io = ConsoleOutput::create($output);
-        $platform = $this->entityManager->createDatabasePlatform();
+        $platform = $this->entityManager->getPlatform();
         if ($input->hasOption('if-not-exists') && $input->getOptionValue('if-not-exists') === true) {
             $platform->createDatabaseIfNotExists();
             $io->info(sprintf('The SQL database "%s" has been successfully created (if it did not already exist).', $platform->getDatabaseName()));

+ 1 - 1
src/Command/DatabaseDropCommand.php

@@ -56,7 +56,7 @@ class DatabaseDropCommand implements CommandInterface
             throw new LogicException('You must use the --force option to drop the database.');
         }
 
-        $platform = $this->entityManager->createDatabasePlatform();
+        $platform = $this->entityManager->getPlatform();
         $platform->dropDatabase();
         $io->success('The SQL database has been successfully dropped.');
     }

+ 1 - 1
src/Command/Migration/MigrationDiffCommand.php

@@ -59,7 +59,7 @@ class MigrationDiffCommand implements CommandInterface
             throw new \LogicException('The --entities-dir option is required');
         }
 
-        $platform = $this->paperMigration->getEntityManager()->createDatabasePlatform();
+        $platform = $this->paperMigration->getEntityManager()->getPlatform();
 
         $io->title('Starting migration diff on ' . $platform->getDatabaseName());
         $io->list([

+ 1 - 1
src/Command/Migration/MigrationMigrateCommand.php

@@ -42,7 +42,7 @@ class MigrationMigrateCommand implements CommandInterface
     {
         $io = ConsoleOutput::create($output);
 
-        $platform = $this->paperMigration->getEntityManager()->createDatabasePlatform();
+        $platform = $this->paperMigration->getEntityManager()->getPlatform();
 
         $io->title('Starting migration migrate on ' . $platform->getDatabaseName());
 

+ 1 - 1
src/Command/QueryExecuteCommand.php

@@ -47,7 +47,7 @@ class QueryExecuteCommand implements CommandInterface
         if ($query === null) {
             throw new \LogicException("SQL query is required");
         }
-        $io->title('Starting query on ' . $this->entityManager->createDatabasePlatform()->getDatabaseName());
+        $io->title('Starting query on ' . $this->entityManager->getPlatform()->getDatabaseName());
         
         $data = $this->entityManager->getConnection()->fetchAll($query);
         $io->listKeyValues([

+ 1 - 1
src/Command/ShowTablesCommand.php

@@ -55,7 +55,7 @@ class ShowTablesCommand implements CommandInterface
             $withColumns = $input->getOptionValue('columns');
         }
 
-        $platform = $this->entityManager->createDatabasePlatform();
+        $platform = $this->entityManager->getPlatform();
         $io->info('Database : ' . $platform->getDatabaseName());
         $tables = $platform->listTables();
         if ($tableName !== null) {

+ 2 - 0
src/Driver/MariaDBDriver.php

@@ -51,6 +51,8 @@ final class MariaDBDriver implements DriverInterface
 
         if (isset($params['dbname'])) {
             $dsn .= 'dbname=' . $params['dbname'] . ';';
+        }elseif (isset($params['path'])) {
+            $dsn .= 'dbname=' . $params['path'] . ';';
         }
 
         if (isset($params['unix_socket'])) {

+ 7 - 3
src/EntityManager.php

@@ -33,6 +33,7 @@ class EntityManager implements EntityManagerInterface
 
     private ListenerProviderInterface $listener;
     private EventDispatcherInterface $dispatcher;
+    private ?PlatformInterface $platform = null;
 
     public static function createFromDsn(string $dsn, bool $debug = false, LoggerInterface $logger = null, array $listeners = []): self
     {
@@ -136,10 +137,13 @@ class EntityManager implements EntityManagerInterface
     }
 
 
-    public function createDatabasePlatform(): PlatformInterface
+    public function getPlatform(): PlatformInterface
     {
-        $driver = $this->connection->getDriver();
-        return $driver->createDatabasePlatform($this->getConnection());
+        if ($this->platform === null) {
+            $driver = $this->connection->getDriver();
+            $this->platform = $driver->createDatabasePlatform($this->getConnection());
+        }
+        return $this->platform;
     }
 
 

+ 1 - 1
src/EntityManagerInterface.php

@@ -16,7 +16,7 @@ interface EntityManagerInterface
 
     public function getRepository(string $entity): Repository;
 
-    public function createDatabasePlatform(): PlatformInterface;
+    public function getPlatform(): PlatformInterface;
 
     public function getConnection(): PaperConnection;
     public function getCache(): EntityMemcachedCache;

+ 0 - 5
src/Expression/Expr.php

@@ -123,11 +123,6 @@ class Expr
         return [$this->getAliasKey() => $this->getValue()];
     }
 
-    public static function or(string ...$expressions): string
-    {
-        return '(' . implode(') OR (', $expressions) . ')';
-    }
-
     public static function equal(string $key, $value): self
     {
         return new self($key, '=', $value);

+ 93 - 0
src/Hydrator/AbstractEntityHydrator.php

@@ -0,0 +1,93 @@
+<?php
+
+namespace PhpDevCommunity\PaperORM\Hydrator;
+
+use PhpDevCommunity\PaperORM\Collection\ObjectStorage;
+use PhpDevCommunity\PaperORM\Entity\EntityInterface;
+use PhpDevCommunity\PaperORM\Mapper\ColumnMapper;
+use PhpDevCommunity\PaperORM\Mapping\Column\JoinColumn;
+use PhpDevCommunity\PaperORM\Mapping\OneToMany;
+use PhpDevCommunity\PaperORM\Proxy\ProxyInterface;
+
+abstract class AbstractEntityHydrator
+{
+    abstract protected function instantiate(string $class, array $data): object;
+
+    /**
+     *
+     * @param object|string $objectOrClass
+     * @param array $data
+     * @return object
+     */
+    public function hydrate($objectOrClass, array $data): object
+    {
+        if (!is_subclass_of($objectOrClass, EntityInterface::class)) {
+            throw new \LogicException(
+                sprintf('Class %s must implement %s', $objectOrClass, EntityInterface::class)
+            );
+        }
+
+        $object = is_string($objectOrClass) ? $this->instantiate($objectOrClass, $data) : $objectOrClass;
+
+        $this->mapProperties($object, $data);
+
+        return $object;
+    }
+
+    private function mapProperties(object $object, array $data): void
+    {
+        $reflection = new \ReflectionClass($object);
+        if ($reflection->getParentClass()) {
+            $reflection = $reflection->getParentClass();
+        }
+
+        $columns = array_merge(
+            ColumnMapper::getColumns($object),
+            ColumnMapper::getOneToManyRelations($object)
+        );
+
+        $properties = [];
+
+        foreach ($columns as $column) {
+            $name = ($column instanceof OneToMany || $column instanceof JoinColumn)
+                ? $column->getProperty()
+                : $column->getName();
+
+            if (!array_key_exists($name, $data)) {
+                continue;
+            }
+
+            $value = $data[$name];
+            if (!$column instanceof OneToMany) {
+                $properties[$column->getProperty()] = $column;
+            }
+
+            $property = $reflection->getProperty($column->getProperty());
+            $property->setAccessible(true);
+
+            if ($column instanceof JoinColumn) {
+                $entityName = $column->getTargetEntity();
+                $value = is_array($value) ? $this->hydrate($entityName, $value) : null;
+                $property->setValue($object, $value);
+                continue;
+            }
+
+            if ($column instanceof OneToMany) {
+                $entityName = $column->getTargetEntity();
+                $storage = $property->getValue($object) ?: new ObjectStorage();
+                foreach ((array) $value as $item) {
+                    $storage->add($this->hydrate($entityName, $item));
+                }
+                $property->setValue($object, $storage);
+                continue;
+            }
+
+            $property->setValue($object, $column->convertToPHP($value));
+        }
+
+        if ($object instanceof ProxyInterface) {
+            $object->__setInitialized($properties);
+        }
+    }
+}
+

+ 16 - 82
src/Hydrator/EntityHydrator.php

@@ -13,107 +13,41 @@ use PhpDevCommunity\PaperORM\Mapping\OneToMany;
 use PhpDevCommunity\PaperORM\Proxy\ProxyInterface;
 use ReflectionClass;
 
-final class EntityHydrator
+final class EntityHydrator extends AbstractEntityHydrator
 {
-    private ?EntityMemcachedCache $cache;
+    private EntityMemcachedCache $cache;
 
-    public function __construct(EntityMemcachedCache $cache = null)
+    public function __construct(EntityMemcachedCache $cache)
     {
         $this->cache = $cache;
     }
 
-    public function hydrate($objectOrClass, array $data): object
+    protected function instantiate(string $class, array $data): object
     {
-        if (!is_subclass_of($objectOrClass, EntityInterface::class)) {
-            throw new LogicException('Class ' . $objectOrClass . ' is not an PhpDevCommunity\PaperORM\Entity\EntityInterface');
-        }
-        $object = $objectOrClass;
-        if (is_string($objectOrClass)) {
-            if (!class_exists($objectOrClass)) {
-                throw new LogicException('Class ' . $objectOrClass . ' does not exist');
-            }
-            $primaryKeyColumn = ColumnMapper::getPrimaryKeyColumnName($object);
-            $object = $this->cache->get($objectOrClass, $data[$primaryKeyColumn]) ?: $this->createProxyObject($objectOrClass);
-            $this->cache->set($objectOrClass, $data[$primaryKeyColumn], $object);
-        }
-        $reflection = new ReflectionClass($object);
-        if ($reflection->getParentClass()) {
-            $reflection = $reflection->getParentClass();
-        }
-        $columns = array_merge(ColumnMapper::getColumns($object), ColumnMapper::getOneToManyRelations($object));
-
-        $properties = [];
-        foreach ($columns as $column) {
-
-            if ($column instanceof OneToMany || $column instanceof JoinColumn) {
-                $name = $column->getProperty();
-            } else {
-                $name = $column->getName();
-            }
-
-            if (!array_key_exists($name, $data)) {
-                continue;
-            }
-            $value = $data[$name];
-
-            if (!$column instanceof OneToMany) {
-                $properties[$column->getProperty()] = $column;
-            }
-
-            $property = $reflection->getProperty($column->getProperty());
-            $property->setAccessible(true);
-            if ($column instanceof JoinColumn) {
-                if (!is_array($value) && $value !== null) {
-                    $value = null;
-                }
-                $entityName = $column->getTargetEntity();
-                if (is_array($value)) {
-                    $value = $this->hydrate($entityName, $value);
-                }
-                $property->setValue($object, $value);
-                continue;
-            } elseif ($column instanceof OneToMany) {
-                if (!is_array($value)) {
-                    $value = [];
-                }
-                $entityName = $column->getTargetEntity();
-                $storage = $property->getValue($object) ?: new ObjectStorage();
-                foreach ($value as $item) {
-                    $storage->add($this->hydrate($entityName, $item));
-                }
-                $property->setValue($object, $storage);
-                continue;
-            }
+        $primaryKey = ColumnMapper::getPrimaryKeyColumnName($class);
 
-            $property->setValue($object, $column->convertToPHP($value));
-        }
+        $object = $this->cache->get($class, $data[$primaryKey])
+            ?: $this->createProxy($class);
 
-        if ($object instanceof ProxyInterface) {
-            $object->__setInitialized($properties);
-        }
+        $this->cache->set($class, $data[$primaryKey], $object);
 
         return $object;
     }
 
-    private function createProxyObject(string $class)
+    private function createProxy(string $class): object
     {
-        if (!class_exists($class)) {
-            throw new InvalidArgumentException("Class $class does not exist.");
-        }
-
-
-        $sanitizedClass = str_replace('\\', '_', $class);
-        $proxyClass = 'Proxy_' . $sanitizedClass;
+        $sanitized = str_replace('\\', '_', $class);
+        $proxyClass = 'Proxy_' . $sanitized;
 
         if (!class_exists($proxyClass)) {
             eval("
-            class $proxyClass extends \\$class implements \\PhpDevCommunity\\PaperORM\\Proxy\\ProxyInterface {
-                use \\PhpDevCommunity\\PaperORM\\Proxy\\ProxyInitializedTrait;
-            }
-        ");
+                class $proxyClass extends \\$class implements \\PhpDevCommunity\\PaperORM\\Proxy\\ProxyInterface {
+                    use \\PhpDevCommunity\\PaperORM\\Proxy\\ProxyInitializedTrait;
+                }
+            ");
         }
 
         return new $proxyClass();
     }
-
 }
+

+ 11 - 0
src/Hydrator/ReadOnlyEntityHydrator.php

@@ -0,0 +1,11 @@
+<?php
+
+namespace PhpDevCommunity\PaperORM\Hydrator;
+
+final class ReadOnlyEntityHydrator extends AbstractEntityHydrator
+{
+    protected function instantiate(string $class, array $data): object
+    {
+        return new $class();
+    }
+}

+ 1 - 1
src/Mapping/Entity.php

@@ -10,7 +10,7 @@ final class Entity
 
     public function __construct( string $table, ?string $repository = null)
     {
-        $this->table = $table;
+        $this->table = trim($table, '`');
         $this->repositoryClass = $repository;
     }
 

+ 1 - 1
src/Migration/PaperMigration.php

@@ -48,7 +48,7 @@ final class PaperMigration
     private function __construct(EntityManagerInterface $em, string $tableName, string $directory)
     {
         $this->em = $em;
-        $this->platform = $em->createDatabasePlatform();
+        $this->platform = $em->getPlatform();
         $this->tableName = $tableName;
         $this->directory = new MigrationDirectory($directory);
     }

+ 0 - 1
src/Parser/DSNParser.php

@@ -46,7 +46,6 @@ final class DSNParser
 
         $options = [];
         if (isset($params['query'])) {
-            parse_str("mysql://user:pass@host/db?charset=utf8mb4&driverClass=App\Database\Driver\MyFancyDriver", $options);
             parse_str($params['query'], $options);
             unset($params['query']);
         }

+ 148 - 0
src/Persistence/EntityPersistence.php

@@ -0,0 +1,148 @@
+<?php
+
+namespace PhpDevCommunity\PaperORM\Persistence;
+
+use PhpDevCommunity\PaperORM\Entity\EntityInterface;
+use PhpDevCommunity\PaperORM\Event\PreCreateEvent;
+use PhpDevCommunity\PaperORM\Event\PreUpdateEvent;
+use PhpDevCommunity\PaperORM\Hydrator\EntityHydrator;
+use PhpDevCommunity\PaperORM\Hydrator\ReadOnlyEntityHydrator;
+use PhpDevCommunity\PaperORM\Mapper\ColumnMapper;
+use PhpDevCommunity\PaperORM\Mapper\EntityMapper;
+use PhpDevCommunity\PaperORM\Platform\PlatformInterface;
+use PhpDevCommunity\PaperORM\Proxy\ProxyInterface;
+use PhpDevCommunity\PaperORM\Serializer\SerializerToDb;
+use PhpDevCommunity\Sql\QueryBuilder;
+use Psr\EventDispatcher\EventDispatcherInterface;
+
+class EntityPersistence
+{
+    private PlatformInterface $platform;
+
+    private ?EventDispatcherInterface $dispatcher;
+    public function __construct(PlatformInterface $platform, EventDispatcherInterface $dispatcher = null)
+    {
+        $this->platform = $platform;
+        $this->dispatcher = $dispatcher;
+    }
+
+    public function insert(object $entity): int
+    {
+        /**
+         * @var EntityInterface $entity
+         */
+        $this->checkEntity($entity);
+        if ($entity->getPrimaryKeyValue() !== null) {
+            throw new \LogicException(static::class . sprintf(' Cannot insert an entity %s with a primary key ', get_class($entity)));
+        }
+
+        if ($this->dispatcher) {
+            $this->dispatcher->dispatch(new PreCreateEvent($entity));
+        }
+        $schema = $this->platform->getSchema();
+        $tableName = EntityMapper::getTable($entity);
+        $qb = QueryBuilder::insert($schema->quote($tableName));
+
+        $values = [];
+        foreach ((new SerializerToDb($entity))->serialize() as $key => $value) {
+            $qb->setValue($schema->quote($key), ":$key");
+            $values[$key] = $value;
+        }
+        $conn = $this->platform->getConnection();
+        $rows = $conn->executeStatement($qb, $values);
+        $lastInsertId = $conn->getPdo()->lastInsertId();
+        if ($rows > 0) {
+            $primaryKeyColumn = ColumnMapper::getPrimaryKeyColumnName($entity);
+            (new ReadOnlyEntityHydrator())->hydrate($entity, [$primaryKeyColumn => $lastInsertId]);
+        }
+        return $rows;
+    }
+
+    public function update(object $entity): int
+    {
+        /**
+         * @var ProxyInterface|EntityInterface $entity
+         */
+        $this->checkEntity($entity, true);
+        if ($entity->getPrimaryKeyValue() === null) {
+            throw new \LogicException(static::class . sprintf(' Cannot update an entity %s without a primary key ', get_class($entity)));
+        }
+
+        if (!$entity->__wasModified()) {
+            return 0;
+        }
+
+        if ($this->dispatcher) {
+            $this->dispatcher->dispatch(new PreUpdateEvent($entity));
+        }
+        $tableName = EntityMapper::getTable($entity);
+        $schema = $this->platform->getSchema();
+        $qb = QueryBuilder::update($schema->quote($tableName))
+            ->where(
+                sprintf('%s = %s',
+                    $schema->quote(ColumnMapper::getPrimaryKeyColumnName($entity)),
+                    $entity->getPrimaryKeyValue()
+                )
+            );
+        $values = [];
+        foreach ((new SerializerToDb($entity))->serialize($entity->__getPropertiesModified()) as $key => $value) {
+            $qb->set($schema->quote($key), ":$key");
+            $values[$key] = $value;
+        }
+        $conn = $this->platform->getConnection();
+        $rows = $conn->executeStatement($qb, $values);
+        if ($rows > 0) {
+            $entity->__reset();
+        }
+        return $rows;
+    }
+
+    public function delete(object $entity): int
+    {
+        /**
+         * @var ProxyInterface|EntityInterface $entity
+         */
+        $this->checkEntity($entity, true);
+        if ($entity->getPrimaryKeyValue() === null) {
+            throw new \LogicException(static::class . sprintf(' Cannot delete an entity %s without a primary key ', get_class($entity)));
+        }
+
+        $tableName = EntityMapper::getTable($entity);
+        $schema = $this->platform->getSchema();
+        $qb = QueryBuilder::delete($schema->quote($tableName))
+            ->where(
+                sprintf('%s = %s',
+                    $schema->quote(ColumnMapper::getPrimaryKeyColumnName($entity)),
+                    $entity->getPrimaryKeyValue()
+                )
+            );
+
+        $conn = $this->platform->getConnection();
+        $rows = $conn->executeStatement($qb);
+        if ($rows > 0) {
+            $entity->__destroy();
+        }
+        return $rows;
+    }
+
+    private function checkEntity(object $entity, bool $proxy = false): void
+    {
+        if (!$entity instanceof EntityInterface) {
+            throw new \LogicException(sprintf(
+                'Invalid entity of type "%s". Expected an instance of "%s".',
+                get_class($entity),
+                EntityInterface::class
+            ));
+        }
+
+        if ($proxy && (!$entity instanceof ProxyInterface || !$entity->__isInitialized())) {
+            throw new \LogicException(sprintf(
+                'Entity of type "%s" is not a valid initialized proxy (expected instance of "%s").',
+                get_class($entity),
+                ProxyInterface::class
+            ));
+        }
+    }
+
+
+}

+ 7 - 2
src/Platform/MariaDBPlatform.php

@@ -265,7 +265,7 @@ class MariaDBPlatform extends AbstractPlatform
                 'args' => []
             ],
             JsonColumn::class => [
-                'type' => 'JSON',
+                'type' => 'LONGTEXT',
                 'args' => []
             ],
             StringColumn::class => [
@@ -301,13 +301,18 @@ class MariaDBPlatform extends AbstractPlatform
         return false;
     }
 
+    public function getConnection(): PaperConnection
+    {
+        return $this->connection;
+    }
+
     public function executeStatement(string $sql): int
     {
         $result = 0;
         foreach (explode(';', $sql) as $stmt) {
             $stmt = trim($stmt);
             if (!empty($stmt)) {
-                $result += $this->connection->executeStatement($stmt);
+                $result += $this->getConnection()->executeStatement($stmt);
             }
         }
         return $result;

+ 3 - 0
src/Platform/PlatformInterface.php

@@ -7,6 +7,7 @@ use PhpDevCommunity\PaperORM\Metadata\ForeignKeyMetadata;
 use PhpDevCommunity\PaperORM\Metadata\DatabaseSchemaDiffMetadata;
 use PhpDevCommunity\PaperORM\Metadata\ColumnMetadata;
 use PhpDevCommunity\PaperORM\Metadata\IndexMetadata;
+use PhpDevCommunity\PaperORM\PaperConnection;
 use PhpDevCommunity\PaperORM\Schema\SchemaInterface;
 
 /**
@@ -94,5 +95,7 @@ interface PlatformInterface
     public function diff(string $tableName, array $columns, array $indexes): DatabaseSchemaDiffMetadata;
     public function getSchema(): SchemaInterface;
     public function supportsTransactionalDDL(): bool;
+
+    public function getConnection(): PaperConnection;
 }
 

+ 5 - 0
src/Platform/SqlitePlatform.php

@@ -309,4 +309,9 @@ class SqlitePlatform extends AbstractPlatform
     {
        return true;
     }
+
+    public function getConnection(): PaperConnection
+    {
+        return $this->connection;
+    }
 }

+ 11 - 2
src/Query/Fetcher.php

@@ -69,10 +69,10 @@ final class Fetcher
     public function toArray(): ?array
     {
         if ($this->collection) {
-            return $this->queryBuilder->getResult($this->arguments, false);
+            return $this->queryBuilder->getResult($this->arguments, QueryBuilder::HYDRATE_ARRAY);
         }
 
-        return $this->queryBuilder->getOneOrNullResult($this->arguments, false);
+        return $this->queryBuilder->getOneOrNullResult($this->arguments, QueryBuilder::HYDRATE_ARRAY);
     }
 
     public function toObject()
@@ -84,6 +84,15 @@ final class Fetcher
         return $this->queryBuilder->getOneOrNullResult($this->arguments);
     }
 
+    public function toReadOnlyObject()
+    {
+        if ($this->collection) {
+            return $this->queryBuilder->getResult($this->arguments, QueryBuilder::HYDRATE_OBJECT_READONLY);
+        }
+
+        return $this->queryBuilder->getOneOrNullResult($this->arguments,QueryBuilder::HYDRATE_OBJECT_READONLY);
+    }
+
     private function joinRelation(string $type, string $expression): void
     {
         $alias = $this->queryBuilder->getPrimaryAlias();

+ 30 - 16
src/Query/QueryBuilder.php

@@ -4,20 +4,29 @@ namespace PhpDevCommunity\PaperORM\Query;
 
 use InvalidArgumentException;
 use LogicException;
-use PhpDevCommunity\PaperORM\EntityManager;
+use PhpDevCommunity\PaperORM\Cache\EntityMemcachedCache;
 use PhpDevCommunity\PaperORM\EntityManagerInterface;
 use PhpDevCommunity\PaperORM\Hydrator\ArrayHydrator;
 use PhpDevCommunity\PaperORM\Hydrator\EntityHydrator;
+use PhpDevCommunity\PaperORM\Hydrator\ReadOnlyEntityHydrator;
 use PhpDevCommunity\PaperORM\Mapper\ColumnMapper;
 use PhpDevCommunity\PaperORM\Mapper\EntityMapper;
 use PhpDevCommunity\PaperORM\Mapping\Column\Column;
 use PhpDevCommunity\PaperORM\Mapping\Column\JoinColumn;
 use PhpDevCommunity\PaperORM\Mapping\OneToMany;
+use PhpDevCommunity\PaperORM\Platform\PlatformInterface;
+use PhpDevCommunity\PaperORM\Schema\SchemaInterface;
 use PhpDevCommunity\Sql\QL\JoinQL;
 
 final class QueryBuilder
 {
-    private EntityManagerInterface $em;
+
+    public const HYDRATE_OBJECT = 'object';
+    public const HYDRATE_OBJECT_READONLY = 'readonly';
+    public const HYDRATE_ARRAY = 'array';
+    private PlatformInterface $platform;
+    private SchemaInterface $schema;
+    private EntityMemcachedCache $cache;
 
     private string $primaryKey;
 
@@ -33,30 +42,32 @@ final class QueryBuilder
 
     public function __construct(EntityManagerInterface $em, string $primaryKey = 'id')
     {
-        $this->em = $em;
+        $this->platform = $em->getPlatform();
+        $this->schema = $this->platform->getSchema();;
+        $this->cache = $em->getCache();
         $this->aliasGenerator = new AliasGenerator();
         $this->primaryKey = $primaryKey;
     }
 
-    public function getResultIterator(array $parameters = [], bool $objectHydrator = true): iterable
+    public function getResultIterator(array $parameters = [], string $hydrationMode = self::HYDRATE_OBJECT): iterable
     {
         foreach ($this->buildSqlQuery()->getResultIterator($parameters) as $item) {
-            yield $this->hydrate([$item], $objectHydrator)[0];
+            yield $this->hydrate([$item], $hydrationMode)[0];
         }
     }
 
-    public function getResult(array $parameters = [], bool $objectHydrator = true): array
+    public function getResult(array $parameters = [],  string $hydrationMode = self::HYDRATE_OBJECT): array
     {
-        return $this->hydrate($this->buildSqlQuery()->getResult($parameters), $objectHydrator);
+        return $this->hydrate($this->buildSqlQuery()->getResult($parameters), $hydrationMode);
     }
 
-    public function getOneOrNullResult(array $parameters = [], bool $objectHydrator = true)
+    public function getOneOrNullResult(array $parameters = [], string $hydrationMode = self::HYDRATE_OBJECT)
     {
         $item = $this->buildSqlQuery()->getOneOrNullResult($parameters);
         if ($item === null) {
             return null;
         }
-        return $this->hydrate([$item], $objectHydrator)[0];
+        return $this->hydrate([$item], $hydrationMode)[0];
     }
 
     public function select(string $entityName, array $properties = []): self
@@ -197,7 +208,7 @@ final class QueryBuilder
                 throw new InvalidArgumentException("Property {$propertyName} not found in class " . $entityName);
             }
 
-            $columns[] = $column->getName();
+            $columns[] = $this->schema->quote($column->getName());
         }
         return $columns;
     }
@@ -267,8 +278,8 @@ final class QueryBuilder
             $properties = ColumnMapper::getColumns($entityName);
         }
         $columns = $this->convertPropertiesToColumns($entityName, $properties);
-        $joinQl = new JoinQL($this->em->getConnection()->getPdo(), $this->primaryKey);
-        $joinQl->select($table, $alias, $columns);
+        $joinQl = new JoinQL($this->platform->getConnection()->getPdo(), $this->primaryKey);
+        $joinQl->select($this->schema->quote($table), $alias, $columns);
         foreach ($this->joins as $join) {
             $fromAlias = $join['fromAlias'];
             $targetTable = $join['targetTable'];
@@ -301,6 +312,7 @@ final class QueryBuilder
                 }
             }
             $joinConditions = [];
+            $targetTable = $this->schema->quote($targetTable);
             foreach ($criteria as $key => $value) {
                 $value = "$alias.$value";
                 $joinConditions[] = "$fromAlias.$key = $value";
@@ -364,12 +376,14 @@ final class QueryBuilder
         return $entityName;
     }
 
-    private function hydrate(array $data, bool $objectHydrator = true): array
+    private function hydrate(array $data, string $hydrationMode): array
     {
-        if (!$objectHydrator) {
+        if ($hydrationMode === self::HYDRATE_ARRAY) {
             $hydrator = new ArrayHydrator();
+        } elseif ($hydrationMode === self::HYDRATE_OBJECT_READONLY) {
+            $hydrator = new ReadOnlyEntityHydrator();
         } else {
-            $hydrator = new EntityHydrator($this->em->getCache());
+            $hydrator = new EntityHydrator($this->cache);
         }
         $collection = [];
         foreach ($data as $item) {
@@ -388,7 +402,7 @@ final class QueryBuilder
                 if ($column === null) {
                     throw new InvalidArgumentException(sprintf('Property %s not found in class %s or is a collection and cannot be used in an expression', $property, $fromEntityName));
                 }
-                $expression = str_replace($alias . '.' . $property, $alias . '.'.$column->getName(), $expression);
+                $expression = str_replace($alias . '.' . $property, $this->schema->quote($alias) . '.'.$this->schema->quote($column->getName()), $expression);
             }
 
         }

+ 17 - 105
src/Repository/Repository.php

@@ -3,31 +3,29 @@
 namespace PhpDevCommunity\PaperORM\Repository;
 
 use InvalidArgumentException;
-use LogicException;
-use PhpDevCommunity\Listener\EventDispatcher;
 use PhpDevCommunity\PaperORM\Entity\EntityInterface;
 use PhpDevCommunity\PaperORM\EntityManagerInterface;
-use PhpDevCommunity\PaperORM\Event\PreCreateEvent;
-use PhpDevCommunity\PaperORM\Event\PreUpdateEvent;
 use PhpDevCommunity\PaperORM\Expression\Expr;
 use PhpDevCommunity\PaperORM\Hydrator\EntityHydrator;
 use PhpDevCommunity\PaperORM\Mapper\ColumnMapper;
 use PhpDevCommunity\PaperORM\Mapper\EntityMapper;
-use PhpDevCommunity\PaperORM\Proxy\ProxyInterface;
+use PhpDevCommunity\PaperORM\Persistence\EntityPersistence;
+use PhpDevCommunity\PaperORM\Platform\PlatformInterface;
 use PhpDevCommunity\PaperORM\Query\Fetcher;
 use PhpDevCommunity\PaperORM\Query\QueryBuilder;
-use PhpDevCommunity\PaperORM\Serializer\SerializerToDb;
 use Psr\EventDispatcher\EventDispatcherInterface;
 
 abstract class Repository
 {
     private EntityManagerInterface $em;
-    private ?EventDispatcherInterface $dispatcher;
+    private PlatformInterface $platform;
+    private EntityPersistence $ep;
 
     public function __construct(EntityManagerInterface $em, EventDispatcherInterface $dispatcher = null)
     {
         $this->em = $em;
-        $this->dispatcher = $dispatcher;
+        $this->platform = $em->getPlatform();
+        $this->ep = new EntityPersistence($this->platform, $dispatcher);
     }
 
     /**
@@ -48,12 +46,6 @@ abstract class Repository
      */
     abstract public function getEntityName(): string;
 
-    public function find(int $pk): Fetcher
-    {
-        $entityName = $this->getEntityName();
-        $primaryKeyColumn = ColumnMapper::getPrimaryKeyColumnName($entityName);
-        return $this->findBy()->where(Expr::equal($primaryKeyColumn, $pk))->first();
-    }
 
     public function findBy(array $arguments = []): Fetcher
     {
@@ -87,111 +79,31 @@ abstract class Repository
         return $this->findBy($arguments)->first();
     }
 
-    public function insert(object $entityToInsert): int
+    public function find(int $pk): Fetcher
     {
-        $this->checkEntity($entityToInsert);
-        if ($entityToInsert->getPrimaryKeyValue() !== null) {
-            throw new LogicException(static::class . sprintf(' Cannot insert an entity %s with a primary key ', get_class($entityToInsert)));
-        }
-
-        if ($this->dispatcher && $entityToInsert instanceof EntityInterface) {
-            $this->dispatcher->dispatch(new PreCreateEvent($entityToInsert));
-        }
-        $qb = \PhpDevCommunity\Sql\QueryBuilder::insert($this->getTableName());
+        $entityName = $this->getEntityName();
+        $primaryKeyColumn = ColumnMapper::getPrimaryKeyColumnName($entityName);
+        return $this->findOneBy([$primaryKeyColumn => $pk]);
+    }
 
-        $values = [];
-        foreach ((new SerializerToDb($entityToInsert))->serialize() as $key => $value) {
-            $keyWithoutBackticks = str_replace("`", "", $key);
-            $qb->setValue($key, ":$keyWithoutBackticks");
-            $values[$keyWithoutBackticks] = $value;
-        }
-        $rows = $this->em->getConnection()->executeStatement($qb, $values);
-        $lastInsertId = $this->em->getConnection()->getPdo()->lastInsertId();
-        if ($rows > 0) {
-            $primaryKeyColumn = ColumnMapper::getPrimaryKeyColumnName($entityToInsert);
-            (new EntityHydrator($this->em->getCache()))->hydrate($entityToInsert, [$primaryKeyColumn => $lastInsertId]);
-        }
-        return $rows;
+    public function insert(object $entityToInsert): int
+    {
+        return $this->ep->insert($entityToInsert);
     }
 
     public function update(object $entityToUpdate): int
     {
-        $this->checkEntity($entityToUpdate, true);
-        if ($entityToUpdate->getPrimaryKeyValue() === null) {
-            throw new LogicException(static::class . sprintf(' Cannot update an entity %s without a primary key ', get_class($entityToUpdate)));
-        }
-
-        /**
-         * @var ProxyInterface|EntityInterface $entityToUpdate
-         */
-        if (!$entityToUpdate->__wasModified()) {
-            return 0;
-        }
-
-        if ($this->dispatcher && $entityToUpdate instanceof EntityInterface) {
-            $this->dispatcher->dispatch(new PreUpdateEvent($entityToUpdate));
-        }
-        $qb = \PhpDevCommunity\Sql\QueryBuilder::update($this->getTableName())
-            ->where(
-                sprintf('`%s` = %s',
-                    ColumnMapper::getPrimaryKeyColumnName($this->getEntityName()),
-                    $entityToUpdate->getPrimaryKeyValue()
-                )
-            );
-        $values = [];
-        foreach ((new SerializerToDb($entityToUpdate))->serialize($entityToUpdate->__getPropertiesModified()) as $key => $value) {
-            $keyWithoutBackticks = str_replace("`", "", $key);
-            $qb->set($key, ":$keyWithoutBackticks");
-            $values[$keyWithoutBackticks] = $value;
-        }
-        $rows = $this->em->getConnection()->executeStatement($qb, $values);
-        if ($rows > 0) {
-            $entityToUpdate->__reset();
-        }
-        return $rows;
-
+        return $this->ep->update($entityToUpdate);
     }
 
     public function delete(object $entityToDelete): int
     {
-        /**
-         * @var ProxyInterface|EntityInterface $entityToUpdate
-         */
-        $this->checkEntity($entityToDelete, true);
-        if ($entityToDelete->getPrimaryKeyValue() === null) {
-            throw new LogicException(static::class . sprintf(' Cannot delete an entity %s without a primary key ', get_class($entityToDelete)));
-        }
-
-        $qb = \PhpDevCommunity\Sql\QueryBuilder::delete($this->getTableName())
-            ->where(
-                sprintf('`%s` = %s',
-                    ColumnMapper::getPrimaryKeyColumnName($this->getEntityName()),
-                    $entityToDelete->getPrimaryKeyValue()
-                )
-            );
-
-        $rows = $this->em->getConnection()->executeStatement($qb);
-        if ($rows > 0) {
-            $entityToDelete->__destroy();
-        }
-        return $rows;
+        return $this->ep->delete($entityToDelete);
     }
 
     public function qb(): QueryBuilder
     {
         $queryBuilder = new QueryBuilder($this->em);
-        return $queryBuilder->select($this->getEntityName(), []);
-    }
-
-    private function checkEntity(object $entity, bool $proxy = false): void
-    {
-        $entityName = $this->getEntityName();
-        if (!$entity instanceof $entityName) {
-            throw new LogicException($entityName . ' Cannot insert an entity of type ' . get_class($entity));
-        }
-
-        if ($proxy && (!$entity instanceof ProxyInterface || !$entity->__isInitialized())) {
-            throw new LogicException($entityName . ' Cannot use an entity is not a proxy');
-        }
+        return $queryBuilder->select($this->getEntityName());
     }
 }

+ 36 - 30
src/Schema/MariaDBSchema.php

@@ -3,17 +3,18 @@
 namespace PhpDevCommunity\PaperORM\Schema;
 
 use LogicException;
-use PhpDevCommunity\PaperORM\Mapping\Column\PrimaryKeyColumn;
-use PhpDevCommunity\PaperORM\Metadata\ForeignKeyMetadata;
 use PhpDevCommunity\PaperORM\Metadata\ColumnMetadata;
+use PhpDevCommunity\PaperORM\Metadata\ForeignKeyMetadata;
 use PhpDevCommunity\PaperORM\Metadata\IndexMetadata;
+use PhpDevCommunity\PaperORM\Schema\Traits\IdentifierQuotingTrait;
 
 class MariaDBSchema implements SchemaInterface
 {
 
+    use IdentifierQuotingTrait;
     public function showDatabases(): string
     {
-        return  "SHOW DATABASES";
+        return "SHOW DATABASES";
     }
 
     public function showTables(): string
@@ -54,7 +55,7 @@ class MariaDBSchema implements SchemaInterface
 
     public function showTableIndexes(string $tableName): string
     {
-        return sprintf('SHOW INDEXES FROM %s', $tableName);
+        return sprintf('SHOW INDEXES FROM %s', $this->quote($tableName));
     }
 
     public function createDatabase(string $databaseName): string
@@ -64,7 +65,7 @@ class MariaDBSchema implements SchemaInterface
 
     public function createDatabaseIfNotExists(string $databaseName): string
     {
-        return  sprintf('CREATE DATABASE IF NOT EXISTS %s', $databaseName);
+        return sprintf('CREATE DATABASE IF NOT EXISTS %s', $databaseName);
     }
 
     public function dropDatabase(string $databaseName): string
@@ -82,16 +83,16 @@ class MariaDBSchema implements SchemaInterface
     {
         $lines = [];
         foreach ($columns as $columnMetadata) {
-            $line = sprintf('%s %s', $columnMetadata->getName(), $columnMetadata->getTypeWithAttributes());
+            $line = sprintf('%s %s', $this->quote($columnMetadata->getName()), $columnMetadata->getTypeWithAttributes());
             if ($columnMetadata->isPrimary()) {
                 $line .= ' AUTO_INCREMENT PRIMARY KEY NOT NULL';
-            }else {
+            } else {
                 if (!$columnMetadata->isNullable()) {
                     $line .= ' NOT NULL';
                 }
                 if ($columnMetadata->getDefaultValue() !== null) {
                     $line .= sprintf(' DEFAULT %s', $columnMetadata->getDefaultValuePrintable());
-                }elseif ($columnMetadata->isNullable()) {
+                } elseif ($columnMetadata->isNullable()) {
                     $line .= ' DEFAULT NULL';
                 }
             }
@@ -101,7 +102,7 @@ class MariaDBSchema implements SchemaInterface
 
 
         $linesString = implode(',', $lines);
-        $createTable = sprintf("CREATE TABLE $tableName (%s)", $linesString);
+        $createTable = sprintf("CREATE TABLE %s (%s)", $this->quote($tableName), $linesString);
 
         $indexesSql = [];
         $options['indexes'] = $options['indexes'] ?? [];
@@ -109,7 +110,7 @@ class MariaDBSchema implements SchemaInterface
             $indexesSql[] = $this->createIndex($index);
         }
 
-        return $createTable.';'.implode(';', $indexesSql);
+        return $createTable . ';' . implode(';', $indexesSql);
     }
 
     public function createTableIfNotExists(string $tableName, array $columns, array $options = []): string
@@ -127,11 +128,11 @@ class MariaDBSchema implements SchemaInterface
         }
 
         $sql[] = sprintf('ALTER TABLE %s ADD CONSTRAINT %s FOREIGN KEY (%s) REFERENCES %s(%s)',
-            $tableName,
+            $this->quote($tableName),
             $foreignKey->getName(),
-            implode(', ', $foreignKey->getColumns()),
-            $foreignKey->getReferenceTable(),
-            implode(', ', $foreignKey->getReferenceColumns())
+            implode(', ',  $this->quotes($foreignKey->getColumns())),
+            $this->quote($foreignKey->getReferenceTable()),
+            implode(', ',  $this->quotes($foreignKey->getReferenceColumns())),
         );
 
         switch ($foreignKey->getOnDelete()) {
@@ -164,34 +165,34 @@ class MariaDBSchema implements SchemaInterface
                 break;
         }
 
-        return implode(' ', $sql).';';
+        return implode(' ', $sql) . ';';
     }
 
-    public function dropForeignKeyConstraints( string $tableName, string $foreignKeyName): string
+    public function dropForeignKeyConstraints(string $tableName, string $foreignKeyName): string
     {
-        return sprintf('ALTER TABLE %s DROP FOREIGN KEY %s', $tableName, $foreignKeyName);
+        return sprintf('ALTER TABLE %s DROP FOREIGN KEY %s', $this->quote($tableName), $foreignKeyName);
     }
 
     public function dropTable(string $tableName): string
     {
-        return sprintf('DROP TABLE %s', $tableName);
+        return sprintf('DROP TABLE %s', $this->quote($tableName));
     }
 
     public function renameTable(string $oldTableName, string $newTableName): string
     {
-        return sprintf('ALTER TABLE %s RENAME TO %s', $oldTableName, $newTableName);
+        return sprintf('ALTER TABLE %s RENAME TO %s', $this->quote($oldTableName), $this->quote($newTableName));
     }
 
     public function addColumn(string $tableName, ColumnMetadata $columnMetadata): string
     {
-        $sql =  sprintf('ALTER TABLE %s ADD COLUMN %s %s', $tableName, $columnMetadata->getName(), $columnMetadata->getTypeWithAttributes());
+        $sql = sprintf('ALTER TABLE %s ADD COLUMN %s %s', $this->quote($tableName), $this->quote($columnMetadata->getName()), $columnMetadata->getTypeWithAttributes());
         if (!$columnMetadata->isNullable()) {
             $sql .= ' NOT NULL';
         }
 
         if ($columnMetadata->getDefaultValue() !== null) {
             $sql .= sprintf(' DEFAULT %s', $columnMetadata->getDefaultValuePrintable());
-        }elseif ($columnMetadata->isNullable()) {
+        } elseif ($columnMetadata->isNullable()) {
             $sql .= ' DEFAULT NULL';
         }
 
@@ -200,18 +201,18 @@ class MariaDBSchema implements SchemaInterface
 
     public function dropColumn(string $tableName, ColumnMetadata $columnMetadata): string
     {
-        return sprintf('ALTER TABLE %s DROP COLUMN %s', $tableName, $columnMetadata->getName());
+        return sprintf('ALTER TABLE %s DROP COLUMN %s', $this->quote($tableName), $this->quote($columnMetadata->getName()));
     }
 
     public function renameColumn(string $tableName, string $oldColumnName, string $newColumnName): string
     {
-        return sprintf('ALTER TABLE %s RENAME COLUMN %s to %s', $tableName, $oldColumnName, $newColumnName);
+        return sprintf('ALTER TABLE %s RENAME COLUMN %s to %s', $this->quote($tableName), $this->quote($oldColumnName), $this->quote($newColumnName));
     }
 
     public function modifyColumn(string $tableName, ColumnMetadata $columnMetadata): string
     {
         $sql = $this->addColumn($tableName, $columnMetadata);
-        return  str_replace('ADD COLUMN', 'MODIFY COLUMN', $sql);
+        return str_replace('ADD COLUMN', 'MODIFY COLUMN', $sql);
     }
 
     /**
@@ -221,17 +222,17 @@ class MariaDBSchema implements SchemaInterface
     public function createIndex(IndexMetadata $indexMetadata): string
     {
         $indexType = $indexMetadata->isUnique() ? 'CREATE UNIQUE INDEX' : 'CREATE INDEX';
-        return  sprintf('%s %s ON %s (%s)',
+        return sprintf('%s %s ON %s (%s)',
             $indexType,
             $indexMetadata->getName(),
-            $indexMetadata->getTableName(),
-            implode(', ', $indexMetadata->getColumns())
+            $this->quote($indexMetadata->getTableName()),
+            implode(', ', $this->quotes($indexMetadata->getColumns())),
         );
     }
 
     public function dropIndex(IndexMetadata $indexMetadata): string
     {
-        return sprintf('DROP INDEX %s ON %s;', $indexMetadata->getName(), $indexMetadata->getTableName());
+        return sprintf('DROP INDEX %s ON %s;', $indexMetadata->getName(), $this->quote($indexMetadata->getTableName()));
     }
 
 
@@ -245,10 +246,9 @@ class MariaDBSchema implements SchemaInterface
         return 'Y-m-d';
     }
 
-
     public function supportsForeignKeyConstraints(): bool
     {
-       return true;
+        return true;
     }
 
     public function supportsIndexes(): bool
@@ -280,4 +280,10 @@ class MariaDBSchema implements SchemaInterface
     {
         return true;
     }
+
+    public function getIdentifierQuoteSymbols(): array
+    {
+        return ['`', '`'];
+    }
+
 }

+ 31 - 0
src/Schema/SchemaInterface.php

@@ -214,4 +214,35 @@ interface SchemaInterface
 
     public function supportsDropForeignKey(): bool;
 
+    /**
+     * Quotes (escapes) a SQL identifier (e.g. table name, column name)
+     * according to the current database dialect rules.
+     *
+     * @param string $identifier
+     *   The raw SQL identifier (table name, column name, index, schema, …).
+     *   It must be passed without any quoting characters.
+     *
+     * @return string
+     *   The quoted identifier, wrapped with the correct quoting characters
+     *   for the active SQL dialect:
+     *     - MySQL/MariaDB: `identifier`
+     *     - PostgreSQL/SQLite: "identifier"
+     *     - SQL Server: [identifier]
+     *
+     *   Any quote characters inside the identifier itself will be escaped
+     *   according to the rules of the current dialect.
+     */
+    public function quote(string $identifier): string;
+
+    /**
+     * Returns the opening and closing symbols used to quote identifiers.
+     *
+     * @return array{0:string,1:string}
+     *   [openingSymbol, closingSymbol]
+     *   Examples:
+     *     - MySQL/MariaDB: ['`', '`']
+     *     - PostgreSQL/SQLite: ['`', '`']
+     *     - SQL Server: ['[', ']']
+     */
+    public function getIdentifierQuoteSymbols(): array;
 }

+ 25 - 9
src/Schema/SqliteSchema.php

@@ -6,11 +6,14 @@ use LogicException;
 use PhpDevCommunity\PaperORM\Metadata\ForeignKeyMetadata;
 use PhpDevCommunity\PaperORM\Metadata\ColumnMetadata;
 use PhpDevCommunity\PaperORM\Metadata\IndexMetadata;
+use PhpDevCommunity\PaperORM\Schema\Traits\IdentifierQuotingTrait;
 use SQLite3;
 
 class SqliteSchema implements SchemaInterface
 {
 
+    use IdentifierQuotingTrait;
+
     public function showDatabases(): string
     {
         throw new LogicException(sprintf("The method '%s' is not supported by the schema interface.", __METHOD__));
@@ -62,7 +65,7 @@ class SqliteSchema implements SchemaInterface
         $lines = [];
         $foreignKeys = [];
         foreach ($columns as $columnMetadata) {
-            $line = sprintf('%s %s', $columnMetadata->getName(), $columnMetadata->getTypeWithAttributes());
+            $line = sprintf('%s %s', $this->quote($columnMetadata->getName()), $columnMetadata->getTypeWithAttributes());
             if ($columnMetadata->isPrimary()) {
                 $line .= ' PRIMARY KEY AUTOINCREMENT';
             }
@@ -86,7 +89,7 @@ class SqliteSchema implements SchemaInterface
 
         $linesString = implode(',', $lines);
 
-        $createTable = sprintf("CREATE TABLE $tableName (%s)", $linesString);
+        $createTable = sprintf("CREATE TABLE %s (%s)", $this->quote($tableName), $linesString);
 
         $indexesSql = [];
         foreach ($options['indexes'] as $index) {
@@ -114,17 +117,17 @@ class SqliteSchema implements SchemaInterface
 
     public function dropTable(string $tableName): string
     {
-        return sprintf('DROP TABLE %s', $tableName);
+        return sprintf('DROP TABLE %s', $this->quote($tableName));
     }
 
     public function renameTable(string $oldTableName, string $newTableName): string
     {
-        return sprintf('ALTER TABLE %s RENAME TO %s', $oldTableName, $newTableName);
+        return sprintf('ALTER TABLE %s RENAME TO %s', $this->quote($oldTableName), $this->quote($newTableName));
     }
 
     public function addColumn(string $tableName, ColumnMetadata $columnMetadata): string
     {
-        $sql = sprintf('ALTER TABLE %s ADD %s %s', $tableName, $columnMetadata->getName(), $columnMetadata->getTypeWithAttributes());
+        $sql = sprintf('ALTER TABLE %s ADD %s %s', $this->quote($tableName), $this->quote($columnMetadata->getName()), $columnMetadata->getTypeWithAttributes());
 
         if (!$columnMetadata->isNullable()) {
             $sql .= ' NOT NULL';
@@ -142,12 +145,12 @@ class SqliteSchema implements SchemaInterface
         if (!$this->supportsDropColumn()) {
             throw new LogicException(sprintf("The method '%s' is not supported with SQLite versions older than 3.35.0.", __METHOD__));
         }
-        return sprintf('ALTER TABLE %s DROP COLUMN %s', $tableName, $columnMetadata->getName());
+        return sprintf('ALTER TABLE %s DROP COLUMN %s', $this->quote($tableName), $this->quote($columnMetadata->getName()));
     }
 
     public function renameColumn(string $tableName, string $oldColumnName, string $newColumnName): string
     {
-        return sprintf('ALTER TABLE %s RENAME COLUMN %s to %s', $tableName, $oldColumnName, $newColumnName);
+        return sprintf('ALTER TABLE %s RENAME COLUMN %s to %s', $this->quote($tableName), $this->quote($oldColumnName), $this->quote($newColumnName));
     }
 
     public function modifyColumn(string $tableName, ColumnMetadata $columnMetadata): string
@@ -161,7 +164,12 @@ class SqliteSchema implements SchemaInterface
      */
     public function createIndex(IndexMetadata $indexMetadata): string
     {
-        $sql = sprintf('CREATE INDEX %s ON %s (%s)', $indexMetadata->getName(), $indexMetadata->getTableName(), implode(', ', $indexMetadata->getColumns()));
+        $sql = sprintf('CREATE INDEX %s ON %s (%s)',
+            $indexMetadata->getName(),
+            $this->quote($indexMetadata->getTableName()),
+            implode(', ', $this->quotes($indexMetadata->getColumns()))
+        );
+
         if ($indexMetadata->isUnique()) {
             $sql = str_replace('CREATE INDEX', 'CREATE UNIQUE INDEX', $sql);
         }
@@ -189,7 +197,11 @@ class SqliteSchema implements SchemaInterface
         $referencedTable = $foreignKey->getReferenceTable();
         $referencedColumns = $foreignKey->getReferenceColumns();
         $sql = [];
-        $sql[] = sprintf('FOREIGN KEY (%s) REFERENCES %s (%s)', implode(', ', $foreignKey->getColumns()), $referencedTable, implode(', ', $referencedColumns));
+        $sql[] = sprintf('FOREIGN KEY (%s) REFERENCES %s (%s)',
+            implode(', ', $this->quotes($foreignKey->getColumns())),
+            $this->quote($referencedTable),
+            implode(', ', $referencedColumns)
+        );
 
         switch ($foreignKey->getOnDelete()) {
             case ForeignKeyMetadata::RESTRICT:
@@ -266,4 +278,8 @@ class SqliteSchema implements SchemaInterface
         return false;
     }
 
+    public function getIdentifierQuoteSymbols(): array
+    {
+        return ['`', '`'];
+    }
 }

+ 26 - 0
src/Schema/Traits/IdentifierQuotingTrait.php

@@ -0,0 +1,26 @@
+<?php
+
+namespace PhpDevCommunity\PaperORM\Schema\Traits;
+
+trait IdentifierQuotingTrait
+{
+
+    public function quotes(array $collection): array
+    {
+        return array_map([$this, 'quote'], $collection);
+    }
+    public function quote(string $identifier): string
+    {
+        $identifiers = $this->getIdentifierQuoteSymbols();
+        if (count($identifiers) != 2) {
+            throw new \LogicException('The method getIdentifierQuoteSymbols() must be an array with 2 elements, ex : ["`", "`"] : ' . __CLASS__);
+        }
+        [$open, $close] = $identifiers;
+        if (strlen($identifier) > 2 && $identifier[0] === $open && $identifier[strlen($identifier) - 1] === $close) {
+            return $identifier;
+        }
+        return $open . $identifier . $close;
+    }
+
+    abstract public function getIdentifierQuoteSymbols(): array;
+}

+ 1 - 1
src/Serializer/SerializerToDb.php

@@ -41,7 +41,7 @@ final class SerializerToDb
             }
 
             $property->setAccessible(true);
-            $key = sprintf('`%s`', $column->getName());
+            $key = $column->getName();
             $value = $property->getValue($entity);
             if ($column instanceof JoinColumn) {
                 if (is_object($value) && ($value instanceof EntityInterface || method_exists($value, 'getPrimaryKeyValue'))) {

+ 1 - 1
tests/DatabaseShowTablesCommandTest.php

@@ -39,7 +39,7 @@ class DatabaseShowTablesCommandTest extends TestCase
 
     private function executeTest(EntityManager $em)
     {
-        $platform = $em->createDatabasePlatform();
+        $platform = $em->getPlatform();
         $platform->createDatabaseIfNotExists();
         $platform->dropDatabase();
         $platform->createDatabaseIfNotExists();

+ 2 - 2
tests/Factory/DatabaseConnectionFactory.php

@@ -21,7 +21,7 @@ final class DatabaseConnectionFactory
                 return new EntityManager([
                     'driver' => 'pdo_mysql',
                     'host' => 'localhost',
-                    'dbname' => 'test_db',
+                    'path' => 'test_db',
                     'user' => 'root',
                     'password' => '',
                 ]);
@@ -29,4 +29,4 @@ final class DatabaseConnectionFactory
                 throw new \InvalidArgumentException("Database driver '$driver' not supported");
         }
     }
-}
+}

+ 1 - 1
tests/Helper/DataBaseHelperTest.php

@@ -67,7 +67,7 @@ class DataBaseHelperTest
             (new JoinColumn('post_id', PostTest::class, 'id', true, false, JoinColumn::SET_NULL)),
             new StringColumn('body'),
         ];
-        $platform = $entityManager->createDatabasePlatform();
+        $platform = $entityManager->getPlatform();
         $platform->createDatabaseIfNotExists();
         $platform->dropDatabase();
         $platform->createDatabaseIfNotExists();

+ 35 - 43
tests/MigrationTest.php

@@ -36,7 +36,7 @@ class MigrationTest extends TestCase
         foreach (DataBaseHelperTest::drivers() as $params) {
             $em = new EntityManager($params);
             $paperMigration = PaperMigration::create($em, 'mig_versions', $this->migrationDir);
-            $platform = $em->createDatabasePlatform();
+            $platform = $em->getPlatform();
             $platform->createDatabaseIfNotExists();
             $platform->dropDatabase();
             $platform->createDatabaseIfNotExists();
@@ -63,36 +63,36 @@ class MigrationTest extends TestCase
         switch (get_class($driver)) {
             case SqliteDriver::class:
                 $lines = file($migrationFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
-                $this->assertEquals($lines, array (
+                $this->assertEquals($lines, array(
                     0 => '-- UP MIGRATION --',
-                    1 => 'CREATE TABLE user (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,firstname VARCHAR(255) NOT NULL,lastname VARCHAR(255) NOT NULL,email VARCHAR(255) NOT NULL,password VARCHAR(255) NOT NULL,is_active BOOLEAN NOT NULL,created_at DATETIME,last_post_id INTEGER,FOREIGN KEY (last_post_id) REFERENCES post (id) ON DELETE SET NULL ON UPDATE NO ACTION);',
-                    2 => 'CREATE UNIQUE INDEX IX_8D93D6492D053F64 ON user (last_post_id);',
-                    3 => 'CREATE TABLE post (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,title VARCHAR(255) NOT NULL,content VARCHAR(255) NOT NULL,created_at DATETIME NOT NULL,user_id INTEGER,FOREIGN KEY (user_id) REFERENCES user (id) ON DELETE SET NULL ON UPDATE NO ACTION);',
-                    4 => 'CREATE INDEX IX_5A8A6C8DA76ED395 ON post (user_id);',
+                    1 => 'CREATE TABLE `user` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,`firstname` VARCHAR(255) NOT NULL,`lastname` VARCHAR(255) NOT NULL,`email` VARCHAR(255) NOT NULL,`password` VARCHAR(255) NOT NULL,`is_active` BOOLEAN NOT NULL,`created_at` DATETIME,`last_post_id` INTEGER,FOREIGN KEY (`last_post_id`) REFERENCES `post` (id) ON DELETE SET NULL ON UPDATE NO ACTION);',
+                    2 => 'CREATE UNIQUE INDEX IX_8D93D6492D053F64 ON `user` (`last_post_id`);',
+                    3 => 'CREATE TABLE `post` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,`title` VARCHAR(255) NOT NULL,`content` VARCHAR(255) NOT NULL,`created_at` DATETIME NOT NULL,`user_id` INTEGER,FOREIGN KEY (`user_id`) REFERENCES `user` (id) ON DELETE SET NULL ON UPDATE NO ACTION);',
+                    4 => 'CREATE INDEX IX_5A8A6C8DA76ED395 ON `post` (`user_id`);',
                     5 => '-- DOWN MIGRATION --',
                     6 => 'DROP INDEX IX_8D93D6492D053F64;',
-                    7 => 'DROP TABLE user;',
+                    7 => 'DROP TABLE `user`;',
                     8 => 'DROP INDEX IX_5A8A6C8DA76ED395;',
-                    9 => 'DROP TABLE post;',
+                    9 => 'DROP TABLE `post`;',
                 ));
                 break;
             case MariaDBDriver::class:
                 $lines = file($migrationFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
-                $this->assertEquals($lines, array (
+                $this->assertEquals($lines, array(
                     0 => '-- UP MIGRATION --',
-                    1 => 'CREATE TABLE user (id INT(11) AUTO_INCREMENT PRIMARY KEY NOT NULL,firstname VARCHAR(255) NOT NULL,lastname VARCHAR(255) NOT NULL,email VARCHAR(255) NOT NULL,password VARCHAR(255) NOT NULL,is_active TINYINT(1) NOT NULL,created_at DATETIME DEFAULT NULL,last_post_id INT(11) DEFAULT NULL);',
-                    2 => 'CREATE UNIQUE INDEX IX_8D93D6492D053F64 ON user (last_post_id);',
-                    3 => 'CREATE TABLE post (id INT(11) AUTO_INCREMENT PRIMARY KEY NOT NULL,title VARCHAR(255) NOT NULL,content VARCHAR(255) NOT NULL,created_at DATETIME NOT NULL,user_id INT(11) DEFAULT NULL);',
-                    4 => 'CREATE INDEX IX_5A8A6C8DA76ED395 ON post (user_id);',
-                    5 => 'ALTER TABLE user ADD CONSTRAINT FK_8D93D6492D053F64 FOREIGN KEY (last_post_id) REFERENCES post(id) ON DELETE SET NULL ON UPDATE NO ACTION;',
-                    6 => 'ALTER TABLE post ADD CONSTRAINT FK_5A8A6C8DA76ED395 FOREIGN KEY (user_id) REFERENCES user(id) ON DELETE SET NULL ON UPDATE NO ACTION;',
+                    1 => 'CREATE TABLE `user` (`id` INT(11) AUTO_INCREMENT PRIMARY KEY NOT NULL,`firstname` VARCHAR(255) NOT NULL,`lastname` VARCHAR(255) NOT NULL,`email` VARCHAR(255) NOT NULL,`password` VARCHAR(255) NOT NULL,`is_active` TINYINT(1) NOT NULL,`created_at` DATETIME DEFAULT NULL,`last_post_id` INT(11) DEFAULT NULL);',
+                    2 => 'CREATE UNIQUE INDEX IX_8D93D6492D053F64 ON `user` (`last_post_id`);',
+                    3 => 'CREATE TABLE `post` (`id` INT(11) AUTO_INCREMENT PRIMARY KEY NOT NULL,`title` VARCHAR(255) NOT NULL,`content` VARCHAR(255) NOT NULL,`created_at` DATETIME NOT NULL,`user_id` INT(11) DEFAULT NULL);',
+                    4 => 'CREATE INDEX IX_5A8A6C8DA76ED395 ON `post` (`user_id`);',
+                    5 => 'ALTER TABLE `user` ADD CONSTRAINT FK_8D93D6492D053F64 FOREIGN KEY (`last_post_id`) REFERENCES `post`(`id`) ON DELETE SET NULL ON UPDATE NO ACTION;',
+                    6 => 'ALTER TABLE `post` ADD CONSTRAINT FK_5A8A6C8DA76ED395 FOREIGN KEY (`user_id`) REFERENCES `user`(`id`) ON DELETE SET NULL ON UPDATE NO ACTION;',
                     7 => '-- DOWN MIGRATION --',
-                    8 => 'ALTER TABLE user DROP FOREIGN KEY FK_8D93D6492D053F64;',
-                    9 => 'ALTER TABLE post DROP FOREIGN KEY FK_5A8A6C8DA76ED395;',
-                    10 => 'DROP INDEX IX_8D93D6492D053F64 ON user;',
-                    11 => 'DROP TABLE user;',
-                    12 => 'DROP INDEX IX_5A8A6C8DA76ED395 ON post;',
-                    13 => 'DROP TABLE post;',
+                    8 => 'ALTER TABLE `user` DROP FOREIGN KEY FK_8D93D6492D053F64;',
+                    9 => 'ALTER TABLE `post` DROP FOREIGN KEY FK_5A8A6C8DA76ED395;',
+                    10 => 'DROP INDEX IX_8D93D6492D053F64 ON `user`;',
+                    11 => 'DROP TABLE `user`;',
+                    12 => 'DROP INDEX IX_5A8A6C8DA76ED395 ON `post`;',
+                    13 => 'DROP TABLE `post`;',
                 ));
                 break;
             default:
@@ -101,7 +101,7 @@ class MigrationTest extends TestCase
 
     }
 
-    private function testExecute(PaperMigration  $paperMigration): void
+    private function testExecute(PaperMigration $paperMigration): void
     {
         $paperMigration->migrate();
         $successList = $paperMigration->getSuccessList();
@@ -111,7 +111,7 @@ class MigrationTest extends TestCase
         $this->assertNull($migrationFile);
     }
 
-    private function testColumnModification(PaperMigration  $paperMigration): void
+    private function testColumnModification(PaperMigration $paperMigration): void
     {
         $em = $paperMigration->getEntityManager();
         $driver = $em->getConnection()->getDriver();
@@ -134,20 +134,12 @@ class MigrationTest extends TestCase
                 $schema = $driver->createDatabaseSchema();
                 $lines = file($migrationFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
                 if ($schema->supportsDropColumn()) {
-                    $this->assertEquals($lines, array (
-                        0 => '-- UP MIGRATION --',
-                        1 => 'ALTER TABLE user ADD childs INTEGER NOT NULL DEFAULT 0;',
-                        2 => 'CREATE UNIQUE INDEX IX_8D93D649E7927C74 ON user (email);',
-                        3 => '-- Modify column email is not supported with PhpDevCommunity\\PaperORM\\Schema\\SqliteSchema. Consider creating a new column and migrating the data.;',
-                        4 => '-- DOWN MIGRATION --',
-                        5 => 'ALTER TABLE user DROP COLUMN childs;',
-                        6 => 'DROP INDEX IX_8D93D649E7927C74;',
-                    ));
+                    $this->assertEquals($lines, array());
                 } else {
-                    $this->assertEquals($lines, array (
+                    $this->assertEquals($lines, array(
                         0 => '-- UP MIGRATION --',
-                        1 => 'ALTER TABLE user ADD childs INTEGER NOT NULL DEFAULT 0;',
-                        2 => 'CREATE UNIQUE INDEX IX_8D93D649E7927C74 ON user (email);',
+                        1 => 'ALTER TABLE `user` ADD `childs` INTEGER NOT NULL DEFAULT 0;',
+                        2 => 'CREATE UNIQUE INDEX IX_8D93D649E7927C74 ON `user` (`email`);',
                         3 => '-- Modify column email is not supported with PhpDevCommunity\\PaperORM\\Schema\\SqliteSchema. Consider creating a new column and migrating the data.;',
                         4 => '-- DOWN MIGRATION --',
                         5 => '-- Drop column childs is not supported with PhpDevCommunity\\PaperORM\\Schema\\SqliteSchema. You might need to manually drop the column.;',
@@ -159,13 +151,13 @@ class MigrationTest extends TestCase
                 $lines = file($migrationFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
                 $this->assertEquals($lines, array (
                     0 => '-- UP MIGRATION --',
-                    1 => 'ALTER TABLE user ADD COLUMN childs INT(11) NOT NULL DEFAULT 0;',
-                    2 => 'CREATE UNIQUE INDEX IX_8D93D649E7927C74 ON user (email);',
-                    3 => 'ALTER TABLE user MODIFY COLUMN email VARCHAR(255) DEFAULT NULL;',
+                    1 => 'ALTER TABLE `user` ADD COLUMN `childs` INT(11) NOT NULL DEFAULT 0;',
+                    2 => 'CREATE UNIQUE INDEX IX_8D93D649E7927C74 ON `user` (`email`);',
+                    3 => 'ALTER TABLE `user` MODIFY COLUMN `email` VARCHAR(255) DEFAULT NULL;',
                     4 => '-- DOWN MIGRATION --',
-                    5 => 'ALTER TABLE user DROP COLUMN childs;',
-                    6 => 'DROP INDEX IX_8D93D649E7927C74 ON user;',
-                    7 => 'ALTER TABLE user MODIFY COLUMN email VARCHAR(255) NOT NULL;',
+                    5 => 'ALTER TABLE `user` DROP COLUMN `childs`;',
+                    6 => 'DROP INDEX IX_8D93D649E7927C74 ON `user`;',
+                    7 => 'ALTER TABLE `user` MODIFY COLUMN `email` VARCHAR(255) NOT NULL;',
                 ));
                 break;
             default:
@@ -174,11 +166,11 @@ class MigrationTest extends TestCase
 
     }
 
-    private function testFailedMigration(PaperMigration  $paperMigration): void
+    private function testFailedMigration(PaperMigration $paperMigration): void
     {
         $paperMigration->generateMigration();
 
-        $this->expectException(RuntimeException::class, function () use ($paperMigration){
+        $this->expectException(RuntimeException::class, function () use ($paperMigration) {
             $paperMigration->migrate();
         });
         $successList = $paperMigration->getSuccessList();

+ 1 - 1
tests/PlatformDiffTest.php

@@ -33,7 +33,7 @@ class PlatformDiffTest extends TestCase
 
     private function executeTest(EntityManager  $em)
     {
-        $platform = $em->createDatabasePlatform();
+        $platform = $em->getPlatform();
         $platform->createDatabaseIfNotExists();
         $platform->dropDatabase();
         $platform->createDatabaseIfNotExists();

+ 9 - 9
tests/PlatformTest.php

@@ -40,7 +40,7 @@ class PlatformTest extends TestCase
     {
         $em->getConnection()->close();
         $em->getConnection()->connect();
-        $platform = $em->createDatabasePlatform();
+        $platform = $em->getPlatform();
         $platform->createDatabaseIfNotExists();
         $platform->dropDatabase();
         $platform->createDatabaseIfNotExists();
@@ -70,12 +70,12 @@ class PlatformTest extends TestCase
     {
         $em->getConnection()->close();
         $em->getConnection()->connect();
-        $platform = $em->createDatabasePlatform();
+        $platform = $em->getPlatform();
         $platform->createDatabaseIfNotExists();
         $platform->dropDatabase();
         $platform->createDatabaseIfNotExists();
 
-        $platform = $em->createDatabasePlatform();
+        $platform = $em->getPlatform();
         $platform->createTable('user', [
             new PrimaryKeyColumn('id'),
             new StringColumn('firstname'),
@@ -92,14 +92,14 @@ class PlatformTest extends TestCase
 
     public function testDropColumn(EntityManager $em)
     {
-        $platform = $em->createDatabasePlatform();
+        $platform = $em->getPlatform();
         if ($platform->getSchema()->supportsDropColumn() === false) {
             return;
         }
 
         $em->getConnection()->close();
         $em->getConnection()->connect();
-        $platform = $em->createDatabasePlatform();
+        $platform = $em->getPlatform();
         $platform->createDatabaseIfNotExists();
         $platform->dropDatabase();
         $platform->createDatabaseIfNotExists();
@@ -122,12 +122,12 @@ class PlatformTest extends TestCase
     {
         $em->getConnection()->close();
         $em->getConnection()->connect();
-        $platform = $em->createDatabasePlatform();
+        $platform = $em->getPlatform();
         $platform->createDatabaseIfNotExists();
         $platform->dropDatabase();
         $platform->createDatabaseIfNotExists();
 
-        $platform = $em->createDatabasePlatform();
+        $platform = $em->getPlatform();
         $platform->createTable('user', [
             new PrimaryKeyColumn('id'),
             new StringColumn('firstname'),
@@ -146,12 +146,12 @@ class PlatformTest extends TestCase
     {
         $em->getConnection()->close();
         $em->getConnection()->connect();
-        $platform = $em->createDatabasePlatform();
+        $platform = $em->getPlatform();
         $platform->createDatabaseIfNotExists();
         $platform->dropDatabase();
         $platform->createDatabaseIfNotExists();
 
-        $platform = $em->createDatabasePlatform();
+        $platform = $em->getPlatform();
         $platform->createTable('user', [
             new PrimaryKeyColumn('id'),
             new StringColumn('firstname'),

+ 37 - 0
tests/RepositoryTest.php

@@ -3,6 +3,7 @@
 namespace Test\PhpDevCommunity\PaperORM;
 
 use PhpDevCommunity\PaperORM\EntityManager;
+use PhpDevCommunity\PaperORM\Proxy\ProxyInterface;
 use PhpDevCommunity\UniTester\TestCase;
 use Test\PhpDevCommunity\PaperORM\Entity\PostTest;
 use Test\PhpDevCommunity\PaperORM\Entity\UserTest;
@@ -25,6 +26,7 @@ class RepositoryTest extends TestCase
             $em = new EntityManager($params);
             DataBaseHelperTest::init($em);
             $this->testSelectWithoutJoin($em);
+            $this->testSelectWithoutProxy($em);
             $this->testSelectInnerJoin($em);
             $this->testSelectLeftJoin($em);
             $em->getConnection()->close();
@@ -76,6 +78,41 @@ class RepositoryTest extends TestCase
         }
     }
 
+    public function testSelectWithoutProxy(EntityManager $em): void
+    {
+        $userRepository = $em->getRepository(UserTest::class);
+        $users = $userRepository->findBy()
+            ->with(PostTest::class)
+            ->orderBy('id', 'DESC')
+            ->toReadOnlyObject();
+
+
+        foreach ($users as $user) {
+            $this->assertInstanceOf(UserTest::class, $user);
+            $this->assertTrue(!$user instanceof ProxyInterface);
+            $this->assertTrue(!$user->getLastPost() instanceof ProxyInterface);
+            foreach ($user->getPosts() as $post) {
+                $this->assertTrue(!$post instanceof ProxyInterface);
+            }
+        }
+
+        $users = $userRepository->findBy()
+            ->with(PostTest::class)
+            ->orderBy('id', 'DESC')
+            ->toObject();
+
+        foreach ($users as $user) {
+            $this->assertInstanceOf(UserTest::class, $user);
+            $this->assertInstanceOf(ProxyInterface::class, $user);
+            if ($user->getLastPost()) {
+                $this->assertInstanceOf(ProxyInterface::class, $user->getLastPost());
+            }
+            foreach ($user->getPosts() as $post) {
+                $this->assertInstanceOf(ProxyInterface::class, $post);
+            }
+        }
+    }
+
     public function testSelectInnerJoin(EntityManager $em): void
     {
         $userRepository = $em->getRepository(UserTest::class);