Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1d05f5af9d | |||
| eda7e0e8cc | |||
| f9f824a2f0 | |||
| 27b973f3db | |||
| 28bc360106 | |||
| 7e81e130c9 |
@@ -87,9 +87,11 @@ server {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
# Handle asset files (css, js, images, etc.) - serve directly if they exist
|
# Handle asset files (css, js, images, etc.) - serve directly if they
|
||||||
|
# exist, otherwise hand off to the front controller so app routes whose
|
||||||
|
# paths end in asset extensions (e.g. DAV resources) keep working.
|
||||||
location ~* \.(css|js|mjs|svg|gif|png|jpg|jpeg|ico|woff|woff2|ttf|eot|map)$ {
|
location ~* \.(css|js|mjs|svg|gif|png|jpg|jpeg|ico|woff|woff2|ttf|eot|map)$ {
|
||||||
try_files $uri =404;
|
try_files $uri @php;
|
||||||
expires 1m;
|
expires 1m;
|
||||||
add_header Cache-Control "public, immutable";
|
add_header Cache-Control "public, immutable";
|
||||||
types {
|
types {
|
||||||
|
|||||||
@@ -57,9 +57,11 @@ server {
|
|||||||
add_header Cache-Control "no-cache, must-revalidate";
|
add_header Cache-Control "no-cache, must-revalidate";
|
||||||
}
|
}
|
||||||
|
|
||||||
# Handle asset files (css, js, images, etc.) - serve directly if they exist
|
# Handle asset files (css, js, images, etc.) - serve directly if they
|
||||||
|
# exist, otherwise hand off to the front controller so app routes whose
|
||||||
|
# paths end in asset extensions (e.g. DAV resources) keep working.
|
||||||
location ~* \.(css|js|svg|gif|png|jpg|jpeg|ico|woff|woff2|ttf|eot|map)$ {
|
location ~* \.(css|js|svg|gif|png|jpg|jpeg|ico|woff|woff2|ttf|eot|map)$ {
|
||||||
try_files $uri =404;
|
try_files $uri @php;
|
||||||
expires 1y;
|
expires 1y;
|
||||||
add_header Cache-Control "public, immutable";
|
add_header Cache-Control "public, immutable";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ use KTXF\Resource\Provider\Node\NodeBaseAbstract;
|
|||||||
abstract class EntityBaseAbstract extends NodeBaseAbstract implements EntityBaseInterface {
|
abstract class EntityBaseAbstract extends NodeBaseAbstract implements EntityBaseInterface {
|
||||||
|
|
||||||
protected string $type = 'chrono:entity';
|
protected string $type = 'chrono:entity';
|
||||||
protected EntityPropertiesBaseInterface $properties;
|
protected EventObject|TaskObject|JournalObject $properties;
|
||||||
|
|
||||||
protected function nodeIdentifier(): EntityIdentifier {
|
protected function nodeIdentifier(): EntityIdentifier {
|
||||||
return new EntityIdentifier($this->data[static::PROPERTY_PROVIDER], $this->data[static::PROPERTY_SERVICE], $this->data[static::PROPERTY_COLLECTION], $this->data[static::PROPERTY_IDENTIFIER]);
|
return new EntityIdentifier($this->data[static::PROPERTY_PROVIDER], $this->data[static::PROPERTY_SERVICE], $this->data[static::PROPERTY_COLLECTION], $this->data[static::PROPERTY_IDENTIFIER]);
|
||||||
@@ -31,7 +31,7 @@ abstract class EntityBaseAbstract extends NodeBaseAbstract implements EntityBase
|
|||||||
/**
|
/**
|
||||||
* @inheritDoc
|
* @inheritDoc
|
||||||
*/
|
*/
|
||||||
public function getProperties(): EntityPropertiesBaseInterface {
|
public function getProperties(): EventObject|TaskObject|JournalObject {
|
||||||
return $this->properties;
|
return $this->properties ??= new EventObject();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,10 +14,10 @@ use KTXF\Resource\Provider\Node\NodeBaseInterface;
|
|||||||
interface EntityBaseInterface extends NodeBaseInterface {
|
interface EntityBaseInterface extends NodeBaseInterface {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the entity properties
|
* Gets the entity properties: the typed chrono entity object
|
||||||
*
|
*
|
||||||
* @since 2025.05.01
|
* @since 2025.05.01
|
||||||
*/
|
*/
|
||||||
public function getProperties(): EntityPropertiesBaseInterface|EntityPropertiesMutableInterface;
|
public function getProperties(): EventObject|TaskObject|JournalObject;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ use KTXF\Resource\Provider\Node\NodePropertiesMutableInterface;
|
|||||||
abstract class EntityMutableAbstract extends NodeMutableAbstract implements EntityMutableInterface {
|
abstract class EntityMutableAbstract extends NodeMutableAbstract implements EntityMutableInterface {
|
||||||
|
|
||||||
protected string $type = 'chrono:entity';
|
protected string $type = 'chrono:entity';
|
||||||
protected EntityPropertiesMutableInterface $properties;
|
protected EventObject|TaskObject|JournalObject $properties;
|
||||||
|
|
||||||
protected function nodeIdentifier(): EntityIdentifier {
|
protected function nodeIdentifier(): EntityIdentifier {
|
||||||
return new EntityIdentifier($this->data[static::PROPERTY_PROVIDER], $this->data[static::PROPERTY_SERVICE], (string) $this->data[static::PROPERTY_COLLECTION], (string) $this->data[static::PROPERTY_IDENTIFIER]);
|
return new EntityIdentifier($this->data[static::PROPERTY_PROVIDER], $this->data[static::PROPERTY_SERVICE], (string) $this->data[static::PROPERTY_COLLECTION], (string) $this->data[static::PROPERTY_IDENTIFIER]);
|
||||||
@@ -32,16 +32,36 @@ abstract class EntityMutableAbstract extends NodeMutableAbstract implements Enti
|
|||||||
/**
|
/**
|
||||||
* @inheritDoc
|
* @inheritDoc
|
||||||
*/
|
*/
|
||||||
public function getProperties(): EntityPropertiesMutableInterface {
|
public function jsonDeserialize(array|string $data): static {
|
||||||
return $this->properties;
|
if (is_string($data)) {
|
||||||
|
$data = json_decode($data, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
$properties = $data[static::PROPERTY_PROPERTIES] ?? null;
|
||||||
|
unset($data[static::PROPERTY_PROPERTIES]);
|
||||||
|
|
||||||
|
parent::jsonDeserialize($data);
|
||||||
|
|
||||||
|
if (is_array($properties)) {
|
||||||
|
$this->properties = EntityType::fromData($properties)->instantiate()->jsonDeserialize($properties);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritDoc
|
||||||
|
*/
|
||||||
|
public function getProperties(): EventObject|TaskObject|JournalObject {
|
||||||
|
return $this->properties ??= new EventObject();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @inheritDoc
|
* @inheritDoc
|
||||||
*/
|
*/
|
||||||
public function setProperties(NodePropertiesMutableInterface $value): static {
|
public function setProperties(NodePropertiesMutableInterface $value): static {
|
||||||
if (!$value instanceof EntityPropertiesMutableInterface) {
|
if (!$value instanceof EventObject && !$value instanceof TaskObject && !$value instanceof JournalObject) {
|
||||||
throw new \InvalidArgumentException('Properties must implement EntityPropertiesMutableInterface');
|
throw new \InvalidArgumentException('Properties must be an EventObject, TaskObject or JournalObject');
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->properties = $value;
|
$this->properties = $value;
|
||||||
|
|||||||
@@ -17,10 +17,10 @@ use KTXF\Resource\Provider\Node\NodeMutableInterface;
|
|||||||
interface EntityMutableInterface extends EntityBaseInterface, NodeMutableInterface {
|
interface EntityMutableInterface extends EntityBaseInterface, NodeMutableInterface {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the entity properties (mutable)
|
* Gets the entity properties: the typed chrono entity object
|
||||||
*
|
*
|
||||||
* @since 2025.05.01
|
* @since 2025.05.01
|
||||||
*/
|
*/
|
||||||
public function getProperties(): EntityPropertiesMutableInterface;
|
public function getProperties(): EventObject|TaskObject|JournalObject;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,14 +9,28 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace KTXF\Chrono\Entity;
|
namespace KTXF\Chrono\Entity;
|
||||||
|
|
||||||
use KTXF\Resource\Provider\Node\NodePropertiesBaseAbstract;
|
use KTXF\Json\JsonSerializableObject;
|
||||||
|
|
||||||
abstract class EntityPropertiesBaseAbstract extends NodePropertiesBaseAbstract implements EntityPropertiesBaseInterface {
|
/**
|
||||||
|
* Common base of the typed chrono entity objects ({@see EventObject},
|
||||||
|
* {@see TaskObject}, {@see JournalObject}).
|
||||||
|
*
|
||||||
|
* Gives providers a single class to build or extend entity properties
|
||||||
|
* against without involving the entity node wrapper, and one home for the
|
||||||
|
* shared behavior: the type/schema markers and the JSON serialization /
|
||||||
|
* deserialization machinery inherited from JsonSerializableObject.
|
||||||
|
*/
|
||||||
|
abstract class EntityPropertiesBaseAbstract extends JsonSerializableObject implements EntityPropertiesBaseInterface {
|
||||||
|
|
||||||
|
public string $type = 'event';
|
||||||
|
public int $schema = 1;
|
||||||
|
|
||||||
|
public function type(): string {
|
||||||
|
return $this->type;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function schema(): int {
|
||||||
|
return $this->schema;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @inheritDoc
|
|
||||||
*/
|
|
||||||
public function getDataRaw(): array|string|null {
|
|
||||||
return $this->data[self::PROPERTY_DATA] ?? null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,8 +14,5 @@ use KTXF\Resource\Provider\Node\NodePropertiesBaseInterface;
|
|||||||
interface EntityPropertiesBaseInterface extends NodePropertiesBaseInterface {
|
interface EntityPropertiesBaseInterface extends NodePropertiesBaseInterface {
|
||||||
|
|
||||||
public const JSON_TYPE = 'chrono:entity';
|
public const JSON_TYPE = 'chrono:entity';
|
||||||
public const PROPERTY_DATA = 'data';
|
|
||||||
|
|
||||||
public function getDataRaw(): array|string|null;
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,22 +9,4 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace KTXF\Chrono\Entity;
|
namespace KTXF\Chrono\Entity;
|
||||||
|
|
||||||
abstract class EntityPropertiesMutableAbstract extends EntityPropertiesBaseAbstract implements EntityPropertiesMutableInterface {
|
abstract class EntityPropertiesMutableAbstract extends EntityPropertiesBaseAbstract implements EntityPropertiesMutableInterface {}
|
||||||
|
|
||||||
public const JSON_TYPE = EntityPropertiesBaseInterface::JSON_TYPE;
|
|
||||||
|
|
||||||
public function jsonDeserialize(array|string $data): static {
|
|
||||||
if (is_string($data)) {
|
|
||||||
$data = json_decode($data, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->data = $data;
|
|
||||||
|
|
||||||
return $this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function setDataRaw(array|string|null $value): static {
|
|
||||||
$this->data[self::PROPERTY_DATA] = $value;
|
|
||||||
return $this;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -11,8 +11,4 @@ namespace KTXF\Chrono\Entity;
|
|||||||
|
|
||||||
use KTXF\Resource\Provider\Node\NodePropertiesMutableInterface;
|
use KTXF\Resource\Provider\Node\NodePropertiesMutableInterface;
|
||||||
|
|
||||||
interface EntityPropertiesMutableInterface extends EntityPropertiesBaseInterface, NodePropertiesMutableInterface {
|
interface EntityPropertiesMutableInterface extends EntityPropertiesBaseInterface, NodePropertiesMutableInterface {}
|
||||||
|
|
||||||
public function setDataRaw(array|string|null $value): static;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SPDX-FileCopyrightText: Sebastian Krupinski <krupinski01@gmail.com>
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace KTXF\Chrono\Entity;
|
||||||
|
|
||||||
|
enum EntityType: string {
|
||||||
|
case Event = 'event';
|
||||||
|
case Task = 'task';
|
||||||
|
case Journal = 'journal';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Canonical derivation of the entity type from a serialized payload's
|
||||||
|
* own type marker.
|
||||||
|
*/
|
||||||
|
public static function fromData(array $data): self {
|
||||||
|
return self::tryFrom((string)($data['type'] ?? '')) ?? self::Event;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Constructs the typed entity object for this type. */
|
||||||
|
public function instantiate(): EventObject|TaskObject|JournalObject {
|
||||||
|
return match ($this) {
|
||||||
|
self::Event => new EventObject(),
|
||||||
|
self::Task => new TaskObject(),
|
||||||
|
self::Journal => new JournalObject(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -17,8 +17,6 @@ use KTXF\Chrono\Entity\Property\EventOccurrenceObject;
|
|||||||
class EventObject extends EventCommonObject {
|
class EventObject extends EventCommonObject {
|
||||||
|
|
||||||
// Meta Information
|
// Meta Information
|
||||||
public string $type = 'event';
|
|
||||||
public int $version = 1;
|
|
||||||
public string|null $urid = null;
|
public string|null $urid = null;
|
||||||
public ?DateTimeInterface $created = null;
|
public ?DateTimeInterface $created = null;
|
||||||
public ?DateTimeInterface $modified = null;
|
public ?DateTimeInterface $modified = null;
|
||||||
|
|||||||
@@ -9,11 +9,9 @@ use KTXF\Chrono\Entity\Property\AttachmentCollection;
|
|||||||
use KTXF\Chrono\Entity\Property\JournalStatusTypes;
|
use KTXF\Chrono\Entity\Property\JournalStatusTypes;
|
||||||
use KTXF\Chrono\Entity\Property\JournalVisibilityTypes;
|
use KTXF\Chrono\Entity\Property\JournalVisibilityTypes;
|
||||||
use KTXF\Chrono\Entity\Property\TagCollection;
|
use KTXF\Chrono\Entity\Property\TagCollection;
|
||||||
use KTXF\Json\JsonSerializableObject;
|
|
||||||
|
|
||||||
class JournalObject extends JsonSerializableObject {
|
class JournalObject extends EntityPropertiesMutableAbstract {
|
||||||
public string $type = 'journal';
|
public string $type = 'journal';
|
||||||
public int $version = 1;
|
|
||||||
public ?string $urid = null;
|
public ?string $urid = null;
|
||||||
public ?DateTimeInterface $created = null;
|
public ?DateTimeInterface $created = null;
|
||||||
public ?DateTimeInterface $modified = null;
|
public ?DateTimeInterface $modified = null;
|
||||||
|
|||||||
@@ -13,9 +13,9 @@ use DateInterval;
|
|||||||
use DateTime;
|
use DateTime;
|
||||||
use DateTimeImmutable;
|
use DateTimeImmutable;
|
||||||
use DateTimeZone;
|
use DateTimeZone;
|
||||||
use KTXF\Json\JsonSerializableObject;
|
use KTXF\Chrono\Entity\EntityPropertiesMutableAbstract;
|
||||||
|
|
||||||
class EventCommonObject extends JsonSerializableObject {
|
class EventCommonObject extends EntityPropertiesMutableAbstract {
|
||||||
|
|
||||||
public int|null $sequence = null;
|
public int|null $sequence = null;
|
||||||
public DateTimeZone|null $timeZone = null;
|
public DateTimeZone|null $timeZone = null;
|
||||||
|
|||||||
@@ -10,11 +10,9 @@ use KTXF\Chrono\Entity\Property\TagCollection;
|
|||||||
use KTXF\Chrono\Entity\Property\TaskRecurrenceObject;
|
use KTXF\Chrono\Entity\Property\TaskRecurrenceObject;
|
||||||
use KTXF\Chrono\Entity\Property\TaskStatusTypes;
|
use KTXF\Chrono\Entity\Property\TaskStatusTypes;
|
||||||
use KTXF\Chrono\Entity\Property\TaskSubtaskCollection;
|
use KTXF\Chrono\Entity\Property\TaskSubtaskCollection;
|
||||||
use KTXF\Json\JsonSerializableObject;
|
|
||||||
|
|
||||||
class TaskObject extends JsonSerializableObject {
|
class TaskObject extends EntityPropertiesMutableAbstract {
|
||||||
public string $type = 'task';
|
public string $type = 'task';
|
||||||
public int $version = 1;
|
|
||||||
public ?string $urid = null;
|
public ?string $urid = null;
|
||||||
public ?DateTimeInterface $created = null;
|
public ?DateTimeInterface $created = null;
|
||||||
public ?DateTimeInterface $modified = null;
|
public ?DateTimeInterface $modified = null;
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ namespace KTXF\Chrono\Service;
|
|||||||
use KTXF\Chrono\Entity\EntityBaseInterface;
|
use KTXF\Chrono\Entity\EntityBaseInterface;
|
||||||
use KTXF\Chrono\Entity\EntityMutableInterface;
|
use KTXF\Chrono\Entity\EntityMutableInterface;
|
||||||
use KTXF\Chrono\Entity\EntityPropertiesMutableInterface;
|
use KTXF\Chrono\Entity\EntityPropertiesMutableInterface;
|
||||||
|
use KTXF\Chrono\Entity\EntityType;
|
||||||
use KTXF\Resource\Identifier\CollectionIdentifierInterface;
|
use KTXF\Resource\Identifier\CollectionIdentifierInterface;
|
||||||
use KTXF\Resource\Identifier\EntityIdentifierInterface;
|
use KTXF\Resource\Identifier\EntityIdentifierInterface;
|
||||||
|
|
||||||
@@ -31,13 +32,14 @@ interface ServiceEntityMutableInterface {
|
|||||||
public const CAPABILITY_ENTITY_MOVE = 'EntityMove';
|
public const CAPABILITY_ENTITY_MOVE = 'EntityMove';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a fresh entity instance for composition
|
* Creates a fresh entity instance for composition, with its properties
|
||||||
|
* object constructed for the given entity type
|
||||||
*
|
*
|
||||||
* @since 2025.05.01
|
* @since 2025.05.01
|
||||||
*
|
*
|
||||||
* @return EntityMutableInterface Fresh entity object
|
* @return EntityMutableInterface Fresh entity object
|
||||||
*/
|
*/
|
||||||
public function entityFresh(): EntityMutableInterface;
|
public function entityFresh(EntityType $type = EntityType::Event): EntityMutableInterface;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates/imports an entity into a collection
|
* Creates/imports an entity into a collection
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ abstract class JsonSerializableCollection extends CollectionAbstract implements
|
|||||||
if (in_array($this->typeValue, $this->primitiveTypes)) {
|
if (in_array($this->typeValue, $this->primitiveTypes)) {
|
||||||
if ($this->associative) {
|
if ($this->associative) {
|
||||||
foreach ($data as $key => $value) {
|
foreach ($data as $key => $value) {
|
||||||
$this[$key] = $value;
|
$this[$this->normalizeKey($key)] = $value;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
foreach ($data as $value) {
|
foreach ($data as $value) {
|
||||||
@@ -43,14 +43,24 @@ abstract class JsonSerializableCollection extends CollectionAbstract implements
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!in_array($this->typeValue, $this->primitiveTypes) && class_exists($this->typeValue)) {
|
if (!in_array($this->typeValue, $this->primitiveTypes) && is_subclass_of($this->typeValue, \BackedEnum::class)) {
|
||||||
|
if ($this->associative) {
|
||||||
|
foreach ($data as $key => $value) {
|
||||||
|
$this[$this->normalizeKey($key)] = ($this->typeValue)::from($value);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
foreach ($data as $value) {
|
||||||
|
$this[] = ($this->typeValue)::from($value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} elseif (!in_array($this->typeValue, $this->primitiveTypes) && class_exists($this->typeValue)) {
|
||||||
$reflection = new \ReflectionClass($this->typeValue);
|
$reflection = new \ReflectionClass($this->typeValue);
|
||||||
if ($reflection->implementsInterface(JsonDeserializable::class)) {
|
if ($reflection->implementsInterface(JsonDeserializable::class)) {
|
||||||
if ($this->associative) {
|
if ($this->associative) {
|
||||||
foreach ($data as $key => $value) {
|
foreach ($data as $key => $value) {
|
||||||
$instance = $reflection->newInstance();
|
$instance = $reflection->newInstance();
|
||||||
/** @var JsonDeserializable $instance */
|
/** @var JsonDeserializable $instance */
|
||||||
$this[$key] = $instance->jsonDeserialize($value);
|
$this[$this->normalizeKey($key)] = $instance->jsonDeserialize($value);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
foreach ($data as $value) {
|
foreach ($data as $value) {
|
||||||
@@ -65,4 +75,13 @@ abstract class JsonSerializableCollection extends CollectionAbstract implements
|
|||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JSON round trips turn numeric string keys into integers (and list-shaped
|
||||||
|
* payloads carry integer keys), which a string-keyed collection would
|
||||||
|
* otherwise reject on offsetSet.
|
||||||
|
*/
|
||||||
|
private function normalizeKey(int|string $key): int|string {
|
||||||
|
return $this->typeKey === self::TYPE_STRING ? (string)$key : $key;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -77,7 +77,14 @@ abstract class JsonSerializableObject implements JsonSerializable, JsonDeseriali
|
|||||||
if (!empty($this->serializableProperties) && !in_array($key, $this->serializableProperties)) {
|
if (!empty($this->serializableProperties) && !in_array($key, $this->serializableProperties)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Null means absent: leave the property at its default rather
|
||||||
|
// than passing null into an object's jsonDeserialize/enum
|
||||||
|
// coercion or overwriting an initialized collection.
|
||||||
|
if ($value === null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
$type = gettype($this->$key);
|
$type = gettype($this->$key);
|
||||||
|
|
||||||
// Handle JsonDeserializable objects
|
// Handle JsonDeserializable objects
|
||||||
@@ -101,9 +108,10 @@ abstract class JsonSerializableObject implements JsonSerializable, JsonDeseriali
|
|||||||
$enumClass = get_class($this->$key);
|
$enumClass = get_class($this->$key);
|
||||||
$this->$key = $enumClass::from($value);
|
$this->$key = $enumClass::from($value);
|
||||||
}
|
}
|
||||||
// Handle regular values
|
// Handle regular values, coercing by declared type when the
|
||||||
|
// current value is null and carries no instance to key off
|
||||||
else {
|
else {
|
||||||
$this->$key = $value;
|
$this->$key = $this->coerceValue($key, $value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -111,6 +119,48 @@ abstract class JsonSerializableObject implements JsonSerializable, JsonDeseriali
|
|||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Coerces a decoded JSON value to the property's declared type. Needed for
|
||||||
|
* properties whose current value is null: the instanceof-based branches
|
||||||
|
* above cannot dispatch without an instance, and assigning the raw string
|
||||||
|
* to a typed property (DateTime, enum, ...) would raise a TypeError.
|
||||||
|
*/
|
||||||
|
private function coerceValue(string $key, mixed $value): mixed {
|
||||||
|
$type = (new \ReflectionProperty($this, $key))->getType();
|
||||||
|
if ($type instanceof \ReflectionNamedType) {
|
||||||
|
$names = [$type->getName()];
|
||||||
|
} elseif ($type instanceof \ReflectionUnionType) {
|
||||||
|
$names = array_map(static fn($member) => $member->getName(), $type->getTypes());
|
||||||
|
} else {
|
||||||
|
return $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($names as $name) {
|
||||||
|
if (!class_exists($name) && !interface_exists($name) && !enum_exists($name)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (is_string($value) && is_a($name, DateTimeInterface::class, true)) {
|
||||||
|
return $name === \DateTime::class ? new \DateTime($value) : new \DateTimeImmutable($value);
|
||||||
|
}
|
||||||
|
if (is_string($value) && is_a($name, DateTimeZone::class, true)) {
|
||||||
|
return new DateTimeZone($value);
|
||||||
|
}
|
||||||
|
if (is_string($value) && is_a($name, DateInterval::class, true)) {
|
||||||
|
return $this->toDateInterval($value);
|
||||||
|
}
|
||||||
|
if ((is_string($value) || is_int($value)) && is_subclass_of($name, \BackedEnum::class)) {
|
||||||
|
return $name::from($value);
|
||||||
|
}
|
||||||
|
if ((is_array($value) || is_string($value)) && is_subclass_of($name, JsonDeserializable::class)) {
|
||||||
|
/** @var JsonDeserializable $instance */
|
||||||
|
$instance = new $name();
|
||||||
|
return $instance->jsonDeserialize($value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $value;
|
||||||
|
}
|
||||||
|
|
||||||
protected function fromDateInterval(DateInterval $interval): string {
|
protected function fromDateInterval(DateInterval $interval): string {
|
||||||
$spec = '';
|
$spec = '';
|
||||||
|
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ class CollectionAbstract extends \ArrayObject {
|
|||||||
* @param class-string<T>|string|null $typeValue
|
* @param class-string<T>|string|null $typeValue
|
||||||
* @param class-string<TKey>|string|null $typeKey
|
* @param class-string<TKey>|string|null $typeKey
|
||||||
*/
|
*/
|
||||||
public function __construct(array $data = [], string $typeValue, string|null $typeKey = null) {
|
public function __construct(array $data = [], string $typeValue = 'string', string|null $typeKey = null) {
|
||||||
// Ensure that all data entries are of the specified type
|
// Ensure that all data entries are of the specified type
|
||||||
$this->typeValue = $typeValue;
|
$this->typeValue = $typeValue;
|
||||||
if ($typeKey !== null) {
|
if ($typeKey !== null) {
|
||||||
|
|||||||
Reference in New Issue
Block a user