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
13 changes: 12 additions & 1 deletion .github/workflows/phpstan.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,18 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: php-actions/composer@v6

- name: Check out Datastore prerequisite
uses: actions/checkout@v4
with:
repository: phpnomad/datastore
ref: codex/ci-prerequisites-clean
path: .ci/datastore

- name: Install dependencies with Datastore prerequisite
run: |
composer config repositories.datastore '{"type":"path","url":".ci/datastore","options":{"symlink":false,"versions":{"phpnomad/datastore":"2.0.99"}}}'
composer update phpnomad/datastore --with-dependencies --no-interaction --prefer-dist

- name: PHPStan Static Analysis
uses: php-actions/phpstan@v3
Expand Down
23 changes: 19 additions & 4 deletions .github/workflows/phpunit.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,24 @@ jobs:
steps:
- uses: actions/checkout@v3

- uses: php-actions/composer@v6
- name: Check out Datastore prerequisite
uses: actions/checkout@v4
with:
repository: phpnomad/datastore
ref: codex/ci-prerequisites-clean
path: .ci/datastore

- name: PHPUnit Tests
uses: php-actions/phpunit@v3
- name: Set up PHP
uses: shivammathur/setup-php@v2
with:
configuration: phpunit.xml
php-version: '8.2'
tools: composer
coverage: none

- name: Install dependencies with Datastore prerequisite
run: |
composer config repositories.datastore '{"type":"path","url":".ci/datastore","options":{"symlink":false,"versions":{"phpnomad/datastore":"2.0.99"}}}'
composer update phpnomad/datastore --with-dependencies --no-interaction --prefer-dist

- name: PHPUnit Tests
run: vendor/bin/phpunit --configuration phpunit.xml
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
"phpnomad/cache": "^1.0",
"phpnomad/chrono": "^1.0",
"phpnomad/datastore": "^2.0",
"phpnomad/event": "^1.0",
"phpnomad/singleton": "^1.0",
"phpnomad/logger": "^1.0"
},
Expand Down
42 changes: 41 additions & 1 deletion composer.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 3 additions & 2 deletions lib/Abstracts/IdentifiableDatabaseDatastoreHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,11 @@
use PHPNomad\Datastore\Interfaces\DataModel;
use PHPNomad\Datastore\Interfaces\Datastore;
use PHPNomad\Datastore\Interfaces\DatastoreHasCounts;
use PHPNomad\Datastore\Interfaces\DatastoreHasIdentityQuery;
use PHPNomad\Datastore\Interfaces\DatastoreHasPrimaryKey;
use PHPNomad\Datastore\Interfaces\DatastoreHasWhere;

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

Expand Down Expand Up @@ -45,4 +46,4 @@ public function update($id, array $attributes): void
{
$this->updateCompound(['id' => $id], $attributes);
}
}
}
88 changes: 87 additions & 1 deletion lib/Traits/WithDatastoreHandlerMethods.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace PHPNomad\Database\Traits;

use InvalidArgumentException;
use PHPNomad\Cache\Enums\Operation;
use PHPNomad\Datastore\Events\RecordCreated;
use PHPNomad\Datastore\Events\RecordDeleted;
Expand Down Expand Up @@ -352,14 +353,20 @@ protected function hydrateItems(array $data): array
* @param int|null $offset
* @return array
* @throws DatastoreErrorException
* @throws InvalidArgumentException
*/
public function findIds(array $conditions, ?int $limit = null, ?int $offset = null): array
{
$this->validateIdentityQuery($conditions, $limit, $offset);

$this->serviceProvider->clauseBuilder
->reset()
->useTable($this->table);
$this->serviceProvider->queryBuilder
->reset()
->from($this->table)
->select(...$this->table->getFieldsForIdentity());


if ($limit) {
$this->serviceProvider->queryBuilder->limit($limit);
}
Expand All @@ -373,6 +380,85 @@ public function findIds(array $conditions, ?int $limit = null, ?int $offset = nu
return $this->serviceProvider->queryStrategy->query($this->serviceProvider->queryBuilder);
}

/**
* Validate the optional identity-query contract without changing the behavior of other query methods.
*
* @param array<array-key, mixed> $conditions
*/
protected function validateIdentityQuery(array $conditions, ?int $limit, ?int $offset): void
{
if ($limit !== null && $limit <= 0) {
throw new InvalidArgumentException('The identity-query limit must be greater than zero.');
}

if ($offset !== null && $offset < 0) {
throw new InvalidArgumentException('The identity-query offset must not be negative.');
}

if ($conditions === []) {
return;
}

if (!$this->isIdentityQueryList($conditions)) {
throw new InvalidArgumentException('Identity-query conditions must be a list of groups.');
}

foreach ($conditions as $group) {
if (!is_array($group)
|| !array_key_exists('clauses', $group)
|| !is_array($group['clauses'])
|| !$this->isIdentityQueryList($group['clauses'])
|| $group['clauses'] === []
) {
throw new InvalidArgumentException('Each identity-query group must contain a non-empty list of clauses.');
}

foreach (['type', 'groupType'] as $key) {
if (array_key_exists($key, $group) && !is_string($group[$key])) {
throw new InvalidArgumentException(sprintf('Identity-query group "%s" must be a string.', $key));
}
}

foreach ($group['clauses'] as $clause) {
if (!is_array($clause)
|| !array_key_exists('column', $clause)
|| !array_key_exists('operator', $clause)
|| !is_string($clause['operator'])
|| $clause['operator'] === ''
) {
throw new InvalidArgumentException('Each identity-query clause must contain a column and string operator.');
}

$column = $clause['column'];
if (is_string($column)) {
if ($column === '') {
throw new InvalidArgumentException('Each identity-query clause column must be a non-empty string or non-empty list of strings.');
}

continue;
}

if (!is_array($column) || !$this->isIdentityQueryList($column) || $column === []) {
throw new InvalidArgumentException('Each identity-query clause column must be a non-empty string or non-empty list of strings.');
}

foreach ($column as $field) {
if (!is_string($field) || $field === '') {
throw new InvalidArgumentException('Each identity-query clause column must be a non-empty string or non-empty list of strings.');
}
}
}
}
}

/**
* @param array<array-key, mixed> $values
*/
protected function isIdentityQueryList(array $values): bool
{
return array_values($values) === $values;
}

/**
* Gets the models from the specified list of IDs.
*
Expand Down
Loading
Loading