Kaynağa Gözat

add auto-handling of createdAt/updatedAt timestamps

phpdevcommunity 1 ay önce
ebeveyn
işleme
de4a0553f6

+ 1 - 1
README.md

@@ -21,7 +21,7 @@ PaperORM is available via **Composer** and installs in seconds.
 
 ### 📦 Via Composer (recommended)
 ```bash
-composer require phpdevcommunity/paper-orm:1.0.6-alpha
+composer require phpdevcommunity/paper-orm:1.0.7-alpha
 ```  
 
 ### 🔧 Minimal Configuration

+ 2 - 1
composer.json

@@ -27,7 +27,8 @@
     "phpdevcommunity/php-console": "^1.0",
     "phpdevcommunity/michel-package-starter": "^1.0",
     "phpdevcommunity/php-filesystem": "^1.0",
-    "psr/log": "^1.1|^2.0|^3.0"
+    "psr/log": "^1.1|^2.0|^3.0",
+    "phpdevcommunity/psr14-event-dispatcher": "^1.0"
   },
   "require-dev": {
     "phpdevcommunity/unitester": "^0.1.0@alpha"

+ 41 - 19
src/Command/Migration/MigrationDiffCommand.php

@@ -8,6 +8,7 @@ use PhpDevCommunity\Console\Option\CommandOption;
 use PhpDevCommunity\Console\Output\ConsoleOutput;
 use PhpDevCommunity\Console\OutputInterface;
 use PhpDevCommunity\FileSystem\Tools\FileExplorer;
+use PhpDevCommunity\PaperORM\Entity\EntityInterface;
 use PhpDevCommunity\PaperORM\Migration\PaperMigration;
 
 class MigrationDiffCommand implements CommandInterface
@@ -70,8 +71,8 @@ class MigrationDiffCommand implements CommandInterface
         $files = $explorer->searchByExtension('php', true);
         $entities = [];
         foreach ($files as $file) {
-            $entityClass = self::getFullClassName($file['path']);
-            if ($entityClass !== null) {
+            $entityClass = self::extractNamespaceAndClass($file['path']);
+            if ($entityClass !== null && class_exists($entityClass) && is_subclass_of($entityClass, EntityInterface::class)) {
                 $entities[$file['path']] = $entityClass;
             }
         }
@@ -98,31 +99,52 @@ class MigrationDiffCommand implements CommandInterface
         $io->success('Migration file successfully generated: ' . $file);
     }
 
-    private static function getFullClassName($file): ?string
+    private static function extractNamespaceAndClass(string $filePath): ?string
     {
-        $content = file_get_contents($file);
-        $tokens = token_get_all($content);
-        $namespace = $className = '';
-
-        foreach ($tokens as $i => $token) {
-            if ($token[0] === T_NAMESPACE) {
-                for ($j = $i + 1; isset($tokens[$j]); $j++) {
-                    if ($tokens[$j] === ';') break;
-                    if (is_array($tokens[$j]) && in_array($tokens[$j][0], [T_STRING, T_NS_SEPARATOR])) {
-                        $namespace .= $tokens[$j][1];
-                    }
+        if (!file_exists($filePath)) {
+            throw new \InvalidArgumentException('File not found: ' . $filePath);
+        }
+
+        $contents = file_get_contents($filePath);
+        $namespace = '';
+        $class = '';
+        $isExtractingNamespace = false;
+        $isExtractingClass = false;
+
+        foreach (token_get_all($contents) as $token) {
+            if (is_array($token) && $token[0] == T_NAMESPACE) {
+                $isExtractingNamespace = true;
+            }
+
+            if (is_array($token) && $token[0] == T_CLASS) {
+                $isExtractingClass = true;
+            }
+
+            if ($isExtractingNamespace) {
+                if (is_array($token) && in_array($token[0], [T_STRING, T_NS_SEPARATOR,  265 /* T_NAME_QUALIFIED For PHP 8*/])) {
+                    $namespace .= $token[1];
+                } else if ($token === ';') {
+                    $isExtractingNamespace = false;
                 }
             }
 
-            if ($token[0] === T_CLASS && isset($tokens[$i + 2][1])) {
-                $className = $tokens[$i + 2][1];
-                break;
+            if ($isExtractingClass) {
+                if (is_array($token) && $token[0] == T_STRING) {
+                    $class = $token[1];
+                    break;
+                }
             }
         }
-        if (empty($className)) {
+
+        if (empty($class)) {
             return null;
         }
 
-        return trim($namespace . '\\' . $className, '\\');
+        $fullClass = $namespace ? $namespace . '\\' . $class : $class;
+        if (class_exists($fullClass) && is_subclass_of($fullClass, EntityInterface::class)) {
+            return $fullClass;
+        }
+
+        return null;
     }
 }

+ 41 - 7
src/EntityManager.php

@@ -2,12 +2,20 @@
 
 namespace PhpDevCommunity\PaperORM;
 
+use PhpDevCommunity\Listener\EventDispatcher;
+use PhpDevCommunity\Listener\ListenerProvider;
 use PhpDevCommunity\PaperORM\Cache\EntityMemcachedCache;
 use PhpDevCommunity\PaperORM\Driver\DriverManager;
+use PhpDevCommunity\PaperORM\Event\PreCreateEvent;
+use PhpDevCommunity\PaperORM\Event\PreUpdateEvent;
+use PhpDevCommunity\PaperORM\EventListener\CreatedAtListener;
+use PhpDevCommunity\PaperORM\EventListener\UpdatedAtListener;
 use PhpDevCommunity\PaperORM\Mapper\EntityMapper;
 use PhpDevCommunity\PaperORM\Parser\DSNParser;
 use PhpDevCommunity\PaperORM\Platform\PlatformInterface;
 use PhpDevCommunity\PaperORM\Repository\Repository;
+use Psr\EventDispatcher\EventDispatcherInterface;
+use Psr\EventDispatcher\ListenerProviderInterface;
 use Psr\Log\LoggerInterface;
 
 class EntityManager implements EntityManagerInterface
@@ -23,7 +31,10 @@ class EntityManager implements EntityManagerInterface
 
     private EntityMemcachedCache $cache;
 
-    public static function createFromDsn(string $dsn, bool $debug = false, LoggerInterface $logger = null): self
+    private ListenerProviderInterface $listener;
+    private EventDispatcherInterface $dispatcher;
+
+    public static function createFromDsn(string $dsn, bool $debug = false, LoggerInterface $logger = null, array $listeners = []): self
     {
         if (empty($dsn)) {
             throw new \LogicException('Cannot create an EntityManager from an empty DSN.');
@@ -33,15 +44,30 @@ class EntityManager implements EntityManagerInterface
         if ($logger !== null) {
             $params['extra']['logger'] = $logger;
         }
+        $params['extra']['listeners'] = $listeners;
         return new self($params);
     }
 
     public function __construct(array $config = [])
     {
-        $driver = $config['driver'];
-        $this->connection = DriverManager::createConnection($driver, $config);
+        if (!isset($config['driver'])) {
+            throw new \InvalidArgumentException('Missing "driver" in EntityManager configuration.');
+        }
+
+        $this->connection = DriverManager::createConnection($config['driver'], $config);
         $this->unitOfWork = new UnitOfWork();
         $this->cache = new EntityMemcachedCache();
+        $this->listener = (new ListenerProvider())
+            ->addListener(PreCreateEvent::class, new CreatedAtListener())
+            ->addListener(PreUpdateEvent::class, new UpdatedAtListener());
+
+        $listeners = $config['extra']['listeners'] ?? [];
+        foreach ((array) $listeners as $event => $listener) {
+            foreach ((array) $listener as $l) {
+                $this->addEventListener($event, $l);
+            }
+        }
+        $this->dispatcher = new EventDispatcher($this->listener);
     }
 
     public function persist(object $entity): void
@@ -83,14 +109,16 @@ class EntityManager implements EntityManagerInterface
         if ($repositoryName === null) {
             $repositoryName = 'ProxyRepository'.$entity;
         }
+
+        $dispatcher = $this->dispatcher;
         if (!isset($this->repositories[$repositoryName])) {
             if (!class_exists($repositoryName)) {
-                $repository = new class($entity, $this) extends Repository
+                $repository = new class($entity, $this, $dispatcher) extends Repository
                 {
                     private string $entityName;
-                    public function __construct($entityName, EntityManager $em)  {
+                    public function __construct($entityName, EntityManager $em, EventDispatcherInterface $dispatcher = null)  {
                         $this->entityName = $entityName;
-                        parent::__construct($em);
+                        parent::__construct($em, $dispatcher);
                     }
 
                     public function getEntityName(): string
@@ -99,7 +127,7 @@ class EntityManager implements EntityManagerInterface
                     }
                 };
             }else {
-                $repository = new $repositoryName($this);
+                $repository = new $repositoryName($this, $dispatcher);
             }
             $this->repositories[$repositoryName] = $repository;
         }
@@ -130,4 +158,10 @@ class EntityManager implements EntityManagerInterface
         $this->getCache()->clear();
     }
 
+    public function addEventListener(string $eventType, callable $callable): self
+    {
+        $this->listener->addListener($eventType, $callable);
+        return $this;
+    }
+
 }

+ 29 - 0
src/Event/PreCreateEvent.php

@@ -0,0 +1,29 @@
+<?php
+
+namespace PhpDevCommunity\PaperORM\Event;
+
+use PhpDevCommunity\Listener\Event;
+use PhpDevCommunity\PaperORM\Entity\EntityInterface;
+
+class PreCreateEvent extends Event
+{
+
+    private EntityInterface $entity;
+
+    /**
+     * PreCreateEvent constructor.
+     *
+     * @param EntityInterface $entity
+     */
+    public function __construct(EntityInterface $entity)
+    {
+        $this->entity = $entity;
+    }
+
+
+    public function getEntity(): EntityInterface
+    {
+        return $this->entity;
+    }
+
+}

+ 29 - 0
src/Event/PreUpdateEvent.php

@@ -0,0 +1,29 @@
+<?php
+
+namespace PhpDevCommunity\PaperORM\Event;
+
+use PhpDevCommunity\Listener\Event;
+use PhpDevCommunity\PaperORM\Entity\EntityInterface;
+
+class PreUpdateEvent extends Event
+{
+
+    private EntityInterface $entity;
+
+    /**
+     * PreCreateEvent constructor.
+     *
+     * @param EntityInterface $entity
+     */
+    public function __construct(EntityInterface $entity)
+    {
+        $this->entity = $entity;
+    }
+
+
+    public function getEntity(): EntityInterface
+    {
+        return $this->entity;
+    }
+
+}

+ 35 - 0
src/EventListener/CreatedAtListener.php

@@ -0,0 +1,35 @@
+<?php
+
+namespace PhpDevCommunity\PaperORM\EventListener;
+
+use PhpDevCommunity\PaperORM\Event\PreCreateEvent;
+use PhpDevCommunity\PaperORM\Mapper\ColumnMapper;
+use PhpDevCommunity\PaperORM\Mapping\Column\Column;
+use PhpDevCommunity\PaperORM\Mapping\Column\TimestampColumn;
+
+class CreatedAtListener
+{
+    public function __invoke(PreCreateEvent $event)
+    {
+        $entity = $event->getEntity();
+
+        foreach (ColumnMapper::getColumns($entity) as $column) {
+            if ($column instanceof TimestampColumn && $column->isOnCreated()) {
+                $property = $column->getProperty();
+                $method   = "set" . ucfirst($property);
+                if (method_exists($entity, $method)) {
+                    $entity->$method(new \DateTimeImmutable('now'));
+                } elseif (array_key_exists($property, get_object_vars($entity))) {
+                    $entity->$property = new \DateTimeImmutable('now'); // OK car public
+                } else {
+                    throw new \LogicException(sprintf(
+                        'Cannot set created-at timestamp: expected setter "%s()" or a public property "%s" in entity "%s".',
+                        $method,
+                        $property,
+                        get_class($entity)
+                    ));
+                }
+            }
+        }
+    }
+}

+ 34 - 0
src/EventListener/UpdatedAtListener.php

@@ -0,0 +1,34 @@
+<?php
+
+namespace PhpDevCommunity\PaperORM\EventListener;
+
+use PhpDevCommunity\PaperORM\Event\PreUpdateEvent;
+use PhpDevCommunity\PaperORM\Mapper\ColumnMapper;
+use PhpDevCommunity\PaperORM\Mapping\Column\TimestampColumn;
+
+class UpdatedAtListener
+{
+
+    public function __invoke(PreUpdateEvent $event)
+    {
+        $entity = $event->getEntity();
+        foreach (ColumnMapper::getColumns($entity) as $column) {
+            if ($column instanceof TimestampColumn && $column->isOnUpdated()) {
+                $property = $column->getProperty();
+                $method   = "set" . ucfirst($property);
+                if (method_exists($entity, $method)) {
+                    $entity->$method(new \DateTimeImmutable('now'));
+                } elseif (array_key_exists($property, get_object_vars($entity))) {
+                    $entity->$property = new \DateTimeImmutable('now'); // OK car public
+                } else {
+                    throw new \LogicException(sprintf(
+                        'Cannot set created-at timestamp: expected setter "%s()" or a public property "%s" in entity "%s".',
+                        $method,
+                        $property,
+                        get_class($entity)
+                    ));
+                }
+            }
+        }
+    }
+}

+ 40 - 0
src/Mapping/Column/TimestampColumn.php

@@ -0,0 +1,40 @@
+<?php
+
+namespace PhpDevCommunity\PaperORM\Mapping\Column;
+
+use Attribute;
+use PhpDevCommunity\PaperORM\Types\DateTimeType;
+
+#[\Attribute(\Attribute::TARGET_PROPERTY | \Attribute::IS_REPEATABLE)]
+final class TimestampColumn extends Column
+{
+
+    private bool $onCreated;
+    private bool $onUpdated;
+    public function __construct(
+        string $name = null,
+        bool $onCreated = false,
+        bool $onUpdated = false,
+        bool   $nullable = true
+    )
+    {
+        if (!$onCreated && !$onUpdated) {
+            throw new \InvalidArgumentException(
+                'A TimestampColumn must be either onCreated or onUpdated (at least one true).'
+            );
+        }
+        parent::__construct('', $name, DateTimeType::class, $nullable);
+        $this->onCreated = $onCreated;
+        $this->onUpdated = $onUpdated;
+    }
+
+    public function isOnCreated(): bool
+    {
+        return $this->onCreated;
+    }
+
+    public function isOnUpdated(): bool
+    {
+        return $this->onUpdated;
+    }
+}

+ 5 - 0
src/Platform/MariaDBPlatform.php

@@ -16,6 +16,7 @@ use PhpDevCommunity\PaperORM\Mapping\Column\JsonColumn;
 use PhpDevCommunity\PaperORM\Mapping\Column\PrimaryKeyColumn;
 use PhpDevCommunity\PaperORM\Mapping\Column\StringColumn;
 use PhpDevCommunity\PaperORM\Mapping\Column\TextColumn;
+use PhpDevCommunity\PaperORM\Mapping\Column\TimestampColumn;
 use PhpDevCommunity\PaperORM\Metadata\ColumnMetadata;
 use PhpDevCommunity\PaperORM\Metadata\ForeignKeyMetadata;
 use PhpDevCommunity\PaperORM\Metadata\IndexMetadata;
@@ -251,6 +252,10 @@ class MariaDBPlatform extends AbstractPlatform
                 'type' => 'DATETIME',
                 'args' => []
             ],
+            TimestampColumn::class => [
+                'type' => 'DATETIME',
+                'args' => [],
+            ],
             BoolColumn::class => [
                 'type' => 'TINYINT',
                 'args' => [1]

+ 5 - 0
src/Platform/SqlitePlatform.php

@@ -16,6 +16,7 @@ use PhpDevCommunity\PaperORM\Mapping\Column\JsonColumn;
 use PhpDevCommunity\PaperORM\Mapping\Column\PrimaryKeyColumn;
 use PhpDevCommunity\PaperORM\Mapping\Column\StringColumn;
 use PhpDevCommunity\PaperORM\Mapping\Column\TextColumn;
+use PhpDevCommunity\PaperORM\Mapping\Column\TimestampColumn;
 use PhpDevCommunity\PaperORM\Metadata\ColumnMetadata;
 use PhpDevCommunity\PaperORM\Metadata\ForeignKeyMetadata;
 use PhpDevCommunity\PaperORM\Metadata\IndexMetadata;
@@ -274,6 +275,10 @@ class SqlitePlatform extends AbstractPlatform
                 'type' => 'VARCHAR',
                 'args' => [255],
             ],
+            TimestampColumn::class => [
+                'type' => 'DATETIME',
+                'args' => [],
+            ]
         ];
     }
 

+ 2 - 1
src/Proxy/ProxyInitializedTrait.php

@@ -8,6 +8,7 @@ use PhpDevCommunity\PaperORM\Mapping\Column\Column;
 use PhpDevCommunity\PaperORM\Mapping\Column\DateColumn;
 use PhpDevCommunity\PaperORM\Mapping\Column\DateTimeColumn;
 use PhpDevCommunity\PaperORM\Mapping\Column\JoinColumn;
+use PhpDevCommunity\PaperORM\Types\DateTimeType;
 
 trait ProxyInitializedTrait
 {
@@ -73,7 +74,7 @@ trait ProxyInitializedTrait
             $property = $reflection->getProperty($key);
             $property->setAccessible(true);
             $value = $property->getValue($this);
-            if ($column instanceof DateTimeColumn && $value instanceof DateTimeInterface) {
+            if ($column->getType() == DateTimeType::class && $value instanceof DateTimeInterface) {
                 $cleanedData[$key] = $value->getTimestamp();
             } elseif ($column instanceof DateColumn && $value instanceof DateTimeInterface) {
                 $cleanedData[$key] = $value;

+ 2 - 1
src/Query/AliasGenerator.php

@@ -6,10 +6,11 @@ final class AliasGenerator
 {
     private array $usedAliases = [];
 
+
     public function generateAlias(string $entity): string
     {
         $entityName = basename(str_replace('\\', '/', $entity));
-        $alias = strtolower(substr($entityName, 0, 2));
+        $alias = strtolower(substr($entityName, 0, 1));
         if (in_array($alias, $this->usedAliases)) {
             $suffix = 1;
             while (in_array($alias . $suffix, $this->usedAliases)) {

+ 33 - 3
src/Repository/Repository.php

@@ -2,10 +2,13 @@
 
 namespace PhpDevCommunity\PaperORM\Repository;
 
+use InvalidArgumentException;
 use LogicException;
+use PhpDevCommunity\Listener\EventDispatcher;
 use PhpDevCommunity\PaperORM\Entity\EntityInterface;
-use PhpDevCommunity\PaperORM\EntityManager;
 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;
@@ -14,14 +17,17 @@ use PhpDevCommunity\PaperORM\Proxy\ProxyInterface;
 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;
 
-    public function __construct(EntityManagerInterface $em)
+    public function __construct(EntityManagerInterface $em, EventDispatcherInterface $dispatcher = null)
     {
         $this->em = $em;
+        $this->dispatcher = $dispatcher;
     }
 
     /**
@@ -53,6 +59,24 @@ abstract class Repository
     {
         $expressions = [];
         foreach ($arguments as $key => $value) {
+            if ($value instanceof EntityInterface) {
+                $value = $value->getPrimaryKeyValue();
+            } elseif (is_array($value)) {
+                $expressions[] = Expr::in($key, $value);
+                continue;
+            } elseif (is_null($value)) {
+                $expressions[] = Expr::isNull($key);
+                continue;
+            }
+            elseif (is_string($value) && strtoupper($value) === "!NULL") {
+                $expressions[] = Expr::isNotNull($key);
+                continue;
+            }
+            elseif (!is_scalar($value)) {
+                throw new InvalidArgumentException(
+                    sprintf('Argument "%s" must be scalar, array, null or EntityInterface, %s given', $key, gettype($value))
+                );
+            }
             $expressions[] = Expr::equal($key, $value);
         }
         return (new Fetcher($this->qb(), true))->where(...$expressions);
@@ -70,6 +94,9 @@ abstract class Repository
             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());
 
         $values = [];
@@ -101,6 +128,9 @@ abstract class Repository
             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',
@@ -140,7 +170,7 @@ abstract class Repository
                 )
             );
 
-        $rows =  $this->em->getConnection()->executeStatement($qb);
+        $rows = $this->em->getConnection()->executeStatement($qb);
         if ($rows > 0) {
             $entityToDelete->__destroy();
         }

+ 1 - 1
src/Schema/SqliteSchema.php

@@ -64,7 +64,7 @@ class SqliteSchema implements SchemaInterface
         foreach ($columns as $columnMetadata) {
             $line = sprintf('%s %s', $columnMetadata->getName(), $columnMetadata->getTypeWithAttributes());
             if ($columnMetadata->isPrimary()) {
-                $line .= ' PRIMARY KEY';
+                $line .= ' PRIMARY KEY AUTOINCREMENT';
             }
             if (!$columnMetadata->isNullable()) {
                 $line .= ' NOT NULL';

+ 33 - 0
src/Types/BlobType.php

@@ -0,0 +1,33 @@
+<?php
+
+namespace PhpDevCommunity\PaperORM\Types;
+
+final class BlobType extends Type
+{
+    public function convertToDatabase($value): ?string
+    {
+        if ($value === null) {
+            return null;
+        }
+
+        if (is_string($value)) {
+            return $value;
+        }
+
+        throw new \LogicException('Blob must be a binary string, got '.gettype($value));
+    }
+
+    public function convertToPHP($value): ?string
+    {
+        if ($value === null) {
+            return null;
+        }
+
+        if (is_string($value)) {
+            return $value;
+        }
+
+        throw new \LogicException('Blob must be a binary string, got '.gettype($value));
+
+    }
+}

+ 7 - 6
tests/Entity/UserTest.php

@@ -2,6 +2,7 @@
 
 namespace Test\PhpDevCommunity\PaperORM\Entity;
 
+use Cassandra\Time;
 use PhpDevCommunity\PaperORM\Collection\ObjectStorage;
 use PhpDevCommunity\PaperORM\Entity\EntityInterface;
 use PhpDevCommunity\PaperORM\Mapping\Column\BoolColumn;
@@ -9,6 +10,7 @@ use PhpDevCommunity\PaperORM\Mapping\Column\StringColumn;
 use PhpDevCommunity\PaperORM\Mapping\Column\DateTimeColumn;
 use PhpDevCommunity\PaperORM\Mapping\Column\JoinColumn;
 use PhpDevCommunity\PaperORM\Mapping\Column\PrimaryKeyColumn;
+use PhpDevCommunity\PaperORM\Mapping\Column\TimestampColumn;
 use PhpDevCommunity\PaperORM\Mapping\Entity;
 use PhpDevCommunity\PaperORM\Mapping\OneToMany;
 use Test\PhpDevCommunity\PaperORM\Repository\PostTestRepository;
@@ -34,8 +36,8 @@ class UserTest implements EntityInterface
     #[BoolColumn(name: 'is_active')]
     private bool $active = false;
 
-    #[DateTimeColumn(name: 'created_at')]
-    private ?\DateTime $createdAt = null;
+    #[TimestampColumn(name: 'created_at', onCreated: true)]
+    private ?\DateTimeInterface $createdAt = null;
 
     #[OneToMany(targetEntity: PostTest::class, mappedBy: 'user')]
     private ObjectStorage $posts;
@@ -45,7 +47,6 @@ class UserTest implements EntityInterface
     public function __construct()
     {
         $this->posts = new ObjectStorage();
-        $this->createdAt = new \DateTime();
     }
 
     static public function getTableName(): string
@@ -70,7 +71,7 @@ class UserTest implements EntityInterface
             (new StringColumn())->bindProperty('email'),
             (new StringColumn())->bindProperty('password'),
             (new BoolColumn( 'is_active'))->bindProperty('active'),
-            (new DateTimeColumn( 'created_at'))->bindProperty('createdAt'),
+            (new TimestampColumn( 'created_at', true))->bindProperty('createdAt'),
             (new OneToMany( PostTest::class,  'user'))->bindProperty('posts'),
             (new JoinColumn( 'last_post_id', PostTest::class, 'id', true, true, JoinColumn::SET_NULL))->bindProperty('lastPost'),
         ];
@@ -147,12 +148,12 @@ class UserTest implements EntityInterface
         return $this;
     }
 
-    public function getCreatedAt(): ?\DateTime
+    public function getCreatedAt(): ?\DateTimeInterface
     {
         return $this->createdAt;
     }
 
-    public function setCreatedAt(?\DateTime $createdAt): UserTest
+    public function setCreatedAt(?\DateTimeInterface $createdAt): UserTest
     {
         $this->createdAt = $createdAt;
         return $this;

+ 3 - 2
tests/Helper/DataBaseHelperTest.php

@@ -10,6 +10,7 @@ use PhpDevCommunity\PaperORM\Mapping\Column\DateTimeColumn;
 use PhpDevCommunity\PaperORM\Mapping\Column\JoinColumn;
 use PhpDevCommunity\PaperORM\Mapping\Column\PrimaryKeyColumn;
 use PhpDevCommunity\PaperORM\Mapping\Column\StringColumn;
+use PhpDevCommunity\PaperORM\Mapping\Column\TimestampColumn;
 use Test\PhpDevCommunity\PaperORM\Entity\PostTest;
 use Test\PhpDevCommunity\PaperORM\Entity\UserTest;
 
@@ -46,14 +47,14 @@ class DataBaseHelperTest
             new StringColumn('email'),
             new StringColumn('password'),
             new BoolColumn('is_active'),
-            new DateTimeColumn('created_at', true),
+            new TimestampColumn('created_at', true),
         ];
         $postColumns = [
             new PrimaryKeyColumn('id'),
             (new JoinColumn('user_id', UserTest::class, 'id', true, false, JoinColumn::SET_NULL)),
             new StringColumn('title'),
             new StringColumn('content'),
-            new DateTimeColumn('created_at', true),
+            new TimestampColumn('created_at', true),
         ];
 
         $tagColumns = [

+ 3 - 3
tests/MigrationTest.php

@@ -65,9 +65,9 @@ class MigrationTest extends TestCase
                 $lines = file($migrationFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
                 $this->assertEquals($lines, array (
                     0 => '-- UP MIGRATION --',
-                    1 => 'CREATE TABLE user (id INTEGER 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 BOOLEAN NOT NULL,created_at DATETIME NOT NULL,last_post_id INTEGER,FOREIGN KEY (last_post_id) REFERENCES post (id) ON DELETE SET NULL ON UPDATE NO ACTION);',
+                    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 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);',
+                    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;',
@@ -80,7 +80,7 @@ class MigrationTest extends TestCase
                 $lines = file($migrationFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
                 $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 NOT NULL,last_post_id INT(11) DEFAULT NULL);',
+                    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);',

+ 3 - 0
tests/PersistAndFlushTest.php

@@ -48,7 +48,10 @@ class PersistAndFlushTest extends TestCase
         $user->setActive(true);
         $em->persist($user);
         $em->flush();
+
         $this->assertNotNull($user->getId());
+        $this->assertInstanceOf(\DateTimeInterface::class, $user->getCreatedAt());
+        $this->assertInstanceOf(\DateTimeInterface::class, $user->getCreatedAt());
         $em->clear();
     }