6 Commits

Author SHA1 Message Date
Sebastian bb13afe810 chore(deps): update typescript-eslint monorepo to v8.64.0
PHP Unit Tests / test (pull_request) Failing after 13m35s
JS Unit Tests / test (pull_request) Failing after 13m42s
Build Test / build (pull_request) Failing after 13m45s
2026-07-18 03:07:56 +00:00
Sebastian eda7e0e8cc fix(shared): hydrate enum members and coerce keys in collections
Signed-off-by: Sebastian Krupinski <krupinski01@gmail.com>
2026-07-17 15:52:07 -04:00
Sebastian f9f824a2f0 fix(shared): deserialize into null-initialized typed properties
Signed-off-by: Sebastian Krupinski <krupinski01@gmail.com>
2026-07-17 15:51:47 -04:00
Sebastian 27b973f3db fix(shared): remove optional-before-required constructor parameter
Signed-off-by: Sebastian Krupinski <krupinski01@gmail.com>
2026-07-17 15:51:09 -04:00
Sebastian 28bc360106 refactor: chrono entity classes
Signed-off-by: Sebastian Krupinski <krupinski01@gmail.com>
2026-07-17 15:43:23 -04:00
Sebastian 7e81e130c9 fix: pass asset request to server if does not exist on system
Signed-off-by: Sebastian Krupinski <krupinski01@gmail.com>
2026-07-17 13:24:27 -04:00
20 changed files with 244 additions and 133 deletions
+4 -2
View File
@@ -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 {
+4 -2
View File
@@ -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";
} }
+61 -61
View File
@@ -1510,17 +1510,17 @@
"optional": true "optional": true
}, },
"node_modules/@typescript-eslint/eslint-plugin": { "node_modules/@typescript-eslint/eslint-plugin": {
"version": "8.63.0", "version": "8.64.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.63.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.64.0.tgz",
"integrity": "sha512-rvwSgqT+DHpWdzfSzPatRLm02a0GlESt++9iy3hLCDY4BgkaLcl8LBi9Yh7XGFBpwcBE/K3024QuXWTpbz4FfQ==", "integrity": "sha512-CGvQPBxN3wZLu6Rz2kFUpZeoCm78xUic92ck39KPePkO1NPOwjCqdQnm5Q87tpWw9vcBvW8XLrDXjH9PWYtJ3Q==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@eslint-community/regexpp": "^4.12.2", "@eslint-community/regexpp": "^4.12.2",
"@typescript-eslint/scope-manager": "8.63.0", "@typescript-eslint/scope-manager": "8.64.0",
"@typescript-eslint/type-utils": "8.63.0", "@typescript-eslint/type-utils": "8.64.0",
"@typescript-eslint/utils": "8.63.0", "@typescript-eslint/utils": "8.64.0",
"@typescript-eslint/visitor-keys": "8.63.0", "@typescript-eslint/visitor-keys": "8.64.0",
"ignore": "^7.0.5", "ignore": "^7.0.5",
"natural-compare": "^1.4.0", "natural-compare": "^1.4.0",
"ts-api-utils": "^2.5.0" "ts-api-utils": "^2.5.0"
@@ -1533,7 +1533,7 @@
"url": "https://opencollective.com/typescript-eslint" "url": "https://opencollective.com/typescript-eslint"
}, },
"peerDependencies": { "peerDependencies": {
"@typescript-eslint/parser": "^8.63.0", "@typescript-eslint/parser": "^8.64.0",
"eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
"typescript": ">=4.8.4 <6.1.0" "typescript": ">=4.8.4 <6.1.0"
} }
@@ -1549,15 +1549,15 @@
} }
}, },
"node_modules/@typescript-eslint/parser": { "node_modules/@typescript-eslint/parser": {
"version": "8.63.0", "version": "8.64.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.63.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.64.0.tgz",
"integrity": "sha512-gwh4gvvlaVDKKxyfxMG+Gnu1u9X0OQBwyGLkbwB65dIzBKnxeRiJlNFqlI3zwVhNXJIs6qV7mlFCn/BIajlVig==", "integrity": "sha512-KA0OshtlcCCXmbfqyZkM5pV3/WNraJf7DkJRLpyrmwPtud57H5BDX7C3k0LPSPxpprfRL+cJDGabF10mvNCoCw==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@typescript-eslint/scope-manager": "8.63.0", "@typescript-eslint/scope-manager": "8.64.0",
"@typescript-eslint/types": "8.63.0", "@typescript-eslint/types": "8.64.0",
"@typescript-eslint/typescript-estree": "8.63.0", "@typescript-eslint/typescript-estree": "8.64.0",
"@typescript-eslint/visitor-keys": "8.63.0", "@typescript-eslint/visitor-keys": "8.64.0",
"debug": "^4.4.3" "debug": "^4.4.3"
}, },
"engines": { "engines": {
@@ -1573,13 +1573,13 @@
} }
}, },
"node_modules/@typescript-eslint/project-service": { "node_modules/@typescript-eslint/project-service": {
"version": "8.63.0", "version": "8.64.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.63.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.64.0.tgz",
"integrity": "sha512-e5dh0/UI0ok53AlZ5wRkXCB32z/f2jUZqPR/ygAw5WYaSw8j9EoJWlS7wQjr/dmOaqWjnPIn2m+HhVPCMWGZVQ==", "integrity": "sha512-tk4WpOJ6IEbGrVHaNmM0YRrwAD3exZlIK3iadQNAxh4YKk6jvUQ4ecq18n+v7+meh+cJ3j+D8nbk8sRKhlwLQg==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@typescript-eslint/tsconfig-utils": "^8.63.0", "@typescript-eslint/tsconfig-utils": "^8.64.0",
"@typescript-eslint/types": "^8.63.0", "@typescript-eslint/types": "^8.64.0",
"debug": "^4.4.3" "debug": "^4.4.3"
}, },
"engines": { "engines": {
@@ -1594,13 +1594,13 @@
} }
}, },
"node_modules/@typescript-eslint/scope-manager": { "node_modules/@typescript-eslint/scope-manager": {
"version": "8.63.0", "version": "8.64.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.63.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.64.0.tgz",
"integrity": "sha512-uUyfMWCnDSN8bCpcrY8nGP2BLkQ9Xn0GsipcONcpIDWhwhO4ZSyHvyS14U3X75mzxWxL3I2UZIrenTzdzcJO8A==", "integrity": "sha512-CXEaFdYXjSTgKhisNkwCcJwTP8Pl+fmRrEQrri4nm3vU743bALrxzLmq7fHG/7e6a5xO0lDYeURpZmBuhHk54w==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@typescript-eslint/types": "8.63.0", "@typescript-eslint/types": "8.64.0",
"@typescript-eslint/visitor-keys": "8.63.0" "@typescript-eslint/visitor-keys": "8.64.0"
}, },
"engines": { "engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0" "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -1611,9 +1611,9 @@
} }
}, },
"node_modules/@typescript-eslint/tsconfig-utils": { "node_modules/@typescript-eslint/tsconfig-utils": {
"version": "8.63.0", "version": "8.64.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.63.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.64.0.tgz",
"integrity": "sha512-sUAbkulqBAsncKnbRP3+7CtQFRKicexnj7ZwNC6ddCR7EmrXvjvdCYMJbUIqMd6lwoEriZjwLo08aS5tSjVMHg==", "integrity": "sha512-2yo8rRNKuzbVWQp5kslhANqZ2uDAeROQHBRZNPu8JDsHmeFNj/XJJhX/FhNUWmkHHvoNsKa6+tHJiig87EzsQw==",
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0" "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -1627,15 +1627,15 @@
} }
}, },
"node_modules/@typescript-eslint/type-utils": { "node_modules/@typescript-eslint/type-utils": {
"version": "8.63.0", "version": "8.64.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.63.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.64.0.tgz",
"integrity": "sha512-Nzzh/OGxVCOjObjaj1CQF2RUasyYy2Jfuh+zZ3PjLzG2fYRriAiZLib9UKtO+CpQAS3YHiAS+ckZDclwqI1TPA==", "integrity": "sha512-XWG4Fmmv/6SvyS9nH8jWrKs6terwJvE8cyRt1CzYYqzp9OrPhCT4cMc/f7C6RZCwG+qMmiffJS1/qJP8G1URtg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@typescript-eslint/types": "8.63.0", "@typescript-eslint/types": "8.64.0",
"@typescript-eslint/typescript-estree": "8.63.0", "@typescript-eslint/typescript-estree": "8.64.0",
"@typescript-eslint/utils": "8.63.0", "@typescript-eslint/utils": "8.64.0",
"debug": "^4.4.3", "debug": "^4.4.3",
"ts-api-utils": "^2.5.0" "ts-api-utils": "^2.5.0"
}, },
@@ -1652,9 +1652,9 @@
} }
}, },
"node_modules/@typescript-eslint/types": { "node_modules/@typescript-eslint/types": {
"version": "8.63.0", "version": "8.64.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.63.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.64.0.tgz",
"integrity": "sha512-xyLtl9DUBBFrcJS4x2pIqGLH68/tC2uOa4Z7pUteW09D3bXnnXUom4dyPikzWgB7llmIc1zoeI3aoUdC4rPK/Q==", "integrity": "sha512-qjhfuTfLXjA4IOzXvz0rTjT01BqEiIgPoUeMwiEjnaHKJMTNo8rH5pYW1a2L/0Dnux2fPC85AeyJoWaGa8WxTA==",
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0" "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -1665,15 +1665,15 @@
} }
}, },
"node_modules/@typescript-eslint/typescript-estree": { "node_modules/@typescript-eslint/typescript-estree": {
"version": "8.63.0", "version": "8.64.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.63.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.64.0.tgz",
"integrity": "sha512-ygBkU+B7ex5UI/gKhaqexWev79uISfIv7XQCRNYO/jmD8rGLPyWLAb3KMRT6nd8Gt9bmUBi9+iX6tBdYfOY81Q==", "integrity": "sha512-Pztpsn1aCE1oWDvDEfUk31nngvvF7vUB5SwHFEaZIFpvw7WJtqUHHL4plBZDA9HfWJJjL13BdG0YrJInTUvoVA==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@typescript-eslint/project-service": "8.63.0", "@typescript-eslint/project-service": "8.64.0",
"@typescript-eslint/tsconfig-utils": "8.63.0", "@typescript-eslint/tsconfig-utils": "8.64.0",
"@typescript-eslint/types": "8.63.0", "@typescript-eslint/types": "8.64.0",
"@typescript-eslint/visitor-keys": "8.63.0", "@typescript-eslint/visitor-keys": "8.64.0",
"debug": "^4.4.3", "debug": "^4.4.3",
"minimatch": "^10.2.2", "minimatch": "^10.2.2",
"semver": "^7.7.3", "semver": "^7.7.3",
@@ -1692,16 +1692,16 @@
} }
}, },
"node_modules/@typescript-eslint/utils": { "node_modules/@typescript-eslint/utils": {
"version": "8.63.0", "version": "8.64.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.63.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.64.0.tgz",
"integrity": "sha512-fUKaeAvrTuQg/Tgt3nliAUSZHJM6DlCcfyEmxCvlX8kieWSStBX+5O5Fnidtc3i2JrH+9c/GL4RY2iasd/GPTA==", "integrity": "sha512-aJUGVB3+U0htrrCjoA8qukw8cm8fNCGAxK/tVoS70k8aeb7DETKeFozRiVFIwEeN9WJLsjaP3ph8I60tY2XZoQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@eslint-community/eslint-utils": "^4.9.1", "@eslint-community/eslint-utils": "^4.9.1",
"@typescript-eslint/scope-manager": "8.63.0", "@typescript-eslint/scope-manager": "8.64.0",
"@typescript-eslint/types": "8.63.0", "@typescript-eslint/types": "8.64.0",
"@typescript-eslint/typescript-estree": "8.63.0" "@typescript-eslint/typescript-estree": "8.64.0"
}, },
"engines": { "engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0" "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -1716,12 +1716,12 @@
} }
}, },
"node_modules/@typescript-eslint/visitor-keys": { "node_modules/@typescript-eslint/visitor-keys": {
"version": "8.63.0", "version": "8.64.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.63.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.64.0.tgz",
"integrity": "sha512-UexrHGnGTpbuQHct2ExOc2ZcFbGUS9FOesCxxqdBGcpI1BxYu/LZ6U8Aq6/72XtF/qRBk9nhuGHFJIXXMhPMdw==", "integrity": "sha512-mrtuL8Nsn6gi2H4mo5KMTp823M+3Q19Ew/i+Zlikq20tIMm99C3Ez0dCmkWWnxut20esQvTg8aUSEhMcAOXhEw==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@typescript-eslint/types": "8.63.0", "@typescript-eslint/types": "8.64.0",
"eslint-visitor-keys": "^5.0.0" "eslint-visitor-keys": "^5.0.0"
}, },
"engines": { "engines": {
@@ -5960,16 +5960,16 @@
} }
}, },
"node_modules/typescript-eslint": { "node_modules/typescript-eslint": {
"version": "8.63.0", "version": "8.64.0",
"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.63.0.tgz", "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.64.0.tgz",
"integrity": "sha512-xgwXyzG4sK9ALkBxbyGkTMMOS+imnW65iPhxCQMK83KhxyoDNW7l+IDqEf9vMdoUidHpOoS967RCq4eMiTexwQ==", "integrity": "sha512-0qg+pDNMnqYzqH9AnNK+39tejHvsShUOUUoRUgtnTGE7QuMZhiFDnozq8nHJVq+Wae6NMLKNWLg5WmkcC/ndyQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@typescript-eslint/eslint-plugin": "8.63.0", "@typescript-eslint/eslint-plugin": "8.64.0",
"@typescript-eslint/parser": "8.63.0", "@typescript-eslint/parser": "8.64.0",
"@typescript-eslint/typescript-estree": "8.63.0", "@typescript-eslint/typescript-estree": "8.64.0",
"@typescript-eslint/utils": "8.63.0" "@typescript-eslint/utils": "8.64.0"
}, },
"engines": { "engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0" "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -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;
}
+33
View File
@@ -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(),
};
}
}
-2
View File
@@ -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;
+1 -3
View File
@@ -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;
+1 -3
View File
@@ -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
+22 -3
View File
@@ -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;
}
} }
+52 -2
View File
@@ -78,6 +78,13 @@ abstract class JsonSerializableObject implements JsonSerializable, JsonDeseriali
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) {