From b69e7baa3661399ed64e9675c55d50ea76c7beb5 Mon Sep 17 00:00:00 2001 From: David Badura Date: Thu, 21 Mar 2024 14:16:38 +0100 Subject: [PATCH 1/7] check docs --- .github/workflows/docs_check.yml | 47 +++ .gitignore | 3 +- bin/extract-php-code-from-docs | 59 ++++ composer.json | 4 +- composer.lock | 474 ++++++++++++++++++++++++++++++- 5 files changed, 584 insertions(+), 3 deletions(-) create mode 100644 .github/workflows/docs_check.yml create mode 100755 bin/extract-php-code-from-docs diff --git a/.github/workflows/docs_check.yml b/.github/workflows/docs_check.yml new file mode 100644 index 000000000..1f0d1f9a4 --- /dev/null +++ b/.github/workflows/docs_check.yml @@ -0,0 +1,47 @@ +# 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" + - "renovate/*" + +jobs: + checkdocs: + name: "Check Docs" + + runs-on: ${{ matrix.operating-system }} + + strategy: + matrix: + dependencies: + - "locked" + php-version: + - "8.3" + operating-system: + - "ubuntu-latest" + + steps: + - name: "Checkout" + uses: actions/checkout@v4 + + - name: "Install PHP" + uses: "shivammathur/setup-php@2.29.0" + with: + coverage: "pcov" + php-version: "${{ matrix.php-version }}" + ini-values: memory_limit=-1 + extensions: pdo_sqlite + + - uses: ramsey/composer-install@2.2.0 + with: + dependency-versions: ${{ matrix.dependencies }} + + - name: "extract php code" + run: "bin/extract-php-code-from-docs" + + - name: "lint php" + run: "php -l docs_php/*.php" \ No newline at end of file diff --git a/.gitignore b/.gitignore index 4e51bc643..387961983 100644 --- a/.gitignore +++ b/.gitignore @@ -8,4 +8,5 @@ infection.log .phpbench/ infection.html *.sqlite3 -.deptrac.cache \ No newline at end of file +.deptrac.cache +docs_php/ \ No newline at end of file diff --git a/bin/extract-php-code-from-docs b/bin/extract-php-code-from-docs new file mode 100755 index 000000000..26cab746e --- /dev/null +++ b/bin/extract-php-code-from-docs @@ -0,0 +1,59 @@ +#!/usr/bin/env php +addExtension(new CommonMarkCoreExtension()); +$environment->addExtension(new GithubFlavoredMarkdownExtension()); + +$parser = new MarkdownParser($environment); + +$path = __DIR__ . '/../docs/pages/subscription.md'; +$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/pages')->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 $node) { + if ($node->getInfo() !== 'php') { + continue; + } + + $source = sprintf('%s:%s', $file->getRealPath(), $node->getStartLine()); + + $code = "getLiteral() . "\n"; + + $targetPath = $targetDir . '/' . $fileName . '_' . $node->getStartLine() . '.php'; + file_put_contents($targetPath, $code); + + $node->setLiteral($source); + } +} diff --git a/composer.json b/composer.json index 2d131fac0..c158b2623 100644 --- a/composer.json +++ b/composer.json @@ -38,6 +38,7 @@ "cspray/phinal": "^2.0.0", "doctrine/orm": "^2.18.0|^3.0.0", "infection/infection": "^0.27.0", + "league/commonmark": "^2.4", "patchlevel/coding-standard": "^1.3.0", "patchlevel/event-sourcing-psalm-plugin": "^2.1.0", "phpbench/phpbench": "^1.2.15", @@ -48,7 +49,8 @@ "roave/infection-static-analysis-plugin": "^1.34.0", "symfony/messenger": "^5.4.31|^6.4.0|^7.0.1", "symfony/var-dumper": "^5.4.29|^6.4.0|^7.0.0", - "vimeo/psalm": "^5.17.0" + "vimeo/psalm": "^5.17.0", + "wnx/commonmark-markdown-renderer": "^1.4" }, "suggest": { "patchlevel/event-sourcing-psalm-plugin": "for psalm support" diff --git a/composer.lock b/composer.lock index 861d65f5b..50f17fc11 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": "0c745538b704294d92dfee9c2b9d6323", + "content-hash": "242dc36fff60bf7f550b3934d70d6054", "packages": [ { "name": "brick/math", @@ -2701,6 +2701,81 @@ }, "time": "2023-01-05T11:28:13+00:00" }, + { + "name": "dflydev/dot-access-data", + "version": "v3.0.2", + "source": { + "type": "git", + "url": "https://github.com/dflydev/dflydev-dot-access-data.git", + "reference": "f41715465d65213d644d3141a6a93081be5d3549" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/dflydev/dflydev-dot-access-data/zipball/f41715465d65213d644d3141a6a93081be5d3549", + "reference": "f41715465d65213d644d3141a6a93081be5d3549", + "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.2" + }, + "time": "2022-10-27T11:44:00+00:00" + }, { "name": "dnoegel/php-xdg-base-dir", "version": "v0.1.1", @@ -3865,6 +3940,194 @@ }, "time": "2023-09-26T02:20:38+00:00" }, + { + "name": "league/commonmark", + "version": "2.4.2", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/commonmark.git", + "reference": "91c24291965bd6d7c46c46a12ba7492f83b1cadf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/commonmark/zipball/91c24291965bd6d7c46c46a12ba7492f83b1cadf", + "reference": "91c24291965bd6d7c46c46a12ba7492f83b1cadf", + "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.30.3", + "commonmark/commonmark.js": "0.30.0", + "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", + "symfony/yaml": "^2.3 | ^3.0 | ^4.0 | ^5.0 | ^6.0 || ^7.0", + "unleashedtech/php-coding-standard": "^3.1.1", + "vimeo/psalm": "^4.24.0 || ^5.0.0" + }, + "suggest": { + "symfony/yaml": "v2.3+ required if using the Front Matter extension" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "2.5-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": "2024-02-02T11:59:32+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": "myclabs/deep-copy", "version": "1.11.1", @@ -3975,6 +4238,154 @@ }, "time": "2024-01-31T06:18:54+00:00" }, + { + "name": "nette/schema", + "version": "v1.3.0", + "source": { + "type": "git", + "url": "https://github.com/nette/schema.git", + "reference": "a6d3a6d1f545f01ef38e60f375d1cf1f4de98188" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nette/schema/zipball/a6d3a6d1f545f01ef38e60f375d1cf1f4de98188", + "reference": "a6d3a6d1f545f01ef38e60f375d1cf1f4de98188", + "shasum": "" + }, + "require": { + "nette/utils": "^4.0", + "php": "8.1 - 8.3" + }, + "require-dev": { + "nette/tester": "^2.4", + "phpstan/phpstan-nette": "^1.0", + "tracy/tracy": "^2.8" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.3-dev" + } + }, + "autoload": { + "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.0" + }, + "time": "2023-12-11T11:54:22+00:00" + }, + { + "name": "nette/utils", + "version": "v4.0.4", + "source": { + "type": "git", + "url": "https://github.com/nette/utils.git", + "reference": "d3ad0aa3b9f934602cb3e3902ebccf10be34d218" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nette/utils/zipball/d3ad0aa3b9f934602cb3e3902ebccf10be34d218", + "reference": "d3ad0aa3b9f934602cb3e3902ebccf10be34d218", + "shasum": "" + }, + "require": { + "php": ">=8.0 <8.4" + }, + "conflict": { + "nette/finder": "<3", + "nette/schema": "<1.2.2" + }, + "require-dev": { + "jetbrains/phpstorm-attributes": "dev-master", + "nette/tester": "^2.5", + "phpstan/phpstan": "^1.0", + "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.0-dev" + } + }, + "autoload": { + "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.0.4" + }, + "time": "2024-01-17T16:50:36+00:00" + }, { "name": "nikic/php-parser", "version": "v4.18.0", @@ -7782,6 +8193,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.4.1", + "source": { + "type": "git", + "url": "https://github.com/stefanzweifel/commonmark-markdown-renderer.git", + "reference": "a500dd421c52057c6ba286afe07d48faa38686a6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/stefanzweifel/commonmark-markdown-renderer/zipball/a500dd421c52057c6ba286afe07d48faa38686a6", + "reference": "a500dd421c52057c6ba286afe07d48faa38686a6", + "shasum": "" + }, + "require": { + "league/commonmark": "^2.0", + "php": "^8.1" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^3.0", + "phpunit/phpunit": "^10.0", + "rector/rector": "^0.15.17", + "vimeo/psalm": "^5.7" + }, + "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.4.1" + }, + "funding": [ + { + "url": "https://github.com/stefanzweifel", + "type": "github" + } + ], + "time": "2023-11-19T06:20:07+00:00" } ], "aliases": [], From b5d4fb541b1af38585f7b73e335edd9c80a08b86 Mon Sep 17 00:00:00 2001 From: David Badura Date: Thu, 21 Mar 2024 15:15:55 +0100 Subject: [PATCH 2/7] code format docs --- .../{docs_check.yml => docs-check.yml} | 2 +- Makefile | 7 +- ...p-code-from-docs => docs-extract-php-code} | 10 +- bin/docs-inject-php-code | 63 ++++++++++++ docs/pages/aggregate.md | 96 ++++++++----------- docs/pages/aggregate_id.md | 26 ++--- docs/pages/cli.md | 7 +- docs/pages/clock.md | 8 +- docs/pages/event_bus.md | 28 ++---- docs/pages/events.md | 43 ++++----- docs/pages/getting_started.md | 35 +++---- docs/pages/index.md | 6 +- docs/pages/message_decorator.md | 16 ++-- docs/pages/normalizer.md | 48 +++------- docs/pages/outbox.md | 27 +++--- docs/pages/pipeline.md | 71 +++++--------- docs/pages/repository.md | 48 ++++------ docs/pages/snapshots.md | 86 ++++++++--------- docs/pages/split_stream.md | 12 +-- docs/pages/store.md | 40 +++----- docs/pages/subscription.md | 87 +++++++---------- docs/pages/testing.md | 6 +- docs/pages/upcasting.md | 11 +-- 23 files changed, 341 insertions(+), 442 deletions(-) rename .github/workflows/{docs_check.yml => docs-check.yml} (95%) rename bin/{extract-php-code-from-docs => docs-extract-php-code} (79%) create mode 100755 bin/docs-inject-php-code diff --git a/.github/workflows/docs_check.yml b/.github/workflows/docs-check.yml similarity index 95% rename from .github/workflows/docs_check.yml rename to .github/workflows/docs-check.yml index 1f0d1f9a4..af5bf9ac4 100644 --- a/.github/workflows/docs_check.yml +++ b/.github/workflows/docs-check.yml @@ -41,7 +41,7 @@ jobs: dependency-versions: ${{ matrix.dependencies }} - name: "extract php code" - run: "bin/extract-php-code-from-docs" + run: "bin/docs-extract-php-code" - name: "lint php" run: "php -l docs_php/*.php" \ No newline at end of file diff --git a/Makefile b/Makefile index d50ed25b1..faab8130a 100644 --- a/Makefile +++ b/Makefile @@ -77,5 +77,8 @@ benchmark-diff-test: vendor # dev: static test ## run dev tools .PHONY: docs -docs: ## run mkdocs - cd docs && mkdocs serve \ No newline at end of file +docs: mkdocs ## run mkdocs + cd docs && python3 -m mkdocs serve + +mkdocs: ## run mkdocs + cd docs && pip3 install -r requirements.txt \ No newline at end of file diff --git a/bin/extract-php-code-from-docs b/bin/docs-extract-php-code similarity index 79% rename from bin/extract-php-code-from-docs rename to bin/docs-extract-php-code index 26cab746e..697b78a8d 100755 --- a/bin/extract-php-code-from-docs +++ b/bin/docs-extract-php-code @@ -2,22 +2,18 @@ addExtension(new CommonMarkCoreExtension()); -$environment->addExtension(new GithubFlavoredMarkdownExtension()); +$environment->addExtension(new MarkdownRendererExtension()); $parser = new MarkdownParser($environment); -$path = __DIR__ . '/../docs/pages/subscription.md'; $targetDir = __DIR__ . '/../docs_php'; if (file_exists($targetDir)) { @@ -53,7 +49,5 @@ foreach ($finder as $file) { $targetPath = $targetDir . '/' . $fileName . '_' . $node->getStartLine() . '.php'; file_put_contents($targetPath, $code); - - $node->setLiteral($source); } } diff --git a/bin/docs-inject-php-code b/bin/docs-inject-php-code new file mode 100755 index 000000000..47ca77da5 --- /dev/null +++ b/bin/docs-inject-php-code @@ -0,0 +1,63 @@ +#!/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/pages')->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 $node) { + if ($node->getInfo() !== 'php') { + continue; + } + + $targetPath = $targetDir . '/' . $fileName . '_' . $node->getStartLine() . '.php'; + $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/docs/pages/aggregate.md b/docs/pages/aggregate.md index 5b947dd3e..2e3d6aff8 100644 --- a/docs/pages/aggregate.md +++ b/docs/pages/aggregate.md @@ -1,6 +1,6 @@ # Aggregate -The linchpin of event-sourcing is the aggregate. These aggregates can be imagined like entities in ORM. +The linchpin of event-sourcing is the aggregate. These aggregates can be imagined like entities in ORM. One main difference is that we don't save the current state, but only the individual events that led to the state. This means it is always possible to build the current state again from the events. @@ -8,7 +8,7 @@ This means it is always possible to build the current state again from the event The term aggregate itself comes from DDD and has nothing to do with event sourcing and can be used independently as a pattern. You can find out more about Aggregates [here](https://martinfowler.com/bliki/DDD_Aggregate.html). - + An aggregate must fulfill a few points so that we can use it in event-sourcing: * It must implement the `AggregateRoot` interface. @@ -18,8 +18,8 @@ An aggregate must fulfill a few points so that we can use it in event-sourcing: * And rebuild/catchup its state from the events. We can implement this ourselves, or use the `BasicAggregateRoot` implementation that already brings everything with it. -This basic implementation uses attributes to configure the aggregate and to specify how it should handle events. -We are building a minimal aggregate class here which only has an ID and mark this with the `Id` attribute. +This basic implementation uses attributes to configure the aggregate and to specify how it should handle events. +We are building a minimal aggregate class here which only has an ID and mark this with the `Id` attribute. To make it easy to register with a name, we also add the `Aggregate` attribute. This is what it looks like: ```php @@ -44,15 +44,14 @@ final class Profile extends BasicAggregateRoot } } ``` - !!! warning The aggregate is not yet finished and has only been built to the point that you can instantiate the object. - + !!! tip Find out more about aggregate IDs [here](./aggregate_id.md). - + We use a so-called named constructor here to create an object of the AggregateRoot. The constructor itself is protected and cannot be called from outside. But it is possible to define different named constructors for different use-cases like `import`. @@ -76,23 +75,22 @@ final class CreateProfileHandler } } ``` - !!! warning If you look in the database now, you would see that nothing has been saved. This is because only events are stored in the database and as long as no events exist, nothing happens. - + !!! tip A **command bus** system is not necessary, only recommended. The interaction can also easily take place in a controller or service. - + ## Create a new aggregate In order that an aggregate is actually saved, at least one event must exist in the DB. For our aggregate we create the Event `ProfileRegistered` with an ID and a name. -Since the ID is a complex data type and cannot be easily serialized, we need to define a normalizer for the ID. +Since the ID is a complex data type and cannot be easily serialized, we need to define a normalizer for the ID. We do this with the `IdNormalizer` attribute. We also give the event a unique name using the `Event` attribute. @@ -111,12 +109,11 @@ final class ProfileRegistered ) {} } ``` - !!! note You can find out more about events [here](./events.md). And for normalizer [here](./normalizer.md). - + After we have defined the event, we have to adapt the profile aggregate: ```php @@ -154,23 +151,22 @@ final class Profile extends BasicAggregateRoot } } ``` - !!! tip Prefixing the apply methods with "apply" improves readability. - + In our named constructor `register` we have now created the event and recorded it with the method `recordThat`. The aggregate remembers all new recorded events in order to save them later. At the same time, a defined `apply` method is executed directly so that we can change our state. -So that the AggregateRoot also knows which method it should call, +So that the AggregateRoot also knows which method it should call, we have to mark it with the `Apply` attribute. We did that in the `applyProfileRegistered` method. In there we then change the state of the aggregate by filling the properties with the values from the event. !!! success The aggregate is now ready to be saved! - + ### Modify an aggregate In order to change the state of the aggregates afterwards, only further events have to be defined. @@ -188,12 +184,11 @@ final class NameChanged } } ``` - !!! note Events should best be written in the past, as they describe a state that has happened. - -After we have defined the event, we can define a new public method called `changeName` to change the profile name. + +After we have defined the event, we can define a new public method called `changeName` to change the profile name. This method then creates the event `NameChanged` and records it: ```php @@ -242,8 +237,7 @@ final class Profile extends BasicAggregateRoot } } ``` - -We have also defined a new `apply` method named `applyNameChanged` +We have also defined a new `apply` method named `applyNameChanged` where we change the name depending on the value in the event. When using it, it can look like this: @@ -270,15 +264,14 @@ final class ChangeNameHandler } } ``` - !!! success Our aggregate can now be changed and saved. - + !!! note You can read more about Repository [here](./repository.md). - + Here the aggregate is loaded from the `repository` by fetching all events from the database. These events are then executed again with the `apply` methods in order to rebuild the current state. All of this happens automatically in the `load` method. @@ -287,7 +280,7 @@ The method `changeName` is then executed on the aggregate to change the name. In this method the event `NameChanged` is generated and recorded. The `applyNameChanged` method was also called again internally to adjust the state. -When the `save` method is called on the repository, +When the `save` method is called on the repository, all newly recorded events are then fetched and written to the database. In this specific case only the `NameChanged` changed event. @@ -317,12 +310,11 @@ final class Profile extends BasicAggregateRoot } } ``` - ## Suppress missing apply methods -Sometimes you have events that do not change the state of the aggregate itself, -but are still recorded for the future or to subscribe for processor and projection. -So that you are not forced to write an apply method for it, +Sometimes you have events that do not change the state of the aggregate itself, +but are still recorded for the future or to subscribe for processor and projection. +So that you are not forced to write an apply method for it, you can suppress the missing apply exceptions these events with the `SuppressMissingApply` attribute. ```php @@ -345,7 +337,6 @@ final class Profile extends BasicAggregateRoot } } ``` - ## Suppress missing apply for all methods You can also completely deactivate the exceptions for missing apply methods. @@ -370,20 +361,19 @@ final class Profile extends BasicAggregateRoot } } ``` - !!! warning When all events are suppressed, debugging becomes more difficult if you forget an apply method. - + ## Business rules Usually, aggregates have business rules that must be observed. Like there may not be more than 10 people in a group. -These rules must be checked before an event is recorded. +These rules must be checked before an event is recorded. As soon as an event was recorded, the described thing happened and cannot be undone. -A further check in the apply method is also not possible because these events have already happened -and were then also saved in the database. +A further check in the apply method is also not possible because these events have already happened +and were then also saved in the database. In the next example we want to make sure that **the name is at least 3 characters long**: @@ -413,15 +403,14 @@ final class Profile extends BasicAggregateRoot } } ``` - !!! danger Validations during "apply" can brake the rebuilding of the aggregate. - -We have now ensured that this rule takes effect when a name is changed with the method `changeName`. + +We have now ensured that this rule takes effect when a name is changed with the method `changeName`. But when we create a new profile this rule does not currently apply. -In order for this to work, we either have to duplicate the rule or outsource it. +In order for this to work, we either have to duplicate the rule or outsource it. Here we show how we can do it all with a value object: ```php @@ -444,7 +433,6 @@ final class Name } } ``` - We can now use the value object `Name` in our aggregate: ```php @@ -488,8 +476,7 @@ final class Profile extends BasicAggregateRoot } } ``` - -In order for the whole thing to work, we still have to adapt our `NameChanged` event, +In order for the whole thing to work, we still have to adapt our `NameChanged` event, since we only expected a string before but now passed a `Name` value object. ```php @@ -507,18 +494,18 @@ final class NameChanged !!! warning The payload must be serializable and unserializable as json. - + !!! note You can find out more about normalizer [here](./normalizer.md). - + There are also cases where business rules have to be defined depending on the aggregate state. Sometimes also from states, which were changed in the same method. This is not a problem, as the `apply` methods are always executed immediately. In the next case we throw an exception if the hotel is already overbooked. -Besides that, we record another event `FullyBooked`, if the hotel is fully booked with the last booking. -With this event we could [notify](./subscription.md) external systems +Besides that, we record another event `FullyBooked`, if the hotel is fully booked with the last booking. +With this event we could [notify](./subscription.md) external systems or fill a [projection](./subscription.md) with fully booked hotels. ```php @@ -557,13 +544,12 @@ final class Hotel extends BasicAggregateRoot } } ``` - ## Working with dates -An aggregate should always be deterministic. In other words, whenever I execute methods on the aggregate, +An aggregate should always be deterministic. In other words, whenever I execute methods on the aggregate, I always get the same result. This also makes testing much easier. -But that often doesn't seem to be possible, e.g. if you want to save a createAt date. +But that often doesn't seem to be possible, e.g. if you want to save a createAt date. But you can pass this information by yourself. ```php @@ -592,7 +578,6 @@ final class Profile extends BasicAggregateRoot // ... } ``` - But if you still want to make sure that the time is "now" and not in the past or future, you can pass a clock. ```php @@ -622,14 +607,13 @@ final class Profile extends BasicAggregateRoot // ... } ``` - -Now you can pass the `SystemClock` to determine the current time. +Now you can pass the `SystemClock` to determine the current time. Or for test purposes the `FrozenClock`, which always returns the same time. !!! note You can find out more about clock [here](./clock.md). - + ## Aggregate Root Registry The library needs to know about all aggregates so that the correct aggregate class is used to load from the database. @@ -642,7 +626,6 @@ $aggregateRegistry = new AggregateRootRegistry([ 'profile' => Profile::class ]); ``` - So that you don't have to create it by hand, you can use a factory. By default, the `AttributeAggregateRootRegistryFactory` is used. There, with the help of paths, all classes with the attribute `Aggregate` are searched for @@ -654,11 +637,10 @@ use Patchlevel\EventSourcing\Metadata\AggregateRoot\AggregateRootRegistry; $aggregateRegistry = (new AttributeEventRegistryFactory())->create($paths); ``` - ## Learn more * [How to create own aggregate id](aggregate_id.md) * [How to store and load aggregates](repository.md) * [How to snapshot aggregates](snapshots.md) * [How to create Projections](subscription.md) -* [How to split streams](split_stream.md) \ No newline at end of file +* [How to split streams](split_stream.md) diff --git a/docs/pages/aggregate_id.md b/docs/pages/aggregate_id.md index 0f3fb76c3..19d551224 100644 --- a/docs/pages/aggregate_id.md +++ b/docs/pages/aggregate_id.md @@ -1,15 +1,15 @@ # Aggregate ID -The `aggregate id` is a unique identifier for an aggregate. +The `aggregate id` is a unique identifier for an aggregate. It is used to identify the aggregate in the event store. -The `aggregate` does not care how the id is generated, +The `aggregate` does not care how the id is generated, since only an aggregate-wide unique string is expected in the store. This library provides you with a few options for generating the id. ## Uuid -The easiest way is to use an `uuid` as an aggregate ID. +The easiest way is to use an `uuid` as an aggregate ID. For this, we have the `Uuid` class, which is a simple wrapper for the [ramsey/uuid](https://github.com/ramsey/uuid) library. You can use it like this: @@ -30,12 +30,11 @@ final class Profile extends BasicAggregateRoot private Uuid $id; } ``` - !!! note If you want to use snapshots, then you have to make sure that the aggregate id are normalized. You can find how to do this [here](normalizer.md). - + You have multiple options for generating an uuid: ```php @@ -45,15 +44,14 @@ $uuid = Uuid::v6(); $uuid = Uuid::v7(); $uuid = Uuid::fromString('d6e8d7a0-4b0b-4e6a-8a9a-3a0b2d9d0e4e'); ``` - !!! Note We offer you the uuid versions 6 and 7, because they are the most suitable for event sourcing. More information about uuid versions can be found [here](https://uuid.ramsey.dev/en/stable/rfc4122.html). - + ## Custom ID -If you don't want to use an uuid, you can also use the custom ID implementation. +If you don't want to use an uuid, you can also use the custom ID implementation. This is a value object that holds any string. ```php @@ -72,12 +70,11 @@ final class Profile extends BasicAggregateRoot private CustomId $id; } ``` - !!! note If you want to use snapshots, then you have to make sure that the aggregate id are normalized. You can find how to do this [here](normalizer.md). - + So you can use any string as an id: ```php @@ -85,7 +82,6 @@ use Patchlevel\EventSourcing\Aggregate\CustomId; $id = CustomId::fromString('my-id'); ``` - ## Implement own ID Or even better, you create your own aggregate-specific ID class. @@ -113,7 +109,6 @@ class ProfileId implements AggregateRootId } } ``` - So you can use it like this: ```php @@ -131,12 +126,11 @@ final class Profile extends BasicAggregateRoot private ProfileId $id; } ``` - !!! note If you want to use snapshots, then you have to make sure that the aggregate id are normalized. You can find how to do this [here](normalizer.md). - + We also offer you some traits, so that you don't have to implement the `AggregateRootId` interface yourself. Here for the uuid: @@ -150,7 +144,6 @@ class ProfileId implements AggregateRootId use RamseyUuidBehaviour; } ``` - Or for the custom id: ```php @@ -162,10 +155,9 @@ class ProfileId implements AggregateRootId use CustomIdBehaviour; } ``` - ### Learn more * [How to create an aggregate](aggregate.md) * [How to create an event](events.md) * [How to test an aggregate](testing.md) -* [How to normalize value objects](normalizer.md) \ No newline at end of file +* [How to normalize value objects](normalizer.md) diff --git a/docs/pages/cli.md b/docs/pages/cli.md index 34eeda166..7f7f996f6 100644 --- a/docs/pages/cli.md +++ b/docs/pages/cli.md @@ -26,7 +26,7 @@ The database schema can also be created, updated and dropped. !!! note You can also register doctrine migration commands. - + ## Subscription commands To manage your subscriptions there are the following cli commands. @@ -43,7 +43,7 @@ To manage your subscriptions there are the following cli commands. !!! note You can find out more about subscriptions [here](subscription.md). - + ## Inspector commands The inspector is a tool to inspect the event streams. @@ -90,7 +90,6 @@ $cli->addCommands(array( $cli->run(); ``` - ### Doctrine Migrations If you want to use doctrine migrations, you can register the commands like this: @@ -138,8 +137,8 @@ $cli->addCommands([ new Command\VersionCommand($dependencyFactory, 'event-sourcing:migrations:version'), ]); ``` - !!! note Here you can find more information on how to [configure doctrine migration](https://www.doctrine-project.org/projects/doctrine-migrations/en/3.3/reference/custom-configuration.html). + \ No newline at end of file diff --git a/docs/pages/clock.md b/docs/pages/clock.md index 41f884310..dde12a7bb 100644 --- a/docs/pages/clock.md +++ b/docs/pages/clock.md @@ -19,7 +19,6 @@ $date2 = $clock->now(); $date == $date2 // false $date === $date2 // false ``` - ## FrozenClock This implementation should only be used for the tests. This enables you to freeze the time and with that to have @@ -36,7 +35,6 @@ $frozenDate = $clock->now(); // gets the date provided before $date == $frozenDate // true $date === $frozenDate // false ``` - The `FrozenClock` can also be updated with a new date, so you can test a jump in time. ```php @@ -53,7 +51,6 @@ $frozenDate = $clock->now(); $firstDate == $frozenDate // false $secondDate == $frozenDate // true ``` - Or you can use the `sleep` method to simulate a time jump. ```php @@ -64,14 +61,13 @@ $clock = new FrozenClock($firstDate); $clock->sleep(10); // sleep 10 seconds ``` - !!! note The instance of the frozen datetime will be cloned internally, so the it's not the same instance but equals. - + ## Learn more * [How to test with datetime](testing.md) * [How to normalize datetime](normalizer.md) * [How to use messages](event_bus.md) -* [How to decorate messages](message_decorator.md) \ No newline at end of file +* [How to decorate messages](message_decorator.md) diff --git a/docs/pages/event_bus.md b/docs/pages/event_bus.md index 3c9627e62..32f7550f6 100644 --- a/docs/pages/event_bus.md +++ b/docs/pages/event_bus.md @@ -32,11 +32,10 @@ $message = Message::create(new NameChanged('foo')) $eventBus->dispatch($message); ``` - !!! note The message object is immutable. - + You don't have to create the message yourself, it is automatically created, saved and dispatched in the [repository](repository.md). @@ -52,11 +51,10 @@ $message = Message::create(new NameChanged('foo')) // ... ->withHeader('application-id', 'app'); ``` - !!! note You can read about how to pass additional headers to the message object in the [message decorator](message_decorator.md) docs. - + You can also access your custom headers. For this case there is also a method to only retrieve the headers which are not used internally. @@ -64,7 +62,6 @@ used internally. $message->header('application-id'); // app $message->customHeaders(); // ['application-id' => 'app'] ``` - If you want *all* the headers you can also retrieve them. ```php @@ -77,11 +74,10 @@ $message->headers(); 'application-id' => 'app' ] ``` - !!! warning Relying on internal meta data could be dangerous as they could be changed. So be cautios if you want to implement logic on them. - + ## Event Bus The event bus is responsible for dispatching the messages to the listeners. @@ -94,11 +90,10 @@ $eventBus = DefaultEventBus::create([ $mailListener, ]); ``` - !!! note The order in which the listeners are executed is determined by the order in which they are passed to the factory. - + Internally, the event bus uses the `Consumer` to consume the messages and call the listeners. ## Consumer @@ -114,7 +109,6 @@ $consumer = DefaultConsumer::create([ $consumer->consume($message); ``` - Internally, the consumer uses the `ListenerProvider` to find the listeners for the message. ## Listener provider @@ -135,11 +129,10 @@ $eventBus = new DefaultEventBus( new DefaultConsumer($listenerProvider) ); ``` - !!! tip The `DefaultEventBus::create` method uses the `DefaultConsumer` and `AttributeListenerProvider` by default. - + ### Custom listener provider You can also use your own listener provider. @@ -160,11 +153,10 @@ $listenerProvider = new class implements ListenerProvider { } }; ``` - !!! tip You can use `$listenerDiscriptor->name()` to get the name of the listener. - + ## Listener You can listen for specific events with the attribute `Subscribe`. @@ -184,11 +176,10 @@ final class WelcomeSubscriber } } ``` - !!! tip If you use psalm, you can use the [event sourcing plugin](https://github.com/patchlevel/event-sourcing-psalm-plugin) for better type support. - + ### Listen on all events If you want to listen on all events, you can pass `*` or `Subscribe::ALL` instead of the event class. @@ -207,7 +198,6 @@ final class WelcomeSubscriber } } ``` - ## Psr-14 Event Bus You can also use a [psr-14](https://www.php-fig.org/psr/psr-14/) compatible event bus. @@ -219,12 +209,10 @@ use Patchlevel\EventSourcing\EventBus\Psr14EventBus; $eventBus = new Psr14EventBus($psr14EventDispatcher); ``` - !!! warning You can't use the `Subscribe` attribute with the psr-14 event bus. - - + ## Learn more * [How to decorate messages](message_decorator.md) diff --git a/docs/pages/events.md b/docs/pages/events.md index 24c6d90ab..c66ea3154 100644 --- a/docs/pages/events.md +++ b/docs/pages/events.md @@ -1,17 +1,17 @@ # Events -Events are used to describe things that happened in the application. -Since the events already happened, they are also immnutable. -In event sourcing, these are used to save and rebuild the current state. +Events are used to describe things that happened in the application. +Since the events already happened, they are also immnutable. +In event sourcing, these are used to save and rebuild the current state. You can also listen on events to react and perform different actions. An event has a name and additional information called payload. Such an event can be represented as any class. -It is important that the payload can be serialized as JSON at the end. +It is important that the payload can be serialized as JSON at the end. Later it will be explained how to ensure it for all values. -To register an event you have to set the `Event` attribute over the class, -otherwise it will not be recognized as an event. +To register an event you have to set the `Event` attribute over the class, +otherwise it will not be recognized as an event. There you also have to give the event a name. ```php @@ -26,26 +26,25 @@ final class ProfileCreated ) {} } ``` - !!! warning The payload must be serializable and unserializable as json. - + !!! tip An event should be named in the past because it has already happened. - + Best practice is to prefix the event names with the aggregate name, lowercase everything, and replace spaces with underscores. Here are some examples: - + * `profile.created` * `profile.name_changed` * `hotel.guest_checked_out` - + ## Serializer So that the events can be saved in the database, they must be serialized and deserialized. -That's what the serializer is for. +That's what the serializer is for. The library comes with a `DefaultEventSerializer` that can be given further instructions using attributes. ```php @@ -53,15 +52,14 @@ use Patchlevel\EventSourcing\Serializer\DefaultEventSerializer; $serializer = DefaultEventSerializer::createFromPaths(['src/Domain']); ``` - -The serializer needs the path information where the event classes are located -so that it can instantiate the correct classes. +The serializer needs the path information where the event classes are located +so that it can instantiate the correct classes. Internally, an EventRegistry is used, which will be described later. ## Normalizer Sometimes you also want to add more complex data as a payload. For example DateTime or value objects. -You can do that too. However, you must define a normalizer for this +You can do that too. However, you must define a normalizer for this so that the library knows how to write this data to the database and load it again. ```php @@ -82,14 +80,13 @@ final class ProfileCreated ) {} } ``` - !!! note You can find out more about normalizer [here](normalizer.md). - + ## Event Registry -The library needs to know about all events +The library needs to know about all events so that the correct event class is used for the serialization and deserialization of an event. There is an EventRegistry for this purpose. The registry is a simple hashmap between event name and event class. @@ -100,10 +97,9 @@ $eventRegistry = new EventRegistry([ 'profile.created' => ProfileCreated::class ]); ``` - So that you don't have to create it by hand, you can use a factory. -By default, the `AttributeEventRegistryFactory` is used. -There, with the help of paths, all classes with the attribute `Event` are searched for +By default, the `AttributeEventRegistryFactory` is used. +There, with the help of paths, all classes with the attribute `Event` are searched for and the `EventRegistry` is built up. ```php @@ -112,7 +108,6 @@ use Patchlevel\EventSourcing\Metadata\Event\EventRegistry; $eventRegistry = (new AttributeEventRegistryFactory())->create($paths); ``` - ## Learn more * [How to normalize events](normalizer.md) @@ -120,4 +115,4 @@ $eventRegistry = (new AttributeEventRegistryFactory())->create($paths); * [How to listen on events](processor.md) * [How to store events](store.md) * [How to split streams](split_stream.md) -* [How to upcast events](upcasting.md) \ No newline at end of file +* [How to upcast events](upcasting.md) diff --git a/docs/pages/getting_started.md b/docs/pages/getting_started.md index 9318678d9..a412706ea 100644 --- a/docs/pages/getting_started.md +++ b/docs/pages/getting_started.md @@ -24,7 +24,6 @@ final class HotelCreated } } ``` - A guest can check in by `name`: ```php @@ -37,7 +36,6 @@ final class GuestIsCheckedIn } } ``` - And also check out again: ```php @@ -50,14 +48,13 @@ final class GuestIsCheckedOut } } ``` - !!! note You can find out more about events [here](events.md). - + ## Define aggregates -Next we need to define the hotel aggregate. +Next we need to define the hotel aggregate. How you can interact with it, which events happen and what the business rules are. For this we create the methods `create`, `checkIn` and `checkOut`. In these methods the business checks are made and the events are recorded. @@ -145,15 +142,14 @@ final class Hotel extends BasicAggregateRoot } } ``` - !!! note You can find out more about aggregates [here](aggregate.md). - + ## Define projections So that we can see all the hotels on our website and also see how many guests are currently visiting the hotels, -we need a projection for it. To create a projection we need a projector. +we need a projection for it. To create a projection we need a projector. Each projector is then responsible for a specific projection. ```php @@ -234,11 +230,10 @@ final class HotelProjector } } ``` - !!! note You can find out more about projector [here](subscription.md). - + ## Processor In our example we also want to email the head office as soon as a guest is checked in. @@ -267,11 +262,10 @@ final class SendCheckInEmailProcessor } } ``` - !!! note You can find out more about processor [here](subscription.md). - + ## Configuration After we have defined everything, we still have to plug the whole thing together: @@ -330,11 +324,10 @@ $repositoryManager = new DefaultRepositoryManager( $hotelRepository = $repositoryManager->get(Hotel::class); ``` - !!! note You can find out more about stores [here](store.md). - + ## Database setup So that we can actually write the data to a database, @@ -355,11 +348,10 @@ $schemaDirector = new DoctrineSchemaDirector( $schemaDirector->create(); $projectionist->setup(skipBooting: true); ``` - !!! note you can use the predefined [cli commands](cli.md) for this. - + ## Usage We are now ready to use the Event Sourcing System. We can load, change and save aggregates. @@ -382,25 +374,24 @@ $projectionist->run(); $hotels = $hotelProjection->getHotels(); ``` - !!! warning You need to run the projectionist to update the projections. - + !!! note You can also use other forms of IDs such as uuid version 6 or a custom format. You can find more about this [here](aggregate_id.md). - + ## Result !!! success We have successfully implemented and used event sourcing. - + Feel free to browse further in the documentation for more detailed information. If there are still open questions, create a ticket on Github and we will try to help you. - + ## Learn more * [How to create an aggregate](aggregate.md) @@ -408,4 +399,4 @@ $hotels = $hotelProjection->getHotels(); * [How to store aggregates](repository.md) * [How to process events](subscription.md) * [How to create a projection](subscription.md) -* [How to setup the database](store.md) \ No newline at end of file +* [How to setup the database](store.md) diff --git a/docs/pages/index.md b/docs/pages/index.md index c945298a1..0286f2066 100644 --- a/docs/pages/index.md +++ b/docs/pages/index.md @@ -20,8 +20,11 @@ A lightweight but also all-inclusive event sourcing library with a focus on deve ```bash composer require patchlevel/event-sourcing -``` + + + +``` ## Integration * [Symfony](https://github.com/patchlevel/event-sourcing-bundle) @@ -30,3 +33,4 @@ composer require patchlevel/event-sourcing !!! tip Start with the [quickstart](./getting_started.md) to get a feeling for the library. + \ No newline at end of file diff --git a/docs/pages/message_decorator.md b/docs/pages/message_decorator.md index 18bc850fd..34190953b 100644 --- a/docs/pages/message_decorator.md +++ b/docs/pages/message_decorator.md @@ -2,7 +2,7 @@ There are use-cases where you want to add some extra context to your events like metadata which is not directly relevant for your domain. With `MessageDecorator` we are providing a solution to add this metadata to your events. The metadata -will also be persisted in the database and can be retrieved later on. +will also be persisted in the database and can be retrieved later on. ## Built-in decorator @@ -19,7 +19,6 @@ use Patchlevel\EventSourcing\Repository\MessageDecorator\SplitStreamDecorator; $eventMetadataFactory = new AttributeEventMetadataFactory(); $decorator = new SplitStreamDecorator($eventMetadataFactory); ``` - ### ChainMessageDecorator To use multiple decorators at the same time, you can use the `ChainMessageDecorator`. @@ -32,10 +31,9 @@ $decorator = new ChainMessageDecorator([ $decorator2, ]); ``` - ## Use decorator -To use the message decorator, you have to pass it to the `DefaultRepositoryManager`, +To use the message decorator, you have to pass it to the `DefaultRepositoryManager`, which will then pass it to all Repositories. ```php @@ -57,11 +55,10 @@ $repositoryManager = new DefaultRepositoryManager( $repository = $repositoryManager->get(Profile::class); ``` - !!! note You can find out more about repository [here](repository). - + ## Create own decorator You can also use this feature to add your own metadata to your events. For this the have an extra methods on `Message` @@ -77,17 +74,16 @@ final class OnSystemRecordedDecorator implements MessageDecorator { return $message->withHeader('system', 'accounting_system'); } -} +} ``` - !!! note The Message is immutable, for more information look up [here](event_bus.md#message). - + !!! tip You can also set multiple headers with `withHeaders` which expects an hashmap. - + ## Learn more * [How to define events](events.md) diff --git a/docs/pages/normalizer.md b/docs/pages/normalizer.md index 5b86c5916..34bd859fc 100644 --- a/docs/pages/normalizer.md +++ b/docs/pages/normalizer.md @@ -9,7 +9,7 @@ how to write this data to the database and load it again. The underlying system called hydrator exists as a library. You can find out more details [here](https://github.com/patchlevel/hydrator). - + ## Usage You have to set the normalizer to the properties using the specific normalizer class. @@ -23,7 +23,6 @@ final class DTO public DateTimeImmutable $date; } ``` - The whole thing also works with property promotion and readonly properties. ```php @@ -37,7 +36,6 @@ final class DTO ) {} } ``` - ### Event For the event, the properties are normalized to a payload and saved in the DB at the end. @@ -57,7 +55,6 @@ final class CreateHotel ) {} } ``` - ### Aggregate For the aggregates it is very similar to the events. However, the normalizer is only used for the snapshots. @@ -80,11 +77,10 @@ final class Hotel extends BasicAggregateRoot // ... } ``` - !!! note You can learn more about snapshots [here](snapshots.md). - + ## Built-in Normalizer For some the standard cases we already offer built-in normalizers. @@ -106,11 +102,10 @@ final class DTO public array $dates; } ``` - !!! note The keys from the arrays are taken over here. - + ### DateTimeImmutable With the `DateTimeImmutable` Normalizer, as the name suggests, @@ -125,7 +120,6 @@ final class DTO public DateTimeImmutable $date; } ``` - You can also define the format. Either describe it yourself as a string or use one of the existing constants. The default is `DateTimeImmutable::ATOM`. @@ -138,11 +132,10 @@ final class DTO public DateTimeImmutable $date; } ``` - !!! note You can read about how the format is structured in the [php docs](https://www.php.net/manual/de/datetime.format.php). - + ### DateTime The `DateTime` Normalizer works exactly like the DateTimeNormalizer. Only for DateTime objects. @@ -156,7 +149,6 @@ final class DTO public DateTime $date; } ``` - You can also specify the format here. The default is `DateTime::ATOM`. ```php @@ -168,16 +160,15 @@ final class DTO public DateTime $date; } ``` - !!! warning It is highly recommended to only ever use DateTimeImmutable objects and the DateTimeImmutableNormalizer. This prevents you from accidentally changing the state of the DateTime and thereby causing bugs. - + !!! note You can read about how the format is structured in the [php docs](https://www.php.net/manual/de/datetime.format.php). - + ### DateTimeZone To normalize a `DateTimeZone` one can use the `DateTimeZoneNormalizer`. @@ -191,10 +182,9 @@ final class DTO public DateTimeZone $timeZone; } ``` - ### Enum -Backed enums can also be normalized. +Backed enums can also be normalized. ```php use Patchlevel\Hydrator\Normalizer\EnumNormalizer; @@ -205,7 +195,6 @@ final class DTO public Status $status; } ``` - You can also specify the enum class. ```php @@ -217,7 +206,6 @@ final class DTO public Status $status; } ``` - ### Id If you have your own AggregateRootId, you can use the `IdNormalizer`. @@ -232,7 +220,6 @@ final class DTO public Uuid $id; } ``` - Optional you can also define the type of the id. ```php @@ -245,7 +232,6 @@ final class DTO public Uuid $id; } ``` - ### Object If you have a complex object that you want to normalize, you can use the `ObjectNormalizer`. @@ -260,7 +246,6 @@ final class DTO public ComplexObject $object; } ``` - Optional you can also define the type of the object. ```php @@ -272,10 +257,9 @@ final class DTO public object $object; } ``` - ## Custom Normalizer -Since we only offer normalizers for PHP native things, +Since we only offer normalizers for PHP native things, you have to write your own normalizers for your own structures, such as value objects. In our example we have built a value object that should hold a name. @@ -300,7 +284,6 @@ final class Name } } ``` - For this we now need a custom normalizer. This normalizer must implement the `Normalizer` interface. You also need to implement a `normalize` and `denormalize` method. @@ -336,11 +319,10 @@ class NameNormalizer implements Normalizer } } ``` - !!! warning The important thing is that the result of Normalize is serializable! - + Now we can also use the normalizer directly. ```php @@ -350,11 +332,10 @@ final class DTO public Name $name } ``` - !!! tip Every normalizer, including the custom normalizer, can be used both for the events and for the snapshots. - + ## Normalized Name By default, the property name is used to name the field in the normalized result. @@ -369,7 +350,6 @@ final class DTO public string $name } ``` - The whole thing looks like this ```php @@ -377,17 +357,16 @@ The whole thing looks like this 'profile_name': 'David' ] ``` - !!! tip You can also rename properties to events without having a backwards compatibility break by keeping the serialized name. - + !!! note NormalizedName also works for snapshots. But since a snapshot is just a cache, you can also just invalidate it, if you have backwards compatibility break in the property name - + ## Ignore You can also ignore properties with the `Ignore` attribute. @@ -401,11 +380,10 @@ final class DTO public string $name } ``` - ## Learn more * [How to use the Hydrator](https://github.com/patchlevel/hydrator) * [How to define aggregates](aggregate.md) * [How to snapshot aggregates](snapshots.md) * [How to create own aggregate id](aggregate_id.md) -* [How to define events](events.md) \ No newline at end of file +* [How to define events](events.md) diff --git a/docs/pages/outbox.md b/docs/pages/outbox.md index 4404a538d..ca20220d5 100644 --- a/docs/pages/outbox.md +++ b/docs/pages/outbox.md @@ -1,17 +1,17 @@ # Outbox -There is the problem that errors can occur when saving an aggregate or in the individual event listeners. -This means that you either saved an aggregate, but an error occurred in the email listener, so that no email went out. +There is the problem that errors can occur when saving an aggregate or in the individual event listeners. +This means that you either saved an aggregate, but an error occurred in the email listener, so that no email went out. Or that an email was sent but the aggregate could not be saved. -Both cases are very bad and can only be solved if both the saving of an aggregate +Both cases are very bad and can only be solved if both the saving of an aggregate and the dispatching of the events are in a transaction. -The best way to ensure this is to store the events to be dispatched together +The best way to ensure this is to store the events to be dispatched together with the aggregate in a transaction in the same database. -After the transaction becomes successful, the events can be loaded from the outbox table with a worker -and then dispatched into the correct event bus. As soon as the events have been dispatched, +After the transaction becomes successful, the events can be loaded from the outbox table with a worker +and then dispatched into the correct event bus. As soon as the events have been dispatched, they are deleted from the outbox table. If an error occurs when dispatching, the whole thing will be retrieved later. ## Configuration @@ -29,10 +29,9 @@ $repositoryManager = new DefaultRepositoryManager( $aggregateRootRegistry, $store, $eventBus -); +); ``` - -And then you have to define the consumer. This gets the right event bus. +And then you have to define the consumer. This gets the right event bus. It is used to load the events to be dispatched from the database, dispatch the events and then empty the outbox table. ```php @@ -51,7 +50,6 @@ $processor = new StoreOutboxProcessor( $processor->process(); ``` - ## Using outbox So that this is also executed in a transaction, you have to make sure that a transaction has also been started. @@ -66,11 +64,10 @@ $store->transactional(function () use ($command, $profileRepository) { $profileRepository->save($profile); }); ``` - !!! note You can find out more about transaction [here](store.md#transaction). - + You can also interact directly with the outbox store. ```php @@ -80,11 +77,11 @@ $store->markOutboxMessageConsumed($message); $store->retrieveOutboxMessages(); $store->countOutboxMessages() ``` - !!! note Both single table store and multi table store implement the outbox store. - + !!! tip - Interacting with the outbox store is also possible via the [cli](cli.md). \ No newline at end of file + Interacting with the outbox store is also possible via the [cli](cli.md). + \ No newline at end of file diff --git a/docs/pages/pipeline.md b/docs/pages/pipeline.md index bbdcced14..8a181fe86 100644 --- a/docs/pages/pipeline.md +++ b/docs/pages/pipeline.md @@ -29,12 +29,11 @@ $pipeline = new Pipeline( ] ); ``` - !!! danger Under no circumstances may the same store be used that is used for the source. Otherwise the store will be broken afterwards! - + The pipeline can also be used to create or rebuild a projection: ```php @@ -47,8 +46,7 @@ $pipeline = new Pipeline( ConsumerTarget::create([$projection]), ); ``` - -The principle remains the same. +The principle remains the same. There is a source where the data comes from. A target where the data should flow. And any number of middlewares to do something with the data beforehand. @@ -66,7 +64,6 @@ use Patchlevel\EventSourcing\Pipeline\Source\StoreSource; $source = new StoreSource($store); ``` - ### In Memory There is an `InMemorySource` that receives the messages in an array. This source can be used to write pipeline tests. @@ -85,10 +82,9 @@ $source = new InMemorySource([ // ... ]); ``` - ### Custom Source -You can also create your own source class. It has to inherit from `Source`. +You can also create your own source class. It has to inherit from `Source`. Here you can, for example, create a migration from another event sourcing system or similar system. ```php @@ -115,7 +111,6 @@ $source = new class implements Source { } } ``` - ## Target After you have a source, you still need the destination of the pipeline. @@ -129,17 +124,16 @@ use Patchlevel\EventSourcing\Pipeline\Target\StoreTarget; $target = new StoreTarget($store); ``` - !!! danger Under no circumstances may the same store be used that is used for the source. Otherwise the store will be broken afterwards! - + !!! note It does not matter whether the previous store was a SingleTable or a MultiTable. You can switch back and forth between both store types using the pipeline. - + ### Consumer A consumer can also be used as a target. @@ -149,15 +143,14 @@ use Patchlevel\EventSourcing\Pipeline\Target\ConsumerTarget; $target = new ConsumerTarget($consumer); ``` - !!! tip You can also use it to build a new projection from scratch. - + !!! note More about the consumer can be found [here](event_bus.md). - + ### In Memory There is also an in-memory variant for the target. This target can also be used for tests. @@ -172,7 +165,6 @@ $target = new InMemoryTarget(); $messages = $target->messages(); ``` - ### Custom Target You can also define your own target. To do this, you need to implement the `Target` interface. @@ -195,7 +187,6 @@ final class OtherStoreTarget implements Target } } ``` - ## Middlewares Middelwares can be used to manipulate, delete or expand messages or events during the process. @@ -205,7 +196,7 @@ Middelwares can be used to manipulate, delete or expand messages or events durin It is important to know that some middlewares require recalculation from the playhead, if the target is a store. This is a numbering of the events that must be in ascending order. A corresponding note is supplied with every middleware. - + ### Exclude With this middleware you can exclude certain events. @@ -215,14 +206,12 @@ use Patchlevel\EventSourcing\Pipeline\Middleware\ExcludeEventMiddleware; $middleware = new ExcludeEventMiddleware([EmailChanged::class]); ``` - !!! warning After this middleware, the playhead must be recalculated! - + ### Include - With this middleware you can only allow certain events. ```php @@ -230,15 +219,14 @@ use Patchlevel\EventSourcing\Pipeline\Middleware\IncludeEventMiddleware; $middleware = new IncludeEventMiddleware([ProfileCreated::class]); ``` - !!! warning After this middleware, the playhead must be recalculated! - + ### Filter -If the middlewares `ExcludeEventMiddleware` and `IncludeEventMiddleware` are not sufficient, -you can also write your own filter. +If the middlewares `ExcludeEventMiddleware` and `IncludeEventMiddleware` are not sufficient, +you can also write your own filter. This middleware expects a callback that returns either true to allow events or false to not allow them. ```php @@ -253,11 +241,10 @@ $middleware = new FilterEventMiddleware(function (AggregateChanged $event) { return $event->allowNewsletter(); }); ``` - !!! warning After this middleware, the playhead must be recalculated! - + ### Exclude Archived Events With this middleware you can exclude archived events. @@ -267,11 +254,10 @@ use Patchlevel\EventSourcing\Pipeline\Middleware\ExcludeArchivedEventMiddleware; $middleware = new ExcludeArchivedEventMiddleware(); ``` - !!! warning After this middleware, the playhead must be recalculated! - + ### Only Archived Events With this middleware you can only allow archived events. @@ -281,11 +267,10 @@ use Patchlevel\EventSourcing\Pipeline\Middleware\OnlyArchivedEventMiddleware; $middleware = new OnlyArchivedEventMiddleware(); ``` - !!! warning After this middleware, the playhead must be recalculated! - + ### Replace If you want to replace an event, you can use the `ReplaceEventMiddleware`. @@ -299,11 +284,10 @@ $middleware = new ReplaceEventMiddleware(OldVisited::class, static function (Old return new NewVisited($oldVisited->profileId()); }); ``` - !!! note The middleware takes over the playhead and recordedAt information. - + ### Until A use case could also be that you want to look at the projection from a previous point in time. @@ -314,15 +298,14 @@ use Patchlevel\EventSourcing\Pipeline\Middleware\ClassRenameMiddleware; $middleware = new UntilEventMiddleware(new DateTimeImmutable('2020-01-01 12:00:00')); ``` - !!! warning After this middleware, the playhead must be recalculated! - + ### Recalculate playhead This middleware can be used to recalculate the playhead. -The playhead must always be in ascending order so that the data is valid. +The playhead must always be in ascending order so that the data is valid. Some middleware can break this order and the middleware `RecalculatePlayheadMiddleware` can fix this problem. ```php @@ -330,11 +313,10 @@ use Patchlevel\EventSourcing\Pipeline\Middleware\RecalculatePlayheadMiddleware; $middleware = new RecalculatePlayheadMiddleware(); ``` - !!! note You only need to add this middleware once at the end of the pipeline. - + ### Chain If you want to group your middleware, you can use one or more `ChainMiddleware`. @@ -349,10 +331,9 @@ $middleware = new ChainMiddleware([ new RecalculatePlayheadMiddleware() ]); ``` - ### Custom middleware -You can also write a custom middleware. The middleware gets a message and can return `N` messages. +You can also write a custom middleware. The middleware gets a message and can return `N` messages. There are the following possibilities: * Return only the message to an array to leave it unchanged. @@ -360,9 +341,9 @@ There are the following possibilities: * Return an empty array to remove the message. * Or return multiple messages to enrich the stream. -In our case, the domain has changed a bit. -In the beginning we had a `ProfileCreated` event that just created a profile. -Now we have a `ProfileRegistered` and a `ProfileActivated` event, +In our case, the domain has changed a bit. +In the beginning we had a `ProfileCreated` event that just created a profile. +Now we have a `ProfileRegistered` and a `ProfileActivated` event, which should replace the `ProfileCreated` event. ```php @@ -393,11 +374,11 @@ final class SplitProfileCreatedMiddleware implements Middleware } } ``` - !!! warning Since we changed the number of messages, we have to recalculate the playhead. - + !!! note - You can find more about messages [here](event_bus.md). \ No newline at end of file + You can find more about messages [here](event_bus.md). + \ No newline at end of file diff --git a/docs/pages/repository.md b/docs/pages/repository.md index dd3adf485..732ba0541 100644 --- a/docs/pages/repository.md +++ b/docs/pages/repository.md @@ -3,7 +3,7 @@ A `repository` takes care of storing and loading the `aggregates`. He is also responsible for building [messages](event_bus.md) from the events and then dispatching them to the event bus. -Every aggregate needs a repository to be stored. +Every aggregate needs a repository to be stored. And each repository is only responsible for one aggregate. ## Create a repository @@ -11,9 +11,9 @@ And each repository is only responsible for one aggregate. The best way to create a repository is to use the `DefaultRepositoryManager`. This helps to build the repository correctly. -The `DefaultRepositoryManager` needs some services to work. -For one, it needs [AggregateRootRegistry](aggregate.md#aggregate-root-registry) so that it knows which aggregates exist. -The [store](store.md), which is then given to the repository so that it can save and load the events at the end. +The `DefaultRepositoryManager` needs some services to work. +For one, it needs [AggregateRootRegistry](aggregate.md#aggregate-root-registry) so that it knows which aggregates exist. +The [store](store.md), which is then given to the repository so that it can save and load the events at the end. And the [EventBus](event_bus.md) to publish the new events. After plugging the `DefaultRepositoryManager` together, you can create the repository associated with the aggregate. @@ -29,14 +29,13 @@ $repositoryManager = new DefaultRepositoryManager( $repository = $repositoryManager->get(Profile::class); ``` - !!! note The same repository instance is always returned for a specific aggregate. - + ### Snapshots -Loading events for an aggregate is superfast. +Loading events for an aggregate is superfast. You can have thousands of events in the database that load in a few milliseconds and build the corresponding aggregate. But at some point you realize that it takes time. To counteract this there is a snapshot store. @@ -60,11 +59,10 @@ $repositoryManager = new DefaultRepositoryManager( $repository = $repositoryManager->get(Profile::class); ``` - !!! note You can find out more about snapshots [here](snapshots.md). - + ### Decorator If you want to add more metadata to the message, like e.g. an application id, then you can use decorators. @@ -84,22 +82,21 @@ $repositoryManager = new DefaultRepositoryManager( $repository = $repositoryManager->get(Profile::class); ``` - !!! note You can find out more about message decorator [here](message_decorator.md). - + ## Use the repository -Each `repository` has three methods that are responsible for loading an `aggregate`, +Each `repository` has three methods that are responsible for loading an `aggregate`, saving it or checking whether it exists. ### Save an aggregate -An `aggregate` can be `saved`. -All new events that have not yet been written to the database are fetched from the aggregate. -These events are then also append to the database. -After the events have been written, +An `aggregate` can be `saved`. +All new events that have not yet been written to the database are fetched from the aggregate. +These events are then also append to the database. +After the events have been written, the new events are dispatched on the [event bus](./event_bus.md). ```php @@ -110,19 +107,18 @@ $profile = Profile::create($id, 'david.badura@patchlevel.de'); $repository->save($profile); ``` - !!! note All events are written to the database with one transaction in order to ensure data consistency. - + !!! tip If you want to make sure that dispatching events and storing events is transaction safe, then you should look at the [outbox](outbox.md) pattern. - + ### Load an aggregate -An `aggregate` can be loaded using the `load` method. +An `aggregate` can be loaded using the `load` method. All events for the aggregate are loaded from the database and the current state is rebuilt. ```php @@ -131,19 +127,18 @@ use Patchlevel\EventSourcing\Aggregate\Uuid; $id = Uuid::fromString('229286ff-6f95-4df6-bc72-0a239fe7b284'); $profile = $repository->load($id); ``` - !!! warning When the method is called, the aggregate is always reloaded and rebuilt from the database. - + !!! note You can only fetch one aggregate at a time and don't do any complex queries either. Projections are used for this purpose. - + ### Has an aggregate -You can also check whether an `aggregate` with a certain id exists. +You can also check whether an `aggregate` with a certain id exists. It is checked whether any event with this id exists in the database. ```php @@ -153,12 +148,11 @@ if($repository->has($id)) { // ... } ``` - !!! note The query is fast and does not load any event. This means that the state of the aggregate is not rebuild either. - + ## Custom Repository In clean code you want to have explicit type hints for the repositories @@ -198,4 +192,4 @@ class ProfileRepository return $this->repository->has($id); } } -``` +``` \ No newline at end of file diff --git a/docs/pages/snapshots.md b/docs/pages/snapshots.md index fc33c026e..10e2c9349 100644 --- a/docs/pages/snapshots.md +++ b/docs/pages/snapshots.md @@ -1,21 +1,21 @@ # Snapshots -Some aggregates can have a large number of events. -This is not a problem if there are a few hundred. -But if the number gets bigger at some point, then loading and rebuilding can become slow. +Some aggregates can have a large number of events. +This is not a problem if there are a few hundred. +But if the number gets bigger at some point, then loading and rebuilding can become slow. The `snapshot` system can be used to control this. -Normally, the events are all executed again on the aggregate in order to rebuild the current state. +Normally, the events are all executed again on the aggregate in order to rebuild the current state. With a `snapshot`, we can shorten the way in which we temporarily save the current state of the aggregate. -When loading it is checked whether the snapshot exists. -If a hit exists, the aggregate is built up with the help of the snapshot. -A check is then made to see whether further events have existed since the snapshot -and these are then also executed on the aggregate. +When loading it is checked whether the snapshot exists. +If a hit exists, the aggregate is built up with the help of the snapshot. +A check is then made to see whether further events have existed since the snapshot +and these are then also executed on the aggregate. Here, however, only the last events are loaded from the database and not all. ## Configuration -First of all you have to define a snapshot store. This store may have multiple adapters for different caches. +First of all you have to define a snapshot store. This store may have multiple adapters for different caches. These caches also need a name so that you can determine which aggregates should be stored in which cache. ```php @@ -28,7 +28,6 @@ $snapshotStore = new DefaultSnapshotStore([ 'other_cache' => new Psr16SnapshotAdapter($otherCache), ]); ``` - After creating the snapshot store, you need to pass that store to the DefaultRepositoryManager. ```php @@ -43,12 +42,11 @@ $repositoryManager = new DefaultRepositoryManager( $snapshotStore ); ``` - !!! note You can read more about Repository [here](./repository.md). - -Next we need to tell the Aggregate to take a snapshot of it. We do this using the snapshot attribute. + +Next we need to tell the Aggregate to take a snapshot of it. We do this using the snapshot attribute. There we also specify where it should be saved. ```php @@ -63,11 +61,10 @@ final class Profile extends BasicAggregateRoot // ... } ``` - -When taking a snapshot, all properties are extracted and saved. -When loading, this data is written back to the properties. -In other words, in the end everything has to be serializable. -To ensure this, the same system is used as for the events. +When taking a snapshot, all properties are extracted and saved. +When loading, this data is written back to the properties. +In other words, in the end everything has to be serializable. +To ensure this, the same system is used as for the events. You can define normalizers to bring the properties into the correct format. ```php @@ -92,25 +89,24 @@ final class Profile extends BasicAggregateRoot // ... } ``` - !!! danger If anything changes in the properties of the aggregate, then the cache must be cleared. Or the snapshot version needs to be changed so that the previous snapshot is invalid. - + !!! warning In the end it the complete aggregate must be serializeable as json, also the aggregate Id. - + !!! note You can find more about normalizer [here](normalizer.md). - + ### Snapshot batching -Since the loading of events in itself is quite fast and only becomes noticeably slower with thousands of events, -we do not need to create a snapshot after each event. That would also have a negative impact on performance. -Instead, we can also create a snapshot after `N` events. +Since the loading of events in itself is quite fast and only becomes noticeably slower with thousands of events, +we do not need to create a snapshot after each event. That would also have a negative impact on performance. +Instead, we can also create a snapshot after `N` events. The remaining events that are not in the snapshot are then loaded from store. ```php @@ -125,14 +121,13 @@ final class Profile extends BasicAggregateRoot // ... } ``` - ### Snapshot versioning -Whenever something changes on the aggregate, the previous snapshot must be discarded. -You can do this by removing the entire snapshot cache when deploying. -But that can be quickly forgotten. It is much easier to specify a snapshot version. -This snapshot version is also saved. When loading, the versions are compared and if they do not match, -the snapshot is discarded and the aggregate is rebuilt from scratch. +Whenever something changes on the aggregate, the previous snapshot must be discarded. +You can do this by removing the entire snapshot cache when deploying. +But that can be quickly forgotten. It is much easier to specify a snapshot version. +This snapshot version is also saved. When loading, the versions are compared and if they do not match, +the snapshot is discarded and the aggregate is rebuilt from scratch. The new aggregate is then saved again as a snapshot. ```php @@ -147,22 +142,21 @@ final class Profile extends BasicAggregateRoot // ... } ``` - !!! warning If the snapshots are discarded, a load peak can occur since the aggregates have to be rebuilt. - + !!! tip You can also use uuids for the snapshot version. - + ## Adapter We offer a few `SnapshotAdapter` implementations that you can use. -But not a direct implementation of a cache. -There are many good libraries out there that address this problem, -and before we reinvent the wheel, choose one of them. -Since there is a psr-6 and psr-16 standard, there are plenty of libraries. +But not a direct implementation of a cache. +There are many good libraries out there that address this problem, +and before we reinvent the wheel, choose one of them. +Since there is a psr-6 and psr-16 standard, there are plenty of libraries. Here are a few listed: * [symfony cache](https://symfony.com/doc/current/components/cache.html) @@ -178,7 +172,6 @@ use Patchlevel\EventSourcing\Snapshot\Adapter\Psr6SnapshotAdapter; $adapter = new Psr6SnapshotAdapter($cache); ``` - ### psr16 A `Psr16SnapshotAdapter`, the associated documentation can be found [here](https://www.php-fig.org/psr/psr-16/). @@ -188,7 +181,6 @@ use Patchlevel\EventSourcing\Snapshot\Adapter\Psr16SnapshotAdapter; $adapter = new Psr16SnapshotAdapter($cache); ``` - ### in memory A `InMemorySnapshotAdapter` that can be used for test purposes. @@ -198,10 +190,9 @@ use Patchlevel\EventSourcing\Snapshot\Adapter\InMemorySnapshotAdapter; $adapter = new InMemorySnapshotAdapter(); ``` - ## Usage -The snapshot store is automatically used by the repository and takes care of saving and loading. +The snapshot store is automatically used by the repository and takes care of saving and loading. But you can also use the snapshot store yourself. ### Save @@ -211,12 +202,11 @@ This allows you to save the aggregate as a snapshot: ```php $snapshotStore->save($aggregate); ``` - !!! danger If the state of an aggregate is saved as a snapshot without being saved to the event store (database), it can lead to data loss or broken aggregates! - + ### Load You can also load an aggregate from the snapshot store: @@ -227,12 +217,12 @@ use Patchlevel\EventSourcing\Aggregate\Uuid; $id = Uuid::fromString('229286ff-6f95-4df6-bc72-0a239fe7b284'); $aggregate = $snapshotStore->load(Profile::class, $id); ``` - -The method returns the Aggregate if it was loaded successfully. -If the aggregate was not found, then a `SnapshotNotFound` is thrown. +The method returns the Aggregate if it was loaded successfully. +If the aggregate was not found, then a `SnapshotNotFound` is thrown. And if the version is no longer correct and the snapshot is therefore invalid, then a `SnapshotVersionInvalid` is thrown. !!! warning The aggregate may be in an old state as the snapshot may lag behind. - You still have to bring the aggregate up to date by loading the missing events from the event store. \ No newline at end of file + You still have to bring the aggregate up to date by loading the missing events from the event store. + \ No newline at end of file diff --git a/docs/pages/split_stream.md b/docs/pages/split_stream.md index cdc5f749b..6ec833a90 100644 --- a/docs/pages/split_stream.md +++ b/docs/pages/split_stream.md @@ -6,14 +6,14 @@ says if the user start a new subscription all past events should not be consider banking scenario. There the business decides to save the current state every quarter for each banking account. Not only that some businesses requires such an action it also increases the performance for aggregate which would have a -really long event stream. +really long event stream. ## Flagging an event to split the stream To use this feature you need to add the `SplitStreamDecorator`. You will also need events which will trigger this -action. For that you can use the `#[SplitStream]` attribute. We decided that we are not literallty splitting the stream, -instead we are marking all past events as archived as soon as this event is saved. Then the past events will not be -loaded anymore for building the aggregate. This means that all needed data has to be present in these events which +action. For that you can use the `#[SplitStream]` attribute. We decided that we are not literallty splitting the stream, +instead we are marking all past events as archived as soon as this event is saved. Then the past events will not be +loaded anymore for building the aggregate. This means that all needed data has to be present in these events which should trigger the event split. ```php @@ -34,12 +34,12 @@ final class MonthPassed } } ``` - !!! warning The event needs all data which is relevant the aggregate to be used since all past event will not be loaded! Keep this in mind if you want to use this feature. - + !!! note This archive flag only impacts the Store::load method which is used the build the aggregate from the stream. + \ No newline at end of file diff --git a/docs/pages/store.md b/docs/pages/store.md index 10a7b3549..1d4943eae 100644 --- a/docs/pages/store.md +++ b/docs/pages/store.md @@ -6,7 +6,7 @@ Each message contains an event and the associated headers. !!! note More information about the message can be found [here](event_bus.md). - + The store is optimized to efficiently store and load events for aggregates. We currently only offer one [doctrine dbal](https://www.doctrine-project.org/projects/dbal.html) store. @@ -21,12 +21,11 @@ $connection = DriverManager::getConnection([ 'url' => 'mysql://user:secret@localhost/app' ]); ``` - !!! note You can find out more about how to create a connection [here](https://www.doctrine-project.org/projects/doctrine-dbal/en/latest/reference/configuration.html) - + ## Configure Store You can create a store with the `DoctrineDbalStore` class. @@ -46,7 +45,6 @@ $store = new DoctrineDbalStore( 'eventstore' ); ``` - ## Schema The table structure of the `DoctrineDbalStore` looks like this: @@ -69,7 +67,7 @@ With the help of the `SchemaDirector`, the database structure can be created, up !!! tip You can also use doctrine [migration](migration.md) to create and keep your schema in sync. - + ### Schema Director The `SchemaDirector` is responsible for creating, updating and deleting the database schema. @@ -84,11 +82,10 @@ $schemaDirector = new DoctrineSchemaDirector( $store ); ``` - !!! note How to setup cli commands for schema director can be found [here](cli.md). - + #### Create schema You can create the table from scratch using the `create` method. @@ -96,14 +93,12 @@ You can create the table from scratch using the `create` method. ```php $schemaDirector->create(); ``` - Or can give you back which SQL statements would be necessary for this. Either for a dry run, or to define your own migrations. ```php $sql = $schemaDirector->dryRunCreate(); ``` - #### Update schema The update method compares the current state in the database and how the table should be structured. @@ -112,13 +107,11 @@ As a result, the diff is executed to bring the table to the desired state. ```php $schemaDirector->update(); ``` - Or can give you back which SQL statements would be necessary for this. ```php $sql = $schemaDirector->dryRunUpdate(); ``` - #### Drop schema You can also delete the table with the `drop` method. @@ -126,13 +119,11 @@ You can also delete the table with the `drop` method. ```php $schemaDirector->drop(); ``` - Or can give you back which SQL statements would be necessary for this. ```php $sql = $schemaDirector->dryRunDrop(); ``` - ### Doctrine Migrations You can use [doctrine migration](https://www.doctrine-project.org/projects/migrations.html), @@ -166,16 +157,15 @@ $dependencyFactory->setService( $schemaProvider ); ``` - !!! note Here you can find more information on how to [configure doctrine migration](https://www.doctrine-project.org/projects/doctrine-migrations/en/3.3/reference/custom-configuration.html). - + !!! note How to setup cli commands for doctrine migration can be found [here](cli.md). - + ## Usage The store has a few methods to interact with the database. @@ -188,7 +178,6 @@ This method returns a `Stream` object, which is a collection of events. ```php $stream = $store->load(); ``` - The load method also has a few parameters to filter, limit and sort the events. ```php @@ -201,7 +190,6 @@ $stream = $store->load( true, // latest first ); ``` - #### Criteria The `Criteria` object is used to filter the events. @@ -217,11 +205,10 @@ $criteria = new Criteria( archived: true, ); ``` - !!! note The individual criteria must all apply, but not all of them have to be set. - + #### Stream The load method returns a `Stream` object and is a generator. @@ -241,16 +228,15 @@ foreach ($stream as $message) { $message->event(); // get the event } ``` - !!! note You can find more information about the `Message` object [here](event_bus.md). - + !!! warning The stream cannot rewind, so you can only iterate over it once. If you want to iterate over it again, you have to call the `load` method again. - + ### Count You can count the number of events in the store with the `count` method. @@ -258,7 +244,6 @@ You can count the number of events in the store with the `count` method. ```php $count = $store->count(); ``` - The count method also has the possibility to filter the events. ```php @@ -268,7 +253,6 @@ $count = $store->count( new Criteria() // filter criteria ); ``` - ### Save You can save a message with the `save` method. @@ -278,11 +262,10 @@ $store->save($message); $store->save($message1, $message2, $message3); $store->save(...$messages); ``` - !!! note The saving happens in a transaction, so all messages are saved or none. - + ### Delete & Update It is not possible to delete or update events. @@ -304,5 +287,4 @@ $store->transactional(function () use ($command, $bankAccountRepository) { $bankAccountRepository->save($accountFrom); $bankAccountRepository->save($accountTo); }); -``` - +``` \ No newline at end of file diff --git a/docs/pages/subscription.md b/docs/pages/subscription.md index 3830f2bc1..9036c93f1 100644 --- a/docs/pages/subscription.md +++ b/docs/pages/subscription.md @@ -26,18 +26,17 @@ final class DoStuffSubscriber { } ``` - !!! note For each subsciber ID, the engine will create a subscription. If the subscriber ID changes, a new subscription will be created. In some cases like projections, you want to change the subscriber ID to rebuild the projection. - + !!! tip You can use specific attributes for specific subscribers like `Projector` or `Processor`. So you don't have to define the group and run mode every time. - + ### Projector You can create projections and read models with a subscriber. @@ -58,7 +57,6 @@ final class ProfileProjector } } ``` - Mostly you want process the events from the beginning. For this reason, it is also possible to use the `Projector` attribute. It extends the `Subscriber` attribute with a default group and run mode. @@ -77,22 +75,21 @@ final class ProfileProjector } } ``` - !!! warning MySQL and MariaDB don't support transactions for DDL statements. So you must use a different database connection for your subscriptions. - + !!! note More about the projector and projections can be found [here](projection.md). - + !!! tip Add a version as suffix to the subscriber id so you can increment it when the subscription changes. Like `profile_1` to `profile_2`. - + ### Processor The other way to react to events is to take actions like sending an email, dispatch commands or change other aggregates. @@ -112,8 +109,7 @@ final class WelcomeEmailProcessor } } ``` - -Mostly you want process the events from now, +Mostly you want process the events from now, because you don't want to email users who already have an account since a long time. For this reason, it is also possible to use the `Processor` attribute. @@ -133,17 +129,16 @@ final class WelcomeEmailProcessor } } ``` - !!! note More about the processor can be found [here](processor.md). - + ### Subscribe A subscriber (projector/processor) can subscribe any number of events. In order to say which method is responsible for which event, you need the `Subscribe` attribute. There you can pass the event class to which the reaction should then take place. -The method itself must expect a `Message`, which then contains the event. +The method itself must expect a `Message`, which then contains the event. The method name itself doesn't matter. ```php @@ -163,17 +158,16 @@ final class DoStuffSubscriber } } ``` - !!! note You can subscribe to multiple events on the same method or you can use "*" to subscribe to all events. More about this can be found [here](./event_bus.md#listener). - + !!! tip If you are using psalm then you can install the event sourcing [plugin](https://github.com/patchlevel/event-sourcing-psalm-plugin) to make the event method return the correct type. - + ### Setup and Teardown Subscribers can have one `setup` and `teardown` method that is executed when the subscription is created or deleted. @@ -213,25 +207,24 @@ final class ProfileProjector } } ``` - !!! danger MySQL and MariaDB don't support transactions for DDL statements. So you must use a different database connection in your projectors, otherwise you will get an error when the subscription tries to create the table. - + !!! warning If you change the subscriber id, you must also change the table/collection name. The subscription engine will create a new subscription with the new subscriber id. That means the setup method will be called again and the table/collection will conflict with the old existing projection. You can use the `SubscriberUtil` to build the table/collection name. - + !!! note Most databases have a limit on the length of the table/collection name. The limit is usually 64 characters. - + ### Versioning As soon as the structure of a projection changes, or you need other events from the past, @@ -248,18 +241,17 @@ final class ProfileSubscriber // ... } ``` - !!! warning If you change the `subscriberID`, you must also change the table/collection name. Otherwise the table/collection will conflict with the old subscription. - + !!! tip Add a version as suffix to the subscriber id so you can increment it when the subscription changes. Like `profile_1` to `profile_2`. - + ### Grouping You can also group subscribers together and filter them in the subscription engine. @@ -274,15 +266,14 @@ final class ProfileSubscriber // ... } ``` - !!! note The different attributes has different default group. - + * `Subscriber` - `default` * `Projector` - `projector` * `Processor` - `processor` - + ### Run Mode The run mode determines how the subscriber should behave. @@ -303,11 +294,10 @@ final class WelcomeEmailSubscriber // ... } ``` - !!! tip If you want create projections and run from the beginning, you can use the `Projector` attribute. - + #### From Now Certain subscribers operate exclusively on post-release events, disregarding historical data. @@ -323,11 +313,10 @@ final class WelcomeEmailSubscriber // ... } ``` - !!! tip If you want process events from now, you can use the `Processor` attribute. - + #### Once This mode is useful for subscribers that only need to run once. @@ -343,7 +332,6 @@ final class MigrationSubscriber // ... } ``` - ## Subscription Engine The subscription engine manages individual subscribers and keeps the subscriptions running. @@ -356,10 +344,10 @@ If something breaks, the subscription engine marks the individual subscriptions !!! tip The Subscription Engine was inspired by the following two blog posts: - + * [Projection Building Blocks: What you'll need to build projections](https://barryosull.com/blog/projection-building-blocks-what-you-ll-need-to-build-projections/) * [Managing projectors is harder than you think](https://barryosull.com/blog/managing-projectors-is-harder-than-you-think/) - + ## Subscription ID The subscription ID is taken from the associated subscriber and corresponds to the subscriber ID. @@ -377,7 +365,7 @@ So that the subscription engine knows where the subscription stopped and must co There is a lifecycle for each subscription. This cycle is tracked by the subscription engine. -``` mermaid +```mermaid stateDiagram-v2 direction LR [*] --> New @@ -404,8 +392,11 @@ stateDiagram-v2 Error --> [*] Detached --> Active Detached --> [*] -``` + + + +``` ### New A subscription is created and "new" if a subscriber exists with an ID that is not yet tracked. @@ -483,7 +474,6 @@ use Patchlevel\EventSourcing\Subscription\Store\DoctrineSubscriptionStore; $subscriptionStore = new DoctrineSubscriptionStore($connection); ``` - So that the schema for the subscription store can also be created, we have to tell the `DoctrineSchemaDirector` our schema configuration. Using `ChainDoctrineSchemaConfigurator` we can add multiple schema configurators. @@ -501,11 +491,10 @@ $schemaDirector = new DoctrineSchemaDirector( ]), ); ``` - !!! note You can find more about schema configurator [here](./store.md) - + ### Retry Strategy The subscription engine uses a retry strategy to retry subscriptions that have failed. @@ -524,11 +513,10 @@ $retryStrategy = new ClockBasedRetryStrategy( maxAttempts: 5, ); ``` - !!! tip You can reactivate the subscription manually or remove it and rebuild it from scratch. - + ### Subscriber Accessor The subscriber accessor repository is responsible for providing the subscribers to the subscription engine. @@ -539,11 +527,10 @@ use Patchlevel\EventSourcing\Subscription\Subscriber\MetadataSubscriberAccessorR $subscriberAccessorRepository = new MetadataSubscriberAccessorRepository([$subscriber1, $subscriber2, $subscriber3]); ``` - ### Subscription Engine Now we can create the subscription engine and plug together the necessary services. -The event store is needed to load the events, the Subscription Store to store the subscription state +The event store is needed to load the events, the Subscription Store to store the subscription state and we need the subscriber accessor repository. Optionally, we can also pass a retry strategy. ```php @@ -556,7 +543,6 @@ $subscriptionEngine = new DefaultSubscriptionEngine( $retryStrategy, ); ``` - ## Usage The Subscription Engine has a few methods needed to use it effectively. @@ -570,11 +556,10 @@ $criteria = new SubscriptionEngineCriteria( groups: ['default'] ); ``` - !!! note An `OR` check is made for the respective criteria and all criteria are checked with an `AND`. - + ### Setup New subscriptions need to be set up before they can be used. @@ -584,11 +569,10 @@ After the setup process, the subscription is set to booting or active. ```php $subscriptionEngine->setup($criteria); ``` - !!! tip You can skip the booting step with the second boolean parameter named `skipBooting`. - + ### Boot You can boot the subscriptions with the `boot` method. @@ -598,7 +582,6 @@ After the boot process, the subscription is set to active or finished. ```php $subscriptionEngine->boot($criteria); ``` - ### Run All active subscriptions are continued and updated here. @@ -606,7 +589,6 @@ All active subscriptions are continued and updated here. ```php $subscriptionEngine->run($criteria); ``` - ### Teardown If subscriptions are detached, they can be cleaned up here. @@ -615,7 +597,6 @@ The subscription engine also tries to call the `teardown` method if available. ```php $subscriptionEngine->teardown($criteria); ``` - ### Remove You can also directly remove a subscription regardless of its status. @@ -625,7 +606,6 @@ But the entry will still be removed if it doesn't work. ```php $subscriptionEngine->remove($criteria); ``` - ### Reactivate If a subscription had an error or is outdated, you can reactivate it. @@ -634,7 +614,6 @@ As a result, the subscription gets in the last status again. ```php $subscriptionEngine->reactivate($criteria); ``` - ### Pause Pausing a subscription is also possible. @@ -644,7 +623,6 @@ You can reactivate the subscription if you want so that it continues. ```php $subscriptionEngine->pause($criteria); ``` - ### Status To get the current status of all subscriptions, you can get them using the `subscriptions` method. @@ -656,10 +634,9 @@ foreach ($subscriptions as $subscription) { echo $subscription->status(); } ``` - ## Learn more * [How to use CLI commands](./cli.md) * [How to use Pipeline](./pipeline.md) * [How to use Event Bus](./event_bus.md) -* [How to Test](./testing.md) \ No newline at end of file +* [How to Test](./testing.md) diff --git a/docs/pages/testing.md b/docs/pages/testing.md index 36b34a952..bfdb2417f 100644 --- a/docs/pages/testing.md +++ b/docs/pages/testing.md @@ -1,7 +1,7 @@ # Tests -The aggregates can also be tested very well. -You can test whether certain events have been thrown +The aggregates can also be tested very well. +You can test whether certain events have been thrown or whether the state is set up correctly when the aggregate is set up again via the events. ```php @@ -44,4 +44,4 @@ final class ProfileTest extends TestCase self::assertEquals('bar@email.com', $profile->email()->toString()); } } -``` +``` \ No newline at end of file diff --git a/docs/pages/upcasting.md b/docs/pages/upcasting.md index ef2d42bac..32ad31811 100644 --- a/docs/pages/upcasting.md +++ b/docs/pages/upcasting.md @@ -28,11 +28,10 @@ final class ProfileCreatedEmailLowerCastUpcaster implements Upcaster } } ``` - !!! warning You need to consider that other events are passed to the Upcaster. So and early out is here endorsed. - + ## Adjust event name For the upgrade to 2.0.0 this feature is also really handy since we adjusted the event value from FQCN to an unique @@ -58,10 +57,9 @@ final class LegacyEventNameUpaster implements Upcaster } } ``` - ## Use upcasting -After we have defined the upcasting rules, we also have to pass the whole thing to the serializer. +After we have defined the upcasting rules, we also have to pass the whole thing to the serializer. Since we have multiple upcasters, we use a chain here. ```php @@ -78,7 +76,6 @@ $serializer = DefaultEventSerializer::createFromPaths( $upcaster ); ``` - ## Update event stream But what if we need it also in our stream because some other applications has also access on it? Or want to cleanup our @@ -107,12 +104,12 @@ final class EventStreamCleanupCommand extends Command $pipeline->run(); } ``` - !!! danger Under no circumstances may the same store be used that is used for the source. Otherwise the store will be broken afterwards! - + !!! note You can find out more about the pipeline [here](pipeline.md). + \ No newline at end of file From a12b2474c0e1c1290be38439c55c97ccf75d7b9b Mon Sep 17 00:00:00 2001 From: David Badura Date: Thu, 21 Mar 2024 15:28:51 +0100 Subject: [PATCH 3/7] fix php errors in docs --- docs/pages/cli.md | 10 +++++----- docs/pages/clock.md | 12 ++++++------ docs/pages/event_bus.md | 8 +++++--- docs/pages/getting_started.md | 4 ++-- docs/pages/index.md | 1 + docs/pages/normalizer.md | 18 +++++++++--------- docs/pages/outbox.md | 2 +- docs/pages/pipeline.md | 2 +- docs/pages/repository.md | 2 +- docs/pages/snapshots.md | 2 +- docs/pages/store.md | 4 ++-- docs/pages/subscription.md | 3 ++- docs/pages/upcasting.md | 15 +++++++++------ 13 files changed, 45 insertions(+), 38 deletions(-) diff --git a/docs/pages/cli.md b/docs/pages/cli.md index 7f7f996f6..9ed3de0a7 100644 --- a/docs/pages/cli.md +++ b/docs/pages/cli.md @@ -62,8 +62,8 @@ use Patchlevel\EventSourcing\Console\DoctrineHelper; use Patchlevel\EventSourcing\Schema\DoctrineSchemaManager; use Symfony\Component\Console\Application; -$store = /* define your doctrine store */; -$projectionist = /* create projectionist */; +$store;/* define your doctrine store */; +$projectionist;/* create projectionist */; $cli = new Application('Event-Sourcing CLI'); $cli->setCatchExceptions(true); @@ -104,15 +104,15 @@ use Symfony\Component\Console\Application; use Patchlevel\EventSourcing\Schema\DoctrineMigrationSchemaProvider; use Patchlevel\EventSourcing\Schema\DoctrineSchemaDirector; -$connection = /* create connection */ -$store = /* define your doctrine store */; +$connection; /* create connection */ +$store; /* define your doctrine store */ $schemaDirector = new DoctrineSchemaDirector( $store, $connection ); -$migrationConfig = /* define your migration config */; +$migrationConfig; /* define your migration config */ $dependencyFactory = DependencyFactory::fromConnection( diff --git a/docs/pages/clock.md b/docs/pages/clock.md index dde12a7bb..03778646c 100644 --- a/docs/pages/clock.md +++ b/docs/pages/clock.md @@ -16,8 +16,8 @@ $clock = new SystemClock(); $date = $clock->now(); // get the actual datetime $date2 = $clock->now(); -$date == $date2 // false -$date === $date2 // false +$date == $date2; // false +$date === $date2; // false ``` ## FrozenClock @@ -32,8 +32,8 @@ $date = new DateTimeImmutable(); $clock = new FrozenClock($date); $frozenDate = $clock->now(); // gets the date provided before -$date == $frozenDate // true -$date === $frozenDate // false +$date == $frozenDate; // true +$date === $frozenDate; // false ``` The `FrozenClock` can also be updated with a new date, so you can test a jump in time. @@ -48,8 +48,8 @@ $clock->update($secondDate); $frozenDate = $clock->now(); -$firstDate == $frozenDate // false -$secondDate == $frozenDate // true +$firstDate == $frozenDate; // false +$secondDate == $frozenDate; // true ``` Or you can use the `sleep` method to simulate a time jump. diff --git a/docs/pages/event_bus.md b/docs/pages/event_bus.md index 32f7550f6..c83a34555 100644 --- a/docs/pages/event_bus.md +++ b/docs/pages/event_bus.md @@ -65,14 +65,16 @@ $message->customHeaders(); // ['application-id' => 'app'] If you want *all* the headers you can also retrieve them. ```php -$message->headers(); -// results in: +$message->headers(); + +/* [ 'aggregateName' => 'profile', 'aggregateId' => '1', // {...}, 'application-id' => 'app' ] +*/ ``` !!! warning @@ -105,7 +107,7 @@ use Patchlevel\EventSourcing\EventBus\DefaultConsumer; $consumer = DefaultConsumer::create([ $mailListener, -]; +]); $consumer->consume($message); ``` diff --git a/docs/pages/getting_started.md b/docs/pages/getting_started.md index a412706ea..daa5374fb 100644 --- a/docs/pages/getting_started.md +++ b/docs/pages/getting_started.md @@ -136,7 +136,7 @@ final class Hotel extends BasicAggregateRoot $this->guests = array_values( array_filter( $this->guests, - fn ($name) => $name !== $event->guestName; + fn ($name) => $name !== $event->guestName ) ); } @@ -288,7 +288,7 @@ $projectionConnection = DriverManager::getConnection([ 'url' => 'mysql://user:secret@localhost/projection' ]); -$mailer = /* your own mailer */; +$mailer; $serializer = DefaultEventSerializer::createFromPaths(['src/Domain/Hotel/Event']); $aggregateRegistry = (new AttributeAggregateRootRegistryFactory)->create(['src/Domain/Hotel']); diff --git a/docs/pages/index.md b/docs/pages/index.md index 0286f2066..ff3a132db 100644 --- a/docs/pages/index.md +++ b/docs/pages/index.md @@ -24,6 +24,7 @@ composer require patchlevel/event-sourcing + ``` ## Integration diff --git a/docs/pages/normalizer.md b/docs/pages/normalizer.md index 34bd859fc..75711cd09 100644 --- a/docs/pages/normalizer.md +++ b/docs/pages/normalizer.md @@ -70,9 +70,9 @@ use Patchlevel\Hydrator\Normalizer\DateTimeImmutableNormalizer; #[Snapshot('default')] final class Hotel extends BasicAggregateRoot { - private string $name, + private string $name; #[DateTimeImmutableNormalizer] - private DateTimeImmutable $createAt + private DateTimeImmutable $createAt; // ... } @@ -329,7 +329,7 @@ Now we can also use the normalizer directly. final class DTO { #[NameNormalizer] - public Name $name + public Name $name; } ``` !!! tip @@ -347,15 +347,15 @@ use Patchlevel\Hydrator\Attribute\NormalizedName; final class DTO { #[NormalizedName('profile_name')] - public string $name + public string $name; } ``` The whole thing looks like this -```php -[ - 'profile_name': 'David' -] +```json +{ + "profile_name": "David" +} ``` !!! tip @@ -377,7 +377,7 @@ use Patchlevel\Hydrator\Attribute\Ignore; final class DTO { #[Ignore] - public string $name + public string $name; } ``` ## Learn more diff --git a/docs/pages/outbox.md b/docs/pages/outbox.md index ca20220d5..416edeb3d 100644 --- a/docs/pages/outbox.md +++ b/docs/pages/outbox.md @@ -75,7 +75,7 @@ $store->saveOutboxMessage($message); $store->markOutboxMessageConsumed($message); $store->retrieveOutboxMessages(); -$store->countOutboxMessages() +$store->countOutboxMessages(); ``` !!! note diff --git a/docs/pages/pipeline.md b/docs/pages/pipeline.md index 8a181fe86..a705c8187 100644 --- a/docs/pages/pipeline.md +++ b/docs/pages/pipeline.md @@ -109,7 +109,7 @@ $source = new class implements Source { { return 1; } -} +}; ``` ## Target diff --git a/docs/pages/repository.md b/docs/pages/repository.md index 732ba0541..13806c983 100644 --- a/docs/pages/repository.md +++ b/docs/pages/repository.md @@ -184,7 +184,7 @@ class ProfileRepository public function save(Profile $profile): void { - return $this->repository->save($profile); + $this->repository->save($profile); } public function has(ProfileId $id): bool diff --git a/docs/pages/snapshots.md b/docs/pages/snapshots.md index 10e2c9349..f7db738b0 100644 --- a/docs/pages/snapshots.md +++ b/docs/pages/snapshots.md @@ -82,7 +82,7 @@ final class Profile extends BasicAggregateRoot #[Id] #[IdNormalizer] public Uuid $id; - public string $name, + public string $name; #[Normalize(new DateTimeImmutableNormalizer())] public DateTimeImmutable $createdAt; diff --git a/docs/pages/store.md b/docs/pages/store.md index 1d4943eae..d71a73c74 100644 --- a/docs/pages/store.md +++ b/docs/pages/store.md @@ -184,8 +184,8 @@ The load method also has a few parameters to filter, limit and sort the events. use Patchlevel\EventSourcing\Store\Criteria; $stream = $store->load( - new Criteria() // filter criteria - 100 // limit + new Criteria(), // filter criteria + 100, // limit 50, // offset true, // latest first ); diff --git a/docs/pages/subscription.md b/docs/pages/subscription.md index 9036c93f1..509530eca 100644 --- a/docs/pages/subscription.md +++ b/docs/pages/subscription.md @@ -396,6 +396,7 @@ stateDiagram-v2 + ``` ### New @@ -484,7 +485,7 @@ use Patchlevel\EventSourcing\Schema\ChainDoctrineSchemaConfigurator; use Patchlevel\EventSourcing\Schema\DoctrineSchemaDirector; $schemaDirector = new DoctrineSchemaDirector( - $connection + $connection, new ChainDoctrineSchemaConfigurator([ $eventStore, $subscriptionStore diff --git a/docs/pages/upcasting.md b/docs/pages/upcasting.md index 32ad31811..7dc69a4f2 100644 --- a/docs/pages/upcasting.md +++ b/docs/pages/upcasting.md @@ -24,7 +24,7 @@ final class ProfileCreatedEmailLowerCastUpcaster implements Upcaster return $upcast; } - return $upcast->replacePayloadByKey('email', strtolower($upcast->payload['email']); + return $upcast->replacePayloadByKey('email', strtolower($upcast->payload['email'])); } } ``` @@ -89,20 +89,23 @@ final class EventStreamCleanupCommand extends Command protected static $defaultDescription = 'rebuild event stream'; public function __construct( - private readonly Store $sourceStore, - private readonly Store $targetStore, - ){ + private readonly Store $sourceStore, + private readonly Store $targetStore, + ) { } protected function execute(InputInterface $input, OutputInterface $output): int { $pipeline = new Pipeline( - new StoreSource($sourceStore), + new StoreSource($sourceStore), new StoreTarget($targetStore) ); - + $pipeline->run(); + + return 0; } +} ``` !!! danger From 9a2948ae1a9ad7fe2d08473b63541db2f324670d Mon Sep 17 00:00:00 2001 From: David Badura Date: Thu, 21 Mar 2024 15:40:34 +0100 Subject: [PATCH 4/7] fix --- .gitattributes | 1 + Makefile | 10 +++++++++- bin/docs-inject-php-code | 1 + docs/pages/index.md | 5 ----- docs/pages/subscription.md | 5 ----- 5 files changed, 11 insertions(+), 11 deletions(-) diff --git a/.gitattributes b/.gitattributes index 494375bf4..4a2defe0f 100644 --- a/.gitattributes +++ b/.gitattributes @@ -14,3 +14,4 @@ /psalm-baseline.xml export-ignore /test/ export-ignore /tools/ export-ignore +/bin/ export-ignore diff --git a/Makefile b/Makefile index faab8130a..8be23ad54 100644 --- a/Makefile +++ b/Makefile @@ -81,4 +81,12 @@ docs: mkdocs cd docs && python3 -m mkdocs serve mkdocs: ## run mkdocs - cd docs && pip3 install -r requirements.txt \ No newline at end of file + cd docs && pip3 install -r requirements.txt + +docs-extract-php: + bin/docs-extract-php-code + +docs-inject-php: + bin/docs-inject-php-code + +docs-format: docs-extract-php docs-inject-php \ No newline at end of file diff --git a/bin/docs-inject-php-code b/bin/docs-inject-php-code index 47ca77da5..f78030b64 100755 --- a/bin/docs-inject-php-code +++ b/bin/docs-inject-php-code @@ -42,6 +42,7 @@ foreach ($finder as $file) { */ foreach ($result as $node) { if ($node->getInfo() !== 'php') { + $node->setLiteral(trim($node->getLiteral())); continue; } diff --git a/docs/pages/index.md b/docs/pages/index.md index ff3a132db..b5789df1b 100644 --- a/docs/pages/index.md +++ b/docs/pages/index.md @@ -20,11 +20,6 @@ A lightweight but also all-inclusive event sourcing library with a focus on deve ```bash composer require patchlevel/event-sourcing - - - - - ``` ## Integration diff --git a/docs/pages/subscription.md b/docs/pages/subscription.md index 509530eca..374ea4301 100644 --- a/docs/pages/subscription.md +++ b/docs/pages/subscription.md @@ -392,11 +392,6 @@ stateDiagram-v2 Error --> [*] Detached --> Active Detached --> [*] - - - - - ``` ### New From 644d4507e6777fdafcff85798210b0312d14dd5c Mon Sep 17 00:00:00 2001 From: David Badura Date: Fri, 22 Mar 2024 09:49:51 +0100 Subject: [PATCH 5/7] fix code style in docs --- .github/workflows/docs-check.yml | 5 +- Makefile | 14 +++- bin/docs-extract-php-code | 6 +- bin/docs-inject-php-code | 10 ++- docs/pages/aggregate.md | 140 +++++++++++++++---------------- docs/pages/aggregate_id.md | 12 +-- docs/pages/cli.md | 27 +++--- docs/pages/clock.md | 12 +-- docs/pages/event_bus.md | 31 +++---- docs/pages/events.md | 13 +-- docs/pages/getting_started.md | 91 +++++++++----------- docs/pages/message_decorator.md | 6 +- docs/pages/normalizer.md | 77 ++++++----------- docs/pages/outbox.md | 20 ++--- docs/pages/pipeline.md | 58 +++++++------ docs/pages/repository.md | 28 +++---- docs/pages/snapshots.md | 5 +- docs/pages/store.md | 35 +++----- docs/pages/subscription.md | 38 +++++---- docs/pages/testing.md | 20 ++--- docs/pages/upcasting.md | 19 +++-- psalm_docs.xml | 24 ++++++ 22 files changed, 339 insertions(+), 352 deletions(-) create mode 100644 psalm_docs.xml diff --git a/.github/workflows/docs-check.yml b/.github/workflows/docs-check.yml index af5bf9ac4..727920f5b 100644 --- a/.github/workflows/docs-check.yml +++ b/.github/workflows/docs-check.yml @@ -44,4 +44,7 @@ jobs: run: "bin/docs-extract-php-code" - name: "lint php" - run: "php -l docs_php/*.php" \ No newline at end of file + run: "php -l docs_php/*.php" + + - name: "docs code style" + run: "vendor/bin/phpcbf docs_php --exclude=SlevomatCodingStandard.TypeHints.DeclareStrictTypes" \ No newline at end of file diff --git a/Makefile b/Makefile index 8be23ad54..eab1f00c0 100644 --- a/Makefile +++ b/Makefile @@ -80,13 +80,25 @@ dev: static test docs: mkdocs ## run mkdocs cd docs && python3 -m mkdocs serve +.PHONY: mkdocs mkdocs: ## run mkdocs cd docs && pip3 install -r requirements.txt +.PHONY: docs-extract-php docs-extract-php: bin/docs-extract-php-code +.PHONY: docs-inject-php docs-inject-php: bin/docs-inject-php-code -docs-format: docs-extract-php docs-inject-php \ No newline at end of file +.PHONY: docs-format +docs-format: docs-phpcs docs-inject-php + +.PHONY: docs-phpcs +docs-phpcs: docs-extract-php + vendor/bin/phpcbf docs_php --exclude=SlevomatCodingStandard.TypeHints.DeclareStrictTypes || true + +.PHONY: docs-psalm +docs-psalm: docs-extract-php + vendor/bin/psalm --config=psalm_docs.xml diff --git a/bin/docs-extract-php-code b/bin/docs-extract-php-code index 697b78a8d..b0364c2d8 100755 --- a/bin/docs-extract-php-code +++ b/bin/docs-extract-php-code @@ -38,16 +38,16 @@ foreach ($finder as $file) { /** * @var FencedCode $node */ - foreach ($result as $node) { + foreach ($result as $i => $node) { if ($node->getInfo() !== 'php') { continue; } $source = sprintf('%s:%s', $file->getRealPath(), $node->getStartLine()); - $code = "getLiteral() . "\n"; + $code = "getLiteral(); - $targetPath = $targetDir . '/' . $fileName . '_' . $node->getStartLine() . '.php'; + $targetPath = $targetDir . '/' . $fileName . '_' . $i . '.php'; file_put_contents($targetPath, $code); } } diff --git a/bin/docs-inject-php-code b/bin/docs-inject-php-code index f78030b64..8cd0e05f5 100755 --- a/bin/docs-inject-php-code +++ b/bin/docs-inject-php-code @@ -40,13 +40,19 @@ foreach ($finder as $file) { /** * @var FencedCode $node */ - foreach ($result as $node) { + foreach ($result as $i => $node) { if ($node->getInfo() !== 'php') { $node->setLiteral(trim($node->getLiteral())); continue; } - $targetPath = $targetDir . '/' . $fileName . '_' . $node->getStartLine() . '.php'; + $targetPath = $targetDir . '/' . $fileName . '_' . $i . '.php'; + + if (!file_exists($targetPath)) { + $node->setLiteral(trim($node->getLiteral())); + continue; + } + $code = file_get_contents($targetPath); $lines = explode("\n", $code); diff --git a/docs/pages/aggregate.md b/docs/pages/aggregate.md index 2e3d6aff8..7468022e7 100644 --- a/docs/pages/aggregate.md +++ b/docs/pages/aggregate.md @@ -34,12 +34,12 @@ final class Profile extends BasicAggregateRoot #[Id] private Uuid $id; - public static function register(Uuid $id): self + public static function register(Uuid $id): self { $self = new self(); - + $self->id = $id; // we need to set the id temporary here for the basic example and will be replaced later. - + return $self; } } @@ -64,13 +64,14 @@ use Patchlevel\EventSourcing\Repository\Repository; final class CreateProfileHandler { public function __construct( - private readonly Repository $profileRepository - ) {} - + private readonly Repository $profileRepository, + ) { + } + public function __invoke(CreateProfile $command): void { $profile = Profile::register($command->id()); - + $this->profileRepository->save($profile); } } @@ -105,8 +106,9 @@ final class ProfileRegistered public function __construct( #[IdNormalizer] public readonly Uuid $profileId, - public readonly string $name - ) {} + public readonly string $name, + ) { + } } ``` !!! note @@ -120,8 +122,8 @@ After we have defined the event, we have to adapt the profile aggregate: use Patchlevel\EventSourcing\Aggregate\BasicAggregateRoot; use Patchlevel\EventSourcing\Aggregate\Uuid; use Patchlevel\EventSourcing\Attribute\Aggregate; -use Patchlevel\EventSourcing\Attribute\Id; use Patchlevel\EventSourcing\Attribute\Apply; +use Patchlevel\EventSourcing\Attribute\Id; #[Aggregate('profile')] final class Profile extends BasicAggregateRoot @@ -129,8 +131,8 @@ final class Profile extends BasicAggregateRoot #[Id] private Uuid $id; private string $name; - - public function name(): string + + public function name(): string { return $this->name; } @@ -142,9 +144,9 @@ final class Profile extends BasicAggregateRoot return $self; } - + #[Apply] - protected function applyProfileRegistered(ProfileRegistered $event): void + protected function applyProfileRegistered(ProfileRegistered $event): void { $this->id = $event->profileId; $this->name = $event->name; @@ -179,7 +181,7 @@ use Patchlevel\EventSourcing\Attribute\Event; final class NameChanged { public function __construct( - public readonly string $name + public readonly string $name, ) { } } @@ -195,8 +197,8 @@ This method then creates the event `NameChanged` and records it: use Patchlevel\EventSourcing\Aggregate\BasicAggregateRoot; use Patchlevel\EventSourcing\Aggregate\Uuid; use Patchlevel\EventSourcing\Attribute\Aggregate; -use Patchlevel\EventSourcing\Attribute\Id; use Patchlevel\EventSourcing\Attribute\Apply; +use Patchlevel\EventSourcing\Attribute\Id; #[Aggregate('profile')] final class Profile extends BasicAggregateRoot @@ -205,7 +207,7 @@ final class Profile extends BasicAggregateRoot private Uuid $id; private string $name; - public function name(): string + public function name(): string { return $this->name; } @@ -217,19 +219,19 @@ final class Profile extends BasicAggregateRoot return $self; } - - public function changeName(string $name): void + + public function changeName(string $name): void { $this->recordThat(new NameChanged($name)); } - + #[Apply] protected function applyProfileRegistered(ProfileRegistered $event): void { $this->id = $event->profileId; $this->name = $event->name; } - + #[Apply] protected function applyNameChanged(NameChanged $event): void { @@ -243,23 +245,19 @@ where we change the name depending on the value in the event. When using it, it can look like this: ```php -use Patchlevel\EventSourcing\Aggregate\BasicAggregateRoot; use Patchlevel\EventSourcing\Repository\Repository; final class ChangeNameHandler { - private Repository $profileRepository; - - public function __construct(Repository $profileRepository) + public function __construct(private Repository $profileRepository) { - $this->profileRepository = $profileRepository; } - + public function __invoke(ChangeName $command): void { $profile = $this->profileRepository->load($command->id()); $profile->changeName($command->name()); - + $this->profileRepository->save($profile); } } @@ -297,7 +295,7 @@ use Patchlevel\EventSourcing\Attribute\Apply; final class Profile extends BasicAggregateRoot { // ... - + #[Apply(ProfileCreated::class)] #[Apply(NameChanged::class)] protected function applyProfileCreated(ProfileCreated|NameChanged $event): void @@ -305,7 +303,7 @@ final class Profile extends BasicAggregateRoot if ($event instanceof ProfileCreated) { $this->id = $event->profileId; } - + $this->name = $event->name; } } @@ -328,7 +326,7 @@ use Patchlevel\EventSourcing\Attribute\SuppressMissingApply; final class Profile extends BasicAggregateRoot { // ... - + #[Apply] protected function applyProfileCreated(ProfileCreated $event): void { @@ -352,7 +350,7 @@ use Patchlevel\EventSourcing\Attribute\SuppressMissingApply; final class Profile extends BasicAggregateRoot { // ... - + #[Apply] protected function applyProfileCreated(ProfileCreated $event): void { @@ -386,18 +384,18 @@ use Patchlevel\EventSourcing\Attribute\Apply; final class Profile extends BasicAggregateRoot { // ... - - public function changeName(string $name): void + + public function changeName(string $name): void { if (strlen($name) < 3) { throw new NameIsToShortException($name); } - + $this->recordThat(new NameChanged($name)); } - + #[Apply] - protected function applyNameChanged(NameChanged $event): void + protected function applyNameChanged(NameChanged $event): void { $this->name = $event->name(); } @@ -416,18 +414,14 @@ Here we show how we can do it all with a value object: ```php final class Name { - private string $value; - - public function __construct(string $value) + public function __construct(private string $value) { if (strlen($value) < 3) { throw new NameIsToShortException($value); } - - $this->value = $value; } - - public function toString(): string + + public function toString(): string { return $this->value; } @@ -439,8 +433,8 @@ We can now use the value object `Name` in our aggregate: use Patchlevel\EventSourcing\Aggregate\BasicAggregateRoot; use Patchlevel\EventSourcing\Aggregate\Uuid; use Patchlevel\EventSourcing\Attribute\Aggregate; -use Patchlevel\EventSourcing\Attribute\Id; use Patchlevel\EventSourcing\Attribute\Apply; +use Patchlevel\EventSourcing\Attribute\Id; #[Aggregate('profile')] final class Profile extends BasicAggregateRoot @@ -448,7 +442,7 @@ final class Profile extends BasicAggregateRoot #[Id] private Uuid $id; private Name $name; - + public static function register(Uuid $id, Name $name): static { $self = new static(); @@ -456,21 +450,21 @@ final class Profile extends BasicAggregateRoot return $self; } - + // ... - - public function name(): Name + + public function name(): Name { return $this->name; } - - public function changeName(Name $name): void + + public function changeName(Name $name): void { $this->recordThat(new NameChanged($name)); } - + #[Apply] - protected function applyNameChanged(NameChanged $event): void + protected function applyNameChanged(NameChanged $event): void { $this->name = $event->name; } @@ -487,8 +481,9 @@ final class NameChanged { public function __construct( #[NameNormalizer] - public readonly Name $name - ) {} + public readonly Name $name, + ) { + } } ``` !!! warning @@ -521,24 +516,26 @@ final class Hotel extends BasicAggregateRoot private const SIZE = 5; private int $people; - + // ... - - public function book(string $name): void + + public function book(string $name): void { if ($this->people === self::SIZE) { throw new NoPlaceException($name); } - + $this->recordThat(new RoomBocked($name)); - - if ($this->people === self::SIZE) { - $this->recordThat(new FullyBooked()); + + if ($this->people !== self::SIZE) { + return; } + + $this->recordThat(new FullyBooked()); } - + #[Apply] - protected function applyRoomBocked(RoomBocked $event): void + protected function applyRoomBocked(RoomBocked $event): void { $this->people++; } @@ -557,7 +554,6 @@ use Patchlevel\EventSourcing\Aggregate\BasicAggregateRoot; use Patchlevel\EventSourcing\Aggregate\Uuid; use Patchlevel\EventSourcing\Attribute\Aggregate; use Patchlevel\EventSourcing\Attribute\Id; -use Patchlevel\EventSourcing\Attribute\Apply; #[Aggregate('profile')] final class Profile extends BasicAggregateRoot @@ -566,7 +562,7 @@ final class Profile extends BasicAggregateRoot private Uuid $id; private Name $name; private DateTimeImmutable $registeredAt; - + public static function register(Uuid $id, string $name, DateTimeImmutable $registeredAt): static { $self = new static(); @@ -574,7 +570,7 @@ final class Profile extends BasicAggregateRoot return $self; } - + // ... } ``` @@ -585,7 +581,6 @@ use Patchlevel\EventSourcing\Aggregate\BasicAggregateRoot; use Patchlevel\EventSourcing\Aggregate\Uuid; use Patchlevel\EventSourcing\Attribute\Aggregate; use Patchlevel\EventSourcing\Attribute\Id; -use Patchlevel\EventSourcing\Attribute\Apply; use Patchlevel\EventSourcing\Clock\Clock; #[Aggregate('profile')] @@ -595,7 +590,7 @@ final class Profile extends BasicAggregateRoot private Uuid $id; private Name $name; private DateTimeImmutable $registeredAt; - + public static function register(Uuid $id, string $name, Clock $clock): static { $self = new static(); @@ -603,7 +598,7 @@ final class Profile extends BasicAggregateRoot return $self; } - + // ... } ``` @@ -623,7 +618,7 @@ There is an `AggregateRootRegistry` for this purpose. The registry is a simple h use Patchlevel\EventSourcing\Metadata\AggregateRoot\AggregateRootRegistry; $aggregateRegistry = new AggregateRootRegistry([ - 'profile' => Profile::class + 'profile' => Profile::class, ]); ``` So that you don't have to create it by hand, you can use a factory. @@ -632,9 +627,6 @@ There, with the help of paths, all classes with the attribute `Aggregate` are se and the `AggregateRootRegistry` is built up. ```php -use Patchlevel\EventSourcing\Metadata\AggregateRoot\AttributeAggregateRootRegistryFactory; -use Patchlevel\EventSourcing\Metadata\AggregateRoot\AggregateRootRegistry; - $aggregateRegistry = (new AttributeEventRegistryFactory())->create($paths); ``` ## Learn more diff --git a/docs/pages/aggregate_id.md b/docs/pages/aggregate_id.md index 19d551224..e1470ff38 100644 --- a/docs/pages/aggregate_id.md +++ b/docs/pages/aggregate_id.md @@ -18,7 +18,6 @@ You can use it like this: use Patchlevel\EventSourcing\Aggregate\BasicAggregateRoot; use Patchlevel\EventSourcing\Aggregate\Uuid; use Patchlevel\EventSourcing\Attribute\Aggregate; -use Patchlevel\EventSourcing\Attribute\Apply; use Patchlevel\EventSourcing\Attribute\Id; use Patchlevel\EventSourcing\Serializer\Normalizer\IdNormalizer; @@ -58,7 +57,6 @@ This is a value object that holds any string. use Patchlevel\EventSourcing\Aggregate\BasicAggregateRoot; use Patchlevel\EventSourcing\Aggregate\CustomId; use Patchlevel\EventSourcing\Attribute\Aggregate; -use Patchlevel\EventSourcing\Attribute\Apply; use Patchlevel\EventSourcing\Attribute\Id; use Patchlevel\EventSourcing\Serializer\Normalizer\IdNormalizer; @@ -94,16 +92,16 @@ use Patchlevel\EventSourcing\Aggregate\AggregateRootId; class ProfileId implements AggregateRootId { private function __construct( - private readonly string $id - ){ + private readonly string $id, + ) { } - public function toString(): string + public function toString(): string { return $this->id; } - public static function fromString(string $id): self + public static function fromString(string $id): self { return new self($id); } @@ -114,7 +112,6 @@ So you can use it like this: ```php use Patchlevel\EventSourcing\Aggregate\BasicAggregateRoot; use Patchlevel\EventSourcing\Attribute\Aggregate; -use Patchlevel\EventSourcing\Attribute\Apply; use Patchlevel\EventSourcing\Attribute\Id; use Patchlevel\EventSourcing\Serializer\Normalizer\IdNormalizer; @@ -137,7 +134,6 @@ Here for the uuid: ```php use Patchlevel\EventSourcing\Aggregate\AggregateRootId; use Patchlevel\EventSourcing\Aggregate\RamseyUuidBehaviour; -use Patchlevel\EventSourcing\Aggregate\Uuid; class ProfileId implements AggregateRootId { diff --git a/docs/pages/cli.md b/docs/pages/cli.md index 9ed3de0a7..d96b8c5c9 100644 --- a/docs/pages/cli.md +++ b/docs/pages/cli.md @@ -62,8 +62,10 @@ use Patchlevel\EventSourcing\Console\DoctrineHelper; use Patchlevel\EventSourcing\Schema\DoctrineSchemaManager; use Symfony\Component\Console\Application; -$store;/* define your doctrine store */; -$projectionist;/* create projectionist */; +$store; +/* define your doctrine store */ +$projectionist; +/* create projectionist */ $cli = new Application('Event-Sourcing CLI'); $cli->setCatchExceptions(true); @@ -71,7 +73,7 @@ $cli->setCatchExceptions(true); $doctrineHelper = new DoctrineHelper(); $schemaManager = new DoctrineSchemaManager(); -$cli->addCommands(array( +$cli->addCommands([ new Command\DatabaseCreateCommand($store, $doctrineHelper), new Command\DatabaseDropCommand($store, $doctrineHelper), new Command\SubscriptionBootCommand($projectionist), @@ -86,7 +88,7 @@ $cli->addCommands(array( new Command\SchemaCreateCommand($store, $schemaManager), new Command\SchemaDropCommand($store, $schemaManager), new Command\SchemaUpdateCommand($store, $schemaManager), -)); +]); $cli->run(); ``` @@ -95,12 +97,9 @@ $cli->run(); If you want to use doctrine migrations, you can register the commands like this: ```php -use Doctrine\DBAL\DriverManager; -use Doctrine\Migrations\DependencyFactory; -use Doctrine\Migrations\Configuration\Migration\PhpFile; use Doctrine\Migrations\Configuration\Connection\ExistingConnection; +use Doctrine\Migrations\DependencyFactory; use Doctrine\Migrations\Tools\Console\Command; -use Symfony\Component\Console\Application; use Patchlevel\EventSourcing\Schema\DoctrineMigrationSchemaProvider; use Patchlevel\EventSourcing\Schema\DoctrineSchemaDirector; @@ -109,24 +108,24 @@ $store; /* define your doctrine store */ $schemaDirector = new DoctrineSchemaDirector( $store, - $connection + $connection, ); $migrationConfig; /* define your migration config */ $dependencyFactory = DependencyFactory::fromConnection( - $migrationConfig, - new ExistingConnection($connection) + $migrationConfig, + new ExistingConnection($connection), ); $dependencyFactory->setService( - SchemaProvider::class, - new DoctrineMigrationSchemaProvider($schemaDirector) + SchemaProvider::class, + new DoctrineMigrationSchemaProvider($schemaDirector), ); -$cli->addCommands([ +$cli->addCommands([ new Command\ExecuteCommand($dependencyFactory, 'event-sourcing:migrations:execute'), new Command\GenerateCommand($dependencyFactory, 'event-sourcing:migrations:generate'), new Command\LatestCommand($dependencyFactory, 'event-sourcing:migrations:latest'), diff --git a/docs/pages/clock.md b/docs/pages/clock.md index 03778646c..1e7f42133 100644 --- a/docs/pages/clock.md +++ b/docs/pages/clock.md @@ -13,10 +13,10 @@ This uses the native system clock to return the `DateTimeImmutable` instance. use Patchlevel\EventSourcing\Clock\SystemClock; $clock = new SystemClock(); -$date = $clock->now(); // get the actual datetime +$date = $clock->now(); // get the actual datetime $date2 = $clock->now(); -$date == $date2; // false +$date === $date2; // false $date === $date2; // false ``` ## FrozenClock @@ -30,9 +30,9 @@ use Patchlevel\EventSourcing\Clock\FrozenClock; $date = new DateTimeImmutable(); $clock = new FrozenClock($date); -$frozenDate = $clock->now(); // gets the date provided before +$frozenDate = $clock->now(); // gets the date provided before -$date == $frozenDate; // true +$date === $frozenDate; // true $date === $frozenDate; // false ``` The `FrozenClock` can also be updated with a new date, so you can test a jump in time. @@ -48,8 +48,8 @@ $clock->update($secondDate); $frozenDate = $clock->now(); -$firstDate == $frozenDate; // false -$secondDate == $frozenDate; // true +$firstDate === $frozenDate; // false +$secondDate === $frozenDate; // true ``` Or you can use the `sleep` method to simulate a time jump. diff --git a/docs/pages/event_bus.md b/docs/pages/event_bus.md index c83a34555..a25e2c492 100644 --- a/docs/pages/event_bus.md +++ b/docs/pages/event_bus.md @@ -69,9 +69,9 @@ $message->headers(); /* [ - 'aggregateName' => 'profile', - 'aggregateId' => '1', - // {...}, + 'aggregateName' => 'profile', + 'aggregateId' => '1', + // {...}, 'application-id' => 'app' ] */ @@ -88,9 +88,7 @@ The library also delivers a light-weight event bus for which you can register li ```php use Patchlevel\EventSourcing\EventBus\DefaultEventBus; -$eventBus = DefaultEventBus::create([ - $mailListener, -]); +$eventBus = DefaultEventBus::create([$mailListener]); ``` !!! note @@ -105,9 +103,7 @@ The consumer is responsible for consuming the messages and calling the listeners ```php use Patchlevel\EventSourcing\EventBus\DefaultConsumer; -$consumer = DefaultConsumer::create([ - $mailListener, -]); +$consumer = DefaultConsumer::create([$mailListener]); $consumer->consume($message); ``` @@ -119,16 +115,14 @@ The listener provider is responsible for finding all listeners for a specific ev The default listener provider uses attributes to find the listeners. ```php +use Patchlevel\EventSourcing\EventBus\AttributeListenerProvider; use Patchlevel\EventSourcing\EventBus\DefaultConsumer; use Patchlevel\EventSourcing\EventBus\DefaultEventBus; -use Patchlevel\EventSourcing\EventBus\AttributeListenerProvider; -$listenerProvider = new AttributeListenerProvider([ - $mailListener, -]); +$listenerProvider = new AttributeListenerProvider([$mailListener]); $eventBus = new DefaultEventBus( - new DefaultConsumer($listenerProvider) + new DefaultConsumer($listenerProvider), ); ``` !!! tip @@ -140,9 +134,8 @@ $eventBus = new DefaultEventBus( You can also use your own listener provider. ```php -use Patchlevel\EventSourcing\EventBus\DefaultEventBus; -use Patchlevel\EventSourcing\EventBus\ListenerProvider; use Patchlevel\EventSourcing\EventBus\ListenerDescriptor; +use Patchlevel\EventSourcing\EventBus\ListenerProvider; $listenerProvider = new class implements ListenerProvider { public function listenersForEvent(string $eventClass): iterable @@ -166,10 +159,9 @@ This listener is then called for all saved events / messages. ```php use Patchlevel\EventSourcing\Attribute\Subscribe; -use Patchlevel\EventSourcing\EventBus\Listener; use Patchlevel\EventSourcing\Message\Message; -final class WelcomeSubscriber +final class WelcomeSubscriber { #[Subscribe(ProfileCreated::class)] public function onProfileCreated(Message $message): void @@ -188,10 +180,9 @@ If you want to listen on all events, you can pass `*` or `Subscribe::ALL` instea ```php use Patchlevel\EventSourcing\Attribute\Subscribe; -use Patchlevel\EventSourcing\EventBus\Listener; use Patchlevel\EventSourcing\Message\Message; -final class WelcomeSubscriber +final class WelcomeSubscriber { #[Subscribe('*')] public function onProfileCreated(Message $message): void diff --git a/docs/pages/events.md b/docs/pages/events.md index c66ea3154..581d2a3ec 100644 --- a/docs/pages/events.md +++ b/docs/pages/events.md @@ -22,8 +22,9 @@ final class ProfileCreated { public function __construct( public readonly string $profileId, - public readonly string $name - ) {} + public readonly string $name, + ) { + } } ``` !!! warning @@ -76,8 +77,9 @@ final class ProfileCreated #[NameNormalizer] public readonly Name $name, #[DateTimeImmutableNormalizer] - public readonly DateTimeImmutable $createdAt - ) {} + public readonly DateTimeImmutable $createdAt, + ) { + } } ``` !!! note @@ -94,7 +96,7 @@ There is an EventRegistry for this purpose. The registry is a simple hashmap bet use Patchlevel\EventSourcing\Metadata\Event\EventRegistry; $eventRegistry = new EventRegistry([ - 'profile.created' => ProfileCreated::class + 'profile.created' => ProfileCreated::class, ]); ``` So that you don't have to create it by hand, you can use a factory. @@ -104,7 +106,6 @@ and the `EventRegistry` is built up. ```php use Patchlevel\EventSourcing\Metadata\Event\AttributeEventRegistryFactory; -use Patchlevel\EventSourcing\Metadata\Event\EventRegistry; $eventRegistry = (new AttributeEventRegistryFactory())->create($paths); ``` diff --git a/docs/pages/getting_started.md b/docs/pages/getting_started.md index daa5374fb..f2fd6920f 100644 --- a/docs/pages/getting_started.md +++ b/docs/pages/getting_started.md @@ -19,7 +19,7 @@ final class HotelCreated public function __construct( #[IdNormalizer] public readonly Uuid $hotelId, - public readonly string $hotelName + public readonly string $hotelName, ) { } } @@ -31,7 +31,7 @@ A guest can check in by `name`: final class GuestIsCheckedIn { public function __construct( - public readonly string $guestName + public readonly string $guestName, ) { } } @@ -43,7 +43,7 @@ And also check out again: final class GuestIsCheckedOut { public function __construct( - public readonly string $guestName + public readonly string $guestName, ) { } } @@ -61,12 +61,11 @@ In these methods the business checks are made and the events are recorded. Last but not least, we need the associated apply methods to change the state. ```php -use Patchlevel\EventSourcing\Aggregate\AggregateChanged; use Patchlevel\EventSourcing\Aggregate\BasicAggregateRoot; use Patchlevel\EventSourcing\Aggregate\Uuid; use Patchlevel\EventSourcing\Attribute\Aggregate; -use Patchlevel\EventSourcing\Attribute\Id; use Patchlevel\EventSourcing\Attribute\Apply; +use Patchlevel\EventSourcing\Attribute\Id; #[Aggregate('hotel')] final class Hotel extends BasicAggregateRoot @@ -74,10 +73,8 @@ final class Hotel extends BasicAggregateRoot #[Id] private Uuid $id; private string $name; - - /** - * @var list - */ + + /** @var list */ private array $guests; public function name(): string @@ -103,41 +100,41 @@ final class Hotel extends BasicAggregateRoot if (in_array($guestName, $this->guests, true)) { throw new GuestHasAlreadyCheckedIn($guestName); } - + $this->recordThat(new GuestIsCheckedIn($guestName)); } - + public function checkOut(string $guestName): void { if (!in_array($guestName, $this->guests, true)) { throw new IsNotAGuest($guestName); } - + $this->recordThat(new GuestIsCheckedOut($guestName)); } - + #[Apply] - protected function applyHotelCreated(HotelCreated $event): void + protected function applyHotelCreated(HotelCreated $event): void { $this->id = $event->hotelId; $this->name = $event->hotelName; - $this->guests = []; + $this->guests = []; } - + #[Apply] - protected function applyGuestIsCheckedIn(GuestIsCheckedIn $event): void + protected function applyGuestIsCheckedIn(GuestIsCheckedIn $event): void { $this->guests[] = $event->guestName; } - + #[Apply] - protected function applyGuestIsCheckedOut(GuestIsCheckedOut $event): void + protected function applyGuestIsCheckedOut(GuestIsCheckedOut $event): void { $this->guests = array_values( array_filter( $this->guests, - fn ($name) => $name !== $event->guestName - ) + static fn ($name) => $name !== $event->guestName, + ), ); } } @@ -167,14 +164,12 @@ final class HotelProjector use SubscriberUtil; public function __construct( - private readonly Connection $db + private readonly Connection $db, ) { } - - /** - * @return list - */ - public function getHotels(): array + + /** @return list */ + public function getHotels(): array { return $this->db->fetchAllAssociative("SELECT id, name, guests FROM {$this->table()};"); } @@ -183,35 +178,35 @@ final class HotelProjector public function handleHotelCreated(Message $message): void { $event = $message->event(); - + $this->db->insert( - $this->table(), + $this->table(), [ - 'id' => $message->aggregateId(), + 'id' => $message->aggregateId(), 'name' => $event->hotelName, - 'guests' => 0 - ] + 'guests' => 0, + ], ); } - + #[Subscribe(GuestIsCheckedIn::class)] public function handleGuestIsCheckedIn(Message $message): void { $this->db->executeStatement( "UPDATE {$this->table()} SET guests = guests + 1 WHERE id = ?;", - [$message->aggregateId()] + [$message->aggregateId()], ); } - + #[Subscribe(GuestIsCheckedOut::class)] public function handleGuestIsCheckedOut(Message $message): void { $this->db->executeStatement( "UPDATE {$this->table()} SET guests = guests - 1 WHERE id = ?;", - [$message->aggregateId()] + [$message->aggregateId()], ); } - + #[Setup] public function create(): void { @@ -223,7 +218,7 @@ final class HotelProjector { $this->db->executeStatement("DROP TABLE IF EXISTS {$this->table()};"); } - + private function table(): string { return 'projection_' . $this->subscriberId(); @@ -247,7 +242,7 @@ use Patchlevel\EventSourcing\Message\Message; final class SendCheckInEmailProcessor { public function __construct( - private readonly Mailer $mailer + private readonly Mailer $mailer, ) { } @@ -257,7 +252,7 @@ final class SendCheckInEmailProcessor $this->mailer->send( 'hq@patchlevel.de', 'Guest is checked in', - sprintf('A new guest named "%s" is checked in', $message->event()->guestName) + sprintf('A new guest named "%s" is checked in', $message->event()->guestName), ); } } @@ -274,24 +269,20 @@ After we have defined everything, we still have to plug the whole thing together use Doctrine\DBAL\DriverManager; use Patchlevel\EventSourcing\EventBus\DefaultEventBus; use Patchlevel\EventSourcing\Projection\Engine\DefaultSubscriptionEngine; -use Patchlevel\EventSourcing\Projection\Subscriber\MetadataSubscriberAccessorRepository; use Patchlevel\EventSourcing\Projection\Store\DoctrineSubscriptionStore; +use Patchlevel\EventSourcing\Projection\Subscriber\MetadataSubscriberAccessorRepository; use Patchlevel\EventSourcing\Repository\DefaultRepositoryManager; use Patchlevel\EventSourcing\Serializer\DefaultEventSerializer; use Patchlevel\EventSourcing\Store\DoctrineDbalStore; -$connection = DriverManager::getConnection([ - 'url' => 'mysql://user:secret@localhost/app' -]); +$connection = DriverManager::getConnection(['url' => 'mysql://user:secret@localhost/app']); -$projectionConnection = DriverManager::getConnection([ - 'url' => 'mysql://user:secret@localhost/projection' -]); +$projectionConnection = DriverManager::getConnection(['url' => 'mysql://user:secret@localhost/projection']); $mailer; $serializer = DefaultEventSerializer::createFromPaths(['src/Domain/Hotel/Event']); -$aggregateRegistry = (new AttributeAggregateRootRegistryFactory)->create(['src/Domain/Hotel']); +$aggregateRegistry = (new AttributeAggregateRootRegistryFactory())->create(['src/Domain/Hotel']); $eventStore = new DoctrineDbalStore( $connection, @@ -341,8 +332,8 @@ $schemaDirector = new DoctrineSchemaDirector( $connection, new ChainDoctrineSchemaConfigurator([ $eventStore, - $projectionStore - ]) + $projectionStore, + ]), ); $schemaDirector->create(); diff --git a/docs/pages/message_decorator.md b/docs/pages/message_decorator.md index 34190953b..2abd3a0df 100644 --- a/docs/pages/message_decorator.md +++ b/docs/pages/message_decorator.md @@ -41,16 +41,14 @@ use Patchlevel\EventSourcing\Repository\DefaultRepositoryManager; use Patchlevel\EventSourcing\Repository\MessageDecorator\ChainMessageDecorator; use Patchlevel\EventSourcing\Repository\MessageDecorator\SplitStreamDecorator; -$decorator = new ChainMessageDecorator([ - new SplitStreamDecorator($eventMetadataFactory) -]); +$decorator = new ChainMessageDecorator([new SplitStreamDecorator($eventMetadataFactory)]); $repositoryManager = new DefaultRepositoryManager( $aggregateRootRegistry, $store, $eventBus, null, - $decorator + $decorator, ); $repository = $repositoryManager->get(Profile::class); diff --git a/docs/pages/normalizer.md b/docs/pages/normalizer.md index 75711cd09..34894520d 100644 --- a/docs/pages/normalizer.md +++ b/docs/pages/normalizer.md @@ -17,7 +17,7 @@ You have to set the normalizer to the properties using the specific normalizer c ```php use Patchlevel\Hydrator\Normalizer\DateTimeImmutableNormalizer; -final class DTO +final class DTO { #[DateTimeImmutableNormalizer] public DateTimeImmutable $date; @@ -28,12 +28,13 @@ The whole thing also works with property promotion and readonly properties. ```php use Patchlevel\Hydrator\Normalizer\DateTimeImmutableNormalizer; -final class DTO +final class DTO { public function __construct( #[DateTimeImmutableNormalizer] - public readonly DateTimeImmutable $date - ) {} + public readonly DateTimeImmutable $date, + ) { + } } ``` ### Event @@ -51,8 +52,9 @@ final class CreateHotel public function __construct( public readonly string $name, #[DateTimeImmutableNormalizer] - public readonly DateTimeImmutable $createAt - ) {} + public readonly DateTimeImmutable $createAt, + ) { + } } ``` ### Aggregate @@ -96,7 +98,7 @@ objects. Internally, it basically does an `array_map` and then runs the specifie use Patchlevel\Hydrator\Normalizer\ArrayNormalizer; use Patchlevel\Hydrator\Normalizer\DateTimeImmutableNormalizer; -final class DTO +final class DTO { #[ArrayNormalizer(new DateTimeImmutableNormalizer())] public array $dates; @@ -114,7 +116,7 @@ you can convert DateTimeImmutable objects to a String and back again. ```php use Patchlevel\Hydrator\Normalizer\DateTimeImmutableNormalizer; -final class DTO +final class DTO { #[DateTimeImmutableNormalizer] public DateTimeImmutable $date; @@ -126,7 +128,7 @@ The default is `DateTimeImmutable::ATOM`. ```php use Patchlevel\Hydrator\Normalizer\DateTimeImmutableNormalizer; -final class DTO +final class DTO { #[DateTimeImmutableNormalizer(format: DateTimeImmutable::RFC3339_EXTENDED)] public DateTimeImmutable $date; @@ -143,7 +145,7 @@ The `DateTime` Normalizer works exactly like the DateTimeNormalizer. Only for Da ```php use Patchlevel\Hydrator\Normalizer\DateTimeNormalizer; -final class DTO +final class DTO { #[DateTimeNormalizer] public DateTime $date; @@ -154,7 +156,7 @@ You can also specify the format here. The default is `DateTime::ATOM`. ```php use Patchlevel\Hydrator\Normalizer\DateTimeNormalizer; -final class DTO +final class DTO { #[DateTimeNormalizer(format: DateTime::RFC3339_EXTENDED)] public DateTime $date; @@ -176,7 +178,7 @@ To normalize a `DateTimeZone` one can use the `DateTimeZoneNormalizer`. ```php use Patchlevel\Hydrator\Normalizer\DateTimeZoneNormalizer; -final class DTO +final class DTO { #[DateTimeZoneNormalizer] public DateTimeZone $timeZone; @@ -189,7 +191,7 @@ Backed enums can also be normalized. ```php use Patchlevel\Hydrator\Normalizer\EnumNormalizer; -final class DTO +final class DTO { #[EnumNormalizer] public Status $status; @@ -200,7 +202,7 @@ You can also specify the enum class. ```php use Patchlevel\Hydrator\Normalizer\EnumNormalizer; -final class DTO +final class DTO { #[EnumNormalizer(Status::class)] public Status $status; @@ -214,7 +216,7 @@ If you have your own AggregateRootId, you can use the `IdNormalizer`. use Patchlevel\EventSourcing\Aggregate\Uuid; use Patchlevel\Hydrator\Normalizer\IdNormalizer; -final class DTO +final class DTO { #[IdNormalizer] public Uuid $id; @@ -226,7 +228,7 @@ Optional you can also define the type of the id. use Patchlevel\EventSourcing\Aggregate\Uuid; use Patchlevel\Hydrator\Normalizer\IdNormalizer; -final class DTO +final class DTO { #[IdNormalizer(Uuid::class)] public Uuid $id; @@ -240,7 +242,7 @@ Internally, it uses the `Hydrator` to normalize and denormalize the object. ```php use Patchlevel\Hydrator\Normalizer\ObjectNormalizer; -final class DTO +final class DTO { #[ObjectNormalizer] public ComplexObject $object; @@ -251,7 +253,7 @@ Optional you can also define the type of the object. ```php use Patchlevel\Hydrator\Normalizer\ObjectNormalizer; -final class DTO +final class DTO { #[ObjectNormalizer(ComplexObject::class)] public object $object; @@ -267,18 +269,14 @@ In our example we have built a value object that should hold a name. ```php final class Name { - private string $value; - - public function __construct(string $value) + public function __construct(private string $value) { if (strlen($value) < 3) { throw new NameIsToShortException($value); } - - $this->value = $value; } - - public function toString(): string + + public function toString(): string { return $this->value; } @@ -290,33 +288,12 @@ You also need to implement a `normalize` and `denormalize` method. Finally, you have to allow the normalizer to be used as an attribute. ```php -use Patchlevel\Hydrator\Normalizer\Normalizer; -use Patchlevel\Hydrator\Normalizer\InvalidArgument; +use Patchlevel\Hydrator\Normalizer\DateTimeImmutableNormalizer; -#[Attribute(Attribute::TARGET_PROPERTY)] -class NameNormalizer implements Normalizer +final class DTO { - public function normalize(mixed $value): string - { - if (!$value instanceof Name) { - throw InvalidArgument::withWrongType(Name::class, $value); - } - - return $value->toString(); - } - - public function denormalize(mixed $value): ?Name - { - if ($value === null) { - return null; - } - - if (!is_string($value)) { - throw InvalidArgument::withWrongType('string', $value); - } - - return new Name($value); - } + #[DateTimeImmutableNormalizer] + public DateTimeImmutable $date; } ``` !!! warning diff --git a/docs/pages/outbox.md b/docs/pages/outbox.md index 416edeb3d..141b612b0 100644 --- a/docs/pages/outbox.md +++ b/docs/pages/outbox.md @@ -20,15 +20,15 @@ First you have to replace the correct event bus with an outbox event bus. This stores the events to be dispatched in the database. ```php -use Patchlevel\EventSourcing\Repository\DefaultRepositoryManager; use Patchlevel\EventSourcing\Outbox\OutboxEventBus; +use Patchlevel\EventSourcing\Repository\DefaultRepositoryManager; $eventBus = new OutboxEventBus($store); $repositoryManager = new DefaultRepositoryManager( $aggregateRootRegistry, $store, - $eventBus + $eventBus, ); ``` And then you have to define the consumer. This gets the right event bus. @@ -39,13 +39,11 @@ use Patchlevel\EventSourcing\EventBus\DefaultConsumer; use Patchlevel\EventSourcing\Outbox\EventBusPublisher; use Patchlevel\EventSourcing\Outbox\StoreOutboxProcessor; -$consumer = DefaultConsumer::create([ - $mailListener, -]); +$consumer = DefaultConsumer::create([$mailListener]); $processor = new StoreOutboxProcessor( - $store, - new EventBusPublisher($consumer) + $store, + new EventBusPublisher($consumer), ); $processor->process(); @@ -55,12 +53,12 @@ $processor->process(); So that this is also executed in a transaction, you have to make sure that a transaction has also been started. ```php -$store->transactional(function () use ($command, $profileRepository) { +$store->transactional(static function () use ($command, $profileRepository): void { $profile = Profile::register( $command->id(), - $command->email() + $command->email(), ); - + $profileRepository->save($profile); }); ``` @@ -74,7 +72,7 @@ You can also interact directly with the outbox store. $store->saveOutboxMessage($message); $store->markOutboxMessageConsumed($message); -$store->retrieveOutboxMessages(); +$store->retrieveOutboxMessages(); $store->countOutboxMessages(); ``` !!! note diff --git a/docs/pages/pipeline.md b/docs/pages/pipeline.md index a705c8187..de57940f3 100644 --- a/docs/pages/pipeline.md +++ b/docs/pages/pipeline.md @@ -26,7 +26,7 @@ $pipeline = new Pipeline( return new NewVisited($oldVisited->profileId()); }), new RecalculatePlayheadMiddleware(), - ] + ], ); ``` !!! danger @@ -92,16 +92,14 @@ use Patchlevel\EventSourcing\Message\Message; use Patchlevel\EventSourcing\Pipeline\Source\Source; $source = new class implements Source { - /** - * @return Generator - */ + /** @return Generator */ public function load(): Generator { yield new Message( Profile::class, '1', - 0, - new ProfileCreated('1', ['name' => 'David']) + 0, + new ProfileCreated('1', ['name' => 'David']), ); } @@ -174,11 +172,8 @@ use Patchlevel\EventSourcing\Message\Message; final class OtherStoreTarget implements Target { - private OtherStore $store; - - public function __construct(OtherStore $store) + public function __construct(private OtherStore $store) { - $this->store = $store; } public function save(Message $message): void @@ -233,11 +228,11 @@ This middleware expects a callback that returns either true to allow events or f use Patchlevel\EventSourcing\Aggregate\AggregateChanged; use Patchlevel\EventSourcing\Pipeline\Middleware\FilterEventMiddleware; -$middleware = new FilterEventMiddleware(function (AggregateChanged $event) { +$middleware = new FilterEventMiddleware(static function (AggregateChanged $event) { if (!$event instanceof ProfileCreated) { return true; } - + return $event->allowNewsletter(); }); ``` @@ -250,9 +245,24 @@ $middleware = new FilterEventMiddleware(function (AggregateChanged $event) { With this middleware you can exclude archived events. ```php -use Patchlevel\EventSourcing\Pipeline\Middleware\ExcludeArchivedEventMiddleware; +use Patchlevel\EventSourcing\Pipeline\Middleware\ExcludeEventMiddleware; +use Patchlevel\EventSourcing\Pipeline\Middleware\RecalculatePlayheadMiddleware; +use Patchlevel\EventSourcing\Pipeline\Middleware\ReplaceEventMiddleware; +use Patchlevel\EventSourcing\Pipeline\Pipeline; +use Patchlevel\EventSourcing\Pipeline\Source\StoreSource; +use Patchlevel\EventSourcing\Pipeline\Target\StoreTarget; -$middleware = new ExcludeArchivedEventMiddleware(); +$pipeline = new Pipeline( + new StoreSource($oldStore), + new StoreTarget($newStore), + [ + new ExcludeEventMiddleware([PrivacyAdded::class]), + new ReplaceEventMiddleware(OldVisited::class, static function (OldVisited $oldVisited) { + return new NewVisited($oldVisited->profileId()); + }), + new RecalculatePlayheadMiddleware(), + ], +); ``` !!! warning @@ -294,8 +304,6 @@ A use case could also be that you want to look at the projection from a previous You can use the `UntilEventMiddleware` to only allow events that were `recorded` before this point in time. ```php -use Patchlevel\EventSourcing\Pipeline\Middleware\ClassRenameMiddleware; - $middleware = new UntilEventMiddleware(new DateTimeImmutable('2020-01-01 12:00:00')); ``` !!! warning @@ -328,7 +336,7 @@ use Patchlevel\EventSourcing\Pipeline\Middleware\RecalculatePlayheadMiddleware; $middleware = new ChainMiddleware([ new ExcludeEventMiddleware([EmailChanged::class]), - new RecalculatePlayheadMiddleware() + new RecalculatePlayheadMiddleware(), ]); ``` ### Custom middleware @@ -355,23 +363,23 @@ final class SplitProfileCreatedMiddleware implements Middleware public function __invoke(Message $message): array { $event = $message->event(); - + if (!$event instanceof ProfileCreated) { return [$message]; } - + $profileRegisteredMessage = Message::createWithHeaders( - new ProfileRegistered($event->id(), $event->name()), - $message->headers() + new ProfileRegistered($event->id(), $event->name()), + $message->headers(), ); - + $profileActivatedMessage = Message::createWithHeaders( - new ProfileActivated($event->id()), - $message->headers() + new ProfileActivated($event->id()), + $message->headers(), ); return [$profileRegisteredMessage, $profileActivatedMessage]; - } + } } ``` !!! warning diff --git a/docs/pages/repository.md b/docs/pages/repository.md index 13806c983..1e177b3ff 100644 --- a/docs/pages/repository.md +++ b/docs/pages/repository.md @@ -24,7 +24,7 @@ use Patchlevel\EventSourcing\Repository\DefaultRepositoryManager; $repositoryManager = new DefaultRepositoryManager( $aggregateRootRegistry, $store, - $eventBus + $eventBus, ); $repository = $repositoryManager->get(Profile::class); @@ -46,15 +46,13 @@ use Patchlevel\EventSourcing\Snapshot\Adapter\Psr16SnapshotAdapter; use Patchlevel\EventSourcing\Snapshot\DefaultSnapshotStore; $adapter = new Psr16SnapshotAdapter($cache); -$snapshotStore = new DefaultSnapshotStore([ - 'default' => $adapter -]); +$snapshotStore = new DefaultSnapshotStore(['default' => $adapter]); $repositoryManager = new DefaultRepositoryManager( $aggregateRootRegistry, $store, $eventBus, - $snapshotStore + $snapshotStore, ); $repository = $repositoryManager->get(Profile::class); @@ -77,7 +75,7 @@ $repositoryManager = new DefaultRepositoryManager( $store, $eventBus, null, - $decorator + $decorator, ); $repository = $repositoryManager->get(Profile::class); @@ -144,7 +142,7 @@ It is checked whether any event with this id exists in the database. ```php $id = Uuid::fromString('229286ff-6f95-4df6-bc72-0a239fe7b284'); -if($repository->has($id)) { +if ($repository->has($id)) { // ... } ``` @@ -167,27 +165,27 @@ This also gives you more type security. use Patchlevel\EventSourcing\Repository\Repository; use Patchlevel\EventSourcing\Repository\RepositoryManager; -class ProfileRepository +class ProfileRepository { /** @var Repository */ private Repository $repository; - public function __construct(RepositoryManager $repositoryManager) + public function __construct(RepositoryManager $repositoryManager) { $this->repository = $repositoryManager->get(Profile::class); } - - public function load(ProfileId $id): Profile + + public function load(ProfileId $id): Profile { return $this->repository->load($id); } - - public function save(Profile $profile): void + + public function save(Profile $profile): void { $this->repository->save($profile); } - - public function has(ProfileId $id): bool + + public function has(ProfileId $id): bool { return $this->repository->has($id); } diff --git a/docs/pages/snapshots.md b/docs/pages/snapshots.md index f7db738b0..d79662b02 100644 --- a/docs/pages/snapshots.md +++ b/docs/pages/snapshots.md @@ -19,7 +19,6 @@ First of all you have to define a snapshot store. This store may have multiple a These caches also need a name so that you can determine which aggregates should be stored in which cache. ```php -use Patchlevel\EventSourcing\Repository\DefaultRepositoryManager; use Patchlevel\EventSourcing\Snapshot\Adapter\Psr16SnapshotAdapter; use Patchlevel\EventSourcing\Snapshot\DefaultSnapshotStore; @@ -39,7 +38,7 @@ $repositoryManager = new DefaultRepositoryManager( $aggregateRootRegistry, $store, $eventBus, - $snapshotStore + $snapshotStore, ); ``` !!! note @@ -85,7 +84,7 @@ final class Profile extends BasicAggregateRoot public string $name; #[Normalize(new DateTimeImmutableNormalizer())] public DateTimeImmutable $createdAt; - + // ... } ``` diff --git a/docs/pages/store.md b/docs/pages/store.md index d71a73c74..46cec7ae6 100644 --- a/docs/pages/store.md +++ b/docs/pages/store.md @@ -17,9 +17,7 @@ The first thing we need for our store is a DBAL connection: ```php use Doctrine\DBAL\DriverManager; -$connection = DriverManager::getConnection([ - 'url' => 'mysql://user:secret@localhost/app' -]); +$connection = DriverManager::getConnection(['url' => 'mysql://user:secret@localhost/app']); ``` !!! note @@ -40,9 +38,9 @@ $store = new DoctrineDbalStore( $connection, DefaultEventSerializer::createFromPaths(['src/Event']), new AggregateRootRegistry([ - 'profile' => Profile::class + 'profile' => Profile::class, ]), - 'eventstore' + 'eventstore', ); ``` ## Schema @@ -79,7 +77,7 @@ use Patchlevel\EventSourcing\Schema\DoctrineSchemaDirector; $schemaDirector = new DoctrineSchemaDirector( $connection, - $store + $store, ); ``` !!! note @@ -140,7 +138,7 @@ use Patchlevel\EventSourcing\Schema\DoctrineSchemaDirector; $schemaDirector = new DoctrineSchemaDirector( $store, - $connection + $connection, ); $schemaProvider = new DoctrineMigrationSchemaProvider($schemaDirector); @@ -148,13 +146,13 @@ $schemaProvider = new DoctrineMigrationSchemaProvider($schemaDirector); // doctrine migration configuration $dependencyFactory = DependencyFactory::fromConnection( - $config, - new ExistingConnection($connection) + $config, + new ExistingConnection($connection), ); $dependencyFactory->setService( - SchemaProvider::class, - $schemaProvider + SchemaProvider::class, + $schemaProvider, ); ``` !!! note @@ -250,7 +248,7 @@ The count method also has the possibility to filter the events. use Patchlevel\EventSourcing\Store\Criteria; $count = $store->count( - new Criteria() // filter criteria + new Criteria(), // filter criteria ); ``` ### Save @@ -277,14 +275,7 @@ There is also the possibility of executing a function in a transaction. Then dbal takes care of starting a transaction, committing it and then possibly rollback it again. ```php -$store->transactional(function () use ($command, $bankAccountRepository) { - $accountFrom = $bankAccountRepository->get($command->from()); - $accountTo = $bankAccountRepository->get($command->to()); - - $accountFrom->transferMoney($command->to(), $command->amount()); - $accountTo->receiveMoney($command->from(), $command->amount()); - - $bankAccountRepository->save($accountFrom); - $bankAccountRepository->save($accountTo); -}); +use Doctrine\DBAL\DriverManager; + +$connection = DriverManager::getConnection(['url' => 'mysql://user:secret@localhost/app']); ``` \ No newline at end of file diff --git a/docs/pages/subscription.md b/docs/pages/subscription.md index 374ea4301..6f2296526 100644 --- a/docs/pages/subscription.md +++ b/docs/pages/subscription.md @@ -45,14 +45,13 @@ We named this type of subscriber `projector`. But in the end it's the same. ```php use Doctrine\DBAL\Connection; use Patchlevel\EventSourcing\Attribute\Subscriber; -use Patchlevel\EventSourcing\Subscription\Subscriber\SubscriberUtil; use Patchlevel\EventSourcing\Subscription\RunMode; #[Subscriber('profile_1', RunMode::FromBeginning)] final class ProfileProjector { public function __construct( - private readonly Connection $connection + private readonly Connection $connection, ) { } } @@ -64,13 +63,12 @@ It extends the `Subscriber` attribute with a default group and run mode. ```php use Doctrine\DBAL\Connection; use Patchlevel\EventSourcing\Attribute\Projector; -use Patchlevel\EventSourcing\Subscription\Subscriber\SubscriberUtil; #[Projector('profile_1')] final class ProfileProjector { public function __construct( - private readonly Connection $connection + private readonly Connection $connection, ) { } } @@ -97,14 +95,13 @@ We named this type of subscriber `processor`. ```php use Patchlevel\EventSourcing\Attribute\Subscriber; -use Patchlevel\EventSourcing\Subscription\Subscriber\SubscriberUtil; use Patchlevel\EventSourcing\Subscription\RunMode; #[Subscriber('welcome_email', RunMode::FromNow)] final class WelcomeEmailProcessor { public function __construct( - private readonly Mailer $mailer + private readonly Mailer $mailer, ) { } } @@ -118,13 +115,12 @@ It extends the `Subscriber` attribute with a default group and run mode. ```php use Doctrine\DBAL\Connection; use Patchlevel\EventSourcing\Attribute\Processor; -use Patchlevel\EventSourcing\Subscription\Subscriber\SubscriberUtil; #[Processor('welcome_email')] final class WelcomeEmailProcessor { public function __construct( - private readonly Connection $connection + private readonly Connection $connection, ) { } } @@ -153,7 +149,7 @@ final class DoStuffSubscriber public function onProfileCreated(Message $message): void { $profileCreated = $message->event(); - + // do something } } @@ -175,23 +171,23 @@ For this there are the attributes `Setup` and `Teardown`. The method name itself This is especially helpful for projectors, as they can create the necessary structures for the projection here. ```php +use Patchlevel\EventSourcing\Attribute\Projector; use Patchlevel\EventSourcing\Attribute\Setup; use Patchlevel\EventSourcing\Attribute\Teardown; -use Patchlevel\EventSourcing\Attribute\Projector; use Patchlevel\EventSourcing\Subscription\Subscriber\SubscriberUtil; #[Projector('profile_1')] final class ProfileProjector { use SubscriberUtil; - + // ... #[Setup] public function create(): void { $this->connection->executeStatement( - "CREATE TABLE IF NOT EXISTS {$this->table()} (id VARCHAR PRIMARY KEY, name VARCHAR NOT NULL);" + "CREATE TABLE IF NOT EXISTS {$this->table()} (id VARCHAR PRIMARY KEY, name VARCHAR NOT NULL);", ); } @@ -201,7 +197,7 @@ final class ProfileProjector $this->connection->executeStatement("DROP TABLE IF EXISTS {$this->table()};"); } - private function table(): string + private function table(): string { return 'projection_' . $this->subscriberId(); } @@ -233,7 +229,6 @@ This will trigger the subscription engine to create a new subscription and boot ```php use Patchlevel\EventSourcing\Attribute\Projector; -use Patchlevel\EventSourcing\Attribute\Subscriber; #[Projector('profile_2')] final class ProfileSubscriber @@ -305,7 +300,8 @@ This is useful for subscribers that are only interested in events that occur aft As example, a welcome email subscriber that only wants to send emails to new users. ```php -use Patchlevel\EventSourcing\Attribute\Subscriber;use Patchlevel\EventSourcing\Subscription\RunMode; +use Patchlevel\EventSourcing\Attribute\Subscriber; +use Patchlevel\EventSourcing\Subscription\RunMode; #[Subscriber('welcome_email', RunMode::FromNow)] final class WelcomeEmailSubscriber @@ -483,7 +479,7 @@ $schemaDirector = new DoctrineSchemaDirector( $connection, new ChainDoctrineSchemaConfigurator([ $eventStore, - $subscriptionStore + $subscriptionStore, ]), ); ``` @@ -549,7 +545,7 @@ use Patchlevel\EventSourcing\Subscription\Engine\SubscriptionEngineCriteria; $criteria = new SubscriptionEngineCriteria( ids: ['profile_1', 'welcome_email'], - groups: ['default'] + groups: ['default'], ); ``` !!! note @@ -576,7 +572,13 @@ All booting subscriptions will catch up to the current event stream. After the boot process, the subscription is set to active or finished. ```php -$subscriptionEngine->boot($criteria); +use Patchlevel\EventSourcing\Attribute\Subscriber; +use Patchlevel\EventSourcing\Subscription\RunMode; + +#[Subscriber('do_stuff', RunMode::Once)] +final class DoStuffSubscriber +{ +} ``` ### Run diff --git a/docs/pages/testing.md b/docs/pages/testing.md index bfdb2417f..1886e3eea 100644 --- a/docs/pages/testing.md +++ b/docs/pages/testing.md @@ -15,30 +15,30 @@ final class ProfileTest extends TestCase $profile = Profile::createProfile($id, Email::fromString('foo@email.com')); self::assertEquals( - $profile->releaseEvents(), + $profile->releaseEvents(), [ - new ProfileCreated($id, Email::fromString('foo@email.com')), - ] + new ProfileCreated($id, Email::fromString('foo@email.com')), + ], ); self::assertEquals('foo@email.com', $profile->email()->toString()); } - + public function testChangeName(): void { $id = ProfileId::generate(); - + $profile = Profile::createFromEvents([ new ProfileCreated($id, Email::fromString('foo@email.com')), ]); - + $profile->changeEmail(Email::fromString('bar@email.com')); - + self::assertEquals( - $profile->releaseEvents(), + $profile->releaseEvents(), [ - new EmailChanged(Email::fromString('bar@email.com')), - ] + new EmailChanged(Email::fromString('bar@email.com')), + ], ); self::assertEquals('bar@email.com', $profile->email()->toString()); diff --git a/docs/pages/upcasting.md b/docs/pages/upcasting.md index 7dc69a4f2..7a4464196 100644 --- a/docs/pages/upcasting.md +++ b/docs/pages/upcasting.md @@ -23,7 +23,7 @@ final class ProfileCreatedEmailLowerCastUpcaster implements Upcaster if ($upcast->eventName !== 'profile_created') { return $upcast; } - + return $upcast->replacePayloadByKey('email', strtolower($upcast->payload['email'])); } } @@ -40,19 +40,20 @@ the upgrade path. ```php use Patchlevel\EventSourcing\Metadata\Event\EventRegistry; -use Patchlevel\EventSourcing\Serializer\Upcast\Upcaster; use Patchlevel\EventSourcing\Serializer\Upcast\Upcast; +use Patchlevel\EventSourcing\Serializer\Upcast\Upcaster; final class LegacyEventNameUpaster implements Upcaster { public function __construct( - private readonly EventRegistry $eventRegistry - ){} - + private readonly EventRegistry $eventRegistry, + ) { + } + public function __invoke(Upcast $upcast): Upcast { return $upcast->replaceEventName( - $this->eventRegistry->eventName($upcast->eventName) + $this->eventRegistry->eventName($upcast->eventName), ); } } @@ -68,12 +69,12 @@ use Patchlevel\EventSourcing\Serializer\Upcast\UpcasterChain; $upcaster = new UpcasterChain([ new ProfileCreatedEmailLowerCastUpcaster(), - new LegacyEventNameUpaster($eventRegistry) + new LegacyEventNameUpaster($eventRegistry), ]); $serializer = DefaultEventSerializer::createFromPaths( ['src/Domain'], - $upcaster + $upcaster, ); ``` ## Update event stream @@ -98,7 +99,7 @@ final class EventStreamCleanupCommand extends Command { $pipeline = new Pipeline( new StoreSource($sourceStore), - new StoreTarget($targetStore) + new StoreTarget($targetStore), ); $pipeline->run(); diff --git a/psalm_docs.xml b/psalm_docs.xml new file mode 100644 index 000000000..5d3e0ee8c --- /dev/null +++ b/psalm_docs.xml @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + From a9e61ecb303f0fbc282b3b8b15c94b112544c649 Mon Sep 17 00:00:00 2001 From: David Badura Date: Fri, 22 Mar 2024 10:09:27 +0100 Subject: [PATCH 6/7] revert & fix again --- Makefile | 4 ++++ docs/pages/cli.md | 14 +++++++++----- docs/pages/clock.md | 12 ++++++------ docs/pages/event_bus.md | 3 +-- docs/pages/getting_started.md | 1 + docs/pages/normalizer.md | 29 +++++++++++++++++++++++++---- docs/pages/pipeline.md | 19 ++----------------- docs/pages/store.md | 11 +++++++++-- docs/pages/subscription.md | 8 +------- docs/pages/upcasting.md | 2 +- 10 files changed, 59 insertions(+), 44 deletions(-) diff --git a/Makefile b/Makefile index eab1f00c0..63cd8e000 100644 --- a/Makefile +++ b/Makefile @@ -95,6 +95,10 @@ docs-inject-php: .PHONY: docs-format docs-format: docs-phpcs docs-inject-php +.PHONY: docs-php-lint +docs-php-lint: docs-extract-php + php -l docs_php/*.php | grep 'Parse error: ' + .PHONY: docs-phpcs docs-phpcs: docs-extract-php vendor/bin/phpcbf docs_php --exclude=SlevomatCodingStandard.TypeHints.DeclareStrictTypes || true diff --git a/docs/pages/cli.md b/docs/pages/cli.md index d96b8c5c9..b354b13ed 100644 --- a/docs/pages/cli.md +++ b/docs/pages/cli.md @@ -62,10 +62,11 @@ use Patchlevel\EventSourcing\Console\DoctrineHelper; use Patchlevel\EventSourcing\Schema\DoctrineSchemaManager; use Symfony\Component\Console\Application; -$store; /* define your doctrine store */ -$projectionist; +$store; + /* create projectionist */ +$projectionist; $cli = new Application('Event-Sourcing CLI'); $cli->setCatchExceptions(true); @@ -103,15 +104,18 @@ use Doctrine\Migrations\Tools\Console\Command; use Patchlevel\EventSourcing\Schema\DoctrineMigrationSchemaProvider; use Patchlevel\EventSourcing\Schema\DoctrineSchemaDirector; -$connection; /* create connection */ -$store; /* define your doctrine store */ +/* create connection */ +$connection; +/* define your doctrine store */ +$store; $schemaDirector = new DoctrineSchemaDirector( $store, $connection, ); -$migrationConfig; /* define your migration config */ +/* define your migration config */ +$migrationConfig; $dependencyFactory = DependencyFactory::fromConnection( diff --git a/docs/pages/clock.md b/docs/pages/clock.md index 1e7f42133..65c6992bd 100644 --- a/docs/pages/clock.md +++ b/docs/pages/clock.md @@ -16,8 +16,8 @@ $clock = new SystemClock(); $date = $clock->now(); // get the actual datetime $date2 = $clock->now(); -$date === $date2; // false -$date === $date2; // false +// $date == $date2 => false +// $date === $date2 => false ``` ## FrozenClock @@ -32,8 +32,8 @@ $date = new DateTimeImmutable(); $clock = new FrozenClock($date); $frozenDate = $clock->now(); // gets the date provided before -$date === $frozenDate; // true -$date === $frozenDate; // false +// $date == $frozenDate => true +// $date === $frozenDate => false ``` The `FrozenClock` can also be updated with a new date, so you can test a jump in time. @@ -48,8 +48,8 @@ $clock->update($secondDate); $frozenDate = $clock->now(); -$firstDate === $frozenDate; // false -$secondDate === $frozenDate; // true +// $firstDate == $frozenDate => false +// $secondDate == $frozenDate => true ``` Or you can use the `sleep` method to simulate a time jump. diff --git a/docs/pages/event_bus.md b/docs/pages/event_bus.md index a25e2c492..de0d58a8c 100644 --- a/docs/pages/event_bus.md +++ b/docs/pages/event_bus.md @@ -65,8 +65,7 @@ $message->customHeaders(); // ['application-id' => 'app'] If you want *all* the headers you can also retrieve them. ```php -$message->headers(); - +$headers = $message->headers(); /* [ 'aggregateName' => 'profile', diff --git a/docs/pages/getting_started.md b/docs/pages/getting_started.md index f2fd6920f..f7cd413cc 100644 --- a/docs/pages/getting_started.md +++ b/docs/pages/getting_started.md @@ -279,6 +279,7 @@ $connection = DriverManager::getConnection(['url' => 'mysql://user:secret@localh $projectionConnection = DriverManager::getConnection(['url' => 'mysql://user:secret@localhost/projection']); +/* your own mailer */ $mailer; $serializer = DefaultEventSerializer::createFromPaths(['src/Domain/Hotel/Event']); diff --git a/docs/pages/normalizer.md b/docs/pages/normalizer.md index 34894520d..6683756af 100644 --- a/docs/pages/normalizer.md +++ b/docs/pages/normalizer.md @@ -288,12 +288,33 @@ You also need to implement a `normalize` and `denormalize` method. Finally, you have to allow the normalizer to be used as an attribute. ```php -use Patchlevel\Hydrator\Normalizer\DateTimeImmutableNormalizer; +use Patchlevel\Hydrator\Normalizer\InvalidArgument; +use Patchlevel\Hydrator\Normalizer\Normalizer; -final class DTO +#[Attribute(Attribute::TARGET_PROPERTY)] +class NameNormalizer implements Normalizer { - #[DateTimeImmutableNormalizer] - public DateTimeImmutable $date; + public function normalize(mixed $value): string + { + if (!$value instanceof Name) { + throw InvalidArgument::withWrongType(Name::class, $value); + } + + return $value->toString(); + } + + public function denormalize(mixed $value): Name|null + { + if ($value === null) { + return null; + } + + if (!is_string($value)) { + throw InvalidArgument::withWrongType('string', $value); + } + + return new Name($value); + } } ``` !!! warning diff --git a/docs/pages/pipeline.md b/docs/pages/pipeline.md index de57940f3..1fb1e478d 100644 --- a/docs/pages/pipeline.md +++ b/docs/pages/pipeline.md @@ -245,24 +245,9 @@ $middleware = new FilterEventMiddleware(static function (AggregateChanged $event With this middleware you can exclude archived events. ```php -use Patchlevel\EventSourcing\Pipeline\Middleware\ExcludeEventMiddleware; -use Patchlevel\EventSourcing\Pipeline\Middleware\RecalculatePlayheadMiddleware; -use Patchlevel\EventSourcing\Pipeline\Middleware\ReplaceEventMiddleware; -use Patchlevel\EventSourcing\Pipeline\Pipeline; -use Patchlevel\EventSourcing\Pipeline\Source\StoreSource; -use Patchlevel\EventSourcing\Pipeline\Target\StoreTarget; +use Patchlevel\EventSourcing\Pipeline\Middleware\ExcludeArchivedEventMiddleware; -$pipeline = new Pipeline( - new StoreSource($oldStore), - new StoreTarget($newStore), - [ - new ExcludeEventMiddleware([PrivacyAdded::class]), - new ReplaceEventMiddleware(OldVisited::class, static function (OldVisited $oldVisited) { - return new NewVisited($oldVisited->profileId()); - }), - new RecalculatePlayheadMiddleware(), - ], -); +$middleware = new ExcludeArchivedEventMiddleware(); ``` !!! warning diff --git a/docs/pages/store.md b/docs/pages/store.md index 46cec7ae6..648b6ff33 100644 --- a/docs/pages/store.md +++ b/docs/pages/store.md @@ -275,7 +275,14 @@ There is also the possibility of executing a function in a transaction. Then dbal takes care of starting a transaction, committing it and then possibly rollback it again. ```php -use Doctrine\DBAL\DriverManager; +$store->transactional(static function () use ($command, $bankAccountRepository): void { + $accountFrom = $bankAccountRepository->get($command->from()); + $accountTo = $bankAccountRepository->get($command->to()); -$connection = DriverManager::getConnection(['url' => 'mysql://user:secret@localhost/app']); + $accountFrom->transferMoney($command->to(), $command->amount()); + $accountTo->receiveMoney($command->from(), $command->amount()); + + $bankAccountRepository->save($accountFrom); + $bankAccountRepository->save($accountTo); +}); ``` \ No newline at end of file diff --git a/docs/pages/subscription.md b/docs/pages/subscription.md index 6f2296526..0b6607fd2 100644 --- a/docs/pages/subscription.md +++ b/docs/pages/subscription.md @@ -572,13 +572,7 @@ All booting subscriptions will catch up to the current event stream. After the boot process, the subscription is set to active or finished. ```php -use Patchlevel\EventSourcing\Attribute\Subscriber; -use Patchlevel\EventSourcing\Subscription\RunMode; - -#[Subscriber('do_stuff', RunMode::Once)] -final class DoStuffSubscriber -{ -} +$subscriptionEngine->boot($criteria); ``` ### Run diff --git a/docs/pages/upcasting.md b/docs/pages/upcasting.md index 7a4464196..70a4bb3a6 100644 --- a/docs/pages/upcasting.md +++ b/docs/pages/upcasting.md @@ -104,7 +104,7 @@ final class EventStreamCleanupCommand extends Command $pipeline->run(); - return 0; + return Command::SUCCESS; } } ``` From 2c6770c6a74d817dfaa80ea8edb2ea707e910fc8 Mon Sep 17 00:00:00 2001 From: David Badura Date: Fri, 22 Mar 2024 10:23:36 +0100 Subject: [PATCH 7/7] don't run for renovate --- .github/workflows/docs-check.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/docs-check.yml b/.github/workflows/docs-check.yml index 727920f5b..dd9c8ffad 100644 --- a/.github/workflows/docs-check.yml +++ b/.github/workflows/docs-check.yml @@ -7,7 +7,6 @@ on: push: branches: - "[0-9]+.[0-9]+.x" - - "renovate/*" jobs: checkdocs: