Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
173 changes: 127 additions & 46 deletions src/Database/Database.php
Original file line number Diff line number Diff line change
Expand Up @@ -387,11 +387,34 @@ class Database
*/
protected static array $filters = [];

protected static bool $defaultFiltersRegistered = false;

protected static int $filtersVersion = 0;

/**
* @var array<array<string, mixed>>|null
*/
private static ?array $tenantlessInternalAttributes = null;

/**
* @var array<string, array{encode: callable, decode: callable, signature: string}>
*/
protected array $instanceFilters = [];

/**
* @var array<string, string>
*/
private array $filterSignatures = [];

private string $filterSignaturesEncoded = '';

private int $filterSignaturesVersion = -1;

/**
* @var array<string, array{encode: callable, decode: callable, signature: string}>
*/
private array $filterSignaturesSource = [];

/**
* @var array<string, array<string, callable>>
*/
Expand Down Expand Up @@ -494,13 +517,30 @@ public function __construct(

$this->setAuthorization(new Authorization());

self::registerDefaultFilters();
}

/**
* Registers the built-in filters on first touch of the registry, so an
* explicit addFilter() always wins regardless of whether it ran before or
* after the first instance. The flag is set first: addFilter() calls back
* into this, and the guard is what terminates that recursion.
*/
private static function registerDefaultFilters(): void
{
if (self::$defaultFiltersRegistered) {
return;
}

self::$defaultFiltersRegistered = true;

self::addFilter(
'json',
/**
* @param mixed $value
* @return mixed
*/
function (mixed $value) {
static function (mixed $value) {
$value = ($value instanceof Document) ? $value->getArrayCopy() : $value;

if (!is_array($value) && !$value instanceof \stdClass) {
Expand All @@ -514,7 +554,7 @@ function (mixed $value) {
* @return mixed
* @throws Exception
*/
function (mixed $value) {
static function (mixed $value) {
if (!is_string($value)) {
return $value;
}
Expand All @@ -524,7 +564,7 @@ function (mixed $value) {
if (array_key_exists('$id', $value)) {
return new Document($value);
} else {
$value = array_map(function ($item) {
$value = array_map(static function ($item) {
if (is_array($item) && array_key_exists('$id', $item)) { // if `$id` exists, create a Document instance
return new Document($item);
}
Expand All @@ -542,7 +582,7 @@ function (mixed $value) {
* @param mixed $value
* @return mixed
*/
function (mixed $value) {
static function (mixed $value) {
if (is_null($value)) {
return;
}
Expand All @@ -558,7 +598,7 @@ function (mixed $value) {
* @param string|null $value
* @return string|null
*/
function (?string $value) {
static function (?string $value) {
return DateTime::formatTz($value);
}
);
Expand All @@ -567,83 +607,95 @@ function (?string $value) {
Database::VAR_POINT,
/**
* @param mixed $value
* @param Document $document
* @param Database $database
* @return mixed
*/
function (mixed $value) {
static function (mixed $value, Document $document, Database $database) {
if (!is_array($value)) {
return $value;
}
try {
return self::encodeSpatialData($value, Database::VAR_POINT);
return $database->encodeSpatialData($value, Database::VAR_POINT);
} catch (\Throwable) {
return $value;
}
},
/**
* @param string|null $value
* @param Document $document
* @param Database $database
* @return array|null
*/
function (?string $value) {
static function (?string $value, Document $document, Database $database) {
if ($value === null) {
return null;
}
return $this->adapter->decodePoint($value);
return $database->adapter->decodePoint($value);
}
);

self::addFilter(
Database::VAR_LINESTRING,
/**
* @param mixed $value
* @param Document $document
* @param Database $database
* @return mixed
*/
function (mixed $value) {
static function (mixed $value, Document $document, Database $database) {
if (!is_array($value)) {
return $value;
}
try {
return self::encodeSpatialData($value, Database::VAR_LINESTRING);
return $database->encodeSpatialData($value, Database::VAR_LINESTRING);
} catch (\Throwable) {
return $value;
}
},
/**
* @param string|null $value
* @param Document $document
* @param Database $database
* @return array|null
*/
function (?string $value) {
static function (?string $value, Document $document, Database $database) {
if (is_null($value)) {
return null;
}
return $this->adapter->decodeLinestring($value);
return $database->adapter->decodeLinestring($value);
}
);

self::addFilter(
Database::VAR_POLYGON,
/**
* @param mixed $value
* @param Document $document
* @param Database $database
* @return mixed
*/
function (mixed $value) {
static function (mixed $value, Document $document, Database $database) {
if (!is_array($value)) {
return $value;
}
try {
return self::encodeSpatialData($value, Database::VAR_POLYGON);
return $database->encodeSpatialData($value, Database::VAR_POLYGON);
} catch (\Throwable) {
return $value;
}
},
/**
* @param string|null $value
* @param Document $document
* @param Database $database
* @return array|null
*/
function (?string $value) {
static function (?string $value, Document $document, Database $database) {
if (is_null($value)) {
return null;
}
return $this->adapter->decodePolygon($value);
return $database->adapter->decodePolygon($value);
}
);

Expand All @@ -653,7 +705,7 @@ function (?string $value) {
* @param mixed $value
* @return mixed
*/
function (mixed $value) {
static function (mixed $value) {
if (!\is_array($value)) {
return $value;
}
Expand All @@ -672,7 +724,7 @@ function (mixed $value) {
* @param string|null $value
* @return mixed
*/
function (?string $value) {
static function (?string $value) {
if (is_null($value)) {
return null;
}
Expand All @@ -690,7 +742,7 @@ function (?string $value) {
* @param mixed $value
* @return mixed
*/
function (mixed $value) {
static function (mixed $value) {
if (!\is_array($value) && !$value instanceof \stdClass) {
return $value;
}
Expand All @@ -701,7 +753,7 @@ function (mixed $value) {
* @param mixed $value
* @return array|null
*/
function (mixed $value) {
static function (mixed $value) {
if (is_null($value)) {
return;
}
Expand Down Expand Up @@ -9285,11 +9337,15 @@ public function sum(string $collection, string $attribute, array $queries = [],
*/
public static function addFilter(string $name, callable $encode, callable $decode): void
{
self::registerDefaultFilters();

self::$filters[$name] = [
'encode' => $encode,
'decode' => $decode,
'signature' => self::computeCallableSignature($encode) . ':' . self::computeCallableSignature($decode),
];

self::$filtersVersion++;
}

/**
Expand Down Expand Up @@ -9911,15 +9967,14 @@ public function convertQuery(Document $collection, Query $query): Query
*/
public function getInternalAttributes(): array
{
$attributes = self::INTERNAL_ATTRIBUTES;

if (!$this->adapter->getSharedTables()) {
$attributes = \array_filter(Database::INTERNAL_ATTRIBUTES, function ($attribute) {
return $attribute['$id'] !== '$tenant';
});
if ($this->adapter->getSharedTables()) {
return self::INTERNAL_ATTRIBUTES;
}

return $attributes;
return self::$tenantlessInternalAttributes ??= \array_values(\array_filter(
self::INTERNAL_ATTRIBUTES,
fn (array $attribute): bool => $attribute['$id'] !== '$tenant',
));
}

/**
Expand Down Expand Up @@ -9990,11 +10045,10 @@ public function getCacheKeys(string $collectionId, ?string $documentId = null, a
$sortedSelects = $selects;
\sort($sortedSelects);

$payload = \json_encode([
'selects' => $sortedSelects,
'relationships' => $this->resolveRelationships,
'filters' => $this->getActiveFilterSignatures(),
]) ?: '';
$payload = ($this->resolveRelationships ? '1' : '0')
. ':' . $this->getFilterSignatureKey()
. ':' . ($sortedSelects === [] ? '' : (\json_encode($sortedSelects) ?: ''));

$documentHashKey = $documentKey . ':' . \md5($payload);
}

Expand Down Expand Up @@ -10136,33 +10190,60 @@ private function normalizeQueryCacheQueryValue(mixed $value): mixed
*/
private function getActiveFilterSignatures(): array
{
$filterSignatures = [];
if (!$this->filter) {
return $filterSignatures;
return [];
}

$this->refreshFilterSignatures();

return $this->disabledFilters
? \array_diff_key($this->filterSignatures, $this->disabledFilters)
: $this->filterSignatures;
}

private function refreshFilterSignatures(): void
{
if (
$this->filterSignaturesVersion === self::$filtersVersion
&& $this->filterSignaturesSource === $this->instanceFilters
) {
return;
}

$disabled = $this->disabledFilters ?? [];
$signatures = [];

foreach (self::$filters as $name => $callbacks) {
if (isset($disabled[$name])) {
continue;
}
if (\array_key_exists($name, $this->instanceFilters)) {
continue;
}
$filterSignatures[$name] = $callbacks['signature'];
$signatures[$name] = $callbacks['signature'];
}

foreach ($this->instanceFilters as $name => $callbacks) {
if (isset($disabled[$name])) {
continue;
}
$filterSignatures[$name] = $callbacks['signature'];
$signatures[$name] = $callbacks['signature'];
}

\ksort($signatures);

$this->filterSignatures = $signatures;
$this->filterSignaturesEncoded = \json_encode($signatures) ?: '';
$this->filterSignaturesVersion = self::$filtersVersion;
$this->filterSignaturesSource = $this->instanceFilters;
}

private function getFilterSignatureKey(): string
{
if (!$this->filter) {
return '';
}

if ($this->disabledFilters) {
return \json_encode($this->getActiveFilterSignatures()) ?: '';
}

\ksort($filterSignatures);
$this->refreshFilterSignatures();

return $filterSignatures;
return $this->filterSignaturesEncoded;
}

private static function computeCallableSignature(callable $callable): string
Expand Down
Loading
Loading