diff --git a/.github/workflows/phpstan.yml b/.github/workflows/phpstan.yml index 3681a3d..a3c140f 100644 --- a/.github/workflows/phpstan.yml +++ b/.github/workflows/phpstan.yml @@ -14,4 +14,4 @@ jobs: with: path: lib/ tests/ level: 9 - php_version: 8.1 \ No newline at end of file + php_version: 8.2 diff --git a/.wordlist.txt b/.wordlist.txt index 7261aef..b650614 100644 --- a/.wordlist.txt +++ b/.wordlist.txt @@ -23,4 +23,20 @@ Traceback nodejs npm fediverse -readme \ No newline at end of file +readme +phpnomad +datastore +schemas +lookups +DateCreatedFactory +DateModifiedFactory +PHPNomad +PrimaryKeyFactory +PostsTable +getUnprefixedName +DatabaseServiceProvider +IdentifiableDatabaseDatastoreHandler +TableSchemaService +WithDatastoreHandlerMethods +PostDatabaseDatastoreHandler +txt diff --git a/lib/Abstracts/IdentifiableDatabaseDatastoreHandler.php b/lib/Abstracts/IdentifiableDatabaseDatastoreHandler.php index ccbf7ed..9bb21f0 100644 --- a/lib/Abstracts/IdentifiableDatabaseDatastoreHandler.php +++ b/lib/Abstracts/IdentifiableDatabaseDatastoreHandler.php @@ -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; @@ -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; @@ -45,4 +46,4 @@ public function update($id, array $attributes): void { $this->updateCompound(['id' => $id], $attributes); } -} \ No newline at end of file +} diff --git a/lib/Factories/DatabaseDatastoreHandler.php b/lib/Factories/DatabaseDatastoreHandler.php index 143ca0a..734b43c 100644 --- a/lib/Factories/DatabaseDatastoreHandler.php +++ b/lib/Factories/DatabaseDatastoreHandler.php @@ -3,6 +3,7 @@ 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; @@ -10,7 +11,7 @@ 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; @@ -28,4 +29,4 @@ public function __construct( $this->serviceProvider = $serviceProvider; $this->contextProvider = $contextProvider; } -} \ No newline at end of file +} diff --git a/lib/Interfaces/DatabaseHandler.php b/lib/Interfaces/DatabaseHandler.php new file mode 100644 index 0000000..ad2e9d9 --- /dev/null +++ b/lib/Interfaces/DatabaseHandler.php @@ -0,0 +1,15 @@ + */ + private array $publicationFailures; + + /** @param list $publicationFailures */ + public function __construct(mixed $value, array $publicationFailures = []) + { + $this->value = $value; + $this->publicationFailures = $publicationFailures; + } + + public function getValue(): mixed + { + return $this->value; + } + + /** @return list */ + public function getPublicationFailures(): array + { + return $this->publicationFailures; + } + + public function hasPublicationFailures(): bool + { + return $this->publicationFailures !== []; + } +} diff --git a/lib/Providers/DatabaseServiceProvider.php b/lib/Providers/DatabaseServiceProvider.php index 922f084..1d68918 100644 --- a/lib/Providers/DatabaseServiceProvider.php +++ b/lib/Providers/DatabaseServiceProvider.php @@ -35,4 +35,28 @@ public function __construct( $this->cacheableService = $cacheableService; $this->eventStrategy = $eventStrategy; } -} \ No newline at end of file + + /** + * 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; + } +} diff --git a/lib/Services/CloningOperationDatabaseProviderFactory.php b/lib/Services/CloningOperationDatabaseProviderFactory.php new file mode 100644 index 0000000..e073828 --- /dev/null +++ b/lib/Services/CloningOperationDatabaseProviderFactory.php @@ -0,0 +1,27 @@ +getDatabaseServiceProvider()->forOperation( + $queryStrategy, + null, + null, + $cache, + $events + ); + } +} diff --git a/lib/Services/OperationCacheableService.php b/lib/Services/OperationCacheableService.php new file mode 100644 index 0000000..edea363 --- /dev/null +++ b/lib/Services/OperationCacheableService.php @@ -0,0 +1,112 @@ + */ + private array $items = []; + + /** @var array> */ + private array $invalidations = []; + + public function __construct(CacheableService $sharedService) + { + $this->sharedService = $sharedService; + parent::__construct( + $sharedService->eventStrategy, + $sharedService->cacheStrategy, + $sharedService->cachePolicy + ); + } + + /** + * @param Operation::* $operation + * @param array $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 $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 $context */ + public function set(array $context, mixed $value): void + { + $key = $this->cachePolicy->getCacheKey($context); + $this->items[$key] = $value; + $this->invalidations[$key] = $context; + } + + /** @param array $context */ + public function delete(array $context): void + { + $key = $this->cachePolicy->getCacheKey($context); + unset($this->items[$key]); + $this->invalidations[$key] = $context; + } + + /** @param array $context */ + public function exists(array $context): bool + { + return array_key_exists($this->cachePolicy->getCacheKey($context), $this->items); + } + + /** @return list */ + 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 = []; + } +} diff --git a/lib/Services/OperationDatabaseHandlerBridge.php b/lib/Services/OperationDatabaseHandlerBridge.php new file mode 100644 index 0000000..2af5033 --- /dev/null +++ b/lib/Services/OperationDatabaseHandlerBridge.php @@ -0,0 +1,197 @@ +providerFactory = $providerFactory; + } + + /** + * @param array $identity + * @param array $participants + * @param array $handlers + * @param callable(array, QueryStrategy): mixed $operation + * @return CoordinatedOperationResult + */ + public function coordinate( + CoordinatedQueryStrategy $coordinator, + Table $coordinationTable, + array $identity, + array $participants, + array $handlers, + callable $operation + ): CoordinatedOperationResult { + if ($identity === []) { + throw new InvalidArgumentException('An operation needs a non-empty identity.'); + } + $this->validateParticipants($participants, $handlers, $coordinator); + /** @var non-empty-array $identity */ + /** @var non-empty-list $participants */ + /** @var non-empty-array $handlers */ + + /** @var array $caches */ + $caches = []; + /** @var array $eventStrategies */ + $eventStrategies = []; + $providerFactory = $this->providerFactory; + + try { + $value = $coordinator->coordinate( + $coordinationTable, + $identity, + $participants, + function (QueryStrategy $queryStrategy) use ( + $handlers, + $providerFactory, + &$caches, + &$eventStrategies, + $operation + ) { + $localHandlers = []; + $queryBuilderIds = []; + $clauseBuilderIds = []; + + foreach ($handlers as $key => $handler) { + $sourceProvider = $handler->getDatabaseServiceProvider(); + $cacheKey = spl_object_id($sourceProvider->cacheableService); + $eventKey = spl_object_id($sourceProvider->eventStrategy); + $caches[$cacheKey] = $caches[$cacheKey] ?? new OperationCacheableService($sourceProvider->cacheableService); + $eventStrategies[$eventKey] = $eventStrategies[$eventKey] ?? new OperationEventStrategy($sourceProvider->eventStrategy); + $provider = $providerFactory->create($handler, $queryStrategy, $caches[$cacheKey], $eventStrategies[$eventKey]); + if (!$provider instanceof DatabaseServiceProvider) { + throw new InvalidArgumentException('The operation provider factory must return a database service provider.'); + } + if ($provider->queryStrategy !== $queryStrategy) { + throw new InvalidArgumentException('The operation provider must use the operation query strategy.'); + } + if ( + $provider->cacheableService !== $caches[$cacheKey] + || $provider->eventStrategy !== $eventStrategies[$eventKey] + || $provider->loggerStrategy !== $sourceProvider->loggerStrategy + ) { + throw new InvalidArgumentException('The operation provider must use the operation-local publication services and source logger.'); + } + if ($provider->queryBuilder === $sourceProvider->queryBuilder || $provider->clauseBuilder === $sourceProvider->clauseBuilder) { + throw new InvalidArgumentException('The operation provider must use independent query and clause builders.'); + } + if (in_array(spl_object_id($provider->queryBuilder), $queryBuilderIds, true) || in_array(spl_object_id($provider->clauseBuilder), $clauseBuilderIds, true)) { + throw new InvalidArgumentException('Each operation handler must use independent query and clause builders.'); + } + $queryBuilderIds[] = spl_object_id($provider->queryBuilder); + $clauseBuilderIds[] = spl_object_id($provider->clauseBuilder); + $provider->queryBuilder->reset(); + $provider->clauseBuilder->reset(); + + $localHandler = $handler->cloneForOperation($provider); + if ($localHandler === $handler || $localHandler->getDatabaseServiceProvider() !== $provider) { + throw new InvalidArgumentException('The operation handler must be a clone with the operation provider.'); + } + $localHandlers[$key] = $localHandler; + } + + return $operation($localHandlers, $queryStrategy); + } + ); + } catch (Throwable $failure) { + foreach ($caches as $cache) { + $cache->discard(); + } + foreach ($eventStrategies as $events) { + $events->discard(); + } + + throw $failure; + } + + $publicationFailures = []; + foreach ($caches as $cache) { + $publicationFailures = array_merge($publicationFailures, $cache->publishInvalidations()); + } + foreach ($eventStrategies as $events) { + $publicationFailures = array_merge($publicationFailures, $events->publish()); + } + + $logger = $handlers[array_key_first($handlers)]->getDatabaseServiceProvider()->loggerStrategy; + foreach ($publicationFailures as $failure) { + try { + if ($failure instanceof Exception) { + $logger->logException($failure, 'Coordinated operation publication failed.'); + } else { + $logger->error('Coordinated operation publication failed.', ['exception' => get_class($failure)]); + } + } catch (Throwable $loggingFailure) { + $publicationFailures[] = $loggingFailure; + } + } + + return new CoordinatedOperationResult($value, $publicationFailures); + } + + /** + * @param array $participants + * @param array $handlers + * @param CoordinatedQueryStrategy $coordinator + */ + private function validateParticipants(array $participants, array $handlers, CoordinatedQueryStrategy $coordinator): void + { + if ($participants === [] || $handlers === []) { + throw new InvalidArgumentException('An operation needs participants and handlers.'); + } + + $participantNames = []; + $firstProvider = null; + foreach ($participants as $participant) { + if (!$participant instanceof Table || $participant->getName() === '') { + throw new InvalidArgumentException('Operation participants must be named tables.'); + } + $participantNames[] = $participant->getName(); + } + + foreach ($handlers as $handler) { + if (!$handler instanceof DatabaseHandler) { + throw new InvalidArgumentException('Operation handlers must be database handlers.'); + } + + $provider = $handler->getDatabaseServiceProvider(); + if ($provider->queryStrategy !== $coordinator) { + throw new InvalidArgumentException('Every handler must use the coordinating query strategy.'); + } + if ($firstProvider === null) { + $firstProvider = $provider; + } elseif ( + $provider->cacheableService !== $firstProvider->cacheableService + || $provider->eventStrategy !== $firstProvider->eventStrategy + || $provider->loggerStrategy !== $firstProvider->loggerStrategy + ) { + throw new InvalidArgumentException('Every handler must share the coordinating publication policy.'); + } + + if (!in_array($handler->getDatabaseTable()->getName(), $participantNames, true)) { + throw new InvalidArgumentException('Every handler table must be a declared participant.'); + } + } + } +} diff --git a/lib/Services/OperationEventStrategy.php b/lib/Services/OperationEventStrategy.php new file mode 100644 index 0000000..f571cd0 --- /dev/null +++ b/lib/Services/OperationEventStrategy.php @@ -0,0 +1,60 @@ + */ + private array $events = []; + + public function __construct(EventStrategy $sharedStrategy) + { + $this->sharedStrategy = $sharedStrategy; + } + + public function broadcast(Event $event): void + { + $this->events[] = $event; + } + + public function attach(string $event, callable $action, ?int $priority = null): void + { + throw new UnsupportedCoordinationException('Event bindings cannot change inside a coordinated operation.'); + } + + public function detach(string $event, callable $action, ?int $priority = null): void + { + throw new UnsupportedCoordinationException('Event bindings cannot change inside a coordinated operation.'); + } + + /** @return list */ + public function publish(): array + { + $failures = []; + + foreach ($this->events as $event) { + try { + $this->sharedStrategy->broadcast($event); + } catch (Throwable $failure) { + $failures[] = $failure; + } + } + + $this->events = []; + + return $failures; + } + + public function discard(): void + { + $this->events = []; + } +} diff --git a/lib/Traits/WithDatastoreHandlerMethods.php b/lib/Traits/WithDatastoreHandlerMethods.php index dbd3ac6..b04ef50 100644 --- a/lib/Traits/WithDatastoreHandlerMethods.php +++ b/lib/Traits/WithDatastoreHandlerMethods.php @@ -7,6 +7,7 @@ use PHPNomad\Datastore\Events\RecordDeleted; use PHPNomad\Datastore\Events\RecordUpdated; use PHPNomad\Datastore\Exceptions\RecordNotFoundException; +use PHPNomad\Database\Interfaces\DatabaseHandler; use PHPNomad\Database\Interfaces\Table; use PHPNomad\Database\Providers\DatabaseServiceProvider; use PHPNomad\Database\Services\TableSchemaService; @@ -31,6 +32,28 @@ trait WithDatastoreHandlerMethods protected string $model; protected ModelAdapter $modelAdapter; + public function getDatabaseTable(): Table + { + return $this->table; + } + + public function getDatabaseServiceProvider(): DatabaseServiceProvider + { + return $this->serviceProvider; + } + + public function cloneForOperation(DatabaseServiceProvider $serviceProvider): DatabaseHandler + { + $clone = clone $this; + $clone->serviceProvider = $serviceProvider; + + if (!$clone instanceof DatabaseHandler) { + throw new \LogicException('The datastore handler must implement DatabaseHandler.'); + } + + return $clone; + } + /** * @inheritDoc */ diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon new file mode 100644 index 0000000..ef7c3d7 --- /dev/null +++ b/phpstan-baseline.neon @@ -0,0 +1,946 @@ +parameters: + ignoreErrors: + - + message: "#^Access to constant class on an unknown class PHPNomad\\\\Datastore\\\\Interfaces\\\\T\\.$#" + count: 1 + path: lib/Abstracts/IdentifiableDatabaseDatastoreHandler.php + + - + message: "#^Call to method getIdentity\\(\\) on an unknown class PHPNomad\\\\Datastore\\\\Interfaces\\\\T\\.$#" + count: 1 + path: lib/Abstracts/IdentifiableDatabaseDatastoreHandler.php + + - + message: "#^Cannot access constant class on mixed\\.$#" + count: 1 + path: lib/Abstracts/IdentifiableDatabaseDatastoreHandler.php + + - + message: "#^Else branch is unreachable because ternary operator condition is always true\\.$#" + count: 1 + path: lib/Abstracts/IdentifiableDatabaseDatastoreHandler.php + + - + message: "#^Method PHPNomad\\\\Database\\\\Abstracts\\\\IdentifiableDatabaseDatastoreHandler\\:\\:andWhere\\(\\) has invalid return type PHPNomad\\\\Datastore\\\\Interfaces\\\\T\\.$#" + count: 1 + path: lib/Abstracts/IdentifiableDatabaseDatastoreHandler.php + + - + message: "#^Method PHPNomad\\\\Database\\\\Abstracts\\\\IdentifiableDatabaseDatastoreHandler\\:\\:buildConditions\\(\\) has parameter \\$groups with no value type specified in iterable type array\\.$#" + count: 1 + path: lib/Abstracts/IdentifiableDatabaseDatastoreHandler.php + + - + message: "#^Method PHPNomad\\\\Database\\\\Abstracts\\\\IdentifiableDatabaseDatastoreHandler\\:\\:countWhere\\(\\) should return int but returns mixed\\.$#" + count: 1 + path: lib/Abstracts/IdentifiableDatabaseDatastoreHandler.php + + - + message: "#^Method PHPNomad\\\\Database\\\\Abstracts\\\\IdentifiableDatabaseDatastoreHandler\\:\\:deleteWhere\\(\\) has parameter \\$conditions with no value type specified in iterable type array\\.$#" + count: 1 + path: lib/Abstracts/IdentifiableDatabaseDatastoreHandler.php + + - + message: "#^Method PHPNomad\\\\Database\\\\Abstracts\\\\IdentifiableDatabaseDatastoreHandler\\:\\:find\\(\\) should return PHPNomad\\\\Datastore\\\\Interfaces\\\\DataModel but returns mixed\\.$#" + count: 1 + path: lib/Abstracts/IdentifiableDatabaseDatastoreHandler.php + + - + message: "#^Method PHPNomad\\\\Database\\\\Abstracts\\\\IdentifiableDatabaseDatastoreHandler\\:\\:findBy\\(\\) has parameter \\$value with no type specified\\.$#" + count: 1 + path: lib/Abstracts/IdentifiableDatabaseDatastoreHandler.php + + - + message: "#^Method PHPNomad\\\\Database\\\\Abstracts\\\\IdentifiableDatabaseDatastoreHandler\\:\\:findBy\\(\\) should return PHPNomad\\\\Datastore\\\\Interfaces\\\\DataModel but returns mixed\\.$#" + count: 1 + path: lib/Abstracts/IdentifiableDatabaseDatastoreHandler.php + + - + message: "#^Method PHPNomad\\\\Database\\\\Abstracts\\\\IdentifiableDatabaseDatastoreHandler\\:\\:findIds\\(\\) has parameter \\$conditions with no value type specified in iterable type array\\.$#" + count: 1 + path: lib/Abstracts/IdentifiableDatabaseDatastoreHandler.php + + - + message: "#^Method PHPNomad\\\\Database\\\\Abstracts\\\\IdentifiableDatabaseDatastoreHandler\\:\\:findIds\\(\\) return type has no value type specified in iterable type array\\.$#" + count: 1 + path: lib/Abstracts/IdentifiableDatabaseDatastoreHandler.php + + - + message: "#^Method PHPNomad\\\\Database\\\\Abstracts\\\\IdentifiableDatabaseDatastoreHandler\\:\\:findMultiple\\(\\) has parameter \\$ids with no value type specified in iterable type array\\.$#" + count: 1 + path: lib/Abstracts/IdentifiableDatabaseDatastoreHandler.php + + - + message: "#^Method PHPNomad\\\\Database\\\\Abstracts\\\\IdentifiableDatabaseDatastoreHandler\\:\\:findMultiple\\(\\) return type has no value type specified in iterable type array\\.$#" + count: 1 + path: lib/Abstracts/IdentifiableDatabaseDatastoreHandler.php + + - + message: "#^Method PHPNomad\\\\Database\\\\Abstracts\\\\IdentifiableDatabaseDatastoreHandler\\:\\:getCacheContextForItem\\(\\) return type has no value type specified in iterable type array\\.$#" + count: 1 + path: lib/Abstracts/IdentifiableDatabaseDatastoreHandler.php + + - + message: "#^Method PHPNomad\\\\Database\\\\Abstracts\\\\IdentifiableDatabaseDatastoreHandler\\:\\:getDuplicates\\(\\) has parameter \\$data with no value type specified in iterable type array\\.$#" + count: 1 + path: lib/Abstracts/IdentifiableDatabaseDatastoreHandler.php + + - + message: "#^Method PHPNomad\\\\Database\\\\Abstracts\\\\IdentifiableDatabaseDatastoreHandler\\:\\:getDuplicates\\(\\) should return array\\ but returns array\\\\.$#" + count: 1 + path: lib/Abstracts/IdentifiableDatabaseDatastoreHandler.php + + - + message: "#^Method PHPNomad\\\\Database\\\\Abstracts\\\\IdentifiableDatabaseDatastoreHandler\\:\\:getModels\\(\\) return type has no value type specified in iterable type array\\.$#" + count: 1 + path: lib/Abstracts/IdentifiableDatabaseDatastoreHandler.php + + - + message: "#^Method PHPNomad\\\\Database\\\\Abstracts\\\\IdentifiableDatabaseDatastoreHandler\\:\\:hydrateItems\\(\\) has parameter \\$data with no value type specified in iterable type array\\.$#" + count: 1 + path: lib/Abstracts/IdentifiableDatabaseDatastoreHandler.php + + - + message: "#^Method PHPNomad\\\\Database\\\\Abstracts\\\\IdentifiableDatabaseDatastoreHandler\\:\\:initiateQuery\\(\\) has parameter \\$select with no value type specified in iterable type array\\.$#" + count: 1 + path: lib/Abstracts/IdentifiableDatabaseDatastoreHandler.php + + - + message: "#^Method PHPNomad\\\\Database\\\\Abstracts\\\\IdentifiableDatabaseDatastoreHandler\\:\\:maybeThrowForDuplicateIdentity\\(\\) has parameter \\$attributes with no value type specified in iterable type array\\.$#" + count: 1 + path: lib/Abstracts/IdentifiableDatabaseDatastoreHandler.php + + - + message: "#^Method PHPNomad\\\\Database\\\\Abstracts\\\\IdentifiableDatabaseDatastoreHandler\\:\\:maybeThrowForDuplicateIdentity\\(\\) has parameter \\$fields with no value type specified in iterable type array\\.$#" + count: 1 + path: lib/Abstracts/IdentifiableDatabaseDatastoreHandler.php + + - + message: "#^Method PHPNomad\\\\Database\\\\Abstracts\\\\IdentifiableDatabaseDatastoreHandler\\:\\:maybeThrowForDuplicateUniqueFields\\(\\) has parameter \\$data with no value type specified in iterable type array\\.$#" + count: 1 + path: lib/Abstracts/IdentifiableDatabaseDatastoreHandler.php + + - + message: "#^Method PHPNomad\\\\Database\\\\Abstracts\\\\IdentifiableDatabaseDatastoreHandler\\:\\:maybeThrowForDuplicateUniqueFields\\(\\) has parameter \\$updateIdentity with no value type specified in iterable type array\\.$#" + count: 1 + path: lib/Abstracts/IdentifiableDatabaseDatastoreHandler.php + + - + message: "#^Method PHPNomad\\\\Database\\\\Abstracts\\\\IdentifiableDatabaseDatastoreHandler\\:\\:orWhere\\(\\) has invalid return type PHPNomad\\\\Datastore\\\\Interfaces\\\\T\\.$#" + count: 1 + path: lib/Abstracts/IdentifiableDatabaseDatastoreHandler.php + + - + message: "#^Method PHPNomad\\\\Database\\\\Abstracts\\\\IdentifiableDatabaseDatastoreHandler\\:\\:removeIdentifiableFields\\(\\) has parameter \\$attributes with no value type specified in iterable type array\\.$#" + count: 1 + path: lib/Abstracts/IdentifiableDatabaseDatastoreHandler.php + + - + message: "#^Method PHPNomad\\\\Database\\\\Abstracts\\\\IdentifiableDatabaseDatastoreHandler\\:\\:removeIdentifiableFields\\(\\) has parameter \\$fields with no value type specified in iterable type array\\.$#" + count: 1 + path: lib/Abstracts/IdentifiableDatabaseDatastoreHandler.php + + - + message: "#^Method PHPNomad\\\\Database\\\\Abstracts\\\\IdentifiableDatabaseDatastoreHandler\\:\\:removeIdentifiableFields\\(\\) return type has no value type specified in iterable type array\\.$#" + count: 1 + path: lib/Abstracts/IdentifiableDatabaseDatastoreHandler.php + + - + message: "#^Method PHPNomad\\\\Database\\\\Abstracts\\\\IdentifiableDatabaseDatastoreHandler\\:\\:where\\(\\) has invalid return type PHPNomad\\\\Datastore\\\\Interfaces\\\\T\\.$#" + count: 1 + path: lib/Abstracts/IdentifiableDatabaseDatastoreHandler.php + + - + message: "#^Parameter \\#1 \\$array of function array_shift expects array, mixed given\\.$#" + count: 1 + path: lib/Abstracts/IdentifiableDatabaseDatastoreHandler.php + + - + message: "#^Parameter \\#1 \\$array of method PHPNomad\\\\Datastore\\\\Interfaces\\\\ModelAdapter\\\\:\\:toModel\\(\\) expects array, mixed given\\.$#" + count: 1 + path: lib/Abstracts/IdentifiableDatabaseDatastoreHandler.php + + - + message: "#^Parameter \\#1 \\$conditions of method PHPNomad\\\\Database\\\\Abstracts\\\\IdentifiableDatabaseDatastoreHandler\\:\\:countWhere\\(\\) expects array\\{type\\: string, clauses\\: array\\\\}, array\\{array\\{type\\: 'AND', clauses\\: array\\\\}\\} given\\.$#" + count: 1 + path: lib/Abstracts/IdentifiableDatabaseDatastoreHandler.php + + - + message: "#^Parameter \\#1 \\$conditions of method PHPNomad\\\\Database\\\\Abstracts\\\\IdentifiableDatabaseDatastoreHandler\\:\\:countWhere\\(\\) expects array\\{type\\: string, clauses\\: array\\\\}, array\\{array\\{type\\: 'OR', clauses\\: array\\\\}\\} given\\.$#" + count: 1 + path: lib/Abstracts/IdentifiableDatabaseDatastoreHandler.php + + - + message: "#^Parameter \\#1 \\$field of method PHPNomad\\\\Database\\\\Interfaces\\\\ClauseBuilder\\:\\:andWhere\\(\\) expects array\\\\|string, mixed given\\.$#" + count: 1 + path: lib/Abstracts/IdentifiableDatabaseDatastoreHandler.php + + - + message: "#^Parameter \\#1 \\$field of method PHPNomad\\\\Database\\\\Interfaces\\\\ClauseBuilder\\:\\:orWhere\\(\\) expects array\\\\|string, mixed given\\.$#" + count: 1 + path: lib/Abstracts/IdentifiableDatabaseDatastoreHandler.php + + - + message: "#^Parameter \\#1 \\$field of method PHPNomad\\\\Database\\\\Interfaces\\\\ClauseBuilder\\:\\:where\\(\\) expects array\\\\|string, mixed given\\.$#" + count: 1 + path: lib/Abstracts/IdentifiableDatabaseDatastoreHandler.php + + - + message: "#^Parameter \\#1 \\$ids of method PHPNomad\\\\Database\\\\Abstracts\\\\IdentifiableDatabaseDatastoreHandler\\:\\:findFromCompound\\(\\) expects non\\-empty\\-array\\, array given\\.$#" + count: 2 + path: lib/Abstracts/IdentifiableDatabaseDatastoreHandler.php + + - + message: "#^Parameter \\#1 \\$ids of method PHPNomad\\\\Database\\\\Abstracts\\\\IdentifiableDatabaseDatastoreHandler\\:\\:findFromCompound\\(\\) expects non\\-empty\\-array\\, array\\ given\\.$#" + count: 1 + path: lib/Abstracts/IdentifiableDatabaseDatastoreHandler.php + + - + message: "#^Parameter \\#1 \\$ids of method PHPNomad\\\\Database\\\\Abstracts\\\\IdentifiableDatabaseDatastoreHandler\\:\\:findFromCompound\\(\\) expects non\\-empty\\-array\\, array\\{id\\: mixed\\} given\\.$#" + count: 1 + path: lib/Abstracts/IdentifiableDatabaseDatastoreHandler.php + + - + message: "#^Parameter \\#1 \\$ids of method PHPNomad\\\\Database\\\\Abstracts\\\\IdentifiableDatabaseDatastoreHandler\\:\\:updateCompound\\(\\) expects array\\, array\\ given\\.$#" + count: 1 + path: lib/Abstracts/IdentifiableDatabaseDatastoreHandler.php + + - + message: "#^Parameter \\#1 \\$limit of method PHPNomad\\\\Database\\\\Interfaces\\\\QueryBuilder\\:\\:limit\\(\\) expects int\\<1, max\\>, int\\\\|int\\<1, max\\> given\\.$#" + count: 2 + path: lib/Abstracts/IdentifiableDatabaseDatastoreHandler.php + + - + message: "#^Parameter \\#1 \\$offset of method PHPNomad\\\\Database\\\\Interfaces\\\\QueryBuilder\\:\\:offset\\(\\) expects int\\<1, max\\>, int\\\\|int\\<1, max\\> given\\.$#" + count: 2 + path: lib/Abstracts/IdentifiableDatabaseDatastoreHandler.php + + - + message: "#^Parameter \\#1 \\$string of function strtoupper expects string, mixed given\\.$#" + count: 3 + path: lib/Abstracts/IdentifiableDatabaseDatastoreHandler.php + + - + message: "#^Parameter \\#1 \\$subject of static method PHPNomad\\\\Utils\\\\Helpers\\\\Arr\\:\\:get\\(\\) expects array, mixed given\\.$#" + count: 6 + path: lib/Abstracts/IdentifiableDatabaseDatastoreHandler.php + + - + message: "#^Parameter \\#2 \\$key of static method PHPNomad\\\\Utils\\\\Helpers\\\\Arr\\:\\:get\\(\\) expects string, int given\\.$#" + count: 2 + path: lib/Abstracts/IdentifiableDatabaseDatastoreHandler.php + + - + message: "#^Parameter \\#2 \\$operator of method PHPNomad\\\\Database\\\\Interfaces\\\\ClauseBuilder\\:\\:andWhere\\(\\) expects string, mixed given\\.$#" + count: 1 + path: lib/Abstracts/IdentifiableDatabaseDatastoreHandler.php + + - + message: "#^Parameter \\#2 \\$operator of method PHPNomad\\\\Database\\\\Interfaces\\\\ClauseBuilder\\:\\:orWhere\\(\\) expects string, mixed given\\.$#" + count: 1 + path: lib/Abstracts/IdentifiableDatabaseDatastoreHandler.php + + - + message: "#^Parameter \\#2 \\$operator of method PHPNomad\\\\Database\\\\Interfaces\\\\ClauseBuilder\\:\\:where\\(\\) expects string, mixed given\\.$#" + count: 1 + path: lib/Abstracts/IdentifiableDatabaseDatastoreHandler.php + + - + message: "#^Parameter \\#3 \\$default of static method PHPNomad\\\\Utils\\\\Helpers\\\\Arr\\:\\:get\\(\\) expects null, array given\\.$#" + count: 2 + path: lib/Abstracts/IdentifiableDatabaseDatastoreHandler.php + + - + message: "#^Parameter \\#3 \\$default of static method PHPNomad\\\\Utils\\\\Helpers\\\\Arr\\:\\:get\\(\\) expects null, int given\\.$#" + count: 1 + path: lib/Abstracts/IdentifiableDatabaseDatastoreHandler.php + + - + message: "#^Parameter \\#3 \\$default of static method PHPNomad\\\\Utils\\\\Helpers\\\\Arr\\:\\:get\\(\\) expects null, string given\\.$#" + count: 2 + path: lib/Abstracts/IdentifiableDatabaseDatastoreHandler.php + + - + message: "#^Property PHPNomad\\\\Database\\\\Abstracts\\\\IdentifiableDatabaseDatastoreHandler\\:\\:\\$modelAdapter with generic interface PHPNomad\\\\Datastore\\\\Interfaces\\\\ModelAdapter does not specify its types\\: TModel$#" + count: 1 + path: lib/Abstracts/IdentifiableDatabaseDatastoreHandler.php + + - + message: "#^Variable \\$ids in empty\\(\\) always exists and is not falsy\\.$#" + count: 1 + path: lib/Abstracts/IdentifiableDatabaseDatastoreHandler.php + + - + message: "#^Method PHPNomad\\\\Database\\\\Abstracts\\\\JunctionTable\\:\\:getAlias\\(\\) should return string but returns mixed\\.$#" + count: 1 + path: lib/Abstracts/JunctionTable.php + + - + message: "#^Method PHPNomad\\\\Database\\\\Abstracts\\\\JunctionTable\\:\\:getTables\\(\\) return type has no value type specified in iterable type array\\.$#" + count: 1 + path: lib/Abstracts/JunctionTable.php + + - + message: "#^Method PHPNomad\\\\Database\\\\Abstracts\\\\JunctionTable\\:\\:getUnprefixedName\\(\\) should return string but returns mixed\\.$#" + count: 1 + path: lib/Abstracts/JunctionTable.php + + - + message: "#^PHPDoc tag @var for property PHPNomad\\\\Database\\\\Abstracts\\\\JunctionTable\\:\\:\\$logger with type mixed is not subtype of native type PHPNomad\\\\Logger\\\\Interfaces\\\\LoggerStrategy\\.$#" + count: 1 + path: lib/Abstracts/JunctionTable.php + + - + message: "#^Parameter \\#1 \\$localPrefixProvider of method PHPNomad\\\\Database\\\\Abstracts\\\\Table\\:\\:__construct\\(\\) expects PHPNomad\\\\Database\\\\Interfaces\\\\HasLocalDatabasePrefix, mixed given\\.$#" + count: 1 + path: lib/Abstracts/JunctionTable.php + + - + message: "#^Property PHPNomad\\\\Database\\\\Abstracts\\\\JunctionTable\\:\\:\\$leftTable \\(PHPNomad\\\\Database\\\\Abstracts\\\\Table\\) does not accept mixed\\.$#" + count: 1 + path: lib/Abstracts/JunctionTable.php + + - + message: "#^Property PHPNomad\\\\Database\\\\Abstracts\\\\JunctionTable\\:\\:\\$logger \\(PHPNomad\\\\Logger\\\\Interfaces\\\\LoggerStrategy\\) does not accept mixed\\.$#" + count: 1 + path: lib/Abstracts/JunctionTable.php + + - + message: "#^Property PHPNomad\\\\Database\\\\Abstracts\\\\JunctionTable\\:\\:\\$rightTable \\(PHPNomad\\\\Database\\\\Abstracts\\\\Table\\) does not accept mixed\\.$#" + count: 1 + path: lib/Abstracts/JunctionTable.php + + - + message: "#^Method PHPNomad\\\\Database\\\\Abstracts\\\\Table\\:\\:getFieldsForIdentity\\(\\) should return non\\-empty\\-array\\ but returns array\\.$#" + count: 1 + path: lib/Abstracts/Table.php + + - + message: "#^Method PHPNomad\\\\Database\\\\Factories\\\\Column\\:\\:__construct\\(\\) has parameter \\$attributes with no type specified\\.$#" + count: 1 + path: lib/Factories/Column.php + + - + message: "#^Method PHPNomad\\\\Database\\\\Factories\\\\Column\\:\\:__construct\\(\\) has parameter \\$typeArgs with no value type specified in iterable type array\\.$#" + count: 1 + path: lib/Factories/Column.php + + - + message: "#^Method PHPNomad\\\\Database\\\\Factories\\\\Column\\:\\:getTypeArgs\\(\\) return type has no value type specified in iterable type array\\.$#" + count: 1 + path: lib/Factories/Column.php + + - + message: "#^Property PHPNomad\\\\Database\\\\Factories\\\\Column\\:\\:\\$attributes type has no value type specified in iterable type array\\.$#" + count: 1 + path: lib/Factories/Column.php + + - + message: "#^Property PHPNomad\\\\Database\\\\Factories\\\\Column\\:\\:\\$typeArgs type has no value type specified in iterable type array\\.$#" + count: 1 + path: lib/Factories/Column.php + + - + message: "#^Access to constant class on an unknown class PHPNomad\\\\Datastore\\\\Interfaces\\\\T\\.$#" + count: 1 + path: lib/Factories/DatabaseDatastoreHandler.php + + - + message: "#^Call to method getIdentity\\(\\) on an unknown class PHPNomad\\\\Datastore\\\\Interfaces\\\\T\\.$#" + count: 1 + path: lib/Factories/DatabaseDatastoreHandler.php + + - + message: "#^Cannot access constant class on mixed\\.$#" + count: 1 + path: lib/Factories/DatabaseDatastoreHandler.php + + - + message: "#^Else branch is unreachable because ternary operator condition is always true\\.$#" + count: 1 + path: lib/Factories/DatabaseDatastoreHandler.php + + - + message: "#^Method PHPNomad\\\\Database\\\\Factories\\\\DatabaseDatastoreHandler\\:\\:andWhere\\(\\) has invalid return type PHPNomad\\\\Datastore\\\\Interfaces\\\\T\\.$#" + count: 1 + path: lib/Factories/DatabaseDatastoreHandler.php + + - + message: "#^Method PHPNomad\\\\Database\\\\Factories\\\\DatabaseDatastoreHandler\\:\\:buildConditions\\(\\) has parameter \\$groups with no value type specified in iterable type array\\.$#" + count: 1 + path: lib/Factories/DatabaseDatastoreHandler.php + + - + message: "#^Method PHPNomad\\\\Database\\\\Factories\\\\DatabaseDatastoreHandler\\:\\:countWhere\\(\\) should return int but returns mixed\\.$#" + count: 1 + path: lib/Factories/DatabaseDatastoreHandler.php + + - + message: "#^Method PHPNomad\\\\Database\\\\Factories\\\\DatabaseDatastoreHandler\\:\\:deleteWhere\\(\\) has parameter \\$conditions with no value type specified in iterable type array\\.$#" + count: 1 + path: lib/Factories/DatabaseDatastoreHandler.php + + - + message: "#^Method PHPNomad\\\\Database\\\\Factories\\\\DatabaseDatastoreHandler\\:\\:findBy\\(\\) has parameter \\$value with no type specified\\.$#" + count: 1 + path: lib/Factories/DatabaseDatastoreHandler.php + + - + message: "#^Method PHPNomad\\\\Database\\\\Factories\\\\DatabaseDatastoreHandler\\:\\:findBy\\(\\) should return PHPNomad\\\\Datastore\\\\Interfaces\\\\DataModel but returns mixed\\.$#" + count: 1 + path: lib/Factories/DatabaseDatastoreHandler.php + + - + message: "#^Method PHPNomad\\\\Database\\\\Factories\\\\DatabaseDatastoreHandler\\:\\:findIds\\(\\) has parameter \\$conditions with no value type specified in iterable type array\\.$#" + count: 1 + path: lib/Factories/DatabaseDatastoreHandler.php + + - + message: "#^Method PHPNomad\\\\Database\\\\Factories\\\\DatabaseDatastoreHandler\\:\\:findIds\\(\\) return type has no value type specified in iterable type array\\.$#" + count: 1 + path: lib/Factories/DatabaseDatastoreHandler.php + + - + message: "#^Method PHPNomad\\\\Database\\\\Factories\\\\DatabaseDatastoreHandler\\:\\:getCacheContextForItem\\(\\) return type has no value type specified in iterable type array\\.$#" + count: 1 + path: lib/Factories/DatabaseDatastoreHandler.php + + - + message: "#^Method PHPNomad\\\\Database\\\\Factories\\\\DatabaseDatastoreHandler\\:\\:getDuplicates\\(\\) has parameter \\$data with no value type specified in iterable type array\\.$#" + count: 1 + path: lib/Factories/DatabaseDatastoreHandler.php + + - + message: "#^Method PHPNomad\\\\Database\\\\Factories\\\\DatabaseDatastoreHandler\\:\\:getDuplicates\\(\\) should return array\\ but returns array\\\\.$#" + count: 1 + path: lib/Factories/DatabaseDatastoreHandler.php + + - + message: "#^Method PHPNomad\\\\Database\\\\Factories\\\\DatabaseDatastoreHandler\\:\\:getModels\\(\\) return type has no value type specified in iterable type array\\.$#" + count: 1 + path: lib/Factories/DatabaseDatastoreHandler.php + + - + message: "#^Method PHPNomad\\\\Database\\\\Factories\\\\DatabaseDatastoreHandler\\:\\:hydrateItems\\(\\) has parameter \\$data with no value type specified in iterable type array\\.$#" + count: 1 + path: lib/Factories/DatabaseDatastoreHandler.php + + - + message: "#^Method PHPNomad\\\\Database\\\\Factories\\\\DatabaseDatastoreHandler\\:\\:initiateQuery\\(\\) has parameter \\$select with no value type specified in iterable type array\\.$#" + count: 1 + path: lib/Factories/DatabaseDatastoreHandler.php + + - + message: "#^Method PHPNomad\\\\Database\\\\Factories\\\\DatabaseDatastoreHandler\\:\\:maybeThrowForDuplicateIdentity\\(\\) has parameter \\$attributes with no value type specified in iterable type array\\.$#" + count: 1 + path: lib/Factories/DatabaseDatastoreHandler.php + + - + message: "#^Method PHPNomad\\\\Database\\\\Factories\\\\DatabaseDatastoreHandler\\:\\:maybeThrowForDuplicateIdentity\\(\\) has parameter \\$fields with no value type specified in iterable type array\\.$#" + count: 1 + path: lib/Factories/DatabaseDatastoreHandler.php + + - + message: "#^Method PHPNomad\\\\Database\\\\Factories\\\\DatabaseDatastoreHandler\\:\\:maybeThrowForDuplicateUniqueFields\\(\\) has parameter \\$data with no value type specified in iterable type array\\.$#" + count: 1 + path: lib/Factories/DatabaseDatastoreHandler.php + + - + message: "#^Method PHPNomad\\\\Database\\\\Factories\\\\DatabaseDatastoreHandler\\:\\:maybeThrowForDuplicateUniqueFields\\(\\) has parameter \\$updateIdentity with no value type specified in iterable type array\\.$#" + count: 1 + path: lib/Factories/DatabaseDatastoreHandler.php + + - + message: "#^Method PHPNomad\\\\Database\\\\Factories\\\\DatabaseDatastoreHandler\\:\\:orWhere\\(\\) has invalid return type PHPNomad\\\\Datastore\\\\Interfaces\\\\T\\.$#" + count: 1 + path: lib/Factories/DatabaseDatastoreHandler.php + + - + message: "#^Method PHPNomad\\\\Database\\\\Factories\\\\DatabaseDatastoreHandler\\:\\:removeIdentifiableFields\\(\\) has parameter \\$attributes with no value type specified in iterable type array\\.$#" + count: 1 + path: lib/Factories/DatabaseDatastoreHandler.php + + - + message: "#^Method PHPNomad\\\\Database\\\\Factories\\\\DatabaseDatastoreHandler\\:\\:removeIdentifiableFields\\(\\) has parameter \\$fields with no value type specified in iterable type array\\.$#" + count: 1 + path: lib/Factories/DatabaseDatastoreHandler.php + + - + message: "#^Method PHPNomad\\\\Database\\\\Factories\\\\DatabaseDatastoreHandler\\:\\:removeIdentifiableFields\\(\\) return type has no value type specified in iterable type array\\.$#" + count: 1 + path: lib/Factories/DatabaseDatastoreHandler.php + + - + message: "#^Method PHPNomad\\\\Database\\\\Factories\\\\DatabaseDatastoreHandler\\:\\:where\\(\\) has invalid return type PHPNomad\\\\Datastore\\\\Interfaces\\\\T\\.$#" + count: 1 + path: lib/Factories/DatabaseDatastoreHandler.php + + - + message: "#^Parameter \\#1 \\$array of function array_shift expects array, mixed given\\.$#" + count: 1 + path: lib/Factories/DatabaseDatastoreHandler.php + + - + message: "#^Parameter \\#1 \\$array of method PHPNomad\\\\Datastore\\\\Interfaces\\\\ModelAdapter\\\\:\\:toModel\\(\\) expects array, mixed given\\.$#" + count: 1 + path: lib/Factories/DatabaseDatastoreHandler.php + + - + message: "#^Parameter \\#1 \\$conditions of method PHPNomad\\\\Database\\\\Factories\\\\DatabaseDatastoreHandler\\:\\:countWhere\\(\\) expects array\\{type\\: string, clauses\\: array\\\\}, array\\{array\\{type\\: 'AND', clauses\\: array\\\\}\\} given\\.$#" + count: 1 + path: lib/Factories/DatabaseDatastoreHandler.php + + - + message: "#^Parameter \\#1 \\$conditions of method PHPNomad\\\\Database\\\\Factories\\\\DatabaseDatastoreHandler\\:\\:countWhere\\(\\) expects array\\{type\\: string, clauses\\: array\\\\}, array\\{array\\{type\\: 'OR', clauses\\: array\\\\}\\} given\\.$#" + count: 1 + path: lib/Factories/DatabaseDatastoreHandler.php + + - + message: "#^Parameter \\#1 \\$field of method PHPNomad\\\\Database\\\\Interfaces\\\\ClauseBuilder\\:\\:andWhere\\(\\) expects array\\\\|string, mixed given\\.$#" + count: 1 + path: lib/Factories/DatabaseDatastoreHandler.php + + - + message: "#^Parameter \\#1 \\$field of method PHPNomad\\\\Database\\\\Interfaces\\\\ClauseBuilder\\:\\:orWhere\\(\\) expects array\\\\|string, mixed given\\.$#" + count: 1 + path: lib/Factories/DatabaseDatastoreHandler.php + + - + message: "#^Parameter \\#1 \\$field of method PHPNomad\\\\Database\\\\Interfaces\\\\ClauseBuilder\\:\\:where\\(\\) expects array\\\\|string, mixed given\\.$#" + count: 1 + path: lib/Factories/DatabaseDatastoreHandler.php + + - + message: "#^Parameter \\#1 \\$ids of method PHPNomad\\\\Database\\\\Factories\\\\DatabaseDatastoreHandler\\:\\:findFromCompound\\(\\) expects non\\-empty\\-array\\, array given\\.$#" + count: 2 + path: lib/Factories/DatabaseDatastoreHandler.php + + - + message: "#^Parameter \\#1 \\$ids of method PHPNomad\\\\Database\\\\Factories\\\\DatabaseDatastoreHandler\\:\\:findFromCompound\\(\\) expects non\\-empty\\-array\\, array\\ given\\.$#" + count: 1 + path: lib/Factories/DatabaseDatastoreHandler.php + + - + message: "#^Parameter \\#1 \\$limit of method PHPNomad\\\\Database\\\\Interfaces\\\\QueryBuilder\\:\\:limit\\(\\) expects int\\<1, max\\>, int\\\\|int\\<1, max\\> given\\.$#" + count: 2 + path: lib/Factories/DatabaseDatastoreHandler.php + + - + message: "#^Parameter \\#1 \\$offset of method PHPNomad\\\\Database\\\\Interfaces\\\\QueryBuilder\\:\\:offset\\(\\) expects int\\<1, max\\>, int\\\\|int\\<1, max\\> given\\.$#" + count: 2 + path: lib/Factories/DatabaseDatastoreHandler.php + + - + message: "#^Parameter \\#1 \\$string of function strtoupper expects string, mixed given\\.$#" + count: 3 + path: lib/Factories/DatabaseDatastoreHandler.php + + - + message: "#^Parameter \\#1 \\$subject of static method PHPNomad\\\\Utils\\\\Helpers\\\\Arr\\:\\:get\\(\\) expects array, mixed given\\.$#" + count: 6 + path: lib/Factories/DatabaseDatastoreHandler.php + + - + message: "#^Parameter \\#2 \\$key of static method PHPNomad\\\\Utils\\\\Helpers\\\\Arr\\:\\:get\\(\\) expects string, int given\\.$#" + count: 2 + path: lib/Factories/DatabaseDatastoreHandler.php + + - + message: "#^Parameter \\#2 \\$operator of method PHPNomad\\\\Database\\\\Interfaces\\\\ClauseBuilder\\:\\:andWhere\\(\\) expects string, mixed given\\.$#" + count: 1 + path: lib/Factories/DatabaseDatastoreHandler.php + + - + message: "#^Parameter \\#2 \\$operator of method PHPNomad\\\\Database\\\\Interfaces\\\\ClauseBuilder\\:\\:orWhere\\(\\) expects string, mixed given\\.$#" + count: 1 + path: lib/Factories/DatabaseDatastoreHandler.php + + - + message: "#^Parameter \\#2 \\$operator of method PHPNomad\\\\Database\\\\Interfaces\\\\ClauseBuilder\\:\\:where\\(\\) expects string, mixed given\\.$#" + count: 1 + path: lib/Factories/DatabaseDatastoreHandler.php + + - + message: "#^Parameter \\#3 \\$default of static method PHPNomad\\\\Utils\\\\Helpers\\\\Arr\\:\\:get\\(\\) expects null, array given\\.$#" + count: 2 + path: lib/Factories/DatabaseDatastoreHandler.php + + - + message: "#^Parameter \\#3 \\$default of static method PHPNomad\\\\Utils\\\\Helpers\\\\Arr\\:\\:get\\(\\) expects null, int given\\.$#" + count: 1 + path: lib/Factories/DatabaseDatastoreHandler.php + + - + message: "#^Parameter \\#3 \\$default of static method PHPNomad\\\\Utils\\\\Helpers\\\\Arr\\:\\:get\\(\\) expects null, string given\\.$#" + count: 2 + path: lib/Factories/DatabaseDatastoreHandler.php + + - + message: "#^Property PHPNomad\\\\Database\\\\Factories\\\\DatabaseDatastoreHandler\\:\\:\\$modelAdapter with generic interface PHPNomad\\\\Datastore\\\\Interfaces\\\\ModelAdapter does not specify its types\\: TModel$#" + count: 1 + path: lib/Factories/DatabaseDatastoreHandler.php + + - + message: "#^Variable \\$ids in empty\\(\\) always exists and is not falsy\\.$#" + count: 1 + path: lib/Factories/DatabaseDatastoreHandler.php + + - + message: "#^Method PHPNomad\\\\Database\\\\Factories\\\\Index\\:\\:__construct\\(\\) has parameter \\$attributes with no type specified\\.$#" + count: 1 + path: lib/Factories/Index.php + + - + message: "#^Method PHPNomad\\\\Database\\\\Factories\\\\Index\\:\\:__construct\\(\\) has parameter \\$columns with no value type specified in iterable type array\\.$#" + count: 1 + path: lib/Factories/Index.php + + - + message: "#^Method PHPNomad\\\\Database\\\\Factories\\\\Index\\:\\:getAttributes\\(\\) return type has no value type specified in iterable type array\\.$#" + count: 1 + path: lib/Factories/Index.php + + - + message: "#^Property PHPNomad\\\\Database\\\\Factories\\\\Index\\:\\:\\$attributes type has no value type specified in iterable type array\\.$#" + count: 1 + path: lib/Factories/Index.php + + - + message: "#^Property PHPNomad\\\\Database\\\\Factories\\\\Index\\:\\:\\$columns type has no value type specified in iterable type array\\.$#" + count: 1 + path: lib/Factories/Index.php + + - + message: "#^Method PHPNomad\\\\Database\\\\Interfaces\\\\DatabaseContextProvider\\:\\:getModelAdapter\\(\\) has invalid return type PHPNomad\\\\Database\\\\Interfaces\\\\ModelAdapter\\.$#" + count: 2 + path: lib/Interfaces/DatabaseContextProvider.php + + - + message: "#^PHPDoc tag @param references unknown parameter\\: \\$tableName$#" + count: 2 + path: lib/Interfaces/JunctionTableQueryService.php + + - + message: "#^Method PHPNomad\\\\Database\\\\Interfaces\\\\QueryStrategy\\:\\:insert\\(\\) has parameter \\$data with no value type specified in iterable type array\\.$#" + count: 1 + path: lib/Interfaces/QueryStrategy.php + + - + message: "#^Method PHPNomad\\\\Database\\\\Interfaces\\\\QueryStrategy\\:\\:query\\(\\) return type has no value type specified in iterable type array\\.$#" + count: 1 + path: lib/Interfaces/QueryStrategy.php + + - + message: "#^Method PHPNomad\\\\Database\\\\Interfaces\\\\QueryStrategy\\:\\:update\\(\\) has parameter \\$data with no value type specified in iterable type array\\.$#" + count: 1 + path: lib/Interfaces/QueryStrategy.php + + - + message: "#^Method PHPNomad\\\\Database\\\\Services\\\\TableSchemaService\\:\\:getCacheContext\\(\\) return type has no value type specified in iterable type array\\.$#" + count: 1 + path: lib/Services/TableSchemaService.php + + - + message: "#^Method PHPNomad\\\\Database\\\\Services\\\\TableSchemaService\\:\\:getPrimaryColumnNameForTable\\(\\) should return PHPNomad\\\\Database\\\\Factories\\\\Column but returns mixed\\.$#" + count: 1 + path: lib/Services/TableSchemaService.php + + - + message: "#^PHPDoc tag @param references unknown parameter\\: \\$table$#" + count: 1 + path: lib/Services/TableSchemaService.php + + - + message: "#^PHPDoc tag @return with type mixed is not subtype of native type PHPNomad\\\\Database\\\\Factories\\\\Column\\.$#" + count: 1 + path: lib/Services/TableSchemaService.php + + - + message: "#^Cannot access constant class on mixed\\.$#" + count: 1 + path: tests/Unit/Traits/WithDatastoreHandlerMethodsTest.php + + - + message: "#^Else branch is unreachable because ternary operator condition is always true\\.$#" + count: 1 + path: tests/Unit/Traits/WithDatastoreHandlerMethodsTest.php + + - + message: "#^Method PHPNomad\\\\Database\\\\Tests\\\\Unit\\\\Traits\\\\DummyDatastoreHandler\\:\\:__construct\\(\\) has parameter \\$modelAdapter with generic interface PHPNomad\\\\Datastore\\\\Interfaces\\\\ModelAdapter but does not specify its types\\: TModel$#" + count: 1 + path: tests/Unit/Traits/WithDatastoreHandlerMethodsTest.php + + - + message: "#^Method PHPNomad\\\\Database\\\\Tests\\\\Unit\\\\Traits\\\\DummyDatastoreHandler\\:\\:andWhere\\(\\) has parameter \\$conditions with no value type specified in iterable type array\\.$#" + count: 1 + path: tests/Unit/Traits/WithDatastoreHandlerMethodsTest.php + + - + message: "#^Method PHPNomad\\\\Database\\\\Tests\\\\Unit\\\\Traits\\\\DummyDatastoreHandler\\:\\:andWhere\\(\\) return type has no value type specified in iterable type array\\.$#" + count: 1 + path: tests/Unit/Traits/WithDatastoreHandlerMethodsTest.php + + - + message: "#^Method PHPNomad\\\\Database\\\\Tests\\\\Unit\\\\Traits\\\\DummyDatastoreHandler\\:\\:buildConditions\\(\\) has parameter \\$groups with no value type specified in iterable type array\\.$#" + count: 1 + path: tests/Unit/Traits/WithDatastoreHandlerMethodsTest.php + + - + message: "#^Method PHPNomad\\\\Database\\\\Tests\\\\Unit\\\\Traits\\\\DummyDatastoreHandler\\:\\:countAndWhere\\(\\) has parameter \\$conditions with no value type specified in iterable type array\\.$#" + count: 1 + path: tests/Unit/Traits/WithDatastoreHandlerMethodsTest.php + + - + message: "#^Method PHPNomad\\\\Database\\\\Tests\\\\Unit\\\\Traits\\\\DummyDatastoreHandler\\:\\:countOrWhere\\(\\) has parameter \\$conditions with no value type specified in iterable type array\\.$#" + count: 1 + path: tests/Unit/Traits/WithDatastoreHandlerMethodsTest.php + + - + message: "#^Method PHPNomad\\\\Database\\\\Tests\\\\Unit\\\\Traits\\\\DummyDatastoreHandler\\:\\:countWhere\\(\\) has parameter \\$conditions with no value type specified in iterable type array\\.$#" + count: 1 + path: tests/Unit/Traits/WithDatastoreHandlerMethodsTest.php + + - + message: "#^Method PHPNomad\\\\Database\\\\Tests\\\\Unit\\\\Traits\\\\DummyDatastoreHandler\\:\\:countWhere\\(\\) should return int but returns mixed\\.$#" + count: 1 + path: tests/Unit/Traits/WithDatastoreHandlerMethodsTest.php + + - + message: "#^Method PHPNomad\\\\Database\\\\Tests\\\\Unit\\\\Traits\\\\DummyDatastoreHandler\\:\\:create\\(\\) has parameter \\$attributes with no value type specified in iterable type array\\.$#" + count: 1 + path: tests/Unit/Traits/WithDatastoreHandlerMethodsTest.php + + - + message: "#^Method PHPNomad\\\\Database\\\\Tests\\\\Unit\\\\Traits\\\\DummyDatastoreHandler\\:\\:deleteWhere\\(\\) has parameter \\$conditions with no value type specified in iterable type array\\.$#" + count: 1 + path: tests/Unit/Traits/WithDatastoreHandlerMethodsTest.php + + - + message: "#^Method PHPNomad\\\\Database\\\\Tests\\\\Unit\\\\Traits\\\\DummyDatastoreHandler\\:\\:exposeCacheContext\\(\\) has parameter \\$ids with no value type specified in iterable type array\\.$#" + count: 1 + path: tests/Unit/Traits/WithDatastoreHandlerMethodsTest.php + + - + message: "#^Method PHPNomad\\\\Database\\\\Tests\\\\Unit\\\\Traits\\\\DummyDatastoreHandler\\:\\:exposeCacheContext\\(\\) return type has no value type specified in iterable type array\\.$#" + count: 1 + path: tests/Unit/Traits/WithDatastoreHandlerMethodsTest.php + + - + message: "#^Method PHPNomad\\\\Database\\\\Tests\\\\Unit\\\\Traits\\\\DummyDatastoreHandler\\:\\:findBy\\(\\) has parameter \\$value with no type specified\\.$#" + count: 1 + path: tests/Unit/Traits/WithDatastoreHandlerMethodsTest.php + + - + message: "#^Method PHPNomad\\\\Database\\\\Tests\\\\Unit\\\\Traits\\\\DummyDatastoreHandler\\:\\:findBy\\(\\) should return PHPNomad\\\\Datastore\\\\Interfaces\\\\DataModel but returns mixed\\.$#" + count: 1 + path: tests/Unit/Traits/WithDatastoreHandlerMethodsTest.php + + - + message: "#^Method PHPNomad\\\\Database\\\\Tests\\\\Unit\\\\Traits\\\\DummyDatastoreHandler\\:\\:findByIdentity\\(\\) has no return type specified\\.$#" + count: 1 + path: tests/Unit/Traits/WithDatastoreHandlerMethodsTest.php + + - + message: "#^Method PHPNomad\\\\Database\\\\Tests\\\\Unit\\\\Traits\\\\DummyDatastoreHandler\\:\\:findByIdentity\\(\\) has parameter \\$ids with no value type specified in iterable type array\\.$#" + count: 1 + path: tests/Unit/Traits/WithDatastoreHandlerMethodsTest.php + + - + message: "#^Method PHPNomad\\\\Database\\\\Tests\\\\Unit\\\\Traits\\\\DummyDatastoreHandler\\:\\:findIds\\(\\) has parameter \\$conditions with no value type specified in iterable type array\\.$#" + count: 1 + path: tests/Unit/Traits/WithDatastoreHandlerMethodsTest.php + + - + message: "#^Method PHPNomad\\\\Database\\\\Tests\\\\Unit\\\\Traits\\\\DummyDatastoreHandler\\:\\:findIds\\(\\) return type has no value type specified in iterable type array\\.$#" + count: 1 + path: tests/Unit/Traits/WithDatastoreHandlerMethodsTest.php + + - + message: "#^Method PHPNomad\\\\Database\\\\Tests\\\\Unit\\\\Traits\\\\DummyDatastoreHandler\\:\\:getCacheContextForItem\\(\\) return type has no value type specified in iterable type array\\.$#" + count: 1 + path: tests/Unit/Traits/WithDatastoreHandlerMethodsTest.php + + - + message: "#^Method PHPNomad\\\\Database\\\\Tests\\\\Unit\\\\Traits\\\\DummyDatastoreHandler\\:\\:getDuplicates\\(\\) has parameter \\$data with no value type specified in iterable type array\\.$#" + count: 1 + path: tests/Unit/Traits/WithDatastoreHandlerMethodsTest.php + + - + message: "#^Method PHPNomad\\\\Database\\\\Tests\\\\Unit\\\\Traits\\\\DummyDatastoreHandler\\:\\:getModels\\(\\) return type has no value type specified in iterable type array\\.$#" + count: 1 + path: tests/Unit/Traits/WithDatastoreHandlerMethodsTest.php + + - + message: "#^Method PHPNomad\\\\Database\\\\Tests\\\\Unit\\\\Traits\\\\DummyDatastoreHandler\\:\\:hydrateItems\\(\\) has parameter \\$data with no value type specified in iterable type array\\.$#" + count: 1 + path: tests/Unit/Traits/WithDatastoreHandlerMethodsTest.php + + - + message: "#^Method PHPNomad\\\\Database\\\\Tests\\\\Unit\\\\Traits\\\\DummyDatastoreHandler\\:\\:initiateQuery\\(\\) has parameter \\$select with no value type specified in iterable type array\\.$#" + count: 1 + path: tests/Unit/Traits/WithDatastoreHandlerMethodsTest.php + + - + message: "#^Method PHPNomad\\\\Database\\\\Tests\\\\Unit\\\\Traits\\\\DummyDatastoreHandler\\:\\:maybeThrowForDuplicateIdentity\\(\\) has parameter \\$attributes with no value type specified in iterable type array\\.$#" + count: 1 + path: tests/Unit/Traits/WithDatastoreHandlerMethodsTest.php + + - + message: "#^Method PHPNomad\\\\Database\\\\Tests\\\\Unit\\\\Traits\\\\DummyDatastoreHandler\\:\\:maybeThrowForDuplicateIdentity\\(\\) has parameter \\$fields with no value type specified in iterable type array\\.$#" + count: 1 + path: tests/Unit/Traits/WithDatastoreHandlerMethodsTest.php + + - + message: "#^Method PHPNomad\\\\Database\\\\Tests\\\\Unit\\\\Traits\\\\DummyDatastoreHandler\\:\\:maybeThrowForDuplicateUniqueFields\\(\\) has parameter \\$data with no value type specified in iterable type array\\.$#" + count: 1 + path: tests/Unit/Traits/WithDatastoreHandlerMethodsTest.php + + - + message: "#^Method PHPNomad\\\\Database\\\\Tests\\\\Unit\\\\Traits\\\\DummyDatastoreHandler\\:\\:maybeThrowForDuplicateUniqueFields\\(\\) has parameter \\$updateIdentity with no value type specified in iterable type array\\.$#" + count: 1 + path: tests/Unit/Traits/WithDatastoreHandlerMethodsTest.php + + - + message: "#^Method PHPNomad\\\\Database\\\\Tests\\\\Unit\\\\Traits\\\\DummyDatastoreHandler\\:\\:orWhere\\(\\) has parameter \\$conditions with no value type specified in iterable type array\\.$#" + count: 1 + path: tests/Unit/Traits/WithDatastoreHandlerMethodsTest.php + + - + message: "#^Method PHPNomad\\\\Database\\\\Tests\\\\Unit\\\\Traits\\\\DummyDatastoreHandler\\:\\:orWhere\\(\\) return type has no value type specified in iterable type array\\.$#" + count: 1 + path: tests/Unit/Traits/WithDatastoreHandlerMethodsTest.php + + - + message: "#^Method PHPNomad\\\\Database\\\\Tests\\\\Unit\\\\Traits\\\\DummyDatastoreHandler\\:\\:removeIdentifiableFields\\(\\) has parameter \\$attributes with no value type specified in iterable type array\\.$#" + count: 1 + path: tests/Unit/Traits/WithDatastoreHandlerMethodsTest.php + + - + message: "#^Method PHPNomad\\\\Database\\\\Tests\\\\Unit\\\\Traits\\\\DummyDatastoreHandler\\:\\:removeIdentifiableFields\\(\\) has parameter \\$fields with no value type specified in iterable type array\\.$#" + count: 1 + path: tests/Unit/Traits/WithDatastoreHandlerMethodsTest.php + + - + message: "#^Method PHPNomad\\\\Database\\\\Tests\\\\Unit\\\\Traits\\\\DummyDatastoreHandler\\:\\:removeIdentifiableFields\\(\\) return type has no value type specified in iterable type array\\.$#" + count: 1 + path: tests/Unit/Traits/WithDatastoreHandlerMethodsTest.php + + - + message: "#^Method PHPNomad\\\\Database\\\\Tests\\\\Unit\\\\Traits\\\\DummyDatastoreHandler\\:\\:updateCompound\\(\\) has parameter \\$attributes with no value type specified in iterable type array\\.$#" + count: 1 + path: tests/Unit/Traits/WithDatastoreHandlerMethodsTest.php + + - + message: "#^Method PHPNomad\\\\Database\\\\Tests\\\\Unit\\\\Traits\\\\DummyDatastoreHandler\\:\\:updateCompound\\(\\) has parameter \\$ids with no type specified\\.$#" + count: 1 + path: tests/Unit/Traits/WithDatastoreHandlerMethodsTest.php + + - + message: "#^Method PHPNomad\\\\Database\\\\Tests\\\\Unit\\\\Traits\\\\DummyDatastoreHandler\\:\\:where\\(\\) has parameter \\$conditions with no value type specified in iterable type array\\.$#" + count: 1 + path: tests/Unit/Traits/WithDatastoreHandlerMethodsTest.php + + - + message: "#^Method PHPNomad\\\\Database\\\\Tests\\\\Unit\\\\Traits\\\\DummyDatastoreHandler\\:\\:where\\(\\) return type has no value type specified in iterable type array\\.$#" + count: 1 + path: tests/Unit/Traits/WithDatastoreHandlerMethodsTest.php + + - + message: "#^Parameter \\#1 \\$array of function array_shift expects array, mixed given\\.$#" + count: 1 + path: tests/Unit/Traits/WithDatastoreHandlerMethodsTest.php + + - + message: "#^Parameter \\#1 \\$array of method PHPNomad\\\\Datastore\\\\Interfaces\\\\ModelAdapter\\\\:\\:toModel\\(\\) expects array, mixed given\\.$#" + count: 1 + path: tests/Unit/Traits/WithDatastoreHandlerMethodsTest.php + + - + message: "#^Parameter \\#1 \\$field of method PHPNomad\\\\Database\\\\Interfaces\\\\ClauseBuilder\\:\\:andWhere\\(\\) expects array\\\\|string, mixed given\\.$#" + count: 1 + path: tests/Unit/Traits/WithDatastoreHandlerMethodsTest.php + + - + message: "#^Parameter \\#1 \\$field of method PHPNomad\\\\Database\\\\Interfaces\\\\ClauseBuilder\\:\\:orWhere\\(\\) expects array\\\\|string, mixed given\\.$#" + count: 1 + path: tests/Unit/Traits/WithDatastoreHandlerMethodsTest.php + + - + message: "#^Parameter \\#1 \\$field of method PHPNomad\\\\Database\\\\Interfaces\\\\ClauseBuilder\\:\\:where\\(\\) expects array\\\\|string, mixed given\\.$#" + count: 1 + path: tests/Unit/Traits/WithDatastoreHandlerMethodsTest.php + + - + message: "#^Parameter \\#1 \\$ids of method PHPNomad\\\\Database\\\\Tests\\\\Unit\\\\Traits\\\\DummyDatastoreHandler\\:\\:findFromCompound\\(\\) expects non\\-empty\\-array\\, array given\\.$#" + count: 3 + path: tests/Unit/Traits/WithDatastoreHandlerMethodsTest.php + + - + message: "#^Parameter \\#1 \\$limit of method PHPNomad\\\\Database\\\\Interfaces\\\\QueryBuilder\\:\\:limit\\(\\) expects int\\<1, max\\>, int\\\\|int\\<1, max\\> given\\.$#" + count: 2 + path: tests/Unit/Traits/WithDatastoreHandlerMethodsTest.php + + - + message: "#^Parameter \\#1 \\$offset of method PHPNomad\\\\Database\\\\Interfaces\\\\QueryBuilder\\:\\:offset\\(\\) expects int\\<1, max\\>, int\\\\|int\\<1, max\\> given\\.$#" + count: 2 + path: tests/Unit/Traits/WithDatastoreHandlerMethodsTest.php + + - + message: "#^Parameter \\#1 \\$string of function strtoupper expects string, mixed given\\.$#" + count: 3 + path: tests/Unit/Traits/WithDatastoreHandlerMethodsTest.php + + - + message: "#^Parameter \\#1 \\$subject of static method PHPNomad\\\\Utils\\\\Helpers\\\\Arr\\:\\:get\\(\\) expects array, mixed given\\.$#" + count: 6 + path: tests/Unit/Traits/WithDatastoreHandlerMethodsTest.php + + - + message: "#^Parameter \\#2 \\$key of static method PHPNomad\\\\Utils\\\\Helpers\\\\Arr\\:\\:get\\(\\) expects string, int given\\.$#" + count: 2 + path: tests/Unit/Traits/WithDatastoreHandlerMethodsTest.php + + - + message: "#^Parameter \\#2 \\$operator of method PHPNomad\\\\Database\\\\Interfaces\\\\ClauseBuilder\\:\\:andWhere\\(\\) expects string, mixed given\\.$#" + count: 1 + path: tests/Unit/Traits/WithDatastoreHandlerMethodsTest.php + + - + message: "#^Parameter \\#2 \\$operator of method PHPNomad\\\\Database\\\\Interfaces\\\\ClauseBuilder\\:\\:orWhere\\(\\) expects string, mixed given\\.$#" + count: 1 + path: tests/Unit/Traits/WithDatastoreHandlerMethodsTest.php + + - + message: "#^Parameter \\#2 \\$operator of method PHPNomad\\\\Database\\\\Interfaces\\\\ClauseBuilder\\:\\:where\\(\\) expects string, mixed given\\.$#" + count: 1 + path: tests/Unit/Traits/WithDatastoreHandlerMethodsTest.php + + - + message: "#^Parameter \\#3 \\$default of static method PHPNomad\\\\Utils\\\\Helpers\\\\Arr\\:\\:get\\(\\) expects null, array given\\.$#" + count: 2 + path: tests/Unit/Traits/WithDatastoreHandlerMethodsTest.php + + - + message: "#^Parameter \\#3 \\$default of static method PHPNomad\\\\Utils\\\\Helpers\\\\Arr\\:\\:get\\(\\) expects null, int given\\.$#" + count: 1 + path: tests/Unit/Traits/WithDatastoreHandlerMethodsTest.php + + - + message: "#^Parameter \\#3 \\$default of static method PHPNomad\\\\Utils\\\\Helpers\\\\Arr\\:\\:get\\(\\) expects null, string given\\.$#" + count: 2 + path: tests/Unit/Traits/WithDatastoreHandlerMethodsTest.php + + - + message: "#^Property PHPNomad\\\\Database\\\\Tests\\\\Unit\\\\Traits\\\\DummyDatastoreHandler\\:\\:\\$model \\(class\\-string\\\\) does not accept string\\.$#" + count: 1 + path: tests/Unit/Traits/WithDatastoreHandlerMethodsTest.php + + - + message: "#^Property PHPNomad\\\\Database\\\\Tests\\\\Unit\\\\Traits\\\\DummyDatastoreHandler\\:\\:\\$modelAdapter with generic interface PHPNomad\\\\Datastore\\\\Interfaces\\\\ModelAdapter does not specify its types\\: TModel$#" + count: 1 + path: tests/Unit/Traits/WithDatastoreHandlerMethodsTest.php + + - + message: "#^Variable \\$ids in empty\\(\\) always exists and is not falsy\\.$#" + count: 1 + path: tests/Unit/Traits/WithDatastoreHandlerMethodsTest.php diff --git a/phpstan.neon b/phpstan.neon new file mode 100644 index 0000000..de3beb4 --- /dev/null +++ b/phpstan.neon @@ -0,0 +1,5 @@ +includes: + - phpstan-baseline.neon + +parameters: + level: 9 diff --git a/tests/Unit/Services/OperationDatabaseHandlerBridgeTest.php b/tests/Unit/Services/OperationDatabaseHandlerBridgeTest.php new file mode 100644 index 0000000..2f27f1c --- /dev/null +++ b/tests/Unit/Services/OperationDatabaseHandlerBridgeTest.php @@ -0,0 +1,448 @@ +createMock(EventStrategy::class); + $sharedCache = $this->createMock(CacheStrategy::class); + $sharedCache->expects($this->exactly(2))->method('delete'); + $sharedCache->expects($this->never())->method('exists'); + $sharedCacheable = $this->cacheable($sharedEvents, $sharedCache); + $provider = $this->provider($sharedEvents, $sharedCacheable); + $handler = $this->handler($provider, 'records'); + $operationQuery = $this->createMock(QueryStrategy::class); + $coordinator = $this->coordinator($operationQuery); + $provider->queryStrategy = $coordinator; + $event = $this->createMock(Event::class); + $sharedEvents->expects($this->once())->method('broadcast')->with($event); + $factoryCalled = false; + + $result = (new OperationDatabaseHandlerBridge(new BridgeProviderFactory( + function ($databaseHandler, QueryStrategy $query, $cache, $events) use (&$factoryCalled, $provider) { + $factoryCalled = true; + + return $provider->forOperation($query, clone $provider->queryBuilder, clone $provider->clauseBuilder, $cache, $events); + } + )))->coordinate( + $coordinator, + $handler->getDatabaseTable(), + ['id' => 1], + [$handler->getDatabaseTable()], + ['record' => $handler], + function (array $handlers, QueryStrategy $query) use ($handler, $operationQuery, $event): string { + $local = $handlers['record']; + self::assertInstanceOf(BridgeTestHandler::class, $local); + self::assertSame('preserve-me', $local->marker); + self::assertSame($operationQuery, $local->getDatabaseServiceProvider()->queryStrategy); + self::assertNotSame($handler, $local); + self::assertNotSame( + $handler->getDatabaseServiceProvider()->queryBuilder, + $local->getDatabaseServiceProvider()->queryBuilder + ); + $local->getDatabaseServiceProvider()->cacheableService->set(['id' => 9], 'fresh'); + self::assertSame( + 'fresh', + $local->getDatabaseServiceProvider()->cacheableService->getWithCache( + 'read', + ['id' => 9], + static function (): string { + throw new \LogicException('shared cache was consulted'); + } + ) + ); + $local->getDatabaseServiceProvider()->cacheableService->delete(['id' => 1]); + $local->getDatabaseServiceProvider()->eventStrategy->broadcast($event); + + return 'committed'; + } + ); + + self::assertTrue($factoryCalled); + self::assertInstanceOf(CoordinatedOperationResult::class, $result); + self::assertSame('committed', $result->getValue()); + self::assertSame([], $result->getPublicationFailures()); + self::assertSame($coordinator, $provider->queryStrategy); + self::assertSame($sharedCacheable, $provider->cacheableService); + self::assertSame($sharedEvents, $provider->eventStrategy); + } + + public function testCallbackFailureDiscardsCacheAndEvents(): void + { + $sharedEvents = $this->createMock(EventStrategy::class); + $sharedEvents->expects($this->never())->method('broadcast'); + $sharedCache = $this->createMock(CacheStrategy::class); + $sharedCache->expects($this->never())->method('delete'); + $sharedCacheable = $this->cacheable($sharedEvents, $sharedCache); + $provider = $this->provider($sharedEvents, $sharedCacheable); + $handler = $this->handler($provider, 'records'); + $operationQuery = $this->createMock(QueryStrategy::class); + $coordinator = $this->coordinator($operationQuery); + $provider->queryStrategy = $coordinator; + $event = $this->createMock(Event::class); + + $this->expectException(\RuntimeException::class); + $this->bridge()->coordinate( + $coordinator, + $handler->getDatabaseTable(), + ['id' => 1], + [$handler->getDatabaseTable()], + [$handler], + function (array $handlers) use ($event): void { + $provider = $handlers[0]->getDatabaseServiceProvider(); + $provider->cacheableService->delete(['id' => 1]); + $provider->eventStrategy->broadcast($event); + throw new \RuntimeException('rollback'); + } + ); + } + + public function testHandlerFromAnotherParticipantIsRejectedBeforeCoordination(): void + { + $sharedEvents = $this->createMock(EventStrategy::class); + $sharedCacheable = $this->cacheable($sharedEvents, $this->createMock(CacheStrategy::class)); + $first = $this->handler($this->provider($sharedEvents, $sharedCacheable), 'first'); + $second = $this->handler($this->provider($sharedEvents, $sharedCacheable), 'second'); + $coordinator = $this->createMock(CoordinatedQueryStrategy::class); + $coordinator->expects($this->never())->method('coordinate'); + $first->getDatabaseServiceProvider()->queryStrategy = $coordinator; + $second->getDatabaseServiceProvider()->queryStrategy = $coordinator; + + $this->expectException(InvalidArgumentException::class); + $this->bridge()->coordinate( + $coordinator, + $first->getDatabaseTable(), + ['id' => 1], + [$first->getDatabaseTable()], + [$second], + static function (): void {} + ); + } + + public function testCommittedResultRetainsCacheAndEventPublicationFailures(): void + { + $sharedEvents = $this->createMock(EventStrategy::class); + $sharedEvents->expects($this->once())->method('broadcast')->willThrowException(new \RuntimeException('event publish')); + $sharedCache = $this->createMock(CacheStrategy::class); + $sharedCache->expects($this->once())->method('delete')->willThrowException(new \RuntimeException('cache invalidate')); + $sharedCacheable = $this->cacheable($sharedEvents, $sharedCache); + $provider = $this->provider($sharedEvents, $sharedCacheable); + $handler = $this->handler($provider, 'records'); + $operationQuery = $this->createMock(QueryStrategy::class); + $coordinator = $this->coordinator($operationQuery); + $provider->queryStrategy = $coordinator; + $event = $this->createMock(Event::class); + + $result = $this->bridge()->coordinate( + $coordinator, + $handler->getDatabaseTable(), + ['id' => 1], + [$handler->getDatabaseTable()], + [$handler], + function (array $handlers) use ($event): string { + $provider = $handlers[0]->getDatabaseServiceProvider(); + $provider->cacheableService->delete(['id' => 1]); + $provider->eventStrategy->broadcast($event); + + return 'committed'; + } + ); + + self::assertSame('committed', $result->getValue()); + self::assertCount(2, $result->getPublicationFailures()); + self::assertContainsOnlyInstancesOf(\RuntimeException::class, $result->getPublicationFailures()); + } + + public function testPublicationLoggingFailureIsRetainedAndLaterPublicationsContinue(): void + { + $sharedEvents = $this->createMock(EventStrategy::class); + $sharedEvents->expects($this->once())->method('broadcast')->willThrowException(new \RuntimeException('event publish')); + $sharedCache = $this->createMock(CacheStrategy::class); + $sharedCache->expects($this->exactly(2))->method('delete')->willReturnCallback(static function (string $key): void { + throw new \RuntimeException('cache invalidate ' . $key); + }); + $logger = $this->createMock(LoggerStrategy::class); + $logger->expects($this->exactly(3))->method('logException')->willThrowException(new \RuntimeException('logger failed')); + $sharedCacheable = $this->cacheable($sharedEvents, $sharedCache); + $provider = $this->provider($sharedEvents, $sharedCacheable, $logger); + $handler = $this->handler($provider, 'records'); + $operationQuery = $this->createMock(QueryStrategy::class); + $coordinator = $this->coordinator($operationQuery); + $provider->queryStrategy = $coordinator; + $event = $this->createMock(Event::class); + + $result = $this->bridge()->coordinate( + $coordinator, + $handler->getDatabaseTable(), + ['id' => 1], + [$handler->getDatabaseTable()], + [$handler], + function (array $handlers) use ($event): string { + $provider = $handlers[0]->getDatabaseServiceProvider(); + $provider->cacheableService->set(['id' => 2], 'created'); + $provider->cacheableService->delete(['id' => 1]); + $provider->eventStrategy->broadcast($event); + + return 'committed'; + } + ); + + self::assertSame('committed', $result->getValue()); + self::assertCount(6, $result->getPublicationFailures()); + self::assertContainsOnlyInstancesOf(\RuntimeException::class, $result->getPublicationFailures()); + } + + public function testHandlerUsingAnotherCoordinatorIsRejectedBeforeCoordination(): void + { + $sharedEvents = $this->createMock(EventStrategy::class); + $sharedCacheable = $this->cacheable($sharedEvents, $this->createMock(CacheStrategy::class)); + $provider = $this->provider($sharedEvents, $sharedCacheable); + $handler = $this->handler($provider, 'records'); + $coordinator = $this->createMock(CoordinatedQueryStrategy::class); + $coordinator->expects($this->never())->method('coordinate'); + + $this->expectException(InvalidArgumentException::class); + $this->bridge()->coordinate( + $coordinator, + $handler->getDatabaseTable(), + ['id' => 1], + [$handler->getDatabaseTable()], + [$handler], + static function (): void {} + ); + } + + public function testHandlersWithDifferentSharedPoliciesAreRejectedBeforeCoordination(): void + { + $sharedEvents = $this->createMock(EventStrategy::class); + $firstProvider = $this->provider($sharedEvents, $this->cacheable($sharedEvents, $this->createMock(CacheStrategy::class))); + $secondProvider = $this->provider($sharedEvents, $this->cacheable($sharedEvents, $this->createMock(CacheStrategy::class))); + $first = $this->handler($firstProvider, 'first'); + $second = $this->handler($secondProvider, 'second'); + $coordinator = $this->createMock(CoordinatedQueryStrategy::class); + $firstProvider->queryStrategy = $coordinator; + $secondProvider->queryStrategy = $coordinator; + $coordinator->expects($this->never())->method('coordinate'); + + $this->expectException(InvalidArgumentException::class); + $this->bridge()->coordinate( + $coordinator, + $first->getDatabaseTable(), + ['id' => 1], + [$first->getDatabaseTable(), $second->getDatabaseTable()], + [$first, $second], + static function (): void {} + ); + } + + public function testFactoryCannotEscapeOperationLocalCacheOrEvents(): void + { + $sharedEvents = $this->createMock(EventStrategy::class); + $sharedCacheable = $this->cacheable($sharedEvents, $this->createMock(CacheStrategy::class)); + $provider = $this->provider($sharedEvents, $sharedCacheable); + $handler = $this->handler($provider, 'records'); + $operationQuery = $this->createMock(QueryStrategy::class); + $coordinator = $this->coordinator($operationQuery); + $provider->queryStrategy = $coordinator; + + $this->expectException(InvalidArgumentException::class); + (new OperationDatabaseHandlerBridge(new BridgeProviderFactory( + function (DatabaseHandler $source, QueryStrategy $query) use ($provider): DatabaseServiceProvider { + return $provider->forOperation($query, clone $provider->queryBuilder, clone $provider->clauseBuilder, $provider->cacheableService, $provider->eventStrategy); + } + )))->coordinate( + $coordinator, + $handler->getDatabaseTable(), + ['id' => 1], + [$handler->getDatabaseTable()], + [$handler], + static function (): void { + self::fail('The operation callback must not run.'); + } + ); + } + + public function testFactoryCannotReuseMutableBuildersAcrossHandlers(): void + { + $sharedEvents = $this->createMock(EventStrategy::class); + $sharedCacheable = $this->cacheable($sharedEvents, $this->createMock(CacheStrategy::class)); + $firstProvider = $this->provider($sharedEvents, $sharedCacheable); + $secondProvider = $this->provider($sharedEvents, $sharedCacheable); + $secondProvider->loggerStrategy = $firstProvider->loggerStrategy; + $first = $this->handler($firstProvider, 'first'); + $second = $this->handler($secondProvider, 'second'); + $operationQuery = $this->createMock(QueryStrategy::class); + $coordinator = $this->coordinator($operationQuery); + $firstProvider->queryStrategy = $coordinator; + $secondProvider->queryStrategy = $coordinator; + $queryBuilder = $this->createMock(QueryBuilder::class); + $clauseBuilder = $this->createMock(ClauseBuilder::class); + + $this->expectException(InvalidArgumentException::class); + (new OperationDatabaseHandlerBridge(new BridgeProviderFactory( + function (DatabaseHandler $source, QueryStrategy $query, OperationCacheableService $cache, OperationEventStrategy $events) use ($queryBuilder, $clauseBuilder): DatabaseServiceProvider { + return new DatabaseServiceProvider( + $source->getDatabaseServiceProvider()->loggerStrategy, + $query, + $queryBuilder, + $clauseBuilder, + $cache, + $events + ); + } + )))->coordinate( + $coordinator, + $first->getDatabaseTable(), + ['id' => 1], + [$first->getDatabaseTable(), $second->getDatabaseTable()], + [$first, $second], + static function (): void { + self::fail('The operation callback must not run.'); + } + ); + } + + public function testOperationEventBindingsCannotMutateSharedStrategy(): void + { + $sharedEvents = $this->createMock(EventStrategy::class); + $events = new OperationEventStrategy($sharedEvents); + + $this->expectException(\PHPNomad\Database\Exceptions\UnsupportedCoordinationException::class); + $events->attach('record.created', static function (): void {}); + } + + private function coordinator(QueryStrategy $operationQuery): CoordinatedQueryStrategy + { + $coordinator = $this->createMock(CoordinatedQueryStrategy::class); + $coordinator->method('coordinate')->willReturnCallback(static function ($table, $identity, $participants, $operation) use ($operationQuery) { + return $operation($operationQuery); + }); + + return $coordinator; + } + + private function bridge(): OperationDatabaseHandlerBridge + { + return new OperationDatabaseHandlerBridge(new CloningOperationDatabaseProviderFactory()); + } + + private function handler(DatabaseServiceProvider $provider, string $name): DatabaseDatastoreHandler + { + $table = $this->createMock(TableContract::class); + $table->method('getName')->willReturn($name); + + return new BridgeTestHandler( + $provider, + $this->createMock(\PHPNomad\Database\Interfaces\DatabaseContextProvider::class), + $this->createMock(\PHPNomad\Database\Services\TableSchemaService::class), + $table + ); + } + + private function provider(EventStrategy $events, CacheableService $cacheable, ?LoggerStrategy $logger = null): DatabaseServiceProvider + { + return new DatabaseServiceProvider( + $logger ?: $this->createMock(LoggerStrategy::class), + $this->createMock(QueryStrategy::class), + $this->createMock(QueryBuilder::class), + $this->createMock(ClauseBuilder::class), + $cacheable, + $events + ); + } + + private function cacheable(EventStrategy $events, CacheStrategy $cacheStrategy): CacheableService + { + $policy = $this->createMock(CachePolicy::class); + $policy->method('getCacheKey')->willReturnCallback(static fn (array $context): string => serialize($context)); + $policy->method('getTtl')->willReturn(null); + $policy->method('shouldCache')->willReturn(true); + + return new CacheableService($events, $cacheStrategy, $policy); + } +} + +class BridgeTestHandler extends DatabaseDatastoreHandler +{ + public string $marker = 'preserve-me'; + + public function __construct( + DatabaseServiceProvider $provider, + \PHPNomad\Database\Interfaces\DatabaseContextProvider $contextProvider, + \PHPNomad\Database\Services\TableSchemaService $schemaService, + TableContract $table + ) { + parent::__construct($provider, $contextProvider, $schemaService); + $this->table = $table; + $this->model = BridgeTestModel::class; + $this->modelAdapter = new BridgeTestModelAdapter(); + } +} + +class BridgeTestModel implements \PHPNomad\Datastore\Interfaces\DataModel +{ + public function getIdentity(): array + { + return []; + } +} + +/** @implements \PHPNomad\Datastore\Interfaces\ModelAdapter */ +class BridgeTestModelAdapter implements \PHPNomad\Datastore\Interfaces\ModelAdapter +{ + /** @param array $array */ + public function toModel(array $array): BridgeTestModel + { + throw new \LogicException(); + } + + /** @return array */ + public function toArray(\PHPNomad\Datastore\Interfaces\DataModel $model): array + { + throw new \LogicException(); + } +} + +class BridgeProviderFactory implements \PHPNomad\Database\Interfaces\OperationDatabaseProviderFactory +{ + private Closure $factory; + + public function __construct(callable $factory) + { + $this->factory = Closure::fromCallable($factory); + } + + public function create( + \PHPNomad\Database\Interfaces\DatabaseHandler $handler, + QueryStrategy $queryStrategy, + OperationCacheableService $cache, + OperationEventStrategy $events + ): DatabaseServiceProvider { + return ($this->factory)($handler, $queryStrategy, $cache, $events); + } +}