From 1ecb92e7384670c57db67ae88caa564e4d787261 Mon Sep 17 00:00:00 2001 From: Daniel Badura Date: Mon, 15 Jun 2026 16:46:53 +0200 Subject: [PATCH 1/2] Update docs, add tools --- .gitattributes | 13 + .github/workflows/docs-check.yml | 48 + .github/workflows/docs-deploy.yml | 22 + Makefile | 22 + README.md | 170 +- bin/docs-extract-php-code | 53 + bin/docs-inject-php-code | 70 + composer.json | 9 +- composer.lock | 569 +++- docs/databases.md | 92 + docs/documents.md | 161 + docs/encryption.md | 103 + docs/field-mapping.md | 157 + docs/getting-started.md | 152 + docs/introduction.md | 46 + docs/project.json | 21 + docs/repository.md | 210 ++ tools/composer.json | 6 + tools/composer.lock | 4680 +++++++++++++++++++++++++++++ 19 files changed, 6456 insertions(+), 148 deletions(-) create mode 100644 .gitattributes create mode 100644 .github/workflows/docs-check.yml create mode 100644 .github/workflows/docs-deploy.yml create mode 100755 bin/docs-extract-php-code create mode 100755 bin/docs-inject-php-code create mode 100644 docs/databases.md create mode 100644 docs/documents.md create mode 100644 docs/encryption.md create mode 100644 docs/field-mapping.md create mode 100644 docs/getting-started.md create mode 100644 docs/introduction.md create mode 100644 docs/project.json create mode 100644 docs/repository.md create mode 100644 tools/composer.json create mode 100644 tools/composer.lock diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..57f1752 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,13 @@ +*.php text eol=lf +*.phpt text eol=lf +/.gitattributes export-ignore +/.github/ export-ignore +/.gitignore export-ignore +/composer.lock export-ignore +/docs/ export-ignore +/phpbench.json export-ignore +/phpcs.xml.dist export-ignore +/phpstan.neon export-ignore +/phpunit.xml.dist export-ignore +/tests/ export-ignore +/tools/ export-ignore diff --git a/.github/workflows/docs-check.yml b/.github/workflows/docs-check.yml new file mode 100644 index 0000000..b36231f --- /dev/null +++ b/.github/workflows/docs-check.yml @@ -0,0 +1,48 @@ +# https://help.github.com/en/categories/automating-your-workflow-with-github-actions + +name: "Check Docs" + +on: + pull_request: + push: + branches: + - "[0-9]+.[0-9]+.x" + +jobs: + checkdocs: + name: "Check Docs" + + runs-on: ${{ matrix.operating-system }} + + strategy: + matrix: + dependencies: + - "locked" + php-version: + - "8.4" + operating-system: + - "ubuntu-latest" + + steps: + - name: "Checkout" + uses: actions/checkout@v6 + + - name: "Install PHP" + uses: "shivammathur/setup-php@2.37.2" + with: + coverage: none + php-version: "${{ matrix.php-version }}" + ini-values: memory_limit=-1, opcache.enable_cli=1 + + - uses: ramsey/composer-install@4.0.0 + with: + dependency-versions: ${{ matrix.dependencies }} + + - name: "extract php code" + run: "bin/docs-extract-php-code" + + - name: "lint php" + run: "php -l docs_php/*.php" + + - name: "docs code style" + run: "vendor/bin/phpcbf docs_php --exclude=SlevomatCodingStandard.TypeHints.DeclareStrictTypes,SlevomatCodingStandard.ControlStructures.EarlyExit" \ No newline at end of file diff --git a/.github/workflows/docs-deploy.yml b/.github/workflows/docs-deploy.yml new file mode 100644 index 0000000..7cb425e --- /dev/null +++ b/.github/workflows/docs-deploy.yml @@ -0,0 +1,22 @@ +name: Publish docs + +on: + push: + branches: + - "[0-9]+.[0-9]+.x" + release: + types: + - published + +jobs: + trigger: + runs-on: ubuntu-latest + steps: + - name: Trigger workflow in other repo + run: | + curl -L -X POST \ + -H "Accept: application/vnd.github+json" \ + -H "Authorization: Bearer ${{ secrets.ORGANIZATION_ADMIN_TOKEN }}" \ + -H "X-GitHub-Api-Version: 2026-03-10" \ + https://api.github.com/repos/patchlevel/patchlevel.dev/actions/workflows/prod-deployment.yaml/dispatches \ + -d '{"ref":"main"}' \ No newline at end of file diff --git a/Makefile b/Makefile index 4b83860..36b081b 100644 --- a/Makefile +++ b/Makefile @@ -61,3 +61,25 @@ benchmark-diff-test: vendor .PHONY: dev dev: static test ## run dev tools + +.PHONY: docs +docs: docs-extract-php docs-php-lint docs-phpcs docs-inject-php + +.PHONY: docs-extract-php +docs-extract-php: + bin/docs-extract-php-code + +.PHONY: docs-inject-php +docs-inject-php: + bin/docs-inject-php-code + +.PHONY: docs-format ## format docs +docs-format: docs-phpcs docs-inject-php + +.PHONY: docs-php-lint ## lint docs code +docs-php-lint: docs-extract-php + php -l docs_php/*.php | grep 'Parse error: ' || true + +.PHONY: docs-phpcs +docs-phpcs: docs-extract-php + vendor/bin/phpcbf docs_php --exclude=SlevomatCodingStandard.TypeHints.DeclareStrictTypes,SlevomatCodingStandard.ControlStructures.EarlyExit || true diff --git a/README.md b/README.md index 9cb4aea..17e3583 100644 --- a/README.md +++ b/README.md @@ -1,160 +1,42 @@ -# Patchlevel ODM - -Patchlevel ODM is a lightweight **Object Document Mapper (ODM)** for PHP that works with **MongoDB** and **PostgreSQL (via [patchlevel/rango](https://github.com/patchlevel/rango/))**. -It is built on top of our superfast **[patchlevel/hydrator](https://github.com/patchlevel/hydrator/)**, providing a simple attribute-based mapping layer and -enterprise-grade features like cyptography. - -## πŸš€ Why Patchlevel ODM? +[![Mutation testing badge](https://img.shields.io/endpoint?style=flat&url=https%3A%2F%2Fbadge-api.stryker-mutator.io%2Fgithub.com%2Fpatchlevel%2Fodm%2F1.0.x)](https://dashboard.stryker-mutator.io/reports/github.com/patchlevel/odm/1.0.x) +[![Latest Stable Version](https://poser.pugx.org/patchlevel/odm/v)](//packagist.org/packages/patchlevel/odm) +[![License](https://poser.pugx.org/patchlevel/odm/license)](//packagist.org/packages/patchlevel/odm) -* **Postgres and MongoDB support** – Use the same ODM for both Postgres and MongoDB, with a consistent API. -* **Attribute-based Mapping** – Define documents and indexes using modern PHP attributes. -* **No Unit of Work** – Patchlevel ODM does not use a Unit of Work, giving you more control over when changes are persisted. -* **Built on Patchlevel Hydrator** – Benefit from the performance and extensibility (like [crypto shredding](https://github.com/patchlevel/hydrator/#cryptography)) of our powerful [hydrator library](https://github.com/patchlevel/hydrator/). +# Patchlevel ODM -## πŸ“¦ Installation +Patchlevel ODM is a lightweight **Object Document Mapper (ODM)** for PHP that works with **MongoDB** and **PostgreSQL** (via [patchlevel/rango](https://github.com/patchlevel/rango/)). It is built on top of our superfast **[patchlevel/hydrator](https://github.com/patchlevel/hydrator/)**, providing a simple attribute-based mapping layer and enterprise-grade features like cryptography. -You can install Patchlevel ODM using Composer. -Depending on your database choice, you will need to require the appropriate packages. +Unlike Doctrine ODM, Patchlevel ODM has **no Unit of Work**. Repositories control persistence explicitly, so every write is deliberate and easy to reason about, which makes the library a good fit for long-running worker processes. -### PostgreSQL (via [Rango](https://github.com/patchlevel/rango/)) +## Features -```bash -composer require patchlevel/odm patchlevel/rango -``` +* [MongoDB and PostgreSQL support](https://patchlevel.dev/docs/odm/latest/databases) with a single, consistent API +* [Attribute-based document mapping](https://patchlevel.dev/docs/odm/latest/documents) with `#[Document]` and `#[Id]` +* [Repositories without a Unit of Work](https://patchlevel.dev/docs/odm/latest/repository) for predictable writes +* [Querying](https://patchlevel.dev/docs/odm/latest/repository#querying) with filters, sorting and pagination +* [Indexes](https://patchlevel.dev/docs/odm/latest/documents#indexes) defined with `#[Index]`, including unique constraints +* [Field mapping and normalization](https://patchlevel.dev/docs/odm/latest/field-mapping) for nested objects and custom field names +* [Encryption and crypto shredding](https://patchlevel.dev/docs/odm/latest/encryption) for sensitive data -### MongoDB +## Installation ```bash -composer require patchlevel/odm mongodb/mongodb +composer require patchlevel/odm ``` -## πŸ›  How it Works - -Patchlevel ODM maps PHP objects to document storage. - -* **Documents** are defined using the `#[Document]` attribute. -* **Identifiers** are declared with `#[Id]`. -* **Indexes** can be defined using `#[Index]`. -* **Repositories** provide a simple API for loading and storing documents. - -Internally the ODM uses: - -* **[patchlevel/rango](https://github.com/patchlevel/rango/)** as the database abstraction layer for PostgreSQL -* **[mongodb/mongodb](https://github.com/mongodb/mongo-php-library)** as the database abstraction layer for MongoDB -* **[patchlevel/hydrator](https://github.com/patchlevel/hydrator/)** for object mapping and normalization - -## 🚦 Quick Start - -Define your documents and indexes using PHP attributes. - -```php -use Patchlevel\ODM\Attribute\Document; -use Patchlevel\ODM\Attribute\Id; -use Patchlevel\ODM\Attribute\Index; - -#[Document('profiles')] -#[Index('by_status', ['status' => 'asc'])] -final class Profile -{ - /** @param list $skills */ - public function __construct( - #[Id] - public readonly string $id, - public string $name, - public Status $status, - public array $skills, - ) { - } -} - -final readonly class Skill -{ - public function __construct( - public string $value, - ) { - } -} - -enum Status: string -{ - case ACTIVE = 'active'; - case INACTIVE = 'inactive'; -} -``` - -### Setup PostgreSQL (via [Rango](https://github.com/patchlevel/rango/)) - -```php -use Patchlevel\ODM\Repository\RangoRepositoryManager; -use Patchlevel\Rango\Client; - -$client = new Client($_ENV['POSTGRES_URI']); - -$manager = RangoRepositoryManager::create($client); -``` - -### Setup MongoDB - -```php -use MongoDB\Client; -use Patchlevel\ODM\Repository\MongoDBRepositoryManager; - -$client = new Client($_ENV['MONGODB_URI']); - -$manager = MongoDBRepositoryManager::create($client); -``` - -### Usage - -Now you can use the repository manager to access your documents. - -```php -$repository = $manager->get(Profile::class); - -$repository->insert( - new Profile('r-1', 'Rango', Status::ACTIVE, [new Skill('php')]), - new Profile('r-2', 'Foo', Status::ACTIVE, [new Skill('node'), new Skill('js')]), - new Profile('r-3', 'Bar', Status::INACTIVE, [new Skill('mongodb')]), -); - -$profiles = $repository->findBy( - filter: ['status' => Status::ACTIVE->value], - sort: ['name' => 'asc'], - limit: 10, - offset: 0 -); - -$profile = $repository->get('r-2'); -$profile->name = 'New Foo'; -$repository->update($profile); - -$repository->remove('r-3'); -``` - -## πŸ—οΈ Design differences compared to Doctrine ODM - -Doctrine ODM has a feature that we don't want: a Unit of Work (UOW). -A UOW tracks new objects and changes to existing objects. -To commit changes to the database, you need to call `flush()`. -In this step, the Unit of Work calculates the changes and applies them to the database. +## Documentation -This approach has several side effects: +* Latest [Docs](https://patchlevel.dev/docs/odm/latest) +* Related [Blog](https://patchlevel.dev/blog) -- **Growing memory usage in long-running workers** - Since the Unit of Work keeps references to managed documents, memory usage can continuously grow in worker processes - unless documents are manually detached or the UOW is cleared. +## Integration -- **Risk of unintentionally persisting changes** - Because documents are tracked automatically, changes to a document may be persisted later by a `flush()` call in a - completely different part of the codebase. +* [patchlevel/hydrator](https://github.com/patchlevel/hydrator) +* [patchlevel/rango](https://github.com/patchlevel/rango) -- **Risk of stale data** - Managed documents may become outdated if they remain in the UOW for too long, especially in long-running processes. +## Contributing -- **Complex lifecycle management** - To avoid these issues, developers often need to call `clear()` or `detach()`, which adds complexity and can easily - introduce subtle bugs. +We are open to contributions as long as they are in line with +our [BC-Policy](https://patchlevel.dev/our-backward-compatibility-promise). -Because of these trade-offs, we intentionally avoid using a Unit of Work. Instead, repositories explicitly control -persistence operations. This means every insert or update must be triggered deliberately, making database writes -predictable and easier to reason about. +Also note that the `composer.lock` is always generated with the newest supported PHP version as this is the version our tools run in the CI. diff --git a/bin/docs-extract-php-code b/bin/docs-extract-php-code new file mode 100755 index 0000000..d50b642 --- /dev/null +++ b/bin/docs-extract-php-code @@ -0,0 +1,53 @@ +#!/usr/bin/env php +addExtension(new MarkdownRendererExtension()); + +$parser = new MarkdownParser($environment); + +$targetDir = __DIR__ . '/../docs_php'; + +if (file_exists($targetDir)) { + exec('rm -rf ' . $targetDir); +} + +mkdir($targetDir); + +$finder = new Symfony\Component\Finder\Finder(); +$finder->files()->in(__DIR__ . '/../docs')->name('*.md'); + +foreach ($finder as $file) { + $fileName = pathinfo($file->getBasename(), PATHINFO_FILENAME); + + $content = file_get_contents($file->getPathname()); + $document = $parser->parse($content); + + $result = (new Query()) + ->where(Query::type(FencedCode::class)) + ->findAll($document); + + /** + * @var FencedCode $node + */ + foreach ($result as $i => $node) { + if ($node->getInfo() !== 'php') { + continue; + } + + $source = sprintf('%s:%s', $file->getRealPath(), $node->getStartLine()); + + $code = "getLiteral(); + + $targetPath = $targetDir . '/' . $fileName . '_' . $i . '.php'; + file_put_contents($targetPath, $code); + } +} diff --git a/bin/docs-inject-php-code b/bin/docs-inject-php-code new file mode 100755 index 0000000..e4e81ea --- /dev/null +++ b/bin/docs-inject-php-code @@ -0,0 +1,70 @@ +#!/usr/bin/env php +addExtension(new MarkdownRendererExtension()); + +$parser = new MarkdownParser($environment); +$markdownRenderer = new MarkdownRenderer($environment); + +$targetDir = __DIR__ . '/../docs_php'; + +if (!file_exists($targetDir)) { + exit(1); +} + +$finder = new Symfony\Component\Finder\Finder(); +$finder->files()->in(__DIR__ . '/../docs')->name('*.md'); + +foreach ($finder as $file) { + $fileName = pathinfo($file->getBasename(), PATHINFO_FILENAME); + + $content = file_get_contents($file->getPathname()); + $document = $parser->parse($content); + + $result = (new Query()) + ->where(Query::type(FencedCode::class)) + ->findAll($document); + + /** + * @var FencedCode $node + */ + foreach ($result as $i => $node) { + if ($node->getInfo() !== 'php') { + $node->setLiteral(trim($node->getLiteral())); + continue; + } + + $targetPath = $targetDir . '/' . $fileName . '_' . $i . '.php'; + + if (!file_exists($targetPath)) { + $node->setLiteral(trim($node->getLiteral())); + continue; + } + + $code = file_get_contents($targetPath); + + $lines = explode("\n", $code); + array_splice($lines, 0, 2); + $code = implode("\n", $lines); + + $node->setLiteral(trim($code)); + } + + file_put_contents($file->getPathname(), $markdownRenderer->renderDocument($document)); +} + +if (file_exists($targetDir)) { + exec('rm -rf ' . $targetDir); +} \ No newline at end of file diff --git a/composer.json b/composer.json index ee0e846..ad1689c 100644 --- a/composer.json +++ b/composer.json @@ -2,14 +2,16 @@ "name": "patchlevel/odm", "type": "library", "license": "MIT", - "description": "A simple ODM for MongoDB and Postgres", + "description": "A lightweight Object Document Mapper for PHP that runs on both MongoDB and PostgreSQL with one consistent API", "keywords": [ + "patchlevel", + "odm", "mongodb", "postgres", "rango", "hydrator" ], - "homepage": "https://github.com/patchlevel/odm", + "homepage": "https://patchlevel.dev/docs/odm/latest", "authors": [ { "name": "Daniel Badura", @@ -35,7 +37,8 @@ "phpstan/phpstan": "^2.1.46", "phpstan/phpstan-phpunit": "^2.0.16", "phpunit/phpunit": "^11.5.55", - "symfony/var-dumper": "^v7.4.8 || ^v8.0.0" + "symfony/var-dumper": "^v7.4.8 || ^v8.0.0", + "wnx/commonmark-markdown-renderer": "^1.6" }, "config": { "preferred-install": { diff --git a/composer.lock b/composer.lock index 6540086..b0b19d6 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": "62bba0fff901fc3b8200ff3cabe759a4", + "content-hash": "88c29c9b6bcb5f0d3df42fb8b427da5f", "packages": [ { "name": "patchlevel/hydrator", @@ -847,6 +847,81 @@ ], "time": "2025-11-11T04:32:07+00:00" }, + { + "name": "dflydev/dot-access-data", + "version": "v3.0.3", + "source": { + "type": "git", + "url": "https://github.com/dflydev/dflydev-dot-access-data.git", + "reference": "a23a2bf4f31d3518f3ecb38660c95715dfead60f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/dflydev/dflydev-dot-access-data/zipball/a23a2bf4f31d3518f3ecb38660c95715dfead60f", + "reference": "a23a2bf4f31d3518f3ecb38660c95715dfead60f", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^0.12.42", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.3", + "scrutinizer/ocular": "1.6.0", + "squizlabs/php_codesniffer": "^3.5", + "vimeo/psalm": "^4.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Dflydev\\DotAccessData\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Dragonfly Development Inc.", + "email": "info@dflydev.com", + "homepage": "http://dflydev.com" + }, + { + "name": "Beau Simensen", + "email": "beau@dflydev.com", + "homepage": "http://beausimensen.com" + }, + { + "name": "Carlos Frutos", + "email": "carlos@kiwing.it", + "homepage": "https://github.com/cfrutos" + }, + { + "name": "Colin O'Dell", + "email": "colinodell@gmail.com", + "homepage": "https://www.colinodell.com" + } + ], + "description": "Given a deep data structure, access data by dot notation.", + "homepage": "https://github.com/dflydev/dflydev-dot-access-data", + "keywords": [ + "access", + "data", + "dot", + "notation" + ], + "support": { + "issues": "https://github.com/dflydev/dflydev-dot-access-data/issues", + "source": "https://github.com/dflydev/dflydev-dot-access-data/tree/v3.0.3" + }, + "time": "2024-07-08T12:26:09+00:00" + }, { "name": "doctrine/annotations", "version": "2.0.2", @@ -1505,6 +1580,195 @@ }, "time": "2026-04-02T12:43:11+00:00" }, + { + "name": "league/commonmark", + "version": "2.8.2", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/commonmark.git", + "reference": "59fb075d2101740c337c7216e3f32b36c204218b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/commonmark/zipball/59fb075d2101740c337c7216e3f32b36c204218b", + "reference": "59fb075d2101740c337c7216e3f32b36c204218b", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "league/config": "^1.1.1", + "php": "^7.4 || ^8.0", + "psr/event-dispatcher": "^1.0", + "symfony/deprecation-contracts": "^2.1 || ^3.0", + "symfony/polyfill-php80": "^1.16" + }, + "require-dev": { + "cebe/markdown": "^1.0", + "commonmark/cmark": "0.31.1", + "commonmark/commonmark.js": "0.31.1", + "composer/package-versions-deprecated": "^1.8", + "embed/embed": "^4.4", + "erusev/parsedown": "^1.0", + "ext-json": "*", + "github/gfm": "0.29.0", + "michelf/php-markdown": "^1.4 || ^2.0", + "nyholm/psr7": "^1.5", + "phpstan/phpstan": "^1.8.2", + "phpunit/phpunit": "^9.5.21 || ^10.5.9 || ^11.0.0", + "scrutinizer/ocular": "^1.8.1", + "symfony/finder": "^5.3 | ^6.0 | ^7.0 || ^8.0", + "symfony/process": "^5.4 | ^6.0 | ^7.0 || ^8.0", + "symfony/yaml": "^2.3 | ^3.0 | ^4.0 | ^5.0 | ^6.0 | ^7.0 || ^8.0", + "unleashedtech/php-coding-standard": "^3.1.1", + "vimeo/psalm": "^4.24.0 || ^5.0.0 || ^6.0.0" + }, + "suggest": { + "symfony/yaml": "v2.3+ required if using the Front Matter extension" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "2.9-dev" + } + }, + "autoload": { + "psr-4": { + "League\\CommonMark\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Colin O'Dell", + "email": "colinodell@gmail.com", + "homepage": "https://www.colinodell.com", + "role": "Lead Developer" + } + ], + "description": "Highly-extensible PHP Markdown parser which fully supports the CommonMark spec and GitHub-Flavored Markdown (GFM)", + "homepage": "https://commonmark.thephpleague.com", + "keywords": [ + "commonmark", + "flavored", + "gfm", + "github", + "github-flavored", + "markdown", + "md", + "parser" + ], + "support": { + "docs": "https://commonmark.thephpleague.com/", + "forum": "https://github.com/thephpleague/commonmark/discussions", + "issues": "https://github.com/thephpleague/commonmark/issues", + "rss": "https://github.com/thephpleague/commonmark/releases.atom", + "source": "https://github.com/thephpleague/commonmark" + }, + "funding": [ + { + "url": "https://www.colinodell.com/sponsor", + "type": "custom" + }, + { + "url": "https://www.paypal.me/colinpodell/10.00", + "type": "custom" + }, + { + "url": "https://github.com/colinodell", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/league/commonmark", + "type": "tidelift" + } + ], + "time": "2026-03-19T13:16:38+00:00" + }, + { + "name": "league/config", + "version": "v1.2.0", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/config.git", + "reference": "754b3604fb2984c71f4af4a9cbe7b57f346ec1f3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/config/zipball/754b3604fb2984c71f4af4a9cbe7b57f346ec1f3", + "reference": "754b3604fb2984c71f4af4a9cbe7b57f346ec1f3", + "shasum": "" + }, + "require": { + "dflydev/dot-access-data": "^3.0.1", + "nette/schema": "^1.2", + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^1.8.2", + "phpunit/phpunit": "^9.5.5", + "scrutinizer/ocular": "^1.8.1", + "unleashedtech/php-coding-standard": "^3.1", + "vimeo/psalm": "^4.7.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.2-dev" + } + }, + "autoload": { + "psr-4": { + "League\\Config\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Colin O'Dell", + "email": "colinodell@gmail.com", + "homepage": "https://www.colinodell.com", + "role": "Lead Developer" + } + ], + "description": "Define configuration arrays with strict schemas and access values with dot notation", + "homepage": "https://config.thephpleague.com", + "keywords": [ + "array", + "config", + "configuration", + "dot", + "dot-access", + "nested", + "schema" + ], + "support": { + "docs": "https://config.thephpleague.com/", + "issues": "https://github.com/thephpleague/config/issues", + "rss": "https://github.com/thephpleague/config/releases.atom", + "source": "https://github.com/thephpleague/config" + }, + "funding": [ + { + "url": "https://www.colinodell.com/sponsor", + "type": "custom" + }, + { + "url": "https://www.paypal.me/colinpodell/10.00", + "type": "custom" + }, + { + "url": "https://github.com/colinodell", + "type": "github" + } + ], + "time": "2022-12-11T20:36:23+00:00" + }, { "name": "marc-mabe/php-enum", "version": "v4.7.2", @@ -1715,6 +1979,164 @@ ], "time": "2025-08-01T08:46:24+00:00" }, + { + "name": "nette/schema", + "version": "v1.3.5", + "source": { + "type": "git", + "url": "https://github.com/nette/schema.git", + "reference": "f0ab1a3cda782dbc5da270d28545236aa80c4002" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nette/schema/zipball/f0ab1a3cda782dbc5da270d28545236aa80c4002", + "reference": "f0ab1a3cda782dbc5da270d28545236aa80c4002", + "shasum": "" + }, + "require": { + "nette/utils": "^4.0", + "php": "8.1 - 8.5" + }, + "require-dev": { + "nette/phpstan-rules": "^1.0", + "nette/tester": "^2.6", + "phpstan/extension-installer": "^1.4@stable", + "phpstan/phpstan": "^2.1.39@stable", + "tracy/tracy": "^2.8" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.3-dev" + } + }, + "autoload": { + "psr-4": { + "Nette\\": "src" + }, + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause", + "GPL-2.0-only", + "GPL-3.0-only" + ], + "authors": [ + { + "name": "David Grudl", + "homepage": "https://davidgrudl.com" + }, + { + "name": "Nette Community", + "homepage": "https://nette.org/contributors" + } + ], + "description": "πŸ“ Nette Schema: validating data structures against a given Schema.", + "homepage": "https://nette.org", + "keywords": [ + "config", + "nette" + ], + "support": { + "issues": "https://github.com/nette/schema/issues", + "source": "https://github.com/nette/schema/tree/v1.3.5" + }, + "time": "2026-02-23T03:47:12+00:00" + }, + { + "name": "nette/utils", + "version": "v4.1.4", + "source": { + "type": "git", + "url": "https://github.com/nette/utils.git", + "reference": "7da6c396d7ebe142bc857c20479d5e70a5e1aac7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nette/utils/zipball/7da6c396d7ebe142bc857c20479d5e70a5e1aac7", + "reference": "7da6c396d7ebe142bc857c20479d5e70a5e1aac7", + "shasum": "" + }, + "require": { + "php": "8.2 - 8.5" + }, + "conflict": { + "nette/finder": "<3", + "nette/schema": "<1.2.2" + }, + "require-dev": { + "jetbrains/phpstorm-attributes": "^1.2", + "nette/phpstan-rules": "^1.0", + "nette/tester": "^2.5", + "phpstan/extension-installer": "^1.4@stable", + "phpstan/phpstan": "^2.1@stable", + "tracy/tracy": "^2.9" + }, + "suggest": { + "ext-gd": "to use Image", + "ext-iconv": "to use Strings::webalize(), toAscii(), chr() and reverse()", + "ext-intl": "to use Strings::webalize(), toAscii(), normalize() and compare()", + "ext-json": "to use Nette\\Utils\\Json", + "ext-mbstring": "to use Strings::lower() etc...", + "ext-tokenizer": "to use Nette\\Utils\\Reflection::getUseStatements()" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.1-dev" + } + }, + "autoload": { + "psr-4": { + "Nette\\": "src" + }, + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause", + "GPL-2.0-only", + "GPL-3.0-only" + ], + "authors": [ + { + "name": "David Grudl", + "homepage": "https://davidgrudl.com" + }, + { + "name": "Nette Community", + "homepage": "https://nette.org/contributors" + } + ], + "description": "πŸ›  Nette Utils: lightweight utilities for string & array manipulation, image handling, safe JSON encoding/decoding, validation, slug or strong password generating etc.", + "homepage": "https://nette.org", + "keywords": [ + "array", + "core", + "datetime", + "images", + "json", + "nette", + "paginator", + "password", + "slugify", + "string", + "unicode", + "utf-8", + "utility", + "validation" + ], + "support": { + "issues": "https://github.com/nette/utils/issues", + "source": "https://github.com/nette/utils/tree/v4.1.4" + }, + "time": "2026-05-11T20:49:54+00:00" + }, { "name": "nikic/php-parser", "version": "v5.7.0", @@ -5225,6 +5647,90 @@ ], "time": "2026-04-10T17:25:58+00:00" }, + { + "name": "symfony/polyfill-php80", + "version": "v1.37.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php80.git", + "reference": "dfb55726c3a76ea3b6459fcfda1ec2d80a682411" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/dfb55726c3a76ea3b6459fcfda1ec2d80a682411", + "reference": "dfb55726c3a76ea3b6459fcfda1ec2d80a682411", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php80\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ion Bazan", + "email": "ion.bazan@gmail.com" + }, + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php80/tree/v1.37.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "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": "2026-04-10T16:19:22+00:00" + }, { "name": "symfony/polyfill-php85", "version": "v1.37.0", @@ -5937,6 +6443,67 @@ "source": "https://github.com/webmozarts/glob/tree/4.7.0" }, "time": "2024-03-07T20:33:40+00:00" + }, + { + "name": "wnx/commonmark-markdown-renderer", + "version": "v1.6.0", + "source": { + "type": "git", + "url": "https://github.com/stefanzweifel/commonmark-markdown-renderer.git", + "reference": "3a283076abd1a1ed043940f9be43cd35470cd0d4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/stefanzweifel/commonmark-markdown-renderer/zipball/3a283076abd1a1ed043940f9be43cd35470cd0d4", + "reference": "3a283076abd1a1ed043940f9be43cd35470cd0d4", + "shasum": "" + }, + "require": { + "league/commonmark": "^2.0", + "php": "^8.1" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^3.0", + "phpstan/phpstan": "^2.0", + "phpunit/phpunit": "^10.0", + "rector/rector": "^2.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Wnx\\CommonmarkMarkdownRenderer\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Stefan Zweifel", + "email": "stefan@stefanzweifel.dev", + "role": "Developer" + } + ], + "description": "Render Markdown AST back to Markdown.", + "homepage": "https://github.com/stefanzweifel/commonmark-markdown-renderer", + "keywords": [ + "commonmark-markdown-renderer", + "markdown", + "renderer", + "wnx" + ], + "support": { + "issues": "https://github.com/stefanzweifel/commonmark-markdown-renderer/issues", + "source": "https://github.com/stefanzweifel/commonmark-markdown-renderer/tree/v1.6.0" + }, + "funding": [ + { + "url": "https://github.com/stefanzweifel", + "type": "github" + } + ], + "time": "2025-11-23T20:14:14+00:00" } ], "aliases": [], diff --git a/docs/databases.md b/docs/databases.md new file mode 100644 index 0000000..5164a14 --- /dev/null +++ b/docs/databases.md @@ -0,0 +1,92 @@ +# Databases + +Patchlevel ODM runs on two backends: MongoDB and PostgreSQL through +[Rango](https://github.com/patchlevel/rango/). You pick the backend by choosing a repository manager. +Both managers implement the same interface and hand you repositories with an identical API, so your +documents and your application code stay the same across backends. + +## PostgreSQL via Rango + +Require the Rango package and build a `RangoRepositoryManager` from a Rango client. The default +database is `public`. + +```php +use Patchlevel\ODM\Repository\RangoRepositoryManager; +use Patchlevel\Rango\Client; + +$client = new Client($_ENV['POSTGRES_URI']); + +$manager = RangoRepositoryManager::create($client); +$repository = $manager->get(Profile::class); +``` +## MongoDB + +Require `mongodb/mongodb` and build a `MongoDBRepositoryManager` from a MongoDB client. The default +database is `default`. + +```php +use MongoDB\Client; +use Patchlevel\ODM\Repository\MongoDBRepositoryManager; + +$client = new Client($_ENV['MONGODB_URI']); + +$manager = MongoDBRepositoryManager::create($client); +$repository = $manager->get(Profile::class); +``` +:::note +Rango mirrors the MongoDB query API, so the [filter operators](repository.md#querying) and +[index definitions](documents.md#indexes) you write work the same on both backends. +::: + +## Choosing the database + +A repository uses the manager's default database unless the document pins its own with the second +argument of `#[Document]`. The default database is set when constructing the manager directly. + +```php +#[Document('profiles', database: 'analytics')] +final class Profile +{ + // ... +} +``` +The `create()` factory uses the backend default (`public` or `default`). To set a different default +database, construct the manager yourself and pass the `defaultDatabase` argument. + +## Manual construction + +`create()` wires up sensible defaults, including the hydrator and the metadata factory. When you need +full control, for example to inject a custom hydrator for [encryption](encryption.md) or a shared +metadata factory, construct the manager directly. + +```php +use Patchlevel\Hydrator\StackHydrator; +use Patchlevel\ODM\Metadata\AttributeDocumentMetadataFactory; +use Patchlevel\ODM\Metadata\StackHydratorFieldMappingResolver; +use Patchlevel\ODM\Repository\RangoRepositoryManager; +use Patchlevel\Rango\Client; + +$client = new Client($_ENV['POSTGRES_URI']); +$hydrator = new StackHydrator(); + +$metadataFactory = new AttributeDocumentMetadataFactory( + new StackHydratorFieldMappingResolver($hydrator), +); + +$manager = new RangoRepositoryManager( + $client, + $metadataFactory, + $hydrator, + defaultDatabase: 'public', +); +``` +:::tip +For most applications the static `create()` factory is enough. Reach for manual construction only when +you need to customize the hydrator or share infrastructure across managers. +::: + +## Learn more + +* [How to store and load documents](repository.md) +* [How to encrypt sensitive data](encryption.md) +* [How to define documents](documents.md) diff --git a/docs/documents.md b/docs/documents.md new file mode 100644 index 0000000..9e867bb --- /dev/null +++ b/docs/documents.md @@ -0,0 +1,161 @@ +# Documents + +A document is a plain PHP class that Patchlevel ODM maps to a collection in your database. You mark +the class with the `#[Document]` attribute and one property with `#[Id]`. There is no base class to +extend and no interface to implement, so your documents stay free of framework coupling. + +## Defining a document + +The `#[Document]` attribute takes the collection name. Exactly one property must carry the `#[Id]` +attribute, which becomes the document identifier and is stored as the `_id` field. + +```php +use Patchlevel\ODM\Attribute\Document; +use Patchlevel\ODM\Attribute\Id; + +#[Document('profiles')] +final class Profile +{ + /** @param list $skills */ + public function __construct( + #[Id] + public readonly string $id, + public string $name, + public Status $status, + public array $skills, + ) { + } +} +``` +:::warning +A document needs exactly one `#[Id]` property. The library throws `NoIdPropertyFound` when none is +present and `MultipleIdPropertiesFound` when more than one property is marked. +::: + +## Identifiers + +The identifier is a string. The ODM always stores it under the reserved `_id` field, regardless of +the property name. If you rename the id property with a [field mapping](field-mapping.md) attribute, +the ODM still maps it to and from `_id` transparently. + +```php +$repository->insert(new Profile('r-1', 'Rango', Status::ACTIVE, [new Skill('php')])); + +$profile = $repository->get('r-1'); +``` +## Choosing a database + +By default a document lives in the manager's default database. You can pin a document to a specific +database with the second argument of `#[Document]`, which is useful when you spread collections +across several databases. + +```php +#[Document('profiles', database: 'analytics')] +final class Profile +{ + // ... +} +``` +:::note +The default database differs per backend (`public` for PostgreSQL, `default` for MongoDB). The +[databases](databases.md) page explains how the default is resolved. +::: + +## Property values + +Properties are mapped by the [hydrator](https://github.com/patchlevel/hydrator/). Scalars, enums, +nested objects, arrays and value objects are all supported. Complex values are normalized into a +storable representation and reconstructed on load. + +```php +enum Status: string +{ + case ACTIVE = 'active'; + case INACTIVE = 'inactive'; +} +``` +:::note +How nested objects, enums and custom field names are stored is described on the +[field mapping](field-mapping.md) page. +::: + +## Indexes + +Indexes speed up queries and can enforce uniqueness. You declare them on the document with the +`#[Index]` attribute, and the repository synchronizes them with the database on demand. The attribute +is repeatable, so a document can carry as many indexes as it needs. + +### Declaring an index + +`#[Index]` takes a name and a map of property names to a sort direction (`asc` or `desc`). The +property names are mapped to their stored [field names](field-mapping.md) automatically. + +```php +use Patchlevel\ODM\Attribute\Document; +use Patchlevel\ODM\Attribute\Id; +use Patchlevel\ODM\Attribute\Index; + +#[Document('profiles')] +#[Index('by_status', ['status' => 'asc'])] +#[Index('by_name', ['name' => 'asc'])] +final class Profile +{ + public function __construct( + #[Id] + public readonly string $id, + public string $name, + public Status $status, + ) { + } +} +``` +### Unique indexes + +Set `unique: true` to enforce that no two documents share the same value for the indexed properties. + +```php +#[Document('profiles')] +#[Index('by_email', ['email' => 'asc'], unique: true)] +final class Profile +{ + public function __construct( + #[Id] + public readonly string $id, + public string $email, + ) { + } +} +``` +:::warning +Inserting a document that violates a unique index fails with an `InsertionFailed` exception. Catch it +to detect duplicates. +::: + +### Synchronizing indexes + +Indexes are not created automatically when you insert documents. Call `updateIndexes()` to create the +declared indexes, or `createCollection()`, which creates the collection together with its indexes. + +```php +$repository->updateIndexes(); + +$repository->createCollection(); +``` +### Removing stale indexes + +By default `updateIndexes()` only adds missing indexes. Pass `true` to also drop indexes that are no +longer declared on the document. The primary key index is always preserved. + +```php +$repository->updateIndexes(dropUnknown: true); +``` +:::tip +Run index synchronization as part of a deployment or migration step rather than on every request, so +your collections stay in sync with the document definitions. +::: + +## Learn more + +* [How to store and load documents](repository.md) +* [How to control field names and normalization](field-mapping.md) +* [How to query documents efficiently](repository.md#querying) diff --git a/docs/encryption.md b/docs/encryption.md new file mode 100644 index 0000000..65cad2f --- /dev/null +++ b/docs/encryption.md @@ -0,0 +1,103 @@ +# Encryption + +Patchlevel ODM can transparently encrypt sensitive document fields using the cryptography extension +of [patchlevel/hydrator](https://github.com/patchlevel/hydrator/). Each data subject gets its own +encryption key, stored separately from the documents. Deleting a subject's key makes their encrypted +data unrecoverable, a technique known as crypto shredding that helps with data deletion requests. + +## How it works + +You mark one property as the data subject id and the sensitive properties as encrypted. On write, the +sensitive values are encrypted with the subject's key. On read, they are decrypted again. The keys +live in a separate collection, managed by a key store that ships with the ODM for each backend. + +## Marking sensitive data + +Use the `#[DataSubjectId]` attribute to identify the subject and `#[SensitiveData]` on the properties +that should be encrypted. You can provide a fallback value that is returned when the key is gone. + +```php +use Patchlevel\Hydrator\Extension\Cryptography\Attribute\DataSubjectId; +use Patchlevel\Hydrator\Extension\Cryptography\Attribute\SensitiveData; +use Patchlevel\ODM\Attribute\Document; +use Patchlevel\ODM\Attribute\Id; + +#[Document('profiles')] +final class Profile +{ + public function __construct( + #[Id] + #[DataSubjectId] + public readonly string $id, + #[SensitiveData] + public string $name, + #[SensitiveData(fallback: 'unknown')] + public string $email, + ) { + } +} +``` +:::note +The fallback is used when the subject's key has been deleted, so the document still hydrates after the +encrypted data became unreadable. +::: + +## Setting up the hydrator + +Encryption is configured on the hydrator, which you then pass to the repository manager's `create()` +factory. Build the hydrator with the `CryptographyExtension` and a key store for your backend. + +```php +use Patchlevel\Hydrator\Extension\Cryptography\BaseCryptographer; +use Patchlevel\Hydrator\Extension\Cryptography\CryptographyExtension; +use Patchlevel\Hydrator\StackHydratorBuilder; +use Patchlevel\ODM\Hydrator\RangoCipherKeyStore; +use Patchlevel\ODM\Repository\RangoRepositoryManager; +use Patchlevel\Rango\Client; + +$client = new Client($_ENV['POSTGRES_URI']); + +$keyStore = new RangoCipherKeyStore($client->selectDatabase('public')); +$cryptographer = BaseCryptographer::createWithOpenssl($keyStore); + +$hydrator = (new StackHydratorBuilder()) + ->useExtension(new CryptographyExtension($cryptographer)) + ->build(); + +$manager = RangoRepositoryManager::create($client, $hydrator); +``` +:::note +On MongoDB use `MongoDBCipherKeyStore` with a `MongoDB\Database` and `MongoDBRepositoryManager`. The +rest of the setup is identical. See the [databases](databases.md) page. +::: + +## Storing and loading + +Once configured, encryption is transparent. You store and load documents exactly as before, and the +sensitive fields are encrypted at rest. + +```php +$repository = $manager->get(Profile::class); + +$repository->insert(new Profile('r-1', 'Rango', 'rango@example.com')); + +$profile = $repository->get('r-1'); // name and email are decrypted +``` +## Crypto shredding + +To erase a subject's data, delete their key from the key store. The encrypted fields can no longer be +decrypted, and the fallback value is returned instead. + +```php +$keyStore->removeWithSubjectId('r-1'); +``` +:::danger +Removing a key is irreversible. The encrypted data stays in the document but can never be decrypted +again. +::: + +## Learn more + +* [How fields are mapped and normalized](field-mapping.md) +* [How to define documents](documents.md) +* [How to choose a database backend](databases.md) diff --git a/docs/field-mapping.md b/docs/field-mapping.md new file mode 100644 index 0000000..992e671 --- /dev/null +++ b/docs/field-mapping.md @@ -0,0 +1,157 @@ +# Field Mapping + +Patchlevel ODM maps document properties to storage through +[patchlevel/hydrator](https://github.com/patchlevel/hydrator/). Scalars, enums, nested objects and +value objects are normalized into a storable shape on write and reconstructed on read. This page +shows how the stored field names are chosen and how to customize them. + +## Default mapping + +By default a property is stored under its own name. The only exception is the `#[Id]` property, which +is always stored under the reserved `_id` field no matter what the property is called. + +```php +use Patchlevel\ODM\Attribute\Document; +use Patchlevel\ODM\Attribute\Id; + +#[Document('profiles')] +final class Profile +{ + public function __construct( + #[Id] + public readonly string $id, // stored as _id + public string $name, // stored as name + public Status $status, // stored as status + ) { + } +} +``` +## Renaming fields + +Use the hydrator's `#[NormalizedName]` attribute to store a property under a different field name. +This is useful for keeping a stable storage schema while renaming properties in code. + +```php +use Patchlevel\Hydrator\Attribute\NormalizedName; +use Patchlevel\ODM\Attribute\Document; +use Patchlevel\ODM\Attribute\Id; + +#[Document('profiles')] +final class Profile +{ + public function __construct( + #[Id] + public readonly string $id, + #[NormalizedName('_name')] + public string $name, + ) { + } +} +``` +:::note +You still filter and sort by the property name (`name`), not by the stored field (`_name`). The ODM +translates property paths to field paths for you, as described in [querying](repository.md#querying). +::: + +## Nested objects + +Nested objects are normalized recursively. Renamed fields on nested objects are respected, and you can +filter on them with dot notation. + +```php +use Patchlevel\Hydrator\Attribute\NormalizedName; + +final readonly class PersonalData +{ + public function __construct( + #[NormalizedName('_name')] + public string $name, + #[NormalizedName('_age')] + public int $age, + ) { + } +} + +#[Document('profiles')] +final class Profile +{ + public function __construct( + #[Id] + public readonly string $id, + #[NormalizedName('_personal_data')] + public PersonalData $personalData, + ) { + } +} +``` +A filter on the nested property uses the property path, which is mapped to the stored field path: + +```php +$result = iterator_to_array( + $repository->findBy(['personalData.name' => 'Rango']), + false, +); +``` +## Custom normalizers + +For value objects with their own representation, write a normalizer and attach it as an attribute. +The normalizer converts the object to a storable value and back. + +```php +use Patchlevel\Hydrator\Normalizer\InvalidType; +use Patchlevel\Hydrator\Normalizer\NormalizerWithContext; + +#[Attribute(Attribute::TARGET_CLASS)] +final class SkillNormalizer implements NormalizerWithContext +{ + /** @param array $context */ + public function normalize(mixed $value, array $context = []): mixed + { + if ($value === null) { + return null; + } + + if (!$value instanceof Skill) { + throw new InvalidType(); + } + + return $value->value; + } + + /** @param array $context */ + public function denormalize(mixed $value, array $context = []): mixed + { + if ($value === null) { + return null; + } + + if (!is_string($value)) { + throw new InvalidType(); + } + + return new Skill($value); + } +} +``` +Attach the normalizer to the value object, then use it like any other property: + +```php +#[SkillNormalizer] +final readonly class Skill +{ + public function __construct( + public string $value, + ) { + } +} +``` +:::tip +The hydrator ships normalizers for enums, dates and arrays out of the box. See the +[hydrator documentation](https://github.com/patchlevel/hydrator/) for the full list. +::: + +## Learn more + +* [How to query renamed and nested properties](repository.md#querying) +* [How to define documents](documents.md) +* [How to encrypt sensitive fields](encryption.md) diff --git a/docs/getting-started.md b/docs/getting-started.md new file mode 100644 index 0000000..d240d67 --- /dev/null +++ b/docs/getting-started.md @@ -0,0 +1,152 @@ +# Getting Started + +This guide walks you through a complete example: you define a `Profile` document, set up a +repository manager, and then insert, load, query, update and remove documents. The example uses +PostgreSQL through [Rango](databases.md), but every step works the same way on MongoDB. + +## Installation + +Install the library together with the driver for your database: + +```bash +composer require patchlevel/odm patchlevel/rango +``` +:::note +For MongoDB, require `mongodb/mongodb` instead. The [databases](databases.md) page explains both setups. +::: + +## Define a document + +A document is a plain PHP class marked with the `#[Document]` attribute. The collection name is the +first argument. One property carries the `#[Id]` attribute and becomes the document identifier. + +```php +use Patchlevel\ODM\Attribute\Document; +use Patchlevel\ODM\Attribute\Id; +use Patchlevel\ODM\Attribute\Index; + +#[Document('profiles')] +#[Index('by_status', ['status' => 'asc'])] +final class Profile +{ + /** @param list $skills */ + public function __construct( + #[Id] + public readonly string $id, + public string $name, + public Status $status, + public array $skills, + ) { + } +} +``` +The document references two small value types and an enum: + +```php +enum Status: string +{ + case ACTIVE = 'active'; + case INACTIVE = 'inactive'; +} + +#[SkillNormalizer] +final readonly class Skill +{ + public function __construct( + public string $value, + ) { + } +} +``` +:::note +The enum and the `Skill` value object are turned into scalars by the hydrator. The `#[SkillNormalizer]` +attribute is a custom normalizer. Both are explained on the [field mapping](field-mapping.md) page. +::: + +## Set up the repository manager + +The repository manager creates and caches one repository per document class. Build it with the +static `create()` factory and pass your database client. + +```php +use Patchlevel\ODM\Repository\RangoRepositoryManager; +use Patchlevel\Rango\Client; + +$client = new Client($_ENV['POSTGRES_URI']); + +$manager = RangoRepositoryManager::create($client); +$repository = $manager->get(Profile::class); +``` +:::tip +On MongoDB you use `MongoDBRepositoryManager` with a `MongoDB\Client`. The API of the resulting +repository is identical. See the [databases](databases.md) page. +::: + +## Create the collection + +Before storing documents, create the collection and its [indexes](documents.md#indexes): + +```php +$repository->createCollection(); +``` +## Insert documents + +`insert()` accepts one or many documents and writes them in a single operation. + +```php +$repository->insert( + new Profile('r-1', 'Rango', Status::ACTIVE, [new Skill('php')]), + new Profile('r-2', 'Beans', Status::ACTIVE, [new Skill('node'), new Skill('js')]), + new Profile('r-3', 'Elsa', Status::INACTIVE, [new Skill('mongodb')]), +); +``` +## Load documents + +Use `find()` to load a document by id, or `get()` if a missing document should raise an exception. + +```php +$profile = $repository->find('r-1'); // Profile|null +$profile = $repository->get('r-1'); // Profile, throws DocumentNotFound when missing +``` +## Query documents + +`findBy()` filters documents and returns an iterable. You can sort, limit and offset the result. + +```php +$profiles = iterator_to_array( + $repository->findBy( + filter: ['status' => Status::ACTIVE->value], + orderBy: ['name' => 'asc'], + limit: 10, + ), + false, +); +``` +:::note +Filters and sorting accept the document property names, even when the stored field is renamed. +The [querying](repository.md#querying) section covers operators like `$in` and `$or`. +::: + +## Update and remove + +There is no Unit of Work, so changes are persisted only when you call `update()`. Remove documents +by id with `remove()`. + +```php +$profile = $repository->get('r-2'); +$profile->name = 'New Beans'; +$repository->update($profile); + +$repository->remove('r-3'); +``` +## Result + +You now have a working document store: a `Profile` document is mapped through attributes, persisted +through a repository, and queried with filters and sorting, all without a Unit of Work. + +## Learn more + +* [How to define documents](documents.md) +* [How to use the repository](repository.md) +* [How to query documents](repository.md#querying) +* [How to encrypt sensitive data](encryption.md) diff --git a/docs/introduction.md b/docs/introduction.md new file mode 100644 index 0000000..c97c846 --- /dev/null +++ b/docs/introduction.md @@ -0,0 +1,46 @@ +# Patchlevel ODM + +Patchlevel ODM is a lightweight Object Document Mapper for PHP. It maps plain PHP objects to +document storage and runs on both MongoDB and PostgreSQL (through +[patchlevel/rango](https://github.com/patchlevel/rango/)), exposing the same API for both. +It is built on top of [patchlevel/hydrator](https://github.com/patchlevel/hydrator/), which gives +you fast attribute-based mapping and enterprise features like encryption out of the box. + +Unlike Doctrine ODM, Patchlevel ODM has no Unit of Work. Repositories control persistence +explicitly, so every write is deliberate and easy to reason about, which makes the library a good +fit for long-running worker processes. + +## Features + +* [MongoDB and PostgreSQL support](databases.md) with a single, consistent API +* [Attribute-based document mapping](documents.md) with `#[Document]` and `#[Id]` +* [Repositories without a Unit of Work](repository.md) for predictable writes +* [Querying](repository.md#querying) with filters, sorting and pagination +* [Indexes](documents.md#indexes) defined with `#[Index]`, including unique constraints +* [Field mapping and normalization](field-mapping.md) for nested objects and custom field names +* [Encryption and crypto shredding](encryption.md) for sensitive data + +## Installation + +Install the library with Composer. Depending on your database, you also need the matching driver +package. + +For PostgreSQL via [Rango](https://github.com/patchlevel/rango/): + +```bash +composer require patchlevel/odm patchlevel/rango +``` +For MongoDB: + +```bash +composer require patchlevel/odm mongodb/mongodb +``` +## Integration + +* [patchlevel/hydrator](https://github.com/patchlevel/hydrator/) powers the object mapping and normalization +* [patchlevel/rango](https://github.com/patchlevel/rango/) is the PostgreSQL document layer + +:::tip +New to the library? The [getting started](getting-started.md) guide builds a complete example from +defining a document to querying it. +::: diff --git a/docs/project.json b/docs/project.json new file mode 100644 index 0000000..eda35cc --- /dev/null +++ b/docs/project.json @@ -0,0 +1,21 @@ +{ + "navigation": [ + { "title": "Introduction", "file": "introduction.md" }, + { "title": "Getting Started", "file": "getting-started.md" }, + { + "title": "Basics", + "subEntries": [ + { "title": "Documents", "file": "documents.md" }, + { "title": "Repository", "file": "repository.md" } + ] + }, + { + "title": "Advanced", + "subEntries": [ + { "title": "Field Mapping", "file": "field-mapping.md" }, + { "title": "Encryption", "file": "encryption.md" }, + { "title": "Databases", "file": "databases.md" } + ] + } + ] +} diff --git a/docs/repository.md b/docs/repository.md new file mode 100644 index 0000000..8daa7b5 --- /dev/null +++ b/docs/repository.md @@ -0,0 +1,210 @@ +# Repository + +A repository stores and loads documents of a single type. You obtain one from the repository manager +by passing the document class. The manager creates the repository on first use and caches it, so +calling `get()` repeatedly returns the same instance. + +```php +use Patchlevel\ODM\Repository\RangoRepositoryManager; +use Patchlevel\Rango\Client; + +$client = new Client($_ENV['POSTGRES_URI']); + +$manager = RangoRepositoryManager::create($client); +$repository = $manager->get(Profile::class); +``` +:::note +The manager is backend specific. Use `MongoDBRepositoryManager` for MongoDB. The resulting +repository exposes the same methods either way. See the [databases](databases.md) page. +::: + +## No Unit of Work + +Patchlevel ODM has no Unit of Work. The repository never tracks your documents and never persists +changes on its own. A document is written only when you explicitly call `insert()` or `update()`. +This keeps memory usage flat in long-running workers and prevents changes from leaking into the +database from unrelated parts of the code. + +## Inserting + +`insert()` accepts one or many documents. A single document is written with one operation, multiple +documents are written in a batch. + +```php +$repository->insert(new Profile('r-1', 'Rango', Status::ACTIVE, [new Skill('php')])); + +$repository->insert( + new Profile('r-2', 'Beans', Status::ACTIVE, [new Skill('js')]), + new Profile('r-3', 'Elsa', Status::INACTIVE, [new Skill('go')]), +); +``` +:::warning +Inserting a document whose id already exists fails with an `InsertionFailed` exception. The same +exception is raised when a [unique index](documents.md#indexes) is violated. +::: + +## Updating + +`update()` replaces the stored fields of existing documents. Because there is no change tracking, you +pass the full document you want to persist. + +```php +$profile = $repository->get('r-1'); +$profile->name = 'Rango Updated'; + +$repository->update($profile); +``` +## Loading by id + +`find()` returns the document or `null`. `get()` returns the document or throws `DocumentNotFound` +when it does not exist, which is convenient when the document is required. + +```php +$profile = $repository->find('r-1'); // Profile|null +$profile = $repository->get('r-1'); // Profile, throws DocumentNotFound when missing +``` +## Existence and counting + +```php +$repository->has('r-1'); // bool +$repository->count(); // int, number of documents in the collection +``` +## Iterating all documents + +`findAll()` streams every document in the collection as an iterable. + +```php +foreach ($repository->findAll() as $profile) { + echo $profile->name; +} +``` +:::tip +To filter, sort or paginate instead of loading everything, use `findBy()` and `findOneBy()` from the +[querying](#querying) section below. +::: + +## Querying + +Besides loading documents by id, the repository can query a collection with filters, sorting and +pagination. Queries use the document property names, even when a property is stored under a different +field name, so you never deal with the raw storage layout. + +### Filtering + +`findBy()` takes a filter array and returns an iterable of documents. Each key is a property name and +each value is the value to match. + +```php +$active = iterator_to_array( + $repository->findBy(['status' => 'active']), + false, +); +``` +:::note +`findBy()` returns a generator, so wrap it in `iterator_to_array()` when you need an array. Pass +`false` as the second argument to reindex the result. +::: + +### Operators + +Filter values support the MongoDB-style query operators. Operator keys start with `$` and are passed +through untouched, while the property names around them are still mapped to their stored field names. + +```php +$selected = iterator_to_array( + $repository->findBy(['id' => ['$in' => ['r-1', 'r-3']]]), + false, +); + +$result = iterator_to_array( + $repository->findBy([ + '$or' => [ + ['status' => 'active'], + ['name' => 'Rango'], + ], + ]), + false, +); +``` +:::tip +The same operators work on both backends, because the PostgreSQL layer +[Rango](https://github.com/patchlevel/rango/) mirrors the MongoDB query API. +::: + +### Sorting + +Pass an `orderBy` array of property names mapped to `asc` or `desc`. + +```php +$sorted = iterator_to_array( + $repository->findBy([], orderBy: ['name' => 'asc']), + false, +); +``` +### Pagination + +Use `limit` and `offset` to page through a result set. + +```php +$page = iterator_to_array( + $repository->findBy( + filter: ['status' => 'active'], + orderBy: ['name' => 'asc'], + limit: 10, + offset: 20, + ), + false, +); +``` +### Fetching a single document + +`findOneBy()` returns the first matching document or `null`. It accepts the same filter and an +optional `orderBy`. + +```php +$profile = $repository->findOneBy(['name' => 'Beans']); + +$newest = $repository->findOneBy([], orderBy: ['name' => 'desc']); +``` +### Filtering on nested properties + +You can filter and sort on nested properties using dot notation. The path is resolved through the +[field mapping](field-mapping.md), so renamed fields are handled automatically. + +```php +$result = iterator_to_array( + $repository->findBy(['personalData.name' => 'Rango']), + false, +); +``` +:::warning +Every property in a filter or sort must exist on the document. An unknown path raises +`UnknownPropertyPath`, which lists the properties that are available at that level. +::: + +## Removing + +`remove()` deletes documents by id and accepts one or many ids. + +```php +$repository->remove('r-1'); +$repository->remove('r-2', 'r-3'); +``` +## Managing the collection + +The repository can create and drop its own collection. `createCollection()` also creates the +[indexes](documents.md#indexes) declared on the document. + +```php +$repository->createCollection(); +$repository->dropCollection(); +``` +:::warning +`dropCollection()` permanently deletes the collection and all of its documents. +::: + +## Learn more + +* [How to define indexes and unique constraints](documents.md#indexes) +* [How field names and nested objects are mapped](field-mapping.md) +* [How to wire up MongoDB or PostgreSQL](databases.md) diff --git a/tools/composer.json b/tools/composer.json new file mode 100644 index 0000000..ff61614 --- /dev/null +++ b/tools/composer.json @@ -0,0 +1,6 @@ +{ + "require": { + "php": "~8.5.0", + "roave/backward-compatibility-check": "^8.21.0" + } +} diff --git a/tools/composer.lock b/tools/composer.lock new file mode 100644 index 0000000..6992538 --- /dev/null +++ b/tools/composer.lock @@ -0,0 +1,4680 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "4705d21107ae6e4e393cde5a3787e0b1", + "packages": [ + { + "name": "beberlei/assert", + "version": "v3.3.4", + "source": { + "type": "git", + "url": "https://github.com/beberlei/assert.git", + "reference": "f193f4613c7d7fbcee2c05e4daff4061d49c040e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/beberlei/assert/zipball/f193f4613c7d7fbcee2c05e4daff4061d49c040e", + "reference": "f193f4613c7d7fbcee2c05e4daff4061d49c040e", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "ext-json": "*", + "ext-mbstring": "*", + "ext-simplexml": "*", + "php": "^7.1 || ^8.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "*", + "phpstan/phpstan": "*", + "phpunit/phpunit": ">=6.0.0", + "yoast/phpunit-polyfills": "^0.1.0" + }, + "suggest": { + "ext-intl": "Needed to allow Assertion::count(), Assertion::isCountable(), Assertion::minCount(), and Assertion::maxCount() to operate on ResourceBundles" + }, + "type": "library", + "autoload": { + "files": [ + "lib/Assert/functions.php" + ], + "psr-4": { + "Assert\\": "lib/Assert" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-2-Clause" + ], + "authors": [ + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de", + "role": "Lead Developer" + }, + { + "name": "Richard Quadling", + "email": "rquadling@gmail.com", + "role": "Collaborator" + } + ], + "description": "Thin assertion library for input validation in business models.", + "keywords": [ + "assert", + "assertion", + "validation" + ], + "support": { + "issues": "https://github.com/beberlei/assert/issues", + "source": "https://github.com/beberlei/assert/tree/v3.3.4" + }, + "time": "2026-06-10T19:47:05+00:00" + }, + { + "name": "composer/ca-bundle", + "version": "1.5.12", + "source": { + "type": "git", + "url": "https://github.com/composer/ca-bundle.git", + "reference": "00a2f4201641d5c53f7fc0195e6c8d9fcc321a78" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/ca-bundle/zipball/00a2f4201641d5c53f7fc0195e6c8d9fcc321a78", + "reference": "00a2f4201641d5c53f7fc0195e6c8d9fcc321a78", + "shasum": "" + }, + "require": { + "ext-openssl": "*", + "ext-pcre": "*", + "php": "^7.2 || ^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^1.10", + "phpunit/phpunit": "^8 || ^9", + "psr/log": "^1.0 || ^2.0 || ^3.0", + "symfony/process": "^4.0 || ^5.0 || ^6.0 || ^7.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Composer\\CaBundle\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + } + ], + "description": "Lets you find a path to the system CA bundle, and includes a fallback to the Mozilla CA bundle.", + "keywords": [ + "cabundle", + "cacert", + "certificate", + "ssl", + "tls" + ], + "support": { + "irc": "irc://irc.freenode.org/composer", + "issues": "https://github.com/composer/ca-bundle/issues", + "source": "https://github.com/composer/ca-bundle/tree/1.5.12" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + } + ], + "time": "2026-05-19T11:26:22+00:00" + }, + { + "name": "composer/class-map-generator", + "version": "1.7.3", + "source": { + "type": "git", + "url": "https://github.com/composer/class-map-generator.git", + "reference": "86d8208fc3c649a3a999daf1a63c25201be2990f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/class-map-generator/zipball/86d8208fc3c649a3a999daf1a63c25201be2990f", + "reference": "86d8208fc3c649a3a999daf1a63c25201be2990f", + "shasum": "" + }, + "require": { + "composer/pcre": "^2.1 || ^3.1", + "php": "^7.2 || ^8.0", + "symfony/finder": "^4.4 || ^5.3 || ^6 || ^7 || ^8" + }, + "require-dev": { + "phpstan/phpstan": "^1.12 || ^2", + "phpstan/phpstan-deprecation-rules": "^1 || ^2", + "phpstan/phpstan-phpunit": "^1 || ^2", + "phpstan/phpstan-strict-rules": "^1.1 || ^2", + "phpunit/phpunit": "^8", + "symfony/filesystem": "^5.4 || ^6 || ^7 || ^8" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Composer\\ClassMapGenerator\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "https://seld.be" + } + ], + "description": "Utilities to scan PHP code and generate class maps.", + "keywords": [ + "classmap" + ], + "support": { + "issues": "https://github.com/composer/class-map-generator/issues", + "source": "https://github.com/composer/class-map-generator/tree/1.7.3" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + } + ], + "time": "2026-05-05T09:17:07+00:00" + }, + { + "name": "composer/composer", + "version": "2.10.1", + "source": { + "type": "git", + "url": "https://github.com/composer/composer.git", + "reference": "4120703b9bda8795075047b40361d7ec4d2abe49" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/composer/zipball/4120703b9bda8795075047b40361d7ec4d2abe49", + "reference": "4120703b9bda8795075047b40361d7ec4d2abe49", + "shasum": "" + }, + "require": { + "composer/ca-bundle": "^1.5", + "composer/class-map-generator": "^1.4.0", + "composer/metadata-minifier": "^1.0", + "composer/pcre": "^2.3 || ^3.3", + "composer/semver": "^3.3", + "composer/spdx-licenses": "^1.5.7", + "composer/xdebug-handler": "^2.0.2 || ^3.0.3", + "ext-json": "*", + "justinrainbow/json-schema": "^6.5.1", + "php": "^7.2.5 || ^8.0", + "psr/log": "^1.0 || ^2.0 || ^3.0", + "react/promise": "^3.3", + "seld/jsonlint": "^1.4", + "seld/phar-utils": "^1.2", + "seld/signal-handler": "^2.0", + "symfony/console": "^5.4.47 || ^6.4.25 || ^7.1.10 || ^8.0", + "symfony/filesystem": "^5.4.45 || ^6.4.24 || ^7.1.10 || ^8.0", + "symfony/finder": "^5.4.45 || ^6.4.24 || ^7.1.10 || ^8.0", + "symfony/polyfill-php73": "^1.24", + "symfony/polyfill-php80": "^1.24", + "symfony/polyfill-php81": "^1.24", + "symfony/polyfill-php84": "^1.30", + "symfony/process": "^5.4.47 || ^6.4.25 || ^7.1.10 || ^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^1.11.8", + "phpstan/phpstan-deprecation-rules": "^1.2.0", + "phpstan/phpstan-phpunit": "^1.4.0", + "phpstan/phpstan-strict-rules": "^1.6.0", + "phpstan/phpstan-symfony": "^1.4.0", + "symfony/phpunit-bridge": "^6.4.25 || ^7.3.3 || ^8.0" + }, + "suggest": { + "ext-curl": "Provides HTTP support (will fallback to PHP streams if missing)", + "ext-openssl": "Enables access to repositories and packages over HTTPS", + "ext-zip": "Allows direct extraction of ZIP archives (unzip/7z binaries will be used instead if available)", + "ext-zlib": "Enables gzip for HTTP requests" + }, + "bin": [ + "bin/composer" + ], + "type": "library", + "extra": { + "phpstan": { + "includes": [ + "phpstan/rules.neon" + ] + }, + "branch-alias": { + "dev-main": "2.10-dev" + } + }, + "autoload": { + "psr-4": { + "Composer\\": "src/Composer/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nils Adermann", + "email": "naderman@naderman.de", + "homepage": "https://www.naderman.de" + }, + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "https://seld.be" + } + ], + "description": "Composer helps you declare, manage and install dependencies of PHP projects. It ensures you have the right stack everywhere.", + "homepage": "https://getcomposer.org/", + "keywords": [ + "autoload", + "dependency", + "package" + ], + "support": { + "irc": "ircs://irc.libera.chat:6697/composer", + "issues": "https://github.com/composer/composer/issues", + "security": "https://github.com/composer/composer/security/policy", + "source": "https://github.com/composer/composer/tree/2.10.1" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + } + ], + "time": "2026-06-04T08:25:59+00:00" + }, + { + "name": "composer/metadata-minifier", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/composer/metadata-minifier.git", + "reference": "c549d23829536f0d0e984aaabbf02af91f443207" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/metadata-minifier/zipball/c549d23829536f0d0e984aaabbf02af91f443207", + "reference": "c549d23829536f0d0e984aaabbf02af91f443207", + "shasum": "" + }, + "require": { + "php": "^5.3.2 || ^7.0 || ^8.0" + }, + "require-dev": { + "composer/composer": "^2", + "phpstan/phpstan": "^0.12.55", + "symfony/phpunit-bridge": "^4.2 || ^5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Composer\\MetadataMinifier\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + } + ], + "description": "Small utility library that handles metadata minification and expansion.", + "keywords": [ + "composer", + "compression" + ], + "support": { + "issues": "https://github.com/composer/metadata-minifier/issues", + "source": "https://github.com/composer/metadata-minifier/tree/1.0.0" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "type": "tidelift" + } + ], + "time": "2021-04-07T13:37:33+00:00" + }, + { + "name": "composer/pcre", + "version": "3.4.0", + "source": { + "type": "git", + "url": "https://github.com/composer/pcre.git", + "reference": "d5a341b3fb61f3001970940afb1d332968a183ed" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/pcre/zipball/d5a341b3fb61f3001970940afb1d332968a183ed", + "reference": "d5a341b3fb61f3001970940afb1d332968a183ed", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0" + }, + "conflict": { + "phpstan/phpstan": "<2.2.2" + }, + "require-dev": { + "phpstan/phpstan": "^2", + "phpstan/phpstan-deprecation-rules": "^2", + "phpstan/phpstan-strict-rules": "^2", + "phpunit/phpunit": "^9" + }, + "type": "library", + "extra": { + "phpstan": { + "includes": [ + "extension.neon" + ] + }, + "branch-alias": { + "dev-main": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Composer\\Pcre\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + } + ], + "description": "PCRE wrapping library that offers type-safe preg_* replacements.", + "keywords": [ + "PCRE", + "preg", + "regex", + "regular expression" + ], + "support": { + "issues": "https://github.com/composer/pcre/issues", + "source": "https://github.com/composer/pcre/tree/3.4.0" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + } + ], + "time": "2026-06-07T11:47:49+00:00" + }, + { + "name": "composer/semver", + "version": "3.4.4", + "source": { + "type": "git", + "url": "https://github.com/composer/semver.git", + "reference": "198166618906cb2de69b95d7d47e5fa8aa1b2b95" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/semver/zipball/198166618906cb2de69b95d7d47e5fa8aa1b2b95", + "reference": "198166618906cb2de69b95d7d47e5fa8aa1b2b95", + "shasum": "" + }, + "require": { + "php": "^5.3.2 || ^7.0 || ^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^1.11", + "symfony/phpunit-bridge": "^3 || ^7" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Composer\\Semver\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nils Adermann", + "email": "naderman@naderman.de", + "homepage": "http://www.naderman.de" + }, + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + }, + { + "name": "Rob Bast", + "email": "rob.bast@gmail.com", + "homepage": "http://robbast.nl" + } + ], + "description": "Semver library that offers utilities, version constraint parsing and validation.", + "keywords": [ + "semantic", + "semver", + "validation", + "versioning" + ], + "support": { + "irc": "ircs://irc.libera.chat:6697/composer", + "issues": "https://github.com/composer/semver/issues", + "source": "https://github.com/composer/semver/tree/3.4.4" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + } + ], + "time": "2025-08-20T19:15:30+00:00" + }, + { + "name": "composer/spdx-licenses", + "version": "1.6.0", + "source": { + "type": "git", + "url": "https://github.com/composer/spdx-licenses.git", + "reference": "5ecd0cb4177696f9fd48f1605dda81db3dee7889" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/spdx-licenses/zipball/5ecd0cb4177696f9fd48f1605dda81db3dee7889", + "reference": "5ecd0cb4177696f9fd48f1605dda81db3dee7889", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^1.11", + "symfony/phpunit-bridge": "^6.4.25 || ^7.3.3 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Composer\\Spdx\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nils Adermann", + "email": "naderman@naderman.de", + "homepage": "http://www.naderman.de" + }, + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + }, + { + "name": "Rob Bast", + "email": "rob.bast@gmail.com", + "homepage": "http://robbast.nl" + } + ], + "description": "SPDX licenses list and validation library.", + "keywords": [ + "license", + "spdx", + "validator" + ], + "support": { + "irc": "ircs://irc.libera.chat:6697/composer", + "issues": "https://github.com/composer/spdx-licenses/issues", + "source": "https://github.com/composer/spdx-licenses/tree/1.6.0" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + } + ], + "time": "2026-04-08T20:18:39+00:00" + }, + { + "name": "composer/xdebug-handler", + "version": "3.0.5", + "source": { + "type": "git", + "url": "https://github.com/composer/xdebug-handler.git", + "reference": "6c1925561632e83d60a44492e0b344cf48ab85ef" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/xdebug-handler/zipball/6c1925561632e83d60a44492e0b344cf48ab85ef", + "reference": "6c1925561632e83d60a44492e0b344cf48ab85ef", + "shasum": "" + }, + "require": { + "composer/pcre": "^1 || ^2 || ^3", + "php": "^7.2.5 || ^8.0", + "psr/log": "^1 || ^2 || ^3" + }, + "require-dev": { + "phpstan/phpstan": "^1.0", + "phpstan/phpstan-strict-rules": "^1.1", + "phpunit/phpunit": "^8.5 || ^9.6 || ^10.5" + }, + "type": "library", + "autoload": { + "psr-4": { + "Composer\\XdebugHandler\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "John Stevenson", + "email": "john-stevenson@blueyonder.co.uk" + } + ], + "description": "Restarts a process without Xdebug.", + "keywords": [ + "Xdebug", + "performance" + ], + "support": { + "irc": "ircs://irc.libera.chat:6697/composer", + "issues": "https://github.com/composer/xdebug-handler/issues", + "source": "https://github.com/composer/xdebug-handler/tree/3.0.5" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "type": "tidelift" + } + ], + "time": "2024-05-06T16:37:16+00:00" + }, + { + "name": "jetbrains/phpstorm-stubs", + "version": "v2026.1", + "source": { + "type": "git", + "url": "https://github.com/JetBrains/phpstorm-stubs", + "reference": "2cdd054c4109dfb76667c9198bf9427606354243" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/JetBrains/phpstorm-stubs/zipball/2cdd054c4109dfb76667c9198bf9427606354243", + "reference": "2cdd054c4109dfb76667c9198bf9427606354243", + "shasum": "" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^v3.86", + "nikic/php-parser": "^v5.6", + "phpdocumentor/reflection-docblock": "^5.6", + "phpunit/phpunit": "^12.3" + }, + "type": "library", + "autoload": { + "files": [ + "PhpStormStubsMap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "description": "PHP runtime & extensions header files for PhpStorm", + "homepage": "https://www.jetbrains.com/phpstorm", + "keywords": [ + "autocomplete", + "code", + "inference", + "inspection", + "jetbrains", + "phpstorm", + "stubs", + "type" + ], + "time": "2026-02-19T20:12:01+00:00" + }, + { + "name": "justinrainbow/json-schema", + "version": "6.9.0", + "source": { + "type": "git", + "url": "https://github.com/jsonrainbow/json-schema.git", + "reference": "bd1bda2ebfc8bff418565941771ea8f03c557886" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/jsonrainbow/json-schema/zipball/bd1bda2ebfc8bff418565941771ea8f03c557886", + "reference": "bd1bda2ebfc8bff418565941771ea8f03c557886", + "shasum": "" + }, + "require": { + "ext-json": "*", + "marc-mabe/php-enum": "^4.4", + "php": "^7.2 || ^8.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "3.3.0", + "json-schema/json-schema-test-suite": "^23.2", + "marc-mabe/php-enum-phpstan": "^2.0", + "phpspec/prophecy": "^1.19", + "phpstan/phpstan": "^1.12", + "phpunit/phpunit": "^8.5" + }, + "bin": [ + "bin/validate-json" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "6.x-dev" + } + }, + "autoload": { + "psr-4": { + "JsonSchema\\": "src/JsonSchema/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Bruno Prieto Reis", + "email": "bruno.p.reis@gmail.com" + }, + { + "name": "Justin Rainbow", + "email": "justin.rainbow@gmail.com" + }, + { + "name": "Igor Wiedler", + "email": "igor@wiedler.ch" + }, + { + "name": "Robert SchΓΆnthal", + "email": "seroscho@googlemail.com" + } + ], + "description": "A library to validate a json schema.", + "homepage": "https://github.com/jsonrainbow/json-schema", + "keywords": [ + "json", + "schema" + ], + "support": { + "issues": "https://github.com/jsonrainbow/json-schema/issues", + "source": "https://github.com/jsonrainbow/json-schema/tree/6.9.0" + }, + "time": "2026-06-05T14:05:24+00:00" + }, + { + "name": "marc-mabe/php-enum", + "version": "v4.7.2", + "source": { + "type": "git", + "url": "https://github.com/marc-mabe/php-enum.git", + "reference": "bb426fcdd65c60fb3638ef741e8782508fda7eef" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/marc-mabe/php-enum/zipball/bb426fcdd65c60fb3638ef741e8782508fda7eef", + "reference": "bb426fcdd65c60fb3638ef741e8782508fda7eef", + "shasum": "" + }, + "require": { + "ext-reflection": "*", + "php": "^7.1 | ^8.0" + }, + "require-dev": { + "phpbench/phpbench": "^0.16.10 || ^1.0.4", + "phpstan/phpstan": "^1.3.1", + "phpunit/phpunit": "^7.5.20 | ^8.5.22 | ^9.5.11", + "vimeo/psalm": "^4.17.0 | ^5.26.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-3.x": "3.2-dev", + "dev-master": "4.7-dev" + } + }, + "autoload": { + "psr-4": { + "MabeEnum\\": "src/" + }, + "classmap": [ + "stubs/Stringable.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Marc Bennewitz", + "email": "dev@mabe.berlin", + "homepage": "https://mabe.berlin/", + "role": "Lead" + } + ], + "description": "Simple and fast implementation of enumerations with native PHP", + "homepage": "https://github.com/marc-mabe/php-enum", + "keywords": [ + "enum", + "enum-map", + "enum-set", + "enumeration", + "enumerator", + "enummap", + "enumset", + "map", + "set", + "type", + "type-hint", + "typehint" + ], + "support": { + "issues": "https://github.com/marc-mabe/php-enum/issues", + "source": "https://github.com/marc-mabe/php-enum/tree/v4.7.2" + }, + "time": "2025-09-14T11:18:39+00:00" + }, + { + "name": "nikic/php-parser", + "version": "v5.7.0", + "source": { + "type": "git", + "url": "https://github.com/nikic/PHP-Parser.git", + "reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/dca41cd15c2ac9d055ad70dbfd011130757d1f82", + "reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "ext-json": "*", + "ext-tokenizer": "*", + "php": ">=7.4" + }, + "require-dev": { + "ircmaxell/php-yacc": "^0.0.7", + "phpunit/phpunit": "^9.0" + }, + "bin": [ + "bin/php-parse" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.x-dev" + } + }, + "autoload": { + "psr-4": { + "PhpParser\\": "lib/PhpParser" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Nikita Popov" + } + ], + "description": "A PHP parser written in PHP", + "keywords": [ + "parser", + "php" + ], + "support": { + "issues": "https://github.com/nikic/PHP-Parser/issues", + "source": "https://github.com/nikic/PHP-Parser/tree/v5.7.0" + }, + "time": "2025-12-06T11:56:16+00:00" + }, + { + "name": "nikolaposa/version", + "version": "4.2.1", + "source": { + "type": "git", + "url": "https://github.com/nikolaposa/version.git", + "reference": "2b9ee2f0b09333b6ce00bd6b63132cdf1d7a1428" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nikolaposa/version/zipball/2b9ee2f0b09333b6ce00bd6b63132cdf1d7a1428", + "reference": "2b9ee2f0b09333b6ce00bd6b63132cdf1d7a1428", + "shasum": "" + }, + "require": { + "beberlei/assert": "^3.2", + "php": "^8.1" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^3.44", + "phpstan/phpstan": "^1.10", + "phpstan/phpstan-beberlei-assert": "^1.1", + "phpstan/phpstan-phpunit": "^1.3", + "phpunit/phpunit": "^10.5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.2.x-dev" + } + }, + "autoload": { + "psr-4": { + "Version\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nikola PoΕ‘a", + "email": "posa.nikola@gmail.com", + "homepage": "https://www.nikolaposa.in.rs" + } + ], + "description": "Value Object that represents a SemVer-compliant version number.", + "homepage": "https://github.com/nikolaposa/version", + "keywords": [ + "semantic", + "semver", + "version", + "versioning" + ], + "support": { + "issues": "https://github.com/nikolaposa/version/issues", + "source": "https://github.com/nikolaposa/version/tree/4.2.1" + }, + "time": "2025-03-24T19:12:02+00:00" + }, + { + "name": "ocramius/package-versions", + "version": "2.12.0", + "source": { + "type": "git", + "url": "https://github.com/Ocramius/PackageVersions.git", + "reference": "18b02a63e837246e812cae72e211db32d7980019" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Ocramius/PackageVersions/zipball/18b02a63e837246e812cae72e211db32d7980019", + "reference": "18b02a63e837246e812cae72e211db32d7980019", + "shasum": "" + }, + "require": { + "composer-runtime-api": "^2.2.0", + "php": "~8.4.0 || ~8.5.0" + }, + "replace": { + "composer/package-versions-deprecated": "*" + }, + "require-dev": { + "composer/composer": "^2.9.8", + "doctrine/coding-standard": "^14.0.0", + "ext-zip": "^1.15.0", + "phpunit/phpunit": "^13.1.11", + "psalm/plugin-phpunit": "^0.19.7", + "roave/infection-static-analysis-plugin": "^1.44.0", + "vimeo/psalm": "^6.16.1" + }, + "type": "library", + "autoload": { + "psr-4": { + "PackageVersions\\": "src/PackageVersions" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Marco Pivetta", + "email": "ocramius@gmail.com" + } + ], + "description": "Provides efficient querying for installed package versions (no runtime IO)", + "support": { + "issues": "https://github.com/Ocramius/PackageVersions/issues", + "source": "https://github.com/Ocramius/PackageVersions/tree/2.12.0" + }, + "funding": [ + { + "url": "https://github.com/Ocramius", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/ocramius/package-versions", + "type": "tidelift" + } + ], + "time": "2026-05-21T19:52:53+00:00" + }, + { + "name": "php-standard-library/async", + "version": "6.2.1", + "source": { + "type": "git", + "url": "https://github.com/php-standard-library/async.git", + "reference": "8c0c63d3e304318a7c78846044edc1dfef398d1d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-standard-library/async/zipball/8c0c63d3e304318a7c78846044edc1dfef398d1d", + "reference": "8c0c63d3e304318a7c78846044edc1dfef398d1d", + "shasum": "" + }, + "require": { + "php": "~8.4.0 || ~8.5.0", + "php-standard-library/date-time": "^6.0", + "php-standard-library/default": "^6.0", + "php-standard-library/foundation": "^6.0", + "php-standard-library/promise": "^6.0", + "revolt/event-loop": "^1.0.8" + }, + "conflict": { + "azjezz/psl": "*" + }, + "require-dev": { + "php-standard-library/dict": "^6.0", + "php-standard-library/result": "^6.0", + "php-standard-library/str": "^6.0", + "phpunit/phpunit": "^13.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-next": "6.2.x-dev" + } + }, + "autoload": { + "files": [ + "src/Psl/bootstrap.php" + ], + "psr-4": { + "Psl\\Async\\": "src/Psl/Async/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Seifeddine Gmati", + "email": "azjezz@carthage.software" + }, + { + "name": "Contributors", + "homepage": "https://github.com/php-standard-library/php-standard-library/graphs/contributors" + } + ], + "description": "Fiber-based structured concurrency using cooperative multitasking", + "homepage": "https://php-standard-library.dev", + "keywords": [ + "Fibers", + "async", + "concurrency", + "cooperative-multitasking" + ], + "support": { + "source": "https://github.com/php-standard-library/async/tree/6.2.1" + }, + "time": "2026-05-23T20:26:52+00:00" + }, + { + "name": "php-standard-library/channel", + "version": "6.2.1", + "source": { + "type": "git", + "url": "https://github.com/php-standard-library/channel.git", + "reference": "1d90ea4e262978a583ad24c2c9a97c9daa26b6f1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-standard-library/channel/zipball/1d90ea4e262978a583ad24c2c9a97c9daa26b6f1", + "reference": "1d90ea4e262978a583ad24c2c9a97c9daa26b6f1", + "shasum": "" + }, + "require": { + "php": "~8.4.0 || ~8.5.0", + "php-standard-library/async": "^6.0", + "php-standard-library/foundation": "^6.0", + "revolt/event-loop": "^1.0.8" + }, + "conflict": { + "azjezz/psl": "*" + }, + "require-dev": { + "php-standard-library/date-time": "^6.0", + "php-standard-library/file": "^6.0", + "phpunit/phpunit": "^13.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-next": "6.2.x-dev" + } + }, + "autoload": { + "files": [ + "src/Psl/bootstrap.php" + ], + "psr-4": { + "Psl\\Channel\\": "src/Psl/Channel/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Seifeddine Gmati", + "email": "azjezz@carthage.software" + }, + { + "name": "Contributors", + "homepage": "https://github.com/php-standard-library/php-standard-library/graphs/contributors" + } + ], + "description": "Message-passing channels for async communication, inspired by Go and Rust", + "homepage": "https://php-standard-library.dev", + "keywords": [ + "async", + "channel", + "concurrency", + "message-passing" + ], + "support": { + "source": "https://github.com/php-standard-library/channel/tree/6.2.1" + }, + "time": "2026-03-28T18:39:47+00:00" + }, + { + "name": "php-standard-library/collection", + "version": "6.2.1", + "source": { + "type": "git", + "url": "https://github.com/php-standard-library/collection.git", + "reference": "0e8b757a16ccb1f68a70f4ada8565099743d50b9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-standard-library/collection/zipball/0e8b757a16ccb1f68a70f4ada8565099743d50b9", + "reference": "0e8b757a16ccb1f68a70f4ada8565099743d50b9", + "shasum": "" + }, + "require": { + "php": "~8.4.0 || ~8.5.0", + "php-standard-library/default": "^6.0", + "php-standard-library/foundation": "^6.0" + }, + "conflict": { + "azjezz/psl": "*" + }, + "require-dev": { + "php-standard-library/str": "^6.0", + "php-standard-library/vec": "^6.0", + "phpunit/phpunit": "^13.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-next": "6.2.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psl\\Collection\\": "src/Psl/Collection/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Seifeddine Gmati", + "email": "azjezz@carthage.software" + }, + { + "name": "Contributors", + "homepage": "https://github.com/php-standard-library/php-standard-library/graphs/contributors" + } + ], + "description": "Generic, object-oriented Vector, Map, and Set collections with immutable and mutable variants", + "homepage": "https://php-standard-library.dev", + "keywords": [ + "collection", + "generics", + "map", + "set", + "vector" + ], + "support": { + "source": "https://github.com/php-standard-library/collection/tree/6.2.1" + }, + "time": "2026-05-18T22:19:21+00:00" + }, + { + "name": "php-standard-library/comparison", + "version": "6.2.1", + "source": { + "type": "git", + "url": "https://github.com/php-standard-library/comparison.git", + "reference": "1884e2218c231c285b6039e9b2010f5dd956ae20" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-standard-library/comparison/zipball/1884e2218c231c285b6039e9b2010f5dd956ae20", + "reference": "1884e2218c231c285b6039e9b2010f5dd956ae20", + "shasum": "" + }, + "require": { + "php": "~8.4.0 || ~8.5.0", + "php-standard-library/default": "^6.0", + "php-standard-library/foundation": "^6.0" + }, + "conflict": { + "azjezz/psl": "*" + }, + "require-dev": { + "phpunit/phpunit": "^13.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-next": "6.2.x-dev" + } + }, + "autoload": { + "files": [ + "src/Psl/bootstrap.php" + ], + "psr-4": { + "Psl\\Comparison\\": "src/Psl/Comparison/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Seifeddine Gmati", + "email": "azjezz@carthage.software" + }, + { + "name": "Contributors", + "homepage": "https://github.com/php-standard-library/php-standard-library/graphs/contributors" + } + ], + "description": "Interfaces and functions for type-safe, consistent value comparison", + "homepage": "https://php-standard-library.dev", + "keywords": [ + "comparable", + "comparison", + "ordering" + ], + "support": { + "source": "https://github.com/php-standard-library/comparison/tree/6.2.1" + }, + "time": "2026-03-28T18:39:47+00:00" + }, + { + "name": "php-standard-library/date-time", + "version": "6.2.1", + "source": { + "type": "git", + "url": "https://github.com/php-standard-library/date-time.git", + "reference": "1e5e2b51eb2b27c2933859872e7c4a45abc5226e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-standard-library/date-time/zipball/1e5e2b51eb2b27c2933859872e7c4a45abc5226e", + "reference": "1e5e2b51eb2b27c2933859872e7c4a45abc5226e", + "shasum": "" + }, + "require": { + "ext-intl": "*", + "php": "~8.4.0 || ~8.5.0", + "php-standard-library/comparison": "^6.0", + "php-standard-library/default": "^6.0", + "php-standard-library/foundation": "^6.0", + "php-standard-library/interoperability": "^6.0", + "php-standard-library/locale": "^6.0" + }, + "conflict": { + "azjezz/psl": "*" + }, + "require-dev": { + "php-standard-library/json": "^6.0", + "php-standard-library/math": "^6.0", + "phpunit/phpunit": "^13.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-next": "6.2.x-dev" + } + }, + "autoload": { + "files": [ + "src/Psl/bootstrap.php" + ], + "psr-4": { + "Psl\\DateTime\\": "src/Psl/DateTime/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Seifeddine Gmati", + "email": "azjezz@carthage.software" + }, + { + "name": "Contributors", + "homepage": "https://github.com/php-standard-library/php-standard-library/graphs/contributors" + } + ], + "description": "Immutable, timezone-aware date and time types with Duration, Period, and Interval", + "homepage": "https://php-standard-library.dev", + "keywords": [ + "datetime", + "duration", + "immutable", + "timezone" + ], + "support": { + "source": "https://github.com/php-standard-library/date-time/tree/6.2.1" + }, + "time": "2026-05-23T20:26:52+00:00" + }, + { + "name": "php-standard-library/default", + "version": "6.2.1", + "source": { + "type": "git", + "url": "https://github.com/php-standard-library/default.git", + "reference": "89f05ec6e6a29e8c07de7b6755d14d05b06048e2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-standard-library/default/zipball/89f05ec6e6a29e8c07de7b6755d14d05b06048e2", + "reference": "89f05ec6e6a29e8c07de7b6755d14d05b06048e2", + "shasum": "" + }, + "require": { + "php": "~8.4.0 || ~8.5.0" + }, + "conflict": { + "azjezz/psl": "*" + }, + "require-dev": { + "phpunit/phpunit": "^13.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-next": "6.2.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psl\\Default\\": "src/Psl/Default/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Seifeddine Gmati", + "email": "azjezz@carthage.software" + }, + { + "name": "Contributors", + "homepage": "https://github.com/php-standard-library/php-standard-library/graphs/contributors" + } + ], + "description": "DefaultInterface for classes to provide standardized default instances", + "homepage": "https://php-standard-library.dev", + "keywords": [ + "default", + "interface" + ], + "support": { + "source": "https://github.com/php-standard-library/default/tree/6.2.1" + }, + "time": "2026-03-28T18:39:47+00:00" + }, + { + "name": "php-standard-library/dict", + "version": "6.2.1", + "source": { + "type": "git", + "url": "https://github.com/php-standard-library/dict.git", + "reference": "e06a4b7dea0f870b909e9f7d81d9dc70395f0eeb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-standard-library/dict/zipball/e06a4b7dea0f870b909e9f7d81d9dc70395f0eeb", + "reference": "e06a4b7dea0f870b909e9f7d81d9dc70395f0eeb", + "shasum": "" + }, + "require": { + "php": "~8.4.0 || ~8.5.0", + "php-standard-library/foundation": "^6.0" + }, + "conflict": { + "azjezz/psl": "*" + }, + "require-dev": { + "php-standard-library/collection": "^6.0", + "php-standard-library/iter": "^6.0", + "php-standard-library/str": "^6.0", + "php-standard-library/vec": "^6.0", + "phpunit/phpunit": "^13.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-next": "6.2.x-dev" + } + }, + "autoload": { + "files": [ + "src/Psl/bootstrap.php" + ], + "psr-4": { + "Psl\\Dict\\": "src/Psl/Dict/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Seifeddine Gmati", + "email": "azjezz@carthage.software" + }, + { + "name": "Contributors", + "homepage": "https://github.com/php-standard-library/php-standard-library/graphs/contributors" + } + ], + "description": "Functions for creating and transforming associative arrays with preserved keys", + "homepage": "https://php-standard-library.dev", + "keywords": [ + "Associative", + "Dict", + "array", + "map" + ], + "support": { + "source": "https://github.com/php-standard-library/dict/tree/6.2.1" + }, + "time": "2026-03-31T16:59:27+00:00" + }, + { + "name": "php-standard-library/either-or-both", + "version": "6.2.1", + "source": { + "type": "git", + "url": "https://github.com/php-standard-library/either-or-both.git", + "reference": "a1f4d80ee5ee616272688941390d2eae2d1948e6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-standard-library/either-or-both/zipball/a1f4d80ee5ee616272688941390d2eae2d1948e6", + "reference": "a1f4d80ee5ee616272688941390d2eae2d1948e6", + "shasum": "" + }, + "require": { + "php": "~8.4.0 || ~8.5.0", + "php-standard-library/comparison": "^6.0", + "php-standard-library/foundation": "^6.0", + "php-standard-library/option": "^6.0" + }, + "conflict": { + "azjezz/psl": "*" + }, + "require-dev": { + "php-standard-library/str": "^6.0", + "phpunit/phpunit": "^13.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-next": "6.2.x-dev" + } + }, + "autoload": { + "files": [ + "src/Psl/bootstrap.php" + ], + "psr-4": { + "Psl\\EitherOrBoth\\": "src/Psl/EitherOrBoth/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Seifeddine Gmati", + "email": "azjezz@carthage.software" + }, + { + "name": "Contributors", + "homepage": "https://github.com/php-standard-library/php-standard-library/graphs/contributors" + } + ], + "description": "Three-variant disjoint union type (Left/Right/Both) for values that may be present on either or both of two sides", + "homepage": "https://php-standard-library.dev", + "keywords": [ + "both", + "either", + "either-or-both", + "these", + "union-type" + ], + "support": { + "source": "https://github.com/php-standard-library/either-or-both/tree/6.2.1" + }, + "time": "2026-05-23T20:26:52+00:00" + }, + { + "name": "php-standard-library/env", + "version": "6.2.1", + "source": { + "type": "git", + "url": "https://github.com/php-standard-library/env.git", + "reference": "1c6d72eef53e904506daa021141a5981a30c4403" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-standard-library/env/zipball/1c6d72eef53e904506daa021141a5981a30c4403", + "reference": "1c6d72eef53e904506daa021141a5981a30c4403", + "shasum": "" + }, + "require": { + "php": "~8.4.0 || ~8.5.0", + "php-standard-library/foundation": "^6.0" + }, + "conflict": { + "azjezz/psl": "*" + }, + "require-dev": { + "php-standard-library/filesystem": "^6.0", + "phpunit/phpunit": "^13.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-next": "6.2.x-dev" + } + }, + "autoload": { + "files": [ + "src/Psl/bootstrap.php" + ], + "psr-4": { + "Psl\\Env\\": "src/Psl/Env/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Seifeddine Gmati", + "email": "azjezz@carthage.software" + }, + { + "name": "Contributors", + "homepage": "https://github.com/php-standard-library/php-standard-library/graphs/contributors" + } + ], + "description": "Functions for inspecting and modifying environment variables, working directory, and paths", + "homepage": "https://php-standard-library.dev", + "keywords": [ + "env", + "environment" + ], + "support": { + "source": "https://github.com/php-standard-library/env/tree/6.2.1" + }, + "time": "2026-03-31T16:59:27+00:00" + }, + { + "name": "php-standard-library/file", + "version": "6.2.1", + "source": { + "type": "git", + "url": "https://github.com/php-standard-library/file.git", + "reference": "64c064f8a599cb6621e7b9329a4f1b7c5bc04152" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-standard-library/file/zipball/64c064f8a599cb6621e7b9329a4f1b7c5bc04152", + "reference": "64c064f8a599cb6621e7b9329a4f1b7c5bc04152", + "shasum": "" + }, + "require": { + "php": "~8.4.0 || ~8.5.0", + "php-standard-library/async": "^6.0", + "php-standard-library/date-time": "^6.0", + "php-standard-library/foundation": "^6.0", + "php-standard-library/io": "^6.0" + }, + "conflict": { + "azjezz/psl": "*" + }, + "require-dev": { + "php-standard-library/env": "^6.0", + "php-standard-library/filesystem": "^6.0", + "php-standard-library/os": "^6.0", + "php-standard-library/str": "^6.0", + "phpunit/phpunit": "^13.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-next": "6.2.x-dev" + } + }, + "autoload": { + "files": [ + "src/Psl/bootstrap.php" + ], + "psr-4": { + "Psl\\File\\": "src/Psl/File/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Seifeddine Gmati", + "email": "azjezz@carthage.software" + }, + { + "name": "Contributors", + "homepage": "https://github.com/php-standard-library/php-standard-library/graphs/contributors" + } + ], + "description": "Typed file handles for reading and writing with write modes and advisory locking", + "homepage": "https://php-standard-library.dev", + "keywords": [ + "file", + "handle", + "io" + ], + "support": { + "source": "https://github.com/php-standard-library/file/tree/6.2.1" + }, + "time": "2026-04-06T03:33:20+00:00" + }, + { + "name": "php-standard-library/filesystem", + "version": "6.2.1", + "source": { + "type": "git", + "url": "https://github.com/php-standard-library/filesystem.git", + "reference": "d1e87eaee4d8180842f38d010747ee5d963a8d5b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-standard-library/filesystem/zipball/d1e87eaee4d8180842f38d010747ee5d963a8d5b", + "reference": "d1e87eaee4d8180842f38d010747ee5d963a8d5b", + "shasum": "" + }, + "require": { + "php": "~8.4.0 || ~8.5.0", + "php-standard-library/foundation": "^6.0" + }, + "conflict": { + "azjezz/psl": "*" + }, + "require-dev": { + "php-standard-library/env": "^6.0", + "php-standard-library/file": "^6.0", + "php-standard-library/os": "^6.0", + "php-standard-library/str": "^6.0", + "php-standard-library/type": "^6.0", + "php-standard-library/vec": "^6.0", + "phpunit/phpunit": "^13.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-next": "6.2.x-dev" + } + }, + "autoload": { + "files": [ + "src/Psl/bootstrap.php" + ], + "psr-4": { + "Psl\\Filesystem\\": "src/Psl/Filesystem/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Seifeddine Gmati", + "email": "azjezz@carthage.software" + }, + { + "name": "Contributors", + "homepage": "https://github.com/php-standard-library/php-standard-library/graphs/contributors" + } + ], + "description": "Type-safe functions for file system operations with proper exception handling", + "homepage": "https://php-standard-library.dev", + "keywords": [ + "file-system", + "filesystem" + ], + "support": { + "source": "https://github.com/php-standard-library/filesystem/tree/6.2.1" + }, + "time": "2026-03-31T16:59:27+00:00" + }, + { + "name": "php-standard-library/foundation", + "version": "6.2.1", + "source": { + "type": "git", + "url": "https://github.com/php-standard-library/foundation.git", + "reference": "7f65cec48c8ed3d53dc8dd7643796e2ba6de6008" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-standard-library/foundation/zipball/7f65cec48c8ed3d53dc8dd7643796e2ba6de6008", + "reference": "7f65cec48c8ed3d53dc8dd7643796e2ba6de6008", + "shasum": "" + }, + "require": { + "php": "~8.4.0 || ~8.5.0" + }, + "conflict": { + "azjezz/psl": "*" + }, + "require-dev": { + "phpunit/phpunit": "^13.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-next": "6.2.x-dev" + } + }, + "autoload": { + "files": [ + "src/Psl/bootstrap.php" + ], + "psr-4": { + "Psl\\": "src/Psl/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Seifeddine Gmati", + "email": "azjezz@carthage.software" + }, + { + "name": "Contributors", + "homepage": "https://github.com/php-standard-library/php-standard-library/graphs/contributors" + } + ], + "description": "Exceptions, Ref, and invariant functions", + "homepage": "https://php-standard-library.dev", + "keywords": [ + "exceptions", + "foundation", + "invariant" + ], + "support": { + "source": "https://github.com/php-standard-library/foundation/tree/6.2.1" + }, + "time": "2026-04-28T06:28:49+00:00" + }, + { + "name": "php-standard-library/interoperability", + "version": "6.2.1", + "source": { + "type": "git", + "url": "https://github.com/php-standard-library/interoperability.git", + "reference": "09ae66cc3e6463538c3a4592bb8762cf60113821" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-standard-library/interoperability/zipball/09ae66cc3e6463538c3a4592bb8762cf60113821", + "reference": "09ae66cc3e6463538c3a4592bb8762cf60113821", + "shasum": "" + }, + "require": { + "php": "~8.4.0 || ~8.5.0" + }, + "conflict": { + "azjezz/psl": "*" + }, + "require-dev": { + "phpunit/phpunit": "^13.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-next": "6.2.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psl\\Interoperability\\": "src/Psl/Interoperability/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Seifeddine Gmati", + "email": "azjezz@carthage.software" + }, + { + "name": "Contributors", + "homepage": "https://github.com/php-standard-library/php-standard-library/graphs/contributors" + } + ], + "description": "Interfaces for converting between PSL types and PHP stdlib/intl equivalents", + "homepage": "https://php-standard-library.dev", + "keywords": [ + "conversion", + "interoperability" + ], + "support": { + "source": "https://github.com/php-standard-library/interoperability/tree/6.2.1" + }, + "time": "2026-03-28T18:39:47+00:00" + }, + { + "name": "php-standard-library/io", + "version": "6.2.1", + "source": { + "type": "git", + "url": "https://github.com/php-standard-library/io.git", + "reference": "0b61f6b5e0a392c52d3cb2b386d8bba379b91f3c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-standard-library/io/zipball/0b61f6b5e0a392c52d3cb2b386d8bba379b91f3c", + "reference": "0b61f6b5e0a392c52d3cb2b386d8bba379b91f3c", + "shasum": "" + }, + "require": { + "php": "~8.4.0 || ~8.5.0", + "php-standard-library/async": "^6.0", + "php-standard-library/channel": "^6.0", + "php-standard-library/foundation": "^6.0", + "php-standard-library/result": "^6.0", + "revolt/event-loop": "^1.0.8" + }, + "conflict": { + "azjezz/psl": "*" + }, + "require-dev": { + "php-standard-library/date-time": "^6.0", + "php-standard-library/os": "^6.0", + "php-standard-library/str": "^6.0", + "phpunit/phpunit": "^13.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-next": "6.2.x-dev" + } + }, + "autoload": { + "files": [ + "src/Psl/bootstrap.php" + ], + "psr-4": { + "Psl\\IO\\": "src/Psl/IO/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Seifeddine Gmati", + "email": "azjezz@carthage.software" + }, + { + "name": "Contributors", + "homepage": "https://github.com/php-standard-library/php-standard-library/graphs/contributors" + } + ], + "description": "Handle-based I/O abstractions - composable, testable, and async-ready", + "homepage": "https://php-standard-library.dev", + "keywords": [ + "async", + "handle", + "io", + "stream" + ], + "support": { + "source": "https://github.com/php-standard-library/io/tree/6.2.1" + }, + "time": "2026-04-28T03:11:45+00:00" + }, + { + "name": "php-standard-library/iter", + "version": "6.2.1", + "source": { + "type": "git", + "url": "https://github.com/php-standard-library/iter.git", + "reference": "d3f2db3adec4cbe5129f3428969ae8508a54aeb7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-standard-library/iter/zipball/d3f2db3adec4cbe5129f3428969ae8508a54aeb7", + "reference": "d3f2db3adec4cbe5129f3428969ae8508a54aeb7", + "shasum": "" + }, + "require": { + "php": "~8.4.0 || ~8.5.0", + "php-standard-library/comparison": "^6.0", + "php-standard-library/either-or-both": "^6.0", + "php-standard-library/foundation": "^6.0", + "php-standard-library/option": "^6.0" + }, + "conflict": { + "azjezz/psl": "*" + }, + "require-dev": { + "php-standard-library/collection": "^6.0", + "php-standard-library/dict": "^6.0", + "php-standard-library/math": "^6.0", + "php-standard-library/vec": "^6.0", + "phpunit/phpunit": "^13.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-next": "6.2.x-dev" + } + }, + "autoload": { + "files": [ + "src/Psl/bootstrap.php" + ], + "psr-4": { + "Psl\\Iter\\": "src/Psl/Iter/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Seifeddine Gmati", + "email": "azjezz@carthage.software" + }, + { + "name": "Contributors", + "homepage": "https://github.com/php-standard-library/php-standard-library/graphs/contributors" + } + ], + "description": "Utility functions for inspecting and reducing iterables - arrays, generators, and iterators", + "homepage": "https://php-standard-library.dev", + "keywords": [ + "generator", + "iter", + "iterable", + "iterator" + ], + "support": { + "source": "https://github.com/php-standard-library/iter/tree/6.2.1" + }, + "time": "2026-04-28T06:28:49+00:00" + }, + { + "name": "php-standard-library/json", + "version": "6.2.1", + "source": { + "type": "git", + "url": "https://github.com/php-standard-library/json.git", + "reference": "03e00062efdd704d874012b5412340f3003a043d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-standard-library/json/zipball/03e00062efdd704d874012b5412340f3003a043d", + "reference": "03e00062efdd704d874012b5412340f3003a043d", + "shasum": "" + }, + "require": { + "ext-json": "*", + "php": "~8.4.0 || ~8.5.0", + "php-standard-library/foundation": "^6.0", + "php-standard-library/type": "^6.0" + }, + "conflict": { + "azjezz/psl": "*" + }, + "require-dev": { + "php-standard-library/collection": "^6.0", + "php-standard-library/math": "^6.0", + "php-standard-library/str": "^6.0", + "phpunit/phpunit": "^13.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-next": "6.2.x-dev" + } + }, + "autoload": { + "files": [ + "src/Psl/bootstrap.php" + ], + "psr-4": { + "Psl\\Json\\": "src/Psl/Json/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Seifeddine Gmati", + "email": "azjezz@carthage.software" + }, + { + "name": "Contributors", + "homepage": "https://github.com/php-standard-library/php-standard-library/graphs/contributors" + } + ], + "description": "JSON encoding and decoding with typed exceptions and sensible defaults", + "homepage": "https://php-standard-library.dev", + "keywords": [ + "decoding", + "encoding", + "json" + ], + "support": { + "source": "https://github.com/php-standard-library/json/tree/6.2.1" + }, + "time": "2026-03-28T18:39:47+00:00" + }, + { + "name": "php-standard-library/locale", + "version": "6.2.1", + "source": { + "type": "git", + "url": "https://github.com/php-standard-library/locale.git", + "reference": "0771cfb19757e8923c66b7f46ab90280775caecd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-standard-library/locale/zipball/0771cfb19757e8923c66b7f46ab90280775caecd", + "reference": "0771cfb19757e8923c66b7f46ab90280775caecd", + "shasum": "" + }, + "require": { + "ext-intl": "*", + "php": "~8.4.0 || ~8.5.0" + }, + "conflict": { + "azjezz/psl": "*" + }, + "require-dev": { + "php-standard-library/str": "^6.0", + "phpunit/phpunit": "^13.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-next": "6.2.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psl\\Locale\\": "src/Psl/Locale/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Seifeddine Gmati", + "email": "azjezz@carthage.software" + }, + { + "name": "Contributors", + "homepage": "https://github.com/php-standard-library/php-standard-library/graphs/contributors" + } + ], + "description": "Backed enum with 700+ locale identifiers for type-safe internationalization", + "homepage": "https://php-standard-library.dev", + "keywords": [ + "i18n", + "internationalization", + "locale" + ], + "support": { + "source": "https://github.com/php-standard-library/locale/tree/6.2.1" + }, + "time": "2026-03-28T18:39:47+00:00" + }, + { + "name": "php-standard-library/option", + "version": "6.2.1", + "source": { + "type": "git", + "url": "https://github.com/php-standard-library/option.git", + "reference": "afe2059dbb3a9e2e3ba6edd371ccbb7d87355a66" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-standard-library/option/zipball/afe2059dbb3a9e2e3ba6edd371ccbb7d87355a66", + "reference": "afe2059dbb3a9e2e3ba6edd371ccbb7d87355a66", + "shasum": "" + }, + "require": { + "php": "~8.4.0 || ~8.5.0", + "php-standard-library/comparison": "^6.0", + "php-standard-library/foundation": "^6.0" + }, + "conflict": { + "azjezz/psl": "*" + }, + "require-dev": { + "phpunit/phpunit": "^13.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-next": "6.2.x-dev" + } + }, + "autoload": { + "files": [ + "src/Psl/bootstrap.php" + ], + "psr-4": { + "Psl\\Option\\": "src/Psl/Option/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Seifeddine Gmati", + "email": "azjezz@carthage.software" + }, + { + "name": "Contributors", + "homepage": "https://github.com/php-standard-library/php-standard-library/graphs/contributors" + } + ], + "description": "Option type (Some/None) replacing nullable types with explicit presence semantics", + "homepage": "https://php-standard-library.dev", + "keywords": [ + "monad", + "none", + "option", + "some" + ], + "support": { + "source": "https://github.com/php-standard-library/option/tree/6.2.1" + }, + "time": "2026-05-23T20:26:52+00:00" + }, + { + "name": "php-standard-library/process", + "version": "6.2.1", + "source": { + "type": "git", + "url": "https://github.com/php-standard-library/process.git", + "reference": "ab41e8b8de07289971f447ce289ec1effde321f3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-standard-library/process/zipball/ab41e8b8de07289971f447ce289ec1effde321f3", + "reference": "ab41e8b8de07289971f447ce289ec1effde321f3", + "shasum": "" + }, + "require": { + "php": "~8.4.0 || ~8.5.0", + "php-standard-library/async": "^6.0", + "php-standard-library/date-time": "^6.0", + "php-standard-library/foundation": "^6.0", + "php-standard-library/io": "^6.0" + }, + "conflict": { + "azjezz/psl": "*" + }, + "require-dev": { + "php-standard-library/env": "^6.0", + "php-standard-library/filesystem": "^6.0", + "php-standard-library/os": "^6.0", + "phpunit/phpunit": "^13.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-next": "6.2.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psl\\Process\\": "src/Psl/Process/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Seifeddine Gmati", + "email": "azjezz@carthage.software" + }, + { + "name": "Contributors", + "homepage": "https://github.com/php-standard-library/php-standard-library/graphs/contributors" + } + ], + "description": "Typed, non-blocking API for spawning and managing child processes", + "homepage": "https://php-standard-library.dev", + "keywords": [ + "async", + "process", + "subprocess" + ], + "support": { + "source": "https://github.com/php-standard-library/process/tree/6.2.1" + }, + "time": "2026-05-23T20:46:16+00:00" + }, + { + "name": "php-standard-library/promise", + "version": "6.2.1", + "source": { + "type": "git", + "url": "https://github.com/php-standard-library/promise.git", + "reference": "a38a2694e06609874e856e68951bca24ad00009c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-standard-library/promise/zipball/a38a2694e06609874e856e68951bca24ad00009c", + "reference": "a38a2694e06609874e856e68951bca24ad00009c", + "shasum": "" + }, + "require": { + "php": "~8.4.0 || ~8.5.0" + }, + "conflict": { + "azjezz/psl": "*" + }, + "require-dev": { + "phpunit/phpunit": "^13.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-next": "6.2.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psl\\Promise\\": "src/Psl/Promise/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Seifeddine Gmati", + "email": "azjezz@carthage.software" + }, + { + "name": "Contributors", + "homepage": "https://github.com/php-standard-library/php-standard-library/graphs/contributors" + } + ], + "description": "Promise interface for deferred computations - resolved or rejected", + "homepage": "https://php-standard-library.dev", + "keywords": [ + "async", + "deferred", + "promise" + ], + "support": { + "source": "https://github.com/php-standard-library/promise/tree/6.2.1" + }, + "time": "2026-04-26T16:26:10+00:00" + }, + { + "name": "php-standard-library/range", + "version": "6.2.1", + "source": { + "type": "git", + "url": "https://github.com/php-standard-library/range.git", + "reference": "34befd85fa3e048972aabac7cdf6a86e2f4b833f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-standard-library/range/zipball/34befd85fa3e048972aabac7cdf6a86e2f4b833f", + "reference": "34befd85fa3e048972aabac7cdf6a86e2f4b833f", + "shasum": "" + }, + "require": { + "php": "~8.4.0 || ~8.5.0", + "php-standard-library/foundation": "^6.0", + "php-standard-library/iter": "^6.0" + }, + "conflict": { + "azjezz/psl": "*" + }, + "require-dev": { + "php-standard-library/math": "^6.0", + "phpunit/phpunit": "^13.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-next": "6.2.x-dev" + } + }, + "autoload": { + "files": [ + "src/Psl/bootstrap.php" + ], + "psr-4": { + "Psl\\Range\\": "src/Psl/Range/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Seifeddine Gmati", + "email": "azjezz@carthage.software" + }, + { + "name": "Contributors", + "homepage": "https://github.com/php-standard-library/php-standard-library/graphs/contributors" + } + ], + "description": "Range types for integer sequences with iteration support", + "homepage": "https://php-standard-library.dev", + "keywords": [ + "iterator", + "range" + ], + "support": { + "source": "https://github.com/php-standard-library/range/tree/6.2.1" + }, + "time": "2026-03-28T18:39:47+00:00" + }, + { + "name": "php-standard-library/regex", + "version": "6.2.1", + "source": { + "type": "git", + "url": "https://github.com/php-standard-library/regex.git", + "reference": "150533c75ee7aaf83a1254837690603b22bc0191" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-standard-library/regex/zipball/150533c75ee7aaf83a1254837690603b22bc0191", + "reference": "150533c75ee7aaf83a1254837690603b22bc0191", + "shasum": "" + }, + "require": { + "php": "~8.4.0 || ~8.5.0", + "php-standard-library/foundation": "^6.0", + "php-standard-library/type": "^6.0" + }, + "conflict": { + "azjezz/psl": "*" + }, + "require-dev": { + "phpunit/phpunit": "^13.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-next": "6.2.x-dev" + } + }, + "autoload": { + "files": [ + "src/Psl/bootstrap.php" + ], + "psr-4": { + "Psl\\Regex\\": "src/Psl/Regex/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Seifeddine Gmati", + "email": "azjezz@carthage.software" + }, + { + "name": "Contributors", + "homepage": "https://github.com/php-standard-library/php-standard-library/graphs/contributors" + } + ], + "description": "Type-safe regular expressions with typed capture groups and proper error handling", + "homepage": "https://php-standard-library.dev", + "keywords": [ + "preg", + "regex", + "regular-expression" + ], + "support": { + "source": "https://github.com/php-standard-library/regex/tree/6.2.1" + }, + "time": "2026-05-23T20:26:52+00:00" + }, + { + "name": "php-standard-library/result", + "version": "6.2.1", + "source": { + "type": "git", + "url": "https://github.com/php-standard-library/result.git", + "reference": "242bbf872662f5091a37d7db9d45f119225a01f5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-standard-library/result/zipball/242bbf872662f5091a37d7db9d45f119225a01f5", + "reference": "242bbf872662f5091a37d7db9d45f119225a01f5", + "shasum": "" + }, + "require": { + "php": "~8.4.0 || ~8.5.0", + "php-standard-library/foundation": "^6.0", + "php-standard-library/promise": "^6.0" + }, + "conflict": { + "azjezz/psl": "*" + }, + "require-dev": { + "php-standard-library/fun": "^6.0", + "phpunit/phpunit": "^13.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-next": "6.2.x-dev" + } + }, + "autoload": { + "files": [ + "src/Psl/bootstrap.php" + ], + "psr-4": { + "Psl\\Result\\": "src/Psl/Result/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Seifeddine Gmati", + "email": "azjezz@carthage.software" + }, + { + "name": "Contributors", + "homepage": "https://github.com/php-standard-library/php-standard-library/graphs/contributors" + } + ], + "description": "Result type capturing success or failure as a value for controlled error handling", + "homepage": "https://php-standard-library.dev", + "keywords": [ + "error-handling", + "monad", + "result" + ], + "support": { + "source": "https://github.com/php-standard-library/result/tree/6.2.1" + }, + "time": "2026-04-26T16:26:10+00:00" + }, + { + "name": "php-standard-library/shell", + "version": "6.2.1", + "source": { + "type": "git", + "url": "https://github.com/php-standard-library/shell.git", + "reference": "6d57e1192d0ecce76e8c33e7507505b03c1bc539" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-standard-library/shell/zipball/6d57e1192d0ecce76e8c33e7507505b03c1bc539", + "reference": "6d57e1192d0ecce76e8c33e7507505b03c1bc539", + "shasum": "" + }, + "require": { + "php": "~8.4.0 || ~8.5.0", + "php-standard-library/async": "^6.0", + "php-standard-library/default": "^6.0", + "php-standard-library/foundation": "^6.0", + "php-standard-library/process": "^6.0" + }, + "conflict": { + "azjezz/psl": "*" + }, + "require-dev": { + "php-standard-library/date-time": "^6.0", + "php-standard-library/env": "^6.0", + "php-standard-library/os": "^6.0", + "php-standard-library/secure-random": "^6.0", + "php-standard-library/str": "^6.0", + "phpunit/phpunit": "^13.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-next": "6.2.x-dev" + } + }, + "autoload": { + "files": [ + "src/Psl/bootstrap.php" + ], + "psr-4": { + "Psl\\Shell\\": "src/Psl/Shell/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Seifeddine Gmati", + "email": "azjezz@carthage.software" + }, + { + "name": "Contributors", + "homepage": "https://github.com/php-standard-library/php-standard-library/graphs/contributors" + } + ], + "description": "Shell command execution with argument escaping and error output management", + "homepage": "https://php-standard-library.dev", + "keywords": [ + "command", + "execute", + "shell" + ], + "support": { + "source": "https://github.com/php-standard-library/shell/tree/6.2.1" + }, + "time": "2026-05-23T20:26:52+00:00" + }, + { + "name": "php-standard-library/str", + "version": "6.2.1", + "source": { + "type": "git", + "url": "https://github.com/php-standard-library/str.git", + "reference": "e88e7c79e1c07227aa3c50005a3a91e88425e56a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-standard-library/str/zipball/e88e7c79e1c07227aa3c50005a3a91e88425e56a", + "reference": "e88e7c79e1c07227aa3c50005a3a91e88425e56a", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "php": "~8.4.0 || ~8.5.0", + "php-standard-library/default": "^6.0", + "php-standard-library/foundation": "^6.0", + "php-standard-library/range": "^6.0" + }, + "conflict": { + "azjezz/psl": "*" + }, + "require-dev": { + "phpunit/phpunit": "^13.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-next": "6.2.x-dev" + } + }, + "autoload": { + "files": [ + "src/Psl/bootstrap.php" + ], + "psr-4": { + "Psl\\Str\\": "src/Psl/Str/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Seifeddine Gmati", + "email": "azjezz@carthage.software" + }, + { + "name": "Contributors", + "homepage": "https://github.com/php-standard-library/php-standard-library/graphs/contributors" + } + ], + "description": "Unicode-aware string functions replacing PHP mb_* and standard string functions", + "homepage": "https://php-standard-library.dev", + "keywords": [ + "mbstring", + "string", + "unicode", + "utf8" + ], + "support": { + "source": "https://github.com/php-standard-library/str/tree/6.2.1" + }, + "time": "2026-04-29T06:23:35+00:00" + }, + { + "name": "php-standard-library/type", + "version": "6.2.1", + "source": { + "type": "git", + "url": "https://github.com/php-standard-library/type.git", + "reference": "ffc050e32bc5def0e03aaae3d52c78dfc746e143" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-standard-library/type/zipball/ffc050e32bc5def0e03aaae3d52c78dfc746e143", + "reference": "ffc050e32bc5def0e03aaae3d52c78dfc746e143", + "shasum": "" + }, + "require": { + "ext-json": "*", + "php": "~8.4.0 || ~8.5.0", + "php-standard-library/collection": "^6.0", + "php-standard-library/foundation": "^6.0", + "php-standard-library/iter": "^6.0" + }, + "conflict": { + "azjezz/psl": "*" + }, + "require-dev": { + "php-standard-library/dict": "^6.0", + "php-standard-library/math": "^6.0", + "php-standard-library/result": "^6.0", + "php-standard-library/str": "^6.0", + "php-standard-library/vec": "^6.0", + "phpunit/phpunit": "^13.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-next": "6.2.x-dev" + } + }, + "autoload": { + "files": [ + "src/Psl/bootstrap.php" + ], + "psr-4": { + "Psl\\Type\\": "src/Psl/Type/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Seifeddine Gmati", + "email": "azjezz@carthage.software" + }, + { + "name": "Contributors", + "homepage": "https://github.com/php-standard-library/php-standard-library/graphs/contributors" + } + ], + "description": "Runtime type validation implementing Parse, Don't Validate - coerce and assert unstructured input into well-typed data", + "homepage": "https://php-standard-library.dev", + "keywords": [ + "coercion", + "type", + "type-safety", + "validation" + ], + "support": { + "source": "https://github.com/php-standard-library/type/tree/6.2.1" + }, + "time": "2026-05-23T20:26:52+00:00" + }, + { + "name": "php-standard-library/vec", + "version": "6.2.1", + "source": { + "type": "git", + "url": "https://github.com/php-standard-library/vec.git", + "reference": "ac119fa952177c10eeb3d5d6bca04130491c7f2e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-standard-library/vec/zipball/ac119fa952177c10eeb3d5d6bca04130491c7f2e", + "reference": "ac119fa952177c10eeb3d5d6bca04130491c7f2e", + "shasum": "" + }, + "require": { + "php": "~8.4.0 || ~8.5.0", + "php-standard-library/foundation": "^6.0" + }, + "conflict": { + "azjezz/psl": "*" + }, + "require-dev": { + "php-standard-library/collection": "^6.0", + "php-standard-library/fun": "^6.0", + "php-standard-library/iter": "^6.0", + "php-standard-library/str": "^6.0", + "phpunit/phpunit": "^13.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-next": "6.2.x-dev" + } + }, + "autoload": { + "files": [ + "src/Psl/bootstrap.php" + ], + "psr-4": { + "Psl\\Vec\\": "src/Psl/Vec/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Seifeddine Gmati", + "email": "azjezz@carthage.software" + }, + { + "name": "Contributors", + "homepage": "https://github.com/php-standard-library/php-standard-library/graphs/contributors" + } + ], + "description": "Functions for creating and transforming sequential, 0-indexed arrays (lists)", + "homepage": "https://php-standard-library.dev", + "keywords": [ + "array", + "list", + "vec", + "vector" + ], + "support": { + "source": "https://github.com/php-standard-library/vec/tree/6.2.1" + }, + "time": "2026-03-28T18:39:47+00:00" + }, + { + "name": "psr/container", + "version": "2.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/container.git", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/container/zipball/c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "shasum": "" + }, + "require": { + "php": ">=7.4.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Container\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common Container Interface (PHP FIG PSR-11)", + "homepage": "https://github.com/php-fig/container", + "keywords": [ + "PSR-11", + "container", + "container-interface", + "container-interop", + "psr" + ], + "support": { + "issues": "https://github.com/php-fig/container/issues", + "source": "https://github.com/php-fig/container/tree/2.0.2" + }, + "time": "2021-11-05T16:47:00+00:00" + }, + { + "name": "psr/log", + "version": "3.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/log.git", + "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/log/zipball/f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", + "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", + "shasum": "" + }, + "require": { + "php": ">=8.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Log\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for logging libraries", + "homepage": "https://github.com/php-fig/log", + "keywords": [ + "log", + "psr", + "psr-3" + ], + "support": { + "source": "https://github.com/php-fig/log/tree/3.0.2" + }, + "time": "2024-09-11T13:17:53+00:00" + }, + { + "name": "react/promise", + "version": "v3.3.0", + "source": { + "type": "git", + "url": "https://github.com/reactphp/promise.git", + "reference": "23444f53a813a3296c1368bb104793ce8d88f04a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/promise/zipball/23444f53a813a3296c1368bb104793ce8d88f04a", + "reference": "23444f53a813a3296c1368bb104793ce8d88f04a", + "shasum": "" + }, + "require": { + "php": ">=7.1.0" + }, + "require-dev": { + "phpstan/phpstan": "1.12.28 || 1.4.10", + "phpunit/phpunit": "^9.6 || ^7.5" + }, + "type": "library", + "autoload": { + "files": [ + "src/functions_include.php" + ], + "psr-4": { + "React\\Promise\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Christian LΓΌck", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" + } + ], + "description": "A lightweight implementation of CommonJS Promises/A for PHP", + "keywords": [ + "promise", + "promises" + ], + "support": { + "issues": "https://github.com/reactphp/promise/issues", + "source": "https://github.com/reactphp/promise/tree/v3.3.0" + }, + "funding": [ + { + "url": "https://opencollective.com/reactphp", + "type": "open_collective" + } + ], + "time": "2025-08-19T18:57:03+00:00" + }, + { + "name": "revolt/event-loop", + "version": "v1.0.9", + "source": { + "type": "git", + "url": "https://github.com/revoltphp/event-loop.git", + "reference": "44061cf513e53c6200372fc935ac42271566295d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/revoltphp/event-loop/zipball/44061cf513e53c6200372fc935ac42271566295d", + "reference": "44061cf513e53c6200372fc935ac42271566295d", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "ext-json": "*", + "jetbrains/phpstorm-stubs": "^2019.3", + "phpunit/phpunit": "^9", + "psalm/phar": "6.16.*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Revolt\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "ceesjank@gmail.com" + }, + { + "name": "Christian LΓΌck", + "email": "christian@clue.engineering" + }, + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + } + ], + "description": "Rock-solid event loop for concurrent PHP applications.", + "keywords": [ + "async", + "asynchronous", + "concurrency", + "event", + "event-loop", + "non-blocking", + "scheduler" + ], + "support": { + "issues": "https://github.com/revoltphp/event-loop/issues", + "source": "https://github.com/revoltphp/event-loop/tree/v1.0.9" + }, + "time": "2026-05-16T17:55:38+00:00" + }, + { + "name": "roave/backward-compatibility-check", + "version": "8.21.0", + "source": { + "type": "git", + "url": "https://github.com/Roave/BackwardCompatibilityCheck.git", + "reference": "ee85fb8879cfe939814d84e18ec6cb5c33904b75" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Roave/BackwardCompatibilityCheck/zipball/ee85fb8879cfe939814d84e18ec6cb5c33904b75", + "reference": "ee85fb8879cfe939814d84e18ec6cb5c33904b75", + "shasum": "" + }, + "require": { + "composer/composer": "^2.9.8", + "ext-dom": "*", + "ext-json": "*", + "ext-libxml": "*", + "ext-simplexml": "*", + "nikic/php-parser": "^5.7.0", + "nikolaposa/version": "^4.2.1", + "ocramius/package-versions": "^2.11.0", + "php": "~8.4.0 || ~8.5.0", + "php-standard-library/async": "^6.1.1", + "php-standard-library/dict": "^6.1.1", + "php-standard-library/env": "^6.1.1", + "php-standard-library/file": "^6.1.1", + "php-standard-library/filesystem": "^6.1.1", + "php-standard-library/foundation": "^6.1.1", + "php-standard-library/iter": "^6.1.1", + "php-standard-library/json": "^6.1.1", + "php-standard-library/regex": "^6.1.1", + "php-standard-library/shell": "^6.1.1", + "php-standard-library/str": "^6.1.1", + "php-standard-library/type": "^6.1.1", + "php-standard-library/vec": "^6.1.1", + "roave/better-reflection": "^6.71.0", + "symfony/console": "^7.4.11" + }, + "conflict": { + "marc-mabe/php-enum": "<4.7.2", + "revolt/event-loop": "<0.2.5", + "symfony/process": "<5.3.7" + }, + "require-dev": { + "doctrine/coding-standard": "^14.0.0", + "justinrainbow/json-schema": "^6.8.2", + "php-standard-library/hash": "^6.1.1", + "php-standard-library/psalm-plugin": "^2.4.0", + "php-standard-library/secure-random": "^6.1.1", + "phpunit/phpunit": "^13.1.10", + "psalm/plugin-phpunit": "^0.19.7", + "roave/infection-static-analysis-plugin": "^1.44.0", + "roave/security-advisories": "dev-master", + "squizlabs/php_codesniffer": "^4.0.1", + "vimeo/psalm": "^6.16.1" + }, + "bin": [ + "bin/roave-backward-compatibility-check" + ], + "type": "library", + "autoload": { + "psr-4": { + "Roave\\BackwardCompatibility\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "James Titcumb", + "email": "james@asgrim.com" + }, + { + "name": "Marco Pivetta", + "email": "ocramius@gmail.com" + } + ], + "description": "Tool to compare two revisions of a public API to check for BC breaks", + "support": { + "issues": "https://github.com/Roave/BackwardCompatibilityCheck/issues", + "source": "https://github.com/Roave/BackwardCompatibilityCheck/tree/8.21.0" + }, + "time": "2026-05-15T14:19:15+00:00" + }, + { + "name": "roave/better-reflection", + "version": "6.71.0", + "source": { + "type": "git", + "url": "https://github.com/Roave/BetterReflection.git", + "reference": "3ec176d4a161b4e8764edc625d69ff624ca390b1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Roave/BetterReflection/zipball/3ec176d4a161b4e8764edc625d69ff624ca390b1", + "reference": "3ec176d4a161b4e8764edc625d69ff624ca390b1", + "shasum": "" + }, + "require": { + "ext-json": "*", + "jetbrains/phpstorm-stubs": "2026.1", + "nikic/php-parser": "^5.7.0", + "php": "~8.4.1 || ~8.5.0" + }, + "conflict": { + "thecodingmachine/safe": "<1.1.3" + }, + "require-dev": { + "phpbench/phpbench": "^1.6.1", + "phpunit/phpunit": "^13.1.8" + }, + "suggest": { + "composer/composer": "Required to use the ComposerSourceLocator" + }, + "type": "library", + "autoload": { + "psr-4": { + "Roave\\BetterReflection\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "James Titcumb", + "email": "james@asgrim.com", + "homepage": "https://github.com/asgrim" + }, + { + "name": "Marco Pivetta", + "email": "ocramius@gmail.com", + "homepage": "https://ocramius.github.io/" + }, + { + "name": "Gary Hockin", + "email": "gary@roave.com", + "homepage": "https://github.com/geeh" + }, + { + "name": "Jaroslav HanslΓ­k", + "email": "kukulich@kukulich.cz", + "homepage": "https://github.com/kukulich" + } + ], + "description": "Better Reflection - an improved code reflection API", + "support": { + "issues": "https://github.com/Roave/BetterReflection/issues", + "source": "https://github.com/Roave/BetterReflection/tree/6.71.0" + }, + "time": "2026-05-02T10:56:00+00:00" + }, + { + "name": "seld/jsonlint", + "version": "1.12.1", + "source": { + "type": "git", + "url": "https://github.com/Seldaek/jsonlint.git", + "reference": "9a90eb5d32d5a500296bf43f946d60246444d5f7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Seldaek/jsonlint/zipball/9a90eb5d32d5a500296bf43f946d60246444d5f7", + "reference": "9a90eb5d32d5a500296bf43f946d60246444d5f7", + "shasum": "" + }, + "require": { + "php": "^5.3 || ^7.0 || ^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^1.11", + "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.0 || ^8.5.13" + }, + "bin": [ + "bin/jsonlint" + ], + "type": "library", + "autoload": { + "psr-4": { + "Seld\\JsonLint\\": "src/Seld/JsonLint/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "https://seld.be" + } + ], + "description": "JSON Linter", + "keywords": [ + "json", + "linter", + "parser", + "validator" + ], + "support": { + "issues": "https://github.com/Seldaek/jsonlint/issues", + "source": "https://github.com/Seldaek/jsonlint/tree/1.12.1" + }, + "funding": [ + { + "url": "https://github.com/Seldaek", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/seld/jsonlint", + "type": "tidelift" + } + ], + "time": "2026-06-12T11:32:29+00:00" + }, + { + "name": "seld/phar-utils", + "version": "1.2.1", + "source": { + "type": "git", + "url": "https://github.com/Seldaek/phar-utils.git", + "reference": "ea2f4014f163c1be4c601b9b7bd6af81ba8d701c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Seldaek/phar-utils/zipball/ea2f4014f163c1be4c601b9b7bd6af81ba8d701c", + "reference": "ea2f4014f163c1be4c601b9b7bd6af81ba8d701c", + "shasum": "" + }, + "require": { + "php": ">=5.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Seld\\PharUtils\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be" + } + ], + "description": "PHAR file format utilities, for when PHP phars you up", + "keywords": [ + "phar" + ], + "support": { + "issues": "https://github.com/Seldaek/phar-utils/issues", + "source": "https://github.com/Seldaek/phar-utils/tree/1.2.1" + }, + "time": "2022-08-31T10:31:18+00:00" + }, + { + "name": "seld/signal-handler", + "version": "2.0.2", + "source": { + "type": "git", + "url": "https://github.com/Seldaek/signal-handler.git", + "reference": "04a6112e883ad76c0ada8e4a9f7520bbfdb6bb98" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Seldaek/signal-handler/zipball/04a6112e883ad76c0ada8e4a9f7520bbfdb6bb98", + "reference": "04a6112e883ad76c0ada8e4a9f7520bbfdb6bb98", + "shasum": "" + }, + "require": { + "php": ">=7.2.0" + }, + "require-dev": { + "phpstan/phpstan": "^1", + "phpstan/phpstan-deprecation-rules": "^1.0", + "phpstan/phpstan-phpunit": "^1", + "phpstan/phpstan-strict-rules": "^1.3", + "phpunit/phpunit": "^7.5.20 || ^8.5.23", + "psr/log": "^1 || ^2 || ^3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "2.x-dev" + } + }, + "autoload": { + "psr-4": { + "Seld\\Signal\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + } + ], + "description": "Simple unix signal handler that silently fails where signals are not supported for easy cross-platform development", + "keywords": [ + "posix", + "sigint", + "signal", + "sigterm", + "unix" + ], + "support": { + "issues": "https://github.com/Seldaek/signal-handler/issues", + "source": "https://github.com/Seldaek/signal-handler/tree/2.0.2" + }, + "time": "2023-09-03T09:24:00+00:00" + }, + { + "name": "symfony/console", + "version": "v7.4.13", + "source": { + "type": "git", + "url": "https://github.com/symfony/console.git", + "reference": "85095d2573eaefaf35e40b9513a9bf09f72cd217" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/console/zipball/85095d2573eaefaf35e40b9513a9bf09f72cd217", + "reference": "85095d2573eaefaf35e40b9513a9bf09f72cd217", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-mbstring": "~1.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/string": "^7.2|^8.0" + }, + "conflict": { + "symfony/dependency-injection": "<6.4", + "symfony/dotenv": "<6.4", + "symfony/event-dispatcher": "<6.4", + "symfony/lock": "<6.4", + "symfony/process": "<6.4" + }, + "provide": { + "psr/log-implementation": "1.0|2.0|3.0" + }, + "require-dev": { + "psr/log": "^1|^2|^3", + "symfony/config": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/event-dispatcher": "^6.4|^7.0|^8.0", + "symfony/http-foundation": "^6.4|^7.0|^8.0", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/lock": "^6.4|^7.0|^8.0", + "symfony/messenger": "^6.4|^7.0|^8.0", + "symfony/process": "^6.4|^7.0|^8.0", + "symfony/stopwatch": "^6.4|^7.0|^8.0", + "symfony/var-dumper": "^6.4|^7.0|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Console\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Eases the creation of beautiful and testable command line interfaces", + "homepage": "https://symfony.com", + "keywords": [ + "cli", + "command-line", + "console", + "terminal" + ], + "support": { + "source": "https://github.com/symfony/console/tree/v7.4.13" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "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": "2026-05-24T08:56:14+00:00" + }, + { + "name": "symfony/deprecation-contracts", + "version": "v3.7.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/deprecation-contracts.git", + "reference": "50f59d1f3ca46d41ac911f97a78626b6756af35b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/50f59d1f3ca46d41ac911f97a78626b6756af35b", + "reference": "50f59d1f3ca46d41ac911f97a78626b6756af35b", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.7-dev" + } + }, + "autoload": { + "files": [ + "function.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "A generic function and convention to trigger deprecation notices", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.7.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "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": "2026-04-13T15:52:40+00:00" + }, + { + "name": "symfony/filesystem", + "version": "v8.1.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/filesystem.git", + "reference": "99aec13b82b4967ec5088222c4a3ecca955949c2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/99aec13b82b4967ec5088222c4a3ecca955949c2", + "reference": "99aec13b82b4967ec5088222c4a3ecca955949c2", + "shasum": "" + }, + "require": { + "php": ">=8.4.1", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-mbstring": "~1.8" + }, + "require-dev": { + "symfony/process": "^7.4|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Filesystem\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides basic utilities for the filesystem", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/filesystem/tree/v8.1.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "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": "2026-05-29T05:06:50+00:00" + }, + { + "name": "symfony/finder", + "version": "v8.1.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/finder.git", + "reference": "58d2e767a66052c1487356f953445634a8194c64" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/finder/zipball/58d2e767a66052c1487356f953445634a8194c64", + "reference": "58d2e767a66052c1487356f953445634a8194c64", + "shasum": "" + }, + "require": { + "php": ">=8.4.1" + }, + "require-dev": { + "symfony/filesystem": "^7.4|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Finder\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Finds files and directories via an intuitive fluent interface", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/finder/tree/v8.1.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "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": "2026-05-29T05:06:50+00:00" + }, + { + "name": "symfony/polyfill-ctype", + "version": "v1.37.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-ctype.git", + "reference": "141046a8f9477948ff284fa65be2095baafb94f2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/141046a8f9477948ff284fa65be2095baafb94f2", + "reference": "141046a8f9477948ff284fa65be2095baafb94f2", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "provide": { + "ext-ctype": "*" + }, + "suggest": { + "ext-ctype": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Ctype\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Gert de Pagter", + "email": "BackEndTea@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for ctype functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "ctype", + "polyfill", + "portable" + ], + "support": { + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.37.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "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": "2026-04-10T16:19:22+00:00" + }, + { + "name": "symfony/polyfill-intl-grapheme", + "version": "v1.38.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-grapheme.git", + "reference": "e9247d281d694a5120554d9afaf54e070e88a603" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/e9247d281d694a5120554d9afaf54e070e88a603", + "reference": "e9247d281d694a5120554d9afaf54e070e88a603", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Grapheme\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's grapheme_* functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "grapheme", + "intl", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.38.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "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": "2026-05-26T05:58:03+00:00" + }, + { + "name": "symfony/polyfill-intl-normalizer", + "version": "v1.38.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-normalizer.git", + "reference": "2d446c214bdbe5b71bde5011b060a05fece3ae6b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/2d446c214bdbe5b71bde5011b060a05fece3ae6b", + "reference": "2d446c214bdbe5b71bde5011b060a05fece3ae6b", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Normalizer\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's Normalizer class and related functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "intl", + "normalizer", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.38.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "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": "2026-05-25T13:48:31+00:00" + }, + { + "name": "symfony/polyfill-mbstring", + "version": "v1.38.2", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-mbstring.git", + "reference": "d3d318bad5e7a1bfbd026009c8bfb8d8f99ae6b6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/d3d318bad5e7a1bfbd026009c8bfb8d8f99ae6b6", + "reference": "d3d318bad5e7a1bfbd026009c8bfb8d8f99ae6b6", + "shasum": "" + }, + "require": { + "ext-iconv": "*", + "php": ">=7.2" + }, + "provide": { + "ext-mbstring": "*" + }, + "suggest": { + "ext-mbstring": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Mbstring\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for the Mbstring extension", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "mbstring", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.38.2" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "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": "2026-05-27T06:59:30+00:00" + }, + { + "name": "symfony/polyfill-php73", + "version": "v1.37.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php73.git", + "reference": "0f68c03565dcaaf25a890667542e8bd75fe7e5bb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php73/zipball/0f68c03565dcaaf25a890667542e8bd75fe7e5bb", + "reference": "0f68c03565dcaaf25a890667542e8bd75fe7e5bb", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php73\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 7.3+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php73/tree/v1.37.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "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" + }, + { + "name": "symfony/polyfill-php80", + "version": "v1.37.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php80.git", + "reference": "dfb55726c3a76ea3b6459fcfda1ec2d80a682411" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/dfb55726c3a76ea3b6459fcfda1ec2d80a682411", + "reference": "dfb55726c3a76ea3b6459fcfda1ec2d80a682411", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php80\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ion Bazan", + "email": "ion.bazan@gmail.com" + }, + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php80/tree/v1.37.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "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": "2026-04-10T16:19:22+00:00" + }, + { + "name": "symfony/polyfill-php81", + "version": "v1.38.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php81.git", + "reference": "6bfb9c766cacffbc8e118cb87217d08ed84e5cd7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php81/zipball/6bfb9c766cacffbc8e118cb87217d08ed84e5cd7", + "reference": "6bfb9c766cacffbc8e118cb87217d08ed84e5cd7", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php81\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.1+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php81/tree/v1.38.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "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": "2026-05-26T12:45:58+00:00" + }, + { + "name": "symfony/polyfill-php84", + "version": "v1.38.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php84.git", + "reference": "f4e1dfaee5b74aba5964fe1fd4dfc7ba5e3085fa" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php84/zipball/f4e1dfaee5b74aba5964fe1fd4dfc7ba5e3085fa", + "reference": "f4e1dfaee5b74aba5964fe1fd4dfc7ba5e3085fa", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php84\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.4+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php84/tree/v1.38.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "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": "2026-05-26T12:51:13+00:00" + }, + { + "name": "symfony/process", + "version": "v8.1.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/process.git", + "reference": "c4a9e58f235a6bf7f97ffbfedae2687353ac79e5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/process/zipball/c4a9e58f235a6bf7f97ffbfedae2687353ac79e5", + "reference": "c4a9e58f235a6bf7f97ffbfedae2687353ac79e5", + "shasum": "" + }, + "require": { + "php": ">=8.4.1" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Process\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Executes commands in sub-processes", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/process/tree/v8.1.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "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": "2026-05-29T05:06:50+00:00" + }, + { + "name": "symfony/service-contracts", + "version": "v3.7.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/service-contracts.git", + "reference": "d25d82433a80eba6aa0e6c24b61d7370d99e444a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/d25d82433a80eba6aa0e6c24b61d7370d99e444a", + "reference": "d25d82433a80eba6aa0e6c24b61d7370d99e444a", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/container": "^1.1|^2.0", + "symfony/deprecation-contracts": "^2.5|^3" + }, + "conflict": { + "ext-psr": "<1.1|>=2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.7-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\Service\\": "" + }, + "exclude-from-classmap": [ + "/Test/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to writing services", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/service-contracts/tree/v3.7.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "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": "2026-03-28T09:44:51+00:00" + }, + { + "name": "symfony/string", + "version": "v8.1.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/string.git", + "reference": "afd5944f4005862d961efb85c8bbd5c523c4e3c9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/string/zipball/afd5944f4005862d961efb85c8bbd5c523c4e3c9", + "reference": "afd5944f4005862d961efb85c8bbd5c523c4e3c9", + "shasum": "" + }, + "require": { + "php": ">=8.4.1", + "symfony/polyfill-ctype": "^1.8", + "symfony/polyfill-intl-grapheme": "^1.33", + "symfony/polyfill-intl-normalizer": "^1.0", + "symfony/polyfill-mbstring": "^1.0" + }, + "conflict": { + "symfony/translation-contracts": "<2.5" + }, + "require-dev": { + "symfony/emoji": "^7.4|^8.0", + "symfony/http-client": "^7.4|^8.0", + "symfony/intl": "^7.4|^8.0", + "symfony/translation-contracts": "^2.5|^3.0", + "symfony/var-exporter": "^7.4|^8.0" + }, + "type": "library", + "autoload": { + "files": [ + "Resources/functions.php" + ], + "psr-4": { + "Symfony\\Component\\String\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides an object-oriented API to strings and deals with bytes, UTF-8 code points and grapheme clusters in a unified way", + "homepage": "https://symfony.com", + "keywords": [ + "grapheme", + "i18n", + "string", + "unicode", + "utf-8", + "utf8" + ], + "support": { + "source": "https://github.com/symfony/string/tree/v8.1.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "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": "2026-05-29T05:06:50+00:00" + } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": {}, + "prefer-stable": false, + "prefer-lowest": false, + "platform": { + "php": "~8.5.0" + }, + "platform-dev": {}, + "plugin-api-version": "2.9.0" +} From a7128b072fc4227c3875a2dc5d53466b2e76fc40 Mon Sep 17 00:00:00 2001 From: Daniel Badura Date: Tue, 16 Jun 2026 20:56:14 +0200 Subject: [PATCH 2/2] Make mongodb/rango the same, update some sections --- docs/databases.md | 10 ++++++---- docs/documents.md | 25 +++++++------------------ docs/encryption.md | 30 +++++++++++++++++++++++++++--- docs/field-mapping.md | 2 +- docs/getting-started.md | 39 ++++++++++++++++++++++++++++----------- docs/repository.md | 25 +++++++++++++++---------- 6 files changed, 84 insertions(+), 47 deletions(-) diff --git a/docs/databases.md b/docs/databases.md index 5164a14..98e89bc 100644 --- a/docs/databases.md +++ b/docs/databases.md @@ -5,6 +5,11 @@ Patchlevel ODM runs on two backends: MongoDB and PostgreSQL through Both managers implement the same interface and hand you repositories with an identical API, so your documents and your application code stay the same across backends. +:::note +Both backends expose the same query API, so the [filter operators](repository.md#querying) and +[index definitions](documents.md#indexes) you write work the same on either one. +::: + ## PostgreSQL via Rango Require the Rango package and build a `RangoRepositoryManager` from a Rango client. The default @@ -19,6 +24,7 @@ $client = new Client($_ENV['POSTGRES_URI']); $manager = RangoRepositoryManager::create($client); $repository = $manager->get(Profile::class); ``` + ## MongoDB Require `mongodb/mongodb` and build a `MongoDBRepositoryManager` from a MongoDB client. The default @@ -33,10 +39,6 @@ $client = new Client($_ENV['MONGODB_URI']); $manager = MongoDBRepositoryManager::create($client); $repository = $manager->get(Profile::class); ``` -:::note -Rango mirrors the MongoDB query API, so the [filter operators](repository.md#querying) and -[index definitions](documents.md#indexes) you write work the same on both backends. -::: ## Choosing the database diff --git a/docs/documents.md b/docs/documents.md index 9e867bb..7687d31 100644 --- a/docs/documents.md +++ b/docs/documents.md @@ -32,6 +32,12 @@ A document needs exactly one `#[Id]` property. The library throws `NoIdPropertyF present and `MultipleIdPropertiesFound` when more than one property is marked. ::: +:::note +The `#[Document]` attribute also takes an optional second argument to store the document in a specific +[database](databases.md), for example `#[Document('profiles', database: 'analytics')]`. Otherwise the +document lives in the manager's default database. +::: + ## Identifiers The identifier is a string. The ODM always stores it under the reserved `_id` field, regardless of @@ -43,24 +49,6 @@ $repository->insert(new Profile('r-1', 'Rango', Status::ACTIVE, [new Skill('php' $profile = $repository->get('r-1'); ``` -## Choosing a database - -By default a document lives in the manager's default database. You can pin a document to a specific -database with the second argument of `#[Document]`, which is useful when you spread collections -across several databases. - -```php -#[Document('profiles', database: 'analytics')] -final class Profile -{ - // ... -} -``` -:::note -The default database differs per backend (`public` for PostgreSQL, `default` for MongoDB). The -[databases](databases.md) page explains how the default is resolved. -::: - ## Property values Properties are mapped by the [hydrator](https://github.com/patchlevel/hydrator/). Scalars, enums, @@ -159,3 +147,4 @@ your collections stay in sync with the document definitions. * [How to store and load documents](repository.md) * [How to control field names and normalization](field-mapping.md) * [How to query documents efficiently](repository.md#querying) +* [How to choose a database backend](databases.md) diff --git a/docs/encryption.md b/docs/encryption.md index 65cad2f..48a56b1 100644 --- a/docs/encryption.md +++ b/docs/encryption.md @@ -45,7 +45,10 @@ encrypted data became unreadable. ## Setting up the hydrator Encryption is configured on the hydrator, which you then pass to the repository manager's `create()` -factory. Build the hydrator with the `CryptographyExtension` and a key store for your backend. +factory. Build the hydrator with the `CryptographyExtension` and the key store for your backend. Each +backend ships its own key store; the rest of the setup is the same. + +For PostgreSQL via Rango: ```php use Patchlevel\Hydrator\Extension\Cryptography\BaseCryptographer; @@ -66,9 +69,30 @@ $hydrator = (new StackHydratorBuilder()) $manager = RangoRepositoryManager::create($client, $hydrator); ``` +For MongoDB: + +```php +use MongoDB\Client; +use Patchlevel\Hydrator\Extension\Cryptography\BaseCryptographer; +use Patchlevel\Hydrator\Extension\Cryptography\CryptographyExtension; +use Patchlevel\Hydrator\StackHydratorBuilder; +use Patchlevel\ODM\Hydrator\MongoDBCipherKeyStore; +use Patchlevel\ODM\Repository\MongoDBRepositoryManager; + +$client = new Client($_ENV['MONGODB_URI']); + +$keyStore = new MongoDBCipherKeyStore($client->selectDatabase('default')); +$cryptographer = BaseCryptographer::createWithOpenssl($keyStore); + +$hydrator = (new StackHydratorBuilder()) + ->useExtension(new CryptographyExtension($cryptographer)) + ->build(); + +$manager = MongoDBRepositoryManager::create($client, $hydrator); +``` :::note -On MongoDB use `MongoDBCipherKeyStore` with a `MongoDB\Database` and `MongoDBRepositoryManager`. The -rest of the setup is identical. See the [databases](databases.md) page. +The cipher key store is the only backend-specific part. The `#[DataSubjectId]` and `#[SensitiveData]` +attributes and everything else work the same on both. ::: ## Storing and loading diff --git a/docs/field-mapping.md b/docs/field-mapping.md index 992e671..cd9c236 100644 --- a/docs/field-mapping.md +++ b/docs/field-mapping.md @@ -147,7 +147,7 @@ final readonly class Skill ``` :::tip The hydrator ships normalizers for enums, dates and arrays out of the box. See the -[hydrator documentation](https://github.com/patchlevel/hydrator/) for the full list. +[hydrator documentation](https://patchlevel.dev/docs/hydrator/latest) for the full list. ::: ## Learn more diff --git a/docs/getting-started.md b/docs/getting-started.md index d240d67..5fc7121 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -1,19 +1,23 @@ # Getting Started This guide walks you through a complete example: you define a `Profile` document, set up a -repository manager, and then insert, load, query, update and remove documents. The example uses -PostgreSQL through [Rango](databases.md), but every step works the same way on MongoDB. +repository manager, and then insert, load, query, update and remove documents. Only the initial +setup differs between MongoDB and PostgreSQL; every step after it is identical on both. ## Installation -Install the library together with the driver for your database: +Install the library together with the driver for your backend. + +For PostgreSQL via [Rango](databases.md): ```bash composer require patchlevel/odm patchlevel/rango ``` -:::note -For MongoDB, require `mongodb/mongodb` instead. The [databases](databases.md) page explains both setups. -::: +For MongoDB: + +```bash +composer require patchlevel/odm mongodb/mongodb +``` ## Define a document @@ -66,7 +70,10 @@ attribute is a custom normalizer. Both are explained on the [field mapping](fiel ## Set up the repository manager The repository manager creates and caches one repository per document class. Build it with the -static `create()` factory and pass your database client. +static `create()` factory and pass your database client. Pick the manager for your backend; the +repository you get back behaves the same either way. + +For PostgreSQL via Rango: ```php use Patchlevel\ODM\Repository\RangoRepositoryManager; @@ -75,12 +82,22 @@ use Patchlevel\Rango\Client; $client = new Client($_ENV['POSTGRES_URI']); $manager = RangoRepositoryManager::create($client); +``` +For MongoDB: + +```php +use MongoDB\Client; +use Patchlevel\ODM\Repository\MongoDBRepositoryManager; + +$client = new Client($_ENV['MONGODB_URI']); + +$manager = MongoDBRepositoryManager::create($client); +``` +From here on the code is the same for both backends: + +```php $repository = $manager->get(Profile::class); ``` -:::tip -On MongoDB you use `MongoDBRepositoryManager` with a `MongoDB\Client`. The API of the resulting -repository is identical. See the [databases](databases.md) page. -::: ## Create the collection diff --git a/docs/repository.md b/docs/repository.md index 8daa7b5..f73bb3a 100644 --- a/docs/repository.md +++ b/docs/repository.md @@ -1,21 +1,26 @@ # Repository -A repository stores and loads documents of a single type. You obtain one from the repository manager -by passing the document class. The manager creates the repository on first use and caches it, so -calling `get()` repeatedly returns the same instance. +A repository stores and loads documents of a single type. You obtain one from a +[repository manager](databases.md) by passing the document class. The manager creates the repository +on first use and caches it, so calling `get()` repeatedly returns the same instance. + +The manager you pick depends on your backend, but the repository it returns exposes the same methods +on both MongoDB and PostgreSQL: ```php +use Patchlevel\ODM\Repository\MongoDBRepositoryManager; use Patchlevel\ODM\Repository\RangoRepositoryManager; -use Patchlevel\Rango\Client; -$client = new Client($_ENV['POSTGRES_URI']); +// PostgreSQL via Rango +$manager = RangoRepositoryManager::create($rangoClient); + +// MongoDB +$manager = MongoDBRepositoryManager::create($mongoClient); -$manager = RangoRepositoryManager::create($client); $repository = $manager->get(Profile::class); ``` :::note -The manager is backend specific. Use `MongoDBRepositoryManager` for MongoDB. The resulting -repository exposes the same methods either way. See the [databases](databases.md) page. +See the [databases](databases.md) page for how to create the client and manager for each backend. ::: ## No Unit of Work @@ -127,8 +132,8 @@ $result = iterator_to_array( ); ``` :::tip -The same operators work on both backends, because the PostgreSQL layer -[Rango](https://github.com/patchlevel/rango/) mirrors the MongoDB query API. +The same operators work on both backends, because MongoDB and +[Rango](https://github.com/patchlevel/rango/) share the same query API. ::: ### Sorting