├── .coveralls.yml ├── .docheader ├── .gitignore ├── .php_cs ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── composer.json ├── docs ├── bookdown.json └── snapshot_store.md ├── phpunit.xml.dist ├── src ├── CallbackSerializer.php ├── CompositeSnapshotStore.php ├── Container │ └── CompositeSnapshotStoreFactory.php ├── InMemorySnapshotStore.php ├── Serializer.php ├── Snapshot.php └── SnapshotStore.php └── tests ├── CallbackSerializerTest.php ├── CompositeSnapshotStoreTest.php ├── Container └── CompositeSnapshotStoreFactoryTest.php ├── InMemorySnapshotStoreTest.php └── SnapshotTest.php /.coveralls.yml: -------------------------------------------------------------------------------- 1 | # for php-coveralls 2 | service_name: travis-ci 3 | coverage_clover: build/logs/clover.xml 4 | -------------------------------------------------------------------------------- /.docheader: -------------------------------------------------------------------------------- 1 | /** 2 | * This file is part of prooph/snapshot-store. 3 | * (c) 2017-%year% prooph software GmbH 4 | * (c) 2017-%year% Sascha-Oliver Prolic 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.settings 2 | /.project 3 | /.buildpath 4 | /vendor 5 | .idea 6 | .php_cs.cache 7 | nbproject 8 | composer.lock 9 | docs/html 10 | -------------------------------------------------------------------------------- /.php_cs: -------------------------------------------------------------------------------- 1 | getFinder()->in(__DIR__); 5 | 6 | $cacheDir = getenv('TRAVIS') ? getenv('HOME') . '/.php-cs-fixer' : __DIR__; 7 | 8 | $config->setCacheFile($cacheDir . '/.php_cs.cache'); 9 | 10 | return $config; 11 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | matrix: 4 | fast_finish: true 5 | include: 6 | - php: 7.1 7 | env: 8 | - DEPENDENCIES="" 9 | - EXECUTE_CS_CHECK=true 10 | - TEST_COVERAGE=true 11 | - php: 7.1 12 | env: 13 | - DEPENDENCIES="--prefer-lowest --prefer-stable" 14 | - php: 7.2 15 | env: 16 | - DEPENDENCIES="" 17 | - php: 7.2 18 | env: 19 | - DEPENDENCIES="--prefer-lowest --prefer-stable" 20 | - php: 7.3 21 | env: 22 | - DEPENDENCIES="" 23 | - TEST_COVERAGE=true 24 | - php: 7.3 25 | env: 26 | - DEPENDENCIES="--prefer-lowest --prefer-stable" 27 | - php: 7.4 28 | env: 29 | - DEPENDENCIES="" 30 | - TEST_COVERAGE=true 31 | - php: 7.4 32 | env: 33 | - DEPENDENCIES="--prefer-lowest --prefer-stable" 34 | - php: 8.0 35 | env: 36 | - DEPENDENCIES="" 37 | - php: 8.0 38 | env: 39 | - DEPENDENCIES="--prefer-lowest --prefer-stable" 40 | 41 | cache: 42 | directories: 43 | - $HOME/.composer/cache 44 | - $HOME/.php-cs-fixer 45 | - $HOME/.local 46 | 47 | before_script: 48 | - mkdir -p "$HOME/.php-cs-fixer" 49 | - phpenv config-rm xdebug.ini 50 | - composer self-update 51 | - composer update $DEPENDENCIES 52 | 53 | script: 54 | - if [[ $TEST_COVERAGE == 'true' ]]; then php -dzend_extension=xdebug.so ./vendor/bin/phpunit --coverage-text --coverage-clover ./build/logs/clover.xml; else ./vendor/bin/phpunit; fi 55 | - if [[ $EXECUTE_CS_CHECK == 'true' ]]; then ./vendor/bin/php-cs-fixer fix -v --diff --dry-run; fi 56 | 57 | after_success: 58 | - if [[ $TEST_COVERAGE == 'true' ]]; then php vendor/bin/coveralls -v; fi 59 | 60 | notifications: 61 | webhooks: 62 | urls: 63 | - https://webhooks.gitter.im/e/61c75218816eebde4486 64 | on_success: change # options: [always|never|change] default: always 65 | on_failure: always # options: [always|never|change] default: always 66 | on_start: never # options: [always|never|change] default: always 67 | 68 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | ## [v1.3.0](https://github.com/prooph/snapshot-store/tree/v1.3.0) 4 | 5 | [Full Changelog](https://github.com/prooph/snapshot-store/compare/v1.2.0...v1.3.0) 6 | 7 | **Implemented enhancements:** 8 | 9 | - test php 7.2 on travis [\#10](https://github.com/prooph/snapshot-store/pull/10) ([prolic](https://github.com/prolic)) 10 | 11 | **Merged pull requests:** 12 | 13 | - Restructure docs [\#9](https://github.com/prooph/snapshot-store/pull/9) ([codeliner](https://github.com/codeliner)) 14 | 15 | ## [v1.2.0](https://github.com/prooph/snapshot-store/tree/v1.2.0) (2017-04-11) 16 | [Full Changelog](https://github.com/prooph/snapshot-store/compare/v1.1.0...v1.2.0) 17 | 18 | **Implemented enhancements:** 19 | 20 | - Composite Snapshot Store [\#8](https://github.com/prooph/snapshot-store/pull/8) ([prolic](https://github.com/prolic)) 21 | 22 | **Closed issues:** 23 | 24 | - Idea: compressed snapshots [\#6](https://github.com/prooph/snapshot-store/issues/6) 25 | 26 | ## [v1.1.0](https://github.com/prooph/snapshot-store/tree/v1.1.0) (2017-04-05) 27 | [Full Changelog](https://github.com/prooph/snapshot-store/compare/v1.0.0...v1.1.0) 28 | 29 | **Implemented enhancements:** 30 | 31 | - Feature/serializer [\#7](https://github.com/prooph/snapshot-store/pull/7) ([basz](https://github.com/basz)) 32 | 33 | ## [v1.0.0](https://github.com/prooph/snapshot-store/tree/v1.0.0) (2017-03-30) 34 | **Implemented enhancements:** 35 | 36 | - Change SnapshotStore Interface [\#1](https://github.com/prooph/snapshot-store/issues/1) 37 | - Add possibility to remove all snapshots by aggregate type [\#4](https://github.com/prooph/snapshot-store/pull/4) ([prolic](https://github.com/prolic)) 38 | - Change snapshot store interface [\#2](https://github.com/prooph/snapshot-store/pull/2) ([prolic](https://github.com/prolic)) 39 | 40 | **Merged pull requests:** 41 | 42 | - update snapshot docs [\#5](https://github.com/prooph/snapshot-store/pull/5) ([prolic](https://github.com/prolic)) 43 | 44 | 45 | 46 | \* *This Change Log was automatically generated by [github_changelog_generator](https://github.com/skywinder/Github-Changelog-Generator)* 47 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017-2017, prooph software GmbH 2 | Copyright (c) 2017-2017, Sascha-Oliver Prolic 3 | All rights reserved. 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are met: 7 | 8 | * Redistributions of source code must retain the above copyright notice, this 9 | list of conditions and the following disclaimer. 10 | 11 | * Redistributions in binary form must reproduce the above copyright notice, 12 | this list of conditions and the following disclaimer in the documentation 13 | and/or other materials provided with the distribution. 14 | 15 | * Neither the name of the {organization} nor the names of its 16 | contributors may be used to endorse or promote products derived from 17 | this software without specific prior written permission. 18 | 19 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 20 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 21 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 22 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 23 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 24 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 25 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 26 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 27 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Prooph Snapshot Store 2 | 3 | Simple and lightweight snapshot store 4 | 5 | [![Build Status](https://travis-ci.org/prooph/snapshot-store.svg?branch=master)](https://travis-ci.org/prooph/snapshot-store) 6 | [![Coverage Status](https://img.shields.io/coveralls/prooph/snapshot-store.svg)](https://coveralls.io/r/prooph/snapshot-store?branch=master) 7 | [![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/prooph/improoph) 8 | 9 | ## Important 10 | 11 | This library will receive support until December 31, 2019 and will then be deprecated. 12 | 13 | For further information see the official announcement here: [https://www.sasaprolic.com/2018/08/the-future-of-prooph-components.html](https://www.sasaprolic.com/2018/08/the-future-of-prooph-components.html) 14 | 15 | ## Installation 16 | 17 | You can install Prooph Snapshot Store via composer by adding `"prooph/snapshot-store": "^1.0"` as requirement to your composer.json. 18 | 19 | # Support 20 | 21 | - Ask questions on Stack Overflow tagged with [#prooph](https://stackoverflow.com/questions/tagged/prooph). 22 | - File issues at [https://github.com/prooph/snapshot-store/issues](https://github.com/prooph/snapshot-store/issues). 23 | - Say hello in the [prooph gitter](https://gitter.im/prooph/improoph) chat. 24 | 25 | ## Contribute 26 | 27 | Please feel free to fork and extend existing or add new plugins and send a pull request with your changes! 28 | To establish a consistent code quality, please provide unit tests for all your changes and may adapt the documentation. 29 | 30 | ## License 31 | 32 | Released under the [New BSD License](LICENSE). 33 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "prooph/snapshot-store", 3 | "description": "Prooph Snapshot Store", 4 | "type": "library", 5 | "license": "BSD-3-Clause", 6 | "homepage": "http://getprooph.org/", 7 | "authors": [ 8 | { 9 | "name": "Alexander Miertsch", 10 | "email": "contact@prooph.de", 11 | "homepage": "http://www.prooph.de" 12 | }, 13 | { 14 | "name": "Sascha-Oliver Prolic", 15 | "email": "saschaprolic@googlemail.com" 16 | } 17 | ], 18 | "keywords": [ 19 | "EventSourcing", 20 | "DDD", 21 | "prooph" 22 | ], 23 | "minimum-stability": "dev", 24 | "prefer-stable": true, 25 | "require": { 26 | "php": "^7.1 || ^8.0", 27 | "prooph/common" : "^4.1" 28 | }, 29 | "require-dev": { 30 | "phpunit/phpunit": "^6.0 || ^9.3", 31 | "phpspec/prophecy": "^1.9", 32 | "prooph/php-cs-fixer-config": "^0.3 || ^0.4", 33 | "satooshi/php-coveralls": "^1.0", 34 | "prooph/bookdown-template": "^0.2.3", 35 | "psr/container": "^1.0", 36 | "sandrokeil/interop-config": "^2.0.1" 37 | }, 38 | "suggest": { 39 | "sandrokeil/interop-config": "^2.0.1 for usage of provided factories" 40 | }, 41 | "extra": { 42 | "branch-alias": { 43 | "dev-master": "1.2-dev" 44 | } 45 | }, 46 | "autoload": { 47 | "psr-4": { 48 | "Prooph\\SnapshotStore\\": "src/" 49 | } 50 | }, 51 | "autoload-dev": { 52 | "psr-4": { 53 | "ProophTest\\SnapshotStore\\": "tests/" 54 | } 55 | }, 56 | "config": { 57 | "preferred-install": { 58 | "prooph/*": "source" 59 | } 60 | }, 61 | "scripts": { 62 | "check": [ 63 | "@cs", 64 | "@test" 65 | ], 66 | "cs": "php-cs-fixer fix -v --diff --dry-run", 67 | "cs-fix": "php-cs-fixer fix -v --diff", 68 | "test": "phpunit" 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /docs/bookdown.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Snapshot Store", 3 | "content": [ 4 | {"snapshot_store": "snapshot_store.md"}, 5 | {"pdo_snapshot_store": "https://raw.githubusercontent.com/prooph/pdo-snapshot-store/master/docs/bookdown.json"}, 6 | {"mongodb_snapshot_store": "https://raw.githubusercontent.com/prooph/mongodb-snapshot-store/master/docs/bookdown.json"}, 7 | {"memcached_snapshot_store": "https://raw.githubusercontent.com/prooph/memcached-snapshot-store/master/docs/bookdown.json"}, 8 | {"arangodb_snapshot_store": "https://raw.githubusercontent.com/prooph/arangodb-snapshot-store/master/docs/bookdown.json"} 9 | ], 10 | "tocDepth": 1, 11 | "numbering": false, 12 | "target": "./html", 13 | "template": "../vendor/prooph/bookdown-template/templates/main.php" 14 | } 15 | -------------------------------------------------------------------------------- /docs/snapshot_store.md: -------------------------------------------------------------------------------- 1 | # Overview 2 | 3 | Simple and lightweight snapshot store that works together with `prooph/event-sourcing` to speed up loading of aggregates. 4 | 5 | ## Installation 6 | 7 | ```bash 8 | composer require prooph/snapshot-store 9 | ``` 10 | 11 | ## Creating snapshots from event streams 12 | 13 | This feature is provided by the [prooph/snapshotter](https://github.com/prooph/snapshotter) package. 14 | Please refer to the docs of the package to learn more about it. 15 | 16 | Also choose one of the `Prooph\*SnapshotStore` to take snapshots. 17 | Inject the snapshot store into an aggregate repository and the repository will use the snapshot store to speed up 18 | aggregate loading. 19 | 20 | Our example application [proophessor-do](https://github.com/prooph/proophessor-do) contains a snapshotting tutorial. 21 | 22 | ## Using a different Serializer. 23 | 24 | By default prooph uses PHP's own serialise and unserialize methods. These may not suite your needs so as of v1.1 of the snapshot store you can use a custom serialiser. 25 | 26 | You can use the provided CallbackSerializer to do this. 27 | 28 | ```php 29 | [ 46 | 'factories' => [ 47 | \Prooph\SnapshotStore\Serializer::class => My\CallbackSerializerFactory::class, 48 | ], 49 | ], 50 | ]; 51 | ``` 52 | 53 | *Note: All SnapshotStores ship with interop factories to ease set up.* 54 | 55 | ## Composite Snapshot Store 56 | 57 | This component ships with a composite snapshot store, that aggregates multiple snapshot stores. When asked to save a 58 | snapshot or removeAll, it will call the method in all aggregated snapshot stores. If you try to get a snapshot from the 59 | composite, it will ask each snapshot store for the snapshot and returns the first snapshot found or null. 60 | 61 | This is especially useful to combine a memcached snapshot store for high speed with a fallback like pdo or mongodb. 62 | 63 | Example: 64 | 65 | ```php 66 | 2 | 13 | 14 | ./tests/ 15 | 16 | 17 | 18 | 19 | ./src/ 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/CallbackSerializer.php: -------------------------------------------------------------------------------- 1 | 6 | * (c) 2017-2021 Sascha-Oliver Prolic 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | declare(strict_types=1); 13 | 14 | namespace Prooph\SnapshotStore; 15 | 16 | final class CallbackSerializer implements Serializer 17 | { 18 | /** 19 | * callable 20 | */ 21 | private $serializeCallback = 'serialize'; 22 | 23 | /** 24 | * callable 25 | */ 26 | private $unserializeCallback = 'unserialize'; 27 | 28 | public function __construct(?callable $serializeCallback, ?callable $unserializeCallback) 29 | { 30 | if (null !== $serializeCallback && null !== $unserializeCallback) { 31 | $this->serializeCallback = $serializeCallback; 32 | $this->unserializeCallback = $unserializeCallback; 33 | } 34 | } 35 | 36 | /** 37 | * @param object|array $data 38 | * @return string 39 | */ 40 | public function serialize($data): string 41 | { 42 | return \call_user_func($this->serializeCallback, $data); 43 | } 44 | 45 | /** 46 | * @param string $serialized 47 | * @return object|array 48 | */ 49 | public function unserialize(string $serialized) 50 | { 51 | return \call_user_func($this->unserializeCallback, $serialized); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/CompositeSnapshotStore.php: -------------------------------------------------------------------------------- 1 | 6 | * (c) 2017-2021 Sascha-Oliver Prolic 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | declare(strict_types=1); 13 | 14 | namespace Prooph\SnapshotStore; 15 | 16 | final class CompositeSnapshotStore implements SnapshotStore 17 | { 18 | /** 19 | * @var SnapshotStore[] 20 | */ 21 | private $snapshotStores; 22 | 23 | public function __construct(SnapshotStore ...$snapshotStores) 24 | { 25 | $this->snapshotStores = $snapshotStores; 26 | } 27 | 28 | public function get(string $aggregateType, string $aggregateId): ?Snapshot 29 | { 30 | foreach ($this->snapshotStores as $snapshotStore) { 31 | $snapshot = $snapshotStore->get($aggregateType, $aggregateId); 32 | 33 | if (null !== $snapshot) { 34 | return $snapshot; 35 | } 36 | } 37 | 38 | return null; 39 | } 40 | 41 | public function save(Snapshot ...$snapshots): void 42 | { 43 | foreach ($this->snapshotStores as $snapshotStore) { 44 | $snapshotStore->save(...$snapshots); 45 | } 46 | } 47 | 48 | public function removeAll(string $aggregateType): void 49 | { 50 | foreach ($this->snapshotStores as $snapshotStore) { 51 | $snapshotStore->removeAll($aggregateType); 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Container/CompositeSnapshotStoreFactory.php: -------------------------------------------------------------------------------- 1 | 6 | * (c) 2017-2021 Sascha-Oliver Prolic 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | declare(strict_types=1); 13 | 14 | namespace Prooph\SnapshotStore\Container; 15 | 16 | use Interop\Config\ConfigurationTrait; 17 | use Interop\Config\RequiresConfigId; 18 | use Prooph\SnapshotStore\CompositeSnapshotStore; 19 | use Psr\Container\ContainerInterface; 20 | 21 | final class CompositeSnapshotStoreFactory implements RequiresConfigId 22 | { 23 | use ConfigurationTrait; 24 | 25 | /** 26 | * @var string 27 | */ 28 | private $configId; 29 | 30 | /** 31 | * Creates a new instance from a specified config, specifically meant to be used as static factory. 32 | * 33 | * In case you want to use another config key than provided by the factories, you can add the following factory to 34 | * your config: 35 | * 36 | * 37 | * [CompositeSnapshotStoreFactory::class, 'service_name'], 40 | * ]; 41 | * 42 | * 43 | * @throws \InvalidArgumentException 44 | */ 45 | public static function __callStatic(string $name, array $arguments): CompositeSnapshotStore 46 | { 47 | if (! isset($arguments[0]) || ! $arguments[0] instanceof ContainerInterface) { 48 | throw new \InvalidArgumentException( 49 | \sprintf('The first argument must be of type %s', ContainerInterface::class) 50 | ); 51 | } 52 | 53 | return (new static($name))->__invoke($arguments[0]); 54 | } 55 | 56 | public function __invoke(ContainerInterface $container): CompositeSnapshotStore 57 | { 58 | $config = $container->get('config'); 59 | $config = $this->options($config, $this->configId); 60 | 61 | $snapshotStores = []; 62 | 63 | foreach ($config as $snapshotStoreName) { 64 | $snapshotStores[] = $container->get($snapshotStoreName); 65 | } 66 | 67 | return new CompositeSnapshotStore(...$snapshotStores); 68 | } 69 | 70 | public function __construct(string $configId = 'default') 71 | { 72 | $this->configId = $configId; 73 | } 74 | 75 | public function dimensions(): iterable 76 | { 77 | return ['prooph', 'composite_snapshot_store']; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/InMemorySnapshotStore.php: -------------------------------------------------------------------------------- 1 | 6 | * (c) 2017-2021 Sascha-Oliver Prolic 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | declare(strict_types=1); 13 | 14 | namespace Prooph\SnapshotStore; 15 | 16 | final class InMemorySnapshotStore implements SnapshotStore 17 | { 18 | /** 19 | * @var array 20 | */ 21 | private $map = []; 22 | 23 | public function get(string $aggregateType, string $aggregateId): ?Snapshot 24 | { 25 | if (! isset($this->map[$aggregateType][$aggregateId])) { 26 | return null; 27 | } 28 | 29 | return $this->map[$aggregateType][$aggregateId]; 30 | } 31 | 32 | public function save(Snapshot ...$snapshots): void 33 | { 34 | foreach ($snapshots as $snapshot) { 35 | $this->map[$snapshot->aggregateType()][$snapshot->aggregateId()] = $snapshot; 36 | } 37 | } 38 | 39 | public function removeAll(string $aggregateType): void 40 | { 41 | unset($this->map[$aggregateType]); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Serializer.php: -------------------------------------------------------------------------------- 1 | 6 | * (c) 2017-2021 Sascha-Oliver Prolic 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | declare(strict_types=1); 13 | 14 | namespace Prooph\SnapshotStore; 15 | 16 | interface Serializer 17 | { 18 | public function serialize($data): string; 19 | 20 | public function unserialize(string $data); 21 | } 22 | -------------------------------------------------------------------------------- /src/Snapshot.php: -------------------------------------------------------------------------------- 1 | 6 | * (c) 2017-2021 Sascha-Oliver Prolic 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | declare(strict_types=1); 13 | 14 | namespace Prooph\SnapshotStore; 15 | 16 | use Assert\Assertion; 17 | use DateTimeImmutable; 18 | 19 | final class Snapshot 20 | { 21 | /** 22 | * @var string 23 | */ 24 | private $aggregateType; 25 | 26 | /** 27 | * @var string 28 | */ 29 | private $aggregateId; 30 | 31 | /** 32 | * @var object|array 33 | */ 34 | private $aggregateRoot; 35 | 36 | /** 37 | * @var int 38 | */ 39 | private $lastVersion; 40 | 41 | /** 42 | * @var DateTimeImmutable 43 | */ 44 | private $createdAt; 45 | 46 | public function __construct( 47 | string $aggregateType, 48 | string $aggregateId, 49 | $aggregateRoot, 50 | int $lastVersion, 51 | DateTimeImmutable $createdAt 52 | ) { 53 | Assertion::minLength($aggregateType, 1); 54 | Assertion::minLength($aggregateId, 1); 55 | Assertion::min($lastVersion, 1); 56 | 57 | $this->aggregateType = $aggregateType; 58 | $this->aggregateId = $aggregateId; 59 | $this->aggregateRoot = $aggregateRoot; 60 | $this->lastVersion = $lastVersion; 61 | $this->createdAt = $createdAt; 62 | } 63 | 64 | public function aggregateType(): string 65 | { 66 | return $this->aggregateType; 67 | } 68 | 69 | public function aggregateId(): string 70 | { 71 | return $this->aggregateId; 72 | } 73 | 74 | public function aggregateRoot() 75 | { 76 | return $this->aggregateRoot; 77 | } 78 | 79 | public function lastVersion(): int 80 | { 81 | return $this->lastVersion; 82 | } 83 | 84 | public function createdAt(): DateTimeImmutable 85 | { 86 | return $this->createdAt; 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/SnapshotStore.php: -------------------------------------------------------------------------------- 1 | 6 | * (c) 2017-2021 Sascha-Oliver Prolic 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | declare(strict_types=1); 13 | 14 | namespace Prooph\SnapshotStore; 15 | 16 | interface SnapshotStore 17 | { 18 | public function get(string $aggregateType, string $aggregateId): ?Snapshot; 19 | 20 | public function save(Snapshot ...$snapshots): void; 21 | 22 | public function removeAll(string $aggregateType): void; 23 | } 24 | -------------------------------------------------------------------------------- /tests/CallbackSerializerTest.php: -------------------------------------------------------------------------------- 1 | 6 | * (c) 2017-2021 Sascha-Oliver Prolic 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | declare(strict_types=1); 13 | 14 | namespace ProophTest\SnapshotStore; 15 | 16 | use PHPUnit\Framework\TestCase; 17 | use Prooph\SnapshotStore\CallbackSerializer; 18 | use Prooph\SnapshotStore\Serializer; 19 | 20 | class CallbackSerializerTest extends TestCase 21 | { 22 | /** 23 | * @test 24 | */ 25 | public function it_implements_interface(): void 26 | { 27 | $serializer = new CallbackSerializer(null, null); 28 | 29 | $this->assertInstanceOf(Serializer::class, $serializer); 30 | } 31 | 32 | /** 33 | * @test 34 | */ 35 | public function it_uses_serializer_by_default(): void 36 | { 37 | $serializer = new CallbackSerializer(null, null); 38 | $before = new \stdClass(); 39 | $serialized = $serializer->serialize($before); 40 | $after = $serializer->unserialize($serialized); 41 | 42 | $this->assertEquals('O:8:"stdClass":0:{}', $serialized); 43 | $this->assertEquals($before, $after); 44 | } 45 | 46 | /** 47 | * @test 48 | */ 49 | public function it_uses_default_if_only_one_callback_provided_instead_of_two(): void 50 | { 51 | $serializer = new CallbackSerializer(function ($data): string { 52 | return (string) ($data * 2); 53 | }, null); 54 | 55 | $before = new \stdClass(); 56 | $serialized = $serializer->serialize($before); 57 | $after = $serializer->unserialize($serialized); 58 | 59 | $this->assertEquals('O:8:"stdClass":0:{}', $serialized); 60 | $this->assertEquals($before, $after); 61 | 62 | $serializer = new CallbackSerializer(null, 63 | function (string $data): string { 64 | return (string) ($data / 2); 65 | }); 66 | 67 | $serialized = $serializer->serialize($before); 68 | $after = $serializer->unserialize($serialized); 69 | 70 | $this->assertEquals('O:8:"stdClass":0:{}', $serialized); 71 | $this->assertEquals($before, $after); 72 | } 73 | 74 | /** 75 | * @test 76 | */ 77 | public function it_can_use_any_callback(): void 78 | { 79 | $serializer = new CallbackSerializer(function ($data): string { 80 | return (string) ($data * 2); 81 | }, function (string $data): string { 82 | return (string) ($data / 2); 83 | }); 84 | 85 | $before = '10'; 86 | $serialized = $serializer->serialize($before); 87 | $after = $serializer->unserialize($serialized); 88 | 89 | $this->assertEquals('20', $serialized); 90 | $this->assertEquals($before, $after); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /tests/CompositeSnapshotStoreTest.php: -------------------------------------------------------------------------------- 1 | 6 | * (c) 2017-2021 Sascha-Oliver Prolic 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | declare(strict_types=1); 13 | 14 | namespace ProophTest\SnapshotStore; 15 | 16 | use PHPUnit\Framework\TestCase; 17 | use Prooph\SnapshotStore\CompositeSnapshotStore; 18 | use Prooph\SnapshotStore\InMemorySnapshotStore; 19 | use Prooph\SnapshotStore\Snapshot; 20 | use Prooph\SnapshotStore\SnapshotStore; 21 | 22 | class CompositeSnapshotStoreTest extends TestCase 23 | { 24 | /** 25 | * @test 26 | */ 27 | public function it_saves_snapshots_in_all_stores(): void 28 | { 29 | $now = new \DateTimeImmutable(); 30 | 31 | $snapshot = new Snapshot( 32 | 'foo', 33 | 'some_id', 34 | [ 35 | 'some' => 'thing', 36 | ], 37 | 1, 38 | $now 39 | ); 40 | 41 | $snapshotStore1 = new InMemorySnapshotStore(); 42 | $snapshotStore2 = new InMemorySnapshotStore(); 43 | $snapshotStore = new CompositeSnapshotStore($snapshotStore1, $snapshotStore2); 44 | 45 | $snapshotStore->save($snapshot); 46 | 47 | $this->assertSame($snapshot, $snapshotStore->get('foo', 'some_id')); 48 | $this->assertSame($snapshot, $snapshotStore1->get('foo', 'some_id')); 49 | $this->assertSame($snapshot, $snapshotStore2->get('foo', 'some_id')); 50 | 51 | $this->assertNull($snapshotStore->get('some', 'invalid')); 52 | $this->assertNull($snapshotStore1->get('some', 'invalid')); 53 | $this->assertNull($snapshotStore2->get('some', 'invalid')); 54 | } 55 | 56 | /** 57 | * @test 58 | */ 59 | public function it_saves_multiple_snapshots_and_removes_them(): void 60 | { 61 | $now = new \DateTimeImmutable(); 62 | 63 | $snapshot1 = new Snapshot( 64 | 'foo', 65 | 'some_id', 66 | [ 67 | 'some' => 'thing', 68 | ], 69 | 1, 70 | $now 71 | ); 72 | 73 | $snapshot2 = new Snapshot( 74 | 'bar', 75 | 'some_other_id', 76 | [ 77 | 'some' => 'other_thing', 78 | ], 79 | 1, 80 | $now 81 | ); 82 | 83 | $snapshot3 = new Snapshot( 84 | 'bar', 85 | 'some_other_id_too', 86 | [ 87 | 'some' => 'other_thing_too', 88 | ], 89 | 1, 90 | $now 91 | ); 92 | 93 | $snapshotStore1 = new InMemorySnapshotStore(); 94 | $snapshotStore2 = new InMemorySnapshotStore(); 95 | $snapshotStore = new CompositeSnapshotStore($snapshotStore1, $snapshotStore2); 96 | 97 | $snapshotStore->save($snapshot1, $snapshot2, $snapshot3); 98 | 99 | $this->assertSame($snapshot1, $snapshotStore->get('foo', 'some_id')); 100 | $this->assertSame($snapshot2, $snapshotStore->get('bar', 'some_other_id')); 101 | $this->assertSame($snapshot3, $snapshotStore->get('bar', 'some_other_id_too')); 102 | $this->assertSame($snapshot1, $snapshotStore1->get('foo', 'some_id')); 103 | $this->assertSame($snapshot2, $snapshotStore1->get('bar', 'some_other_id')); 104 | $this->assertSame($snapshot3, $snapshotStore1->get('bar', 'some_other_id_too')); 105 | $this->assertSame($snapshot1, $snapshotStore2->get('foo', 'some_id')); 106 | $this->assertSame($snapshot2, $snapshotStore2->get('bar', 'some_other_id')); 107 | $this->assertSame($snapshot3, $snapshotStore2->get('bar', 'some_other_id_too')); 108 | 109 | $snapshotStore->removeAll('bar'); 110 | 111 | $this->assertSame($snapshot1, $snapshotStore->get('foo', 'some_id')); 112 | $this->assertNull($snapshotStore->get('bar', 'some_other_id')); 113 | $this->assertNull($snapshotStore->get('bar', 'some_other_id_too')); 114 | $this->assertSame($snapshot1, $snapshotStore1->get('foo', 'some_id')); 115 | $this->assertNull($snapshotStore1->get('bar', 'some_other_id')); 116 | $this->assertNull($snapshotStore1->get('bar', 'some_other_id_too')); 117 | $this->assertSame($snapshot1, $snapshotStore2->get('foo', 'some_id')); 118 | $this->assertNull($snapshotStore2->get('bar', 'some_other_id')); 119 | $this->assertNull($snapshotStore2->get('bar', 'some_other_id_too')); 120 | } 121 | 122 | /** 123 | * @test 124 | */ 125 | public function it_returns_result_from_first_store_with_content(): void 126 | { 127 | $now = new \DateTimeImmutable(); 128 | 129 | $snapshot = new Snapshot( 130 | 'foo', 131 | 'some_id', 132 | [ 133 | 'some' => 'thing', 134 | ], 135 | 1, 136 | $now 137 | ); 138 | 139 | $snapshotStore1 = $this->prophesize(SnapshotStore::class); 140 | $snapshotStore1->get('foo', 'some_id')->willReturn(null)->shouldBeCalled(); 141 | 142 | $snapshotStore2 = $this->prophesize(SnapshotStore::class); 143 | $snapshotStore2->get('foo', 'some_id')->willReturn($snapshot)->shouldBeCalled(); 144 | 145 | $snapshotStore3 = $this->prophesize(SnapshotStore::class); 146 | $snapshotStore3->get('foo', 'some_id')->shouldNotBeCalled(); 147 | 148 | $snapshotStore = new CompositeSnapshotStore( 149 | $snapshotStore1->reveal(), 150 | $snapshotStore2->reveal(), 151 | $snapshotStore3->reveal() 152 | ); 153 | 154 | $this->assertSame($snapshot, $snapshotStore->get('foo', 'some_id')); 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /tests/Container/CompositeSnapshotStoreFactoryTest.php: -------------------------------------------------------------------------------- 1 | 6 | * (c) 2017-2021 Sascha-Oliver Prolic 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | declare(strict_types=1); 13 | 14 | namespace ProophTest\SnapshotStore\Container; 15 | 16 | use PHPUnit\Framework\TestCase; 17 | use Prooph\SnapshotStore\CompositeSnapshotStore; 18 | use Prooph\SnapshotStore\Container\CompositeSnapshotStoreFactory; 19 | use Prooph\SnapshotStore\SnapshotStore; 20 | use Psr\Container\ContainerInterface; 21 | 22 | class CompositeSnapshotStoreFactoryTest extends TestCase 23 | { 24 | /** 25 | * @test 26 | */ 27 | public function it_creates_composite_snapshot_store(): void 28 | { 29 | $config['prooph']['composite_snapshot_store']['default'] = [ 30 | 'snapshot_store_1', 31 | 'snapshot_store_2', 32 | ]; 33 | 34 | $snapshotStore1 = $this->prophesize(SnapshotStore::class); 35 | $snapshotStore2 = $this->prophesize(SnapshotStore::class); 36 | 37 | $container = $this->prophesize(ContainerInterface::class); 38 | 39 | $container->get('config')->willReturn($config)->shouldBeCalled(); 40 | $container->get('snapshot_store_1')->willReturn($snapshotStore1->reveal())->shouldBeCalled(); 41 | $container->get('snapshot_store_2')->willReturn($snapshotStore2->reveal())->shouldBeCalled(); 42 | 43 | $factory = new CompositeSnapshotStoreFactory(); 44 | $snapshotStore = $factory($container->reveal()); 45 | 46 | $this->assertInstanceOf(CompositeSnapshotStore::class, $snapshotStore); 47 | } 48 | 49 | /** 50 | * @test 51 | */ 52 | public function it_creates_composite_snapshot_store_via_call_static(): void 53 | { 54 | $config['prooph']['composite_snapshot_store']['default'] = [ 55 | 'snapshot_store_1', 56 | 'snapshot_store_2', 57 | ]; 58 | 59 | $snapshotStore1 = $this->prophesize(SnapshotStore::class); 60 | $snapshotStore2 = $this->prophesize(SnapshotStore::class); 61 | 62 | $container = $this->prophesize(ContainerInterface::class); 63 | 64 | $container->get('config')->willReturn($config)->shouldBeCalled(); 65 | $container->get('snapshot_store_1')->willReturn($snapshotStore1->reveal())->shouldBeCalled(); 66 | $container->get('snapshot_store_2')->willReturn($snapshotStore2->reveal())->shouldBeCalled(); 67 | 68 | $serviceName = 'default'; 69 | $snapshotStore = CompositeSnapshotStoreFactory::$serviceName($container->reveal()); 70 | 71 | $this->assertInstanceOf(CompositeSnapshotStore::class, $snapshotStore); 72 | } 73 | 74 | /** 75 | * @test 76 | */ 77 | public function it_throws_exception_when_invalid_container_given(): void 78 | { 79 | $this->expectException(\InvalidArgumentException::class); 80 | 81 | $eventStoreName = 'custom'; 82 | CompositeSnapshotStoreFactory::$eventStoreName('invalid container'); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /tests/InMemorySnapshotStoreTest.php: -------------------------------------------------------------------------------- 1 | 6 | * (c) 2017-2021 Sascha-Oliver Prolic 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | declare(strict_types=1); 13 | 14 | namespace ProophTest\SnapshotStore; 15 | 16 | use PHPUnit\Framework\TestCase; 17 | use Prooph\SnapshotStore\InMemorySnapshotStore; 18 | use Prooph\SnapshotStore\Snapshot; 19 | 20 | class InMemorySnapshotStoreTest extends TestCase 21 | { 22 | /** 23 | * @test 24 | */ 25 | public function it_saves_snapshots(): void 26 | { 27 | $now = new \DateTimeImmutable(); 28 | 29 | $snapshot = new Snapshot( 30 | 'foo', 31 | 'some_id', 32 | [ 33 | 'some' => 'thing', 34 | ], 35 | 1, 36 | $now 37 | ); 38 | 39 | $snapshotStore = new InMemorySnapshotStore(); 40 | $snapshotStore->save($snapshot); 41 | 42 | $this->assertSame($snapshot, $snapshotStore->get('foo', 'some_id')); 43 | 44 | $this->assertNull($snapshotStore->get('some', 'invalid')); 45 | } 46 | 47 | /** 48 | * @test 49 | */ 50 | public function it_saves_multiple_snapshots_and_removes_them(): void 51 | { 52 | $now = new \DateTimeImmutable(); 53 | 54 | $snapshot1 = new Snapshot( 55 | 'foo', 56 | 'some_id', 57 | [ 58 | 'some' => 'thing', 59 | ], 60 | 1, 61 | $now 62 | ); 63 | 64 | $snapshot2 = new Snapshot( 65 | 'bar', 66 | 'some_other_id', 67 | [ 68 | 'some' => 'other_thing', 69 | ], 70 | 1, 71 | $now 72 | ); 73 | 74 | $snapshot3 = new Snapshot( 75 | 'bar', 76 | 'some_other_id_too', 77 | [ 78 | 'some' => 'other_thing_too', 79 | ], 80 | 1, 81 | $now 82 | ); 83 | 84 | $snapshotStore = new InMemorySnapshotStore(); 85 | $snapshotStore->save($snapshot1, $snapshot2, $snapshot3); 86 | 87 | $this->assertSame($snapshot1, $snapshotStore->get('foo', 'some_id')); 88 | $this->assertSame($snapshot2, $snapshotStore->get('bar', 'some_other_id')); 89 | $this->assertSame($snapshot3, $snapshotStore->get('bar', 'some_other_id_too')); 90 | 91 | $snapshotStore->removeAll('bar'); 92 | 93 | $this->assertSame($snapshot1, $snapshotStore->get('foo', 'some_id')); 94 | $this->assertNull($snapshotStore->get('bar', 'some_other_id')); 95 | $this->assertNull($snapshotStore->get('bar', 'some_other_id_too')); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /tests/SnapshotTest.php: -------------------------------------------------------------------------------- 1 | 6 | * (c) 2017-2021 Sascha-Oliver Prolic 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | declare(strict_types=1); 13 | 14 | namespace ProophTest\SnapshotStore; 15 | 16 | use PHPUnit\Framework\TestCase; 17 | use Prooph\SnapshotStore\Snapshot; 18 | 19 | class SnapshotTest extends TestCase 20 | { 21 | /** 22 | * @test 23 | */ 24 | public function it_creates_and_returns_values(): void 25 | { 26 | $now = new \DateTimeImmutable(); 27 | 28 | $snapshot = new Snapshot( 29 | 'foo', 30 | 'some_id', 31 | [ 32 | 'some' => 'thing', 33 | ], 34 | 1, 35 | $now 36 | ); 37 | 38 | $this->assertEquals('foo', $snapshot->aggregateType()); 39 | $this->assertEquals('some_id', $snapshot->aggregateId()); 40 | $this->assertEquals(['some' => 'thing'], $snapshot->aggregateRoot()); 41 | $this->assertEquals(1, $snapshot->lastVersion()); 42 | $this->assertSame($now, $snapshot->createdAt()); 43 | } 44 | 45 | /** 46 | * @test 47 | */ 48 | public function it_requires_min_length_for_aggregate_type(): void 49 | { 50 | $this->expectException(\InvalidArgumentException::class); 51 | 52 | new Snapshot( 53 | '', 54 | 'some_id', 55 | [ 56 | 'some' => 'thing', 57 | ], 58 | 1, 59 | new \DateTimeImmutable() 60 | ); 61 | } 62 | 63 | /** 64 | * @test 65 | */ 66 | public function it_requires_min_length_for_aggregate_id(): void 67 | { 68 | $this->expectException(\InvalidArgumentException::class); 69 | 70 | new Snapshot( 71 | 'foo', 72 | '', 73 | [ 74 | 'some' => 'thing', 75 | ], 76 | 1, 77 | new \DateTimeImmutable() 78 | ); 79 | } 80 | 81 | /** 82 | * @test 83 | */ 84 | public function it_requires_min_for_last_version(): void 85 | { 86 | $this->expectException(\InvalidArgumentException::class); 87 | 88 | new Snapshot( 89 | 'foo', 90 | 'some_id', 91 | [ 92 | 'some' => 'thing', 93 | ], 94 | 0, 95 | new \DateTimeImmutable() 96 | ); 97 | } 98 | } 99 | --------------------------------------------------------------------------------