Skip to content
Draft
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
2 changes: 1 addition & 1 deletion .github/workflows/phpstan.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,4 @@ jobs:
with:
path: lib/ tests/
level: 9
php_version: 8.1
php_version: 8.2
18 changes: 17 additions & 1 deletion .wordlist.txt
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,20 @@ Traceback
nodejs
npm
fediverse
readme
readme
phpnomad
datastore
schemas
lookups
DateCreatedFactory
DateModifiedFactory
PHPNomad
PrimaryKeyFactory
PostsTable
getUnprefixedName
DatabaseServiceProvider
IdentifiableDatabaseDatastoreHandler
TableSchemaService
WithDatastoreHandlerMethods
PostDatabaseDatastoreHandler
txt
5 changes: 3 additions & 2 deletions lib/Abstracts/IdentifiableDatabaseDatastoreHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace PHPNomad\Database\Abstracts;

use PHPNomad\Database\Interfaces\DatabaseHandler;
use PHPNomad\Database\Providers\DatabaseServiceProvider;
use PHPNomad\Database\Services\TableSchemaService;
use PHPNomad\Database\Traits\WithDatastoreHandlerMethods;
Expand All @@ -11,7 +12,7 @@
use PHPNomad\Datastore\Interfaces\DatastoreHasPrimaryKey;
use PHPNomad\Datastore\Interfaces\DatastoreHasWhere;

abstract class IdentifiableDatabaseDatastoreHandler implements Datastore, DatastoreHasPrimaryKey, DatastoreHasWhere, DatastoreHasCounts
abstract class IdentifiableDatabaseDatastoreHandler implements DatabaseHandler, Datastore, DatastoreHasPrimaryKey, DatastoreHasWhere, DatastoreHasCounts
{
use WithDatastoreHandlerMethods;

Expand Down Expand Up @@ -45,4 +46,4 @@ public function update($id, array $attributes): void
{
$this->updateCompound(['id' => $id], $attributes);
}
}
}
5 changes: 3 additions & 2 deletions lib/Factories/DatabaseDatastoreHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,15 @@
namespace PHPNomad\Database\Factories;

use PHPNomad\Database\Interfaces\DatabaseContextProvider;
use PHPNomad\Database\Interfaces\DatabaseHandler;
use PHPNomad\Database\Providers\DatabaseServiceProvider;
use PHPNomad\Database\Services\TableSchemaService;
use PHPNomad\Database\Traits\WithDatastoreHandlerMethods;
use PHPNomad\Datastore\Interfaces\Datastore;
use PHPNomad\Datastore\Interfaces\DatastoreHasCounts;
use PHPNomad\Datastore\Interfaces\DatastoreHasWhere;

class DatabaseDatastoreHandler implements Datastore, DatastoreHasWhere, DatastoreHasCounts
class DatabaseDatastoreHandler implements DatabaseHandler, Datastore, DatastoreHasWhere, DatastoreHasCounts
{
use WithDatastoreHandlerMethods;

Expand All @@ -28,4 +29,4 @@ public function __construct(
$this->serviceProvider = $serviceProvider;
$this->contextProvider = $contextProvider;
}
}
}
15 changes: 15 additions & 0 deletions lib/Interfaces/DatabaseHandler.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?php

namespace PHPNomad\Database\Interfaces;

use PHPNomad\Database\Providers\DatabaseServiceProvider;

/** A database-backed datastore handler that can be cloned for one operation. */
interface DatabaseHandler
{
public function getDatabaseTable(): Table;

public function getDatabaseServiceProvider(): DatabaseServiceProvider;

public function cloneForOperation(DatabaseServiceProvider $serviceProvider): DatabaseHandler;
}
18 changes: 18 additions & 0 deletions lib/Interfaces/OperationDatabaseProviderFactory.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?php

namespace PHPNomad\Database\Interfaces;

use PHPNomad\Database\Providers\DatabaseServiceProvider;
use PHPNomad\Database\Services\OperationCacheableService;
use PHPNomad\Database\Services\OperationEventStrategy;

/** Creates a resource-bound provider for one coordinated operation. */
interface OperationDatabaseProviderFactory
{
public function create(
DatabaseHandler $handler,
QueryStrategy $queryStrategy,
OperationCacheableService $cache,
OperationEventStrategy $events
): DatabaseServiceProvider;
}
37 changes: 37 additions & 0 deletions lib/Models/CoordinatedOperationResult.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<?php

namespace PHPNomad\Database\Models;

use Throwable;

