From 02eef15e1da406ebf4f6c53740104d8297e1c239 Mon Sep 17 00:00:00 2001 From: Alex Standiford Date: Sun, 20 Sep 2026 15:40:53 -0400 Subject: [PATCH 1/5] Remove obsolete private dependency setup --- .github/workflows/phpstan.yml | 11 ----------- .github/workflows/phpunit.yml | 11 ----------- .wordlist.txt | 21 ++++++++++++++++++++- 3 files changed, 20 insertions(+), 23 deletions(-) diff --git a/.github/workflows/phpstan.yml b/.github/workflows/phpstan.yml index df7ac52..3211def 100644 --- a/.github/workflows/phpstan.yml +++ b/.github/workflows/phpstan.yml @@ -7,17 +7,6 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - - name: Set up SSH key - run: | - mkdir -p ~/.ssh - echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/id_rsa - chmod 600 ~/.ssh/id_rsa - - name: Add GitHub to known hosts - run: ssh-keyscan github.com >> ~/.ssh/known_hosts - - name: Add SSH key to agent - run: | - eval $(ssh-agent -s) - ssh-add ~/.ssh/id_rsa - uses: php-actions/composer@v6 - name: PHPStan Static Analysis uses: php-actions/phpstan@v3 diff --git a/.github/workflows/phpunit.yml b/.github/workflows/phpunit.yml index a456cbf..435456a 100644 --- a/.github/workflows/phpunit.yml +++ b/.github/workflows/phpunit.yml @@ -10,17 +10,6 @@ jobs: php: [ '7.4', '8.0', '8.1', '8.2' ] steps: - uses: actions/checkout@v3 - - name: Set up SSH key - run: | - mkdir -p ~/.ssh - echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/id_rsa - chmod 600 ~/.ssh/id_rsa - - name: Add GitHub to known hosts - run: ssh-keyscan github.com >> ~/.ssh/known_hosts - - name: Add SSH key to agent - run: | - eval $(ssh-agent -s) - ssh-add ~/.ssh/id_rsa - name: Clear composer cache run: composer clear-cache - uses: php-actions/composer@v6 diff --git a/.wordlist.txt b/.wordlist.txt index 7261aef..3669b0d 100644 --- a/.wordlist.txt +++ b/.wordlist.txt @@ -23,4 +23,23 @@ Traceback nodejs npm fediverse -readme \ No newline at end of file +readme +backend +composable +Datastore +datastore +Datastores +DatastoreHasPrimaryKey +DatastoreHasWhere +getPublishedPosts +MyApp +namespace +PHPNomad +phpnomad +PostDatastore +PostDatastoreHandler +PostDatastoreInterface +txt +WithDatastoreDecorator +WithDatastorePrimaryKeyDecorator +WithDatastoreWhereDecorator From f3fb34940dd558c8fd7456b47b7d1eb46ddca77e Mon Sep 17 00:00:00 2001 From: Alex Standiford Date: Sun, 20 Sep 2026 15:45:59 -0400 Subject: [PATCH 2/5] Let Composer derive package version --- composer.json | 1 - composer.lock | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/composer.json b/composer.json index c6c6f89..9c7646e 100644 --- a/composer.json +++ b/composer.json @@ -4,7 +4,6 @@ "type": "library", "homepage": "https://github.com/phpnomad/core", "readme": "README.md", - "version": "2.0.3", "license": "MIT", "scripts": { "php-cs-fixer": "php-cs-fixer fix" diff --git a/composer.lock b/composer.lock index d8369fc..96cf23d 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "8c562d7570b496d644e1f5d5bfcdc6db", + "content-hash": "253386b47711983ff7511e28779d2415", "packages": [], "packages-dev": [ { From eff42b5b7b63e24e7931b7d3848f1bf7cebd1812 Mon Sep 17 00:00:00 2001 From: Alex Standiford Date: Sun, 20 Sep 2026 15:51:15 -0400 Subject: [PATCH 3/5] Refresh vulnerable test tooling --- composer.json | 6 +- composer.lock | 203 +++++++++++++++++++++++++++++++------------------- 2 files changed, 130 insertions(+), 79 deletions(-) diff --git a/composer.json b/composer.json index 9c7646e..55d8c20 100644 --- a/composer.json +++ b/composer.json @@ -25,7 +25,11 @@ } ], "require-dev": { - "phpnomad/tests": "^0.1.0 || ^0.3.0" + "doctrine/instantiator": "^1.5", + "myclabs/deep-copy": ">=1.13.4 <1.14", + "phpnomad/tests": "^0.1.0 || ^0.3.0", + "phpunit/phpunit": "^9.6.33", + "symfony/process": "^5.4.51" }, "config": { "allow-plugins": { diff --git a/composer.lock b/composer.lock index 96cf23d..a6153fe 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "253386b47711983ff7511e28779d2415", + "content-hash": "0fa283ca307757d17b39502f6c4e6736", "packages": [], "packages-dev": [ { @@ -715,16 +715,16 @@ }, { "name": "myclabs/deep-copy", - "version": "1.13.0", + "version": "1.13.4", "source": { "type": "git", "url": "https://github.com/myclabs/DeepCopy.git", - "reference": "024473a478be9df5fdaca2c793f2232fe788e414" + "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/024473a478be9df5fdaca2c793f2232fe788e414", - "reference": "024473a478be9df5fdaca2c793f2232fe788e414", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/07d290f0c47959fd5eed98c95ee5602db07e0b6a", + "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a", "shasum": "" }, "require": { @@ -763,7 +763,7 @@ ], "support": { "issues": "https://github.com/myclabs/DeepCopy/issues", - "source": "https://github.com/myclabs/DeepCopy/tree/1.13.0" + "source": "https://github.com/myclabs/DeepCopy/tree/1.13.4" }, "funding": [ { @@ -771,24 +771,23 @@ "type": "tidelift" } ], - "time": "2025-02-12T12:17:51+00:00" + "time": "2025-08-01T08:46:24+00:00" }, { "name": "nikic/php-parser", - "version": "v5.4.0", + "version": "v5.9.0", "source": { "type": "git", "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "447a020a1f875a434d62f2a401f53b82a396e494" + "reference": "9e33da9553fe7786f0962b35f4e4ecf01be89def" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/447a020a1f875a434d62f2a401f53b82a396e494", - "reference": "447a020a1f875a434d62f2a401f53b82a396e494", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/9e33da9553fe7786f0962b35f4e4ecf01be89def", + "reference": "9e33da9553fe7786f0962b35f4e4ecf01be89def", "shasum": "" }, "require": { - "ext-ctype": "*", "ext-json": "*", "ext-tokenizer": "*", "php": ">=7.4" @@ -803,7 +802,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "5.0-dev" + "dev-master": "5.x-dev" } }, "autoload": { @@ -827,9 +826,9 @@ ], "support": { "issues": "https://github.com/nikic/PHP-Parser/issues", - "source": "https://github.com/nikic/PHP-Parser/tree/v5.4.0" + "source": "https://github.com/nikic/PHP-Parser/tree/v5.9.0" }, - "time": "2024-12-30T11:07:19+00:00" + "time": "2026-09-13T18:51:52+00:00" }, { "name": "phar-io/manifest", @@ -1474,27 +1473,27 @@ }, { "name": "phpunit/phpunit", - "version": "9.6.22", + "version": "9.6.36", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "f80235cb4d3caa59ae09be3adf1ded27521d1a9c" + "reference": "abab27ed286d3e1246fbbfe6b56bfd732d945ec9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/f80235cb4d3caa59ae09be3adf1ded27521d1a9c", - "reference": "f80235cb4d3caa59ae09be3adf1ded27521d1a9c", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/abab27ed286d3e1246fbbfe6b56bfd732d945ec9", + "reference": "abab27ed286d3e1246fbbfe6b56bfd732d945ec9", "shasum": "" }, "require": { "doctrine/instantiator": "^1.5.0 || ^2", "ext-dom": "*", + "ext-filter": "*", "ext-json": "*", "ext-libxml": "*", "ext-mbstring": "*", - "ext-xml": "*", "ext-xmlwriter": "*", - "myclabs/deep-copy": "^1.12.1", + "myclabs/deep-copy": "^1.13.4", "phar-io/manifest": "^2.0.4", "phar-io/version": "^3.2.1", "php": ">=7.3", @@ -1505,11 +1504,11 @@ "phpunit/php-timer": "^5.0.3", "sebastian/cli-parser": "^1.0.2", "sebastian/code-unit": "^1.0.8", - "sebastian/comparator": "^4.0.8", + "sebastian/comparator": "^4.0.10", "sebastian/diff": "^4.0.6", "sebastian/environment": "^5.1.5", - "sebastian/exporter": "^4.0.6", - "sebastian/global-state": "^5.0.7", + "sebastian/exporter": "^4.0.9", + "sebastian/global-state": "^5.0.8", "sebastian/object-enumerator": "^4.0.4", "sebastian/resource-operations": "^3.0.4", "sebastian/type": "^3.2.1", @@ -1557,23 +1556,15 @@ "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", "security": "https://github.com/sebastianbergmann/phpunit/security/policy", - "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.22" + "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.36" }, "funding": [ { - "url": "https://phpunit.de/sponsors.html", - "type": "custom" - }, - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/phpunit/phpunit", - "type": "tidelift" + "url": "https://phpunit.de/sponsoring.html", + "type": "other" } ], - "time": "2024-12-05T13:48:26+00:00" + "time": "2026-08-11T06:25:15+00:00" }, { "name": "psr/container", @@ -2418,16 +2409,16 @@ }, { "name": "sebastian/comparator", - "version": "4.0.8", + "version": "4.0.10", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/comparator.git", - "reference": "fa0f136dd2334583309d32b62544682ee972b51a" + "reference": "e4df00b9b3571187db2831ae9aada2c6efbd715d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/fa0f136dd2334583309d32b62544682ee972b51a", - "reference": "fa0f136dd2334583309d32b62544682ee972b51a", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/e4df00b9b3571187db2831ae9aada2c6efbd715d", + "reference": "e4df00b9b3571187db2831ae9aada2c6efbd715d", "shasum": "" }, "require": { @@ -2480,15 +2471,27 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/comparator/issues", - "source": "https://github.com/sebastianbergmann/comparator/tree/4.0.8" + "source": "https://github.com/sebastianbergmann/comparator/tree/4.0.10" }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/comparator", + "type": "tidelift" } ], - "time": "2022-09-14T12:41:17+00:00" + "time": "2026-01-24T09:22:56+00:00" }, { "name": "sebastian/complexity", @@ -2678,16 +2681,16 @@ }, { "name": "sebastian/exporter", - "version": "4.0.6", + "version": "4.0.9", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/exporter.git", - "reference": "78c00df8f170e02473b682df15bfcdacc3d32d72" + "reference": "4352c1a3df741a7ba9e61af6fed51d1fee41cbf7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/78c00df8f170e02473b682df15bfcdacc3d32d72", - "reference": "78c00df8f170e02473b682df15bfcdacc3d32d72", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/4352c1a3df741a7ba9e61af6fed51d1fee41cbf7", + "reference": "4352c1a3df741a7ba9e61af6fed51d1fee41cbf7", "shasum": "" }, "require": { @@ -2743,28 +2746,40 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/exporter/issues", - "source": "https://github.com/sebastianbergmann/exporter/tree/4.0.6" + "source": "https://github.com/sebastianbergmann/exporter/tree/4.0.9" }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/exporter", + "type": "tidelift" } ], - "time": "2024-03-02T06:33:00+00:00" + "time": "2026-08-11T04:55:59+00:00" }, { "name": "sebastian/global-state", - "version": "5.0.7", + "version": "5.0.8", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/global-state.git", - "reference": "bca7df1f32ee6fe93b4d4a9abbf69e13a4ada2c9" + "reference": "b6781316bdcd28260904e7cc18ec983d0d2ef4f6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/bca7df1f32ee6fe93b4d4a9abbf69e13a4ada2c9", - "reference": "bca7df1f32ee6fe93b4d4a9abbf69e13a4ada2c9", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/b6781316bdcd28260904e7cc18ec983d0d2ef4f6", + "reference": "b6781316bdcd28260904e7cc18ec983d0d2ef4f6", "shasum": "" }, "require": { @@ -2807,15 +2822,27 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/global-state/issues", - "source": "https://github.com/sebastianbergmann/global-state/tree/5.0.7" + "source": "https://github.com/sebastianbergmann/global-state/tree/5.0.8" }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/global-state", + "type": "tidelift" } ], - "time": "2024-03-02T06:35:11+00:00" + "time": "2025-08-10T07:10:35+00:00" }, { "name": "sebastian/lines-of-code", @@ -2988,16 +3015,16 @@ }, { "name": "sebastian/recursion-context", - "version": "4.0.5", + "version": "4.0.7", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/recursion-context.git", - "reference": "e75bd0f07204fec2a0af9b0f3cfe97d05f92efc1" + "reference": "c85be6922b7fd365942b986b9a50397d65407611" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/e75bd0f07204fec2a0af9b0f3cfe97d05f92efc1", - "reference": "e75bd0f07204fec2a0af9b0f3cfe97d05f92efc1", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/c85be6922b7fd365942b986b9a50397d65407611", + "reference": "c85be6922b7fd365942b986b9a50397d65407611", "shasum": "" }, "require": { @@ -3039,15 +3066,27 @@ "homepage": "https://github.com/sebastianbergmann/recursion-context", "support": { "issues": "https://github.com/sebastianbergmann/recursion-context/issues", - "source": "https://github.com/sebastianbergmann/recursion-context/tree/4.0.5" + "source": "https://github.com/sebastianbergmann/recursion-context/tree/4.0.7" }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/recursion-context", + "type": "tidelift" } ], - "time": "2023-02-03T06:07:39+00:00" + "time": "2026-08-11T05:25:24+00:00" }, { "name": "sebastian/resource-operations", @@ -4137,16 +4176,16 @@ }, { "name": "symfony/polyfill-php80", - "version": "v1.31.0", + "version": "v1.37.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php80.git", - "reference": "60328e362d4c2c802a54fcbf04f9d3fb892b4cf8" + "reference": "dfb55726c3a76ea3b6459fcfda1ec2d80a682411" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/60328e362d4c2c802a54fcbf04f9d3fb892b4cf8", - "reference": "60328e362d4c2c802a54fcbf04f9d3fb892b4cf8", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/dfb55726c3a76ea3b6459fcfda1ec2d80a682411", + "reference": "dfb55726c3a76ea3b6459fcfda1ec2d80a682411", "shasum": "" }, "require": { @@ -4197,7 +4236,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php80/tree/v1.31.0" + "source": "https://github.com/symfony/polyfill-php80/tree/v1.37.0" }, "funding": [ { @@ -4208,12 +4247,16 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2024-09-09T11:45:10+00:00" + "time": "2026-04-10T16:19:22+00:00" }, { "name": "symfony/polyfill-php81", @@ -4293,16 +4336,16 @@ }, { "name": "symfony/process", - "version": "v5.4.47", + "version": "v5.4.51", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "5d1662fb32ebc94f17ddb8d635454a776066733d" + "reference": "467bfc56f18f5ef6d5ccb09324d7e988c1c0a98f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/5d1662fb32ebc94f17ddb8d635454a776066733d", - "reference": "5d1662fb32ebc94f17ddb8d635454a776066733d", + "url": "https://api.github.com/repos/symfony/process/zipball/467bfc56f18f5ef6d5ccb09324d7e988c1c0a98f", + "reference": "467bfc56f18f5ef6d5ccb09324d7e988c1c0a98f", "shasum": "" }, "require": { @@ -4335,7 +4378,7 @@ "description": "Executes commands in sub-processes", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/process/tree/v5.4.47" + "source": "https://github.com/symfony/process/tree/v5.4.51" }, "funding": [ { @@ -4346,12 +4389,16 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2024-11-06T11:36:42+00:00" + "time": "2026-01-26T15:53:37+00:00" }, { "name": "symfony/service-contracts", @@ -4586,16 +4633,16 @@ }, { "name": "theseer/tokenizer", - "version": "1.2.3", + "version": "1.3.1", "source": { "type": "git", "url": "https://github.com/theseer/tokenizer.git", - "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2" + "reference": "b7489ce515e168639d17feec34b8847c326b0b3c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/theseer/tokenizer/zipball/737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2", - "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/b7489ce515e168639d17feec34b8847c326b0b3c", + "reference": "b7489ce515e168639d17feec34b8847c326b0b3c", "shasum": "" }, "require": { @@ -4624,7 +4671,7 @@ "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", "support": { "issues": "https://github.com/theseer/tokenizer/issues", - "source": "https://github.com/theseer/tokenizer/tree/1.2.3" + "source": "https://github.com/theseer/tokenizer/tree/1.3.1" }, "funding": [ { @@ -4632,7 +4679,7 @@ "type": "github" } ], - "time": "2024-03-03T12:36:25+00:00" + "time": "2025-11-17T20:03:58+00:00" } ], "aliases": [], From abf2ce80c1aee4791dd9b785fb54616fb38e79a6 Mon Sep 17 00:00:00 2001 From: Alex Standiford Date: Sun, 20 Sep 2026 15:53:49 -0400 Subject: [PATCH 4/5] Recognize project protocol names --- .wordlist.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.wordlist.txt b/.wordlist.txt index 3669b0d..d78e311 100644 --- a/.wordlist.txt +++ b/.wordlist.txt @@ -16,7 +16,9 @@ alex standiford socio GIFs +GraphQL macOS +MCP SDK runtime Traceback From fa0392a74d35bac3a0f1812d691d5a2163d80d68 Mon Sep 17 00:00:00 2001 From: Alex Standiford Date: Sun, 20 Sep 2026 17:28:15 -0400 Subject: [PATCH 5/5] Add optional identity query capability --- composer.json | 3 + composer.lock | 47 +++++- lib/Interfaces/DatastoreHasIdentityQuery.php | 36 ++++ .../WithDatastorePrimaryKeyDecorator.php | 32 +++- phpstan-baseline.neon | 156 ++++++++++++++++++ phpstan.neon.dist | 8 + .../StaticAnalysis/IdentityQueryContract.php | 21 +++ .../WithDatastorePrimaryKeyDecoratorTest.php | 106 ++++++++++++ 8 files changed, 405 insertions(+), 4 deletions(-) create mode 100644 lib/Interfaces/DatastoreHasIdentityQuery.php create mode 100644 phpstan-baseline.neon create mode 100644 phpstan.neon.dist create mode 100644 tests/StaticAnalysis/IdentityQueryContract.php create mode 100644 tests/Unit/WithDatastorePrimaryKeyDecoratorTest.php diff --git a/composer.json b/composer.json index 55d8c20..a90d48a 100644 --- a/composer.json +++ b/composer.json @@ -24,6 +24,9 @@ "email": "alex@standiford.us" } ], + "require": { + "phpnomad/event": "^1.0" + }, "require-dev": { "doctrine/instantiator": "^1.5", "myclabs/deep-copy": ">=1.13.4 <1.14", diff --git a/composer.lock b/composer.lock index a6153fe..403b640 100644 --- a/composer.lock +++ b/composer.lock @@ -4,8 +4,49 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "0fa283ca307757d17b39502f6c4e6736", - "packages": [], + "content-hash": "6638e777210e2a06a5c0d87ec74fd210", + "packages": [ + { + "name": "phpnomad/event", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/phpnomad/event.git", + "reference": "b418d8fc90af18b7cb37e118795f9dd92ffeb538" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpnomad/event/zipball/b418d8fc90af18b7cb37e118795f9dd92ffeb538", + "reference": "b418d8fc90af18b7cb37e118795f9dd92ffeb538", + "shasum": "" + }, + "require-dev": { + "phpnomad/tests": "^0.1.0 || ^0.3.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "PHPNomad\\Events\\": "lib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Alex Standiford", + "email": "alex@standiford.us" + } + ], + "homepage": "https://github.com/phpnomad/core", + "support": { + "issues": "https://github.com/phpnomad/event/issues", + "source": "https://github.com/phpnomad/event/tree/1.0.1" + }, + "time": "2026-06-12T10:56:48+00:00" + } + ], "packages-dev": [ { "name": "clue/ndjson-react", @@ -4689,5 +4730,5 @@ "prefer-lowest": false, "platform": {}, "platform-dev": {}, - "plugin-api-version": "2.6.0" + "plugin-api-version": "2.9.0" } diff --git a/lib/Interfaces/DatastoreHasIdentityQuery.php b/lib/Interfaces/DatastoreHasIdentityQuery.php new file mode 100644 index 0000000..6a540a4 --- /dev/null +++ b/lib/Interfaces/DatastoreHasIdentityQuery.php @@ -0,0 +1,36 @@ +, + * operator: string, + * value?: mixed + * } + * @phpstan-type IdentityQueryGroup array{ + * type?: string, + * groupType?: string, + * clauses: non-empty-list + * } + * @phpstan-type DatastoreIdentity array + */ +interface DatastoreHasIdentityQuery +{ + /** + * Retrieve identity-field rows matching grouped conditions. + * + * @param list $conditions + * @param positive-int|null $limit + * @param int<0, max>|null $offset + * @return array + * @throws DatastoreErrorException + * @throws InvalidArgumentException When conditions or pagination values are invalid. + */ + public function findIds(array $conditions, ?int $limit = null, ?int $offset = null): array; +} diff --git a/lib/Traits/WithDatastorePrimaryKeyDecorator.php b/lib/Traits/WithDatastorePrimaryKeyDecorator.php index 13a5ba5..8a68fce 100644 --- a/lib/Traits/WithDatastorePrimaryKeyDecorator.php +++ b/lib/Traits/WithDatastorePrimaryKeyDecorator.php @@ -2,36 +2,66 @@ namespace PHPNomad\Datastore\Traits; +use LogicException; use PHPNomad\Datastore\Interfaces\DataModel; use PHPNomad\Datastore\Interfaces\Datastore; +use PHPNomad\Datastore\Interfaces\DatastoreHasIdentityQuery; use PHPNomad\Datastore\Interfaces\DatastoreHasPrimaryKey; trait WithDatastorePrimaryKeyDecorator { protected Datastore $datastoreHandler; + /** + * @param mixed $id + * @return DataModel + */ public function find($id): DataModel { return $this->datastoreHandler->find($id); } + /** + * @param array $ids + * @return array + */ public function findMultiple(array $ids): array { return $this->datastoreHandler->findMultiple($ids); } + /** + * @param mixed $id + * @param array $attributes + */ public function update($id, array $attributes): void { $this->datastoreHandler->update($id, $attributes); } + /** @param mixed $id */ public function delete($id): void { $this->datastoreHandler->delete($id); } + /** + * @param list, operator: string, value?: mixed}> + * }> $conditions + * @param positive-int|null $limit + * @param int<0, max>|null $offset + * @return array> + * @throws LogicException + */ public function findIds(array $conditions, ?int $limit = null, ?int $offset = null): array { + if (!$this->datastoreHandler instanceof DatastoreHasIdentityQuery) { + throw new LogicException('The decorated datastore does not support identity queries.'); + } + return $this->datastoreHandler->findIds($conditions, $limit, $offset); } -} \ No newline at end of file +} diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon new file mode 100644 index 0000000..6c34dc2 --- /dev/null +++ b/phpstan-baseline.neon @@ -0,0 +1,156 @@ +parameters: + ignoreErrors: + - + message: "#^Method PHPNomad\\\\Datastore\\\\Events\\\\ModelsMerged\\:\\:getMergedModel\\(\\) should return T of PHPNomad\\\\Datastore\\\\Interfaces\\\\DataModel but returns PHPNomad\\\\Datastore\\\\Interfaces\\\\DataModel\\.$#" + count: 1 + path: lib/Events/ModelsMerged.php + + - + message: "#^Method PHPNomad\\\\Datastore\\\\Events\\\\ModelsMerged\\:\\:getOriginalModel\\(\\) should return T of PHPNomad\\\\Datastore\\\\Interfaces\\\\DataModel but returns PHPNomad\\\\Datastore\\\\Interfaces\\\\DataModel\\.$#" + count: 1 + path: lib/Events/ModelsMerged.php + + - + message: "#^Property PHPNomad\\\\Datastore\\\\Events\\\\ModelsMerged\\:\\:\\$removedModels type has no value type specified in iterable type array\\.$#" + count: 1 + path: lib/Events/ModelsMerged.php + + - + message: "#^Method PHPNomad\\\\Datastore\\\\Events\\\\RecordDeleted\\:\\:__construct\\(\\) has parameter \\$identity with no value type specified in iterable type array\\.$#" + count: 1 + path: lib/Events/RecordDeleted.php + + - + message: "#^Method PHPNomad\\\\Datastore\\\\Events\\\\RecordDeleted\\:\\:getIdentity\\(\\) return type has no value type specified in iterable type array\\.$#" + count: 1 + path: lib/Events/RecordDeleted.php + + - + message: "#^Property PHPNomad\\\\Datastore\\\\Events\\\\RecordDeleted\\:\\:\\$identity type has no value type specified in iterable type array\\.$#" + count: 1 + path: lib/Events/RecordDeleted.php + + - + message: "#^Method PHPNomad\\\\Datastore\\\\Events\\\\RecordUpdated\\:\\:__construct\\(\\) has parameter \\$data with no value type specified in iterable type array\\.$#" + count: 1 + path: lib/Events/RecordUpdated.php + + - + message: "#^Method PHPNomad\\\\Datastore\\\\Events\\\\RecordUpdated\\:\\:__construct\\(\\) has parameter \\$identity with no value type specified in iterable type array\\.$#" + count: 1 + path: lib/Events/RecordUpdated.php + + - + message: "#^Method PHPNomad\\\\Datastore\\\\Events\\\\RecordUpdated\\:\\:getData\\(\\) return type has no value type specified in iterable type array\\.$#" + count: 1 + path: lib/Events/RecordUpdated.php + + - + message: "#^Method PHPNomad\\\\Datastore\\\\Events\\\\RecordUpdated\\:\\:getIdentity\\(\\) return type has no value type specified in iterable type array\\.$#" + count: 1 + path: lib/Events/RecordUpdated.php + + - + message: "#^Property PHPNomad\\\\Datastore\\\\Events\\\\RecordUpdated\\:\\:\\$data type has no value type specified in iterable type array\\.$#" + count: 1 + path: lib/Events/RecordUpdated.php + + - + message: "#^Property PHPNomad\\\\Datastore\\\\Events\\\\RecordUpdated\\:\\:\\$identity type has no value type specified in iterable type array\\.$#" + count: 1 + path: lib/Events/RecordUpdated.php + + - + message: "#^Method PHPNomad\\\\Datastore\\\\Factories\\\\IdentityToWhere\\:\\:__construct\\(\\) has parameter \\$identity with no value type specified in iterable type array\\.$#" + count: 1 + path: lib/Factories/IdentityToWhere.php + + - + message: "#^Method PHPNomad\\\\Datastore\\\\Factories\\\\IdentityToWhere\\:\\:toWhere\\(\\) return type has no value type specified in iterable type array\\.$#" + count: 1 + path: lib/Factories/IdentityToWhere.php + + - + message: "#^Property PHPNomad\\\\Datastore\\\\Factories\\\\IdentityToWhere\\:\\:\\$identity type has no value type specified in iterable type array\\.$#" + count: 1 + path: lib/Factories/IdentityToWhere.php + + - + message: "#^Method PHPNomad\\\\Datastore\\\\Interfaces\\\\CanConvertModelToArray\\:\\:toArray\\(\\) return type has no value type specified in iterable type array\\.$#" + count: 1 + path: lib/Interfaces/CanConvertModelToArray.php + + - + message: "#^Method PHPNomad\\\\Datastore\\\\Interfaces\\\\Datastore\\:\\:create\\(\\) has invalid return type PHPNomad\\\\Datastore\\\\Interfaces\\\\T\\.$#" + count: 1 + path: lib/Interfaces/Datastore.php + + - + message: "#^PHPDoc tag @return with type PHPNomad\\\\Datastore\\\\Interfaces\\\\T is not subtype of native type PHPNomad\\\\Datastore\\\\Interfaces\\\\DataModel\\.$#" + count: 1 + path: lib/Interfaces/Datastore.php + + - + message: "#^PHPDoc tag @template has invalid value \\(T of \\\\)\\: Unexpected token \"\\<\", expected type at offset 22$#" + count: 1 + path: lib/Interfaces/Datastore.php + + - + message: "#^Method PHPNomad\\\\Datastore\\\\Interfaces\\\\DatastoreHasPrimaryKey\\:\\:findMultiple\\(\\) has parameter \\$ids with no value type specified in iterable type array\\.$#" + count: 1 + path: lib/Interfaces/DatastoreHasPrimaryKey.php + + - + message: "#^Method PHPNomad\\\\Datastore\\\\Interfaces\\\\DatastoreHasPrimaryKey\\:\\:findMultiple\\(\\) return type has no value type specified in iterable type array\\.$#" + count: 1 + path: lib/Interfaces/DatastoreHasPrimaryKey.php + + - + message: "#^Method PHPNomad\\\\Datastore\\\\Interfaces\\\\DatastoreHasWhere\\:\\:andWhere\\(\\) has invalid return type PHPNomad\\\\Datastore\\\\Interfaces\\\\T\\.$#" + count: 1 + path: lib/Interfaces/DatastoreHasWhere.php + + - + message: "#^Method PHPNomad\\\\Datastore\\\\Interfaces\\\\DatastoreHasWhere\\:\\:findBy\\(\\) has invalid return type PHPNomad\\\\Datastore\\\\Interfaces\\\\T\\.$#" + count: 1 + path: lib/Interfaces/DatastoreHasWhere.php + + - + message: "#^Method PHPNomad\\\\Datastore\\\\Interfaces\\\\DatastoreHasWhere\\:\\:findBy\\(\\) has parameter \\$value with no type specified\\.$#" + count: 1 + path: lib/Interfaces/DatastoreHasWhere.php + + - + message: "#^Method PHPNomad\\\\Datastore\\\\Interfaces\\\\DatastoreHasWhere\\:\\:orWhere\\(\\) has invalid return type PHPNomad\\\\Datastore\\\\Interfaces\\\\T\\.$#" + count: 1 + path: lib/Interfaces/DatastoreHasWhere.php + + - + message: "#^Method PHPNomad\\\\Datastore\\\\Interfaces\\\\DatastoreHasWhere\\:\\:where\\(\\) has invalid return type PHPNomad\\\\Datastore\\\\Interfaces\\\\T\\.$#" + count: 1 + path: lib/Interfaces/DatastoreHasWhere.php + + - + message: "#^PHPDoc tag @return with type PHPNomad\\\\Datastore\\\\Interfaces\\\\T is not subtype of native type PHPNomad\\\\Datastore\\\\Interfaces\\\\DataModel\\.$#" + count: 1 + path: lib/Interfaces/DatastoreHasWhere.php + + - + message: "#^PHPDoc tag @template has invalid value \\(T of \\\\)\\: Unexpected token \"\\<\", expected type at offset 22$#" + count: 1 + path: lib/Interfaces/DatastoreHasWhere.php + + - + message: "#^Method PHPNomad\\\\Datastore\\\\Interfaces\\\\JunctionHandler\\:\\:getModelsFromResource\\(\\) return type has no value type specified in iterable type array\\.$#" + count: 1 + path: lib/Interfaces/JunctionHandler.php + + - + message: "#^Interface PHPNomad\\\\Datastore\\\\Interfaces\\\\ModelAdapter extends generic interface PHPNomad\\\\Datastore\\\\Interfaces\\\\CanConvertModelToArray but does not specify its types\\: TModel$#" + count: 1 + path: lib/Interfaces/ModelAdapter.php + + - + message: "#^Method PHPNomad\\\\Datastore\\\\Interfaces\\\\ModelAdapter\\:\\:toModel\\(\\) has parameter \\$array with no value type specified in iterable type array\\.$#" + count: 1 + path: lib/Interfaces/ModelAdapter.php diff --git a/phpstan.neon.dist b/phpstan.neon.dist new file mode 100644 index 0000000..ef5c352 --- /dev/null +++ b/phpstan.neon.dist @@ -0,0 +1,8 @@ +includes: + - phpstan-baseline.neon + +parameters: + level: 9 + paths: + - lib + - tests diff --git a/tests/StaticAnalysis/IdentityQueryContract.php b/tests/StaticAnalysis/IdentityQueryContract.php new file mode 100644 index 0000000..5a94286 --- /dev/null +++ b/tests/StaticAnalysis/IdentityQueryContract.php @@ -0,0 +1,21 @@ +findIds([ + [ + 'type' => 'AND', + 'groupType' => 'OR', + 'clauses' => [ + ['column' => ['tenantId', 'externalId'], 'operator' => 'IN', 'value' => [[1, 'record-17']]], + ['column' => 'deletedAt', 'operator' => 'IS NULL'], + ], + ], + ], 1, 0); + + return (string) $rows[0]['externalId']; +} diff --git a/tests/Unit/WithDatastorePrimaryKeyDecoratorTest.php b/tests/Unit/WithDatastorePrimaryKeyDecoratorTest.php new file mode 100644 index 0000000..3d455bf --- /dev/null +++ b/tests/Unit/WithDatastorePrimaryKeyDecoratorTest.php @@ -0,0 +1,106 @@ + [['column' => 'status', 'operator' => 'IS NOT NULL']], + ]]; + + $this->assertSame([['id' => 17, 'externalId' => 'record-17']], $decorator->findIds($conditions, 1, 0)); + $this->assertSame([$conditions, 1, 0], $handler->lastIdentityQuery); + } + + public function testIdentityQueriesFailClearlyWhenTheCapabilityIsUnavailable(): void + { + $decorator = new PrimaryKeyDecoratorFixture(new BaseDatastoreFixture()); + + $this->expectException(LogicException::class); + $this->expectExceptionMessage('does not support identity queries'); + + $decorator->findIds([]); + } + +} + +final class PrimaryKeyDecoratorFixture +{ + use WithDatastorePrimaryKeyDecorator; + + /** @var Datastore&DatastoreHasPrimaryKey */ + protected Datastore $datastoreHandler; + + /** @param Datastore&DatastoreHasPrimaryKey $handler */ + public function __construct(Datastore $handler) + { + $this->datastoreHandler = $handler; + } +} + +class BaseDatastoreFixture implements Datastore, DatastoreHasPrimaryKey +{ + public function create(array $attributes): DataModel + { + return new IdentityModelFixture(); + } + + public function updateCompound(array $ids, array $attributes): void + { + } + + public function find($id): DataModel + { + return new IdentityModelFixture(); + } + + /** + * @param array $ids + * @return array + */ + public function findMultiple(array $ids): array + { + return [new IdentityModelFixture()]; + } + + public function update($id, array $attributes): void + { + } + + public function delete($id): void + { + } +} + +final class IdentityQueryHandlerFixture extends BaseDatastoreFixture implements DatastoreHasIdentityQuery +{ + /** @var array{array, int|null, int|null}|null */ + public ?array $lastIdentityQuery = null; + + public function findIds(array $conditions, ?int $limit = null, ?int $offset = null): array + { + $this->lastIdentityQuery = [$conditions, $limit, $offset]; + + return [['id' => 17, 'externalId' => 'record-17']]; + } +} + +final class IdentityModelFixture implements DataModel +{ + public function getIdentity(): array + { + return ['id' => 17]; + } +}