diff --git a/config/packages/doctrine.yaml b/config/packages/doctrine.yaml index cea0126a..0986565f 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 fbb4bdd2..4bcf3a60 100644 --- a/src/DataFixtures/APITokenFixtures.php +++ b/src/DataFixtures/APITokenFixtures.php @@ -75,7 +75,7 @@ class APITokenFixtures extends Fixture implements DependentFixtureInterface $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 a11b11e9..b7193ec0 100644 --- a/src/DataFixtures/PartFixtures.php +++ b/src/DataFixtures/PartFixtures.php @@ -99,7 +99,7 @@ class PartFixtures extends Fixture implements DependentFixtureInterface $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 d485913c..e60b2867 100644 --- a/src/DataTables/Column/LocaleDateTimeColumn.php +++ b/src/DataTables/Column/LocaleDateTimeColumn.php @@ -47,7 +47,7 @@ class LocaleDateTimeColumn extends AbstractColumn } 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 00000000..c0d6659d --- /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 f8048bc1..c2b56d69 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 240c8ef8..fdf42de5 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 13e6010a..a8735479 100644 --- a/src/Entity/Base/AbstractCompany.php +++ b/src/Entity/Base/AbstractCompany.php @@ -41,9 +41,9 @@ use Symfony\Component\Validator\Constraints as Assert; 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 94ddf236..77506b18 100644 --- a/src/Entity/Base/TimestampTrait.php +++ b/src/Entity/Base/TimestampTrait.php @@ -34,28 +34,28 @@ use Symfony\Component\Serializer\Annotation\Groups; 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 @@ trait TimestampTrait * 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 @@ trait TimestampTrait #[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 ba1a4b23..aa795613 100644 --- a/src/Entity/LogSystem/AbstractLogEntry.php +++ b/src/Entity/LogSystem/AbstractLogEntry.php @@ -25,7 +25,7 @@ namespace App\Entity\LogSystem; 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 @@ abstract class AbstractLogEntry extends AbstractDBElement /** * 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 @@ abstract class AbstractLogEntry extends AbstractDBElement * * @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 54e86b74..0073aeed 100644 --- a/src/Entity/OAuthToken.php +++ b/src/Entity/OAuthToken.php @@ -92,7 +92,7 @@ class OAuthToken extends AbstractNamedDBElement implements AccessTokenInterface 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 30a4cb5f..99ed3c6d 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 238ffb35..6b043562 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 550f82f5..c6888b2c 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 @@ class InfoProviderReference /** * 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 @@ class InfoProviderReference $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 @@ class InfoProviderReference $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 9e5abd2a..79281e9f 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 a563ad6e..4addf503 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 25b40d5a..70475efd 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 @@ class PartLot extends AbstractDBElement implements TimeStampableInterface, Named } //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 @@ class PartLot extends AbstractDBElement implements TimeStampableInterface, Named /** * 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 @@ class PartLot extends AbstractDBElement implements TimeStampableInterface, Named * * */ - 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 782b11cb..6c455ae5 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 cd7c2bc9..8a63046a 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 1275ebc2..3709b37d 100644 --- a/src/Entity/PriceInformations/Orderdetail.php +++ b/src/Entity/PriceInformations/Orderdetail.php @@ -46,7 +46,7 @@ use App\Entity\Contracts\NamedElementInterface; 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 @@ class Orderdetail extends AbstractDBElement implements TimeStampableInterface, N #[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 98d4a491..86a7bcd5 100644 --- a/src/Entity/PriceInformations/Pricedetail.php +++ b/src/Entity/PriceInformations/Pricedetail.php @@ -38,7 +38,7 @@ use App\Validator\Constraints\BigDecimal\BigDecimalPositive; 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 @@ class Pricedetail extends AbstractDBElement implements TimeStampableInterface #[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 5703388b..849aa236 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 751c08c1..f5cbf541 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 @@ class ApiToken implements TimeStampableInterface $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 @@ class ApiToken implements TimeStampableInterface return $this; } - public function getValidUntil(): ?\DateTimeInterface + public function getValidUntil(): ?\DateTimeImmutable { return $this->valid_until; } @@ -127,10 +127,10 @@ class ApiToken implements TimeStampableInterface */ 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 @@ class ApiToken implements TimeStampableInterface /** * 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 38211873..cc218eeb 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 @@ class User extends AttachmentContainingDBElement implements UserInterface, HasPe /** * 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 @@ class User extends AttachmentContainingDBElement implements UserInterface, HasPe /** * 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 @@ class User extends AttachmentContainingDBElement implements UserInterface, HasPe 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 @@ class User extends AttachmentContainingDBElement implements UserInterface, HasPe /** * 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 68868fc6..b2716e07 100644 --- a/src/Entity/UserSystem/WebauthnKey.php +++ b/src/Entity/UserSystem/WebauthnKey.php @@ -82,9 +82,8 @@ class WebauthnKey extends BasePublicKeyCredentialSource implements TimeStampable /** * 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 98451e2f..2973eb7e 100644 --- a/src/Helpers/LabelResponse.php +++ b/src/Helpers/LabelResponse.php @@ -84,7 +84,7 @@ class LabelResponse extends Response */ 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 5d121556..23ab68b9 100644 --- a/src/Security/ApiTokenAuthenticator.php +++ b/src/Security/ApiTokenAuthenticator.php @@ -75,7 +75,7 @@ class ApiTokenAuthenticator implements AuthenticatorInterface $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 ea897b9f..574bc32b 100644 --- a/src/Services/ImportExportSystem/PartKeeprImporter/PKImportHelperTrait.php +++ b/src/Services/ImportExportSystem/PartKeeprImporter/PKImportHelperTrait.php @@ -225,7 +225,7 @@ trait PKImportHelperTrait 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 c9cdbb04..199d5f46 100644 --- a/src/Services/LabelSystem/LabelExampleElementsGenerator.php +++ b/src/Services/LabelSystem/LabelExampleElementsGenerator.php @@ -97,7 +97,7 @@ final class LabelExampleElementsGenerator $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 5a9b2294..ddd4dbf1 100644 --- a/src/Services/LabelSystem/PlaceholderProviders/GlobalProviders.php +++ b/src/Services/LabelSystem/PlaceholderProviders/GlobalProviders.php @@ -81,7 +81,7 @@ final class GlobalProviders implements PlaceholderProviderInterface 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 8588e133..b316abf2 100644 --- a/src/Services/LabelSystem/PlaceholderProviders/TimestampableElementProvider.php +++ b/src/Services/LabelSystem/PlaceholderProviders/TimestampableElementProvider.php @@ -42,7 +42,6 @@ declare(strict_types=1); namespace App\Services\LabelSystem\PlaceholderProviders; use App\Entity\Contracts\TimeStampableInterface; -use DateTime; use IntlDateFormatter; use Locale; @@ -57,11 +56,11 @@ final class TimestampableElementProvider implements PlaceholderProviderInterface $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 c5e82a47..af54c60c 100644 --- a/src/Services/LogSystem/LogDataFormatter.php +++ b/src/Services/LogSystem/LogDataFormatter.php @@ -136,7 +136,7 @@ class LogDataFormatter } 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 1ec742e3..37209415 100644 --- a/src/Services/LogSystem/TimeTravel.php +++ b/src/Services/LogSystem/TimeTravel.php @@ -34,11 +34,13 @@ use App\Repository\LogEntryRepository; 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 @@ class TimeTravel /** * 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 @@ class TimeTravel $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 396410db..c0f93b1f 100644 --- a/src/Services/Trees/SidebarTreeUpdater.php +++ b/src/Services/Trees/SidebarTreeUpdater.php @@ -49,7 +49,7 @@ final class SidebarTreeUpdater //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 31dbdf90..1a78cb60 100644 --- a/src/Services/UserSystem/PasswordResetManager.php +++ b/src/Services/UserSystem/PasswordResetManager.php @@ -59,8 +59,7 @@ class PasswordResetManager $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 @@ class PasswordResetManager } //Check if token is expired yet - if ($user->getPwResetExpires() < new DateTime()) { + if ($user->getPwResetExpires() < new \DateTimeImmutable()) { return false; } @@ -119,7 +118,7 @@ class PasswordResetManager //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 dee09aae..30687b72 100644 --- a/tests/Entity/Parts/PartLotTest.php +++ b/tests/Entity/Parts/PartLotTest.php @@ -33,12 +33,11 @@ class PartLotTest extends TestCase $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 fa1ecc39..3e9233c1 100644 --- a/tests/Entity/Parts/PartTest.php +++ b/tests/Entity/Parts/PartTest.php @@ -95,7 +95,7 @@ class PartTest extends TestCase $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 a8b2cd5f..f11eec0f 100644 --- a/tests/Entity/UserSystem/UserTest.php +++ b/tests/Entity/UserSystem/UserTest.php @@ -73,7 +73,7 @@ class UserTest extends TestCase $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 bfcf4c43..98285aa5 100644 --- a/tests/Services/LabelSystem/PlaceholderProviders/PartLotProviderTest.php +++ b/tests/Services/LabelSystem/PlaceholderProviders/PartLotProviderTest.php @@ -65,7 +65,7 @@ class PartLotProviderTest extends WebTestCase $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();