/** The callback value and post-commit publication failures. */
class CoordinatedOperationResult
{
private mixed $value;

/** @var list<Throwable> */
private array $publicationFailures;

/** @param list<Throwable> $publicationFailures */
public function __construct(mixed $value, array $publicationFailures = [])
{
$this->value = $value;
$this->publicationFailures = $publicationFailures;
}

public function getValue(): mixed
{
return $this->value;
}

/** @return list<Throwable> */
public function getPublicationFailures(): array
{
return $this->publicationFailures;
}

public function hasPublicationFailures(): bool
{
return $this->publicationFailures !== [];
}
}
26 changes: 25 additions & 1 deletion lib/Providers/DatabaseServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,28 @@ public function __construct(
$this->cacheableService = $cacheableService;
$this->eventStrategy = $eventStrategy;
}
}

/**
* Make a provider for one operation without changing this provider.
* Adapters may supply builders bound to their operation-owned resource.
*/
public function forOperation(
QueryStrategy $queryStrategy,
?QueryBuilder $queryBuilder = null,
?ClauseBuilder $clauseBuilder = null,
?CacheableService $cacheableService = null,
?EventStrategy $eventStrategy = null
): self
{
$provider = clone $this;
$provider->queryStrategy = $queryStrategy;
$provider->queryBuilder = $queryBuilder === null ? clone $this->queryBuilder : $queryBuilder;
$provider->clauseBuilder = $clauseBuilder === null ? clone $this->clauseBuilder : $clauseBuilder;
$provider->queryBuilder->reset();
$provider->clauseBuilder->reset();
$provider->cacheableService = $cacheableService ?: $this->cacheableService;
$provider->eventStrategy = $eventStrategy ?: $this->eventStrategy;

return $provider;
}
}
27 changes: 27 additions & 0 deletions lib/Services/CloningOperationDatabaseProviderFactory.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?php

namespace PHPNomad\Database\Services;

use PHPNomad\Database\Interfaces\DatabaseHandler;
use PHPNomad\Database\Interfaces\OperationDatabaseProviderFactory;
use PHPNomad\Database\Interfaces\QueryStrategy;
use PHPNomad\Database\Providers\DatabaseServiceProvider;

/** Default factory for adapters whose builders can be safely cloned. */
class CloningOperationDatabaseProviderFactory implements OperationDatabaseProviderFactory
{
public function create(
DatabaseHandler $handler,
QueryStrategy $queryStrategy,
OperationCacheableService $cache,
OperationEventStrategy $events
): DatabaseServiceProvider {
return $handler->getDatabaseServiceProvider()->forOperation(
$queryStrategy,
null,
null,
$cache,
$events
);
}
}
112 changes: 112 additions & 0 deletions lib/Services/OperationCacheableService.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
<?php

namespace PHPNomad\Database\Services;

use PHPNomad\Cache\Exceptions\CachedItemNotFoundException;
use PHPNomad\Cache\Enums\Operation;
use PHPNomad\Cache\Services\CacheableService;
use Throwable;

/**
* Keeps reads and cache writes inside one coordinated operation.
*
* The wrapped service is touched only while publishing confirmed invalidations.
*/
class OperationCacheableService extends CacheableService
{
private CacheableService $sharedService;

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

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

public function __construct(CacheableService $sharedService)
{
$this->sharedService = $sharedService;
parent::__construct(
$sharedService->eventStrategy,
$sharedService->cacheStrategy,
$sharedService->cachePolicy
);
}

/**
* @param Operation::* $operation
* @param array<string, mixed> $context
*/
public function getWithCache(string $operation, array $context, callable $callback): mixed
{
$key = $this->cachePolicy->getCacheKey($context);

if (array_key_exists($key, $this->items)) {
return $this->items[$key];
}

$value = $callback();
if ($this->cachePolicy->shouldCache($operation, $context)) {
$this->items[$key] = $value;
}

return $value;
}

/** @param array<string, mixed> $context */
public function get(array $context): mixed
{
$key = $this->cachePolicy->getCacheKey($context);

if (!array_key_exists($key, $this->items)) {
throw new CachedItemNotFoundException();
}

return $this->items[$key];
}

/** @param array<string, mixed> $context */
public function set(array $context, mixed $value): void
{
$key = $this->cachePolicy->getCacheKey($context);
$this->items[$key] = $value;
$this->invalidations[$key] = $context;
}

/** @param array<string, mixed> $context */
public function delete(array $context): void
{
$key = $this->cachePolicy->getCacheKey($context);
unset($this->items[$key]);
$this->invalidations[$key] = $context;
}

/** @param array<string, mixed> $context */
public function exists(array $context): bool
{
return array_key_exists($this->cachePolicy->getCacheKey($context), $this->items);
}

/** @return list<Throwable> */
public function publishInvalidations(): array
{
$failures = [];

foreach ($this->invalidations as $context) {
try {
$this->sharedService->delete($context);
} catch (Throwable $failure) {
$failures[] = $failure;
}
}

$this->invalidations = [];

return $failures;
}

public function discard(): void
{
$this->items = [];
$this->invalidations = [];
}
}
Loading
Loading