From 235d572f8c9c870a0f5971eb909cd87eafdcdd84 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20B=C3=B6hmer?= Date: Sat, 22 Jun 2024 17:36:54 +0200 Subject: [PATCH] Use DatetimeImmutable instead of DateTime wherever possible --- config/packages/doctrine.yaml | 7 ++ src/DataFixtures/APITokenFixtures.php | 2 +- src/DataFixtures/PartFixtures.php | 2 +- .../Column/LocaleDateTimeColumn.php | 2 +- .../Types/UTCDateTimeImmutableType.php | 97 +++++++++++++++++++ src/Entity/Attachments/Attachment.php | 4 +- src/Entity/Attachments/AttachmentType.php | 4 +- src/Entity/Base/AbstractCompany.php | 4 +- src/Entity/Base/TimestampTrait.php | 24 ++--- src/Entity/LogSystem/AbstractLogEntry.php | 14 +-- src/Entity/OAuthToken.php | 2 +- src/Entity/Parts/Category.php | 4 +- src/Entity/Parts/Footprint.php | 4 +- src/Entity/Parts/InfoProviderReference.php | 11 +-- src/Entity/Parts/MeasurementUnit.php | 4 +- src/Entity/Parts/Part.php | 4 +- src/Entity/Parts/PartLot.php | 12 +-- src/Entity/Parts/StorageLocation.php | 4 +- src/Entity/PriceInformations/Currency.php | 4 +- src/Entity/PriceInformations/Orderdetail.php | 6 +- src/Entity/PriceInformations/Pricedetail.php | 6 +- src/Entity/ProjectSystem/Project.php | 4 +- src/Entity/UserSystem/ApiToken.php | 22 ++--- src/Entity/UserSystem/User.php | 24 ++--- src/Entity/UserSystem/WebauthnKey.php | 3 +- src/Helpers/LabelResponse.php | 2 +- src/Security/ApiTokenAuthenticator.php | 2 +- .../PartKeeprImporter/PKImportHelperTrait.php | 2 +- .../LabelExampleElementsGenerator.php | 2 +- .../PlaceholderProviders/GlobalProviders.php | 2 +- .../TimestampableElementProvider.php | 5 +- src/Services/LogSystem/LogDataFormatter.php | 2 +- src/Services/LogSystem/TimeTravel.php | 23 ++++- src/Services/Trees/SidebarTreeUpdater.php | 2 +- .../UserSystem/PasswordResetManager.php | 7 +- tests/Entity/Parts/PartLotTest.php | 5 +- tests/Entity/Parts/PartTest.php | 2 +- tests/Entity/UserSystem/UserTest.php | 2 +- .../PartLotProviderTest.php | 2 +- 39 files changed, 222 insertions(+), 112 deletions(-) create mode 100644 src/Doctrine/Types/UTCDateTimeImmutableType.php diff --git a/config/packages/doctrine.yaml b/config/packages/doctrine.yaml index cea0126aa..0986565fc 100644 --- a/config/packages/doctrine.yaml +++ b/config/packages/doctrine.yaml @@ -9,10 +9,17 @@ doctrine: # either here or in the DATABASE_URL env var (see .env file) types: + # UTC datetimes datetime: class: App\Doctrine\Types\UTCDateTimeType date: class: App\Doctrine\Types\UTCDateTimeType + + datetime_immutable: + class: App\Doctrine\Types\UTCDateTimeImmutableType + date_immutable: + class: App\Doctrine\Types\UTCDateTimeImmutableType + big_decimal: class: App\Doctrine\Types\BigDecimalType tinyint: diff --git a/src/DataFixtures/APITokenFixtures.php b/src/DataFixtures/APITokenFixtures.php index fbb4bdd2e..4bcf3a60f 100644 --- a/src/DataFixtures/APITokenFixtures.php +++ b/src/DataFixtures/APITokenFixtures.php @@ -75,7 +75,7 @@ public function load(ObjectManager $manager): void $expired_token->setUser($admin_user); $expired_token->setLevel(ApiTokenLevel::FULL); $expired_token->setName('expired'); - $expired_token->setValidUntil(new \DateTime('-1 day')); + $expired_token->setValidUntil(new \DateTimeImmutable('-1 day')); $this->setTokenSecret($expired_token, self::TOKEN_EXPIRED); $manager->persist($expired_token); diff --git a/src/DataFixtures/PartFixtures.php b/src/DataFixtures/PartFixtures.php index a11b11e91..b7193ec07 100644 --- a/src/DataFixtures/PartFixtures.php +++ b/src/DataFixtures/PartFixtures.php @@ -99,7 +99,7 @@ public function load(ObjectManager $manager): void $part->addPartLot($partLot1); $partLot2 = new PartLot(); - $partLot2->setExpirationDate(new DateTime()); + $partLot2->setExpirationDate(new \DateTimeImmutable()); $partLot2->setComment('Test'); $partLot2->setNeedsRefill(true); $partLot2->setStorageLocation($manager->find(StorageLocation::class, 3)); diff --git a/src/DataTables/Column/LocaleDateTimeColumn.php b/src/DataTables/Column/LocaleDateTimeColumn.php index d485913c8..e60b2867f 100644 --- a/src/DataTables/Column/LocaleDateTimeColumn.php +++ b/src/DataTables/Column/LocaleDateTimeColumn.php @@ -47,7 +47,7 @@ public function normalize($value): string } if (!$value instanceof DateTimeInterface) { - $value = new DateTime((string) $value); + $value = new \DateTimeImmutable((string) $value); } $formatValues = [ diff --git a/src/Doctrine/Types/UTCDateTimeImmutableType.php b/src/Doctrine/Types/UTCDateTimeImmutableType.php new file mode 100644 index 000000000..c0d6659dd --- /dev/null +++ b/src/Doctrine/Types/UTCDateTimeImmutableType.php @@ -0,0 +1,97 @@ +. + */ + +declare(strict_types=1); + +namespace App\Doctrine\Types; + +use DateTime; +use DateTimeInterface; +use DateTimeZone; +use Doctrine\DBAL\Platforms\AbstractPlatform; +use Doctrine\DBAL\Types\ConversionException; +use Doctrine\DBAL\Types\DateTimeImmutableType; +use Doctrine\DBAL\Types\DateTimeType; +use Doctrine\DBAL\Types\Exception\InvalidFormat; + +/** + * This DateTimeImmutableType all dates to UTC, so it can be later used with the timezones. + * Taken (and adapted) from here: https://www.doctrine-project.org/projects/doctrine-orm/en/2.6/cookbook/working-with-datetime.html. + */ +class UTCDateTimeImmutableType extends DateTimeImmutableType +{ + private static ?DateTimeZone $utc_timezone = null; + + /** + * {@inheritdoc} + * + * @param T $value + * + * @return (T is null ? null : string) + * + * @template T + */ + public function convertToDatabaseValue($value, AbstractPlatform $platform): ?string + { + if (!self::$utc_timezone instanceof \DateTimeZone) { + self::$utc_timezone = new DateTimeZone('UTC'); + } + + if ($value instanceof \DateTimeImmutable) { + $value = $value->setTimezone(self::$utc_timezone); + } + + return parent::convertToDatabaseValue($value, $platform); + } + + /** + * {@inheritDoc} + * + * @param T $value + * + * @template T + */ + public function convertToPHPValue($value, AbstractPlatform $platform): ?\DateTimeImmutable + { + if (!self::$utc_timezone instanceof \DateTimeZone) { + self::$utc_timezone = new DateTimeZone('UTC'); + } + + if (null === $value || $value instanceof \DateTimeImmutable) { + return $value; + } + + $converted = \DateTimeImmutable::createFromFormat( + $platform->getDateTimeFormatString(), + $value, + self::$utc_timezone + ); + + if (!$converted) { + throw InvalidFormat::new( + $value, + static::class, + $platform->getDateTimeFormatString(), + ); + } + + return $converted; + } +} diff --git a/src/Entity/Attachments/Attachment.php b/src/Entity/Attachments/Attachment.php index f8048bc14..c2b56d69c 100644 --- a/src/Entity/Attachments/Attachment.php +++ b/src/Entity/Attachments/Attachment.php @@ -185,9 +185,9 @@ abstract class Attachment extends AbstractNamedDBElement protected ?AttachmentType $attachment_type = null; #[Groups(['attachment:read'])] - protected ?\DateTime $addedDate = null; + protected ?\DateTimeImmutable $addedDate = null; #[Groups(['attachment:read'])] - protected ?\DateTime $lastModified = null; + protected ?\DateTimeImmutable $lastModified = null; public function __construct() diff --git a/src/Entity/Attachments/AttachmentType.php b/src/Entity/Attachments/AttachmentType.php index 240c8ef86..fdf42de5f 100644 --- a/src/Entity/Attachments/AttachmentType.php +++ b/src/Entity/Attachments/AttachmentType.php @@ -135,9 +135,9 @@ class AttachmentType extends AbstractStructuralDBElement protected Collection $attachments_with_type; #[Groups(['attachment_type:read'])] - protected ?\DateTime $addedDate = null; + protected ?\DateTimeImmutable $addedDate = null; #[Groups(['attachment_type:read'])] - protected ?\DateTime $lastModified = null; + protected ?\DateTimeImmutable $lastModified = null; public function __construct() diff --git a/src/Entity/Base/AbstractCompany.php b/src/Entity/Base/AbstractCompany.php index 13e6010a7..a87354794 100644 --- a/src/Entity/Base/AbstractCompany.php +++ b/src/Entity/Base/AbstractCompany.php @@ -41,9 +41,9 @@ abstract class AbstractCompany extends AbstractPartsContainingDBElement { #[Groups(['company:read'])] - protected ?\DateTime $addedDate = null; + protected ?\DateTimeImmutable $addedDate = null; #[Groups(['company:read'])] - protected ?\DateTime $lastModified = null; + protected ?\DateTimeImmutable $lastModified = null; /** * @var string The address of the company diff --git a/src/Entity/Base/TimestampTrait.php b/src/Entity/Base/TimestampTrait.php index 94ddf2362..77506b183 100644 --- a/src/Entity/Base/TimestampTrait.php +++ b/src/Entity/Base/TimestampTrait.php @@ -34,28 +34,28 @@ trait TimestampTrait { /** - * @var \DateTime|null the date when this element was modified the last time + * @var \DateTimeImmutable|null the date when this element was modified the last time */ #[Groups(['extended', 'full'])] #[ApiProperty(writable: false)] - #[ORM\Column(name: 'last_modified', type: Types::DATETIME_MUTABLE, options: ['default' => 'CURRENT_TIMESTAMP'])] - protected ?\DateTime $lastModified = null; + #[ORM\Column(name: 'last_modified', type: Types::DATETIME_IMMUTABLE, options: ['default' => 'CURRENT_TIMESTAMP'])] + protected ?\DateTimeImmutable $lastModified = null; /** - * @var \DateTime|null the date when this element was created + * @var \DateTimeImmutable|null the date when this element was created */ #[Groups(['extended', 'full'])] #[ApiProperty(writable: false)] - #[ORM\Column(name: 'datetime_added', type: Types::DATETIME_MUTABLE, options: ['default' => 'CURRENT_TIMESTAMP'])] - protected ?\DateTime $addedDate = null; + #[ORM\Column(name: 'datetime_added', type: Types::DATETIME_IMMUTABLE, options: ['default' => 'CURRENT_TIMESTAMP'])] + protected ?\DateTimeImmutable $addedDate = null; /** * Returns the last time when the element was modified. * Returns null if the element was not yet saved to DB yet. * - * @return \DateTimeInterface|null the time of the last edit + * @return \DateTimeImmutable|null the time of the last edit */ - public function getLastModified(): ?\DateTimeInterface + public function getLastModified(): ?\DateTimeImmutable { return $this->lastModified; } @@ -64,9 +64,9 @@ public function getLastModified(): ?\DateTimeInterface * Returns the date/time when the element was created. * Returns null if the element was not yet saved to DB yet. * - * @return \DateTimeInterface|null the creation time of the part + * @return \DateTimeImmutable|null the creation time of the part */ - public function getAddedDate(): ?\DateTimeInterface + public function getAddedDate(): ?\DateTimeImmutable { return $this->addedDate; } @@ -78,9 +78,9 @@ public function getAddedDate(): ?\DateTimeInterface #[ORM\PreUpdate] public function updateTimestamps(): void { - $this->lastModified = new DateTime('now'); + $this->lastModified = new \DateTimeImmutable('now'); if (null === $this->addedDate) { - $this->addedDate = new DateTime('now'); + $this->addedDate = new \DateTimeImmutable('now'); } } } diff --git a/src/Entity/LogSystem/AbstractLogEntry.php b/src/Entity/LogSystem/AbstractLogEntry.php index ba1a4b232..aa795613b 100644 --- a/src/Entity/LogSystem/AbstractLogEntry.php +++ b/src/Entity/LogSystem/AbstractLogEntry.php @@ -25,7 +25,7 @@ use Doctrine\DBAL\Types\Types; use App\Entity\Base\AbstractDBElement; use App\Entity\UserSystem\User; -use DateTime; + use Doctrine\ORM\Mapping as ORM; use App\Repository\LogEntryRepository; @@ -56,10 +56,10 @@ abstract class AbstractLogEntry extends AbstractDBElement protected string $username = ''; /** - * @var \DateTimeInterface The datetime the event associated with this log entry has occured + * @var \DateTimeImmutable The datetime the event associated with this log entry has occured */ - #[ORM\Column(name: 'datetime', type: Types::DATETIME_MUTABLE)] - protected \DateTimeInterface $timestamp; + #[ORM\Column(name: 'datetime', type: Types::DATETIME_IMMUTABLE)] + protected \DateTimeImmutable $timestamp; /** * @var LogLevel The priority level of the associated level. 0 is highest, 7 lowest @@ -90,7 +90,7 @@ abstract class AbstractLogEntry extends AbstractDBElement public function __construct() { - $this->timestamp = new DateTime(); + $this->timestamp = new \DateTimeImmutable(); } /** @@ -165,7 +165,7 @@ public function getUsername(): string /** * Returns the timestamp when the event that caused this log entry happened. */ - public function getTimestamp(): \DateTimeInterface + public function getTimestamp(): \DateTimeImmutable { return $this->timestamp; } @@ -175,7 +175,7 @@ public function getTimestamp(): \DateTimeInterface * * @return $this */ - public function setTimestamp(\DateTime $timestamp): self + public function setTimestamp(\DateTimeImmutable $timestamp): self { $this->timestamp = $timestamp; diff --git a/src/Entity/OAuthToken.php b/src/Entity/OAuthToken.php index 54e86b746..0073aeed7 100644 --- a/src/Entity/OAuthToken.php +++ b/src/Entity/OAuthToken.php @@ -92,7 +92,7 @@ public function getToken(): ?string return $this->token; } - public function getExpirationDate(): ?\DateTimeInterface + public function getExpirationDate(): ?\DateTimeImmutable { return $this->expires_at; } diff --git a/src/Entity/Parts/Category.php b/src/Entity/Parts/Category.php index 30a4cb5fa..99ed3c6d0 100644 --- a/src/Entity/Parts/Category.php +++ b/src/Entity/Parts/Category.php @@ -183,9 +183,9 @@ class Category extends AbstractPartsContainingDBElement protected Collection $parameters; #[Groups(['category:read'])] - protected ?\DateTime $addedDate = null; + protected ?\DateTimeImmutable $addedDate = null; #[Groups(['category:read'])] - protected ?\DateTime $lastModified = null; + protected ?\DateTimeImmutable $lastModified = null; #[Assert\Valid] #[ORM\Embedded(class: EDACategoryInfo::class)] diff --git a/src/Entity/Parts/Footprint.php b/src/Entity/Parts/Footprint.php index 238ffb355..6b043562f 100644 --- a/src/Entity/Parts/Footprint.php +++ b/src/Entity/Parts/Footprint.php @@ -134,9 +134,9 @@ class Footprint extends AbstractPartsContainingDBElement protected Collection $parameters; #[Groups(['footprint:read'])] - protected ?\DateTime $addedDate = null; + protected ?\DateTimeImmutable $addedDate = null; #[Groups(['footprint:read'])] - protected ?\DateTime $lastModified = null; + protected ?\DateTimeImmutable $lastModified = null; #[Assert\Valid] #[ORM\Embedded(class: EDAFootprintInfo::class)] diff --git a/src/Entity/Parts/InfoProviderReference.php b/src/Entity/Parts/InfoProviderReference.php index 550f82f52..c6888b2c6 100644 --- a/src/Entity/Parts/InfoProviderReference.php +++ b/src/Entity/Parts/InfoProviderReference.php @@ -54,9 +54,9 @@ class InfoProviderReference #[Groups(['provider_reference:read'])] private ?string $provider_url = null; - #[Column(type: Types::DATETIME_MUTABLE, nullable: true, options: ['default' => null])] + #[Column(type: Types::DATETIME_IMMUTABLE, nullable: true, options: ['default' => null])] #[Groups(['provider_reference:read'])] - private ?\DateTime $last_updated = null; + private ?\DateTimeImmutable $last_updated = null; /** * Constructing is forbidden from outside. @@ -95,9 +95,8 @@ public function getProviderUrl(): ?string /** * Gets the time, when the part was last time updated by the provider. - * @return \DateTimeInterface|null */ - public function getLastUpdated(): ?\DateTimeInterface + public function getLastUpdated(): ?\DateTimeImmutable { return $this->last_updated; } @@ -140,7 +139,7 @@ public static function providerReference(string $provider_key, string $provider_ $ref->provider_key = $provider_key; $ref->provider_id = $provider_id; $ref->provider_url = $provider_url; - $ref->last_updated = new \DateTime(); + $ref->last_updated = new \DateTimeImmutable(); return $ref; } @@ -155,7 +154,7 @@ public static function fromPartDTO(SearchResultDTO $dto): self $ref->provider_key = $dto->provider_key; $ref->provider_id = $dto->provider_id; $ref->provider_url = $dto->provider_url; - $ref->last_updated = new \DateTime(); + $ref->last_updated = new \DateTimeImmutable(); return $ref; } } \ No newline at end of file diff --git a/src/Entity/Parts/MeasurementUnit.php b/src/Entity/Parts/MeasurementUnit.php index 9e5abd2ad..79281e9f7 100644 --- a/src/Entity/Parts/MeasurementUnit.php +++ b/src/Entity/Parts/MeasurementUnit.php @@ -156,9 +156,9 @@ class MeasurementUnit extends AbstractPartsContainingDBElement protected Collection $parameters; #[Groups(['measurement_unit:read'])] - protected ?\DateTime $addedDate = null; + protected ?\DateTimeImmutable $addedDate = null; #[Groups(['measurement_unit:read'])] - protected ?\DateTime $lastModified = null; + protected ?\DateTimeImmutable $lastModified = null; /** diff --git a/src/Entity/Parts/Part.php b/src/Entity/Parts/Part.php index a563ad6ec..4addf503c 100644 --- a/src/Entity/Parts/Part.php +++ b/src/Entity/Parts/Part.php @@ -154,9 +154,9 @@ class Part extends AttachmentContainingDBElement protected ?Attachment $master_picture_attachment = null; #[Groups(['part:read'])] - protected ?\DateTime $addedDate = null; + protected ?\DateTimeImmutable $addedDate = null; #[Groups(['part:read'])] - protected ?\DateTime $lastModified = null; + protected ?\DateTimeImmutable $lastModified = null; public function __construct() diff --git a/src/Entity/Parts/PartLot.php b/src/Entity/Parts/PartLot.php index 25b40d5a3..70475efd8 100644 --- a/src/Entity/Parts/PartLot.php +++ b/src/Entity/Parts/PartLot.php @@ -105,13 +105,13 @@ class PartLot extends AbstractDBElement implements TimeStampableInterface, Named protected string $comment = ''; /** - * @var \DateTime|null Set a time until when the lot must be used. + * @var \DateTimeImmutable|null Set a time until when the lot must be used. * Set to null, if the lot can be used indefinitely. */ #[Groups(['extended', 'full', 'import', 'part_lot:read', 'part_lot:write'])] - #[ORM\Column(name: 'expiration_date', type: Types::DATETIME_MUTABLE, nullable: true)] + #[ORM\Column(name: 'expiration_date', type: Types::DATETIME_IMMUTABLE, nullable: true)] #[Year2038BugWorkaround] - protected ?\DateTime $expiration_date = null; + protected ?\DateTimeImmutable $expiration_date = null; /** * @var StorageLocation|null The storelocation of this lot @@ -194,7 +194,7 @@ public function isExpired(): ?bool } //Check if the expiration date is bigger then current time - return $this->expiration_date < new DateTime('now'); + return $this->expiration_date < new \DateTimeImmutable('now'); } /** @@ -236,7 +236,7 @@ public function setComment(string $comment): self /** * Gets the expiration date for the part lot. Returns null, if no expiration date was set. */ - public function getExpirationDate(): ?\DateTimeInterface + public function getExpirationDate(): ?\DateTimeImmutable { return $this->expiration_date; } @@ -246,7 +246,7 @@ public function getExpirationDate(): ?\DateTimeInterface * * */ - public function setExpirationDate(?\DateTime $expiration_date): self + public function setExpirationDate(?\DateTimeImmutable $expiration_date): self { $this->expiration_date = $expiration_date; diff --git a/src/Entity/Parts/StorageLocation.php b/src/Entity/Parts/StorageLocation.php index 782b11cb6..6c455ae5b 100644 --- a/src/Entity/Parts/StorageLocation.php +++ b/src/Entity/Parts/StorageLocation.php @@ -170,9 +170,9 @@ class StorageLocation extends AbstractPartsContainingDBElement protected ?Attachment $master_picture_attachment = null; #[Groups(['location:read'])] - protected ?\DateTime $addedDate = null; + protected ?\DateTimeImmutable $addedDate = null; #[Groups(['location:read'])] - protected ?\DateTime $lastModified = null; + protected ?\DateTimeImmutable $lastModified = null; /******************************************************************************** diff --git a/src/Entity/PriceInformations/Currency.php b/src/Entity/PriceInformations/Currency.php index cd7c2bc92..8a63046a5 100644 --- a/src/Entity/PriceInformations/Currency.php +++ b/src/Entity/PriceInformations/Currency.php @@ -156,9 +156,9 @@ class Currency extends AbstractStructuralDBElement protected Collection $pricedetails; #[Groups(['currency:read'])] - protected ?\DateTime $addedDate = null; + protected ?\DateTimeImmutable $addedDate = null; #[Groups(['currency:read'])] - protected ?\DateTime $lastModified = null; + protected ?\DateTimeImmutable $lastModified = null; public function __construct() diff --git a/src/Entity/PriceInformations/Orderdetail.php b/src/Entity/PriceInformations/Orderdetail.php index 1275ebc2a..3709b37d7 100644 --- a/src/Entity/PriceInformations/Orderdetail.php +++ b/src/Entity/PriceInformations/Orderdetail.php @@ -46,7 +46,7 @@ use App\Entity\Contracts\TimeStampableInterface; use App\Entity\Parts\Part; use App\Entity\Parts\Supplier; -use DateTime; +use DateTimeImmutable; use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\Collection; use Doctrine\ORM\Mapping as ORM; @@ -173,9 +173,9 @@ public function __clone() #[ORM\PreUpdate] public function updateTimestamps(): void { - $this->lastModified = new DateTime('now'); + $this->lastModified = new DateTimeImmutable('now'); if (!$this->addedDate instanceof \DateTimeInterface) { - $this->addedDate = new DateTime('now'); + $this->addedDate = new DateTimeImmutable('now'); } if ($this->part instanceof Part) { diff --git a/src/Entity/PriceInformations/Pricedetail.php b/src/Entity/PriceInformations/Pricedetail.php index 98d4a4913..86a7bcd5f 100644 --- a/src/Entity/PriceInformations/Pricedetail.php +++ b/src/Entity/PriceInformations/Pricedetail.php @@ -38,7 +38,7 @@ use App\Validator\Constraints\Selectable; use Brick\Math\BigDecimal; use Brick\Math\RoundingMode; -use DateTime; +use DateTimeImmutable; use Doctrine\ORM\Mapping as ORM; use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; use Symfony\Component\Serializer\Annotation\Groups; @@ -141,9 +141,9 @@ public function __clone() #[ORM\PreUpdate] public function updateTimestamps(): void { - $this->lastModified = new DateTime('now'); + $this->lastModified = new DateTimeImmutable('now'); if (!$this->addedDate instanceof \DateTimeInterface) { - $this->addedDate = new DateTime('now'); + $this->addedDate = new DateTimeImmutable('now'); } if ($this->orderdetail instanceof Orderdetail) { diff --git a/src/Entity/ProjectSystem/Project.php b/src/Entity/ProjectSystem/Project.php index 5703388ba..849aa2369 100644 --- a/src/Entity/ProjectSystem/Project.php +++ b/src/Entity/ProjectSystem/Project.php @@ -158,9 +158,9 @@ class Project extends AbstractStructuralDBElement protected Collection $parameters; #[Groups(['project:read'])] - protected ?\DateTime $addedDate = null; + protected ?\DateTimeImmutable $addedDate = null; #[Groups(['project:read'])] - protected ?\DateTime $lastModified = null; + protected ?\DateTimeImmutable $lastModified = null; /******************************************************************************** diff --git a/src/Entity/UserSystem/ApiToken.php b/src/Entity/UserSystem/ApiToken.php index 751c08c1d..f5cbf5412 100644 --- a/src/Entity/UserSystem/ApiToken.php +++ b/src/Entity/UserSystem/ApiToken.php @@ -75,10 +75,10 @@ class ApiToken implements TimeStampableInterface #[Groups('token:read')] private ?User $user = null; - #[ORM\Column(type: Types::DATETIME_MUTABLE, nullable: true)] + #[ORM\Column(type: Types::DATETIME_IMMUTABLE, nullable: true)] #[Groups('token:read')] #[Year2038BugWorkaround] - private ?\DateTime $valid_until; + private ?\DateTimeImmutable $valid_until; #[ORM\Column(length: 68, unique: true)] private string $token; @@ -87,9 +87,9 @@ class ApiToken implements TimeStampableInterface #[Groups('token:read')] private ApiTokenLevel $level = ApiTokenLevel::READ_ONLY; - #[ORM\Column(type: Types::DATETIME_MUTABLE, nullable: true)] + #[ORM\Column(type: Types::DATETIME_IMMUTABLE, nullable: true)] #[Groups('token:read')] - private ?\DateTime $last_time_used = null; + private ?\DateTimeImmutable $last_time_used = null; public function __construct(ApiTokenType $tokenType = ApiTokenType::PERSONAL_ACCESS_TOKEN) { @@ -97,7 +97,7 @@ public function __construct(ApiTokenType $tokenType = ApiTokenType::PERSONAL_ACC $this->token = $tokenType->getTokenPrefix() . bin2hex(random_bytes(32)); //By default, tokens are valid for 1 year. - $this->valid_until = new \DateTime('+1 year'); + $this->valid_until = new \DateTimeImmutable('+1 year'); } public function getTokenType(): ApiTokenType @@ -116,7 +116,7 @@ public function setUser(?User $user): ApiToken return $this; } - public function getValidUntil(): ?\DateTimeInterface + public function getValidUntil(): ?\DateTimeImmutable { return $this->valid_until; } @@ -127,10 +127,10 @@ public function getValidUntil(): ?\DateTimeInterface */ public function isValid(): bool { - return $this->valid_until === null || $this->valid_until > new \DateTime(); + return $this->valid_until === null || $this->valid_until > new \DateTimeImmutable(); } - public function setValidUntil(?\DateTime $valid_until): ApiToken + public function setValidUntil(?\DateTimeImmutable $valid_until): ApiToken { $this->valid_until = $valid_until; return $this; @@ -159,19 +159,17 @@ public function setName(string $name): ApiToken /** * Gets the last time the token was used to authenticate or null if it was never used. - * @return \DateTimeInterface|null */ - public function getLastTimeUsed(): ?\DateTimeInterface + public function getLastTimeUsed(): ?\DateTimeImmutable { return $this->last_time_used; } /** * Sets the last time the token was used to authenticate. - * @param \DateTime|null $last_time_used * @return ApiToken */ - public function setLastTimeUsed(?\DateTime $last_time_used): ApiToken + public function setLastTimeUsed(?\DateTimeImmutable $last_time_used): ApiToken { $this->last_time_used = $last_time_used; return $this; diff --git a/src/Entity/UserSystem/User.php b/src/Entity/UserSystem/User.php index 38211873f..cc218eeb9 100644 --- a/src/Entity/UserSystem/User.php +++ b/src/Entity/UserSystem/User.php @@ -117,10 +117,10 @@ class User extends AttachmentContainingDBElement implements UserInterface, HasPe protected ?int $id = null; #[Groups(['user:read'])] - protected ?\DateTime $lastModified = null; + protected ?\DateTimeImmutable $lastModified = null; #[Groups(['user:read'])] - protected ?\DateTime $addedDate = null; + protected ?\DateTimeImmutable $addedDate = null; /** * @var bool Determines if the user is disabled (user can not log in) @@ -277,11 +277,11 @@ class User extends AttachmentContainingDBElement implements UserInterface, HasPe #[Groups(['user:read', 'user:write'])] protected ?Attachment $master_picture_attachment = null; - /** @var \DateTimeInterface|null The time when the backup codes were generated + /** @var \DateTimeImmutable|null The time when the backup codes were generated */ #[Groups(['full'])] - #[ORM\Column(type: Types::DATETIME_MUTABLE, nullable: true)] - protected ?\DateTimeInterface $backupCodesGenerationDate = null; + #[ORM\Column(type: Types::DATETIME_IMMUTABLE, nullable: true)] + protected ?\DateTimeImmutable $backupCodesGenerationDate = null; /** @var Collection */ @@ -318,10 +318,10 @@ class User extends AttachmentContainingDBElement implements UserInterface, HasPe protected ?PermissionData $permissions = null; /** - * @var \DateTime|null the time until the password reset token is valid + * @var \DateTimeImmutable|null the time until the password reset token is valid */ - #[ORM\Column(type: Types::DATETIME_MUTABLE, nullable: true)] - protected ?\DateTime $pw_reset_expires = null; + #[ORM\Column(type: Types::DATETIME_IMMUTABLE, nullable: true)] + protected ?\DateTimeImmutable $pw_reset_expires = null; /** * @var bool True if the user was created by a SAML provider (and therefore cannot change its password) @@ -528,7 +528,7 @@ public function setPwResetToken(?string $pw_reset_token): self /** * Gets the datetime when the password reset token expires. */ - public function getPwResetExpires(): \DateTimeInterface|null + public function getPwResetExpires(): \DateTimeImmutable|null { return $this->pw_reset_expires; } @@ -536,7 +536,7 @@ public function getPwResetExpires(): \DateTimeInterface|null /** * Sets the datetime when the password reset token expires. */ - public function setPwResetExpires(\DateTime $pw_reset_expires): self + public function setPwResetExpires(\DateTimeImmutable $pw_reset_expires): self { $this->pw_reset_expires = $pw_reset_expires; @@ -897,7 +897,7 @@ public function getBackupCodes(): array public function setBackupCodes(array $codes): self { $this->backupCodes = $codes; - $this->backupCodesGenerationDate = $codes === [] ? null : new DateTime(); + $this->backupCodesGenerationDate = $codes === [] ? null : new \DateTimeImmutable(); return $this; } @@ -905,7 +905,7 @@ public function setBackupCodes(array $codes): self /** * Return the date when the backup codes were generated. */ - public function getBackupCodesGenerationDate(): ?\DateTimeInterface + public function getBackupCodesGenerationDate(): ?\DateTimeImmutable { return $this->backupCodesGenerationDate; } diff --git a/src/Entity/UserSystem/WebauthnKey.php b/src/Entity/UserSystem/WebauthnKey.php index 68868fc6c..b2716e07d 100644 --- a/src/Entity/UserSystem/WebauthnKey.php +++ b/src/Entity/UserSystem/WebauthnKey.php @@ -82,9 +82,8 @@ public function getId(): int /** * Retrieve the last time when the key was used. - * @return \DateTimeInterface|null */ - public function getLastTimeUsed(): ?\DateTimeInterface + public function getLastTimeUsed(): ?\DateTimeImmutable { return $this->last_time_used; } diff --git a/src/Helpers/LabelResponse.php b/src/Helpers/LabelResponse.php index 98451e2fa..2973eb7e3 100644 --- a/src/Helpers/LabelResponse.php +++ b/src/Helpers/LabelResponse.php @@ -84,7 +84,7 @@ public function prepare(Request $request): static */ public function setAutoLastModified(): LabelResponse { - $this->setLastModified(new DateTime()); + $this->setLastModified(new \DateTimeImmutable()); return $this; } diff --git a/src/Security/ApiTokenAuthenticator.php b/src/Security/ApiTokenAuthenticator.php index 5d1215565..23ab68b98 100644 --- a/src/Security/ApiTokenAuthenticator.php +++ b/src/Security/ApiTokenAuthenticator.php @@ -75,7 +75,7 @@ private function getTokenFromString(#[\SensitiveParameter] string $accessToken): $old_time = $token->getLastTimeUsed(); //Set the last used date of the token - $token->setLastTimeUsed(new \DateTime()); + $token->setLastTimeUsed(new \DateTimeImmutable()); //Only flush the token if the last used date change is more than 10 minutes //For performance reasons we don't want to flush the token every time it is used, but only if it is used more than 10 minutes after the last time it was used //If a flush is later in the code we don't want to flush the token again diff --git a/src/Services/ImportExportSystem/PartKeeprImporter/PKImportHelperTrait.php b/src/Services/ImportExportSystem/PartKeeprImporter/PKImportHelperTrait.php index ea897b9fc..574bc32b2 100644 --- a/src/Services/ImportExportSystem/PartKeeprImporter/PKImportHelperTrait.php +++ b/src/Services/ImportExportSystem/PartKeeprImporter/PKImportHelperTrait.php @@ -225,7 +225,7 @@ protected function setIDOfEntity(AbstractDBElement $element, int|string $id): vo protected function setCreationDate(TimeStampableInterface $entity, ?string $datetime_str): void { if ($datetime_str !== null && $datetime_str !== '' && $datetime_str !== '0000-00-00 00:00:00') { - $date = new \DateTime($datetime_str); + $date = new \DateTimeImmutable($datetime_str); } else { $date = null; //Null means "now" at persist time } diff --git a/src/Services/LabelSystem/LabelExampleElementsGenerator.php b/src/Services/LabelSystem/LabelExampleElementsGenerator.php index c9cdbb04b..199d5f462 100644 --- a/src/Services/LabelSystem/LabelExampleElementsGenerator.php +++ b/src/Services/LabelSystem/LabelExampleElementsGenerator.php @@ -97,7 +97,7 @@ public function getExamplePartLot(): PartLot $lot->setDescription('Example Lot'); $lot->setComment('Lot comment'); - $lot->setExpirationDate(new DateTime('+1 days')); + $lot->setExpirationDate(new \DateTimeImmutable('+1 day')); $lot->setStorageLocation($this->getStructuralData(StorageLocation::class)); $lot->setAmount(123); $lot->setOwner($this->getUser()); diff --git a/src/Services/LabelSystem/PlaceholderProviders/GlobalProviders.php b/src/Services/LabelSystem/PlaceholderProviders/GlobalProviders.php index 5a9b22945..ddd4dbf17 100644 --- a/src/Services/LabelSystem/PlaceholderProviders/GlobalProviders.php +++ b/src/Services/LabelSystem/PlaceholderProviders/GlobalProviders.php @@ -81,7 +81,7 @@ public function replace(string $placeholder, object $label_target, array $option return 'anonymous'; } - $now = new DateTime(); + $now = new \DateTimeImmutable(); if ('[[DATETIME]]' === $placeholder) { $formatter = IntlDateFormatter::create( diff --git a/src/Services/LabelSystem/PlaceholderProviders/TimestampableElementProvider.php b/src/Services/LabelSystem/PlaceholderProviders/TimestampableElementProvider.php index 8588e133e..b316abf2c 100644 --- a/src/Services/LabelSystem/PlaceholderProviders/TimestampableElementProvider.php +++ b/src/Services/LabelSystem/PlaceholderProviders/TimestampableElementProvider.php @@ -42,7 +42,6 @@ namespace App\Services\LabelSystem\PlaceholderProviders; use App\Entity\Contracts\TimeStampableInterface; -use DateTime; use IntlDateFormatter; use Locale; @@ -57,11 +56,11 @@ public function replace(string $placeholder, object $label_target, array $option $formatter = new IntlDateFormatter(Locale::getDefault(), IntlDateFormatter::SHORT, IntlDateFormatter::SHORT); if ('[[LAST_MODIFIED]]' === $placeholder) { - return $formatter->format($label_target->getLastModified() ?? new DateTime()); + return $formatter->format($label_target->getLastModified() ?? new \DateTimeImmutable()); } if ('[[CREATION_DATE]]' === $placeholder) { - return $formatter->format($label_target->getAddedDate() ?? new DateTime()); + return $formatter->format($label_target->getAddedDate() ?? new \DateTimeImmutable()); } } diff --git a/src/Services/LogSystem/LogDataFormatter.php b/src/Services/LogSystem/LogDataFormatter.php index c5e82a479..af54c60c1 100644 --- a/src/Services/LogSystem/LogDataFormatter.php +++ b/src/Services/LogSystem/LogDataFormatter.php @@ -136,7 +136,7 @@ private function formatDateTime(array $data): string } try { - $dateTime = new \DateTime($date, new \DateTimeZone($timezone)); + $dateTime = new \DateTimeImmutable($date, new \DateTimeZone($timezone)); } catch (\Exception) { return 'unknown DateTime format'; } diff --git a/src/Services/LogSystem/TimeTravel.php b/src/Services/LogSystem/TimeTravel.php index 1ec742e3e..372094151 100644 --- a/src/Services/LogSystem/TimeTravel.php +++ b/src/Services/LogSystem/TimeTravel.php @@ -34,11 +34,13 @@ use Brick\Math\BigDecimal; use DateTime; use Doctrine\Common\Collections\Collection; +use Doctrine\DBAL\Types\Types; use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\Mapping\ClassMetadata; use Doctrine\ORM\Mapping\MappingException; use Exception; use InvalidArgumentException; +use PHPUnit\Util\Type; use ReflectionClass; class TimeTravel @@ -171,17 +173,26 @@ public function revertEntityToTimestamp(AbstractDBElement $element, \DateTimeInt /** * This function decodes the array which is created during the json_encode of a datetime object and returns a DateTime object. * @param array $input - * @return DateTime + * @return \DateTimeInterface * @throws Exception */ - private function dateTimeDecode(?array $input): ?\DateTime + private function dateTimeDecode(?array $input, string $doctrineType): ?\DateTimeInterface { //Allow null values if ($input === null) { return null; } - return new \DateTime($input['date'], new \DateTimeZone($input['timezone'])); + //Mutable types + if (in_array($doctrineType, [Types::DATETIME_MUTABLE, Types::DATE_MUTABLE], true)) { + return new \DateTime($input['date'], new \DateTimeZone($input['timezone'])); + } + //Immutable types + if (in_array($doctrineType, [Types::DATETIME_IMMUTABLE, Types::DATE_IMMUTABLE], true)) { + return new \DateTimeImmutable($input['date'], new \DateTimeZone($input['timezone'])); + } + + throw new InvalidArgumentException('The given doctrine type is not a datetime type!'); } /** @@ -208,8 +219,10 @@ public function applyEntry(AbstractDBElement $element, TimeTravelInterface $logE $data = BigDecimal::of($data); } - if (!$data instanceof DateTime && ('datetime' === $metadata->getFieldMapping($field)['type'])) { - $data = $this->dateTimeDecode($data); + if (!$data instanceof \DateTimeInterface + && (in_array($metadata->getFieldMapping($field)['type'], + [Types::DATETIME_IMMUTABLE, Types::DATETIME_IMMUTABLE, Types::DATE_MUTABLE, Types::DATETIME_IMMUTABLE], true))) { + $data = $this->dateTimeDecode($data, $metadata->getFieldMapping($field)['type']); } $this->setField($element, $field, $data); diff --git a/src/Services/Trees/SidebarTreeUpdater.php b/src/Services/Trees/SidebarTreeUpdater.php index 396410db8..c0f93b1f2 100644 --- a/src/Services/Trees/SidebarTreeUpdater.php +++ b/src/Services/Trees/SidebarTreeUpdater.php @@ -49,7 +49,7 @@ public function getLastTreeUpdate(): \DateTimeInterface //This tag and therfore this whole cache gets cleared by TreeCacheInvalidationListener when a structural element is changed $item->tag('sidebar_tree_update'); - return new \DateTime(); + return new \DateTimeImmutable(); }); } } diff --git a/src/Services/UserSystem/PasswordResetManager.php b/src/Services/UserSystem/PasswordResetManager.php index 31dbdf900..1a78cb605 100644 --- a/src/Services/UserSystem/PasswordResetManager.php +++ b/src/Services/UserSystem/PasswordResetManager.php @@ -59,8 +59,7 @@ public function request(string $name_or_email): void $user->setPwResetToken($this->passwordEncoder->hash($unencrypted_token)); //Determine the expiration datetime of - $expiration_date = new DateTime(); - $expiration_date->add(date_interval_create_from_date_string('1 day')); + $expiration_date = new \DateTimeImmutable("+1 day"); $user->setPwResetExpires($expiration_date); if ($user->getEmail() !== null && $user->getEmail() !== '') { @@ -105,7 +104,7 @@ public function setNewPassword(string $username, string $token, string $new_pass } //Check if token is expired yet - if ($user->getPwResetExpires() < new DateTime()) { + if ($user->getPwResetExpires() < new \DateTimeImmutable()) { return false; } @@ -119,7 +118,7 @@ public function setNewPassword(string $username, string $token, string $new_pass //Remove token $user->setPwResetToken(null); - $user->setPwResetExpires(new DateTime()); + $user->setPwResetExpires(new \DateTimeImmutable()); //Save to DB $this->em->flush(); diff --git a/tests/Entity/Parts/PartLotTest.php b/tests/Entity/Parts/PartLotTest.php index dee09aae0..30687b723 100644 --- a/tests/Entity/Parts/PartLotTest.php +++ b/tests/Entity/Parts/PartLotTest.php @@ -33,12 +33,11 @@ public function testIsExpired(): void $lot = new PartLot(); $this->assertNull($lot->isExpired(), 'Lot must be return null when no Expiration date is set!'); - $datetime = new DateTime(); - $lot->setExpirationDate($datetime->setTimestamp(strtotime('now +1 hour'))); + $lot->setExpirationDate(new \DateTimeImmutable('+1 hour')); $this->assertFalse($lot->isExpired(), 'Lot with expiration date in the future must not be expired!'); - $lot->setExpirationDate($datetime->setTimestamp(strtotime('now -1 hour'))); + $lot->setExpirationDate(new \DateTimeImmutable('-1 hour')); $this->assertTrue($lot->isExpired(), 'Lot with expiration date in the past must be expired!'); } } diff --git a/tests/Entity/Parts/PartTest.php b/tests/Entity/Parts/PartTest.php index fa1ecc390..3e9233c1b 100644 --- a/tests/Entity/Parts/PartTest.php +++ b/tests/Entity/Parts/PartTest.php @@ -95,7 +95,7 @@ public function testGetAmountSum(): void $part->addPartLot( (new PartLot()) ->setAmount(6) - ->setExpirationDate($datetime->setTimestamp(strtotime('now -1 hour'))) + ->setExpirationDate(new \DateTimeImmutable('-1 hour')) ); $this->assertEqualsWithDelta(13.0, $part->getAmountSum(), PHP_FLOAT_EPSILON); diff --git a/tests/Entity/UserSystem/UserTest.php b/tests/Entity/UserSystem/UserTest.php index a8b2cd5f0..f11eec0f3 100644 --- a/tests/Entity/UserSystem/UserTest.php +++ b/tests/Entity/UserSystem/UserTest.php @@ -73,7 +73,7 @@ public function testSetBackupCodes(): void $codes = ['test', 'invalid', 'test']; $user->setBackupCodes($codes); // Backup Codes generation date must be changed! - $this->assertInstanceOf(\DateTime::class, $user->getBackupCodesGenerationDate()); + $this->assertNotNull($user->getBackupCodesGenerationDate()); $this->assertSame($codes, $user->getBackupCodes()); //Test what happens if we delete the backup keys diff --git a/tests/Services/LabelSystem/PlaceholderProviders/PartLotProviderTest.php b/tests/Services/LabelSystem/PlaceholderProviders/PartLotProviderTest.php index bfcf4c43e..98285aa51 100644 --- a/tests/Services/LabelSystem/PlaceholderProviders/PartLotProviderTest.php +++ b/tests/Services/LabelSystem/PlaceholderProviders/PartLotProviderTest.php @@ -65,7 +65,7 @@ protected function setUp(): void $this->target = new PartLot(); $this->target->setDescription('Lot description'); $this->target->setComment('Lot comment'); - $this->target->setExpirationDate(new \DateTime('1999-04-13')); + $this->target->setExpirationDate(new \DateTimeImmutable('1999-04-13')); $this->target->setInstockUnknown(true); $location = new StorageLocation();