generated from Nodarx/template
feat: implement meta and content cache
Signed-off-by: Sebastian Krupinski <krupinski01@gmail.com>
This commit is contained in:
@@ -16,8 +16,8 @@ use KTXF\Mail\Collection\CollectionRoles;
|
||||
/**
|
||||
* IMAP Mail Collection Properties
|
||||
*
|
||||
* Backed by the same internal $data shape as the JMAP provider so that cache
|
||||
* documents are interchangeable with fromStore() / toStore().
|
||||
* Backed by the same internal $data shape as the JMAP provider so that
|
||||
* cache documents are interchangeable with fromCacheMeta() / toCacheMeta().
|
||||
*/
|
||||
class CollectionProperties extends CollectionPropertiesMutableAbstract
|
||||
{
|
||||
@@ -50,14 +50,14 @@ class CollectionProperties extends CollectionPropertiesMutableAbstract
|
||||
return $this;
|
||||
}
|
||||
|
||||
// ── Store (MongoDB cache) ────────────────────────────────────────────────
|
||||
// ── Cache (meta store) ───────────────────────────────────────────────────
|
||||
|
||||
public function toStore(): array
|
||||
public function toCacheMeta(): array
|
||||
{
|
||||
return $this->data;
|
||||
}
|
||||
|
||||
public function fromStore(array $data): static
|
||||
public function fromCacheMeta(array $data): static
|
||||
{
|
||||
$this->data = $data;
|
||||
return $this;
|
||||
|
||||
@@ -54,29 +54,38 @@ class CollectionResource extends CollectionMutableAbstract
|
||||
return $this;
|
||||
}
|
||||
|
||||
// ── Store (MongoDB cache) ────────────────────────────────────────────────
|
||||
// ── Cache (meta store) ───────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Serialise to a MongoDB document.
|
||||
* Serialise to a meta store document.
|
||||
*
|
||||
* The caller must inject the service UUID as `sid` before persisting.
|
||||
* The store adds the key fields it owns (sid, tid).
|
||||
*/
|
||||
public function toStore(): array
|
||||
public function toCacheMeta(): array
|
||||
{
|
||||
return array_merge(
|
||||
$this->data,
|
||||
[
|
||||
'name' => $this->data['identifier'],
|
||||
'properties' => $this->getProperties()->toStore(),
|
||||
],
|
||||
);
|
||||
return [
|
||||
'name' => (string) $this->data[static::PROPERTY_IDENTIFIER],
|
||||
'parent' => $this->data[static::PROPERTY_COLLECTION] ?? null,
|
||||
'signature' => $this->data[static::PROPERTY_SIGNATURE] ?? null,
|
||||
'properties' => $this->getProperties()->toCacheMeta(),
|
||||
];
|
||||
}
|
||||
|
||||
public function fromStore(array $data): static
|
||||
/**
|
||||
* Restore from a meta store document.
|
||||
*
|
||||
* Only the resource's own fields are read; store keys and harmonization state
|
||||
* on the same document are ignored.
|
||||
*/
|
||||
public function fromCacheMeta(array $data): static
|
||||
{
|
||||
$this->data = $data;
|
||||
$this->data[static::PROPERTY_IDENTIFIER] = (string) $data['name'];
|
||||
$this->data[static::PROPERTY_COLLECTION] = $data['parent'] ?? null;
|
||||
if (isset($data['signature'])) {
|
||||
$this->data[static::PROPERTY_SIGNATURE] = $data['signature'];
|
||||
}
|
||||
if (isset($data['properties'])) {
|
||||
$this->getProperties()->fromStore($data['properties']);
|
||||
$this->getProperties()->fromCacheMeta((array) $data['properties']);
|
||||
}
|
||||
return $this;
|
||||
}
|
||||
|
||||
@@ -18,6 +18,9 @@ use KTXF\Mail\Object\MessagePropertiesMutableInterface;
|
||||
*/
|
||||
class EntityResource extends EntityMutableAbstract {
|
||||
|
||||
/** Version of the meta store / content store formats; bump on any change to either */
|
||||
public const CACHE_SCHEMA_VERSION = 1;
|
||||
|
||||
public function __construct(
|
||||
string $provider = 'imap',
|
||||
string|int|null $service = null,
|
||||
@@ -63,6 +66,69 @@ class EntityResource extends EntityMutableAbstract {
|
||||
return $this;
|
||||
}
|
||||
|
||||
// ── Cache (meta store / content store) ───────────────────────────────────
|
||||
|
||||
/**
|
||||
* Serialise to a meta store document.
|
||||
*
|
||||
* The store adds the key fields it owns (sid, tid, uidValidity).
|
||||
*/
|
||||
public function toCacheMeta(): array
|
||||
{
|
||||
return [
|
||||
'mailbox' => (string) $this->data['collection'],
|
||||
'uid' => (int) $this->data['identifier'],
|
||||
'created' => $this->data['created'] ?? null,
|
||||
'schemaVersion' => self::CACHE_SCHEMA_VERSION,
|
||||
...$this->getProperties()->toCacheMeta(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore identity and mutable fields (flags) from a meta store document.
|
||||
*/
|
||||
public function fromCacheMeta(array $document): static
|
||||
{
|
||||
$this->data['collection'] = (string) $document['mailbox'];
|
||||
$this->data['identifier'] = (int) $document['uid'];
|
||||
|
||||
$this->getProperties()->fromCacheMeta($document);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Serialise the immutable message content for the content store (message.json).
|
||||
*/
|
||||
public function toCacheContent(): array
|
||||
{
|
||||
return [
|
||||
'schemaVersion' => self::CACHE_SCHEMA_VERSION,
|
||||
'created' => $this->data['created'] ?? null,
|
||||
'properties' => $this->getProperties()->toCacheContent(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore the immutable message content from the content store (message.json).
|
||||
*
|
||||
* @throws \UnexpectedValueException when the content was written with a different schema version
|
||||
*/
|
||||
public function fromCacheContent(array $content): static
|
||||
{
|
||||
if (($content['schemaVersion'] ?? null) !== self::CACHE_SCHEMA_VERSION) {
|
||||
throw new \UnexpectedValueException('Cached message content has an unsupported schema version');
|
||||
}
|
||||
|
||||
if (isset($content['created'])) {
|
||||
$this->data['created'] = $content['created'];
|
||||
}
|
||||
|
||||
$this->getProperties()->fromCacheContent($content['properties'] ?? []);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
|
||||
@@ -35,9 +35,9 @@ class MessageProperties extends MessagePropertiesMutableAbstract {
|
||||
$this->data[static::PROPERTY_IN_REPLY_TO] = $message->inReplyTo();
|
||||
}
|
||||
|
||||
//if ($message->references() !== []) {
|
||||
// $this->data[static::PROPERTY_REFERENCES] = $message->references();
|
||||
//}
|
||||
if ($message->references() !== []) {
|
||||
$this->data[static::PROPERTY_REFERENCES] = $message->references();
|
||||
}
|
||||
|
||||
$receivedAt = $message->receivedAt() ?? $message->internalDate();
|
||||
if ($receivedAt !== null) {
|
||||
@@ -109,6 +109,107 @@ class MessageProperties extends MessagePropertiesMutableAbstract {
|
||||
return $this;
|
||||
}
|
||||
|
||||
// ── Cache (meta store / content store) ───────────────────────────────────
|
||||
|
||||
/**
|
||||
* Serialise the fields the meta store needs for list, filter and sort.
|
||||
*
|
||||
* Dates are normalised to UTC so they sort correctly as strings; the original
|
||||
* values (with their offsets) are kept in the content store.
|
||||
*/
|
||||
public function toCacheMeta(): array
|
||||
{
|
||||
return [
|
||||
'received' => self::cacheDate($this->data[static::PROPERTY_RECEIVED] ?? null),
|
||||
'sent' => self::cacheDate($this->data[static::PROPERTY_SENT] ?? null),
|
||||
'size' => $this->data[static::PROPERTY_SIZE] ?? 0,
|
||||
'subject' => $this->data[static::PROPERTY_SUBJECT] ?? '',
|
||||
'from' => $this->data[static::PROPERTY_FROM] ?? null,
|
||||
'to' => $this->data[static::PROPERTY_TO] ?? [],
|
||||
'cc' => $this->data[static::PROPERTY_CC] ?? [],
|
||||
'urid' => $this->data[static::PROPERTY_URID] ?? null,
|
||||
'inReplyTo' => $this->data[static::PROPERTY_IN_REPLY_TO] ?? null,
|
||||
'references' => $this->data[static::PROPERTY_REFERENCES] ?? [],
|
||||
// list of set flags, not a map: keywords such as $Forwarded are not valid field names
|
||||
'flags' => array_keys(array_filter($this->data[static::PROPERTY_FLAGS] ?? [])),
|
||||
'hasAttachments' => ($this->data[static::PROPERTY_ATTACHMENTS] ?? []) !== [],
|
||||
'preview' => $this->cachePreview(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore the mutable fields from a meta store document.
|
||||
*
|
||||
* Only flags are taken from the meta store; everything else comes from the
|
||||
* content store, which holds the original values.
|
||||
*/
|
||||
public function fromCacheMeta(array $document): static
|
||||
{
|
||||
$this->data[static::PROPERTY_FLAGS] = array_fill_keys(
|
||||
array_map('strval', (array) ($document['flags'] ?? [])),
|
||||
true,
|
||||
);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Serialise the immutable message content for the content store.
|
||||
*/
|
||||
public function toCacheContent(): array
|
||||
{
|
||||
$content = $this->data;
|
||||
unset($content[static::PROPERTY_FLAGS]);
|
||||
|
||||
return $content;
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore the immutable message content from the content store, keeping any flags already set.
|
||||
*/
|
||||
public function fromCacheContent(array $content): static
|
||||
{
|
||||
unset($content[static::PROPERTY_FLAGS]);
|
||||
$flags = $this->data[static::PROPERTY_FLAGS] ?? null;
|
||||
|
||||
$this->data = $content;
|
||||
if ($flags !== null) {
|
||||
$this->data[static::PROPERTY_FLAGS] = $flags;
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
private function cachePreview(int $length = 200): string
|
||||
{
|
||||
$text = $this->getBodyTextPlain();
|
||||
if ($text === null) {
|
||||
$html = $this->getBodyTextHtml();
|
||||
$text = $html !== null
|
||||
? html_entity_decode(strip_tags((string) preg_replace('/<(style|script)\b[^>]*>.*?<\/\1>/is', ' ', $html)), ENT_QUOTES | ENT_HTML5, 'UTF-8')
|
||||
: '';
|
||||
}
|
||||
|
||||
$text = trim((string) preg_replace('/\s+/u', ' ', $text));
|
||||
|
||||
return mb_substr($text, 0, $length);
|
||||
}
|
||||
|
||||
private static function cacheDate(?string $value): ?string
|
||||
{
|
||||
if ($value === null || $value === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
return (new DateTimeImmutable($value))
|
||||
->setTimezone(new \DateTimeZone('UTC'))
|
||||
->format('Y-m-d\TH:i:s\Z');
|
||||
} catch (\Exception) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively collect attachment parts from body structure
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user