├── .gitattributes ├── CHANGELOG.md ├── LICENSE ├── README.md ├── composer.json ├── docker-compose-tests.yml ├── scripts ├── mysql_snapshot_table.sql └── postgres_snapshot_table.sql └── src ├── Container └── PdoSnapshotStoreFactory.php ├── Exception └── RuntimeException.php └── PdoSnapshotStore.php /.gitattributes: -------------------------------------------------------------------------------- 1 | .coveralls.yml export-ignore 2 | .docheader export-ignore 3 | .gitignore export-ignore 4 | .laminas-ci.json export-ignore 5 | .php-cs-fixer.php export-ignore 6 | /.github export-ignore 7 | /.laminas-ci export-ignore 8 | /docs export-ignore 9 | /examples export-ignore 10 | /tests export-ignore 11 | phpunit.xml.dist export-ignore 12 | docker.compose-tests export-ignore 13 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [v1.6.0](https://github.com/prooph/pdo-snapshot-store/tree/v1.6.0) 4 | 5 | [Full Changelog](https://github.com/prooph/pdo-snapshot-store/compare/v1.5.2...v1.6.0) 6 | 7 | **Implemented enhancements:** 8 | 9 | - dropped travis in favor of github actions via laminas/laminas-continu… [\#33](https://github.com/prooph/pdo-snapshot-store/pull/33) ([basz](https://github.com/basz)) 10 | - php8 compatibility [\#32](https://github.com/prooph/pdo-snapshot-store/pull/32) ([basz](https://github.com/basz)) 11 | 12 | **Merged pull requests:** 13 | 14 | - Change copyright [\#31](https://github.com/prooph/pdo-snapshot-store/pull/31) ([codeliner](https://github.com/codeliner)) 15 | 16 | ## [v1.5.2](https://github.com/prooph/pdo-snapshot-store/tree/v1.5.2) (2019-04-26) 17 | 18 | [Full Changelog](https://github.com/prooph/pdo-snapshot-store/compare/v1.5.1...v1.5.2) 19 | 20 | **Implemented enhancements:** 21 | 22 | - Correctly escape schema namespaced table name in the PostgreSQL. [\#30](https://github.com/prooph/pdo-snapshot-store/pull/30) ([ghettovoice](https://github.com/ghettovoice)) 23 | 24 | **Closed issues:** 25 | 26 | - PdoSnapshotStoreFactory is trying to modificate 'config' object [\#28](https://github.com/prooph/pdo-snapshot-store/issues/28) 27 | - Can't stop snapshotter when it's running [\#26](https://github.com/prooph/pdo-snapshot-store/issues/26) 28 | 29 | **Merged pull requests:** 30 | 31 | - remove docheader script [\#29](https://github.com/prooph/pdo-snapshot-store/pull/29) ([basz](https://github.com/basz)) 32 | - Update cs headers [\#27](https://github.com/prooph/pdo-snapshot-store/pull/27) ([basz](https://github.com/basz)) 33 | 34 | ## [v1.5.1](https://github.com/prooph/pdo-snapshot-store/tree/v1.5.1) (2018-05-16) 35 | 36 | [Full Changelog](https://github.com/prooph/pdo-snapshot-store/compare/v1.5.0...v1.5.1) 37 | 38 | **Fixed bugs:** 39 | 40 | - Quote table names in shapshot store queries [\#25](https://github.com/prooph/pdo-snapshot-store/pull/25) ([fritz-gerneth](https://github.com/fritz-gerneth)) 41 | - adds PKs to schema's [\#24](https://github.com/prooph/pdo-snapshot-store/pull/24) ([basz](https://github.com/basz)) 42 | 43 | **Merged pull requests:** 44 | 45 | - Don't remove deprecated config option [\#22](https://github.com/prooph/pdo-snapshot-store/pull/22) ([sandrokeil](https://github.com/sandrokeil)) 46 | - Issues/19 [\#21](https://github.com/prooph/pdo-snapshot-store/pull/21) ([basz](https://github.com/basz)) 47 | 48 | ## [v1.5.0](https://github.com/prooph/pdo-snapshot-store/tree/v1.5.0) (2018-04-15) 49 | 50 | [Full Changelog](https://github.com/prooph/pdo-snapshot-store/compare/v1.4.0...v1.5.0) 51 | 52 | **Implemented enhancements:** 53 | 54 | - Consistent connection key [\#19](https://github.com/prooph/pdo-snapshot-store/issues/19) 55 | 56 | **Fixed bugs:** 57 | 58 | - snapshot have no index \(postgres\) [\#23](https://github.com/prooph/pdo-snapshot-store/issues/23) 59 | 60 | **Closed issues:** 61 | 62 | - snapshots are created over and over again Postgres [\#20](https://github.com/prooph/pdo-snapshot-store/issues/20) 63 | 64 | ## [v1.4.0](https://github.com/prooph/pdo-snapshot-store/tree/v1.4.0) (2017-12-17) 65 | 66 | [Full Changelog](https://github.com/prooph/pdo-snapshot-store/compare/v1.3.0...v1.4.0) 67 | 68 | **Implemented enhancements:** 69 | 70 | - test php 7.2 on travis [\#18](https://github.com/prooph/pdo-snapshot-store/pull/18) ([prolic](https://github.com/prolic)) 71 | 72 | **Merged pull requests:** 73 | 74 | - Restructure docs [\#17](https://github.com/prooph/pdo-snapshot-store/pull/17) ([codeliner](https://github.com/codeliner)) 75 | 76 | ## [v1.3.0](https://github.com/prooph/pdo-snapshot-store/tree/v1.3.0) (2017-07-30) 77 | 78 | [Full Changelog](https://github.com/prooph/pdo-snapshot-store/compare/v1.2.0...v1.3.0) 79 | 80 | **Implemented enhancements:** 81 | 82 | - Flag / Method to turn off transaction handling [\#15](https://github.com/prooph/pdo-snapshot-store/issues/15) 83 | - disable transaction handling [\#16](https://github.com/prooph/pdo-snapshot-store/pull/16) ([prolic](https://github.com/prolic)) 84 | 85 | ## [v1.2.0](https://github.com/prooph/pdo-snapshot-store/tree/v1.2.0) (2017-05-30) 86 | 87 | [Full Changelog](https://github.com/prooph/pdo-snapshot-store/compare/v1.1.2...v1.2.0) 88 | 89 | **Implemented enhancements:** 90 | 91 | - Catch PDOExceptions and check error codes [\#14](https://github.com/prooph/pdo-snapshot-store/pull/14) ([dragosprotung](https://github.com/dragosprotung)) 92 | 93 | ## [v1.1.2](https://github.com/prooph/pdo-snapshot-store/tree/v1.1.2) (2017-05-12) 94 | 95 | [Full Changelog](https://github.com/prooph/pdo-snapshot-store/compare/v1.1.1...v1.1.2) 96 | 97 | **Fixed bugs:** 98 | 99 | - Fixed aggregate root serialized value binding [\#13](https://github.com/prooph/pdo-snapshot-store/pull/13) ([dragosprotung](https://github.com/dragosprotung)) 100 | 101 | ## [v1.1.1](https://github.com/prooph/pdo-snapshot-store/tree/v1.1.1) (2017-04-06) 102 | 103 | [Full Changelog](https://github.com/prooph/pdo-snapshot-store/compare/v1.1.0...v1.1.1) 104 | 105 | **Merged pull requests:** 106 | 107 | - Missed a typo [\#12](https://github.com/prooph/pdo-snapshot-store/pull/12) ([basz](https://github.com/basz)) 108 | 109 | ## [v1.1.0](https://github.com/prooph/pdo-snapshot-store/tree/v1.1.0) (2017-04-06) 110 | 111 | [Full Changelog](https://github.com/prooph/pdo-snapshot-store/compare/v1.0.0...v1.1.0) 112 | 113 | **Merged pull requests:** 114 | 115 | - Add flexible serializer [\#11](https://github.com/prooph/pdo-snapshot-store/pull/11) ([basz](https://github.com/basz)) 116 | 117 | ## [v1.0.0](https://github.com/prooph/pdo-snapshot-store/tree/v1.0.0) (2017-03-30) 118 | 119 | [Full Changelog](https://github.com/prooph/pdo-snapshot-store/compare/v1.0.0-beta2...v1.0.0) 120 | 121 | **Implemented enhancements:** 122 | 123 | - Change SnapshotStore Interface [\#5](https://github.com/prooph/pdo-snapshot-store/issues/5) 124 | - Use camel-case PDO =\> Pdo [\#3](https://github.com/prooph/pdo-snapshot-store/issues/3) 125 | - remove connection options setup in factory, add docs [\#10](https://github.com/prooph/pdo-snapshot-store/pull/10) ([prolic](https://github.com/prolic)) 126 | - Add possibility to remove all snapshots by aggregate type [\#8](https://github.com/prooph/pdo-snapshot-store/pull/8) ([prolic](https://github.com/prolic)) 127 | - change snapshot store interfaces [\#6](https://github.com/prooph/pdo-snapshot-store/pull/6) ([prolic](https://github.com/prolic)) 128 | - new snapshot store repo [\#4](https://github.com/prooph/pdo-snapshot-store/pull/4) ([prolic](https://github.com/prolic)) 129 | - simplify query [\#2](https://github.com/prooph/pdo-snapshot-store/pull/2) ([prolic](https://github.com/prolic)) 130 | - add implementation and tests [\#1](https://github.com/prooph/pdo-snapshot-store/pull/1) ([prolic](https://github.com/prolic)) 131 | 132 | **Merged pull requests:** 133 | 134 | - fix namespace organization [\#9](https://github.com/prooph/pdo-snapshot-store/pull/9) ([prolic](https://github.com/prolic)) 135 | - update to use psr\container [\#7](https://github.com/prooph/pdo-snapshot-store/pull/7) ([basz](https://github.com/basz)) 136 | 137 | ## [v1.0.0-beta2](https://github.com/prooph/pdo-snapshot-store/tree/v1.0.0-beta2) (2017-01-12) 138 | 139 | [Full Changelog](https://github.com/prooph/pdo-snapshot-store/compare/v1.0.0-beta1...v1.0.0-beta2) 140 | 141 | ## [v1.0.0-beta1](https://github.com/prooph/pdo-snapshot-store/tree/v1.0.0-beta1) (2016-12-13) 142 | 143 | [Full Changelog](https://github.com/prooph/pdo-snapshot-store/compare/e1159674fc127c5a9463802ce35c07c22f14734f...v1.0.0-beta1) 144 | 145 | 146 | 147 | \* *This Changelog was automatically generated by [github_changelog_generator](https://github.com/github-changelog-generator/github-changelog-generator)* 148 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016-2019, Alexander Miertsch 2 | Copyright (c) 2016-2019, 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 prooph 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. 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Prooph PDO Snapshot Store 2 | 3 | [![Continuous Integration](https://github.com/prooph/pdo-snapshot-store/actions/workflows/continuous-integration.yml/badge.svg)](https://github.com/prooph/pdo-snapshot-store/actions/workflows/continuous-integration.yml) 4 | [![Coverage Status](https://coveralls.io/repos/prooph/pdo-snapshot-store/badge.svg?branch=master&service=github)](https://coveralls.io/github/prooph/pdo-snapshot-store?branch=master) 5 | [![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/prooph/improoph) 6 | 7 | ## Important 8 | 9 | This library will receive support until December 31, 2019 and will then be deprecated. 10 | 11 | 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) 12 | 13 | ## Overview 14 | 15 | PDO implementation of snapshot store 16 | 17 | ## Installation 18 | 19 | You can install prooph/pdo-snapshot-store via composer by adding `"prooph/pdo-snapshot-store": "^1.0"` as requirement to your composer.json. 20 | 21 | ## Upgrade 22 | 23 | If you come from version 1.4.0 you are advised to manually update the table schema to fix an omitted primary key. You can issue the following statements or drop the snapshot table, recreate them from the provided scripts and restart projections. 24 | 25 | MySql 26 | 27 | ```sql 28 | ALTER TABLE `snapshots` DROP INDEX `ix_aggregate_id`, ADD PRIMARY KEY(`aggregate_id`); 29 | ``` 30 | 31 | Postgres 32 | 33 | ```sql 34 | ALTER TABLE "snapshots" DROP CONSTRAINT "snapshots_aggregate_id_key", ADD PRIMARY KEY ("aggregate_id"); 35 | ``` 36 | 37 | ## Support 38 | 39 | - Ask questions on Stack Overflow tagged with [#prooph](https://stackoverflow.com/questions/tagged/prooph). 40 | - File issues at [https://github.com/prooph/pdo-snapshot-store/issues](https://github.com/prooph/pdo-snapshot-store/issues). 41 | - Say hello in the [prooph gitter](https://gitter.im/prooph/improoph) chat. 42 | 43 | ## Contribute 44 | 45 | Please feel free to fork and extend existing or add new plugins and send a pull request with your changes! 46 | To establish a consistent code quality, please provide unit tests for all your changes and may adapt the documentation. 47 | 48 | ## License 49 | 50 | Released under the [New BSD License](LICENSE). 51 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "prooph/pdo-snapshot-store", 3 | "description": "PDO Snapshot Store Implementation", 4 | "type": "library", 5 | "license": "BSD-3-Clause", 6 | "homepage": "http://getprooph.org/", 7 | "authors": [ 8 | { 9 | "name": "Alexander Miertsch", 10 | "email": "kontakt@codeliner.ws" 11 | }, 12 | { 13 | "name": "Sascha-Oliver Prolic", 14 | "email": "saschaprolic@googlemail.com" 15 | } 16 | ], 17 | "keywords": [ 18 | "EventStore", 19 | "EventSourcing", 20 | "DDD", 21 | "prooph" 22 | ], 23 | "minimum-stability": "dev", 24 | "prefer-stable": true, 25 | "require": { 26 | "php": "^7.4 || ^8.0", 27 | "prooph/snapshot-store": "^1.1" 28 | }, 29 | "require-dev": { 30 | "php-coveralls/php-coveralls": "^2.2", 31 | "phpspec/prophecy": "^1.10.3", 32 | "phpunit/phpunit": "^9.5", 33 | "prooph/bookdown-template": "^0.2.3", 34 | "prooph/php-cs-fixer-config": "^0.5", 35 | "psr/container": "^1.0", 36 | "sandrokeil/interop-config": "^2.0.1", 37 | "sebastian/comparator": "^4.0" 38 | }, 39 | "suggest": { 40 | "prooph/pdo-event-store": "^1.0 For usage with MySQL or Postgres as event store", 41 | "prooph/snapshotter": "^2.0 Taking snapshots with ease", 42 | "psr/container": "^1.0 for usage of provided factories", 43 | "sandrokeil/interop-config": "^2.0.1 for usage of provided factories" 44 | }, 45 | "conflict": { 46 | "sandrokeil/interop-config": "<2.0.1" 47 | }, 48 | "autoload": { 49 | "psr-4": { 50 | "Prooph\\SnapshotStore\\Pdo\\": "src/" 51 | } 52 | }, 53 | "autoload-dev": { 54 | "psr-4": { 55 | "ProophTest\\SnapshotStore\\Pdo\\": "tests/" 56 | } 57 | }, 58 | "extra": { 59 | "branch-alias": { 60 | "dev-master": "1.1-dev" 61 | } 62 | }, 63 | "scripts": { 64 | "check": [ 65 | "@cs", 66 | "@test" 67 | ], 68 | "cs": "php-cs-fixer fix -v --diff --dry-run", 69 | "cs-fix": "php-cs-fixer fix -v --diff", 70 | "test": "phpunit" 71 | }, 72 | "config": { 73 | "sort-packages": true 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /docker-compose-tests.yml: -------------------------------------------------------------------------------- 1 | version: '2' 2 | services: 3 | composer: 4 | image: prooph/composer:7.1 5 | volumes: 6 | - .:/app 7 | links: 8 | - postgres:postgres 9 | - mysql:mysql 10 | 11 | postgres: 12 | image: postgres:alpine 13 | ports: 14 | - 5432:5432 15 | environment: 16 | - POSTGRES_DB=snapshot_tests 17 | 18 | mysql: 19 | image: mysql 20 | ports: 21 | - 3306:3306 22 | environment: 23 | - MYSQL_ROOT_PASSWORD= 24 | - MYSQL_ALLOW_EMPTY_PASSWORD=yes 25 | - MYSQL_DATABASE=snapshot_tests 26 | -------------------------------------------------------------------------------- /scripts/mysql_snapshot_table.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE `snapshots` ( 2 | `aggregate_id` VARCHAR(150) NOT NULL, 3 | `aggregate_type` VARCHAR(150) NOT NULL, 4 | `last_version` INT(11) NOT NULL, 5 | `created_at` CHAR(26) NOT NULL, 6 | `aggregate_root` BLOB, 7 | PRIMARY KEY (`aggregate_id`) 8 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin; 9 | -------------------------------------------------------------------------------- /scripts/postgres_snapshot_table.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE snapshots ( 2 | aggregate_id VARCHAR(150) NOT NULL, 3 | aggregate_type VARCHAR(150) NOT NULL, 4 | last_version INT NOT NULL, 5 | created_at CHAR(26) NOT NULL, 6 | aggregate_root BYTEA, 7 | PRIMARY KEY (aggregate_id) 8 | ); 9 | -------------------------------------------------------------------------------- /src/Container/PdoSnapshotStoreFactory.php: -------------------------------------------------------------------------------- 1 | 6 | * (c) 2016-2022 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\Pdo\Container; 15 | 16 | use Interop\Config\ConfigurationTrait; 17 | use Interop\Config\ProvidesDefaultOptions; 18 | use Interop\Config\RequiresConfigId; 19 | use Interop\Config\RequiresMandatoryOptions; 20 | use Prooph\SnapshotStore\CallbackSerializer; 21 | use Prooph\SnapshotStore\Pdo\PdoSnapshotStore; 22 | use Prooph\SnapshotStore\Serializer; 23 | use Psr\Container\ContainerInterface; 24 | 25 | class PdoSnapshotStoreFactory implements ProvidesDefaultOptions, RequiresConfigId, RequiresMandatoryOptions 26 | { 27 | use ConfigurationTrait; 28 | 29 | /** 30 | * @var string 31 | */ 32 | private $configId; 33 | 34 | /** 35 | * Creates a new instance from a specified config, specifically meant to be used as static factory. 36 | * 37 | * In case you want to use another config key than provided by the factories, you can add the following factory to 38 | * your config: 39 | * 40 | * 41 | * [PdoSnapshotStoreFactory::class, 'service_name'], 44 | * ]; 45 | * 46 | * 47 | * @throws \InvalidArgumentException 48 | */ 49 | public static function __callStatic(string $name, array $arguments): PdoSnapshotStore 50 | { 51 | if (! isset($arguments[0]) || ! $arguments[0] instanceof ContainerInterface) { 52 | throw new \InvalidArgumentException( 53 | \sprintf('The first argument must be of type %s', ContainerInterface::class) 54 | ); 55 | } 56 | 57 | return (new static($name))->__invoke($arguments[0]); 58 | } 59 | 60 | public function __invoke(ContainerInterface $container): PdoSnapshotStore 61 | { 62 | $config = $container->get('config'); 63 | 64 | /** repair legacy connection_service config key*/ 65 | (function (&$config): void { 66 | foreach ($this->dimensions() as $dimension) { 67 | $config = &$config[$dimension]; 68 | } 69 | 70 | $config = &$config[$this->configId] ?? []; 71 | 72 | if (! isset($config['connection']) && isset($config['connection_service'])) { 73 | $config['connection'] = $config['connection_service']; 74 | } 75 | })($config); 76 | 77 | $config = $this->options($config, $this->configId); 78 | 79 | $connection = $container->get($config['connection']); 80 | $serializer = $config['serializer'] instanceof Serializer ? $config['serializer'] : $container->get($config['serializer']); 81 | 82 | return new PdoSnapshotStore( 83 | $connection, 84 | $config['snapshot_table_map'], 85 | $config['default_snapshot_table_name'], 86 | $serializer, 87 | $config['disable_transaction_handling'] 88 | ); 89 | } 90 | 91 | public function __construct(string $configId = 'default') 92 | { 93 | $this->configId = $configId; 94 | } 95 | 96 | public function dimensions(): iterable 97 | { 98 | return ['prooph', 'pdo_snapshot_store']; 99 | } 100 | 101 | public function mandatoryOptions(): iterable 102 | { 103 | return [ 104 | 'connection', 105 | ]; 106 | } 107 | 108 | public function defaultOptions(): iterable 109 | { 110 | return [ 111 | 'snapshot_table_map' => [], 112 | 'default_snapshot_table_name' => 'snapshots', 113 | 'serializer' => new CallbackSerializer(null, null), 114 | 'disable_transaction_handling' => false, 115 | ]; 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/Exception/RuntimeException.php: -------------------------------------------------------------------------------- 1 | 6 | * (c) 2016-2022 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\Pdo\Exception; 15 | 16 | use RuntimeException as PHPRuntimeException; 17 | 18 | class RuntimeException extends PHPRuntimeException 19 | { 20 | public static function fromStatementErrorInfo(array $errorInfo): RuntimeException 21 | { 22 | return new self( 23 | \sprintf( 24 | "Error %s. \nError-Info: %s", 25 | $errorInfo[0], 26 | $errorInfo[2] 27 | ) 28 | ); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/PdoSnapshotStore.php: -------------------------------------------------------------------------------- 1 | 6 | * (c) 2016-2022 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\Pdo; 15 | 16 | use PDO; 17 | use PDOException; 18 | use Prooph\SnapshotStore\CallbackSerializer; 19 | use Prooph\SnapshotStore\Pdo\Exception\RuntimeException; 20 | use Prooph\SnapshotStore\Serializer; 21 | use Prooph\SnapshotStore\Snapshot; 22 | use Prooph\SnapshotStore\SnapshotStore; 23 | 24 | final class PdoSnapshotStore implements SnapshotStore 25 | { 26 | /** 27 | * @var PDO 28 | */ 29 | private $connection; 30 | 31 | /** 32 | * Custom sourceType to snapshot mapping 33 | * 34 | * @var array 35 | */ 36 | private $snapshotTableMap; 37 | 38 | /** 39 | * @var string 40 | */ 41 | private $defaultSnapshotTableName; 42 | 43 | /** 44 | * @var Serializer 45 | */ 46 | private $serializer; 47 | 48 | /** 49 | * @var bool 50 | */ 51 | private $disableTransactionHandling; 52 | 53 | /** 54 | * @var string 55 | */ 56 | private $vendor; 57 | 58 | public function __construct( 59 | PDO $connection, 60 | array $snapshotTableMap = [], 61 | string $defaultSnapshotTableName = 'snapshots', 62 | Serializer $serializer = null, 63 | bool $disableTransactionHandling = false 64 | ) { 65 | $this->connection = $connection; 66 | $this->snapshotTableMap = $snapshotTableMap; 67 | $this->defaultSnapshotTableName = $defaultSnapshotTableName; 68 | $this->serializer = $serializer ?: new CallbackSerializer(null, null); 69 | $this->disableTransactionHandling = $disableTransactionHandling; 70 | $this->vendor = $this->connection->getAttribute(PDO::ATTR_DRIVER_NAME); 71 | } 72 | 73 | public function get(string $aggregateType, string $aggregateId): ?Snapshot 74 | { 75 | $table = $this->getTableName($aggregateType); 76 | 77 | $query = <<connection->prepare($query); 82 | 83 | try { 84 | $statement->execute([$aggregateId]); 85 | } catch (PDOException $exception) { 86 | // ignore and check error code 87 | } 88 | 89 | if ($statement->errorCode() !== '00000') { 90 | throw RuntimeException::fromStatementErrorInfo($statement->errorInfo()); 91 | } 92 | 93 | $result = $statement->fetch(\PDO::FETCH_OBJ); 94 | 95 | if (! $result) { 96 | return null; 97 | } 98 | 99 | return new Snapshot( 100 | $aggregateType, 101 | $aggregateId, 102 | $this->unserializeAggregateRoot($result->aggregate_root), 103 | (int) $result->last_version, 104 | \DateTimeImmutable::createFromFormat('Y-m-d\TH:i:s.u', $result->created_at, new \DateTimeZone('UTC')) 105 | ); 106 | } 107 | 108 | public function save(Snapshot ...$snapshots): void 109 | { 110 | if (empty($snapshots)) { 111 | return; 112 | } 113 | 114 | $deletes = []; 115 | $inserts = []; 116 | 117 | foreach ($snapshots as $snapshot) { 118 | $deletes[$this->getTableName($snapshot->aggregateType())][] = $snapshot->aggregateId(); 119 | $inserts[$this->getTableName($snapshot->aggregateType())][] = $snapshot; 120 | } 121 | 122 | $statements = []; 123 | 124 | foreach ($deletes as $table => $aggregateIds) { 125 | $ids = \implode(', ', \array_fill(0, \count($aggregateIds), '?')); 126 | $deleteSql = <<connection->prepare($deleteSql); 130 | foreach ($aggregateIds as $position => $aggregateId) { 131 | $statement->bindValue($position + 1, $aggregateId); 132 | } 133 | 134 | $statements[] = $statement; 135 | } 136 | 137 | foreach ($inserts as $table => $snapshots) { 138 | $allPlaces = \implode(', ', \array_fill(0, \count($snapshots), '(?, ?, ?, ?, ?)')); 139 | $insertSql = <<connection->prepare($insertSql); 144 | foreach ($snapshots as $index => $snapshot) { 145 | $position = $index * 5; 146 | $statement->bindValue(++$position, $snapshot->aggregateId()); 147 | $statement->bindValue(++$position, $snapshot->aggregateType()); 148 | $statement->bindValue(++$position, $snapshot->lastVersion(), PDO::PARAM_INT); 149 | $statement->bindValue(++$position, $snapshot->createdAt()->format('Y-m-d\TH:i:s.u')); 150 | $statement->bindValue(++$position, $this->serializer->serialize($snapshot->aggregateRoot()), PDO::PARAM_LOB); 151 | } 152 | $statements[] = $statement; 153 | } 154 | 155 | if (! $this->disableTransactionHandling) { 156 | $this->connection->beginTransaction(); 157 | } 158 | 159 | try { 160 | foreach ($statements as $statement) { 161 | $statement->execute(); 162 | } 163 | } catch (PDOException $exception) { 164 | if (! $this->disableTransactionHandling) { 165 | $this->connection->rollBack(); 166 | } 167 | 168 | throw RuntimeException::fromStatementErrorInfo($statement->errorInfo()); 169 | } 170 | 171 | if (! $this->disableTransactionHandling) { 172 | $this->connection->commit(); 173 | } 174 | } 175 | 176 | public function removeAll(string $aggregateType): void 177 | { 178 | $table = $this->getTableName($aggregateType); 179 | 180 | $sql = <<connection->prepare($sql); 185 | 186 | if (! $this->disableTransactionHandling) { 187 | $this->connection->beginTransaction(); 188 | } 189 | 190 | try { 191 | $statement->execute([$aggregateType]); 192 | } catch (PDOException $exception) { 193 | // ignore and check error code 194 | } 195 | 196 | if ($statement->errorCode() !== '00000') { 197 | if (! $this->disableTransactionHandling) { 198 | $this->connection->rollBack(); 199 | } 200 | 201 | throw RuntimeException::fromStatementErrorInfo($statement->errorInfo()); 202 | } 203 | 204 | if (! $this->disableTransactionHandling) { 205 | $this->connection->commit(); 206 | } 207 | } 208 | 209 | private function getTableName(string $aggregateType): string 210 | { 211 | if (isset($this->snapshotTableMap[$aggregateType])) { 212 | $tableName = $this->snapshotTableMap[$aggregateType]; 213 | } else { 214 | $tableName = $this->defaultSnapshotTableName; 215 | } 216 | 217 | switch ($this->vendor) { 218 | case 'pgsql': 219 | $pos = \strpos($tableName, '.'); 220 | 221 | if (false === $pos) { 222 | return '"' . $tableName . '"'; 223 | } 224 | 225 | $schema = \substr($tableName, 0, $pos); 226 | $table = \substr($tableName, $pos + 1); 227 | 228 | return '"' . $schema . '"."' . $table . '"'; 229 | default: 230 | return "`$tableName`"; 231 | } 232 | } 233 | 234 | /** 235 | * @param string|resource $serialized 236 | * @return object|array 237 | */ 238 | private function unserializeAggregateRoot($serialized) 239 | { 240 | if (\is_resource($serialized)) { 241 | $serialized = \stream_get_contents($serialized); 242 | } 243 | 244 | return $this->serializer->unserialize($serialized); 245 | } 246 | } 247 | --------------------------------------------------------------------------------