diff --git a/.travis.yml b/.travis.yml index c1e034cf..bff1a12f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,44 +3,46 @@ language: php matrix: fast_finish: true include: - - php: 7.1 - env: - - DEPENDENCIES="" - - EXECUTE_CS_CHECK=true - - TEST_COVERAGE=true - - php: 7.1 - env: - - DEPENDENCIES="--prefer-lowest --prefer-stable" - - php: 7.2 - env: - - DEPENDENCIES="" - - php: 7.2 - env: - - DEPENDENCIES="--prefer-lowest --prefer-stable" + - php: 7.2 + env: + - DEPENDENCIES="" + - EXECUTE_CS_CHECK=true + - TEST_COVERAGE=true + - php: 7.2 + env: + - DEPENDENCIES="--prefer-lowest --prefer-stable" + - php: 7.3 + env: + - DEPENDENCIES="" + - TEST_COVERAGE=true + - php: 7.3 + env: + - DEPENDENCIES="--prefer-lowest --prefer-stable" cache: directories: - - $HOME/.composer/cache - - $HOME/.php-cs-fixer - - $HOME/.local + - $HOME/.composer/cache + - $HOME/.php-cs-fixer + - $HOME/.local before_script: - - mkdir -p "$HOME/.php-cs-fixer" - - phpenv config-rm xdebug.ini - - composer self-update - - composer update --prefer-dist $DEPENDENCIES +- mkdir -p "$HOME/.php-cs-fixer" +- if php --ri xdebug >/dev/null; then phpenv config-rm xdebug.ini; fi +- composer self-update +- composer update --prefer-dist $DEPENDENCIES +- if [[ $EXECUTE_CS_CHECK == 'true' ]]; then composer require prooph/php-cs-fixer-config ^0.3; fi script: - - 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 - - if [[ $EXECUTE_CS_CHECK == 'true' ]]; then ./vendor/bin/php-cs-fixer fix -v --diff --dry-run; fi +- if [[ $TEST_COVERAGE == 'true' ]]; then php -dzend_extension=xdebug.so ./vendor/bin/phpunit --exclude-group=ignore --coverage-text --coverage-clover ./build/logs/clover.xml; else ./vendor/bin/phpunit --exclude-group=ignore; fi +- if [[ $EXECUTE_CS_CHECK == 'true' ]]; then ./vendor/bin/php-cs-fixer fix -v --diff --dry-run; fi after_success: - - if [[ $TEST_COVERAGE == 'true' ]]; then php vendor/bin/coveralls -v; fi +- if [[ $TEST_COVERAGE == 'true' ]]; then php vendor/bin/coveralls -v; fi notifications: webhooks: urls: - - https://webhooks.gitter.im/e/61c75218816eebde4486 + - https://webhooks.gitter.im/e/61c75218816eebde4486 on_success: change # options: [always|never|change] default: always on_failure: always # options: [always|never|change] default: always on_start: never # options: [always|never|change] default: always diff --git a/CHANGELOG.md b/CHANGELOG.md deleted file mode 100644 index 8b31dbff..00000000 --- a/CHANGELOG.md +++ /dev/null @@ -1,834 +0,0 @@ -# Change Log - -## [v7.5.0](https://github.com/prooph/event-store/tree/v7.5.0) - -[Full Changelog](https://github.com/prooph/event-store/compare/v7.4.0...v7.5.0) - -**Implemented enhancements:** - -- Update fromStream for Projection, Query and ReadModelProjection [\#350](https://github.com/prooph/event-store/pull/350) ([fjogeleit](https://github.com/fjogeleit)) - -**Merged pull requests:** - -- Update cs headers [\#351](https://github.com/prooph/event-store/pull/351) ([basz](https://github.com/basz)) -- Fix branch-alias [\#348](https://github.com/prooph/event-store/pull/348) ([enumag](https://github.com/enumag)) -- Fix method name in doc [\#347](https://github.com/prooph/event-store/pull/347) ([rodion-k](https://github.com/rodion-k)) - -## [v7.4.0](https://github.com/prooph/event-store/tree/v7.4.0) (2018-09-23) -[Full Changelog](https://github.com/prooph/event-store/compare/v7.3.8...v7.4.0) - -**Implemented enhancements:** - -- \[WIP\] feature/countable stream iterator [\#346](https://github.com/prooph/event-store/pull/346) ([basz](https://github.com/basz)) - -## [v7.3.8](https://github.com/prooph/event-store/tree/v7.3.8) (2018-08-27) -[Full Changelog](https://github.com/prooph/event-store/compare/v7.3.7...v7.3.8) - -**Implemented enhancements:** - -- Create original event-store implementation [\#327](https://github.com/prooph/event-store/issues/327) - -**Fixed bugs:** - -- fix expected projections event count [\#345](https://github.com/prooph/event-store/pull/345) ([basz](https://github.com/basz)) - -**Closed issues:** - -- Documentation uses wrong pcntl signal? [\#344](https://github.com/prooph/event-store/issues/344) -- InMemoryEventStore can't read it's own events if in transaction [\#342](https://github.com/prooph/event-store/issues/342) - -## [v7.3.7](https://github.com/prooph/event-store/tree/v7.3.7) (2018-06-12) -[Full Changelog](https://github.com/prooph/event-store/compare/v7.3.6...v7.3.7) - -**Implemented enhancements:** - -- Preserve original exceptions in parameters [\#341](https://github.com/prooph/event-store/pull/341) ([enumag](https://github.com/enumag)) - -**Merged pull requests:** - -- add missing closing bracket [\#340](https://github.com/prooph/event-store/pull/340) ([darrylhein](https://github.com/darrylhein)) - -## [v7.3.6](https://github.com/prooph/event-store/tree/v7.3.6) (2018-04-30) -[Full Changelog](https://github.com/prooph/event-store/compare/v7.3.5...v7.3.6) - -**Implemented enhancements:** - -- pcntl\_signal\_dispatch should be called after each event was processed [\#337](https://github.com/prooph/event-store/issues/337) -- Dispatch PCNTL signal after each event for immediately shutdown [\#338](https://github.com/prooph/event-store/pull/338) ([sandrokeil](https://github.com/sandrokeil)) - -**Merged pull requests:** - -- Add query option OPTION\_PCNTL\_DISPATCH \(\#337\) [\#339](https://github.com/prooph/event-store/pull/339) ([sandrokeil](https://github.com/sandrokeil)) - -## [v7.3.5](https://github.com/prooph/event-store/tree/v7.3.5) (2018-04-26) -[Full Changelog](https://github.com/prooph/event-store/compare/v7.3.4...v7.3.5) - -**Implemented enhancements:** - -- allows NonTransactionalInMemoryEventStore for projections [\#336](https://github.com/prooph/event-store/pull/336) ([oqq](https://github.com/oqq)) - -**Closed issues:** - -- InMemoryProjectionManager could not handle NonTransactional..EventStore [\#335](https://github.com/prooph/event-store/issues/335) - -## [v7.3.4](https://github.com/prooph/event-store/tree/v7.3.4) (2018-04-26) -[Full Changelog](https://github.com/prooph/event-store/compare/v7.3.3...v7.3.4) - -**Implemented enhancements:** - -- Add update lock threshold to read model prj, too [\#332](https://github.com/prooph/event-store/pull/332) ([codeliner](https://github.com/codeliner)) -- Add projection option update lock threshold [\#331](https://github.com/prooph/event-store/pull/331) ([codeliner](https://github.com/codeliner)) - -**Fixed bugs:** - -- fix upcasting iterator [\#334](https://github.com/prooph/event-store/pull/334) ([prolic](https://github.com/prolic)) - -**Merged pull requests:** - -- Fix time unit for lock timeout ms [\#333](https://github.com/prooph/event-store/pull/333) ([codeliner](https://github.com/codeliner)) - -## [v7.3.3](https://github.com/prooph/event-store/tree/v7.3.3) (2018-03-25) -[Full Changelog](https://github.com/prooph/event-store/compare/v7.3.2...v7.3.3) - -**Fixed bugs:** - -- add test cases for multiple calls to reset/stop/delete projection [\#329](https://github.com/prooph/event-store/pull/329) ([prolic](https://github.com/prolic)) - -**Merged pull requests:** - -- Fix typo [\#323](https://github.com/prooph/event-store/pull/323) ([denniskoenig](https://github.com/denniskoenig)) - -## [v7.3.2](https://github.com/prooph/event-store/tree/v7.3.2) (2018-02-10) -[Full Changelog](https://github.com/prooph/event-store/compare/v6.6.1...v7.3.2) - -**Fixed bugs:** - -- Metadata Aggregate ID can be an integer and causes errors \(v6\) [\#314](https://github.com/prooph/event-store/issues/314) -- fixed metadata enum class doc-blocks [\#320](https://github.com/prooph/event-store/pull/320) ([ged15](https://github.com/ged15)) - -**Merged pull requests:** - -- it's 2018! [\#321](https://github.com/prooph/event-store/pull/321) ([prolic](https://github.com/prolic)) - -## [v6.6.1](https://github.com/prooph/event-store/tree/v6.6.1) (2017-12-17) -[Full Changelog](https://github.com/prooph/event-store/compare/v7.3.1...v6.6.1) - -**Closed issues:** - -- Thank you all and happy Thanksgiving [\#312](https://github.com/prooph/event-store/issues/312) - -**Merged pull requests:** - -- Add explicit string type cast for metadata aggregate id \(fix \#314\) [\#315](https://github.com/prooph/event-store/pull/315) ([sandrokeil](https://github.com/sandrokeil)) - -## [v7.3.1](https://github.com/prooph/event-store/tree/v7.3.1) (2017-12-05) -[Full Changelog](https://github.com/prooph/event-store/compare/v7.3.0...v7.3.1) - -**Implemented enhancements:** - -- test php 7.2 [\#313](https://github.com/prooph/event-store/pull/313) ([prolic](https://github.com/prolic)) - -## [v7.3.0](https://github.com/prooph/event-store/tree/v7.3.0) (2017-11-18) -[Full Changelog](https://github.com/prooph/event-store/compare/v7.2.2...v7.3.0) - -**Implemented enhancements:** - -- Non transactional in memory event store [\#307](https://github.com/prooph/event-store/issues/307) -- Allow marc-mabe/php-enum v3 [\#311](https://github.com/prooph/event-store/pull/311) ([bgaleotti](https://github.com/bgaleotti)) -- Introducing NonTransactionalInMemoryEventStore [\#308](https://github.com/prooph/event-store/pull/308) ([tomcizek](https://github.com/tomcizek)) - -**Fixed bugs:** - -- Catch Throwable in transactional\(\) [\#309](https://github.com/prooph/event-store/pull/309) ([jiripudil](https://github.com/jiripudil)) - -**Closed issues:** - -- Documentation out of sync [\#300](https://github.com/prooph/event-store/issues/300) -- Aggregate root not found in MariaDb [\#297](https://github.com/prooph/event-store/issues/297) -- Promote LTS for current major version [\#199](https://github.com/prooph/event-store/issues/199) - -**Merged pull requests:** - -- Fix exception in InMemoryEventStoreReadModelProjector [\#310](https://github.com/prooph/event-store/pull/310) ([enumag](https://github.com/enumag)) -- Fix typos [\#306](https://github.com/prooph/event-store/pull/306) ([denniskoenig](https://github.com/denniskoenig)) -- Enhancement: Keep packages sorted in composer.json [\#305](https://github.com/prooph/event-store/pull/305) ([localheinz](https://github.com/localheinz)) -- Restructure docs [\#304](https://github.com/prooph/event-store/pull/304) ([codeliner](https://github.com/codeliner)) -- Link to SO [\#303](https://github.com/prooph/event-store/pull/303) ([codeliner](https://github.com/codeliner)) -- Add video introduction [\#302](https://github.com/prooph/event-store/pull/302) ([codeliner](https://github.com/codeliner)) -- Proofread the docs [\#301](https://github.com/prooph/event-store/pull/301) ([camuthig](https://github.com/camuthig)) -- add version guidance [\#298](https://github.com/prooph/event-store/pull/298) ([prolic](https://github.com/prolic)) - -## [v7.2.2](https://github.com/prooph/event-store/tree/v7.2.2) (2017-09-11) -[Full Changelog](https://github.com/prooph/event-store/compare/v7.2.1...v7.2.2) - -**Fixed bugs:** - -- Bug in UpcastingIterator [\#295](https://github.com/prooph/event-store/issues/295) -- Fixed compability issues EmptyIterator [\#296](https://github.com/prooph/event-store/pull/296) ([hiddeco](https://github.com/hiddeco)) - -## [v7.2.1](https://github.com/prooph/event-store/tree/v7.2.1) (2017-07-15) -[Full Changelog](https://github.com/prooph/event-store/compare/v7.2.0...v7.2.1) - -**Fixed bugs:** - -- fix InMemoryEventStore [\#294](https://github.com/prooph/event-store/pull/294) ([prolic](https://github.com/prolic)) - -## [v7.2.0](https://github.com/prooph/event-store/tree/v7.2.0) (2017-07-01) -[Full Changelog](https://github.com/prooph/event-store/compare/v7.1.3...v7.2.0) - -**Implemented enhancements:** - -- Add projection option constants to trigger pcntl\_signal\_dispatch [\#293](https://github.com/prooph/event-store/pull/293) ([fritz-gerneth](https://github.com/fritz-gerneth)) - -## [v7.1.3](https://github.com/prooph/event-store/tree/v7.1.3) (2017-06-24) -[Full Changelog](https://github.com/prooph/event-store/compare/v7.1.2...v7.1.3) - -## [v7.1.2](https://github.com/prooph/event-store/tree/v7.1.2) (2017-06-24) -[Full Changelog](https://github.com/prooph/event-store/compare/v7.1.1...v7.1.2) - -**Implemented enhancements:** - -- add projection not found exception [\#291](https://github.com/prooph/event-store/pull/291) ([prolic](https://github.com/prolic)) - -## [v7.1.1](https://github.com/prooph/event-store/tree/v7.1.1) (2017-06-18) -[Full Changelog](https://github.com/prooph/event-store/compare/v7.1.0...v7.1.1) - -**Fixed bugs:** - -- add test it\_loads\_and\_saves\_within\_one\_transaction [\#290](https://github.com/prooph/event-store/pull/290) ([prolic](https://github.com/prolic)) - -**Merged pull requests:** - -- Fix spelling in test [\#289](https://github.com/prooph/event-store/pull/289) ([prolic](https://github.com/prolic)) - -## [v7.1.0](https://github.com/prooph/event-store/tree/v7.1.0) (2017-05-22) -[Full Changelog](https://github.com/prooph/event-store/compare/1.1.1...v7.1.0) - -**Implemented enhancements:** - -- Support message property filters [\#284](https://github.com/prooph/event-store/issues/284) -- better validation in metadata matcher [\#288](https://github.com/prooph/event-store/pull/288) ([prolic](https://github.com/prolic)) -- reduce duplicate code [\#287](https://github.com/prooph/event-store/pull/287) ([prolic](https://github.com/prolic)) -- Support message property filters + new operators [\#286](https://github.com/prooph/event-store/pull/286) ([prolic](https://github.com/prolic)) - -## [1.1.1](https://github.com/prooph/event-store/tree/1.1.1) (2017-05-16) -[Full Changelog](https://github.com/prooph/event-store/compare/1.1...1.1.1) - -## [1.1](https://github.com/prooph/event-store/tree/1.1) (2017-05-11) -[Full Changelog](https://github.com/prooph/event-store/compare/v7.0.1...1.1) - -**Implemented enhancements:** - -- add read only event store wrapper [\#285](https://github.com/prooph/event-store/pull/285) ([prolic](https://github.com/prolic)) - -**Closed issues:** - -- Trackable transaction in TransactionalActionEventEmitterEventStore [\#283](https://github.com/prooph/event-store/issues/283) - -## [v7.0.1](https://github.com/prooph/event-store/tree/v7.0.1) (2017-03-30) -[Full Changelog](https://github.com/prooph/event-store/compare/v7.0.0...v7.0.1) - -**Closed issues:** - -- Link @prolic's blog post in the docs [\#178](https://github.com/prooph/event-store/issues/178) - -## [v7.0.0](https://github.com/prooph/event-store/tree/v7.0.0) (2017-03-30) -[Full Changelog](https://github.com/prooph/event-store/compare/v7.0.0-beta3...v7.0.0) - -**Implemented enhancements:** - -- RFC some constraints on metadata and event stream names [\#191](https://github.com/prooph/event-store/issues/191) - -**Closed issues:** - -- Data too long for column 'locked\_until' [\#279](https://github.com/prooph/event-store/issues/279) - -**Merged pull requests:** - -- Docs [\#280](https://github.com/prooph/event-store/pull/280) ([prolic](https://github.com/prolic)) -- adds a clustered events constant to event emitter event stores [\#278](https://github.com/prooph/event-store/pull/278) ([oqq](https://github.com/oqq)) - -## [v7.0.0-beta3](https://github.com/prooph/event-store/tree/v7.0.0-beta3) (2017-03-13) -[Full Changelog](https://github.com/prooph/event-store/compare/1.0...v7.0.0-beta3) - -**Implemented enhancements:** - -- rename projection =\> projector where applicable [\#275](https://github.com/prooph/event-store/pull/275) ([prolic](https://github.com/prolic)) - -## [1.0](https://github.com/prooph/event-store/tree/1.0) (2017-03-13) -[Full Changelog](https://github.com/prooph/event-store/compare/v6.6.0...1.0) - -**Implemented enhancements:** - -- Make Projections more abstract [\#265](https://github.com/prooph/event-store/issues/265) -- Provide abstract event store test [\#257](https://github.com/prooph/event-store/issues/257) -- Add support for inline upcasting [\#254](https://github.com/prooph/event-store/issues/254) -- updates unit tests to reflect current implementation [\#276](https://github.com/prooph/event-store/pull/276) ([oqq](https://github.com/oqq)) -- Remove PHP\_INT\_MAX constant from interface [\#274](https://github.com/prooph/event-store/pull/274) ([prolic](https://github.com/prolic)) -- improves fetchProjectionNames method [\#268](https://github.com/prooph/event-store/pull/268) ([oqq](https://github.com/oqq)) -- Projection manager [\#267](https://github.com/prooph/event-store/pull/267) ([prolic](https://github.com/prolic)) -- allow to fetch stream names, category names and projection names [\#261](https://github.com/prooph/event-store/pull/261) ([prolic](https://github.com/prolic)) -- reset/stop/delete projections [\#258](https://github.com/prooph/event-store/pull/258) ([prolic](https://github.com/prolic)) -- Upcasting [\#255](https://github.com/prooph/event-store/pull/255) ([prolic](https://github.com/prolic)) - -**Fixed bugs:** - -- remove / from regexes [\#262](https://github.com/prooph/event-store/pull/262) ([prolic](https://github.com/prolic)) - -**Closed issues:** - -- Remove PHP\_INT\_MAX constant [\#271](https://github.com/prooph/event-store/issues/271) -- count\($stream\) [\#269](https://github.com/prooph/event-store/issues/269) - -**Merged pull requests:** - -- renames some vars [\#277](https://github.com/prooph/event-store/pull/277) ([oqq](https://github.com/oqq)) -- Small doc-block change, states are arrays not objects. [\#272](https://github.com/prooph/event-store/pull/272) ([bweston92](https://github.com/bweston92)) -- update prophecy [\#270](https://github.com/prooph/event-store/pull/270) ([basz](https://github.com/basz)) -- update to use psr\container [\#263](https://github.com/prooph/event-store/pull/263) ([basz](https://github.com/basz)) -- Update test it\_resumes\_projection\_from\_position [\#260](https://github.com/prooph/event-store/pull/260) ([rabbl](https://github.com/rabbl)) - -## [v6.6.0](https://github.com/prooph/event-store/tree/v6.6.0) (2017-02-10) -[Full Changelog](https://github.com/prooph/event-store/compare/v7.0.0-beta2...v6.6.0) - -**Implemented enhancements:** - -- RFC: EventStore::load method changes [\#252](https://github.com/prooph/event-store/issues/252) -- Change EventStore::load / loadReverse methods [\#253](https://github.com/prooph/event-store/pull/253) ([prolic](https://github.com/prolic)) -- add ReadOnlyEventStore interface [\#251](https://github.com/prooph/event-store/pull/251) ([prolic](https://github.com/prolic)) - -**Closed issues:** - -- Read-only event store interface [\#250](https://github.com/prooph/event-store/issues/250) -- ConfigurableAggregateTranslator: method\_exists vs. is\_callable [\#235](https://github.com/prooph/event-store/issues/235) - -**Merged pull requests:** - -- Replace method\_exists\(\) calls with is\_callable\(\) to fix \#235 [\#256](https://github.com/prooph/event-store/pull/256) ([shochdoerfer](https://github.com/shochdoerfer)) -- remove older suggestions [\#249](https://github.com/prooph/event-store/pull/249) ([basz](https://github.com/basz)) - -## [v7.0.0-beta2](https://github.com/prooph/event-store/tree/v7.0.0-beta2) (2017-01-12) -[Full Changelog](https://github.com/prooph/event-store/compare/v7.0.0-beta1...v7.0.0-beta2) - -**Implemented enhancements:** - -- RFC projections: if no new events found at all, sleep for 0.25 secs [\#236](https://github.com/prooph/event-store/issues/236) -- \[RFC\] Projection implementation [\#234](https://github.com/prooph/event-store/issues/234) -- updated interfaces in ActionEventEmitterEventStore [\#248](https://github.com/prooph/event-store/pull/248) ([prolic](https://github.com/prolic)) -- update event store decorator + query factory [\#247](https://github.com/prooph/event-store/pull/247) ([prolic](https://github.com/prolic)) -- Improve projection factories, use a simple callable definition [\#246](https://github.com/prooph/event-store/pull/246) ([sandrokeil](https://github.com/sandrokeil)) -- Add EventStoreDecorator interface [\#245](https://github.com/prooph/event-store/pull/245) ([prolic](https://github.com/prolic)) -- Outsource aggregate translator configuration again [\#242](https://github.com/prooph/event-store/pull/242) ([dropdevcoding](https://github.com/dropdevcoding)) -- Remove abstract classes in projections + sleep when no new events found + projection factories [\#239](https://github.com/prooph/event-store/pull/239) ([prolic](https://github.com/prolic)) -- update plugin registration [\#233](https://github.com/prooph/event-store/pull/233) ([prolic](https://github.com/prolic)) - -**Fixed bugs:** - -- Problem with ActionEventEmitterEventStore and Projections [\#240](https://github.com/prooph/event-store/issues/240) - -**Closed issues:** - -- Add EventStoreDecorator interface [\#244](https://github.com/prooph/event-store/issues/244) -- Outsource AggregateTranslator configuration [\#237](https://github.com/prooph/event-store/issues/237) -- \[RFC\] Simplify Event Store plugin registration [\#232](https://github.com/prooph/event-store/issues/232) -- RFC problem with TransactionalEventStore and ArangoDB [\#221](https://github.com/prooph/event-store/issues/221) - -**Merged pull requests:** - -- Bump year and correct package name in docheader [\#243](https://github.com/prooph/event-store/pull/243) ([codeliner](https://github.com/codeliner)) -- Revert "outsourced configuration from ConfigurableAggregateTranslator" [\#241](https://github.com/prooph/event-store/pull/241) ([codeliner](https://github.com/codeliner)) -- outsourced configuration from ConfigurableAggregateTranslator [\#238](https://github.com/prooph/event-store/pull/238) ([dropdevcoding](https://github.com/dropdevcoding)) - -## [v7.0.0-beta1](https://github.com/prooph/event-store/tree/v7.0.0-beta1) (2016-12-13) -[Full Changelog](https://github.com/prooph/event-store/compare/v6.5.1...v7.0.0-beta1) - -**Implemented enhancements:** - -- Wrap action event emitter [\#229](https://github.com/prooph/event-store/issues/229) -- Add convenience methods to event store [\#226](https://github.com/prooph/event-store/issues/226) -- Add "updateStreamMetadata" to event store interface [\#225](https://github.com/prooph/event-store/issues/225) -- RFC sha1 on stream names [\#196](https://github.com/prooph/event-store/issues/196) -- RFC event-store HTTP API [\#194](https://github.com/prooph/event-store/issues/194) -- RFC stream to stream projections [\#193](https://github.com/prooph/event-store/issues/193) -- RFC query-stream-api [\#192](https://github.com/prooph/event-store/issues/192) -- make rollback consistent with beginTransaction and commit [\#169](https://github.com/prooph/event-store/issues/169) -- plugins require ActionEventEmitterEventStore [\#231](https://github.com/prooph/event-store/pull/231) ([prolic](https://github.com/prolic)) -- Wrap action event emitter [\#230](https://github.com/prooph/event-store/pull/230) ([prolic](https://github.com/prolic)) -- Add convenience methods to event store [\#228](https://github.com/prooph/event-store/pull/228) ([prolic](https://github.com/prolic)) -- add updateStreamMetadata method to event store [\#227](https://github.com/prooph/event-store/pull/227) ([prolic](https://github.com/prolic)) -- update to interop-config 2.0.0 [\#224](https://github.com/prooph/event-store/pull/224) ([sandrokeil](https://github.com/sandrokeil)) -- update docs + transactional method to TransactionalEventStore interface [\#223](https://github.com/prooph/event-store/pull/223) ([prolic](https://github.com/prolic)) -- Improvement/interface names [\#220](https://github.com/prooph/event-store/pull/220) ([basz](https://github.com/basz)) -- Add stream name in handler context [\#219](https://github.com/prooph/event-store/pull/219) ([prolic](https://github.com/prolic)) -- refactor event store interfaces [\#218](https://github.com/prooph/event-store/pull/218) ([prolic](https://github.com/prolic)) -- use distinct event handler context in projections [\#217](https://github.com/prooph/event-store/pull/217) ([prolic](https://github.com/prolic)) -- Transactional in memory event store [\#216](https://github.com/prooph/event-store/pull/216) ([prolic](https://github.com/prolic)) -- Trigger even more events + update docs [\#215](https://github.com/prooph/event-store/pull/215) ([prolic](https://github.com/prolic)) -- Feature/stop method to projections [\#214](https://github.com/prooph/event-store/pull/214) ([basz](https://github.com/basz)) -- Projections [\#213](https://github.com/prooph/event-store/pull/213) ([prolic](https://github.com/prolic)) -- add AbstractActionEventEmitterAwareEventStore [\#212](https://github.com/prooph/event-store/pull/212) ([prolic](https://github.com/prolic)) -- Add EventStore interface - remove Adapters [\#211](https://github.com/prooph/event-store/pull/211) ([prolic](https://github.com/prolic)) - -**Fixed bugs:** - -- Minor fixes and clean up [\#222](https://github.com/prooph/event-store/pull/222) ([prolic](https://github.com/prolic)) - -**Closed issues:** - -- Remove nesting from $recordedEvents [\#202](https://github.com/prooph/event-store/issues/202) - -## [v6.5.1](https://github.com/prooph/event-store/tree/v6.5.1) (2016-11-07) -[Full Changelog](https://github.com/prooph/event-store/compare/v6.5.0...v6.5.1) - -**Implemented enhancements:** - -- RFC load max events and direction [\#198](https://github.com/prooph/event-store/issues/198) -- RFC stream metadata [\#197](https://github.com/prooph/event-store/issues/197) -- RFC Remove Adapter::replay method [\#190](https://github.com/prooph/event-store/issues/190) -- Use aggregate root marker interface for PHP 7.1 [\#188](https://github.com/prooph/event-store/issues/188) -- Remove deprecated AbstractAggregateRepositoryFactory [\#171](https://github.com/prooph/event-store/issues/171) -- Optimize package for PHP7 [\#170](https://github.com/prooph/event-store/issues/170) -- add hasStream method to adapter & event store [\#207](https://github.com/prooph/event-store/pull/207) ([prolic](https://github.com/prolic)) -- improve metadata matcher [\#206](https://github.com/prooph/event-store/pull/206) ([prolic](https://github.com/prolic)) -- improved metadatamatcher [\#205](https://github.com/prooph/event-store/pull/205) ([prolic](https://github.com/prolic)) -- add metadata matcher [\#204](https://github.com/prooph/event-store/pull/204) ([prolic](https://github.com/prolic)) -- add stream metadata [\#203](https://github.com/prooph/event-store/pull/203) ([prolic](https://github.com/prolic)) -- remove snapshot namespace [\#201](https://github.com/prooph/event-store/pull/201) ([prolic](https://github.com/prolic)) -- RFC load max events and direction & RFC remove Adapter::replay [\#200](https://github.com/prooph/event-store/pull/200) ([prolic](https://github.com/prolic)) -- add possibility to clear identity map manually [\#187](https://github.com/prooph/event-store/pull/187) ([sandrokeil](https://github.com/sandrokeil)) -- add possibility to clear identity map manually [\#185](https://github.com/prooph/event-store/pull/185) ([sandrokeil](https://github.com/sandrokeil)) -- Support for PHP 7.1 [\#184](https://github.com/prooph/event-store/pull/184) ([prolic](https://github.com/prolic)) - -**Fixed bugs:** - -- bypass append iterator problems [\#210](https://github.com/prooph/event-store/pull/210) ([prolic](https://github.com/prolic)) -- fix unit test [\#183](https://github.com/prooph/event-store/pull/183) ([prolic](https://github.com/prolic)) - -**Closed issues:** - -- Logo for github [\#86](https://github.com/prooph/event-store/issues/86) - -**Merged pull requests:** - -- removes nesting from recorded events [\#208](https://github.com/prooph/event-store/pull/208) ([oqq](https://github.com/oqq)) - -## [v6.5.0](https://github.com/prooph/event-store/tree/v6.5.0) (2016-09-14) -[Full Changelog](https://github.com/prooph/event-store/compare/v6.4.0...v6.5.0) - -**Implemented enhancements:** - -- Introduce EventStore\#transactional\(callable $callable\) API [\#182](https://github.com/prooph/event-store/pull/182) ([malukenho](https://github.com/malukenho)) -- Revise files header license - add docheader [\#181](https://github.com/prooph/event-store/pull/181) ([prolic](https://github.com/prolic)) - -## [v6.4.0](https://github.com/prooph/event-store/tree/v6.4.0) (2016-09-05) -[Full Changelog](https://github.com/prooph/event-store/compare/v6.3.0...v6.4.0) - -**Implemented enhancements:** - -- Multiple repo get calls sametransaction same instance [\#180](https://github.com/prooph/event-store/pull/180) ([basz](https://github.com/basz)) -- Foreach loop can be optimised into 1 internal function call. [\#176](https://github.com/prooph/event-store/pull/176) ([bweston92](https://github.com/bweston92)) -- Ignore some directories in the dist package [\#175](https://github.com/prooph/event-store/pull/175) ([mikemix](https://github.com/mikemix)) - -**Fixed bugs:** - -- fix tests with -\>getParam\('recordedEvents', new \ArrayIterator\(\)\) [\#177](https://github.com/prooph/event-store/pull/177) ([prolic](https://github.com/prolic)) - -**Closed issues:** - -- Calling `$this-\>repository-\>get` for the same AR twice will not find recorded events on the first result. [\#179](https://github.com/prooph/event-store/issues/179) - -**Merged pull requests:** - -- fix event store tests [\#174](https://github.com/prooph/event-store/pull/174) ([prolic](https://github.com/prolic)) - -## [v6.3.0](https://github.com/prooph/event-store/tree/v6.3.0) (2016-06-27) -[Full Changelog](https://github.com/prooph/event-store/compare/v6.2.0...v6.3.0) - -**Implemented enhancements:** - -- Create custom exception for concurreny issue [\#172](https://github.com/prooph/event-store/issues/172) -- add concurrency exception [\#173](https://github.com/prooph/event-store/pull/173) ([prolic](https://github.com/prolic)) - -**Closed issues:** - -- Support shared transactions [\#150](https://github.com/prooph/event-store/issues/150) - -**Merged pull requests:** - -- Fix/example array to iterator [\#168](https://github.com/prooph/event-store/pull/168) ([anthonysterling](https://github.com/anthonysterling)) - -## [v6.2.0](https://github.com/prooph/event-store/tree/v6.2.0) (2016-05-08) -[Full Changelog](https://github.com/prooph/event-store/compare/v6.1.2...v6.2.0) - -**Merged pull requests:** - -- Prepare 6.2.0 Release [\#167](https://github.com/prooph/event-store/pull/167) ([codeliner](https://github.com/codeliner)) - -## [v6.1.2](https://github.com/prooph/event-store/tree/v6.1.2) (2016-04-22) -[Full Changelog](https://github.com/prooph/event-store/compare/v6.1.1...v6.1.2) - -**Implemented enhancements:** - -- update factories to interop-config 1.0, add static factory support [\#165](https://github.com/prooph/event-store/pull/165) ([sandrokeil](https://github.com/sandrokeil)) -- Setup metadata\_enrichers option in event store factory [\#164](https://github.com/prooph/event-store/pull/164) ([MattKetmo](https://github.com/MattKetmo)) -- Remove sleep\(2\) from tests [\#163](https://github.com/prooph/event-store/pull/163) ([MattKetmo](https://github.com/MattKetmo)) -- Metadata enricher [\#162](https://github.com/prooph/event-store/pull/162) ([MattKetmo](https://github.com/MattKetmo)) - -**Merged pull requests:** - -- Move ramsey/uuid to require-dev [\#166](https://github.com/prooph/event-store/pull/166) ([sbacelic](https://github.com/sbacelic)) - -## [v6.1.1](https://github.com/prooph/event-store/tree/v6.1.1) (2016-02-27) -[Full Changelog](https://github.com/prooph/event-store/compare/v6.1...v6.1.1) - -**Fixed bugs:** - -- Argument 1 passed to AppendIterator::append is not Iterable [\#158](https://github.com/prooph/event-store/issues/158) -- set recordedEvent back to ArrayIterator when rolling back transaction [\#161](https://github.com/prooph/event-store/pull/161) ([prolic](https://github.com/prolic)) - -**Closed issues:** - -- Update to coveralls ^1.0 [\#153](https://github.com/prooph/event-store/issues/153) -- Add Composer suggestions for persistence and snapshot adapter [\#149](https://github.com/prooph/event-store/issues/149) -- Wrong email address in php doc [\#114](https://github.com/prooph/event-store/issues/114) - -**Merged pull requests:** - -- Housekeeping [\#160](https://github.com/prooph/event-store/pull/160) ([prolic](https://github.com/prolic)) - -## [v6.1](https://github.com/prooph/event-store/tree/v6.1) (2016-02-25) -[Full Changelog](https://github.com/prooph/event-store/compare/v6.0...v6.1) - -**Implemented enhancements:** - -- patch aggregate repository to allow inheritance aggregate roots [\#154](https://github.com/prooph/event-store/pull/154) ([prolic](https://github.com/prolic)) - -**Closed issues:** - -- Executing multiple subsequent transactions \(via commands\) seems to lead to duplicate event insertion [\#152](https://github.com/prooph/event-store/issues/152) - -**Merged pull requests:** - -- Prepare 6.1 release [\#156](https://github.com/prooph/event-store/pull/156) ([codeliner](https://github.com/codeliner)) -- add inheritance docs [\#155](https://github.com/prooph/event-store/pull/155) ([prolic](https://github.com/prolic)) - -## [v6.0](https://github.com/prooph/event-store/tree/v6.0) (2015-11-22) -[Full Changelog](https://github.com/prooph/event-store/compare/v6.0-beta.1...v6.0) - -**Implemented enhancements:** - -- Make container-interop optional for all components [\#110](https://github.com/prooph/event-store/issues/110) -- Provide a factory for the AggregateRepository [\#89](https://github.com/prooph/event-store/issues/89) -- aggregate repository can extract version [\#138](https://github.com/prooph/event-store/pull/138) ([prolic](https://github.com/prolic)) -- rename snapshot-store add method to save [\#137](https://github.com/prooph/event-store/pull/137) ([prolic](https://github.com/prolic)) -- extractVersion in aggregate translator [\#135](https://github.com/prooph/event-store/pull/135) ([prolic](https://github.com/prolic)) -- add aggregate type and id as metadata to all stream strategies [\#134](https://github.com/prooph/event-store/pull/134) ([prolic](https://github.com/prolic)) -- Add SnapshotStoreFactory + Test [\#133](https://github.com/prooph/event-store/pull/133) ([codeliner](https://github.com/codeliner)) - -**Fixed bugs:** - -- Rewind stream events when creating a stream [\#147](https://github.com/prooph/event-store/pull/147) ([codeliner](https://github.com/codeliner)) - -**Closed issues:** - -- Document new extractVersion method of AggregateTranslator [\#136](https://github.com/prooph/event-store/issues/136) -- Enhance StreamStrategy docs [\#132](https://github.com/prooph/event-store/issues/132) - -**Merged pull requests:** - -- v6.0 [\#148](https://github.com/prooph/event-store/pull/148) ([codeliner](https://github.com/codeliner)) -- Update docs to better support bookdown docs [\#146](https://github.com/prooph/event-store/pull/146) ([codeliner](https://github.com/codeliner)) -- updated bookdown templates to version 0.2.0 [\#145](https://github.com/prooph/event-store/pull/145) ([sandrokeil](https://github.com/sandrokeil)) -- added bookdown.io documentation [\#144](https://github.com/prooph/event-store/pull/144) ([sandrokeil](https://github.com/sandrokeil)) -- Replay not apply [\#143](https://github.com/prooph/event-store/pull/143) ([codeliner](https://github.com/codeliner)) -- Simplify repository [\#142](https://github.com/prooph/event-store/pull/142) ([codeliner](https://github.com/codeliner)) -- change homepage [\#139](https://github.com/prooph/event-store/pull/139) ([prolic](https://github.com/prolic)) - -## [v6.0-beta.1](https://github.com/prooph/event-store/tree/v6.0-beta.1) (2015-10-21) -[Full Changelog](https://github.com/prooph/event-store/compare/v5.1...v6.0-beta.1) - -**Implemented enhancements:** - -- Make use of interop config [\#116](https://github.com/prooph/event-store/issues/116) -- Wrong namespaces in Tests [\#115](https://github.com/prooph/event-store/issues/115) -- Improve pendingEvents index [\#99](https://github.com/prooph/event-store/issues/99) -- Implement EventStream as an iterator [\#37](https://github.com/prooph/event-store/issues/37) -- Support snapshots [\#28](https://github.com/prooph/event-store/issues/28) -- Add replay functionality [\#13](https://github.com/prooph/event-store/issues/13) -- Add AbstractAggregateRootFactory [\#131](https://github.com/prooph/event-store/pull/131) ([codeliner](https://github.com/codeliner)) -- Make use of interop config for event store factory [\#130](https://github.com/prooph/event-store/pull/130) ([prolic](https://github.com/prolic)) -- Use event store FQCN as service id [\#127](https://github.com/prooph/event-store/pull/127) ([codeliner](https://github.com/codeliner)) -- update composer json [\#126](https://github.com/prooph/event-store/pull/126) ([prolic](https://github.com/prolic)) -- fix namespace organisation in tests [\#125](https://github.com/prooph/event-store/pull/125) ([prolic](https://github.com/prolic)) -- Allow metadatas to be empty [\#122](https://github.com/prooph/event-store/pull/122) ([codeliner](https://github.com/codeliner)) -- Respect new iterator contract [\#121](https://github.com/prooph/event-store/pull/121) ([codeliner](https://github.com/codeliner)) -- Snapshots [\#113](https://github.com/prooph/event-store/pull/113) ([prolic](https://github.com/prolic)) -- apply pending event streams accepts iterator, not array [\#112](https://github.com/prooph/event-store/pull/112) ([prolic](https://github.com/prolic)) -- remove has-method from identity map [\#111](https://github.com/prooph/event-store/pull/111) ([prolic](https://github.com/prolic)) -- Introduce identity map as first-class object [\#106](https://github.com/prooph/event-store/pull/106) ([codeliner](https://github.com/codeliner)) -- update adapter interface [\#105](https://github.com/prooph/event-store/pull/105) ([prolic](https://github.com/prolic)) -- Replay functionality [\#104](https://github.com/prooph/event-store/pull/104) ([prolic](https://github.com/prolic)) -- Adjust AggregateTranslator contract [\#101](https://github.com/prooph/event-store/pull/101) ([codeliner](https://github.com/codeliner)) -- Improve pendingEvents index [\#100](https://github.com/prooph/event-store/pull/100) ([prolic](https://github.com/prolic)) -- Implement EventStream as an iterator [\#98](https://github.com/prooph/event-store/pull/98) ([prolic](https://github.com/prolic)) -- remove nested transaction support [\#97](https://github.com/prooph/event-store/pull/97) ([prolic](https://github.com/prolic)) -- apply events late [\#95](https://github.com/prooph/event-store/pull/95) ([prolic](https://github.com/prolic)) - -**Fixed bugs:** - -- Improve pendingEvents index [\#99](https://github.com/prooph/event-store/issues/99) -- fix order of events with same date time [\#109](https://github.com/prooph/event-store/pull/109) ([prolic](https://github.com/prolic)) -- Adjust AggregateTranslator contract [\#101](https://github.com/prooph/event-store/pull/101) ([codeliner](https://github.com/codeliner)) -- Improve pendingEvents index [\#100](https://github.com/prooph/event-store/pull/100) ([prolic](https://github.com/prolic)) - -**Closed issues:** - -- Revert identity map changes [\#117](https://github.com/prooph/event-store/issues/117) -- Documentation for new IdentityMap [\#108](https://github.com/prooph/event-store/issues/108) -- Documentation for replaying events [\#107](https://github.com/prooph/event-store/issues/107) -- Update documentation for applying events late [\#96](https://github.com/prooph/event-store/issues/96) -- Remove nested transaction support [\#55](https://github.com/prooph/event-store/issues/55) -- Introduce AggregateManager [\#30](https://github.com/prooph/event-store/issues/30) -- Document how a MessageFacory can be used to load older versions of events [\#26](https://github.com/prooph/event-store/issues/26) - -**Merged pull requests:** - -- Add snapshot docs [\#124](https://github.com/prooph/event-store/pull/124) ([codeliner](https://github.com/codeliner)) -- Notes about how to replay history [\#123](https://github.com/prooph/event-store/pull/123) ([codeliner](https://github.com/codeliner)) -- Upcasting the right way [\#120](https://github.com/prooph/event-store/pull/120) ([codeliner](https://github.com/codeliner)) -- Some info about apply events late [\#119](https://github.com/prooph/event-store/pull/119) ([codeliner](https://github.com/codeliner)) -- Revert identity map changes and add clear method [\#118](https://github.com/prooph/event-store/pull/118) ([codeliner](https://github.com/codeliner)) -- Handle pending events of new aggregates correctly [\#102](https://github.com/prooph/event-store/pull/102) ([codeliner](https://github.com/codeliner)) - -## [v5.1](https://github.com/prooph/event-store/tree/v5.1) (2015-09-18) -[Full Changelog](https://github.com/prooph/event-store/compare/v5.0...v5.1) - -**Merged pull requests:** - -- Add method to check whether event store is in transaction [\#94](https://github.com/prooph/event-store/pull/94) ([codeliner](https://github.com/codeliner)) - -## [v5.0](https://github.com/prooph/event-store/tree/v5.0) (2015-09-08) -[Full Changelog](https://github.com/prooph/event-store/compare/v5.0-beta.3...v5.0) - -**Merged pull requests:** - -- Remove InMemoryAdapter as default [\#93](https://github.com/prooph/event-store/pull/93) ([prolic](https://github.com/prolic)) - -## [v5.0-beta.3](https://github.com/prooph/event-store/tree/v5.0-beta.3) (2015-09-02) -[Full Changelog](https://github.com/prooph/event-store/compare/v5.0-beta.2...v5.0-beta.3) - -**Implemented enhancements:** - -- Increase test coverage [\#84](https://github.com/prooph/event-store/issues/84) -- Add tests [\#92](https://github.com/prooph/event-store/pull/92) ([prolic](https://github.com/prolic)) -- Fixes and tests [\#91](https://github.com/prooph/event-store/pull/91) ([prolic](https://github.com/prolic)) - -**Fixed bugs:** - -- Fixes and tests [\#91](https://github.com/prooph/event-store/pull/91) ([prolic](https://github.com/prolic)) - -**Merged pull requests:** - -- test php7 on travis [\#90](https://github.com/prooph/event-store/pull/90) ([prolic](https://github.com/prolic)) - -## [v5.0-beta.2](https://github.com/prooph/event-store/tree/v5.0-beta.2) (2015-08-25) -[Full Changelog](https://github.com/prooph/event-store/compare/v5.0-beta.1...v5.0-beta.2) - -**Implemented enhancements:** - -- Improve set up of AggregateRepository [\#72](https://github.com/prooph/event-store/issues/72) -- Add factory tests [\#70](https://github.com/prooph/event-store/issues/70) -- Move EventStoreFactory one namespace level up [\#69](https://github.com/prooph/event-store/issues/69) -- Convert DefaultAggregateTranslator to ConfigurableAggregateTranslator [\#68](https://github.com/prooph/event-store/issues/68) -- Provide helper trait for adapters to centralize message conversion logic [\#56](https://github.com/prooph/event-store/issues/56) -- Configurable aggregate translator [\#85](https://github.com/prooph/event-store/pull/85) ([codeliner](https://github.com/codeliner)) -- Add factory tests [\#74](https://github.com/prooph/event-store/pull/74) ([prolic](https://github.com/prolic)) - -**Fixed bugs:** - -- Avoid config conflicts with adapter factories [\#87](https://github.com/prooph/event-store/pull/87) ([codeliner](https://github.com/codeliner)) - -**Closed issues:** - -- Update documentation [\#79](https://github.com/prooph/event-store/issues/79) -- Update copyright [\#77](https://github.com/prooph/event-store/issues/77) -- Rename Prooph\EventStore\Feature\Feature to Plugin\Plugin [\#75](https://github.com/prooph/event-store/issues/75) -- Add factory documentation [\#71](https://github.com/prooph/event-store/issues/71) -- Remove adapter helper trait [\#64](https://github.com/prooph/event-store/issues/64) -- Adjust event store adapters to support 5.0 [\#61](https://github.com/prooph/event-store/issues/61) -- Proposal of adapter constructor changes [\#60](https://github.com/prooph/event-store/issues/60) -- Move event-store related factories from proophessor to this repo [\#57](https://github.com/prooph/event-store/issues/57) -- Mark nested transaction usage as deprecated [\#54](https://github.com/prooph/event-store/issues/54) -- Inject MessageFactory and Converter via adapter options [\#51](https://github.com/prooph/event-store/issues/51) -- How to persist custom events with PES [\#49](https://github.com/prooph/event-store/issues/49) -- Improve Readme [\#48](https://github.com/prooph/event-store/issues/48) -- Get rid of ZF2\Event dependency [\#47](https://github.com/prooph/event-store/issues/47) -- FeatureManager should load features via Interop\ContainerInterface [\#46](https://github.com/prooph/event-store/issues/46) -- Reduce hard dependencies on ZF2? [\#45](https://github.com/prooph/event-store/issues/45) -- Find strategy to add custom metadata like causation id to events [\#33](https://github.com/prooph/event-store/issues/33) - -**Merged pull requests:** - -- Restructure and improve documentation [\#88](https://github.com/prooph/event-store/pull/88) ([codeliner](https://github.com/codeliner)) -- Update copyright [\#83](https://github.com/prooph/event-store/pull/83) ([codeliner](https://github.com/codeliner)) -- Remove adapter helper trait [\#82](https://github.com/prooph/event-store/pull/82) ([codeliner](https://github.com/codeliner)) -- Add php cs fixer [\#80](https://github.com/prooph/event-store/pull/80) ([prolic](https://github.com/prolic)) -- Rename Features to Plugins [\#78](https://github.com/prooph/event-store/pull/78) ([prolic](https://github.com/prolic)) -- Change constructor signature of AggregateRepository [\#76](https://github.com/prooph/event-store/pull/76) ([prolic](https://github.com/prolic)) -- Move EventStoreFactory one namespace level up [\#73](https://github.com/prooph/event-store/pull/73) ([prolic](https://github.com/prolic)) -- Move event-store related factories from proophessor to this repo [\#67](https://github.com/prooph/event-store/pull/67) ([prolic](https://github.com/prolic)) -- cleanup .php\_cs config file [\#65](https://github.com/prooph/event-store/pull/65) ([prolic](https://github.com/prolic)) -- Fix php-cs for all files in repo [\#63](https://github.com/prooph/event-store/pull/63) ([prolic](https://github.com/prolic)) -- Add php-cs-fixer to travis [\#62](https://github.com/prooph/event-store/pull/62) ([prolic](https://github.com/prolic)) -- Add AdapterMessageConverter trait and a payload serializer [\#59](https://github.com/prooph/event-store/pull/59) ([codeliner](https://github.com/codeliner)) -- Mark nested transactions as deprecated [\#58](https://github.com/prooph/event-store/pull/58) ([codeliner](https://github.com/codeliner)) -- Improve readme [\#53](https://github.com/prooph/event-store/pull/53) ([codeliner](https://github.com/codeliner)) -- Type hint message interface [\#52](https://github.com/prooph/event-store/pull/52) ([codeliner](https://github.com/codeliner)) - -## [v5.0-beta.1](https://github.com/prooph/event-store/tree/v5.0-beta.1) (2015-07-26) -[Full Changelog](https://github.com/prooph/event-store/compare/v2.1.1...v5.0-beta.1) - -**Merged pull requests:** - -- Feature/v5.0 [\#50](https://github.com/prooph/event-store/pull/50) ([codeliner](https://github.com/codeliner)) - -## [v2.1.1](https://github.com/prooph/event-store/tree/v2.1.1) (2015-07-03) -[Full Changelog](https://github.com/prooph/event-store/compare/v4.0.1...v2.1.1) - -## [v4.0.1](https://github.com/prooph/event-store/tree/v4.0.1) (2015-07-01) -[Full Changelog](https://github.com/prooph/event-store/compare/v4.0...v4.0.1) - -**Merged pull requests:** - -- Reset event store recorded events collection before triggering post commit event [\#44](https://github.com/prooph/event-store/pull/44) ([dottorbabba](https://github.com/dottorbabba)) - -## [v4.0](https://github.com/prooph/event-store/tree/v4.0) (2015-06-23) -[Full Changelog](https://github.com/prooph/event-store/compare/v3.1...v4.0) - -**Closed issues:** - -- Support nested transactions [\#41](https://github.com/prooph/event-store/issues/41) - -**Merged pull requests:** - -- patch-41: Support nested transactions [\#42](https://github.com/prooph/event-store/pull/42) ([codeliner](https://github.com/codeliner)) -- Update composer.json [\#40](https://github.com/prooph/event-store/pull/40) ([prolic](https://github.com/prolic)) - -## [v3.1](https://github.com/prooph/event-store/tree/v3.1) (2015-05-09) -[Full Changelog](https://github.com/prooph/event-store/compare/v3.0...v3.1) - -**Closed issues:** - -- Document/Wiki [\#38](https://github.com/prooph/event-store/issues/38) - -## [v3.0](https://github.com/prooph/event-store/tree/v3.0) (2015-05-01) -[Full Changelog](https://github.com/prooph/event-store/compare/v2.1.0...v3.0) - -## [v2.1.0](https://github.com/prooph/event-store/tree/v2.1.0) (2015-04-16) -[Full Changelog](https://github.com/prooph/event-store/compare/v2.0.1...v2.1.0) - -## [v2.0.1](https://github.com/prooph/event-store/tree/v2.0.1) (2015-01-13) -[Full Changelog](https://github.com/prooph/event-store/compare/v2.0...v2.0.1) - -**Merged pull requests:** - -- Fix broken super class strategy by reorganizing all strategies [\#36](https://github.com/prooph/event-store/pull/36) ([codeliner](https://github.com/codeliner)) - -## [v2.0](https://github.com/prooph/event-store/tree/v2.0) (2015-01-13) -[Full Changelog](https://github.com/prooph/event-store/compare/v1.1.2...v2.0) - -**Merged pull requests:** - -- Simplified aggregate repo [\#35](https://github.com/prooph/event-store/pull/35) ([codeliner](https://github.com/codeliner)) - -## [v1.1.2](https://github.com/prooph/event-store/tree/v1.1.2) (2014-12-11) -[Full Changelog](https://github.com/prooph/event-store/compare/v1.1.1...v1.1.2) - -**Fixed bugs:** - -- MappedSuperclassStreamStrategy::\_\_constructor has wrong initialization sequence [\#34](https://github.com/prooph/event-store/issues/34) -- AggregateRepository::aggregateType attribute not initialized in the constructor [\#32](https://github.com/prooph/event-store/issues/32) - -## [v1.1.1](https://github.com/prooph/event-store/tree/v1.1.1) (2014-10-25) -[Full Changelog](https://github.com/prooph/event-store/compare/v1.1.0...v1.1.1) - -## [v1.1.0](https://github.com/prooph/event-store/tree/v1.1.0) (2014-10-20) -[Full Changelog](https://github.com/prooph/event-store/compare/v1.0.0...v1.1.0) - -**Closed issues:** - -- Support mapped super class [\#31](https://github.com/prooph/event-store/issues/31) -- StreamName and EventName should normalize class names [\#29](https://github.com/prooph/event-store/issues/29) -- Add rtd documentation [\#10](https://github.com/prooph/event-store/issues/10) - -## [v1.0.0](https://github.com/prooph/event-store/tree/v1.0.0) (2014-09-28) -[Full Changelog](https://github.com/prooph/event-store/compare/0.5.0...v1.0.0) - -## [0.5.0](https://github.com/prooph/event-store/tree/0.5.0) (2014-09-07) -[Full Changelog](https://github.com/prooph/event-store/compare/0.4.2...0.5.0) - -**Closed issues:** - -- Wait with 0.5.0 release until all dependent repos are updated [\#27](https://github.com/prooph/event-store/issues/27) -- Provide possibility to link events in projection streams [\#24](https://github.com/prooph/event-store/issues/24) -- RepositoryInterface [\#16](https://github.com/prooph/event-store/issues/16) -- Optimize ES performance [\#12](https://github.com/prooph/event-store/issues/12) - -## [0.4.2](https://github.com/prooph/event-store/tree/0.4.2) (2014-08-20) -[Full Changelog](https://github.com/prooph/event-store/compare/0.4.1...0.4.2) - -**Merged pull requests:** - -- Fix configuration variable of zf2 adapter [\#17](https://github.com/prooph/event-store/pull/17) ([jsor](https://github.com/jsor)) -- Composer tweaks [\#15](https://github.com/prooph/event-store/pull/15) ([jsor](https://github.com/jsor)) - -## [0.4.1](https://github.com/prooph/event-store/tree/0.4.1) (2014-08-19) -[Full Changelog](https://github.com/prooph/event-store/compare/0.4.0...0.4.1) - -**Implemented enhancements:** - -- Feature required to create streams on the fly [\#4](https://github.com/prooph/event-store/issues/4) -- Configurable streaming strategy [\#1](https://github.com/prooph/event-store/issues/1) - -**Closed issues:** - -- Enable configuration of the stream schema [\#11](https://github.com/prooph/event-store/issues/11) -- Provide support for Buttercup.protects [\#8](https://github.com/prooph/event-store/issues/8) - -**Merged pull requests:** - -- Relax rhumsaa/uuid version constraint [\#14](https://github.com/prooph/event-store/pull/14) ([jsor](https://github.com/jsor)) - -## [0.4.0](https://github.com/prooph/event-store/tree/0.4.0) (2014-07-02) -[Full Changelog](https://github.com/prooph/event-store/compare/0.3.1...0.4.0) - -**Implemented enhancements:** - -- Add possibility to load all aggregates of an aggregateFQCN [\#2](https://github.com/prooph/event-store/issues/2) - -## [0.3.1](https://github.com/prooph/event-store/tree/0.3.1) (2014-06-08) -[Full Changelog](https://github.com/prooph/event-store/compare/0.3.0...0.3.1) - -## [0.3.0](https://github.com/prooph/event-store/tree/0.3.0) (2014-06-08) -[Full Changelog](https://github.com/prooph/event-store/compare/0.2.0...0.3.0) - -**Implemented enhancements:** - -- Introduce DomainEventHydratorManager [\#5](https://github.com/prooph/event-store/issues/5) - -## [0.2.0](https://github.com/prooph/event-store/tree/0.2.0) (2014-06-05) -[Full Changelog](https://github.com/prooph/event-store/compare/0.1.0...0.2.0) - -**Implemented enhancements:** - -- Use different repository types to decouple domain from ES [\#7](https://github.com/prooph/event-store/issues/7) -- Improve event-driven support of getRepository [\#3](https://github.com/prooph/event-store/issues/3) - -**Closed issues:** - -- Add support for AggregateTypeProviders [\#6](https://github.com/prooph/event-store/issues/6) - -## [0.1.0](https://github.com/prooph/event-store/tree/0.1.0) (2014-04-20) - - -\* *This Change Log was automatically generated by [github_changelog_generator](https://github.com/skywinder/Github-Changelog-Generator)* diff --git a/LICENSE b/LICENSE index 4292a5a4..c1e371a9 100644 --- a/LICENSE +++ b/LICENSE @@ -1,5 +1,5 @@ -Copyright (c) 2014-2018, prooph software GmbH -Copyright (c) 2015-2018, Sascha-Oliver Prolic +Copyright (c) 2018-2018, prooph software GmbH +Copyright (c) 2018-2018, Sascha-Oliver Prolic All rights reserved. Redistribution and use in source and binary forms, with or without diff --git a/README.md b/README.md index d182d75e..f1341062 100644 --- a/README.md +++ b/README.md @@ -1,48 +1,25 @@ # Prooph Event Store -PHP 7.1 EventStore Implementation. +Common classes and interface for Prooph Event Store implementations. [![Build Status](https://travis-ci.org/prooph/event-store.svg?branch=master)](https://travis-ci.org/prooph/event-store) -[![Coverage Status](https://coveralls.io/repos/prooph/event-store/badge.svg?branch=master&service=github)](https://coveralls.io/github/prooph/event-store?branch=master) +[![Coverage Status](https://coveralls.io/repos/github/prooph/event-store/badge.svg?branch=master)](https://coveralls.io/github/prooph/event-store?branch=master) [![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/prooph/improoph) -## Overview - -Prooph Event Store is capable of persisting event messages that are organized in streams. `Prooph\EventStore\EventStore` -itself is a facade for different persistence adapters (see the list below) and adds event-driven hook points for `Prooph\EventStore\Plugin\Plugin`s -which make the Event Store highly customizable. - ## Installation -You can install prooph/event-store via composer by adding `"prooph/event-store": "^7.0"` as requirement to your composer.json. - -## Available persistent implementations -- [PDO](https://github.com/prooph/pdo-event-store) - stable - -## Available snapshot store implementations -- [Mongo DB](https://github.com/prooph/mongodb-snapshot-store) - stable -- [PDO](https://github.com/prooph/pdo-snapshot-store) - stable -- [Memcached](https://github.com/prooph/memcached-snapshot-store) - stable -- [ArangoDB](https://github.com/prooph/arangodb-snapshot-store) - under development +You can install prooph/event-store via composer by adding `"prooph/event-store": "dev-master"` as requirement to your composer.json. -## Quick Start +### Available persistent implementations -For a short overview please see the annotated Quickstart in the `examples` folder. +- [Event Store Client](https://github.com/prooph/event-store-client) for async TCP connections +- [Event Store HTTP Client](https://github.com/prooph/event-store-http-client) for HTTP connections ## Documentation -Documentation is [in the doc tree](docs/), and can be compiled using [bookdown](http://bookdown.io). - -```console -$ php ./vendor/bin/bookdown docs/bookdown.json -$ php -S 0.0.0.0:8080 -t docs/html/ -``` - -Then browse to [http://localhost:8080/](http://localhost:8080/) - -## Video Introduction +See: [https://github.com/prooph/documentation](https://github.com/prooph/documentation) -[![Prooph Event Store v7](https://img.youtube.com/vi/QhpDIqYQzg0/0.jpg)](https://www.youtube.com/watch?v=QhpDIqYQzg0) +Will be published on the website soon. ## Support @@ -57,11 +34,12 @@ To establish a consistent code quality, please provide unit tests for all your c ## Version Guidance -| Version | Status | PHP Version | Support Until | -|---------|------------|-------------|---------------| -| 5.x | EOL | >= 5.5 | EOL | -| 6.x | Maintained | >= 5.5 | 3 Dec 2017 | -| 7.x | Latest | >= 7.1 | active | +| Version | Status | PHP Version | Support Until | +|---------|-------------|-------------|---------------| +| 5.x | EOL | >= 5.5 | EOL | +| 6.x | Maintained | >= 5.5 | 3 Dec 2017 | +| 7.x | Latest | >= 7.1 | active | +| 8.x | Development | >= 7.2 | active | ## License diff --git a/composer.json b/composer.json index d2889a99..043a6e45 100644 --- a/composer.json +++ b/composer.json @@ -1,78 +1,54 @@ { - "name": "prooph/event-store", - "description": "PHP EventStore Implementation", - "type": "library", - "license": "BSD-3-Clause", - "homepage": "http://getprooph.org/", - "authors": [ - { - "name": "Alexander Miertsch", - "email": "contact@prooph.de", - "homepage": "http://www.prooph.de" - }, - { - "name": "Sascha-Oliver Prolic", - "email": "saschaprolic@googlemail.com" - } - ], - "keywords": [ - "EventStore", - "EventSourcing", - "DDD", - "prooph" - ], - "minimum-stability": "dev", - "prefer-stable": true, - "require": { - "php": "^7.1", - "marc-mabe/php-enum": "^2.3.1 || ^3.0.0", - "prooph/common": "^4.1.0" - }, - "require-dev": { - "phpspec/prophecy": "^1.7", - "phpunit/php-invoker": "^2.0", - "phpunit/phpunit": "^7.1.4", - "prooph/bookdown-template": "^0.2.3", - "prooph/php-cs-fixer-config": "^0.3", - "psr/container": "^1.0", - "sandrokeil/interop-config": "^2.0.1", - "satooshi/php-coveralls": "^1.0" - }, - "suggest" : { - "prooph/pdo-event-store": "For usage with MySQL or Postgres as event store", - "prooph/event-sourcing" : "Basic functionality for event sourced aggregates", - "prooph/service-bus" : "Message bus facade to connect the event store with messaging systems", - "psr/container": "^1.0 for usage of provided factories", - "sandrokeil/interop-config": "For usage of provided factories" - }, - "conflict": { - "sandrokeil/interop-config": "<2.0.1" - }, - "autoload": { - "psr-4": { - "Prooph\\EventStore\\": "src/" - } - }, - "autoload-dev": { - "psr-4": { - "ProophTest\\EventStore\\": "tests/" - } - }, - "extra": { - "branch-alias": { - "dev-master": "7.4-dev" - } - }, - "scripts": { - "check": [ - "@cs", - "@test" - ], - "cs": "php-cs-fixer fix -v --diff --dry-run", - "cs-fix": "php-cs-fixer fix -v --diff", - "test": "phpunit" - }, - "config": { - "sort-packages": true + "name": "prooph/event-store-", + "type": "library", + "license": "BSD-3-Clause", + "authors": [ + { + "name": "Sascha-Oliver Prolic", + "email": "saschaprolic@googlemail.com" + } + ], + "description": "Event Store v8", + "keywords": [ + "EventStore", + "EventSourcing", + "DDD", + "prooph" + ], + "prefer-stable": true, + "require": { + "php": "^7.2", + "ext-json": "*", + "ramsey/uuid": "^3.7.3" + }, + "require-dev": { + "amphp/amp": "^2.0.7", + "phpspec/prophecy": "^1.7", + "phpunit/phpunit": "^7.4", + "doctrine/instantiator": "^1.1", + "sebastian/object-enumerator": "^3.0.3", + "satooshi/php-coveralls": "^2.0" + }, + "autoload": { + "psr-4": { + "Prooph\\EventStore\\": "src/" } + }, + "autoload-dev": { + "psr-4": { + "ProophTest\\EventStore\\": "tests/" + } + }, + "scripts": { + "check": [ + "@cs", + "@test" + ], + "cs": "php-cs-fixer fix -v --diff --dry-run", + "cs-fix": "php-cs-fixer fix -v --diff", + "test": "phpunit" + }, + "config": { + "sort-packages": true + } } diff --git a/docs/bookdown.json b/docs/bookdown.json deleted file mode 100644 index 19b902dc..00000000 --- a/docs/bookdown.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "title": "Event Store", - "content": [ - {"intro": "introduction.md"}, - {"implementations": "implementations/bookdown.json"}, - {"event_store_plugins": "event_store_plugins.md"}, - {"projections": "projections.md"}, - {"standard_projections": "https://raw.githubusercontent.com/prooph/standard-projections/master/docs/bookdown.json"}, - {"upcasting": "upcasting.md"}, - {"interop_factories": "interop_factories.md"}, - {"stream_iterator": "stream_iterator.md"}, - {"migration": "migration.md"} - ], - "tocDepth": 1, - "numbering": false, - "target": "./html", - "template": "../vendor/prooph/bookdown-template/templates/main.php" -} diff --git a/docs/event_store_plugins.md b/docs/event_store_plugins.md deleted file mode 100644 index da1683a4..00000000 --- a/docs/event_store_plugins.md +++ /dev/null @@ -1,119 +0,0 @@ -# Plugins & Extensions - -A prooph Event Store can be expanded using plugins. Some plugins are provided by prooph but you can write your own ones -to customize the event store using event hooks. - -## Event Hooks - -Requirements: an event store wrapped with `Prooph\EventStore\ActionEventEmitterEventStore`. - -Action events are triggered when methods of the event store are invoked. The action events are named like the -event store methods. The following events are available (event target is always the event store): - -- `create`: event params: `stream` - result params: `streamExistsAlready` -- `appendTo`: event params: `streamName`, `streamEvents` - result params: `streamNotFound`, `concurrencyException` -- `load`: event params: `streamName`, `fromNumber`, `count`, `metadatamatcher` - result params: `streamEvents`, `streamNotFound` -- `loadReverse`: event params: `streamName`, `fromNumber`, `count`, `metadatamatcher` - result params: `streamEvents`, `streamNotFound` -- `delete`: event params: `streamName` - result params: `streamNotFound` -- `hasStream`: event params: `streamName` - result params: `result` -- `fetchStreamMetadata`: event params: `streamName` - result params: `metadata`, `streamNotFound` -- `updateStreamMetadata`: event params: `streamName`, `metadata` - result params: `streamNotFound` -- `fetchStreamNames`: event params: `filter`, `metadataMatcher`, `limit`, `offset` - result params: `streamNames` -- `fetchStreamNamesRegex`: event params: `filter`, `metadataMatcher`, `limit`, `offset` - result params: `streamNames` -- `fetchCategoryNames`: event params: `filter`, `offset`, `limit` - result params: `categoryNames` -- `fetchCategoryNamesRegex`: event params: `filter`, `offset`, `limit` - result params: `categoryNames` - -If the event store implements \Prooph\EventStore\TransactionalActionEventEmitterEventStore, -the following additional events are available: - -- `beginTransaction`: event params: none - result params: `transactionAlreadyStarted` -- `commit`: event params: none - result params: `transactionNotStarted` -- `rollback`: event params: none - result params: `transactionNotStarted - -## Attaching Plugins - -If you took a look at the quick start, you should already be familiar with the possibility to attach an event listener plugin. - -```php -$eventStore->attach( - 'commit', - function (\Prooph\Common\Event\ActionEvent $actionEvent) { - //plugin logic here - }, - 1000 // priority -); -``` - -More complex plugins are typically provided as classes with their own dependencies. A plugin can implement the `Prooph\EventStore\Plugin\Plugin` interface -and can then attach itself to the event store in the `Plugin::attachToEventStore($eventStore)` method. -Implementing the interface is especially useful when you use the event store factory. - -## Plugin Use Cases - -The event-driven system opens the door for customizations. Here are some ideas of what you can do with it: - -- Attach a domain event dispatcher on the `create` and `appendTo` event -- Filter events before they are stored -- Add event metadata like a `causation id` (id of the command which caused the event) -- Convert events into custom event objects before they are passed back to a repository -- Implement your own Unit of Work and synchronize it with the `transaction`, `commit` and `rollback` events -- ... - -## Metadata enricher - -By default, the component is shipped with a plugin to automatically add metadata to each event. -For instance, you may want to add information about the command which caused the event or even -the user who triggered that command. - -Here is an example of its usage: - -```php -currentUser) { - $event = $event - ->withAddedMetadata('issuer_type', 'user') - ->withAddedMetadata('issuer_id', $this->currentUser->id()); - } - - return $event; - } -} - -$plugin = new MetadataEnricherPlugin(new MetadataEnricherAggregate([ - $issuerMetadataEnricher, - $causationMetadataEnricher, - $otherMetadataEnricher, -])); - -$plugin->attachToEventStore($eventStore); -``` - -## Internal metadata - -All internal metadata is prefixed with `_` (underscore), f.e. `_causation_id`. Do not use metadata keys starting with an -underscore, as this is reserved for prooph internals. - -## ReadOnlyEventStoreWrapper - -The event store interface is divided into read-only methods, see `Prooph\EventStore\ReadOnlyEventStore` and write methods -see `Prooph\EventStore\EventStore`. This distinction is useful in situations where you want to enforce ready-only access to -the event store. - -In case you need a read only event store, you can wrap your existing event store implementation with the -ReadOnlyEventStoreWrapper. - -```php -$readOnlyEventStore = new ReadOnlyEventStoreWrapper($eventStore); -``` diff --git a/docs/implementations/bookdown.json b/docs/implementations/bookdown.json deleted file mode 100644 index ed83c9c1..00000000 --- a/docs/implementations/bookdown.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "title": "Implementations", - "tocDepth": 1, - "numbering": false, - "content": [ - {"pdo_event_store": "https://raw.githubusercontent.com/prooph/pdo-event-store/master/docs/bookdown.json"} - ] -} \ No newline at end of file diff --git a/docs/interop_factories.md b/docs/interop_factories.md deleted file mode 100644 index d63470f8..00000000 --- a/docs/interop_factories.md +++ /dev/null @@ -1,94 +0,0 @@ -# Interop Factories - -Instead of providing a module, a bundle, a bridge or similar framework integration prooph/event-store ships with `interop factories`. - -## Factory-Driven Creation - -The concept behind these factories (see `src/Container` folder) is simple but powerful. It allows us to provide you with bootstrapping logic for the event store and related components -without the need to rely on a specific framework. However, the factories have three requirements. - -### Requirements - -1. Your Inversion of Control container must implement the [PSR Container interface](https://github.com/php-fig/container). -2. [interop-config](https://github.com/sandrokeil/interop-config) must be installed -3. The application configuration should be registered with the service id `config` in the container. - -*Note: Don't worry, if your environment doesn't provide these requirements, you can -always bootstrap the components by hand. Just look at the factories for inspiration in this case.* - -### InMemoryEventStoreFactory - -If the requirements are met, you just need to add a new section in your application config ... - -```php -[ - 'prooph' => [ - 'event_store' => [ - 'default' => [ - 'wrap_action_event_emitter' => true, - 'metadata_enrichers' => [ - // The factory will get the metadata enrichers and inject them in the MetadataEnricherPlugin. - // Note: you can obtain the same result by instantiating the plugin yourself - // and pass it to the 'plugin' section bellow. - 'metadata_enricher_1', - 'metadata_enricher_2', - // ... - ], - 'plugins' => [ - //And again the factory will use each service id to get the plugin from the container - //Plugin::attachToEventStore($eventStore) is then invoked by the factory so your plugins - // get attached automatically - //Awesome, isn't it? - 'plugin_1_service_id', - 'plugin_2_service_id', - //... - ], - ], - ], - ], - 'dependencies' => [ - 'factories' => [ - 'inmemoryeventstore' => [ - \Prooph\EventStore\Container\InMemoryEventStoreFactory::class, - 'default', - ], - ], - ], - //... other application config here -] -``` - -$eventStore = $container->get('inmemoryeventstore'); - -By default, `InMemoryEventStore` which is transactional, is created by factory. If you want to change this behaviour to -create `NonTransactionalInMemoryEventStore`, simply override default config of given event store to -`'transactional' => false` - -#### ReadOnlyEventStoreWrapper - -If you want to have a read only event store, just add `'read_only' => true` to your event store config. - -### InMemoryProjectionManagerFactory - -```php -[ - 'prooph' => [ - 'projection_manager' => [ - 'default' => [ - 'event_store' => 'inmemoryeventstore', - ], - ], - ], - 'dependencies' => [ - 'factories' => [ - 'inmemoryeventstoreprojectionmanager' => [ - \Prooph\EventStore\Container\InMemoryProjectionManagerFactory::class, - 'default', - ], - ], - ], - //... other application config here -] -``` - -$projectionManager = $container->get('inmemoryeventstoreprojectionmanager'); diff --git a/docs/introduction.md b/docs/introduction.md deleted file mode 100644 index f09423ac..00000000 --- a/docs/introduction.md +++ /dev/null @@ -1,125 +0,0 @@ -# Introduction - -Prooph Event Store is capable of persisting event messages that are organized in streams. `Prooph\EventStore\EventStore` -itself is an interface with implementations available for different databases. - -## Quickstart - -```php -create($singleStream); - -/** - * Next step would be to commit the transaction. - * But let's attach a plugin first that prints some information about currently added events. - * Plugins are simple event listeners. See the docs of prooph/common for more details about event listeners. - */ -$eventStore->attach( - ActionEventEmitterEventStore::EVENT_APPEND_TO, // InMemoryEventStore provides event hooks - function (ActionEvent $actionEvent): void { - /** - * In the *appendTo* action event a plugin has access to - * all recorded events which were added in the current committed transaction. - * It is the ideal place to attach a domain event dispatcher. - * We only use a closure here to print the recorded events in the terminal - */ - $recordedEvents = $actionEvent->getParam('streamEvents'); - - foreach ($recordedEvents as $recordedEvent) { - echo sprintf( - "Event with name %s was recorded. It occurred on %s ///\n\n", - $recordedEvent->messageName(), - $recordedEvent->createdAt()->format('Y-m-d H:i:s') - ); - } - }, - -1000 // low priority, so after action happened -); - -/** - * Now we can easily add events to the stream ... - */ -$eventStore->appendTo($streamName, new ArrayIterator([$quickStartSucceeded /*, ...*/])); - -/** - * Once committed you can of course also load a set of events or the entire stream - * Use $eventStore->load($streamName, $metadata, $minVersion); - * to load a list of events - * - * or the $eventStore->load($streamName); to get all events - */ -$streamIterator = $eventStore->load($streamName); - -foreach ($streamIterator as $event) { - if ($event instanceof QuickStartSucceeded) { - echo $event->getText(); - } -} - -``` - -## Video Introduction - -[![Prooph Event Store v7](https://img.youtube.com/vi/QhpDIqYQzg0/0.jpg)](https://www.youtube.com/watch?v=QhpDIqYQzg0) diff --git a/docs/migration.md b/docs/migration.md deleted file mode 100644 index 0cf17dae..00000000 --- a/docs/migration.md +++ /dev/null @@ -1,70 +0,0 @@ -# Migration from v6 to v7 - -## Moved classes - -The `Prooph\EventStore\Snapshot\*` classes are now moved to their own repository, [SnapshotStore](https://github.com/prooph/snapshot-store). - -The `Prooph\EventStore\Aggregate\*` classes are now moved into the [event-sourcing](https://github.com/prooph/event-sourcing/) repository. - -## Interfaces VS adapters - -The event store now ships with a `Prooph\EventStore\ReadOnlyEventStore` and a `Prooph\EventStore\EventStore` interface. -No event store adapters exist anymore, instead there are different implementations of the event store. - -## ActionEventEmitterEventStore - -In order to use action events like in v6, you need to wrap your event store. - -```php -$eventStore = new ActionEventEmitterEventStore($eventStore); -``` - -or - -```php -$eventStore = new TransactionalActionEventEmitterEventStore($eventStore); -``` - -Also, there are no more `.pre` and `.post` commit hooks anymore, instead, this is handled with different priorities now. - -## Plugins - -Instead of calling - -```php -$plugin->setUp($eventStore); -``` - -you now need to call - -```php -$plugin->attachToEventStore($eventStore); -``` - -## Interaction with the event store - -If you are using the event-store together with the [event-sourcing](https://github.com/prooph/event-sourcing/) component, -most stuff is pretty much unchanged for you, as you don't interact with the event store directly (this is done by the -event-sourcing component). - -If you are making calls to the event store yourself, take a look at the event_store docs on how the new usage is. - -## DB migrations - -If you are using v6 with MySQL (using doctrine adpater) and you want to switch to v7 with MySQL (using pdo-event-store), -you need to upgrade your database before you do this (same for other db vendors of course). The way events are -persisted has changed and you cannot simply update your source code to make this change. You need to write a migration -script, take the database offline, perform the migration and go back online. - -Things to do to migrate: -- Read all events -- Update event metadata -- Persist the event back to a new stream created with v7 - -This would need to be done for all event streams. - -As this is a very tough job, we don't provide any migration script currently. For some applications, a downtime is not -acceptable, in which case v7 might not be the right choice. You should use it when you can take the application offline -for a while and perform the DB migration, or you can wait until you start a new project to use it. - -We will support v6 series with bugfixes until 3 Dec 2017. diff --git a/docs/projections.md b/docs/projections.md deleted file mode 100644 index 1cae143e..00000000 --- a/docs/projections.md +++ /dev/null @@ -1,278 +0,0 @@ -# Projections - -New in v7 are queries and projections. - -## Queries - -Here, we are discussing event store queries, not queries on your read model. An event store query reads one or -multiple event streams, aggregates some state from it and makes it accessible. A query is a non-persistent function, -that will only be executed once, and return a result. That's it. - -To get started, let's take a simple example where we want to query the -event-store for how often a given user has changed his username. - -```php -$query = $projectionManager->createQuery(); -$query - ->init(function (): array { - return ['count' => 0]; - }) - ->fromStream('user-123') - ->when([ - 'user-name-changed' => function ( - array $state, UsernameChanged $event - ): array { - $state['count']++; - return $state; - } - ]) - ->run(); - -echo 'user 123 changed his name ' . $query->getState()['count'] . ' times'; -``` - -You can also reset and run the query again: - -```php -$query->reset(); -$query->run(); -``` - -Or you can stop the query at any point in time. - -```php -$query = $projectionManager->createQuery(); -$query - ->init(function (): array { - return ['count' => 0]; - }) - ->fromStream('user-123') - ->when([ - 'user-name-changed' => function ( - array $state, UsernameChanged $event - ): array { - $state['count']++; - $this->stop(); // stop query now - return $state; - } - ]) - ->run(); -``` - -Queries can be used to answer a given question easily because you don't need to figure out in which read model the -data is present (maybe it's not?) and how to query it there (maybe a lot of joins are needed in RDBMS). -Also you can do temporal queries very easily, which is hard to impossible to do with any other database system. - -## Projections - -Projections are like queries, but they are persistent, the created state is also persistent and can be queried -later, and the projection runs forever (in most cases). - -Compared to queries, the projectors have a couple of additional methods: - -```php -public function getName(): string; - -public function emit(Message $event): void; - -public function linkTo(string $streamName, Message $event): void; - -public function delete(bool $deleteEmittedEvents): void; -``` - -- getName() - obviously returns the given name of that projection -- emit(Message $event) - emits a new event that will be persisted on a stream with the same name as the projection -- linkTo(string $streamName, Message $event) - emits a new event, that will be persisted on a specific stream -- delete(bool $deleteEmittedEvents) - deletes the projection completely, the `$deleteEmittedEvents` flag tells whether or not to delete emitted events. - -An example: - -```php -$projector = $projectionManager->createProjection('test_projection'); -$projector - ->fromStream('user-123') - ->whenAny( - function (array $state, Message $event): array { - $this->linkTo('foo', $event); // create a copy of the event to a new stream - return $state; - } - ) - ->run(); -``` - -```php -$projector = $projectionManager->createProjection('test_projection'); -$projector - ->init(function (): array { - return ['count' => 0]; - }) - ->fromCategory('user') - ->when([ - 'user-registered' => function (array $state, Message $event): array { - $state['count']++; - return $state; - } - ]) - ->run(); -``` - -This would count all registered users. - -### Options - -There are six options common to all projectors: - -OPTION_CACHE_SIZE = 'cache_size'; //Default: 1000 - -The cache size is how many stream names are cached in memory, the higher the number the less queries are executed and therefore -the projection runs faster, but it consumes more memory. - -OPTION_SLEEP = 'sleep'; //Default: 100000 - -The sleep options tells the projection to sleep that many microseconds before querying the event store again when no events -were found in the last trip. This reduces the number of cpu cycles without the projection doing any real work. - -OPTION_PERSIST_BLOCK_SIZE = 'persist_block_size'; //Default: 1000 - -The persist block size tells the projector to persist its changes after a given number of operations. This increases the speed -of the projection a lot. When you only persist every 1000 events compared to persist on every event, then 999 write operations -are saved. The higher the number, the fewer write operations are made to your system, making the projections run faster. -On the other side, in case of an error, you need to redo the last operations again. If you are publishing events to the outside -world within a projection, you may think of a persist block size of 1 only. - -OPTION_LOCK_TIMEOUT_MS = 'lock_timeout_ms'; //Default: 1000 - -Indicates the time (in milliseconds) the projector is locked. During this time no other projector with the same name can -be started. A running projector will update the lock timeout on every loop, except you configure an update lock threshold. - -OPTION_PCNTL_DISPATCH = 'trigger_pcntl_dispatch'; //Default: false - -Enable dispatching of process signals to registered [signal handlers](http://php.net/manual/en/function.pcntl-signal.php) while -the projection is running. You must still register your own signal handler and act accordingly. -For example to gracefully stop the projection you could do -``` -$projection = $projectionManager->createProjection( - 'test_projection', - [ Projector::OPTION_PCNTL_DISPATCH => true, ] -); -pcntl_signal(SIGQUIT, function () use ($projection) { - $projection->stop(); -}); -$projection->run(); -``` - -OPTION_UPDATE_LOCK_THRESHOLD = 'update_lock_threshold'; //Default: 0 - -If update lock threshold is set to a value greater than 0 the projection won't update lock timeout until number of milliseconds -have passed. Let's say your projection has a sleep interval of 100 ms and a lock timeout of 1000 ms. -By default the projector updates lock timeout after each run so basically every 100 ms the lock timeout is set to: `now() + 1000 ms` -This causes a lot of extra work for your database and in case the database is replicated this can cause a lot of network traffic, too. - -This is how the projection works without a threshold set: - -``` -1. Process new events. Update lock timeout -> now() + 1000 ms. Sleep 100 ms -2. Process new events. Update lock timeout -> now() + 1000 ms. Sleep 100 ms -3. Process new events. Update lock timeout -> now() + 1000 ms. Sleep 100 ms -... -``` - -And this is the projection flow with an update lock threshold set to `700 ms`: - -``` -1. Process new events. Sleep 100 ms -2. Process new events. Sleep 100 ms -3. Process new events. Sleep 100 ms -... -7. Process new events. Update lock timeout -> now() + 1000 ms. Sleep 100 ms -8. Process new events. Sleep 100 ms -``` - - -## Read Model Projections - -Projections can also be used to create read models. A read model has to implement `Prooph\EventStore\Projection\ReadModel`. -Prooph also ships with an `Prooph\EventStore\Projection\AbstractReadModel` that helps you implement a read model yourself. - -One nice thing about read model projections is that you don't need a migration script for your read models. -When you need to make a change to your read model, you simply alter your read model implementation, stop your -current running projections, reset it, and run it again. - -### Options - -The read model projectors have the same options as the normal projectors. See above for more explanations. - -### Example - -```php -$projector = $projectionManager->createReadModelProjection( - 'test_projection', - $readModel -); - -$projector - ->fromAll() - ->when([ - 'user-created' => function ($state, Message $event) { - $this->readModel()->insert( - 'name', - $event->payload()['name'] - ); - }, - 'username-changed' => function ($state, Message $event) { - $this->readModel()->update( - 'name', - $event->payload()['name'] - ); - } - ]) - ->run(); -``` - -## Projection Manager - -The projection manager can do the following for you: - -- Create queries & projectors -- Delete / reset / stop projections -- Fetch projection names -- Fetch projection status -- Fetch projection stream position -- Fetch projection state - -While most methods are pretty straightforward, the delete / reset / stop projection methods may need some additional -explanation: - -When you call stopProjection($name) (or delete or reset) a message is sent to the projection. This will notify the -running projection that it should act accordingly. This means that once you call `stopProjection`, it could take a few -seconds before the projection is finally stopped. - -## Internal projection names - -All internal projection names are prefixed with `$` (dollar-sign), f.e. `$ct-`. Do not use projection names starting -with a dollar-sign, as this is reserved for prooph internals. - -## Stream filtering - -The basic InMemory and PDO implementations for `Query`, `Projector` and `ReadModelProjector` have now a second optional `MetadataMatcher` parameter. It makes it possible to filter streams by all basic and custom metadata like `_aggregation_id` or `_aggregation_type`. It allows you for example to query over all events for a specific `Aggregate`. - -### Example - -```php -$query = $projectionManager->createQuery(); -$metaMatcher = (new MetadataMatcher)->withMetadataMatch('_aggregate_id', Operator::EQUALS(), '123') - -$query - ->init(function (): array { - return ['count' => 0]; - }) - ->fromStream('users', $metaMatcher) - ->whenAny(function ( - array $state, DomainEvent $event - ): array { - $state['count']++; - return $state; - } - ) - ->run(); -``` \ No newline at end of file diff --git a/docs/stream_iterator.md b/docs/stream_iterator.md deleted file mode 100644 index cc33728d..00000000 --- a/docs/stream_iterator.md +++ /dev/null @@ -1,49 +0,0 @@ -# StreamIterator - -For `prooph/event-store:7.3` a simple Iterator was returned when calling `$eventStore->load` and `$eventStore->loadReverse`. Getting a count of the total number of matched events in the stream was not possible in a reliably manner. The `iterator_count($streamIterator)` would move the pointer of the Iterator. - -The returned iterator instance has been changed from `prooph/event-store:7.4.0` forward to always return an instance of `Prooph\EventStore\StreamIterator\StreamIterator` which implements both `Countable` and `Iterator`. - -To get a realtime count of the events while iterating over the stream (even when events are added during iteration) simply use `count($streamIterator)` inside the loop. - -One use case is to provide an accurate progress bar whilst processing event streams. - -```php -$streamIterator = $eventStore->load($streamName); -$progress = new \Symfony\Component\Console\Helper\ProgressBar($outputInterface, count($streamIterator)); - -foreach ($streamIterator as $event) { - // do something with $event - - $progress->setMaxSteps(count($streamIterator)); - $progress->advance(); -} - -$progress->finish(); - -``` - -## Notes - -When you specifically specify a count argument while loading a stream calling `count($streamIterator)` will return the lesser of *$count* and *the number of matched events*. - -```php -$streamIterator = $eventStore->load($streamName, 0, 10) - -// with 5 matching events in the store count($streamIterator) would return 5. -// with 15 matching events in the store count($streamIterator) would return 10. -``` - -Depending on the adapter in use a call to `count($streamIterator)` triggers a potentially expensive operation. Throttling count's might just work. - -```php -foreach ($streamIterator as $index => $event) { - // do something with $event - - if (0 === $index % 10) { - $progress->setMaxSteps(count($streamIterator)); - $progress->advance(10); - } -} -``` - diff --git a/docs/upcasting.md b/docs/upcasting.md deleted file mode 100644 index 8a87ff76..00000000 --- a/docs/upcasting.md +++ /dev/null @@ -1,81 +0,0 @@ -# Upcasting - -Imagine `v1` of your application already runs in production. You've worked on great new features the last weeks and -want to deploy `v1.1` but the structure of some domain events changed. The new versions of your aggregates would not be -able to replay `v1` domain events correctly. To solve the issue you can **upcast** your history events. - -## How does it work? - -Basically you need to write a migration script (much like a normal DB migration script). The script should load all -effected events from the event store and manipulate them to be compliant with version `1.1` of your aggregates. -Then simply replace the original events in the stream with the changed ones. - -*Note: The event store offers methods to load events from a stream and add new ones, but it has no method to replace them. -The reason for that is simple: "Upcasting" is something your normal program should **not** have access to. -It is only a way to upgrade your application to the next version, so your upcasting script needs to make use -of low-level functionality provided by the underlying driver for the event store adapter.* - -## But how do I avoid conflicts during the upcasting process? - -Well, that depends on your infrastructure and deployment strategy. The easiest way is to take your application offline, -perform the upcasting script, deploy the new version of the application, and bring the system online again. - -A more complex option with no or very little downtime is to use a special [MessageFactory](https://github.com/prooph/common/blob/master/src/Messaging/MessageFactory.php). -First, make the MessageFactory aware of the differences between `v1` and `v1.1` of your events. Deploy the modified factory together -with version `1.1` of your application. The factory takes care of translating old events into new ones. -Perform the "upcasting" script in the background, and, once it has replaced all old events, you can remove the translation logic -from the factory again and exchange it with the simple factory you used before. - -*Note: Each event store adapter allows you to set it up with a custom message factory. Please refer to the adapter documentation of your choice to get more information.* - -## Upasting on the fly - -Starting in v7 prooph offers an upcasting plugin for the event store. Setup is very easy: - -```php -$upcaster = new MyUpcaster(); -$plugin = new UpcastingPlugin($upcaster); -$plugin->attachToEventStore($eventStore); -``` - -So next time you `load` your events, they will get upcasted automatically (but not persisted back to the database). - -The upcaster interface is very simple: - -```php -interface Upcaster -{ - /** - * @param Message $message - * @return array of messages - */ - public function upcast(Message $message): array; -} -``` - -Prooph also ships with a `SingleEventUpcaster`, an abstract class to help you create upcasters easily. -Additionally an `UpcasterChain` is provided, so you can combine upcasters easily: - -```php -$upcaster1 = new MyUpcaster1(); -$upcaster2 = new MyUpcaster2(); -$upcaster3 = new MyUpcaster3(); - -$chain = new UpcasterChain($upcaster1, $upcaster2, $upcaster3); -$plugin = new UpcastingPlugin($chain); -$plugin->attachToEventStore($eventStore); -``` - -Note: -The `UpcastingIterator` wraps the iterator given by the event-store instance. In case you're using the -`pdo-event-store`, it's an instance of `PdoStreamIterator`. The `UpcastingIterator` will return the -inner iterator's key when calling `$iterator->key()`. As a side-effect of this, if the upcaster upcasts -one event into multiple, all of those upcasted events have the same key. This is normally no problem, as -in userland code, you don't access the key normally. But during projections of the events (f.e. to create -read models), they key could repeat. This means, if the projection stops at event no 35 f.e. and this event -is upcasted into two events, the projector could theoretically apply one of them and stop afterwards. When -the projection starts again, the projection will ask for the next event (no 36), but the second event with -no 35 was never handled by the projection. Normally a projection is a long-running process in the background, -or a process is started by a cron-job (f.e. every hour). The problem described can only be an issue, -if you upcast one event into multiple and manually stop the projection (reset is not a problem of course). -But even then, the chance of running into issues is very small, but you have been noticed. diff --git a/examples/event/QuickStartSucceeded.php b/examples/event/QuickStartSucceeded.php deleted file mode 100644 index 82dd23d4..00000000 --- a/examples/event/QuickStartSucceeded.php +++ /dev/null @@ -1,58 +0,0 @@ - - * (c) 2015-2018 Sascha-Oliver Prolic - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace Prooph\EventStore\QuickStart\Event; - -use Prooph\Common\Messaging\DomainEvent; -use Prooph\EventStore\Util\Assertion; - -/** - * Class QuickStartSucceeded - * - * @author Alexander Miertsch - */ -final class QuickStartSucceeded extends DomainEvent -{ - /** - * @var string - */ - private $text; - - public static function withSuccessMessage(string $text): QuickStartSucceeded - { - return new self($text); - } - - private function __construct(string $text) - { - Assertion::minLength($text, 1, 'Success message must be at least 1 char long'); - $this->text = $text; - $this->metadata['_aggregate_version'] = 1; - $this->init(); - } - - public function getText(): string - { - return $this->text; - } - - public function payload(): array - { - return ['text' => $this->text]; - } - - protected function setPayload(array $payload): void - { - $this->text = $payload['text']; - } -} diff --git a/examples/quickstart.php b/examples/quickstart.php deleted file mode 100644 index 94a863e4..00000000 --- a/examples/quickstart.php +++ /dev/null @@ -1,118 +0,0 @@ - - * (c) 2015-2018 Sascha-Oliver Prolic - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace Prooph\EventStore\QuickStart; - -require_once __DIR__ . '/../vendor/autoload.php'; -require_once __DIR__ . '/event/QuickStartSucceeded.php'; - -use ArrayIterator; -use Prooph\Common\Event\ActionEvent; -use Prooph\Common\Event\ProophActionEventEmitter; -use Prooph\EventStore\ActionEventEmitterEventStore; -use Prooph\EventStore\InMemoryEventStore; -use Prooph\EventStore\QuickStart\Event\QuickStartSucceeded; -use Prooph\EventStore\Stream; -use Prooph\EventStore\StreamName; -use Prooph\EventStore\TransactionalActionEventEmitterEventStore; - -/** - * Here we use the InMemoryEventStore but in a real project - * you need to chose another implementation. - * - * Prooph\Common\Event\ActionEventEmitter is an interface - * that encapsulates functionality of an event dispatcher. - * You can use the one provided by prooph/common or - * you write a wrapper for the event dispatcher used - * by your web framework. - */ -$eventEmitter = new ProophActionEventEmitter(TransactionalActionEventEmitterEventStore::ALL_EVENTS); - -$eventStore = new ActionEventEmitterEventStore(new InMemoryEventStore(), $eventEmitter); - -/** - * We need a test event so let's create one. - * - * As a bare minimum events need to implement - * Prooph\Common\Messaging\Message. - * - * Note: It is possible to use your own events - * in your domain and use a translator to - * convert them. We'll come to that later. - */ -$quickStartSucceeded = QuickStartSucceeded::withSuccessMessage('It works'); - -/** - * Events are organized in so called event streams. - * An event stream is a logical unit for a group of events. - */ -$streamName = new StreamName('event_stream'); - -$singleStream = new Stream($streamName, new ArrayIterator()); - -/** - * As we are using the InMemoryEventStore we have to create the event stream - * each time running the quick start. With a real persistence adapter this - * is not required. In this case you should create the stream once. For example - * with the help of a migration script. - * - * Note: For more details see the docs of the adapter you want to use. - */ -$eventStore->create($singleStream); - -/** - * Next step would be to commit the transaction. - * But let's attach a plugin first that prints some information about currently added events. - * Plugins are simple event listeners. See the docs of prooph/common for more details about event listeners. - */ -$eventStore->attach( - ActionEventEmitterEventStore::EVENT_APPEND_TO, // InMemoryEventStore provides event hooks - function (ActionEvent $actionEvent): void { - /** - * In the *commit.post* action event a plugin has access to - * all recorded events which were added in the current committed transaction. - * It is the ideal place to attach a domain event dispatcher. - * We only use a closure here to print the recorded events in the terminal - */ - $recordedEvents = $actionEvent->getParam('streamEvents'); - - foreach ($recordedEvents as $recordedEvent) { - echo \sprintf( - "Event with name %s was recorded. It occurred on %s ///\n\n", - $recordedEvent->messageName(), - $recordedEvent->createdAt()->format('Y-m-d H:i:s') - ); - } - }, - -1000 // low priority, so after action happened -); - -/** - * Now we can easily add events to the stream ... - */ -$eventStore->appendTo($streamName, new ArrayIterator([$quickStartSucceeded /*, ...*/])); - -/** - * Once committed you can of course also load a set of events or the entire stream - * Use $eventStore->loadEventsByMetadataFrom($streamName, $metadata, $minVersion); - * to load a list of events - * - * or the $eventStore->load($streamName); to get all events - */ -$persistedEventStream = $eventStore->load($streamName); - -foreach ($persistedEventStream as $event) { - if ($event instanceof QuickStartSucceeded) { - echo $event->getText(); - } -} diff --git a/phpunit.xml.dist b/phpunit.xml.dist index ebb0526b..0ce48145 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,21 +1,31 @@ - + ~ (c) 2018-2018 Sascha-Oliver Prolic + ~ + ~ For the full copyright and license information, please view the LICENSE + ~ file that was distributed with this source code. + --> + + - ./tests + ./tests diff --git a/src/ActionEventEmitterEventStore.php b/src/ActionEventEmitterEventStore.php deleted file mode 100644 index 3befd960..00000000 --- a/src/ActionEventEmitterEventStore.php +++ /dev/null @@ -1,456 +0,0 @@ - - * (c) 2015-2018 Sascha-Oliver Prolic - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace Prooph\EventStore; - -use Iterator; -use Prooph\Common\Event\ActionEvent; -use Prooph\Common\Event\ActionEventEmitter; -use Prooph\Common\Event\ListenerHandler; -use Prooph\EventStore\Exception\ConcurrencyException; -use Prooph\EventStore\Exception\StreamExistsAlready; -use Prooph\EventStore\Exception\StreamNotFound; -use Prooph\EventStore\Metadata\MetadataMatcher; -use Prooph\EventStore\Util\Assertion; - -class ActionEventEmitterEventStore implements EventStoreDecorator -{ - public const EVENT_APPEND_TO = 'appendTo'; - public const EVENT_CREATE = 'create'; - public const EVENT_LOAD = 'load'; - public const EVENT_LOAD_REVERSE = 'loadReverse'; - public const EVENT_DELETE = 'delete'; - public const EVENT_HAS_STREAM = 'hasStream'; - public const EVENT_FETCH_STREAM_METADATA = 'fetchStreamMetadata'; - public const EVENT_UPDATE_STREAM_METADATA = 'updateStreamMetadata'; - public const EVENT_FETCH_STREAM_NAMES = 'fetchStreamNames'; - public const EVENT_FETCH_STREAM_NAMES_REGEX = 'fetchStreamNamesRegex'; - public const EVENT_FETCH_CATEGORY_NAMES = 'fetchCategoryNames'; - public const EVENT_FETCH_CATEGORY_NAMES_REGEX = 'fetchCategoryNamesRegex'; - - public const ALL_EVENTS = [ - self::EVENT_APPEND_TO, - self::EVENT_CREATE, - self::EVENT_LOAD, - self::EVENT_LOAD_REVERSE, - self::EVENT_DELETE, - self::EVENT_HAS_STREAM, - self::EVENT_FETCH_STREAM_METADATA, - self::EVENT_UPDATE_STREAM_METADATA, - self::EVENT_FETCH_STREAM_NAMES, - self::EVENT_FETCH_STREAM_NAMES_REGEX, - self::EVENT_FETCH_CATEGORY_NAMES, - self::EVENT_FETCH_CATEGORY_NAMES_REGEX, - ]; - - /** - * @var ActionEventEmitter - */ - protected $actionEventEmitter; - - /** - * @var EventStore - */ - protected $eventStore; - - public function __construct(EventStore $eventStore, ActionEventEmitter $actionEventEmitter) - { - $this->eventStore = $eventStore; - $this->actionEventEmitter = $actionEventEmitter; - - $actionEventEmitter->attachListener(self::EVENT_CREATE, function (ActionEvent $event): void { - $stream = $event->getParam('stream'); - - try { - $this->eventStore->create($stream); - } catch (StreamExistsAlready $exception) { - $event->setParam('streamExistsAlready', $exception); - } - }); - - $actionEventEmitter->attachListener(self::EVENT_APPEND_TO, function (ActionEvent $event): void { - $streamName = $event->getParam('streamName'); - $streamEvents = $event->getParam('streamEvents'); - - try { - $this->eventStore->appendTo($streamName, $streamEvents); - } catch (StreamNotFound $exception) { - $event->setParam('streamNotFound', $exception); - } catch (ConcurrencyException $exception) { - $event->setParam('concurrencyException', $exception); - } - }); - - $actionEventEmitter->attachListener(self::EVENT_LOAD, function (ActionEvent $event): void { - $streamName = $event->getParam('streamName'); - $fromNumber = $event->getParam('fromNumber'); - $count = $event->getParam('count'); - $metadataMatcher = $event->getParam('metadataMatcher'); - - try { - $streamEvents = $this->eventStore->load($streamName, $fromNumber, $count, $metadataMatcher); - $event->setParam('streamEvents', $streamEvents); - } catch (StreamNotFound $exception) { - $event->setParam('streamNotFound', $exception); - } - }); - - $actionEventEmitter->attachListener(self::EVENT_LOAD_REVERSE, function (ActionEvent $event): void { - $streamName = $event->getParam('streamName'); - $fromNumber = $event->getParam('fromNumber'); - $count = $event->getParam('count'); - $metadataMatcher = $event->getParam('metadataMatcher'); - - try { - $streamEvents = $this->eventStore->loadReverse($streamName, $fromNumber, $count, $metadataMatcher); - $event->setParam('streamEvents', $streamEvents); - } catch (StreamNotFound $exception) { - $event->setParam('streamNotFound', $exception); - } - }); - - $actionEventEmitter->attachListener(self::EVENT_DELETE, function (ActionEvent $event): void { - $streamName = $event->getParam('streamName'); - - try { - $this->eventStore->delete($streamName); - } catch (StreamNotFound $exception) { - $event->setParam('streamNotFound', $exception); - } - }); - - $actionEventEmitter->attachListener(self::EVENT_HAS_STREAM, function (ActionEvent $event): void { - $streamName = $event->getParam('streamName'); - - $event->setParam('result', $this->eventStore->hasStream($streamName)); - }); - - $actionEventEmitter->attachListener(self::EVENT_FETCH_STREAM_METADATA, function (ActionEvent $event): void { - $streamName = $event->getParam('streamName'); - - try { - $metadata = $this->eventStore->fetchStreamMetadata($streamName); - $event->setParam('metadata', $metadata); - } catch (StreamNotFound $exception) { - $event->setParam('streamNotFound', $exception); - } - }); - - $actionEventEmitter->attachListener(self::EVENT_UPDATE_STREAM_METADATA, function (ActionEvent $event): void { - $streamName = $event->getParam('streamName'); - $metadata = $event->getParam('metadata'); - - try { - $this->eventStore->updateStreamMetadata($streamName, $metadata); - } catch (StreamNotFound $exception) { - $event->setParam('streamNotFound', $exception); - } - }); - - $actionEventEmitter->attachListener(self::EVENT_FETCH_STREAM_NAMES, function (ActionEvent $event): void { - $filter = $event->getParam('filter'); - $metadataMatcher = $event->getParam('metadataMatcher'); - $limit = $event->getParam('limit'); - $offset = $event->getParam('offset'); - - $streamNames = $this->eventStore->fetchStreamNames($filter, $metadataMatcher, $limit, $offset); - - $event->setParam('streamNames', $streamNames); - }); - - $actionEventEmitter->attachListener(self::EVENT_FETCH_STREAM_NAMES_REGEX, function (ActionEvent $event): void { - $filter = $event->getParam('filter'); - $metadataMatcher = $event->getParam('metadataMatcher'); - $limit = $event->getParam('limit'); - $offset = $event->getParam('offset'); - - $streamNames = $this->eventStore->fetchStreamNamesRegex($filter, $metadataMatcher, $limit, $offset); - - $event->setParam('streamNames', $streamNames); - }); - - $actionEventEmitter->attachListener(self::EVENT_FETCH_CATEGORY_NAMES, function (ActionEvent $event): void { - $filter = $event->getParam('filter'); - $limit = $event->getParam('limit'); - $offset = $event->getParam('offset'); - - $streamNames = $this->eventStore->fetchCategoryNames($filter, $limit, $offset); - - $event->setParam('categoryNames', $streamNames); - }); - - $actionEventEmitter->attachListener(self::EVENT_FETCH_CATEGORY_NAMES_REGEX, function (ActionEvent $event): void { - $filter = $event->getParam('filter'); - $limit = $event->getParam('limit'); - $offset = $event->getParam('offset'); - - $streamNames = $this->eventStore->fetchCategoryNamesRegex($filter, $limit, $offset); - - $event->setParam('categoryNames', $streamNames); - }); - } - - public function create(Stream $stream): void - { - $argv = ['stream' => $stream]; - - $event = $this->actionEventEmitter->getNewActionEvent(self::EVENT_CREATE, $this, $argv); - - $this->actionEventEmitter->dispatch($event); - - if ($exception = $event->getParam('streamExistsAlready', false)) { - throw $exception; - } - } - - public function appendTo(StreamName $streamName, Iterator $streamEvents): void - { - $argv = ['streamName' => $streamName, 'streamEvents' => $streamEvents]; - - $event = $this->actionEventEmitter->getNewActionEvent(self::EVENT_APPEND_TO, $this, $argv); - - $this->actionEventEmitter->dispatch($event); - - if ($exception = $event->getParam('streamNotFound', false)) { - throw $exception; - } - - if ($exception = $event->getParam('concurrencyException', false)) { - throw $exception; - } - } - - public function load( - StreamName $streamName, - int $fromNumber = 1, - int $count = null, - MetadataMatcher $metadataMatcher = null - ): Iterator { - Assertion::greaterOrEqualThan($fromNumber, 1); - Assertion::nullOrGreaterOrEqualThan($count, 1); - - $argv = [ - 'streamName' => $streamName, - 'fromNumber' => $fromNumber, - 'count' => $count, - 'metadataMatcher' => $metadataMatcher, - ]; - - $event = $this->actionEventEmitter->getNewActionEvent(self::EVENT_LOAD, $this, $argv); - - $this->actionEventEmitter->dispatch($event); - - if ($exception = $event->getParam('streamNotFound', false)) { - throw $exception; - } - - $stream = $event->getParam('streamEvents', false); - - if (! $stream instanceof Iterator) { - throw StreamNotFound::with($streamName); - } - - return $stream; - } - - public function loadReverse( - StreamName $streamName, - int $fromNumber = null, - int $count = null, - MetadataMatcher $metadataMatcher = null - ): Iterator { - Assertion::nullOrGreaterOrEqualThan($fromNumber, 1); - Assertion::nullOrGreaterOrEqualThan($count, 1); - - $argv = [ - 'streamName' => $streamName, - 'fromNumber' => $fromNumber, - 'count' => $count, - 'metadataMatcher' => $metadataMatcher, - ]; - - $event = $this->actionEventEmitter->getNewActionEvent(self::EVENT_LOAD_REVERSE, $this, $argv); - - $this->actionEventEmitter->dispatch($event); - - if ($exception = $event->getParam('streamNotFound', false)) { - throw $exception; - } - - $stream = $event->getParam('streamEvents', false); - - if (! $stream instanceof Iterator) { - throw StreamNotFound::with($streamName); - } - - return $stream; - } - - public function delete(StreamName $streamName): void - { - $event = $this->actionEventEmitter->getNewActionEvent(self::EVENT_DELETE, $this, ['streamName' => $streamName]); - - $this->actionEventEmitter->dispatch($event); - - if ($exception = $event->getParam('streamNotFound', false)) { - throw $exception; - } - } - - public function hasStream(StreamName $streamName): bool - { - $event = $this->actionEventEmitter->getNewActionEvent( - self::EVENT_HAS_STREAM, - $this, - ['streamName' => $streamName] - ); - - $this->actionEventEmitter->dispatch($event); - - return $event->getParam('result', false); - } - - public function fetchStreamMetadata(StreamName $streamName): array - { - $event = $this->actionEventEmitter->getNewActionEvent( - self::EVENT_FETCH_STREAM_METADATA, - $this, - ['streamName' => $streamName] - ); - - $this->actionEventEmitter->dispatch($event); - - if ($exception = $event->getParam('streamNotFound', false)) { - throw $exception; - } - - $metadata = $event->getParam('metadata', false); - - if (! \is_array($metadata)) { - throw StreamNotFound::with($streamName); - } - - return $metadata; - } - - public function updateStreamMetadata(StreamName $streamName, array $newMetadata): void - { - $event = $this->actionEventEmitter->getNewActionEvent( - self::EVENT_UPDATE_STREAM_METADATA, - $this, - [ - 'streamName' => $streamName, - 'metadata' => $newMetadata, - ] - ); - - $this->actionEventEmitter->dispatch($event); - - if ($exception = $event->getParam('streamNotFound', false)) { - throw $exception; - } - } - - public function fetchStreamNames( - ?string $filter, - ?MetadataMatcher $metadataMatcher, - int $limit = 20, - int $offset = 0 - ): array { - $event = $this->actionEventEmitter->getNewActionEvent( - self::EVENT_FETCH_STREAM_NAMES, - $this, - [ - 'filter' => $filter, - 'metadataMatcher' => $metadataMatcher, - 'limit' => $limit, - 'offset' => $offset, - ] - ); - - $this->actionEventEmitter->dispatch($event); - - return $event->getParam('streamNames', []); - } - - public function fetchStreamNamesRegex( - string $filter, - ?MetadataMatcher $metadataMatcher, - int $limit = 20, - int $offset = 0 - ): array { - $event = $this->actionEventEmitter->getNewActionEvent( - self::EVENT_FETCH_STREAM_NAMES_REGEX, - $this, - [ - 'filter' => $filter, - 'metadataMatcher' => $metadataMatcher, - 'limit' => $limit, - 'offset' => $offset, - ] - ); - - $this->actionEventEmitter->dispatch($event); - - return $event->getParam('streamNames', []); - } - - public function fetchCategoryNames(?string $filter, int $limit = 20, int $offset = 0): array - { - $event = $this->actionEventEmitter->getNewActionEvent( - self::EVENT_FETCH_CATEGORY_NAMES, - $this, - [ - 'filter' => $filter, - 'limit' => $limit, - 'offset' => $offset, - ] - ); - - $this->actionEventEmitter->dispatch($event); - - return $event->getParam('categoryNames', []); - } - - public function fetchCategoryNamesRegex(string $filter, int $limit = 20, int $offset = 0): array - { - $event = $this->actionEventEmitter->getNewActionEvent( - self::EVENT_FETCH_CATEGORY_NAMES_REGEX, - $this, - [ - 'filter' => $filter, - 'limit' => $limit, - 'offset' => $offset, - ] - ); - - $this->actionEventEmitter->dispatch($event); - - return $event->getParam('categoryNames', []); - } - - public function attach(string $eventName, callable $listener, int $priority = 0): ListenerHandler - { - return $this->actionEventEmitter->attachListener($eventName, $listener, $priority); - } - - public function detach(ListenerHandler $handler): void - { - $this->actionEventEmitter->detachListener($handler); - } - - public function getInnerEventStore(): EventStore - { - return $this->eventStore; - } -} diff --git a/src/Upcasting/Upcaster.php b/src/AllCheckpoint.php similarity index 52% rename from src/Upcasting/Upcaster.php rename to src/AllCheckpoint.php index c7bf17bd..a83d76b7 100644 --- a/src/Upcasting/Upcaster.php +++ b/src/AllCheckpoint.php @@ -11,15 +11,16 @@ declare(strict_types=1); -namespace Prooph\EventStore\Upcasting; +namespace Prooph\EventStore; -use Prooph\Common\Messaging\Message; - -interface Upcaster +/** + * This class contains constants to be used when setting up subscriptions + * using the EventStoreAsyncConnection::subscribeToAllFromAsync method + */ +class AllCheckpoint { /** - * @param Message $message - * @return array of messages + * Indicates that a catch-up subscription should receive all events in the database. */ - public function upcast(Message $message): array; + public const ALL_START = null; } diff --git a/src/AllEventsSlice.php b/src/AllEventsSlice.php new file mode 100644 index 00000000..3c6473d6 --- /dev/null +++ b/src/AllEventsSlice.php @@ -0,0 +1,75 @@ + + * (c) 2015-2018 Sascha-Oliver Prolic + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Prooph\EventStore; + +class AllEventsSlice +{ + /** @var ReadDirection */ + private $readDirection; + /** @var Position */ + private $fromPosition; + /** @var Position */ + private $nextPosition; + /** @var ResolvedEvent[] */ + private $events; + /** @var bool */ + private $isEndOfStream; + + /** + * @internal + * + * @param ReadDirection $readDirection + * @param Position $fromPosition + * @param Position $nextPosition + * @param ResolvedEvent[] $events + */ + public function __construct( + ReadDirection $readDirection, + Position $fromPosition, + Position $nextPosition, + array $events + ) { + $this->readDirection = $readDirection; + $this->fromPosition = $fromPosition; + $this->nextPosition = $nextPosition; + $this->events = $events; + $this->isEndOfStream = \count($events) === 0; + } + + public function readDirection(): ReadDirection + { + return $this->readDirection; + } + + public function fromPosition(): Position + { + return $this->fromPosition; + } + + public function nextPosition(): Position + { + return $this->nextPosition; + } + + /** @return ResolvedEvent[] */ + public function events(): array + { + return $this->events; + } + + public function isEndOfStream(): bool + { + return $this->isEndOfStream; + } +} diff --git a/src/Metadata/FieldType.php b/src/AsyncCatchUpSubscriptionDropped.php similarity index 56% rename from src/Metadata/FieldType.php rename to src/AsyncCatchUpSubscriptionDropped.php index a3ff6c7f..951a1daa 100644 --- a/src/Metadata/FieldType.php +++ b/src/AsyncCatchUpSubscriptionDropped.php @@ -11,16 +11,15 @@ declare(strict_types=1); -namespace Prooph\EventStore\Metadata; +namespace Prooph\EventStore; -use MabeEnum\Enum; +use Throwable; -/** - * @method static FieldType METADATA() - * @method static FieldType MESSAGE_PROPERTY() - */ -final class FieldType extends Enum +interface AsyncCatchUpSubscriptionDropped { - public const METADATA = 0; - public const MESSAGE_PROPERTY = 1; + public function __invoke( + AsyncEventStoreCatchUpSubscription $subscription, + SubscriptionDropReason $reason, + ?Throwable $exception = null + ): void; } diff --git a/src/AsyncEventStoreAllCatchUpSubscription.php b/src/AsyncEventStoreAllCatchUpSubscription.php new file mode 100644 index 00000000..6f85a9ae --- /dev/null +++ b/src/AsyncEventStoreAllCatchUpSubscription.php @@ -0,0 +1,19 @@ + + * (c) 2015-2018 Sascha-Oliver Prolic + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Prooph\EventStore; + +interface AsyncEventStoreAllCatchUpSubscription extends AsyncEventStoreCatchUpSubscription +{ + public function lastProcessedPosition(): Position; +} diff --git a/src/AsyncEventStoreCatchUpSubscription.php b/src/AsyncEventStoreCatchUpSubscription.php new file mode 100644 index 00000000..9ab13971 --- /dev/null +++ b/src/AsyncEventStoreCatchUpSubscription.php @@ -0,0 +1,33 @@ + + * (c) 2015-2018 Sascha-Oliver Prolic + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Prooph\EventStore; + +use Amp\Promise; +use Throwable; + +interface AsyncEventStoreCatchUpSubscription +{ + public function isSubscribedToAll(): bool; + + public function streamId(): string; + + public function subscriptionName(): string; + + /** @internal */ + public function startAsync(): Promise; + + public function stop(?int $timeout = null): Promise; + + public function dropSubscription(SubscriptionDropReason $reason, ?Throwable $error): void; +} diff --git a/src/AsyncEventStoreConnection.php b/src/AsyncEventStoreConnection.php new file mode 100644 index 00000000..e1e0d656 --- /dev/null +++ b/src/AsyncEventStoreConnection.php @@ -0,0 +1,239 @@ + + * (c) 2015-2018 Sascha-Oliver Prolic + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Prooph\EventStore; + +use Amp\Promise; +use Prooph\EventStore\Internal\PersistentSubscriptionCreateResult; +use Prooph\EventStore\Internal\PersistentSubscriptionDeleteResult; +use Prooph\EventStore\Internal\PersistentSubscriptionUpdateResult; + +interface AsyncEventStoreConnection +{ + public function connectionName(): string; + + public function connectAsync(): Promise; + + public function close(): void; + + /** @return Promise */ + public function deleteStreamAsync( + string $stream, + int $expectedVersion, + bool $hardDelete = false, + ?UserCredentials $userCredentials = null + ): Promise; + + /** + * @param string $stream + * @param int $expectedVersion + * @param EventData[] $events + * @param UserCredentials|null $userCredentials + * @return Promise + */ + public function appendToStreamAsync( + string $stream, + int $expectedVersion, + array $events = [], + ?UserCredentials $userCredentials = null + ): Promise; + + /** + * @param string $stream + * @param int $expectedVersion + * @param EventData[] $events + * @param UserCredentials|null $userCredentials + * @return Promise + */ + public function conditionalAppendToStreamAsync( + string $stream, + int $expectedVersion, + array $events = [], + ?UserCredentials $userCredentials = null + ): Promise; + + /** @return Promise */ + public function readEventAsync( + string $stream, + int $eventNumber, + bool $resolveLinkTos = true, + ?UserCredentials $userCredentials = null + ): Promise; + + /** @return Promise */ + public function readStreamEventsForwardAsync( + string $stream, + int $start, + int $count, + bool $resolveLinkTos = true, + ?UserCredentials $userCredentials = null + ): Promise; + + /** @return Promise */ + public function readStreamEventsBackwardAsync( + string $stream, + int $start, + int $count, + bool $resolveLinkTos = true, + ?UserCredentials $userCredentials = null + ): Promise; + + /** @return Promise */ + public function readAllEventsForwardAsync( + Position $position, + int $count, + bool $resolveLinkTos = true, + ?UserCredentials $userCredentials = null + ): Promise; + + /** @return Promise */ + public function readAllEventsBackwardAsync( + Position $position, + int $count, + bool $resolveLinkTos = true, + ?UserCredentials $userCredentials = null + ): Promise; + + /** @return Promise */ + public function setStreamMetadataAsync( + string $stream, + int $expectedMetaStreamVersion, + ?StreamMetadata $metadata = null, + ?UserCredentials $userCredentials = null + ): Promise; + + /** @return Promise */ + public function setRawStreamMetadataAsync( + string $stream, + int $expectedMetaStreamVersion, + string $metadata = '', + ?UserCredentials $userCredentials = null + ): Promise; + + /** @return Promise */ + public function getStreamMetadataAsync(string $stream, ?UserCredentials $userCredentials = null): Promise; + + /** @return Promise */ + public function getRawStreamMetadataAsync(string $stream, ?UserCredentials $userCredentials = null): Promise; + + /** @return Promise */ + public function setSystemSettingsAsync(SystemSettings $settings, ?UserCredentials $userCredentials = null): Promise; + + /** @return Promise */ + public function startTransactionAsync( + string $stream, + int $expectedVersion, + ?UserCredentials $userCredentials = null + ): Promise; + + public function continueTransaction( + int $transactionId, + ?UserCredentials $userCredentials = null + ): AsyncEventStoreTransaction; + + /** @return Promise */ + public function createPersistentSubscriptionAsync( + string $stream, + string $groupName, + PersistentSubscriptionSettings $settings, + ?UserCredentials $userCredentials = null + ): Promise; + + /** @return Promise */ + public function updatePersistentSubscriptionAsync( + string $stream, + string $groupName, + PersistentSubscriptionSettings $settings, + ?UserCredentials $userCredentials = null + ): Promise; + + /** @return Promise */ + public function deletePersistentSubscriptionAsync( + string $stream, + string $groupName, + ?UserCredentials $userCredentials = null + ): Promise; + + /** + * @return Promise + */ + public function subscribeToStreamAsync( + string $stream, + bool $resolveLinkTos, + EventAppearedOnSubscription $eventAppeared, + ?SubscriptionDropped $subscriptionDropped = null, + ?UserCredentials $userCredentials = null + ): Promise; + + /** + * @return Promise + */ + public function subscribeToStreamFromAsync( + string $stream, + ?int $lastCheckpoint, + ?CatchUpSubscriptionSettings $settings, + EventAppearedOnAsyncCatchupSubscription $eventAppeared, + ?LiveProcessingStartedOnAsyncCatchUpSubscription $liveProcessingStarted = null, + ?AsyncCatchUpSubscriptionDropped $subscriptionDropped = null, + ?UserCredentials $userCredentials = null + ): Promise; + + /** + * @return Promise + */ + public function subscribeToAllAsync( + bool $resolveLinkTos, + EventAppearedOnSubscription $eventAppeared, + ?SubscriptionDropped $subscriptionDropped = null, + ?UserCredentials $userCredentials = null + ): Promise; + + /** + * @return Promise + */ + public function subscribeToAllFromAsync( + ?Position $lastCheckpoint, + ?CatchUpSubscriptionSettings $settings, + EventAppearedOnAsyncCatchupSubscription $eventAppeared, + ?LiveProcessingStartedOnAsyncCatchUpSubscription $liveProcessingStarted = null, + ?AsyncCatchUpSubscriptionDropped $subscriptionDropped = null, + ?UserCredentials $userCredentials = null + ): Promise; + + /** + * @return Promise + */ + public function connectToPersistentSubscriptionAsync( + string $stream, + string $groupName, + EventAppearedOnAsyncPersistentSubscription $eventAppeared, + ?AsyncPersistentSubscriptionDropped $subscriptionDropped = null, + int $bufferSize = 10, + bool $autoAck = true, + ?UserCredentials $userCredentials = null + ): Promise; + + public function onConnected(callable $handler): ListenerHandler; + + public function onDisconnected(callable $handler): ListenerHandler; + + public function onReconnecting(callable $handler): ListenerHandler; + + public function onClosed(callable $handler): ListenerHandler; + + public function onErrorOccurred(callable $handler): ListenerHandler; + + public function onAuthenticationFailed(callable $handler): ListenerHandler; + + public function detach(ListenerHandler $handler): void; +} diff --git a/src/AsyncEventStorePersistentSubscription.php b/src/AsyncEventStorePersistentSubscription.php new file mode 100644 index 00000000..e31228c7 --- /dev/null +++ b/src/AsyncEventStorePersistentSubscription.php @@ -0,0 +1,93 @@ + + * (c) 2015-2018 Sascha-Oliver Prolic + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Prooph\EventStore; + +use Amp\Promise; +use Prooph\EventStore\Internal\ResolvedEvent; + +interface AsyncEventStorePersistentSubscription +{ + public const DEFAULT_BUFFER_SIZE = 10; + + /** + * @internal + * + * @return Promise + */ + public function start(): Promise; + + /** + * Acknowledge that a message have completed processing (this will tell the server it has been processed) + * Note: There is no need to ack a message if you have Auto Ack enabled + * + * @param ResolvedEvent $event + * + * @return void + */ + public function acknowledge(ResolvedEvent $event): void; + + /** + * Acknowledge that a message have completed processing (this will tell the server it has been processed) + * Note: There is no need to ack a message if you have Auto Ack enabled + * + * @param ResolvedEvent[] $events + * + * @return void + */ + public function acknowledgeMultiple(array $events): void; + + /** + * Acknowledge that a message have completed processing (this will tell the server it has been processed) + * Note: There is no need to ack a message if you have Auto Ack enabled + * + * @param EventId $eventId + * + * @return void + */ + public function acknowledgeEventId(EventId $eventId): void; + + /** + * Acknowledge that a message have completed processing (this will tell the server it has been processed) + * Note: There is no need to ack a message if you have Auto Ack enabled + * + * @param EventId[] $eventIds + * + * @return void + */ + public function acknowledgeMultipleEventIds(array $eventIds): void; + + /** + * Mark a message failed processing. The server will be take action based upon the action paramter + */ + public function fail( + ResolvedEvent $event, + PersistentSubscriptionNakEventAction $action, + string $reason + ): void; + + /** + * Mark n messages that have failed processing. The server will take action based upon the action parameter + * + * @param ResolvedEvent[] $events + * @param PersistentSubscriptionNakEventAction $action + * @param string $reason + */ + public function failMultiple( + array $events, + PersistentSubscriptionNakEventAction $action, + string $reason + ): void; + + public function stop(?int $timeout = null): Promise; +} diff --git a/src/AsyncEventStoreStreamCatchUpSubscription.php b/src/AsyncEventStoreStreamCatchUpSubscription.php new file mode 100644 index 00000000..fb0cf07c --- /dev/null +++ b/src/AsyncEventStoreStreamCatchUpSubscription.php @@ -0,0 +1,19 @@ + + * (c) 2015-2018 Sascha-Oliver Prolic + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Prooph\EventStore; + +interface AsyncEventStoreStreamCatchUpSubscription extends AsyncEventStoreCatchUpSubscription +{ + public function lastProcessedEventNumber(): int; +} diff --git a/src/AsyncEventStoreTransaction.php b/src/AsyncEventStoreTransaction.php new file mode 100644 index 00000000..08339e42 --- /dev/null +++ b/src/AsyncEventStoreTransaction.php @@ -0,0 +1,87 @@ + + * (c) 2015-2018 Sascha-Oliver Prolic + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Prooph\EventStore; + +use Amp\Promise; +use Prooph\EventStore\Internal\AsyncEventStoreTransactionConnection; + +class AsyncEventStoreTransaction +{ + /** @var int */ + private $transactionId; + /** @var UserCredentials|null */ + private $userCredentials; + /** @var AsyncEventStoreTransactionConnection */ + private $connection; + /** @var bool */ + private $isRolledBack; + /** @var bool */ + private $isCommitted; + + public function __construct( + int $transactionId, + ?UserCredentials $userCredentials, + AsyncEventStoreTransactionConnection $connection + ) { + $this->transactionId = $transactionId; + $this->userCredentials = $userCredentials; + $this->connection = $connection; + } + + public function transactionId(): int + { + return $this->transactionId; + } + + /** @return Promise */ + public function commitAsync(): Promise + { + if ($this->isRolledBack) { + throw new \RuntimeException('Cannot commit a rolledback transaction'); + } + + if ($this->isCommitted) { + throw new \RuntimeException('Transaction is already committed'); + } + + return $this->connection->commitTransactionAsync($this, $this->userCredentials); + } + + /** + * @param EventData[] $events + * + * @return Promise + */ + public function writeAsync(array $events = []): Promise + { + if ($this->isRolledBack) { + throw new \RuntimeException('Cannot commit a rolledback transaction'); + } + + if ($this->isCommitted) { + throw new \RuntimeException('Transaction is already committed'); + } + + return $this->connection->transactionalWriteAsync($this, $events, $this->userCredentials); + } + + public function rollback(): void + { + if ($this->isCommitted) { + throw new \RuntimeException('Transaction is already committed'); + } + + $this->isRolledBack = true; + } +} diff --git a/src/AsyncPersistentSubscriptionDropped.php b/src/AsyncPersistentSubscriptionDropped.php new file mode 100644 index 00000000..c8ec6245 --- /dev/null +++ b/src/AsyncPersistentSubscriptionDropped.php @@ -0,0 +1,25 @@ + + * (c) 2015-2018 Sascha-Oliver Prolic + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Prooph\EventStore; + +use Throwable; + +interface AsyncPersistentSubscriptionDropped +{ + public function __invoke( + AsyncEventStorePersistentSubscription $subscription, + SubscriptionDropReason $reason, + ?Throwable $exception = null + ): void; +} diff --git a/src/CatchUpSubscriptionDropped.php b/src/CatchUpSubscriptionDropped.php new file mode 100644 index 00000000..07b007a1 --- /dev/null +++ b/src/CatchUpSubscriptionDropped.php @@ -0,0 +1,25 @@ + + * (c) 2015-2018 Sascha-Oliver Prolic + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Prooph\EventStore; + +use Throwable; + +interface CatchUpSubscriptionDropped +{ + public function __invoke( + EventStoreCatchUpSubscription $subscription, + SubscriptionDropReason $reason, + ?Throwable $exception = null + ): void; +} diff --git a/src/CatchUpSubscriptionSettings.php b/src/CatchUpSubscriptionSettings.php new file mode 100644 index 00000000..44f4888a --- /dev/null +++ b/src/CatchUpSubscriptionSettings.php @@ -0,0 +1,117 @@ + + * (c) 2015-2018 Sascha-Oliver Prolic + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Prooph\EventStore; + +use Prooph\EventStore\Exception\InvalidArgumentException; +use Prooph\EventStore\Internal\Consts; + +class CatchUpSubscriptionSettings +{ + /** + * The maximum amount of events to cache when processing from a live subscription. + * Going above this value will drop the subscription. + * + * @var int + */ + private $maxLiveQueueSize; + + /** + * The number of events to read per batch when reading the history. + * + * @var int + */ + private $readBatchSize; + + /** @var bool */ + private $verboseLogging; + + /** @var bool */ + private $resolveLinkTos; + + /** @var string */ + private $subscriptionName; + + public function __construct( + int $maxLiveQueueSize, + int $readBatchSize, + bool $verboseLogging, + bool $resolveLinkTos, + string $subscriptionName = '' + ) { + if ($readBatchSize < 1) { + throw new InvalidArgumentException('Read batch size must be positive'); + } + + if ($maxLiveQueueSize < 1) { + throw new InvalidArgumentException('Max live queue size must be positive'); + } + + if ($readBatchSize > Consts::MAX_READ_SIZE) { + throw new InvalidArgumentException(\sprintf( + 'Read batch size should be less than \'%s\'. For larger reads you should page', + Consts::MAX_READ_SIZE + )); + } + + $this->maxLiveQueueSize = $maxLiveQueueSize; + $this->readBatchSize = $readBatchSize; + $this->verboseLogging = $verboseLogging; + $this->resolveLinkTos = $resolveLinkTos; + $this->subscriptionName = $subscriptionName; + } + + public static function default(): self + { + return new self( + Consts::CATCH_UP_DEFAULT_MAX_PUSH_QUEUE_SIZE, + Consts::CATCH_UP_DEFAULT_READ_BATCH_SIZE, + false, + true, + '' + ); + } + + public function maxLiveQueueSize(): int + { + return $this->maxLiveQueueSize; + } + + public function readBatchSize(): int + { + return $this->readBatchSize; + } + + public function verboseLogging(): bool + { + return $this->verboseLogging; + } + + public function enableVerboseLogging(): self + { + $self = clone $this; + $self->verboseLogging = true; + + return $self; + } + + public function resolveLinkTos(): bool + { + return $this->resolveLinkTos; + } + + public function subscriptionName(): string + { + return $this->subscriptionName; + } +} diff --git a/src/ClientAuthenticationFailedEventArgs.php b/src/ClientAuthenticationFailedEventArgs.php new file mode 100644 index 00000000..d1813824 --- /dev/null +++ b/src/ClientAuthenticationFailedEventArgs.php @@ -0,0 +1,38 @@ + + * (c) 2015-2018 Sascha-Oliver Prolic + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Prooph\EventStore; + +class ClientAuthenticationFailedEventArgs implements EventArgs +{ + /** @var AsyncEventStoreConnection */ + private $connection; + /** @var string */ + private $reason; + + public function __construct(AsyncEventStoreConnection $connection, string $reason) + { + $this->connection = $connection; + $this->reason = $reason; + } + + public function connection(): AsyncEventStoreConnection + { + return $this->connection; + } + + public function reason(): string + { + return $this->reason; + } +} diff --git a/src/ClientClosedEventArgs.php b/src/ClientClosedEventArgs.php new file mode 100644 index 00000000..876c5419 --- /dev/null +++ b/src/ClientClosedEventArgs.php @@ -0,0 +1,38 @@ + + * (c) 2015-2018 Sascha-Oliver Prolic + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Prooph\EventStore; + +class ClientClosedEventArgs implements EventArgs +{ + /** @var AsyncEventStoreConnection */ + private $connection; + /** @var string */ + private $reason; + + public function __construct(AsyncEventStoreConnection $connection, string $reason) + { + $this->connection = $connection; + $this->reason = $reason; + } + + public function connection(): AsyncEventStoreConnection + { + return $this->connection; + } + + public function reason(): string + { + return $this->reason; + } +} diff --git a/src/ClientConnectionEventArgs.php b/src/ClientConnectionEventArgs.php new file mode 100644 index 00000000..ca4069b2 --- /dev/null +++ b/src/ClientConnectionEventArgs.php @@ -0,0 +1,38 @@ + + * (c) 2015-2018 Sascha-Oliver Prolic + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Prooph\EventStore; + +class ClientConnectionEventArgs implements EventArgs +{ + /** @var AsyncEventStoreConnection */ + private $connection; + /** @var EndPoint */ + private $remoteEndPoint; + + public function __construct(AsyncEventStoreConnection $connection, EndPoint $remoteEndPoint) + { + $this->connection = $connection; + $this->remoteEndPoint = $remoteEndPoint; + } + + public function connection(): AsyncEventStoreConnection + { + return $this->connection; + } + + public function remoteEndPoint(): EndPoint + { + return $this->remoteEndPoint; + } +} diff --git a/src/ClientErrorEventArgs.php b/src/ClientErrorEventArgs.php new file mode 100644 index 00000000..06ba31b3 --- /dev/null +++ b/src/ClientErrorEventArgs.php @@ -0,0 +1,40 @@ + + * (c) 2015-2018 Sascha-Oliver Prolic + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Prooph\EventStore; + +use Throwable; + +class ClientErrorEventArgs implements EventArgs +{ + /** @var AsyncEventStoreConnection */ + private $connection; + /** @var Throwable */ + private $exception; + + public function __construct(AsyncEventStoreConnection $connection, Throwable $exception) + { + $this->connection = $connection; + $this->exception = $exception; + } + + public function connection(): AsyncEventStoreConnection + { + return $this->connection; + } + + public function exception(): Throwable + { + return $this->exception; + } +} diff --git a/src/StreamName.php b/src/ClientReconnectingEventArgs.php similarity index 51% rename from src/StreamName.php rename to src/ClientReconnectingEventArgs.php index 094b0b6e..6d419a42 100644 --- a/src/StreamName.php +++ b/src/ClientReconnectingEventArgs.php @@ -13,27 +13,18 @@ namespace Prooph\EventStore; -class StreamName +class ClientReconnectingEventArgs implements EventArgs { - /** - * @var string - */ - protected $name; + /** @var AsyncEventStoreConnection */ + private $connection; - public function __construct(string $name) + public function __construct(AsyncEventStoreConnection $connection) { - Util\Assertion::notEmpty($name, 'StreamName must not be empty'); - - $this->name = $name; - } - - public function toString(): string - { - return $this->name; + $this->connection = $connection; } - public function __toString(): string + public function connection(): AsyncEventStoreConnection { - return $this->toString(); + return $this->connection; } } diff --git a/src/Common/SystemConsumerStrategies.php b/src/Common/SystemConsumerStrategies.php new file mode 100644 index 00000000..8c526129 --- /dev/null +++ b/src/Common/SystemConsumerStrategies.php @@ -0,0 +1,25 @@ + + * (c) 2015-2018 Sascha-Oliver Prolic + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Prooph\EventStore\Common; + +class SystemConsumerStrategies +{ + // Distributes events to a single client until it is full. Then round robin to the next client. + public const DISPATCH_TO_SINGLE = 'DispatchToSingle'; + // Distribute events to each client in a round robin fashion. + public const ROUND_ROBIN = 'RoundRobin'; + // Distribute events of the same streamId to the same client until it disconnects on a best efforts basis. + // Designed to be used with indexes such as the category projection. + public const PINNED = 'Pinned'; +} diff --git a/src/Common/SystemEventTypes.php b/src/Common/SystemEventTypes.php new file mode 100644 index 00000000..5f5f3bee --- /dev/null +++ b/src/Common/SystemEventTypes.php @@ -0,0 +1,28 @@ + + * (c) 2015-2018 Sascha-Oliver Prolic + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Prooph\EventStore\Common; + +class SystemEventTypes +{ + // event type for stream deleted + public const STREAM_DELETED = '$streamDeleted'; + // event type for statistics + public const STATS_COLLECTED = '$statsCollected'; + // event type for linkTo + public const LINK_TO = '$>'; + // event type for stream metadata + public const STREAM_METADATA = '$metadata'; + // event type for the system settings + public const SETTINGS = '$settings'; +} diff --git a/src/Common/SystemMetadata.php b/src/Common/SystemMetadata.php new file mode 100644 index 00000000..b191bd88 --- /dev/null +++ b/src/Common/SystemMetadata.php @@ -0,0 +1,45 @@ + + * (c) 2015-2018 Sascha-Oliver Prolic + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Prooph\EventStore\Common; + +class SystemMetadata +{ + // The definition of the MaxAge value assigned to stream metadata + // Setting this allows all events older than the limit to be deleted + public const MAX_AGE = '$maxAge'; + // The definition of the MaxCount value assigned to stream metadata + // setting this allows all events with a sequence less than current -maxcount to be deleted + public const MAX_COUNT = '$maxCount'; + // The definition of the Truncate Before value assigned to stream metadata + // setting this allows all events prior to the integer value to be deleted + public const TRUNCATE_BEFORE = '$tb'; + // Sets the cache control in seconds for the head of the stream. + public const CACHE_CONTROL = '$cacheControl'; + // The acl definition in metadata + public const ACL = '$acl'; + // to read from a stream + public const ACL_READ = '$r'; + // to write to a stream + public const ACL_WRITE = '$w'; + // to delete a stream + public const ACL_DELETE = '$d'; + // to read metadata + public const ACL_META_READ = '$mr'; + // to write metadata + public const ACL_META_WRITE = '$mw'; + // The user default acl stream + public const USER_STREAM_ACL = '$userStreamAcl'; + // the system stream defaults acl stream + public const SYSTEM_STREAM_ACL = '$systemStreamAcl'; +} diff --git a/src/StreamIterator/InMemoryStreamIterator.php b/src/Common/SystemRoles.php similarity index 69% rename from src/StreamIterator/InMemoryStreamIterator.php rename to src/Common/SystemRoles.php index 4defaaa0..2553c6a7 100644 --- a/src/StreamIterator/InMemoryStreamIterator.php +++ b/src/Common/SystemRoles.php @@ -11,10 +11,11 @@ declare(strict_types=1); -namespace Prooph\EventStore\StreamIterator; +namespace Prooph\EventStore\Common; -use ArrayIterator; - -final class InMemoryStreamIterator extends ArrayIterator implements StreamIterator +class SystemRoles { + public const ALL = '$all'; + public const ADMINS = '$admins'; + public const OPS = '$ops'; } diff --git a/src/Common/SystemStreams.php b/src/Common/SystemStreams.php new file mode 100644 index 00000000..2016f23c --- /dev/null +++ b/src/Common/SystemStreams.php @@ -0,0 +1,44 @@ + + * (c) 2015-2018 Sascha-Oliver Prolic + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Prooph\EventStore\Common; + +class SystemStreams +{ + public const PERSISTENT_SUBSCRIPTION_CONFIG = '$persistentSubscriptionConfig'; + public const ALL_STREAM = '$all'; + public const STREAMS_STREAM = '$streams'; + public const SETTINGS_STREAM = '$settings'; + public const STATS_STREAM_PREFIX = '$stats'; + public const SCAVANGE_STREAM = '$scavenges'; + + public static function metastreamOf(string $streamId): string + { + return '$$' . $streamId; + } + + public static function isMetastream(string $streamId): bool + { + return \strlen($streamId) > 1 && \substr($streamId, 0, 2) === '$$'; + } + + public static function originalStreamOf(string $metastreamId): string + { + return \substr($metastreamId, 2); + } + + public static function isSystemStream(string $streamId): bool + { + return \strlen($streamId) !== 0 && $streamId[0] === '$'; + } +} diff --git a/src/ConditionalWriteResult.php b/src/ConditionalWriteResult.php new file mode 100644 index 00000000..34fbff2b --- /dev/null +++ b/src/ConditionalWriteResult.php @@ -0,0 +1,68 @@ + + * (c) 2015-2018 Sascha-Oliver Prolic + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Prooph\EventStore; + +use Prooph\EventStore\Exception\InvalidArgumentException; + +class ConditionalWriteResult +{ + /** @var ConditionalWriteStatus */ + private $status; + /** @var int|null */ + private $nextExpectedVersion; + /** @var Position|null */ + private $logPosition; + + private function __construct() + { + } + + public static function success(int $nextExpectedVersion, Position $logPosition): ConditionalWriteResult + { + $self = new self(); + + $self->status = ConditionalWriteStatus::succeeded(); + $self->nextExpectedVersion = $nextExpectedVersion; + $self->logPosition = $logPosition; + + return $self; + } + + public static function fail(ConditionalWriteStatus $status): ConditionalWriteResult + { + if ($status->equals(ConditionalWriteStatus::succeeded())) { + throw new InvalidArgumentException('For successful write pass next expected version and log position'); + } + + $self = new self(); + $self->status = $status; + + return $self; + } + + public function status(): ConditionalWriteStatus + { + return $this->status; + } + + public function nextExpectedVersion(): ?int + { + return $this->nextExpectedVersion; + } + + public function logPosition(): ?Position + { + return $this->logPosition; + } +} diff --git a/src/ConditionalWriteStatus.php b/src/ConditionalWriteStatus.php new file mode 100644 index 00000000..1e15a561 --- /dev/null +++ b/src/ConditionalWriteStatus.php @@ -0,0 +1,93 @@ + + * (c) 2015-2018 Sascha-Oliver Prolic + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Prooph\EventStore; + +use Prooph\EventStore\Exception\InvalidArgumentException; + +class ConditionalWriteStatus +{ + public const OPTIONS = [ + 'Succeeded' => 0, + 'VersionMismatch' => 1, + 'StreamDeleted' => 2, + ]; + + public const SUCCEEDED = 0; + public const VERSION_MISMATCH = 1; + public const STREAM_DELETED = 2; + + private $name; + private $value; + + private function __construct(string $name) + { + $this->name = $name; + $this->value = self::OPTIONS[$name]; + } + + public static function succeeded(): self + { + return new self('Succeeded'); + } + + public static function versionMismatch(): self + { + return new self('VersionMismatch'); + } + + public static function streamDeleted(): self + { + return new self('StreamDeleted'); + } + + public static function byName(string $value): self + { + if (! isset(self::OPTIONS[$value])) { + throw new InvalidArgumentException('Unknown enum name given'); + } + + return self::{$value}(); + } + + public static function byValue($value): self + { + foreach (self::OPTIONS as $name => $v) { + if ($v === $value) { + return self::{$name}(); + } + } + + throw new InvalidArgumentException('Unknown enum value given'); + } + + public function equals(ConditionalWriteStatus $other): bool + { + return \get_class($this) === \get_class($other) && $this->name === $other->name; + } + + public function name(): string + { + return $this->name; + } + + public function value() + { + return $this->value; + } + + public function __toString(): string + { + return $this->name; + } +} diff --git a/src/Container/InMemoryEventStoreFactory.php b/src/Container/InMemoryEventStoreFactory.php deleted file mode 100644 index bcd0fb41..00000000 --- a/src/Container/InMemoryEventStoreFactory.php +++ /dev/null @@ -1,213 +0,0 @@ - - * (c) 2015-2018 Sascha-Oliver Prolic - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace Prooph\EventStore\Container; - -use Interop\Config\ConfigurationTrait; -use Interop\Config\ProvidesDefaultOptions; -use Interop\Config\RequiresConfig; -use Interop\Config\RequiresConfigId; -use Prooph\Common\Event\ActionEventEmitter; -use Prooph\Common\Event\ProophActionEventEmitter; -use Prooph\EventStore\ActionEventEmitterEventStore; -use Prooph\EventStore\EventStore; -use Prooph\EventStore\Exception\ConfigurationException; -use Prooph\EventStore\Exception\InvalidArgumentException; -use Prooph\EventStore\InMemoryEventStore; -use Prooph\EventStore\Metadata\MetadataEnricher; -use Prooph\EventStore\Metadata\MetadataEnricherAggregate; -use Prooph\EventStore\Metadata\MetadataEnricherPlugin; -use Prooph\EventStore\NonTransactionalInMemoryEventStore; -use Prooph\EventStore\Plugin\Plugin; -use Prooph\EventStore\ReadOnlyEventStore; -use Prooph\EventStore\ReadOnlyEventStoreWrapper; -use Prooph\EventStore\TransactionalActionEventEmitterEventStore; -use Prooph\EventStore\TransactionalEventStore; -use Psr\Container\ContainerInterface; - -final class InMemoryEventStoreFactory implements - ProvidesDefaultOptions, - RequiresConfig, - RequiresConfigId -{ - use ConfigurationTrait; - - /** - * @var string - */ - private $configId; - - /** - * @var bool - */ - private $isTransactional; - - /** - * Creates a new instance from a specified config, specifically meant to be used as static factory. - * - * In case you want to use another config key than provided by the factories, you can add the following factory to - * your config: - * - * - * [InMemoryEventStoreFactory::class, 'service_name'], - * ]; - * - * - * @throws InvalidArgumentException - */ - public static function __callStatic(string $name, array $arguments): ReadOnlyEventStore - { - if (! isset($arguments[0]) || ! $arguments[0] instanceof ContainerInterface) { - throw new InvalidArgumentException( - \sprintf('The first argument must be of type %s', ContainerInterface::class) - ); - } - - return (new static($name))->__invoke($arguments[0]); - } - - public function __construct(string $configId = 'default') - { - $this->configId = $configId; - } - - /** - * @throws ConfigurationException - */ - public function __invoke(ContainerInterface $container): ReadOnlyEventStore - { - $config = $container->get('config'); - $config = $this->options($config, $this->configId); - - $this->isTransactional = $this->isTransactional($config); - - $eventStore = $this->createEventStore(); - - if ($config['read_only']) { - $eventStore = new ReadOnlyEventStoreWrapper($eventStore); - } - - if (! $config['wrap_action_event_emitter']) { - return $eventStore; - } - - if (! isset($config['event_emitter'])) { - $eventEmitter = new ProophActionEventEmitter($this->determineEventsForDefaultEmitter()); - } else { - $eventEmitter = $container->get($config['event_emitter']); - } - - $wrapper = $this->createActionEventEmitterDecorator($eventStore, $eventEmitter); - - foreach ($config['plugins'] as $pluginAlias) { - $plugin = $container->get($pluginAlias); - - if (! $plugin instanceof Plugin) { - throw ConfigurationException::configurationError( - \sprintf( - 'Plugin %s does not implement the Plugin interface', - $pluginAlias - ) - ); - } - - $plugin->attachToEventStore($wrapper); - } - - if (\count($config['metadata_enrichers']) > 0) { - $metadataEnrichers = []; - - foreach ($config['metadata_enrichers'] as $metadataEnricherAlias) { - $metadataEnricher = $container->get($metadataEnricherAlias); - - if (! $metadataEnricher instanceof MetadataEnricher) { - throw ConfigurationException::configurationError( - \sprintf( - 'Metadata enricher %s does not implement the MetadataEnricher interface', - $metadataEnricherAlias - ) - ); - } - - $metadataEnrichers[] = $metadataEnricher; - } - - $plugin = new MetadataEnricherPlugin( - new MetadataEnricherAggregate($metadataEnrichers) - ); - - $plugin->attachToEventStore($wrapper); - } - - return $wrapper; - } - - /** - * {@inheritdoc} - */ - public function dimensions(): iterable - { - return ['prooph', 'event_store']; - } - - /** - * {@inheritdoc} - */ - public function defaultOptions(): iterable - { - return [ - 'metadata_enrichers' => [], - 'plugins' => [], - 'wrap_action_event_emitter' => true, - 'transactional' => true, - 'read_only' => false, - ]; - } - - private function determineEventsForDefaultEmitter(): array - { - if ($this->isTransactional) { - return TransactionalActionEventEmitterEventStore::ALL_EVENTS; - } - - return ActionEventEmitterEventStore::ALL_EVENTS; - } - - private function createEventStore(): EventStore - { - if ($this->isTransactional) { - return new InMemoryEventStore(); - } - - return new NonTransactionalInMemoryEventStore(); - } - - private function createActionEventEmitterDecorator( - EventStore $eventStore, - ActionEventEmitter $actionEventEmitter - ): ActionEventEmitterEventStore { - if ($this->isTransactional) { - /** @var TransactionalEventStore $eventStore */ - return new TransactionalActionEventEmitterEventStore($eventStore, $actionEventEmitter); - } - - return new ActionEventEmitterEventStore($eventStore, $actionEventEmitter); - } - - private function isTransactional(array $config): bool - { - return isset($config['transactional']) && $config['transactional'] === true; - } -} diff --git a/src/Container/InMemoryProjectionManagerFactory.php b/src/Container/InMemoryProjectionManagerFactory.php deleted file mode 100644 index 54a7c92a..00000000 --- a/src/Container/InMemoryProjectionManagerFactory.php +++ /dev/null @@ -1,95 +0,0 @@ - - * (c) 2015-2018 Sascha-Oliver Prolic - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace Prooph\EventStore\Container; - -use Interop\Config\ConfigurationTrait; -use Interop\Config\RequiresConfig; -use Interop\Config\RequiresConfigId; -use Interop\Config\RequiresMandatoryOptions; -use Prooph\EventStore\Exception\ConfigurationException; -use Prooph\EventStore\Exception\InvalidArgumentException; -use Prooph\EventStore\Projection\InMemoryProjectionManager; -use Psr\Container\ContainerInterface; - -final class InMemoryProjectionManagerFactory implements - RequiresConfig, - RequiresConfigId, - RequiresMandatoryOptions -{ - use ConfigurationTrait; - - /** - * @var string - */ - private $configId; - - /** - * Creates a new instance from a specified config, specifically meant to be used as static factory. - * - * In case you want to use another config key than provided by the factories, you can add the following factory to - * your config: - * - * - * [InMemoryProjectionManager::class, 'service_name'], - * ]; - * - * - * @throws InvalidArgumentException - */ - public static function __callStatic(string $name, array $arguments): InMemoryProjectionManager - { - if (! isset($arguments[0]) || ! $arguments[0] instanceof ContainerInterface) { - throw new InvalidArgumentException( - \sprintf('The first argument must be of type %s', ContainerInterface::class) - ); - } - - return (new static($name))->__invoke($arguments[0]); - } - - public function __construct(string $configId = 'default') - { - $this->configId = $configId; - } - - /** - * @throws ConfigurationException - */ - public function __invoke(ContainerInterface $container): InMemoryProjectionManager - { - $config = $container->get('config'); - $config = $this->options($config, $this->configId); - - $eventStore = $container->get($config['event_store']); - - return new InMemoryProjectionManager($eventStore); - } - - /** - * {@inheritdoc} - */ - public function dimensions(): iterable - { - return ['prooph', 'projection_manager']; - } - - public function mandatoryOptions(): iterable - { - return [ - 'event_store', - ]; - } -} diff --git a/src/Upcasting/NoOpEventUpcaster.php b/src/DeleteResult.php similarity index 55% rename from src/Upcasting/NoOpEventUpcaster.php rename to src/DeleteResult.php index 59816770..488183ca 100644 --- a/src/Upcasting/NoOpEventUpcaster.php +++ b/src/DeleteResult.php @@ -11,14 +11,20 @@ declare(strict_types=1); -namespace Prooph\EventStore\Upcasting; +namespace Prooph\EventStore; -use Prooph\Common\Messaging\Message; - -final class NoOpEventUpcaster implements Upcaster +class DeleteResult { - public function upcast(Message $message): array + /** @var Position */ + private $logPosition; + + public function __construct(Position $logPosition) + { + $this->logPosition = $logPosition; + } + + public function logPosition(): Position { - return [$message]; + return $this->logPosition; } } diff --git a/src/EndPoint.php b/src/EndPoint.php new file mode 100644 index 00000000..0dfc16fc --- /dev/null +++ b/src/EndPoint.php @@ -0,0 +1,48 @@ + + * (c) 2015-2018 Sascha-Oliver Prolic + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Prooph\EventStore; + +class EndPoint +{ + /** @var string */ + private $host; + /** @var int */ + private $port; + + public function __construct(string $host, int $port) + { + $this->host = $host; + $this->port = $port; + } + + public function host(): string + { + return $this->host; + } + + public function port(): int + { + return $this->port; + } + + public function equals(EndPoint $endPoint): bool + { + return $this->host === $endPoint->host && $this->port === $endPoint->port; + } + + public function __toString(): string + { + return $this->host . ':' . $this->port; + } +} diff --git a/src/EventAppearedOnAsyncCatchupSubscription.php b/src/EventAppearedOnAsyncCatchupSubscription.php new file mode 100644 index 00000000..462a6b2d --- /dev/null +++ b/src/EventAppearedOnAsyncCatchupSubscription.php @@ -0,0 +1,24 @@ + + * (c) 2015-2018 Sascha-Oliver Prolic + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Prooph\EventStore; + +use Amp\Promise; + +interface EventAppearedOnAsyncCatchupSubscription +{ + public function __invoke( + AsyncEventStoreCatchUpSubscription $subscription, + ResolvedEvent $resolvedEvent + ): Promise; +} diff --git a/src/EventAppearedOnAsyncPersistentSubscription.php b/src/EventAppearedOnAsyncPersistentSubscription.php new file mode 100644 index 00000000..980cc723 --- /dev/null +++ b/src/EventAppearedOnAsyncPersistentSubscription.php @@ -0,0 +1,25 @@ + + * (c) 2015-2018 Sascha-Oliver Prolic + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Prooph\EventStore; + +use Amp\Promise; + +interface EventAppearedOnAsyncPersistentSubscription +{ + public function __invoke( + AsyncEventStorePersistentSubscription $subscription, + ResolvedEvent $resolvedEvent, + ?int $retryCount = null + ): Promise; +} diff --git a/src/Metadata/MetadataEnricher.php b/src/EventAppearedOnCatchupSubscription.php similarity index 60% rename from src/Metadata/MetadataEnricher.php rename to src/EventAppearedOnCatchupSubscription.php index 5daeab91..7c8c9a12 100644 --- a/src/Metadata/MetadataEnricher.php +++ b/src/EventAppearedOnCatchupSubscription.php @@ -11,14 +11,14 @@ declare(strict_types=1); -namespace Prooph\EventStore\Metadata; +namespace Prooph\EventStore; -use Prooph\Common\Messaging\Message; +use Amp\Promise; -interface MetadataEnricher +interface EventAppearedOnCatchupSubscription { - /** - * Return the given message with added metadata. - */ - public function enrich(Message $message): Message; + public function __invoke( + EventStoreCatchUpSubscription $subscription, + ResolvedEvent $resolvedEvent + ): Promise; } diff --git a/src/EventAppearedOnPersistentSubscription.php b/src/EventAppearedOnPersistentSubscription.php new file mode 100644 index 00000000..3de32d9f --- /dev/null +++ b/src/EventAppearedOnPersistentSubscription.php @@ -0,0 +1,25 @@ + + * (c) 2015-2018 Sascha-Oliver Prolic + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Prooph\EventStore; + +use Amp\Promise; + +interface EventAppearedOnPersistentSubscription +{ + public function __invoke( + EventStorePersistentSubscription $subscription, + ResolvedEvent $resolvedEvent, + ?int $retryCount = null + ): Promise; +} diff --git a/src/StreamIterator/EmptyStreamIterator.php b/src/EventAppearedOnSubscription.php similarity index 62% rename from src/StreamIterator/EmptyStreamIterator.php rename to src/EventAppearedOnSubscription.php index fdb8c4ad..6315d8e9 100644 --- a/src/StreamIterator/EmptyStreamIterator.php +++ b/src/EventAppearedOnSubscription.php @@ -11,14 +11,14 @@ declare(strict_types=1); -namespace Prooph\EventStore\StreamIterator; +namespace Prooph\EventStore; -use EmptyIterator; +use Amp\Promise; -final class EmptyStreamIterator extends EmptyIterator implements StreamIterator +interface EventAppearedOnSubscription { - public function count(): int - { - return 0; - } + public function __invoke( + EventStoreSubscription $subscription, + ResolvedEvent $resolvedEvent + ): Promise; } diff --git a/tests/Mock/Product.php b/src/EventArgs.php similarity index 85% rename from tests/Mock/Product.php rename to src/EventArgs.php index 0b68908c..581fa9ba 100644 --- a/tests/Mock/Product.php +++ b/src/EventArgs.php @@ -11,8 +11,8 @@ declare(strict_types=1); -namespace ProophTest\EventStore\Mock; +namespace Prooph\EventStore; -final class Product +interface EventArgs { } diff --git a/src/EventData.php b/src/EventData.php new file mode 100644 index 00000000..e0eb8023 --- /dev/null +++ b/src/EventData.php @@ -0,0 +1,69 @@ + + * (c) 2015-2018 Sascha-Oliver Prolic + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Prooph\EventStore; + +/** + * Represents an event to be written. + */ +class EventData +{ + /** @var EventId */ + private $eventId; + /** @var string */ + private $eventType; + /** @var bool */ + private $isJson; + /** @var string */ + private $data; + /** @var string */ + private $metaData; + + public function __construct(?EventId $eventId, string $eventType, bool $isJson, string $data = '', string $metaData = '') + { + if (null === $eventId) { + $eventId = EventId::generate(); + } + + $this->eventId = $eventId; + $this->eventType = $eventType; + $this->isJson = $isJson; + $this->data = $data; + $this->metaData = $metaData; + } + + public function eventId(): EventId + { + return $this->eventId; + } + + public function eventType(): string + { + return $this->eventType; + } + + public function isJson(): bool + { + return $this->isJson; + } + + public function data(): string + { + return $this->data; + } + + public function metaData(): string + { + return $this->metaData; + } +} diff --git a/src/EventId.php b/src/EventId.php new file mode 100644 index 00000000..072a3d05 --- /dev/null +++ b/src/EventId.php @@ -0,0 +1,63 @@ + + * (c) 2015-2018 Sascha-Oliver Prolic + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Prooph\EventStore; + +use Prooph\EventStore\Util\Guid; +use Ramsey\Uuid\UuidInterface; + +class EventId +{ + /** @var UuidInterface */ + private $uuid; + + public static function generate(): EventId + { + return new self(Guid::generate()); + } + + public static function fromString(string $eventId): EventId + { + return new self(Guid::fromString($eventId)); + } + + public static function fromBinary(string $bytes): EventId + { + return new self(Guid::fromBytes($bytes)); + } + + private function __construct(UuidInterface $eventId) + { + $this->uuid = $eventId; + } + + public function toString(): string + { + return $this->uuid->toString(); + } + + public function toBinary(): string + { + return $this->uuid->getBytes(); + } + + public function __toString(): string + { + return $this->uuid->toString(); + } + + public function equals(EventId $other): bool + { + return $this->uuid->equals($other->uuid); + } +} diff --git a/src/EventReadResult.php b/src/EventReadResult.php new file mode 100644 index 00000000..43d5d547 --- /dev/null +++ b/src/EventReadResult.php @@ -0,0 +1,58 @@ + + * (c) 2015-2018 Sascha-Oliver Prolic + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Prooph\EventStore; + +class EventReadResult +{ + /** @var EventReadStatus */ + private $status; + + /** @var string */ + private $stream; + + /** @var int */ + private $eventNumber; + + /** @var ResolvedEvent|null */ + private $event; + + /** @internal */ + public function __construct(EventReadStatus $status, string $stream, int $eventNumber, ?ResolvedEvent $event) + { + $this->status = $status; + $this->stream = $stream; + $this->eventNumber = $eventNumber; + $this->event = $event; + } + + public function status(): EventReadStatus + { + return $this->status; + } + + public function stream(): string + { + return $this->stream; + } + + public function eventNumber(): int + { + return $this->eventNumber; + } + + public function event(): ?ResolvedEvent + { + return $this->event; + } +} diff --git a/src/EventReadStatus.php b/src/EventReadStatus.php new file mode 100644 index 00000000..2047c619 --- /dev/null +++ b/src/EventReadStatus.php @@ -0,0 +1,100 @@ + + * (c) 2015-2018 Sascha-Oliver Prolic + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Prooph\EventStore; + +use Prooph\EventStore\Exception\InvalidArgumentException; + +class EventReadStatus +{ + public const OPTIONS = [ + 'Success' => 0, + 'NotFound' => 1, + 'NoStream' => 2, + 'StreamDeleted' => 3, + ]; + + public const SUCCESS = 0; + public const NOT_FOUND = 1; + public const NO_STREAM = 2; + public const STREAM_DELETED = 3; + + private $name; + private $value; + + private function __construct(string $name) + { + $this->name = $name; + $this->value = self::OPTIONS[$name]; + } + + public static function success(): self + { + return new self('Success'); + } + + public static function notFound(): self + { + return new self('NotFound'); + } + + public static function noStream(): self + { + return new self('NoStream'); + } + + public static function streamDeleted(): self + { + return new self('StreamDeleted'); + } + + public static function byName(string $value): self + { + if (! isset(self::OPTIONS[$value])) { + throw new InvalidArgumentException('Unknown enum name given'); + } + + return self::{$value}(); + } + + public static function byValue($value): self + { + foreach (self::OPTIONS as $name => $v) { + if ($v === $value) { + return self::{$name}(); + } + } + + throw new InvalidArgumentException('Unknown enum value given'); + } + + public function equals(EventReadStatus $other): bool + { + return static::class === \get_class($other) && $this->name === $other->name; + } + + public function name(): string + { + return $this->name; + } + + public function value() + { + return $this->value; + } + + public function __toString(): string + { + return $this->name; + } +} diff --git a/src/EventStoreAllCatchUpSubscription.php b/src/EventStoreAllCatchUpSubscription.php new file mode 100644 index 00000000..9b978823 --- /dev/null +++ b/src/EventStoreAllCatchUpSubscription.php @@ -0,0 +1,19 @@ + + * (c) 2015-2018 Sascha-Oliver Prolic + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Prooph\EventStore; + +interface EventStoreAllCatchUpSubscription extends EventStoreCatchUpSubscription +{ + public function lastProcessedPosition(): Position; +} diff --git a/src/EventStoreCatchUpSubscription.php b/src/EventStoreCatchUpSubscription.php new file mode 100644 index 00000000..1334b9dd --- /dev/null +++ b/src/EventStoreCatchUpSubscription.php @@ -0,0 +1,31 @@ + + * (c) 2015-2018 Sascha-Oliver Prolic + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Prooph\EventStore; + +use Throwable; + +interface EventStoreCatchUpSubscription +{ + public function isSubscribedToAll(): bool; + + public function streamId(): string; + + public function subscriptionName(): string; + + public function start(): void; + + public function stop(): void; + + public function dropSubscription(SubscriptionDropReason $reason, ?Throwable $error): void; +} diff --git a/src/EventStoreConnection.php b/src/EventStoreConnection.php new file mode 100644 index 00000000..6ffc4bb4 --- /dev/null +++ b/src/EventStoreConnection.php @@ -0,0 +1,182 @@ + + * (c) 2015-2018 Sascha-Oliver Prolic + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Prooph\EventStore; + +use Prooph\EventStore\Internal\PersistentSubscriptionCreateResult; +use Prooph\EventStore\Internal\PersistentSubscriptionDeleteResult; +use Prooph\EventStore\Internal\PersistentSubscriptionUpdateResult; + +interface EventStoreConnection +{ + public function deleteStream( + string $stream, + int $expectedVersion, + bool $hardDelete = false, + ?UserCredentials $userCredentials = null + ): DeleteResult; + + /** + * @param string $stream + * @param int $expectedVersion + * @param EventData[] $events + * @param UserCredentials|null $userCredentials + * + * @return WriteResult + */ + public function appendToStream( + string $stream, + int $expectedVersion, + array $events = [], + ?UserCredentials $userCredentials = null + ): WriteResult; + + public function conditionalAppendToStream( + string $stream, + int $expectedVersion, + array $events = [], + ?UserCredentials $userCredentials = null + ): ConditionalWriteResult; + + public function readEvent( + string $stream, + int $eventNumber, + bool $resolveLinkTos = true, + ?UserCredentials $userCredentials = null + ): EventReadResult; + + public function readStreamEventsForward( + string $stream, + int $start, + int $count, + bool $resolveLinkTos = true, + ?UserCredentials $userCredentials = null + ): StreamEventsSlice; + + public function readStreamEventsBackward( + string $stream, + int $start, + int $count, + bool $resolveLinkTos = true, + ?UserCredentials $userCredentials = null + ): StreamEventsSlice; + + public function readAllEventsForward( + Position $position, + int $count, + bool $resolveLinkTos = true, + ?UserCredentials $userCredentials = null + ): AllEventsSlice; + + public function readAllEventsBackward( + Position $position, + int $count, + bool $resolveLinkTos = true, + ?UserCredentials $userCredentials = null + ): AllEventsSlice; + + public function setStreamMetadata( + string $stream, + int $expectedMetaStreamVersion, + ?StreamMetadata $metadata = null, + ?UserCredentials $userCredentials = null + ): WriteResult; + + public function setRawStreamMetadata( + string $stream, + int $expectedMetaStreamVersion, + string $metadata = '', + ?UserCredentials $userCredentials = null + ): WriteResult; + + public function getStreamMetadata(string $stream, ?UserCredentials $userCredentials = null): StreamMetadataResult; + + public function getRawStreamMetadata(string $stream, ?UserCredentials $userCredentials = null): RawStreamMetadataResult; + + public function setSystemSettings(SystemSettings $settings, ?UserCredentials $userCredentials = null): WriteResult; + + public function startTransaction( + string $stream, + int $expectedVersion, + ?UserCredentials $userCredentials = null + ): EventStoreTransaction; + + public function continueTransaction( + int $transactionId, + ?UserCredentials $userCredentials = null + ): EventStoreTransaction; + + public function createPersistentSubscription( + string $stream, + string $groupName, + PersistentSubscriptionSettings $settings, + ?UserCredentials $userCredentials = null + ): PersistentSubscriptionCreateResult; + + public function updatePersistentSubscription( + string $stream, + string $groupName, + PersistentSubscriptionSettings $settings, + ?UserCredentials $userCredentials = null + ): PersistentSubscriptionUpdateResult; + + public function deletePersistentSubscription( + string $stream, + string $groupName, + ?UserCredentials $userCredentials = null + ): PersistentSubscriptionDeleteResult; + + public function subscribeToStreamAsync( + string $stream, + bool $resolveLinkTos, + EventAppearedOnSubscription $eventAppeared, + ?SubscriptionDropped $subscriptionDropped = null, + ?UserCredentials $userCredentials = null + ): EventStoreSubscription; + + public function subscribeToStreamFromAsync( + string $stream, + ?int $lastCheckpoint, + ?CatchUpSubscriptionSettings $settings, + EventAppearedOnCatchupSubscription $eventAppeared, + ?LiveProcessingStartedOnCatchUpSubscription $liveProcessingStarted = null, + ?CatchUpSubscriptionDropped $subscriptionDropped = null, + ?UserCredentials $userCredentials = null + ): EventStoreStreamCatchUpSubscription; + + public function subscribeToAllAsync( + bool $resolveLinkTos, + EventAppearedOnSubscription $eventAppeared, + ?SubscriptionDropped $subscriptionDropped = null, + ?UserCredentials $userCredentials = null + ): EventStoreSubscription; + + public function subscribeToAllFromAsync( + ?Position $lastCheckpoint, + ?CatchUpSubscriptionSettings $settings, + EventAppearedOnCatchupSubscription $eventAppeared, + ?LiveProcessingStartedOnCatchUpSubscription $liveProcessingStarted = null, + ?CatchUpSubscriptionDropped $subscriptionDropped = null, + ?UserCredentials $userCredentials = null + ): EventStoreAllCatchUpSubscription; + + public function connectToPersistentSubscription( + string $stream, + string $groupName, + EventAppearedOnPersistentSubscription $eventAppeared, + ?PersistentSubscriptionDropped $subscriptionDropped = null, + int $bufferSize = 10, + bool $autoAck = true, + ?UserCredentials $userCredentials = null + ): EventStorePersistentSubscription; +} diff --git a/src/EventStorePersistentSubscription.php b/src/EventStorePersistentSubscription.php new file mode 100644 index 00000000..0f962972 --- /dev/null +++ b/src/EventStorePersistentSubscription.php @@ -0,0 +1,87 @@ + + * (c) 2015-2018 Sascha-Oliver Prolic + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Prooph\EventStore; + +use Prooph\EventStore\Internal\ResolvedEvent; + +interface EventStorePersistentSubscription +{ + public const DEFAULT_BUFFER_SIZE = 10; + + public function start(): void; + + /** + * Acknowledge that a message have completed processing (this will tell the server it has been processed) + * Note: There is no need to ack a message if you have Auto Ack enabled + * + * @param ResolvedEvent $event + * + * @return void + */ + public function acknowledge(ResolvedEvent $event): void; + + /** + * Acknowledge that a message have completed processing (this will tell the server it has been processed) + * Note: There is no need to ack a message if you have Auto Ack enabled + * + * @param ResolvedEvent[] $events + * + * @return void + */ + public function acknowledgeMultiple(array $events): void; + + /** + * Acknowledge that a message have completed processing (this will tell the server it has been processed) + * Note: There is no need to ack a message if you have Auto Ack enabled + * + * @param EventId $eventId + * + * @return void + */ + public function acknowledgeEventId(EventId $eventId): void; + + /** + * Acknowledge that a message have completed processing (this will tell the server it has been processed) + * Note: There is no need to ack a message if you have Auto Ack enabled + * + * @param EventId[] $eventIds + * + * @return void + */ + public function acknowledgeMultipleEventIds(array $eventIds): void; + + /** + * Mark a message failed processing. The server will be take action based upon the action paramter + */ + public function fail( + ResolvedEvent $event, + PersistentSubscriptionNakEventAction $action, + string $reason + ): void; + + /** + * Mark n messages that have failed processing. The server will take action based upon the action parameter + * + * @param ResolvedEvent[] $events + * @param PersistentSubscriptionNakEventAction $action + * @param string $reason + */ + public function failMultiple( + array $events, + PersistentSubscriptionNakEventAction $action, + string $reason + ): void; + + public function stop(): void; +} diff --git a/src/EventStoreStreamCatchUpSubscription.php b/src/EventStoreStreamCatchUpSubscription.php new file mode 100644 index 00000000..a968c20c --- /dev/null +++ b/src/EventStoreStreamCatchUpSubscription.php @@ -0,0 +1,19 @@ + + * (c) 2015-2018 Sascha-Oliver Prolic + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Prooph\EventStore; + +interface EventStoreStreamCatchUpSubscription extends EventStoreCatchUpSubscription +{ + public function lastProcessedEventNumber(): int; +} diff --git a/src/EventStoreSubscription.php b/src/EventStoreSubscription.php new file mode 100644 index 00000000..d8d7480b --- /dev/null +++ b/src/EventStoreSubscription.php @@ -0,0 +1,66 @@ + + * (c) 2015-2018 Sascha-Oliver Prolic + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Prooph\EventStore; + +abstract class EventStoreSubscription +{ + /** @var bool */ + private $isSubscribedToAll; + /** @var string */ + private $streamId; + /** @var int */ + private $lastCommitPosition; + /** @var int|null */ + private $lastEventNumber; + + public function __construct(string $streamId, int $lastCommitPosition, ?int $lastEventNumber) + { + $this->isSubscribedToAll = $streamId === ''; + $this->streamId = $streamId; + $this->lastCommitPosition = $lastCommitPosition; + $this->lastEventNumber = $lastEventNumber; + } + + public function isSubscribedToAll(): bool + { + return $this->isSubscribedToAll; + } + + public function streamId(): string + { + return $this->streamId; + } + + public function lastCommitPosition(): int + { + return $this->lastCommitPosition; + } + + public function lastEventNumber(): ?int + { + return $this->lastEventNumber; + } + + public function __destruct() + { + $this->unsubscribe(); + } + + public function close(): void + { + $this->unsubscribe(); + } + + abstract public function unsubscribe(): void; +} diff --git a/src/EventStoreTransaction.php b/src/EventStoreTransaction.php new file mode 100644 index 00000000..2a7f8a8c --- /dev/null +++ b/src/EventStoreTransaction.php @@ -0,0 +1,85 @@ + + * (c) 2015-2018 Sascha-Oliver Prolic + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Prooph\EventStore; + +use Prooph\EventStore\Internal\EventStoreTransactionConnection; + +class EventStoreTransaction +{ + /** @var int */ + private $transactionId; + /** @var UserCredentials|null */ + private $userCredentials; + /** @var EventStoreTransactionConnection */ + private $connection; + /** @var bool */ + private $isRolledBack; + /** @var bool */ + private $isCommitted; + + public function __construct( + int $transactionId, + ?UserCredentials $userCredentials, + EventStoreTransactionConnection $connection + ) { + $this->transactionId = $transactionId; + $this->userCredentials = $userCredentials; + $this->connection = $connection; + } + + public function transactionId(): int + { + return $this->transactionId; + } + + public function commit(): WriteResult + { + if ($this->isRolledBack) { + throw new \RuntimeException('Cannot commit a rolledback transaction'); + } + + if ($this->isCommitted) { + throw new \RuntimeException('Transaction is already committed'); + } + + return $this->connection->commitTransaction($this, $this->userCredentials); + } + + /** + * @param EventData[] $events + * + * @return void + */ + public function writeAsync(array $events = []): void + { + if ($this->isRolledBack) { + throw new \RuntimeException('Cannot commit a rolledback transaction'); + } + + if ($this->isCommitted) { + throw new \RuntimeException('Transaction is already committed'); + } + + $this->connection->transactionalWrite($this, $events, $this->userCredentials); + } + + public function rollback(): void + { + if ($this->isCommitted) { + throw new \RuntimeException('Transaction is already committed'); + } + + $this->isRolledBack = true; + } +} diff --git a/src/Exception/AccessDeniedException.php b/src/Exception/AccessDeniedException.php new file mode 100644 index 00000000..dd6979f5 --- /dev/null +++ b/src/Exception/AccessDeniedException.php @@ -0,0 +1,33 @@ + + * (c) 2015-2018 Sascha-Oliver Prolic + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Prooph\EventStore\Exception; + +class AccessDeniedException extends RuntimeException +{ + public static function toAllStream(): AccessDeniedException + { + return new self(\sprintf( + 'Access to stream \'%s\' is denied', + '$all' + )); + } + + public static function toStream(string $stream): AccessDeniedException + { + return new self(\sprintf( + 'Access to stream \'%s\' is denied', + $stream + )); + } +} diff --git a/src/Exception/CannotEstablishConnectionException.php b/src/Exception/CannotEstablishConnectionException.php new file mode 100644 index 00000000..3c8f4dd5 --- /dev/null +++ b/src/Exception/CannotEstablishConnectionException.php @@ -0,0 +1,18 @@ + + * (c) 2015-2018 Sascha-Oliver Prolic + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Prooph\EventStore\Exception; + +class CannotEstablishConnectionException extends RuntimeException +{ +} diff --git a/src/Exception/ConnectionClosedException.php b/src/Exception/ConnectionClosedException.php new file mode 100644 index 00000000..afc46979 --- /dev/null +++ b/src/Exception/ConnectionClosedException.php @@ -0,0 +1,25 @@ + + * (c) 2015-2018 Sascha-Oliver Prolic + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Prooph\EventStore\Exception; + +class ConnectionClosedException extends EventStoreConnectionException +{ + public static function withName(string $name): ConnectionClosedException + { + return new self(\sprintf( + 'Connection \'%s\' was closed', + $name + )); + } +} diff --git a/src/Exception/EventStoreConnectionException.php b/src/Exception/EventStoreConnectionException.php new file mode 100644 index 00000000..1702a0ea --- /dev/null +++ b/src/Exception/EventStoreConnectionException.php @@ -0,0 +1,18 @@ + + * (c) 2015-2018 Sascha-Oliver Prolic + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Prooph\EventStore\Exception; + +class EventStoreConnectionException extends RuntimeException +{ +} diff --git a/tests/Mock/PostCreated.php b/src/Exception/InvalidOperationException.php similarity index 78% rename from tests/Mock/PostCreated.php rename to src/Exception/InvalidOperationException.php index 0592cc0c..5f99dafb 100644 --- a/tests/Mock/PostCreated.php +++ b/src/Exception/InvalidOperationException.php @@ -11,8 +11,8 @@ declare(strict_types=1); -namespace ProophTest\EventStore\Mock; +namespace Prooph\EventStore\Exception; -class PostCreated extends TestDomainEvent +class InvalidOperationException extends RuntimeException { } diff --git a/src/Exception/InvalidTransactionException.php b/src/Exception/InvalidTransactionException.php new file mode 100644 index 00000000..46722ec5 --- /dev/null +++ b/src/Exception/InvalidTransactionException.php @@ -0,0 +1,22 @@ + + * (c) 2015-2018 Sascha-Oliver Prolic + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Prooph\EventStore\Exception; + +/** + * Exception thrown if there is an attempt to operate inside a + * transaction which does not exist. + */ +class InvalidTransactionException extends RuntimeException +{ +} diff --git a/src/Exception/ConcurrencyException.php b/src/Exception/JsonException.php similarity index 88% rename from src/Exception/ConcurrencyException.php rename to src/Exception/JsonException.php index 44c3f220..f5de3bc7 100644 --- a/src/Exception/ConcurrencyException.php +++ b/src/Exception/JsonException.php @@ -13,6 +13,6 @@ namespace Prooph\EventStore\Exception; -class ConcurrencyException extends RuntimeException +class JsonException extends RuntimeException { } diff --git a/src/Exception/StreamExistsAlready.php b/src/Exception/MaxQueueSizeLimitReachedException.php similarity index 59% rename from src/Exception/StreamExistsAlready.php rename to src/Exception/MaxQueueSizeLimitReachedException.php index 9acb8968..fc79b0ad 100644 --- a/src/Exception/StreamExistsAlready.php +++ b/src/Exception/MaxQueueSizeLimitReachedException.php @@ -13,16 +13,15 @@ namespace Prooph\EventStore\Exception; -use Prooph\EventStore\StreamName; - -final class StreamExistsAlready extends RuntimeException +class MaxQueueSizeLimitReachedException extends RuntimeException { - public static function with(StreamName $streamName): StreamExistsAlready + public static function with(string $connectionName, int $maxQueueSize): MaxQueueSizeLimitReachedException { return new self( \sprintf( - 'A stream with name %s exists already', - $streamName->toString() + 'EventStoreNodeConnection \'%s\': reached max queue size limit: \'%s\'', + $connectionName, + $maxQueueSize ) ); } diff --git a/src/Exception/MaximumSubscribersReachedException.php b/src/Exception/MaximumSubscribersReachedException.php new file mode 100644 index 00000000..136042cc --- /dev/null +++ b/src/Exception/MaximumSubscribersReachedException.php @@ -0,0 +1,18 @@ + + * (c) 2015-2018 Sascha-Oliver Prolic + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Prooph\EventStore\Exception; + +class MaximumSubscribersReachedException extends RuntimeException +{ +} diff --git a/src/Exception/ProjectionNotFound.php b/src/Exception/NotAuthenticatedException.php similarity index 65% rename from src/Exception/ProjectionNotFound.php rename to src/Exception/NotAuthenticatedException.php index 4f542b4c..fcd18e7a 100644 --- a/src/Exception/ProjectionNotFound.php +++ b/src/Exception/NotAuthenticatedException.php @@ -13,10 +13,10 @@ namespace Prooph\EventStore\Exception; -class ProjectionNotFound extends RuntimeException +class NotAuthenticatedException extends RuntimeException { - public static function withName(string $name): ProjectionNotFound + public function __construct(string $message = 'Not authenticated') { - return new self('A projection with name "' . $name . '" could not be found.'); + parent::__construct($message); } } diff --git a/src/Exception/ObjectDisposedException.php b/src/Exception/ObjectDisposedException.php new file mode 100644 index 00000000..0aeea0f4 --- /dev/null +++ b/src/Exception/ObjectDisposedException.php @@ -0,0 +1,18 @@ + + * (c) 2015-2018 Sascha-Oliver Prolic + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Prooph\EventStore\Exception; + +class ObjectDisposedException extends InvalidOperationException +{ +} diff --git a/src/Exception/OperationTimedOutException.php b/src/Exception/OperationTimedOutException.php new file mode 100644 index 00000000..15bee9cc --- /dev/null +++ b/src/Exception/OperationTimedOutException.php @@ -0,0 +1,18 @@ + + * (c) 2015-2018 Sascha-Oliver Prolic + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Prooph\EventStore\Exception; + +class OperationTimedOutException extends RuntimeException +{ +} diff --git a/src/Exception/OutOfRangeException.php b/src/Exception/OutOfRangeException.php index cbcd26bb..6864baf2 100644 --- a/src/Exception/OutOfRangeException.php +++ b/src/Exception/OutOfRangeException.php @@ -13,6 +13,6 @@ namespace Prooph\EventStore\Exception; -class OutOfRangeException extends InvalidArgumentException implements EventStoreException +class OutOfRangeException extends \OutOfRangeException implements EventStoreException { } diff --git a/src/Exception/TransactionNotStarted.php b/src/Exception/PersistentSubscriptionCommandFailedException.php similarity index 75% rename from src/Exception/TransactionNotStarted.php rename to src/Exception/PersistentSubscriptionCommandFailedException.php index 800829da..91facc8b 100644 --- a/src/Exception/TransactionNotStarted.php +++ b/src/Exception/PersistentSubscriptionCommandFailedException.php @@ -13,7 +13,6 @@ namespace Prooph\EventStore\Exception; -class TransactionNotStarted extends RuntimeException +class PersistentSubscriptionCommandFailedException extends EventStoreConnectionException { - protected $message = 'The transaction has not yet been started.'; } diff --git a/src/Exception/PersistentSubscriptionDeletedException.php b/src/Exception/PersistentSubscriptionDeletedException.php new file mode 100644 index 00000000..bd52facb --- /dev/null +++ b/src/Exception/PersistentSubscriptionDeletedException.php @@ -0,0 +1,18 @@ + + * (c) 2015-2018 Sascha-Oliver Prolic + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Prooph\EventStore\Exception; + +class PersistentSubscriptionDeletedException extends RuntimeException +{ +} diff --git a/src/Exception/TransactionAlreadyStarted.php b/src/Exception/ProjectionCommandConflictException.php similarity index 75% rename from src/Exception/TransactionAlreadyStarted.php rename to src/Exception/ProjectionCommandConflictException.php index 2194aca1..fd40e1d6 100644 --- a/src/Exception/TransactionAlreadyStarted.php +++ b/src/Exception/ProjectionCommandConflictException.php @@ -13,7 +13,6 @@ namespace Prooph\EventStore\Exception; -class TransactionAlreadyStarted extends RuntimeException +class ProjectionCommandConflictException extends ProjectionCommandFailedException { - protected $message = 'The transaction has already been started.'; } diff --git a/src/Exception/ProjectionCommandFailedException.php b/src/Exception/ProjectionCommandFailedException.php new file mode 100644 index 00000000..3ad807ff --- /dev/null +++ b/src/Exception/ProjectionCommandFailedException.php @@ -0,0 +1,18 @@ + + * (c) 2015-2018 Sascha-Oliver Prolic + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Prooph\EventStore\Exception; + +class ProjectionCommandFailedException extends EventStoreConnectionException +{ +} diff --git a/src/Exception/StreamNotFound.php b/src/Exception/RetriesLimitReachedException.php similarity index 64% rename from src/Exception/StreamNotFound.php rename to src/Exception/RetriesLimitReachedException.php index dfb638ff..e0b6fdaf 100644 --- a/src/Exception/StreamNotFound.php +++ b/src/Exception/RetriesLimitReachedException.php @@ -13,16 +13,14 @@ namespace Prooph\EventStore\Exception; -use Prooph\EventStore\StreamName; - -final class StreamNotFound extends RuntimeException +class RetriesLimitReachedException extends RuntimeException { - public static function with(StreamName $streamName): StreamNotFound + public static function with(int $retries): RetriesLimitReachedException { return new self( \sprintf( - 'A stream with name %s could not be found', - $streamName->toString() + 'Operation reached retries limit: \'%s\'', + $retries ) ); } diff --git a/src/Exception/ConfigurationException.php b/src/Exception/ServerError.php similarity index 63% rename from src/Exception/ConfigurationException.php rename to src/Exception/ServerError.php index 728ff12e..e4fc901c 100644 --- a/src/Exception/ConfigurationException.php +++ b/src/Exception/ServerError.php @@ -13,10 +13,14 @@ namespace Prooph\EventStore\Exception; -class ConfigurationException extends RuntimeException implements EventStoreException +class ServerError extends RuntimeException { - public static function configurationError(string $msg): ConfigurationException + public function __construct(string $message = '') { - return new self('[Configuration Error] ' . $msg . "\n"); + if ('' !== $message) { + $message = ': ' . $message; + } + + parent::__construct('Server error' . $message); } } diff --git a/src/Exception/ExtensionNotLoadedException.php b/src/Exception/StreamDeletedException.php similarity index 63% rename from src/Exception/ExtensionNotLoadedException.php rename to src/Exception/StreamDeletedException.php index 8c6d2186..10b8b395 100644 --- a/src/Exception/ExtensionNotLoadedException.php +++ b/src/Exception/StreamDeletedException.php @@ -13,10 +13,13 @@ namespace Prooph\EventStore\Exception; -class ExtensionNotLoadedException extends RuntimeException +class StreamDeletedException extends RuntimeException { - public static function withName(string $name): ExtensionNotLoadedException + public static function with(string $stream): StreamDeletedException { - return new self('The extension "' . $name . '" is not loaded.'); + return new self(\sprintf( + 'Stream \'%s\' is deleted', + $stream + )); } } diff --git a/tests/Mock/UserCreated.php b/src/Exception/TimeoutException.php similarity index 79% rename from tests/Mock/UserCreated.php rename to src/Exception/TimeoutException.php index 2394964f..9f110d9b 100644 --- a/tests/Mock/UserCreated.php +++ b/src/Exception/TimeoutException.php @@ -11,8 +11,8 @@ declare(strict_types=1); -namespace ProophTest\EventStore\Mock; +namespace Prooph\EventStore\Exception; -class UserCreated extends TestDomainEvent +class TimeoutException extends RuntimeException { } diff --git a/src/Exception/UnexpectedCommandException.php b/src/Exception/UnexpectedCommandException.php new file mode 100644 index 00000000..9f46186b --- /dev/null +++ b/src/Exception/UnexpectedCommandException.php @@ -0,0 +1,34 @@ + + * (c) 2015-2018 Sascha-Oliver Prolic + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Prooph\EventStore\Exception; + +class UnexpectedCommandException extends RuntimeException +{ + public static function withName(string $actualCommand): UnexpectedCommandException + { + return new self(\sprintf( + 'Unexpected command \'%s\'', + $actualCommand + )); + } + + public static function with(string $expectedCommand, string $actualCommand): UnexpectedCommandException + { + return new self(\sprintf( + 'Unexpected command \'%s\': expected \'%s\'', + $actualCommand, + $expectedCommand + )); + } +} diff --git a/src/Exception/UnexpectedOperationResult.php b/src/Exception/UnexpectedOperationResult.php new file mode 100644 index 00000000..2dc13b5f --- /dev/null +++ b/src/Exception/UnexpectedOperationResult.php @@ -0,0 +1,22 @@ + + * (c) 2015-2018 Sascha-Oliver Prolic + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Prooph\EventStore\Exception; + +class UnexpectedOperationResult extends RuntimeException +{ + public function __construct(string $message = 'Unexpected operation result') + { + parent::__construct($message); + } +} diff --git a/src/Exception/UnexpectedValueException.php b/src/Exception/UnexpectedValueException.php new file mode 100644 index 00000000..783f5b72 --- /dev/null +++ b/src/Exception/UnexpectedValueException.php @@ -0,0 +1,18 @@ + + * (c) 2015-2018 Sascha-Oliver Prolic + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Prooph\EventStore\Exception; + +class UnexpectedValueException extends \UnexpectedValueException implements EventStoreException +{ +} diff --git a/src/Exception/UserCommandConflictException.php b/src/Exception/UserCommandConflictException.php new file mode 100644 index 00000000..53514a7e --- /dev/null +++ b/src/Exception/UserCommandConflictException.php @@ -0,0 +1,18 @@ + + * (c) 2015-2018 Sascha-Oliver Prolic + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Prooph\EventStore\Exception; + +class UserCommandConflictException extends ProjectionCommandFailedException +{ +} diff --git a/src/Exception/UserCommandFailedException.php b/src/Exception/UserCommandFailedException.php new file mode 100644 index 00000000..76171d1e --- /dev/null +++ b/src/Exception/UserCommandFailedException.php @@ -0,0 +1,18 @@ + + * (c) 2015-2018 Sascha-Oliver Prolic + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Prooph\EventStore\Exception; + +class UserCommandFailedException extends EventStoreConnectionException +{ +} diff --git a/src/Exception/WrongExpectedVersionException.php b/src/Exception/WrongExpectedVersionException.php new file mode 100644 index 00000000..7c42f8de --- /dev/null +++ b/src/Exception/WrongExpectedVersionException.php @@ -0,0 +1,36 @@ + + * (c) 2015-2018 Sascha-Oliver Prolic + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Prooph\EventStore\Exception; + +class WrongExpectedVersionException extends RuntimeException +{ + public static function with( + string $stream, + int $expectedVersion, + ?int $currentVersion = null + ): WrongExpectedVersionException { + $message = 'Operation failed due to WrongExpectedVersion. Stream: \'%s\', Expected version: %d'; + + if (null !== $currentVersion) { + $message = ', Current version: %d'; + } + + return new self(\sprintf( + $message, + $stream, + $expectedVersion, + $currentVersion + )); + } +} diff --git a/src/ExpectedVersion.php b/src/ExpectedVersion.php new file mode 100644 index 00000000..5a039135 --- /dev/null +++ b/src/ExpectedVersion.php @@ -0,0 +1,26 @@ + + * (c) 2015-2018 Sascha-Oliver Prolic + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Prooph\EventStore; + +class ExpectedVersion +{ + // This write should not conflict with anything and should always succeed. + public const ANY = -2; + // The stream being written to should not yet exist. If it does exist treat that as a concurrency problem. + public const NO_STREAM = -1; + // The stream should exist and should be empty. If it does not exist or is not empty treat that as a concurrency problem. + public const EMPTY_STREAM = -1; + // The stream should exist. If it or a metadata stream does not exist treat that as a concurrency problem. + public const STREAM_EXISTS = -4; +} diff --git a/src/InMemoryEventStore.php b/src/InMemoryEventStore.php deleted file mode 100644 index 60bb89df..00000000 --- a/src/InMemoryEventStore.php +++ /dev/null @@ -1,559 +0,0 @@ - - * (c) 2015-2018 Sascha-Oliver Prolic - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace Prooph\EventStore; - -use Iterator; -use Prooph\Common\Messaging\Message; -use Prooph\EventStore\Exception\StreamExistsAlready; -use Prooph\EventStore\Exception\StreamNotFound; -use Prooph\EventStore\Exception\TransactionAlreadyStarted; -use Prooph\EventStore\Exception\TransactionNotStarted; -use Prooph\EventStore\Metadata\FieldType; -use Prooph\EventStore\Metadata\MetadataMatcher; -use Prooph\EventStore\Metadata\Operator; -use Prooph\EventStore\StreamIterator\EmptyStreamIterator; -use Prooph\EventStore\StreamIterator\InMemoryStreamIterator; -use Prooph\EventStore\Util\Assertion; - -final class InMemoryEventStore implements TransactionalEventStore -{ - /** - * @var array - */ - private $streams = []; - - /** - * @var array - */ - private $cachedStreams = []; - - /** - * @var bool - */ - private $inTransaction = false; - - public function create(Stream $stream): void - { - $streamName = $stream->streamName(); - $streamNameString = $streamName->toString(); - - if (isset($this->streams[$streamNameString]) - || isset($this->cachedStreams[$streamNameString]) - ) { - throw StreamExistsAlready::with($streamName); - } - - if ($this->inTransaction) { - $this->cachedStreams[$streamNameString]['events'] = \iterator_to_array($stream->streamEvents()); - $this->cachedStreams[$streamNameString]['metadata'] = $stream->metadata(); - } else { - $this->streams[$streamNameString]['events'] = \iterator_to_array($stream->streamEvents()); - $this->streams[$streamNameString]['metadata'] = $stream->metadata(); - } - } - - public function appendTo(StreamName $streamName, Iterator $streamEvents): void - { - $streamNameString = $streamName->toString(); - - if (! isset($this->streams[$streamNameString]) - && ! isset($this->cachedStreams[$streamNameString]) - ) { - throw StreamNotFound::with($streamName); - } - - if ($this->inTransaction) { - if (! isset($this->cachedStreams[$streamNameString])) { - $this->cachedStreams[$streamNameString]['events'] = []; - } - - foreach ($streamEvents as $streamEvent) { - $this->cachedStreams[$streamNameString]['events'][] = $streamEvent; - } - } else { - foreach ($streamEvents as $streamEvent) { - $this->streams[$streamNameString]['events'][] = $streamEvent; - } - } - } - - public function load( - StreamName $streamName, - int $fromNumber = 1, - int $count = null, - MetadataMatcher $metadataMatcher = null - ): Iterator { - Assertion::greaterOrEqualThan($fromNumber, 1); - Assertion::nullOrGreaterOrEqualThan($count, 1); - - if (! isset($this->streams[$streamName->toString()])) { - throw StreamNotFound::with($streamName); - } - - if (null === $metadataMatcher) { - $metadataMatcher = new MetadataMatcher(); - } - - $found = 0; - $streamEvents = []; - - foreach ($this->streams[$streamName->toString()]['events'] as $key => $streamEvent) { - /* @var Message $streamEvent */ - if (($key + 1) >= $fromNumber - && $this->matchesMetadata($metadataMatcher, $streamEvent->metadata()) - && $this->matchesMessagesProperty($metadataMatcher, $streamEvent) - ) { - ++$found; - $streamEvents[] = $streamEvent; - - if ($found === $count) { - break; - } - } - } - - if (0 === $found) { - return new EmptyStreamIterator(); - } - - return new InMemoryStreamIterator($streamEvents); - } - - public function loadReverse( - StreamName $streamName, - int $fromNumber = null, - int $count = null, - MetadataMatcher $metadataMatcher = null - ): Iterator { - if (null === $fromNumber) { - $fromNumber = PHP_INT_MAX; - } - - Assertion::greaterOrEqualThan($fromNumber, 1); - Assertion::nullOrGreaterOrEqualThan($count, 1); - - if (! isset($this->streams[$streamName->toString()])) { - throw StreamNotFound::with($streamName); - } - - if (null === $metadataMatcher) { - $metadataMatcher = new MetadataMatcher(); - } - - $found = 0; - $streamEvents = []; - - foreach (\array_reverse($this->streams[$streamName->toString()]['events'], true) as $key => $streamEvent) { - /* @var Message $streamEvent */ - if (($key + 1) <= $fromNumber - && $this->matchesMetadata($metadataMatcher, $streamEvent->metadata()) - && $this->matchesMessagesProperty($metadataMatcher, $streamEvent) - ) { - $streamEvents[] = $streamEvent; - ++$found; - - if ($found === $count) { - break; - } - } - } - - if (0 === $found) { - return new EmptyStreamIterator(); - } - - return new InMemoryStreamIterator($streamEvents); - } - - public function delete(StreamName $streamName): void - { - $streamNameString = $streamName->toString(); - - if (isset($this->streams[$streamNameString])) { - unset($this->streams[$streamNameString]); - } else { - throw StreamNotFound::with($streamName); - } - } - - public function hasStream(StreamName $streamName): bool - { - return isset($this->streams[$streamName->toString()]); - } - - public function fetchStreamMetadata(StreamName $streamName): array - { - if (! isset($this->streams[$streamName->toString()])) { - throw StreamNotFound::with($streamName); - } - - return $this->streams[$streamName->toString()]['metadata']; - } - - public function updateStreamMetadata(StreamName $streamName, array $newMetadata): void - { - if (! isset($this->streams[$streamName->toString()])) { - throw StreamNotFound::with($streamName); - } - - $this->streams[$streamName->toString()]['metadata'] = $newMetadata; - } - - public function beginTransaction(): void - { - if ($this->inTransaction) { - throw new TransactionAlreadyStarted(); - } - - $this->inTransaction = true; - } - - public function commit(): void - { - if (! $this->inTransaction) { - throw new TransactionNotStarted(); - } - - foreach ($this->cachedStreams as $streamName => $data) { - if (isset($data['metadata'])) { - $this->streams[$streamName] = $data; - } else { - foreach ($data['events'] as $streamEvent) { - $this->streams[$streamName]['events'][] = $streamEvent; - } - } - } - - $this->cachedStreams = []; - $this->inTransaction = false; - } - - public function rollback(): void - { - if (! $this->inTransaction) { - throw new TransactionNotStarted(); - } - - $this->cachedStreams = []; - $this->inTransaction = false; - } - - public function inTransaction(): bool - { - return $this->inTransaction; - } - - /** - * @throws \Exception - * - * @return mixed - */ - public function transactional(callable $callable) - { - $this->beginTransaction(); - - try { - $result = $callable($this); - $this->commit(); - } catch (\Throwable $e) { - $this->rollback(); - throw $e; - } - - return $result ?: true; - } - - public function fetchStreamNames( - ?string $filter, - ?MetadataMatcher $metadataMatcher, - int $limit = 20, - int $offset = 0 - ): array { - $result = []; - - $skipped = 0; - $found = 0; - - $streams = $this->streams; - - if ($filter - && \array_key_exists($filter, $streams) - && ( - ! $metadataMatcher - || $metadataMatcher && $this->matchesMetadata($metadataMatcher, $streams[$filter]['metadata']) - ) - ) { - return [$filter]; - } - - \ksort($streams); - - foreach ($streams as $streamName => $data) { - if (null === $filter || $filter === $streamName) { - if ($offset > $skipped) { - ++$skipped; - continue; - } - - if ($metadataMatcher && ! $this->matchesMetadata($metadataMatcher, $data['metadata'])) { - continue; - } - - $result[] = new StreamName($streamName); - ++$found; - } - - if ($found === $limit) { - break; - } - } - - return $result; - } - - public function fetchStreamNamesRegex( - string $filter, - ?MetadataMatcher $metadataMatcher, - int $limit = 20, - int $offset = 0 - ): array { - if (false === @\preg_match("/$filter/", '')) { - throw new Exception\InvalidArgumentException('Invalid regex pattern given'); - } - - $result = []; - - $skipped = 0; - $found = 0; - - $streams = $this->streams; - \ksort($streams); - - foreach ($streams as $streamName => $data) { - if (! \preg_match("/$filter/", $streamName)) { - continue; - } - - if ($metadataMatcher && ! $this->matchesMetadata($metadataMatcher, $data['metadata'])) { - continue; - } - - $result[] = new StreamName($streamName); - ++$found; - - if ($found === $limit) { - break; - } - } - - return $result; - } - - public function fetchCategoryNames(?string $filter, int $limit = 20, int $offset = 0): array - { - $result = []; - - $skipped = 0; - $found = 0; - - $categories = \array_unique(\array_reduce( - \array_keys($this->streams), - function (array $result, string $streamName): array { - if (\preg_match('/^(.+)-.+$/', $streamName, $matches)) { - $result[] = $matches[1]; - } - - return $result; - }, - [] - )); - - if ($filter && \in_array($filter, $categories, true)) { - return [$filter]; - } - - \ksort($categories); - - foreach ($categories as $category) { - if (null === $filter || $filter === $category) { - if ($offset > $skipped) { - ++$skipped; - continue; - } - - $result[] = $category; - ++$found; - } - - if ($found === $limit) { - break; - } - } - - return $result; - } - - public function fetchCategoryNamesRegex(string $filter, int $limit = 20, int $offset = 0): array - { - if (false === @\preg_match("/$filter/", '')) { - throw new Exception\InvalidArgumentException('Invalid regex pattern given'); - } - - $result = []; - - $skipped = 0; - $found = 0; - - $categories = \array_unique(\array_reduce( - \array_keys($this->streams), - function (array $result, string $streamName): array { - if (\preg_match('/^(.+)-.+$/', $streamName, $matches)) { - $result[] = $matches[1]; - } - - return $result; - }, - [] - )); - - \ksort($categories); - - foreach ($categories as $category) { - if (! \preg_match("/$filter/", $category)) { - continue; - } - - if ($offset > $skipped) { - ++$skipped; - continue; - } - - $result[] = $category; - ++$found; - - if ($found === $limit) { - break; - } - } - - return $result; - } - - private function matchesMetadata(MetadataMatcher $metadataMatcher, array $metadata): bool - { - foreach ($metadataMatcher->data() as $match) { - if (! FieldType::METADATA()->is($match['fieldType'])) { - continue; - } - - $field = $match['field']; - - if (! isset($metadata[$field])) { - return false; - } - - if (! $this->match($match['operator'], $metadata[$field], $match['value'])) { - return false; - } - } - - return true; - } - - private function matchesMessagesProperty(MetadataMatcher $metadataMatcher, Message $message): bool - { - foreach ($metadataMatcher->data() as $match) { - if (! FieldType::MESSAGE_PROPERTY()->is($match['fieldType'])) { - continue; - } - - switch ($match['field']) { - case 'uuid': - $value = $message->uuid()->toString(); - break; - case 'message_name': - case 'messageName': - $value = $message->messageName(); - break; - case 'created_at': - case 'createdAt': - $value = $message->createdAt()->format('Y-m-d\TH:i:s.u'); - break; - default: - throw new \UnexpectedValueException(\sprintf('Unexpected field "%s" given', $match['field'])); - } - - if (! $this->match($match['operator'], $value, $match['value'])) { - return false; - } - } - - return true; - } - - private function match(Operator $operator, $value, $expected): bool - { - switch ($operator) { - case Operator::EQUALS(): - if ($value !== $expected) { - return false; - } - break; - case Operator::GREATER_THAN(): - if (! ($value > $expected)) { - return false; - } - break; - case Operator::GREATER_THAN_EQUALS(): - if (! ($value >= $expected)) { - return false; - } - break; - case Operator::IN(): - if (! \in_array($value, $expected, true)) { - return false; - } - break; - case Operator::LOWER_THAN(): - if (! ($value < $expected)) { - return false; - } - break; - case Operator::LOWER_THAN_EQUALS(): - if (! ($value <= $expected)) { - return false; - } - break; - case Operator::NOT_EQUALS(): - if ($value === $expected) { - return false; - } - break; - case Operator::NOT_IN(): - if (\in_array($value, $expected, true)) { - return false; - } - break; - case Operator::REGEX(): - if (! \preg_match('/' . $expected . '/', $value)) { - return false; - } - break; - default: - throw new \UnexpectedValueException('Unknown operator found'); - } - - return true; - } -} diff --git a/src/Internal/AsyncEventStoreTransactionConnection.php b/src/Internal/AsyncEventStoreTransactionConnection.php new file mode 100644 index 00000000..b8259ca5 --- /dev/null +++ b/src/Internal/AsyncEventStoreTransactionConnection.php @@ -0,0 +1,34 @@ + + * (c) 2015-2018 Sascha-Oliver Prolic + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Prooph\EventStore\Internal; + +use Amp\Promise; +use Prooph\EventStore\AsyncEventStoreTransaction; +use Prooph\EventStore\UserCredentials; + +/** @internal */ +interface AsyncEventStoreTransactionConnection +{ + public function transactionalWriteAsync( + AsyncEventStoreTransaction $transaction, + array $events, + ?UserCredentials $userCredentials + ): Promise; + + /** @return Promise */ + public function commitTransactionAsync( + AsyncEventStoreTransaction $transaction, + ?UserCredentials $userCredentials + ): Promise; +} diff --git a/src/Internal/ConnectToPersistentSubscriptions.php b/src/Internal/ConnectToPersistentSubscriptions.php new file mode 100644 index 00000000..90e19f94 --- /dev/null +++ b/src/Internal/ConnectToPersistentSubscriptions.php @@ -0,0 +1,37 @@ + + * (c) 2015-2018 Sascha-Oliver Prolic + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Prooph\EventStore\Internal; + +use Prooph\EventStore\EventId; +use Prooph\EventStore\PersistentSubscriptionNakEventAction; + +/** @internal */ +interface ConnectToPersistentSubscriptions +{ + /** @param EventId[] $eventIds */ + public function notifyEventsProcessed(array $eventIds): void; + + /** + * @param EventId[] $eventIds + * @param PersistentSubscriptionNakEventAction $action + * @param string $reason + */ + public function notifyEventsFailed( + array $eventIds, + PersistentSubscriptionNakEventAction $action, + string $reason + ): void; + + public function unsubscribe(): void; +} diff --git a/src/Internal/Consts.php b/src/Internal/Consts.php new file mode 100644 index 00000000..a12e181d --- /dev/null +++ b/src/Internal/Consts.php @@ -0,0 +1,37 @@ + + * (c) 2015-2018 Sascha-Oliver Prolic + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Prooph\EventStore\Internal; + +/** @internal */ +class Consts +{ + public const DEFAULT_MAX_QUEUE_SIZE = 5000; + public const DEFAULT_MAX_CONCURRENT_ITEMS = 5000; + public const DEFAULT_MAX_OPERATIONS_RETRY = 10; + public const DEFAULT_MAX_RECONNECTIONS = 10; + + public const DEFAULT_REQUIRE_MASTER = true; + + public const DEFAULT_RECONNECTION_DELAY = 100; // milliseconds + public const DEFAULT_OPERATION_TIMEOUT = 7000; // milliseconds + public const DEFAULT_OPERATION_TIMEOUT_CHECK_PERIOD = 1000; // milliseconds + + public const TIMER_PERIOD = 200; // milliseconds + public const MAX_READ_SIZE = 4096; + public const DEFAULT_MAX_CLUSTER_DISCOVER_ATTEMPTS = 10; + public const DEFAULT_CLUSTER_MANAGER_EXTERNAL_HTTP_PORT = 30778; + + public const CATCH_UP_DEFAULT_READ_BATCH_SIZE = 500; + public const CATCH_UP_DEFAULT_MAX_PUSH_QUEUE_SIZE = 10000; +} diff --git a/src/Internal/EventHandler.php b/src/Internal/EventHandler.php new file mode 100644 index 00000000..8046c5b2 --- /dev/null +++ b/src/Internal/EventHandler.php @@ -0,0 +1,134 @@ + + * (c) 2015-2018 Sascha-Oliver Prolic + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Prooph\EventStore\Internal; + +use Prooph\EventStore\ClientAuthenticationFailedEventArgs; +use Prooph\EventStore\ClientClosedEventArgs; +use Prooph\EventStore\ClientConnectionEventArgs; +use Prooph\EventStore\ClientErrorEventArgs; +use Prooph\EventStore\ClientReconnectingEventArgs; +use Prooph\EventStore\ListenerHandler; +use SplObjectStorage; + +class EventHandler +{ + /** @var SplObjectStorage[] */ + private $handlers; + + public function __construct() + { + $this->handlers = [ + 'connected' => new SplObjectStorage(), + 'disconnected' => new SplObjectStorage(), + 'reconnecting' => new SplObjectStorage(), + 'closed' => new SplObjectStorage(), + 'errorOccurred' => new SplObjectStorage(), + 'authenticationFailed' => new SplObjectStorage(), + ]; + } + + public function connected(ClientConnectionEventArgs $args): void + { + foreach ($this->handlers['connected'] as $handler) { + \assert($handler instanceof ListenerHandler); + $handler->callback()($args); + } + } + + public function disconnected(ClientConnectionEventArgs $args): void + { + foreach ($this->handlers['disconnected'] as $handler) { + \assert($handler instanceof ListenerHandler); + $handler->callback()($args); + } + } + + public function reconnecting(ClientReconnectingEventArgs $args): void + { + foreach ($this->handlers['reconnecting'] as $handler) { + \assert($handler instanceof ListenerHandler); + $handler->callback()($args); + } + } + + public function closed(ClientClosedEventArgs $args): void + { + foreach ($this->handlers['closed'] as $handler) { + \assert($handler instanceof ListenerHandler); + $handler->callback()($args); + } + } + + public function errorOccurred(ClientErrorEventArgs $args): void + { + foreach ($this->handlers['errorOccurred'] as $handler) { + \assert($handler instanceof ListenerHandler); + $handler->callback()($args); + } + } + + public function authenticationFailed(ClientAuthenticationFailedEventArgs $args): void + { + foreach ($this->handlers['authenticationFailed'] as $handler) { + \assert($handler instanceof ListenerHandler); + $handler->callback()($args); + } + } + + public function whenConnected(callable $handler): ListenerHandler + { + return $this->attach($handler, 'connected'); + } + + public function whenDisconnected(callable $handler): ListenerHandler + { + return $this->attach($handler, 'disconnected'); + } + + public function whenReconnecting(callable $handler): ListenerHandler + { + return $this->attach($handler, 'reconnecting'); + } + + public function whenClosed(callable $handler): ListenerHandler + { + return $this->attach($handler, 'closed'); + } + + public function whenErrorOccurred(callable $handler): ListenerHandler + { + return $this->attach($handler, 'errorOccurred'); + } + + public function whenAuthenticationFailed(callable $handler): ListenerHandler + { + return $this->attach($handler, 'authenticationFailed'); + } + + public function detach(ListenerHandler $handler): void + { + foreach ($this->handlers as $storage) { + $storage->detach($handler); + } + } + + private function attach(callable $handler, string $eventName): ListenerHandler + { + $handler = new ListenerHandler($handler); + + $this->handlers[$eventName]->attach($handler); + + return $handler; + } +} diff --git a/src/Internal/EventStoreTransactionConnection.php b/src/Internal/EventStoreTransactionConnection.php new file mode 100644 index 00000000..74a542db --- /dev/null +++ b/src/Internal/EventStoreTransactionConnection.php @@ -0,0 +1,33 @@ + + * (c) 2015-2018 Sascha-Oliver Prolic + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Prooph\EventStore\Internal; + +use Prooph\EventStore\EventStoreTransaction; +use Prooph\EventStore\UserCredentials; +use Prooph\EventStore\WriteResult; + +/** @internal */ +interface EventStoreTransactionConnection +{ + public function transactionalWrite( + EventStoreTransaction $transaction, + array $events, + ?UserCredentials $userCredentials + ): void; + + public function commitTransaction( + EventStoreTransaction $transaction, + ?UserCredentials $userCredentials + ): WriteResult; +} diff --git a/src/Internal/PersistentEventStoreSubscription.php b/src/Internal/PersistentEventStoreSubscription.php new file mode 100644 index 00000000..9ef31385 --- /dev/null +++ b/src/Internal/PersistentEventStoreSubscription.php @@ -0,0 +1,64 @@ + + * (c) 2015-2018 Sascha-Oliver Prolic + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Prooph\EventStore\Internal; + +use Prooph\EventStore\EventId; +use Prooph\EventStore\EventStoreSubscription; +use Prooph\EventStore\PersistentSubscriptionNakEventAction; + +/** @internal */ +class PersistentEventStoreSubscription extends EventStoreSubscription +{ + /** @var ConnectToPersistentSubscriptions */ + private $subscriptionOperation; + + public function __construct( + ConnectToPersistentSubscriptions $subscriptionOperation, + string $streamId, + int $lastCommitPosition, + ?int $lastEventNumber + ) { + parent::__construct( + $streamId, + $lastCommitPosition, + $lastEventNumber + ); + + $this->subscriptionOperation = $subscriptionOperation; + } + + public function unsubscribe(): void + { + $this->subscriptionOperation->unsubscribe(); + } + + /** @param EventId[] $eventIds */ + public function notifyEventsProcessed(array $eventIds): void + { + $this->subscriptionOperation->notifyEventsProcessed($eventIds); + } + + /** + * @param EventId[] $eventIds + * @param PersistentSubscriptionNakEventAction $action + * @param string $reason + */ + public function notifyEventsFailed( + array $eventIds, + PersistentSubscriptionNakEventAction $action, + string $reason + ): void { + $this->subscriptionOperation->notifyEventsFailed($eventIds, $action, $reason); + } +} diff --git a/src/Internal/PersistentSubscriptionCreateResult.php b/src/Internal/PersistentSubscriptionCreateResult.php new file mode 100644 index 00000000..4b35c069 --- /dev/null +++ b/src/Internal/PersistentSubscriptionCreateResult.php @@ -0,0 +1,31 @@ + + * (c) 2015-2018 Sascha-Oliver Prolic + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Prooph\EventStore\Internal; + +class PersistentSubscriptionCreateResult +{ + /** @var PersistentSubscriptionCreateStatus */ + private $status; + + /** @internal */ + public function __construct(PersistentSubscriptionCreateStatus $status) + { + $this->status = $status; + } + + public function status(): PersistentSubscriptionCreateStatus + { + return $this->status; + } +} diff --git a/src/Internal/PersistentSubscriptionCreateStatus.php b/src/Internal/PersistentSubscriptionCreateStatus.php new file mode 100644 index 00000000..008e514f --- /dev/null +++ b/src/Internal/PersistentSubscriptionCreateStatus.php @@ -0,0 +1,94 @@ + + * (c) 2015-2018 Sascha-Oliver Prolic + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Prooph\EventStore\Internal; + +use Prooph\EventStore\Exception\InvalidArgumentException; + +/** @internal */ +class PersistentSubscriptionCreateStatus +{ + public const OPTIONS = [ + 'Success' => 0, + 'AlreadyExists' => 1, + 'Failure' => 2, + ]; + + public const SUCCESS = 0; + public const ALREADY_EXISTS = 1; + public const FAILURE = 2; + + private $name; + private $value; + + private function __construct(string $name) + { + $this->name = $name; + $this->value = self::OPTIONS[$name]; + } + + public static function success(): self + { + return new self('Success'); + } + + public static function alreadyExists(): self + { + return new self('AlreadyExists'); + } + + public static function failure(): self + { + return new self('Failure'); + } + + public static function byName(string $value): self + { + if (! isset(self::OPTIONS[$value])) { + throw new InvalidArgumentException('Unknown enum name given'); + } + + return self::{$value}(); + } + + public static function byValue($value): self + { + foreach (self::OPTIONS as $name => $v) { + if ($v === $value) { + return self::{$name}(); + } + } + + throw new InvalidArgumentException('Unknown enum value given'); + } + + public function equals(PersistentSubscriptionCreateStatus $other): bool + { + return \get_class($this) === \get_class($other) && $this->name === $other->name; + } + + public function name(): string + { + return $this->name; + } + + public function value() + { + return $this->value; + } + + public function __toString(): string + { + return $this->name; + } +} diff --git a/src/Internal/PersistentSubscriptionDeleteResult.php b/src/Internal/PersistentSubscriptionDeleteResult.php new file mode 100644 index 00000000..5bc1b911 --- /dev/null +++ b/src/Internal/PersistentSubscriptionDeleteResult.php @@ -0,0 +1,31 @@ + + * (c) 2015-2018 Sascha-Oliver Prolic + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Prooph\EventStore\Internal; + +class PersistentSubscriptionDeleteResult +{ + /** @var PersistentSubscriptionDeleteStatus */ + private $status; + + /** @internal */ + public function __construct(PersistentSubscriptionDeleteStatus $status) + { + $this->status = $status; + } + + public function status(): PersistentSubscriptionDeleteStatus + { + return $this->status; + } +} diff --git a/src/Internal/PersistentSubscriptionDeleteStatus.php b/src/Internal/PersistentSubscriptionDeleteStatus.php new file mode 100644 index 00000000..7b8a55c9 --- /dev/null +++ b/src/Internal/PersistentSubscriptionDeleteStatus.php @@ -0,0 +1,87 @@ + + * (c) 2015-2018 Sascha-Oliver Prolic + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Prooph\EventStore\Internal; + +use Prooph\EventStore\Exception\InvalidArgumentException; + +/** @internal */ +class PersistentSubscriptionDeleteStatus +{ + public const OPTIONS = [ + 'Success' => 0, + 'Failure' => 1, + ]; + + public const SUCCESS = 0; + public const FAILURE = 1; + + private $name; + private $value; + + private function __construct(string $name) + { + $this->name = $name; + $this->value = self::OPTIONS[$name]; + } + + public static function success(): self + { + return new self('Success'); + } + + public static function failure(): self + { + return new self('Failure'); + } + + public static function byName(string $value): self + { + if (! isset(self::OPTIONS[$value])) { + throw new InvalidArgumentException('Unknown enum name given'); + } + + return self::{$value}(); + } + + public static function byValue($value): self + { + foreach (self::OPTIONS as $name => $v) { + if ($v === $value) { + return self::{$name}(); + } + } + + throw new InvalidArgumentException('Unknown enum value given'); + } + + public function equals(PersistentSubscriptionDeleteStatus $other): bool + { + return \get_class($this) === \get_class($other) && $this->name === $other->name; + } + + public function name(): string + { + return $this->name; + } + + public function value() + { + return $this->value; + } + + public function __toString(): string + { + return $this->name; + } +} diff --git a/src/Internal/PersistentSubscriptionUpdateResult.php b/src/Internal/PersistentSubscriptionUpdateResult.php new file mode 100644 index 00000000..50789eaa --- /dev/null +++ b/src/Internal/PersistentSubscriptionUpdateResult.php @@ -0,0 +1,31 @@ + + * (c) 2015-2018 Sascha-Oliver Prolic + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Prooph\EventStore\Internal; + +class PersistentSubscriptionUpdateResult +{ + /** @var PersistentSubscriptionUpdateStatus */ + private $status; + + /** @internal */ + public function __construct(PersistentSubscriptionUpdateStatus $status) + { + $this->status = $status; + } + + public function status(): PersistentSubscriptionUpdateStatus + { + return $this->status; + } +} diff --git a/src/Internal/PersistentSubscriptionUpdateStatus.php b/src/Internal/PersistentSubscriptionUpdateStatus.php new file mode 100644 index 00000000..8ddbc5f2 --- /dev/null +++ b/src/Internal/PersistentSubscriptionUpdateStatus.php @@ -0,0 +1,94 @@ + + * (c) 2015-2018 Sascha-Oliver Prolic + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Prooph\EventStore\Internal; + +use Prooph\EventStore\Exception\InvalidArgumentException; + +/** @internal */ +class PersistentSubscriptionUpdateStatus +{ + public const OPTIONS = [ + 'Success' => 0, + 'NotFound' => 1, + 'Failure' => 2, + ]; + + public const SUCCESS = 0; + public const NOT_FOUND = 1; + public const FAILURE = 2; + + private $name; + private $value; + + private function __construct(string $name) + { + $this->name = $name; + $this->value = self::OPTIONS[$name]; + } + + public static function success(): self + { + return new self('Success'); + } + + public static function notFound(): self + { + return new self('NotFound'); + } + + public static function failure(): self + { + return new self('Failure'); + } + + public static function byName(string $value): self + { + if (! isset(self::OPTIONS[$value])) { + throw new InvalidArgumentException('Unknown enum name given'); + } + + return self::{$value}(); + } + + public static function byValue($value): self + { + foreach (self::OPTIONS as $name => $v) { + if ($v === $value) { + return self::{$name}(); + } + } + + throw new InvalidArgumentException('Unknown enum value given'); + } + + public function equals(PersistentSubscriptionUpdateStatus $other): bool + { + return \get_class($this) === \get_class($other) && $this->name === $other->name; + } + + public function name(): string + { + return $this->name; + } + + public function value() + { + return $this->value; + } + + public function __toString(): string + { + return $this->name; + } +} diff --git a/src/Projection/ReadModel.php b/src/Internal/ResolvedEvent.php similarity index 51% rename from src/Projection/ReadModel.php rename to src/Internal/ResolvedEvent.php index d6bc2057..292e575d 100644 --- a/src/Projection/ReadModel.php +++ b/src/Internal/ResolvedEvent.php @@ -11,19 +11,18 @@ declare(strict_types=1); -namespace Prooph\EventStore\Projection; +namespace Prooph\EventStore\Internal; -interface ReadModel -{ - public function init(): void; - - public function isInitialized(): bool; +use Prooph\EventStore\Position; +use Prooph\EventStore\RecordedEvent; - public function reset(): void; +interface ResolvedEvent +{ + public function originalEvent(): ?RecordedEvent; - public function delete(): void; + public function originalPosition(): ?Position; - public function stack(string $operation, ...$args): void; + public function originalStreamName(): string; - public function persist(): void; + public function originalEventNumber(): int; } diff --git a/src/ListenerHandler.php b/src/ListenerHandler.php new file mode 100644 index 00000000..6aa90691 --- /dev/null +++ b/src/ListenerHandler.php @@ -0,0 +1,31 @@ + + * (c) 2015-2018 Sascha-Oliver Prolic + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Prooph\EventStore; + +class ListenerHandler +{ + /** @var callable */ + private $listener; + + /** @internal */ + public function __construct(callable $listener) + { + $this->listener = $listener; + } + + public function callback(): callable + { + return $this->listener; + } +} diff --git a/src/LiveProcessingStartedOnAsyncCatchUpSubscription.php b/src/LiveProcessingStartedOnAsyncCatchUpSubscription.php new file mode 100644 index 00000000..a7c6d88a --- /dev/null +++ b/src/LiveProcessingStartedOnAsyncCatchUpSubscription.php @@ -0,0 +1,19 @@ + + * (c) 2015-2018 Sascha-Oliver Prolic + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Prooph\EventStore; + +interface LiveProcessingStartedOnAsyncCatchUpSubscription +{ + public function __invoke(AsyncEventStoreCatchUpSubscription $subscription): void; +} diff --git a/src/EventStoreDecorator.php b/src/LiveProcessingStartedOnCatchUpSubscription.php similarity index 73% rename from src/EventStoreDecorator.php rename to src/LiveProcessingStartedOnCatchUpSubscription.php index 5c57ffa4..b3af401f 100644 --- a/src/EventStoreDecorator.php +++ b/src/LiveProcessingStartedOnCatchUpSubscription.php @@ -13,7 +13,7 @@ namespace Prooph\EventStore; -interface EventStoreDecorator extends EventStore +interface LiveProcessingStartedOnCatchUpSubscription { - public function getInnerEventStore(): EventStore; + public function __invoke(EventStoreCatchUpSubscription $subscription): void; } diff --git a/src/Metadata/MetadataEnricherAggregate.php b/src/Metadata/MetadataEnricherAggregate.php deleted file mode 100644 index 21236f3a..00000000 --- a/src/Metadata/MetadataEnricherAggregate.php +++ /dev/null @@ -1,44 +0,0 @@ - - * (c) 2015-2018 Sascha-Oliver Prolic - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace Prooph\EventStore\Metadata; - -use Prooph\Common\Messaging\Message; -use Prooph\EventStore\Util\Assertion; - -final class MetadataEnricherAggregate implements MetadataEnricher -{ - /** - * @var MetadataEnricher[] - */ - private $metadataEnrichers; - - /** - * @param MetadataEnricher[] $metadataEnrichers - */ - public function __construct(array $metadataEnrichers) - { - Assertion::allIsInstanceOf($metadataEnrichers, MetadataEnricher::class); - - $this->metadataEnrichers = $metadataEnrichers; - } - - public function enrich(Message $message): Message - { - foreach ($this->metadataEnrichers as $metadataEnricher) { - $message = $metadataEnricher->enrich($message); - } - - return $message; - } -} diff --git a/src/Metadata/MetadataEnricherPlugin.php b/src/Metadata/MetadataEnricherPlugin.php deleted file mode 100644 index aba35028..00000000 --- a/src/Metadata/MetadataEnricherPlugin.php +++ /dev/null @@ -1,97 +0,0 @@ - - * (c) 2015-2018 Sascha-Oliver Prolic - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace Prooph\EventStore\Metadata; - -use ArrayIterator; -use Iterator; -use Prooph\Common\Event\ActionEvent; -use Prooph\EventStore\ActionEventEmitterEventStore; -use Prooph\EventStore\Plugin\AbstractPlugin; -use Prooph\EventStore\Stream; - -final class MetadataEnricherPlugin extends AbstractPlugin -{ - /** - * @var MetadataEnricher - */ - private $metadataEnricher; - - public function __construct(MetadataEnricher $metadataEnricher) - { - $this->metadataEnricher = $metadataEnricher; - } - - public function attachToEventStore(ActionEventEmitterEventStore $eventStore): void - { - $this->listenerHandlers[] = $eventStore->attach( - ActionEventEmitterEventStore::EVENT_CREATE, - [$this, 'onEventStoreCreateStream'], - 1000 - ); - - $this->listenerHandlers[] = $eventStore->attach( - ActionEventEmitterEventStore::EVENT_APPEND_TO, - [$this, 'onEventStoreAppendToStream'], - 1000 - ); - } - - /** - * Add event metadata on event store createStream. - */ - public function onEventStoreCreateStream(ActionEvent $createEvent): void - { - $stream = $createEvent->getParam('stream'); - - if (! $stream instanceof Stream) { - return; - } - - $streamEvents = $stream->streamEvents(); - $streamEvents = $this->handleRecordedEvents($streamEvents); - - $createEvent->setParam('stream', new Stream($stream->streamName(), $streamEvents, $stream->metadata())); - } - - /** - * Add event metadata on event store appendToStream. - */ - public function onEventStoreAppendToStream(ActionEvent $appendToStreamEvent): void - { - $streamEvents = $appendToStreamEvent->getParam('streamEvents'); - - if (! $streamEvents instanceof Iterator) { - return; - } - - $streamEvents = $this->handleRecordedEvents($streamEvents); - - $appendToStreamEvent->setParam('streamEvents', $streamEvents); - } - - /** - * This method takes domain events as argument which are going to be added - * to the event stream and add the metadata via the MetadataEnricher. - */ - private function handleRecordedEvents(Iterator $events): Iterator - { - $enrichedEvents = []; - - foreach ($events as $event) { - $enrichedEvents[] = $this->metadataEnricher->enrich($event); - } - - return new ArrayIterator($enrichedEvents); - } -} diff --git a/src/Metadata/MetadataMatcher.php b/src/Metadata/MetadataMatcher.php deleted file mode 100644 index b8242809..00000000 --- a/src/Metadata/MetadataMatcher.php +++ /dev/null @@ -1,75 +0,0 @@ - - * (c) 2015-2018 Sascha-Oliver Prolic - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace Prooph\EventStore\Metadata; - -use Prooph\EventStore\Exception\InvalidArgumentException; - -class MetadataMatcher -{ - private $data = []; - - public function data(): array - { - return $this->data; - } - - public function withMetadataMatch( - string $field, - Operator $operator, - $value, - FieldType $fieldType = null - ): MetadataMatcher { - $this->validateValue($operator, $value); - - if (null === $fieldType) { - $fieldType = FieldType::METADATA(); - } - - $self = clone $this; - $self->data[] = ['field' => $field, 'operator' => $operator, 'value' => $value, 'fieldType' => $fieldType]; - - return $self; - } - - /** - * @param Operator $operator - * @param mixed $value - * @throws InvalidArgumentException - */ - private function validateValue(Operator $operator, $value): void - { - if ($operator->is(Operator::IN()) || $operator->is(Operator::NOT_IN()) - ) { - if (\is_array($value)) { - return; - } - - throw new InvalidArgumentException(\sprintf( - 'Value must be an array for the operator %s.', - $operator->getName() - )); - } - - if ($operator->is(Operator::REGEX()) && ! \is_string($value)) { - throw new InvalidArgumentException('Value must be a string for the regex operator.'); - } - - if (! \is_scalar($value)) { - throw new InvalidArgumentException(\sprintf( - 'Value must have a scalar type for the operator %s.', - $operator->getName() - )); - } - } -} diff --git a/src/Metadata/Operator.php b/src/Metadata/Operator.php deleted file mode 100644 index 8a1b8e0f..00000000 --- a/src/Metadata/Operator.php +++ /dev/null @@ -1,40 +0,0 @@ - - * (c) 2015-2018 Sascha-Oliver Prolic - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace Prooph\EventStore\Metadata; - -use MabeEnum\Enum; - -/** - * @method static Operator EQUALS() - * @method static Operator GREATER_THAN() - * @method static Operator GREATER_THAN_EQUALS() - * @method static Operator IN() - * @method static Operator LOWER_THAN() - * @method static Operator LOWER_THAN_EQUALS() - * @method static Operator NOT_EQUALS() - * @method static Operator NOT_IN() - * @method static Operator REGEX() - */ -class Operator extends Enum -{ - public const EQUALS = '='; - public const GREATER_THAN = '>'; - public const GREATER_THAN_EQUALS = '>='; - public const IN = 'in'; - public const LOWER_THAN = '<'; - public const LOWER_THAN_EQUALS = '<='; - public const NOT_EQUALS = '!='; - public const NOT_IN = 'nin'; - public const REGEX = 'regex'; -} diff --git a/src/NonTransactionalInMemoryEventStore.php b/src/NonTransactionalInMemoryEventStore.php deleted file mode 100644 index 8ab1d19b..00000000 --- a/src/NonTransactionalInMemoryEventStore.php +++ /dev/null @@ -1,481 +0,0 @@ - - * (c) 2015-2018 Sascha-Oliver Prolic - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace Prooph\EventStore; - -use Iterator; -use Prooph\Common\Messaging\Message; -use Prooph\EventStore\Exception\StreamExistsAlready; -use Prooph\EventStore\Exception\StreamNotFound; -use Prooph\EventStore\Metadata\FieldType; -use Prooph\EventStore\Metadata\MetadataMatcher; -use Prooph\EventStore\Metadata\Operator; -use Prooph\EventStore\StreamIterator\EmptyStreamIterator; -use Prooph\EventStore\StreamIterator\InMemoryStreamIterator; -use Prooph\EventStore\Util\Assertion; - -final class NonTransactionalInMemoryEventStore implements EventStore -{ - /** - * @var array - */ - private $streams = []; - - /** - * @var array - */ - private $cachedStreams = []; - - public function create(Stream $stream): void - { - $streamName = $stream->streamName(); - $streamNameString = $streamName->toString(); - - if ( - isset($this->streams[$streamNameString]) - || isset($this->cachedStreams[$streamNameString]) - ) { - throw StreamExistsAlready::with($streamName); - } - - $this->streams[$streamNameString]['events'] = \iterator_to_array($stream->streamEvents()); - $this->streams[$streamNameString]['metadata'] = $stream->metadata(); - } - - public function appendTo(StreamName $streamName, Iterator $streamEvents): void - { - $streamNameString = $streamName->toString(); - - if ( - ! isset($this->streams[$streamNameString]) - && ! isset($this->cachedStreams[$streamNameString]) - ) { - throw StreamNotFound::with($streamName); - } - - foreach ($streamEvents as $streamEvent) { - $this->streams[$streamNameString]['events'][] = $streamEvent; - } - } - - public function load( - StreamName $streamName, - int $fromNumber = 1, - int $count = null, - MetadataMatcher $metadataMatcher = null - ): Iterator { - Assertion::greaterOrEqualThan($fromNumber, 1); - Assertion::nullOrGreaterOrEqualThan($count, 1); - - if (! isset($this->streams[$streamName->toString()])) { - throw StreamNotFound::with($streamName); - } - - if (null === $metadataMatcher) { - $metadataMatcher = new MetadataMatcher(); - } - - $found = 0; - $streamEvents = []; - - foreach ($this->streams[$streamName->toString()]['events'] as $key => $streamEvent) { - /* @var Message $streamEvent */ - if ( - ($key + 1) >= $fromNumber - && $this->matchesMetadata($metadataMatcher, $streamEvent->metadata()) - && $this->matchesMessagesProperty($metadataMatcher, $streamEvent) - ) { - ++$found; - $streamEvents[] = $streamEvent; - - if ($found === $count) { - break; - } - } - } - - if (0 === $found) { - return new EmptyStreamIterator(); - } - - return new InMemoryStreamIterator($streamEvents); - } - - public function loadReverse( - StreamName $streamName, - int $fromNumber = null, - int $count = null, - MetadataMatcher $metadataMatcher = null - ): Iterator { - if (null === $fromNumber) { - $fromNumber = PHP_INT_MAX; - } - - Assertion::greaterOrEqualThan($fromNumber, 1); - Assertion::nullOrGreaterOrEqualThan($count, 1); - - if (! isset($this->streams[$streamName->toString()])) { - throw StreamNotFound::with($streamName); - } - - if (null === $metadataMatcher) { - $metadataMatcher = new MetadataMatcher(); - } - - $found = 0; - $streamEvents = []; - - foreach (\array_reverse($this->streams[$streamName->toString()]['events'], true) as $key => $streamEvent) { - /* @var Message $streamEvent */ - if ( - ($key + 1) <= $fromNumber - && $this->matchesMetadata($metadataMatcher, $streamEvent->metadata()) - && $this->matchesMessagesProperty($metadataMatcher, $streamEvent) - ) { - $streamEvents[] = $streamEvent; - ++$found; - - if ($found === $count) { - break; - } - } - } - - if (0 === $found) { - return new EmptyStreamIterator(); - } - - return new InMemoryStreamIterator($streamEvents); - } - - public function delete(StreamName $streamName): void - { - $streamNameString = $streamName->toString(); - - if (isset($this->streams[$streamNameString])) { - unset($this->streams[$streamNameString]); - } else { - throw StreamNotFound::with($streamName); - } - } - - public function hasStream(StreamName $streamName): bool - { - return isset($this->streams[$streamName->toString()]); - } - - public function fetchStreamMetadata(StreamName $streamName): array - { - if (! isset($this->streams[$streamName->toString()])) { - throw StreamNotFound::with($streamName); - } - - return $this->streams[$streamName->toString()]['metadata']; - } - - public function updateStreamMetadata(StreamName $streamName, array $newMetadata): void - { - if (! isset($this->streams[$streamName->toString()])) { - throw StreamNotFound::with($streamName); - } - - $this->streams[$streamName->toString()]['metadata'] = $newMetadata; - } - - public function fetchStreamNames( - ?string $filter, - ?MetadataMatcher $metadataMatcher, - int $limit = 20, - int $offset = 0 - ): array { - $result = []; - - $skipped = 0; - $found = 0; - - $streams = $this->streams; - - if ( - $filter - && \array_key_exists($filter, $streams) - && ( - ! $metadataMatcher - || $metadataMatcher && $this->matchesMetadata($metadataMatcher, $streams[$filter]['metadata']) - ) - ) { - return [$filter]; - } - - \ksort($streams); - - foreach ($streams as $streamName => $data) { - if (null === $filter || $filter === $streamName) { - if ($offset > $skipped) { - ++$skipped; - continue; - } - - if ($metadataMatcher && ! $this->matchesMetadata($metadataMatcher, $data['metadata'])) { - continue; - } - - $result[] = new StreamName($streamName); - ++$found; - } - - if ($found === $limit) { - break; - } - } - - return $result; - } - - public function fetchStreamNamesRegex( - string $filter, - ?MetadataMatcher $metadataMatcher, - int $limit = 20, - int $offset = 0 - ): array { - if (false === @\preg_match("/$filter/", '')) { - throw new Exception\InvalidArgumentException('Invalid regex pattern given'); - } - - $result = []; - - $found = 0; - - $streams = $this->streams; - \ksort($streams); - - foreach ($streams as $streamName => $data) { - if (! \preg_match("/$filter/", $streamName)) { - continue; - } - - if ($metadataMatcher && ! $this->matchesMetadata($metadataMatcher, $data['metadata'])) { - continue; - } - - $result[] = new StreamName($streamName); - ++$found; - - if ($found === $limit) { - break; - } - } - - return $result; - } - - public function fetchCategoryNames(?string $filter, int $limit = 20, int $offset = 0): array - { - $result = []; - - $skipped = 0; - $found = 0; - - $categories = \array_unique( - \array_reduce( - \array_keys($this->streams), - function (array $result, string $streamName): array { - if (\preg_match('/^(.+)-.+$/', $streamName, $matches)) { - $result[] = $matches[1]; - } - - return $result; - }, - [] - ) - ); - - if ($filter && \in_array($filter, $categories, true)) { - return [$filter]; - } - - \ksort($categories); - - foreach ($categories as $category) { - if (null === $filter || $filter === $category) { - if ($offset > $skipped) { - ++$skipped; - continue; - } - - $result[] = $category; - ++$found; - } - - if ($found === $limit) { - break; - } - } - - return $result; - } - - public function fetchCategoryNamesRegex(string $filter, int $limit = 20, int $offset = 0): array - { - if (false === @\preg_match("/$filter/", '')) { - throw new Exception\InvalidArgumentException('Invalid regex pattern given'); - } - - $result = []; - - $skipped = 0; - $found = 0; - - $categories = \array_unique( - \array_reduce( - \array_keys($this->streams), - function (array $result, string $streamName): array { - if (\preg_match('/^(.+)-.+$/', $streamName, $matches)) { - $result[] = $matches[1]; - } - - return $result; - }, - [] - ) - ); - - \ksort($categories); - - foreach ($categories as $category) { - if (! \preg_match("/$filter/", $category)) { - continue; - } - - if ($offset > $skipped) { - ++$skipped; - continue; - } - - $result[] = $category; - ++$found; - - if ($found === $limit) { - break; - } - } - - return $result; - } - - private function matchesMetadata(MetadataMatcher $metadataMatcher, array $metadata): bool - { - foreach ($metadataMatcher->data() as $match) { - if (! FieldType::METADATA()->is($match['fieldType'])) { - continue; - } - - $field = $match['field']; - - if (! isset($metadata[$field])) { - return false; - } - - if (! $this->match($match['operator'], $metadata[$field], $match['value'])) { - return false; - } - } - - return true; - } - - private function matchesMessagesProperty(MetadataMatcher $metadataMatcher, Message $message): bool - { - foreach ($metadataMatcher->data() as $match) { - if (! FieldType::MESSAGE_PROPERTY()->is($match['fieldType'])) { - continue; - } - - switch ($match['field']) { - case 'uuid': - $value = $message->uuid()->toString(); - break; - case 'message_name': - case 'messageName': - $value = $message->messageName(); - break; - case 'created_at': - case 'createdAt': - $value = $message->createdAt()->format('Y-m-d\TH:i:s.u'); - break; - default: - throw new \UnexpectedValueException(\sprintf('Unexpected field "%s" given', $match['field'])); - } - - if (! $this->match($match['operator'], $value, $match['value'])) { - return false; - } - } - - return true; - } - - private function match(Operator $operator, $value, $expected): bool - { - switch ($operator) { - case Operator::EQUALS(): - if ($value !== $expected) { - return false; - } - break; - case Operator::GREATER_THAN(): - if (! ($value > $expected)) { - return false; - } - break; - case Operator::GREATER_THAN_EQUALS(): - if (! ($value >= $expected)) { - return false; - } - break; - case Operator::IN(): - if (! \in_array($value, $expected, true)) { - return false; - } - break; - case Operator::LOWER_THAN(): - if (! ($value < $expected)) { - return false; - } - break; - case Operator::LOWER_THAN_EQUALS(): - if (! ($value <= $expected)) { - return false; - } - break; - case Operator::NOT_EQUALS(): - if ($value === $expected) { - return false; - } - break; - case Operator::NOT_IN(): - if (\in_array($value, $expected, true)) { - return false; - } - break; - case Operator::REGEX(): - if (! \preg_match('/' . $expected . '/', $value)) { - return false; - } - break; - default: - throw new \UnexpectedValueException('Unknown operator found'); - } - - return true; - } -} diff --git a/src/EventStore.php b/src/PersistentSubscriptionDropped.php similarity index 52% rename from src/EventStore.php rename to src/PersistentSubscriptionDropped.php index cf301efd..2f1c0805 100644 --- a/src/EventStore.php +++ b/src/PersistentSubscriptionDropped.php @@ -13,15 +13,14 @@ namespace Prooph\EventStore; -use Iterator; +use Prooph\EventStore\Internal\EventStorePersistentSubscription; +use Throwable; -interface EventStore extends ReadOnlyEventStore +interface PersistentSubscriptionDropped { - public function updateStreamMetadata(StreamName $streamName, array $newMetadata): void; - - public function create(Stream $stream): void; - - public function appendTo(StreamName $streamName, Iterator $streamEvents): void; - - public function delete(StreamName $streamName): void; + public function __invoke( + EventStorePersistentSubscription $subscription, + SubscriptionDropReason $reason, + ?Throwable $exception = null + ): void; } diff --git a/src/PersistentSubscriptionNakEventAction.php b/src/PersistentSubscriptionNakEventAction.php new file mode 100644 index 00000000..f754f9bc --- /dev/null +++ b/src/PersistentSubscriptionNakEventAction.php @@ -0,0 +1,112 @@ + + * (c) 2015-2018 Sascha-Oliver Prolic + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Prooph\EventStore; + +use Prooph\EventStore\Exception\InvalidArgumentException; + +class PersistentSubscriptionNakEventAction +{ + public const OPTIONS = [ + 'Unknown' => 0, + 'Park' => 1, + 'Retry' => 2, + 'Skip' => 3, + 'Stop' => 4, + ]; + + // Client unknown on action. Let server decide + public const UNKNOWN = 0; + // Park message do not resend. Put on poison queue + public const PARK = 1; + // Explicitly retry the message + public const RETRY = 2; + // Skip this message do not resend do not put in poison queue + public const SKIP = 3; + // Stop the subscription + public const STOP = 4; + + private $name; + private $value; + + private function __construct(string $name) + { + $this->name = $name; + $this->value = self::OPTIONS[$name]; + } + + public static function unknown(): self + { + return new self('Unknown'); + } + + public static function park(): self + { + return new self('Park'); + } + + public static function retry(): self + { + return new self('Retry'); + } + + public static function skip(): self + { + return new self('Skip'); + } + + public static function stop(): self + { + return new self('Stop'); + } + + public static function byName(string $value): self + { + if (! isset(self::OPTIONS[$value])) { + throw new InvalidArgumentException('Unknown enum name given'); + } + + return self::{$value}(); + } + + public static function byValue($value): self + { + foreach (self::OPTIONS as $name => $v) { + if ($v === $value) { + return self::{$name}(); + } + } + + throw new InvalidArgumentException('Unknown enum value given'); + } + + public function equals(PersistentSubscriptionNakEventAction $other): bool + { + return \get_class($this) === \get_class($other) && $this->name === $other->name; + } + + public function name(): string + { + return $this->name; + } + + public function value() + { + return $this->value; + } + + public function __toString(): string + { + return $this->name; + } +} diff --git a/src/PersistentSubscriptionResolvedEvent.php b/src/PersistentSubscriptionResolvedEvent.php new file mode 100644 index 00000000..dac1c8ff --- /dev/null +++ b/src/PersistentSubscriptionResolvedEvent.php @@ -0,0 +1,61 @@ + + * (c) 2015-2018 Sascha-Oliver Prolic + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Prooph\EventStore; + +use Prooph\EventStore\Internal\ResolvedEvent as InternalResolvedEvent; + +/** @internal */ +class PersistentSubscriptionResolvedEvent implements InternalResolvedEvent +{ + /** @var int|null */ + private $retryCount; + /** @var ResolvedEvent */ + private $event; + + public function __construct(ResolvedEvent $event, ?int $retryCount) + { + $this->event = $event; + $this->retryCount = $retryCount; + } + + public function retryCount(): ?int + { + return $this->retryCount; + } + + public function event(): ResolvedEvent + { + return $this->event; + } + + public function originalEvent(): ?RecordedEvent + { + return $this->event->originalEvent(); + } + + public function originalPosition(): ?Position + { + return $this->event->originalPosition(); + } + + public function originalStreamName(): string + { + return $this->event->originalStreamName(); + } + + public function originalEventNumber(): int + { + return $this->event->originalEventNumber(); + } +} diff --git a/src/PersistentSubscriptionSettings.php b/src/PersistentSubscriptionSettings.php new file mode 100644 index 00000000..a46a33ca --- /dev/null +++ b/src/PersistentSubscriptionSettings.php @@ -0,0 +1,215 @@ + + * (c) 2015-2018 Sascha-Oliver Prolic + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Prooph\EventStore; + +use Prooph\EventStore\Common\SystemConsumerStrategies; +use Prooph\EventStore\Exception\InvalidArgumentException; + +class PersistentSubscriptionSettings +{ + /** + * Tells the subscription to resolve link events. + * @var bool + */ + private $resolveLinkTos; + /** + * Start the subscription from the position-of the event in the stream. + * If the value is set to `-1` that the subscription should start from + * where the stream is when the subscription is first connected. + * @var int + */ + private $startFrom; + /** + * Tells the backend to measure timings on the clients so statistics will contain histograms of them. + * @var bool + */ + private $extraStatistics; + /** + * The amount of time the system should try to checkpoint after. + * @var int + */ + private $checkPointAfterMilliseconds; + /** + * The size of the live buffer (in memory) before resorting to paging. + * @var int + */ + private $liveBufferSize; + /** + * The size of the read batch when in paging mode. + * @var int + */ + private $readBatchSize; + /** + * The number of messages that should be buffered when in paging mode. + * @var int + */ + private $bufferSize; + /** + * The maximum number of messages not checkpointed before forcing a checkpoint. + * @var int + */ + private $maxCheckPointCount; + /** + * Sets the number of times a message should be retried before considered a bad message. + * @var int + */ + private $maxRetryCount; + /** + * Sets the maximum number of allowed subscribers. + * @var int + */ + private $maxSubscriberCount; + /** + * Sets the timeout for a client before the message will be retried. + * @var int + */ + private $messageTimeoutMilliseconds; + /** + * The minimum number of messages to write a checkpoint for. + * @var int + */ + private $minCheckPointCount; + /** @var string */ + private $namedConsumerStrategy; + + private const INT_32_MAX = 2147483647; + + public static function default(): self + { + return self::create()->build(); + } + + public static function create(): PersistentSubscriptionSettingsBuilder + { + return new PersistentSubscriptionSettingsBuilder( + false, + -1, + false, + 30000, + 500, + 500, + 10, + 20, + 2000, + 10, + 1000, + 0, + SystemConsumerStrategies::ROUND_ROBIN + ); + } + + /** @internal */ + public function __construct( + bool $resolveLinkTos, + int $startFrom, + bool $extraStatistics, + int $messageTimeoutMilliseconds, + int $bufferSize, + int $liveBufferSize, + int $maxRetryCount, + int $readBatchSize, + int $checkPointAfterMilliseconds, + int $minCheckPointCount, + int $maxCheckPointCount, + int $maxSubscriberCount, + string $namedConsumerStrategy + ) { + if ($checkPointAfterMilliseconds > self::INT_32_MAX) { + throw new InvalidArgumentException('checkPointAfterMilliseconds must smaller than ' . self::INT_32_MAX); + } + + if ($messageTimeoutMilliseconds > self::INT_32_MAX) { + throw new InvalidArgumentException('messageTimeoutMilliseconds must smaller than ' . self::INT_32_MAX); + } + + $this->resolveLinkTos = $resolveLinkTos; + $this->startFrom = $startFrom; + $this->extraStatistics = $extraStatistics; + $this->checkPointAfterMilliseconds = $checkPointAfterMilliseconds; + $this->liveBufferSize = $liveBufferSize; + $this->readBatchSize = $readBatchSize; + $this->bufferSize = $bufferSize; + $this->maxCheckPointCount = $maxCheckPointCount; + $this->maxRetryCount = $maxRetryCount; + $this->maxSubscriberCount = $maxSubscriberCount; + $this->messageTimeoutMilliseconds = $messageTimeoutMilliseconds; + $this->minCheckPointCount = $minCheckPointCount; + $this->namedConsumerStrategy = $namedConsumerStrategy; + } + + public function resolveLinkTos(): bool + { + return $this->resolveLinkTos; + } + + public function startFrom(): int + { + return $this->startFrom; + } + + public function extraStatistics(): bool + { + return $this->extraStatistics; + } + + public function checkPointAfterMilliseconds(): int + { + return $this->checkPointAfterMilliseconds; + } + + public function liveBufferSize(): int + { + return $this->liveBufferSize; + } + + public function readBatchSize(): int + { + return $this->readBatchSize; + } + + public function bufferSize(): int + { + return $this->bufferSize; + } + + public function maxCheckPointCount(): int + { + return $this->maxCheckPointCount; + } + + public function maxRetryCount(): int + { + return $this->maxRetryCount; + } + + public function maxSubscriberCount(): int + { + return $this->maxSubscriberCount; + } + + public function messageTimeoutMilliseconds(): int + { + return $this->messageTimeoutMilliseconds; + } + + public function minCheckPointCount(): int + { + return $this->minCheckPointCount; + } + + public function namedConsumerStrategy(): string + { + return $this->namedConsumerStrategy; + } +} diff --git a/src/PersistentSubscriptionSettingsBuilder.php b/src/PersistentSubscriptionSettingsBuilder.php new file mode 100644 index 00000000..d779c258 --- /dev/null +++ b/src/PersistentSubscriptionSettingsBuilder.php @@ -0,0 +1,288 @@ + + * (c) 2015-2018 Sascha-Oliver Prolic + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Prooph\EventStore; + +use Prooph\EventStore\Common\SystemConsumerStrategies; +use Prooph\EventStore\Exception\InvalidArgumentException; + +class PersistentSubscriptionSettingsBuilder +{ + /** + * Tells the subscription to resolve link events. + * @var bool + */ + private $resolveLinkTos; + /** + * Start the subscription from the position-of the event in the stream. + * If the value is set to `-1` that the subscription should start from + * where the stream is when the subscription is first connected. + * @var int + */ + private $startFrom; + /** + * Tells the backend to measure timings on the clients so statistics will contain histograms of them. + * @var bool + */ + private $extraStatistics; + /** + * The amount of time the system should try to checkpoint after. + * @var int + */ + private $checkPointAfterMilliseconds; + /** + * The size of the live buffer (in memory) before resorting to paging. + * @var int + */ + private $liveBufferSize; + /** + * The size of the read batch when in paging mode. + * @var int + */ + private $readBatchSize; + /** + * The number of messages that should be buffered when in paging mode. + * @var int + */ + private $bufferSize; + /** + * The maximum number of messages not checkpointed before forcing a checkpoint. + * @var int + */ + private $maxCheckPointCount; + /** + * Sets the number of times a message should be retried before considered a bad message. + * @var int + */ + private $maxRetryCount; + /** + * Sets the maximum number of allowed subscribers. + * @var int + */ + private $maxSubscriberCount; + /** + * Sets the timeout for a client before the message will be retried. + * @var int + */ + private $messageTimeoutMilliseconds; + /** + * The minimum number of messages to write a checkpoint for. + * @var int + */ + private $minCheckPointCount; + /** @var string */ + private $namedConsumerStrategy; + + /** @internal */ + public function __construct( + bool $resolveLinkTos, + int $startFrom, + bool $extraStatistics, + int $messageTimeoutMilliseconds, + int $bufferSize, + int $liveBufferSize, + int $maxRetryCount, + int $readBatchSize, + int $checkPointAfterMilliseconds, + int $minCheckPointCount, + int $maxCheckPointCount, + int $maxSubscriberCount, + string $namedConsumerStrategy + ) { + $this->resolveLinkTos = $resolveLinkTos; + $this->startFrom = $startFrom; + $this->extraStatistics = $extraStatistics; + $this->messageTimeoutMilliseconds = $messageTimeoutMilliseconds; + $this->bufferSize = $bufferSize; + $this->liveBufferSize = $liveBufferSize; + $this->maxRetryCount = $maxRetryCount; + $this->readBatchSize = $readBatchSize; + $this->checkPointAfterMilliseconds = $checkPointAfterMilliseconds; + $this->minCheckPointCount = $minCheckPointCount; + $this->maxCheckPointCount = $maxCheckPointCount; + $this->maxSubscriberCount = $maxSubscriberCount; + $this->namedConsumerStrategy = $namedConsumerStrategy; + } + + public function withExtraStatistics(): self + { + $this->extraStatistics = true; + + return $this; + } + + public function resolveLinkTos(): self + { + $this->resolveLinkTos = true; + + return $this; + } + + public function doNotResolveLinkTos(): self + { + $this->resolveLinkTos = false; + + return $this; + } + + public function preferRoundRobin(): self + { + $this->namedConsumerStrategy = SystemConsumerStrategies::ROUND_ROBIN; + + return $this; + } + + public function preferDispatchToSingle(): self + { + $this->namedConsumerStrategy = SystemConsumerStrategies::DISPATCH_TO_SINGLE; + + return $this; + } + + public function startFromBeginning(): self + { + $this->startFrom = 0; + + return $this; + } + + public function startFrom(int $position): self + { + $this->startFrom = $position; + + return $this; + } + + public function withMessageTimeoutOf(int $timeout): self + { + $this->messageTimeoutMilliseconds = $timeout; + + return $this; + } + + public function dontTimeoutMessages(): self + { + $this->messageTimeoutMilliseconds = 0; + + return $this; + } + + public function checkPointAfterMilliseconds(int $time): self + { + $this->checkPointAfterMilliseconds = $time; + + return $this; + } + + public function minimumCheckPointCountOf(int $count): self + { + $this->minCheckPointCount = $count; + + return $this; + } + + public function maximumCheckPointCountOf(int $count): self + { + $this->maxCheckPointCount = $count; + + return $this; + } + + public function withMaxRetriesOf(int $count): self + { + if ($count < 0) { + throw new InvalidArgumentException('MaxRetries cannot be negative'); + } + + $this->maxRetryCount = $count; + + return $this; + } + + public function withLiveBufferSizeOf(int $count): self + { + if ($count < 0) { + throw new InvalidArgumentException('LiveBufferSize cannot be negative'); + } + + $this->liveBufferSize = $count; + + return $this; + } + + public function withReadBatchOf(int $count): self + { + if ($count < 0) { + throw new InvalidArgumentException('ReadBatchSize cannot be negative'); + } + + $this->readBatchSize = $count; + + return $this; + } + + public function withBufferSizeOf(int $count): self + { + if ($count < 0) { + throw new InvalidArgumentException('BufferSize cannot be negative'); + } + + $this->bufferSize = $count; + + return $this; + } + + public function startFromCurrent(): self + { + $this->startFrom = -1; + + return $this; + } + + public function withMaxSubscriberCountOf(int $count): self + { + if ($count < 0) { + throw new InvalidArgumentException('Max subscriber count cannot be negative'); + } + + $this->maxSubscriberCount = $count; + + return $this; + } + + public function withNamedConsumerStrategy(string $namedConsumerStrategy): self + { + $this->namedConsumerStrategy = $namedConsumerStrategy; + + return $this; + } + + public function build(): PersistentSubscriptionSettings + { + return new PersistentSubscriptionSettings( + $this->resolveLinkTos, + $this->startFrom, + $this->extraStatistics, + $this->messageTimeoutMilliseconds, + $this->bufferSize, + $this->liveBufferSize, + $this->maxRetryCount, + $this->readBatchSize, + $this->checkPointAfterMilliseconds, + $this->minCheckPointCount, + $this->maxCheckPointCount, + $this->maxSubscriberCount, + $this->namedConsumerStrategy + ); + } +} diff --git a/src/PersistentSubscriptions/AsyncPersistentSubscriptionsManager.php b/src/PersistentSubscriptions/AsyncPersistentSubscriptionsManager.php new file mode 100644 index 00000000..204a00ee --- /dev/null +++ b/src/PersistentSubscriptions/AsyncPersistentSubscriptionsManager.php @@ -0,0 +1,45 @@ + + * (c) 2015-2018 Sascha-Oliver Prolic + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Prooph\EventStore\PersistentSubscriptions; + +use Amp\Promise; +use Prooph\EventStore\UserCredentials; + +interface AsyncPersistentSubscriptionsManager +{ + /** + * @param string $stream + * @param string $subscriptionName + * @param null|UserCredentials $userCredentials + * @return Promise + */ + public function describe( + string $stream, + string $subscriptionName, + ?UserCredentials $userCredentials = null + ): Promise; + + public function replayParkedMessages( + string $stream, + string $subscriptionName, + ?UserCredentials $userCredentials = null + ): Promise; + + /** + * @param null|string $stream + * @param null|UserCredentials $userCredentials + * @return Promise + */ + public function list(?string $stream = null, ?UserCredentials $userCredentials = null): Promise; +} diff --git a/src/PersistentSubscriptions/PersistentSubscriptionConfigDetails.php b/src/PersistentSubscriptions/PersistentSubscriptionConfigDetails.php new file mode 100644 index 00000000..a4b5cdb1 --- /dev/null +++ b/src/PersistentSubscriptions/PersistentSubscriptionConfigDetails.php @@ -0,0 +1,143 @@ + + * (c) 2015-2018 Sascha-Oliver Prolic + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Prooph\EventStore\PersistentSubscriptions; + +/** @internal */ +final class PersistentSubscriptionConfigDetails +{ + /** @var bool */ + private $resolveLinktos; + /** @var int */ + private $startFrom; + /** @var int */ + private $messageTimeoutMilliseconds; + /** @var bool */ + private $extraStatistics; + /** @var int */ + private $maxRetryCount; + /** @var int */ + private $liveBufferSize; + /** @var int */ + private $bufferSize; + /** @var int */ + private $readBatchSize; + /** @var int */ + private $checkPointAfterMilliseconds; + /** @var int */ + private $minCheckPointCount; + /** @var int */ + private $maxCheckPointCount; + /** @var int */ + private $maxSubscriberCount; + /** @var string */ + private $namedConsumerStrategy; + /** @var bool */ + private $preferRoundRobin; + + private function __construct() + { + } + + public static function fromArray(array $data): self + { + $details = new self(); + + $details->resolveLinktos = $data['resolveLinktos']; + $details->startFrom = $data['startFrom']; + $details->messageTimeoutMilliseconds = $data['messageTimeoutMilliseconds']; + $details->extraStatistics = $data['extraStatistics']; + $details->maxRetryCount = $data['maxRetryCount']; + $details->liveBufferSize = $data['liveBufferSize']; + $details->bufferSize = $data['bufferSize']; + $details->readBatchSize = $data['readBatchSize']; + $details->checkPointAfterMilliseconds = $data['checkPointAfterMilliseconds']; + $details->minCheckPointCount = $data['minCheckPointCount']; + $details->maxCheckPointCount = $data['maxCheckPointCount']; + $details->maxSubscriberCount = $data['maxSubscriberCount']; + $details->namedConsumerStrategy = $data['namedConsumerStrategy']; + $details->preferRoundRobin = $data['preferRoundRobin']; + + return $details; + } + + public function resolveLinktos(): bool + { + return $this->resolveLinktos; + } + + public function startFrom(): int + { + return $this->startFrom; + } + + public function messageTimeoutMilliseconds(): int + { + return $this->messageTimeoutMilliseconds; + } + + public function extraStatistics(): bool + { + return $this->extraStatistics; + } + + public function maxRetryCount(): int + { + return $this->maxRetryCount; + } + + public function liveBufferSize(): int + { + return $this->liveBufferSize; + } + + public function bufferSize(): int + { + return $this->bufferSize; + } + + public function readBatchSize(): int + { + return $this->readBatchSize; + } + + public function checkPointAfterMilliseconds(): int + { + return $this->checkPointAfterMilliseconds; + } + + public function minCheckPointCount(): int + { + return $this->minCheckPointCount; + } + + public function maxCheckPointCount(): int + { + return $this->maxCheckPointCount; + } + + public function maxSubscriberCount(): int + { + return $this->maxSubscriberCount; + } + + public function namedConsumerStrategy(): string + { + return $this->namedConsumerStrategy; + } + + public function preferRoundRobin(): bool + { + return $this->preferRoundRobin; + } +} diff --git a/src/PersistentSubscriptions/PersistentSubscriptionConnectionDetails.php b/src/PersistentSubscriptions/PersistentSubscriptionConnectionDetails.php new file mode 100644 index 00000000..f676a796 --- /dev/null +++ b/src/PersistentSubscriptions/PersistentSubscriptionConnectionDetails.php @@ -0,0 +1,87 @@ + + * (c) 2015-2018 Sascha-Oliver Prolic + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Prooph\EventStore\PersistentSubscriptions; + +/** @internal */ +final class PersistentSubscriptionConnectionDetails +{ + /** @var string */ + private $from; + /** @var string */ + private $username; + /** @var float */ + private $averageItemsPerSecond; + /** @var int */ + private $totalItemsProcessed; + /** @var int */ + private $countSinceLastMeasurement; + /** @var int */ + private $availableSlots; + /** @var int */ + private $inFlightMessages; + + private function __construct() + { + } + + public static function fromArray(array $data): self + { + $details = new self(); + + $details->from = $data['from']; + $details->username = $data['username']; + $details->averageItemsPerSecond = $data['averageItemsPerSecond']; + $details->totalItemsProcessed = $data['totalItemsProcessed']; + $details->countSinceLastMeasurement = $data['countSinceLastMeasurement']; + $details->availableSlots = $data['availableSlots']; + $details->inFlightMessages = $data['inFlightMessages']; + + return $details; + } + + public function from(): string + { + return $this->from; + } + + public function username(): string + { + return $this->username; + } + + public function averageItemsPerSecond(): float + { + return $this->averageItemsPerSecond; + } + + public function totalItemsProcessed(): int + { + return $this->totalItemsProcessed; + } + + public function countSinceLastMeasurement(): int + { + return $this->countSinceLastMeasurement; + } + + public function availableSlots(): int + { + return $this->availableSlots; + } + + public function inFlightMessages(): int + { + return $this->inFlightMessages; + } +} diff --git a/src/PersistentSubscriptions/PersistentSubscriptionDetails.php b/src/PersistentSubscriptions/PersistentSubscriptionDetails.php new file mode 100644 index 00000000..8b005e26 --- /dev/null +++ b/src/PersistentSubscriptions/PersistentSubscriptionDetails.php @@ -0,0 +1,178 @@ + + * (c) 2015-2018 Sascha-Oliver Prolic + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Prooph\EventStore\PersistentSubscriptions; + +/** @internal */ +final class PersistentSubscriptionDetails +{ + /** + * @var PersistentSubscriptionConfigDetails + * + * Only populated when retrieved via PersistentSubscriptionsManager::describe() method. + */ + private $config; + + /** + * @var PersistentSubscriptionConfigDetails[] + * + * Only populated when retrieved via PersistentSubscriptionsManager::describe() method. + */ + private $connections = []; + + /** @var string */ + private $eventStreamId; + /** @var string */ + private $groupName; + /** @var string */ + private $status; + /** @var float */ + private $averageItemsPerSecond; + /** @var int */ + private $totalItemsProcessed; + /** @var int */ + private $countSinceLastMeasurement; + /** @var int */ + private $lastProcessedEventNumber; + /** @var int */ + private $lastKnownEventNumber; + /** @var int */ + private $readBufferCount; + /** @var int */ + private $liveBufferCount; + /** @var int */ + private $retryBufferCount; + /** @var int */ + private $totalInFlightMessages; + /** @var string */ + private $parkedMessageUri; + /** @var string */ + private $getMessagesUri; + + private function __construct() + { + } + + public static function fromArray(array $data): self + { + $details = new self(); + + if (isset($data['config'])) { + $details->config = PersistentSubscriptionConfigDetails::fromArray($data['config']); + } + + if (isset($data['connections'])) { + foreach ($data['connections'] as $connection) { + $details->connections[] = PersistentSubscriptionConnectionDetails::fromArray($connection); + } + } + + $details->eventStreamId = $data['eventStreamId']; + $details->groupName = $data['groupName']; + $details->status = $data['status']; + $details->averageItemsPerSecond = $data['averageItemsPerSecond']; + $details->totalItemsProcessed = $data['totalItemsProcessed']; + $details->countSinceLastMeasurement = $data['countSinceLastMeasurement'] ?? 0; + $details->lastProcessedEventNumber = $data['lastProcessedEventNumber']; + $details->lastKnownEventNumber = $data['lastKnownEventNumber']; + $details->readBufferCount = $data['readBufferCount'] ?? 0; + $details->liveBufferCount = $data['liveBufferCount'] ?? 0; + $details->retryBufferCount = $data['retryBufferCount'] ?? 0; + $details->totalInFlightMessages = $data['totalInFlightMessages']; + $details->parkedMessageUri = $data['parkedMessageUri']; + $details->getMessagesUri = $data['getMessagesUri']; + + return $details; + } + + public function config(): PersistentSubscriptionConfigDetails + { + return $this->config; + } + + /** @return PersistentSubscriptionConfigDetails[] */ + public function connections(): array + { + return $this->connections; + } + + public function eventStreamId(): string + { + return $this->eventStreamId; + } + + public function groupName(): string + { + return $this->groupName; + } + + public function status(): string + { + return $this->status; + } + + public function averageItemsPerSecond(): float + { + return $this->averageItemsPerSecond; + } + + public function totalItemsProcessed(): int + { + return $this->totalItemsProcessed; + } + + public function countSinceLastMeasurement(): int + { + return $this->countSinceLastMeasurement; + } + + public function lastProcessedEventNumber(): int + { + return $this->lastProcessedEventNumber; + } + + public function lastKnownEventNumber(): int + { + return $this->lastKnownEventNumber; + } + + public function readBufferCount(): int + { + return $this->readBufferCount; + } + + public function liveBufferCount(): int + { + return $this->liveBufferCount; + } + + public function retryBufferCount(): int + { + return $this->retryBufferCount; + } + + public function totalInFlightMessages(): int + { + return $this->totalInFlightMessages; + } + + public function parkedMessageUri(): string + { + return $this->parkedMessageUri; + } + + public function getMessagesUri(): string + { + return $this->getMessagesUri; + } +} diff --git a/src/PersistentSubscriptions/PersistentSubscriptionsManager.php b/src/PersistentSubscriptions/PersistentSubscriptionsManager.php new file mode 100644 index 00000000..d4c0c944 --- /dev/null +++ b/src/PersistentSubscriptions/PersistentSubscriptionsManager.php @@ -0,0 +1,38 @@ + + * (c) 2015-2018 Sascha-Oliver Prolic + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Prooph\EventStore\PersistentSubscriptions; + +use Prooph\EventStore\UserCredentials; + +interface PersistentSubscriptionsManager +{ + public function describe( + string $stream, + string $subscriptionName, + ?UserCredentials $userCredentials = null + ): PersistentSubscriptionDetails; + + public function replayParkedMessages( + string $stream, + string $subscriptionName, + ?UserCredentials $userCredentials = null + ): void; + + /** + * @param null|string $stream + * @param null|UserCredentials $userCredentials + * @return PersistentSubscriptionDetails[] + */ + public function list(?string $stream = null, ?UserCredentials $userCredentials = null): array; +} diff --git a/src/Plugin/AbstractPlugin.php b/src/Plugin/AbstractPlugin.php deleted file mode 100644 index c138c793..00000000 --- a/src/Plugin/AbstractPlugin.php +++ /dev/null @@ -1,30 +0,0 @@ - - * (c) 2015-2018 Sascha-Oliver Prolic - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace Prooph\EventStore\Plugin; - -use Prooph\EventStore\ActionEventEmitterEventStore; - -abstract class AbstractPlugin implements Plugin -{ - protected $listenerHandlers = []; - - public function detachFromEventStore(ActionEventEmitterEventStore $eventStore): void - { - foreach ($this->listenerHandlers as $listenerHandler) { - $eventStore->detach($listenerHandler); - } - - $this->listenerHandlers = []; - } -} diff --git a/src/Plugin/Plugin.php b/src/Plugin/Plugin.php deleted file mode 100644 index 35c26411..00000000 --- a/src/Plugin/Plugin.php +++ /dev/null @@ -1,23 +0,0 @@ - - * (c) 2015-2018 Sascha-Oliver Prolic - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace Prooph\EventStore\Plugin; - -use Prooph\EventStore\ActionEventEmitterEventStore; - -interface Plugin -{ - public function attachToEventStore(ActionEventEmitterEventStore $eventStore): void; - - public function detachFromEventStore(ActionEventEmitterEventStore $eventStore): void; -} diff --git a/src/Plugin/UpcastingPlugin.php b/src/Plugin/UpcastingPlugin.php deleted file mode 100644 index 32403892..00000000 --- a/src/Plugin/UpcastingPlugin.php +++ /dev/null @@ -1,60 +0,0 @@ - - * (c) 2015-2018 Sascha-Oliver Prolic - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace Prooph\EventStore\Plugin; - -use Iterator; -use Prooph\Common\Event\ActionEvent; -use Prooph\EventStore\ActionEventEmitterEventStore; -use Prooph\EventStore\Upcasting\Upcaster; -use Prooph\EventStore\Upcasting\UpcastingIterator; - -final class UpcastingPlugin extends AbstractPlugin -{ - public const ACTION_EVENT_PRIORITY = -1000; - - /** - * @var Upcaster - */ - private $upcaster; - - public function __construct(Upcaster $upcaster) - { - $this->upcaster = $upcaster; - } - - public function attachToEventStore(ActionEventEmitterEventStore $eventStore): void - { - $upcaster = function (ActionEvent $actionEvent): void { - $streamEvents = $actionEvent->getParam('streamEvents'); - - if (! $streamEvents instanceof Iterator) { - return; - } - - $actionEvent->setParam('streamEvents', new UpcastingIterator($this->upcaster, $streamEvents)); - }; - - $eventStore->attach( - ActionEventEmitterEventStore::EVENT_LOAD, - $upcaster, - self::ACTION_EVENT_PRIORITY - ); - - $eventStore->attach( - ActionEventEmitterEventStore::EVENT_LOAD_REVERSE, - $upcaster, - self::ACTION_EVENT_PRIORITY - ); - } -} diff --git a/src/Position.php b/src/Position.php new file mode 100644 index 00000000..bcf20212 --- /dev/null +++ b/src/Position.php @@ -0,0 +1,114 @@ + + * (c) 2015-2018 Sascha-Oliver Prolic + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Prooph\EventStore; + +use Prooph\EventStore\Exception\InvalidArgumentException; + +/** + * Transaction File Position + */ +class Position +{ + /** @var int */ + private $commitPosition; + /** @var int */ + private $preparePosition; + + /** @internal */ + public function __construct(int $commitPosition, int $preparePosition) + { + $this->commitPosition = $commitPosition; + $this->preparePosition = $preparePosition; + } + + public static function invalid(): Position + { + return new Position(-1, -1); + } + + public static function headOfTf(): Position + { + return new Position(-1, -1); + } + + public static function start(): Position + { + return new Position(0, 0); + } + + public static function end(): Position + { + return new Position(-1, -1); + } + + public static function parse(string $string): Position + { + if (\strlen($string) !== 32) { + throw new InvalidArgumentException('string too short'); + } + + $commitPosition = (int) \hexdec(\substr($string, 0, 16)); + $preparePosition = (int) \hexdec(\substr($string, 16, 16)); + + return new Position($commitPosition, $preparePosition); + } + + public function commitPosition(): int + { + return $this->commitPosition; + } + + public function preparePosition(): int + { + return $this->preparePosition; + } + + public function asString(): string + { + return \substr('000000000000000' . \dechex($this->commitPosition), -16) + . \substr('000000000000000' . \dechex($this->preparePosition), -16); + } + + public function __toString(): string + { + return 'C:' . $this->commitPosition . '/P:' . $this->preparePosition; + } + + public function equals(Position $other): bool + { + return $this->commitPosition === $other->commitPosition && $this->preparePosition === $other->preparePosition; + } + + public function greater(Position $other): bool + { + return $this->commitPosition > $other->commitPosition + || ($this->commitPosition === $other->commitPosition && $this->preparePosition > $other->preparePosition); + } + + public function smaller(Position $other): bool + { + return $this->commitPosition < $other->commitPosition + || ($this->commitPosition === $other->commitPosition && $this->preparePosition < $other->preparePosition); + } + + public function greaterOrEquals(Position $other): bool + { + return $this->greater($other) || $this->equals($other); + } + + public function smallerOrEquals(Position $other): bool + { + return $this->smaller($other) || $this->equals($other); + } +} diff --git a/src/Projection/AbstractReadModel.php b/src/Projection/AbstractReadModel.php deleted file mode 100644 index 4e37eb0e..00000000 --- a/src/Projection/AbstractReadModel.php +++ /dev/null @@ -1,36 +0,0 @@ - - * (c) 2015-2018 Sascha-Oliver Prolic - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace Prooph\EventStore\Projection; - -abstract class AbstractReadModel implements ReadModel -{ - /** - * @var array - */ - private $stack = []; - - public function stack(string $operation, ...$args): void - { - $this->stack[] = [$operation, $args]; - } - - public function persist(): void - { - foreach ($this->stack as list($operation, $args)) { - $this->{$operation}(...$args); - } - - $this->stack = []; - } -} diff --git a/src/Projection/InMemoryEventStoreProjector.php b/src/Projection/InMemoryEventStoreProjector.php deleted file mode 100644 index 5bfe0112..00000000 --- a/src/Projection/InMemoryEventStoreProjector.php +++ /dev/null @@ -1,536 +0,0 @@ - - * (c) 2015-2018 Sascha-Oliver Prolic - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace Prooph\EventStore\Projection; - -use ArrayIterator; -use Closure; -use Iterator; -use Prooph\Common\Messaging\Message; -use Prooph\EventStore\EventStore; -use Prooph\EventStore\EventStoreDecorator; -use Prooph\EventStore\Exception; -use Prooph\EventStore\InMemoryEventStore; -use Prooph\EventStore\Metadata\MetadataMatcher; -use Prooph\EventStore\NonTransactionalInMemoryEventStore; -use Prooph\EventStore\Stream; -use Prooph\EventStore\StreamName; -use Prooph\EventStore\Util\ArrayCache; - -final class InMemoryEventStoreProjector implements Projector -{ - /** - * @var string - */ - private $name; - - /** - * @var ProjectionStatus - */ - private $status; - - /** - * @var EventStore - */ - private $eventStore; - - /** - * @var EventStore - */ - private $innerEventStore; - - /** - * @var array - */ - private $streamPositions = []; - - /** - * @var array - */ - private $state = []; - - /** - * @var callable|null - */ - private $initCallback; - - /** - * @var Closure|null - */ - private $handler; - - /** - * @var array - */ - private $handlers = []; - - /** - * @var ArrayCache - */ - private $cachedStreamNames; - - /** - * @var boolean - */ - private $isStopped = false; - - /** - * @var ?string - */ - private $currentStreamName = null; - - /** - * @var int - */ - private $sleep; - - /** - * @var bool - */ - private $triggerPcntlSignalDispatch; - - /** - * @var array|null - */ - private $query; - - /** - * @var bool - */ - private $streamCreated = false; - - /** - * @var MetadataMatcher|null - */ - private $metadataMatcher; - - public function __construct( - EventStore $eventStore, - string $name, - int $cacheSize, - int $sleep, - bool $triggerPcntlSignalDispatch = false - ) { - if ($cacheSize < 1) { - throw new Exception\InvalidArgumentException('cache size must be a positive integer'); - } - - if ($sleep < 1) { - throw new Exception\InvalidArgumentException('sleep must be a positive integer'); - } - - if ($triggerPcntlSignalDispatch && ! \extension_loaded('pcntl')) { - throw Exception\ExtensionNotLoadedException::withName('pcntl'); - } - - $this->eventStore = $eventStore; - $this->name = $name; - $this->cachedStreamNames = new ArrayCache($cacheSize); - $this->sleep = $sleep; - $this->status = ProjectionStatus::IDLE(); - $this->triggerPcntlSignalDispatch = $triggerPcntlSignalDispatch; - - while ($eventStore instanceof EventStoreDecorator) { - $eventStore = $eventStore->getInnerEventStore(); - } - - if ( - ! ( - $eventStore instanceof InMemoryEventStore - || $eventStore instanceof NonTransactionalInMemoryEventStore - ) - ) { - throw new Exception\InvalidArgumentException('Unknown event store instance given'); - } - - $this->innerEventStore = $eventStore; - } - - public function init(Closure $callback): Projector - { - if (null !== $this->initCallback) { - throw new Exception\RuntimeException('Projection already initialized'); - } - - $callback = Closure::bind($callback, $this->createHandlerContext($this->currentStreamName)); - - $result = $callback(); - - if (\is_array($result)) { - $this->state = $result; - } - - $this->initCallback = $callback; - - return $this; - } - - public function fromStream(string $streamName, MetadataMatcher $metadataMatcher = null): Projector - { - if (null !== $this->query) { - throw new Exception\RuntimeException('From was already called'); - } - - $this->query['streams'][] = $streamName; - $this->metadataMatcher = $metadataMatcher; - - return $this; - } - - public function fromStreams(string ...$streamNames): Projector - { - if (null !== $this->query) { - throw new Exception\RuntimeException('From was already called'); - } - - foreach ($streamNames as $streamName) { - $this->query['streams'][] = $streamName; - } - - return $this; - } - - public function fromCategory(string $name): Projector - { - if (null !== $this->query) { - throw new Exception\RuntimeException('From was already called'); - } - - $this->query['categories'][] = $name; - - return $this; - } - - public function fromCategories(string ...$names): Projector - { - if (null !== $this->query) { - throw new Exception\RuntimeException('From was already called'); - } - - foreach ($names as $name) { - $this->query['categories'][] = $name; - } - - return $this; - } - - public function fromAll(): Projector - { - if (null !== $this->query) { - throw new Exception\RuntimeException('From was already called'); - } - - $this->query['all'] = true; - - return $this; - } - - public function when(array $handlers): Projector - { - if (null !== $this->handler || ! empty($this->handlers)) { - throw new Exception\RuntimeException('When was already called'); - } - - foreach ($handlers as $eventName => $handler) { - if (! \is_string($eventName)) { - throw new Exception\InvalidArgumentException('Invalid event name given, string expected'); - } - - if (! $handler instanceof Closure) { - throw new Exception\InvalidArgumentException('Invalid handler given, Closure expected'); - } - - $this->handlers[$eventName] = Closure::bind($handler, $this->createHandlerContext($this->currentStreamName)); - } - - return $this; - } - - public function whenAny(Closure $handler): Projector - { - if (null !== $this->handler || ! empty($this->handlers)) { - throw new Exception\RuntimeException('When was already called'); - } - - $this->handler = Closure::bind($handler, $this->createHandlerContext($this->currentStreamName)); - - return $this; - } - - public function stop(): void - { - $this->isStopped = true; - } - - public function getState(): array - { - return $this->state; - } - - public function getName(): string - { - return $this->name; - } - - public function emit(Message $event): void - { - if (! $this->streamCreated || ! $this->eventStore->hasStream(new StreamName($this->name))) { - $this->eventStore->create(new Stream(new StreamName($this->name), new ArrayIterator())); - $this->streamCreated = true; - } - - $this->linkTo($this->name, $event); - } - - public function linkTo(string $streamName, Message $event): void - { - $sn = new StreamName($streamName); - - if ($this->cachedStreamNames->has($streamName)) { - $append = true; - } else { - $this->cachedStreamNames->rollingAppend($streamName); - $append = $this->eventStore->hasStream($sn); - } - - if ($append) { - $this->eventStore->appendTo($sn, new ArrayIterator([$event])); - } else { - $this->eventStore->create(new Stream($sn, new ArrayIterator([$event]))); - } - } - - public function reset(): void - { - $this->streamPositions = []; - - $callback = $this->initCallback; - - try { - $this->eventStore->delete(new StreamName($this->name)); - } catch (Exception\StreamNotFound $exception) { - // ignore - } - - if (\is_callable($callback)) { - $result = $callback(); - - if (\is_array($result)) { - $this->state = $result; - - return; - } - } - - $this->state = []; - } - - public function run(bool $keepRunning = true): void - { - if (null === $this->query - || (null === $this->handler && empty($this->handlers)) - ) { - throw new Exception\RuntimeException('No handlers configured'); - } - - $this->prepareStreamPositions(); - $this->isStopped = false; - $this->status = ProjectionStatus::RUNNING(); - - do { - $singleHandler = null !== $this->handler; - - $eventCounter = 0; - - foreach ($this->streamPositions as $streamName => $position) { - try { - $streamEvents = $this->eventStore->load(new StreamName($streamName), $position + 1, null, $this->metadataMatcher); - } catch (Exception\StreamNotFound $e) { - // ignore - continue; - } - - if ($singleHandler) { - $this->handleStreamWithSingleHandler($streamName, $streamEvents); - } else { - $this->handleStreamWithHandlers($streamName, $streamEvents); - } - - if ($this->isStopped) { - break; - } - } - - if (0 === $eventCounter) { - \usleep($this->sleep); - } - - if ($this->triggerPcntlSignalDispatch) { - \pcntl_signal_dispatch(); - } - } while ($keepRunning && ! $this->isStopped); - - $this->status = ProjectionStatus::IDLE(); - } - - public function delete(bool $deleteEmittedEvents): void - { - if ($deleteEmittedEvents) { - try { - $this->eventStore->delete(new StreamName($this->name)); - } catch (Exception\StreamNotFound $e) { - // ignore - } - } - - $this->streamPositions = []; - } - - private function handleStreamWithSingleHandler(string $streamName, Iterator $events): void - { - $this->currentStreamName = $streamName; - $handler = $this->handler; - - foreach ($events as $event) { - if ($this->triggerPcntlSignalDispatch) { - \pcntl_signal_dispatch(); - } - /* @var Message $event */ - $this->streamPositions[$streamName]++; - - $result = $handler($this->state, $event); - - if (\is_array($result)) { - $this->state = $result; - } - - if ($this->isStopped) { - break; - } - } - } - - private function handleStreamWithHandlers(string $streamName, Iterator $events): void - { - $this->currentStreamName = $streamName; - - foreach ($events as $event) { - if ($this->triggerPcntlSignalDispatch) { - \pcntl_signal_dispatch(); - } - /* @var Message $event */ - $this->streamPositions[$streamName]++; - - if (! isset($this->handlers[$event->messageName()])) { - continue; - } - - $handler = $this->handlers[$event->messageName()]; - $result = $handler($this->state, $event); - - if (\is_array($result)) { - $this->state = $result; - } - - if ($this->isStopped) { - break; - } - } - } - - private function createHandlerContext(?string &$streamName) - { - return new class($this, $streamName) { - /** - * @var Projector - */ - private $projector; - - /** - * @var ?string - */ - private $streamName; - - public function __construct(Projector $projector, ?string &$streamName) - { - $this->projector = $projector; - $this->streamName = &$streamName; - } - - public function stop(): void - { - $this->projector->stop(); - } - - public function linkTo(string $streamName, Message $event): void - { - $this->projector->linkTo($streamName, $event); - } - - public function emit(Message $event): void - { - $this->projector->emit($event); - } - - public function streamName(): ?string - { - return $this->streamName; - } - }; - } - - private function prepareStreamPositions(): void - { - $reflectionProperty = new \ReflectionProperty(\get_class($this->innerEventStore), 'streams'); - $reflectionProperty->setAccessible(true); - - $streamPositions = []; - $streams = \array_keys($reflectionProperty->getValue($this->eventStore)); - - if (isset($this->query['all'])) { - foreach ($streams as $stream) { - if (\substr($stream, 0, 1) === '$') { - // ignore internal streams - continue; - } - $streamPositions[$stream] = 0; - } - - $this->streamPositions = \array_merge($streamPositions, $this->streamPositions); - - return; - } - - if (isset($this->query['categories'])) { - foreach ($streams as $stream) { - foreach ($this->query['categories'] as $category) { - if (\substr($stream, 0, \strlen($category) + 1) === $category . '-') { - $streamPositions[$stream] = 0; - break; - } - } - } - - $this->streamPositions = \array_merge($streamPositions, $this->streamPositions); - - return; - } - - // stream names given - foreach ($this->query['streams'] as $stream) { - $streamPositions[$stream] = 0; - } - - $this->streamPositions = \array_merge($streamPositions, $this->streamPositions); - } -} diff --git a/src/Projection/InMemoryEventStoreQuery.php b/src/Projection/InMemoryEventStoreQuery.php deleted file mode 100644 index 29021e26..00000000 --- a/src/Projection/InMemoryEventStoreQuery.php +++ /dev/null @@ -1,413 +0,0 @@ - - * (c) 2015-2018 Sascha-Oliver Prolic - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace Prooph\EventStore\Projection; - -use Closure; -use Iterator; -use Prooph\Common\Messaging\Message; -use Prooph\EventStore\EventStore; -use Prooph\EventStore\EventStoreDecorator; -use Prooph\EventStore\Exception; -use Prooph\EventStore\InMemoryEventStore; -use Prooph\EventStore\Metadata\MetadataMatcher; -use Prooph\EventStore\NonTransactionalInMemoryEventStore; -use Prooph\EventStore\StreamName; - -final class InMemoryEventStoreQuery implements Query -{ - /** - * @var EventStore - */ - private $eventStore; - - /** - * @var EventStore - */ - private $innerEventStore; - - /** - * @var array - */ - private $streamPositions = []; - - /** - * @var array - */ - private $state = []; - - /** - * @var callable|null - */ - private $initCallback; - - /** - * @var Closure|null - */ - private $handler; - - /** - * @var array - */ - private $handlers = []; - - /** - * @var boolean - */ - private $isStopped = false; - - /** - * @var ?string - */ - private $currentStreamName = null; - - /** - * @var array|null - */ - private $query; - - /** - * @var bool - */ - private $triggerPcntlSignalDispatch; - - /** - * @var MetadataMatcher|null - */ - private $metadataMatcher; - - public function __construct(EventStore $eventStore, bool $triggerPcntlSignalDispatch = false) - { - $this->eventStore = $eventStore; - $this->triggerPcntlSignalDispatch = $triggerPcntlSignalDispatch; - - while ($eventStore instanceof EventStoreDecorator) { - $eventStore = $eventStore->getInnerEventStore(); - } - - if ( - ! ( - $eventStore instanceof InMemoryEventStore - || $eventStore instanceof NonTransactionalInMemoryEventStore - ) - ) { - throw new Exception\InvalidArgumentException('Unknown event store instance given'); - } - - $this->innerEventStore = $eventStore; - } - - public function init(Closure $callback): Query - { - if (null !== $this->initCallback) { - throw new Exception\RuntimeException('Query is already initialized'); - } - - $callback = Closure::bind($callback, $this->createHandlerContext($this->currentStreamName)); - - $result = $callback(); - - if (\is_array($result)) { - $this->state = $result; - } - - $this->initCallback = $callback; - - return $this; - } - - public function fromStream(string $streamName, MetadataMatcher $metadataMatcher = null): Query - { - if (null !== $this->query) { - throw new Exception\RuntimeException('From was already called'); - } - - $this->query['streams'][] = $streamName; - $this->metadataMatcher = $metadataMatcher; - - return $this; - } - - public function fromStreams(string ...$streamNames): Query - { - if (null !== $this->query) { - throw new Exception\RuntimeException('From was already called'); - } - - foreach ($streamNames as $streamName) { - $this->query['streams'][] = $streamName; - } - - return $this; - } - - public function fromCategory(string $name): Query - { - if (null !== $this->query) { - throw new Exception\RuntimeException('From was already called'); - } - - $this->query['categories'][] = $name; - - return $this; - } - - public function fromCategories(string ...$names): Query - { - if (null !== $this->query) { - throw new Exception\RuntimeException('From was already called'); - } - - foreach ($names as $name) { - $this->query['categories'][] = $name; - } - - return $this; - } - - public function fromAll(): Query - { - if (null !== $this->query) { - throw new Exception\RuntimeException('From was already called'); - } - - $this->query['all'] = true; - - return $this; - } - - public function when(array $handlers): Query - { - if (null !== $this->handler || ! empty($this->handlers)) { - throw new Exception\RuntimeException('When was already called'); - } - - foreach ($handlers as $eventName => $handler) { - if (! \is_string($eventName)) { - throw new Exception\InvalidArgumentException('Invalid event name given, string expected'); - } - - if (! $handler instanceof Closure) { - throw new Exception\InvalidArgumentException('Invalid handler given, Closure expected'); - } - - $this->handlers[$eventName] = Closure::bind($handler, $this->createHandlerContext($this->currentStreamName)); - } - - return $this; - } - - public function whenAny(Closure $handler): Query - { - if (null !== $this->handler || ! empty($this->handlers)) { - throw new Exception\RuntimeException('When was already called'); - } - - $this->handler = Closure::bind($handler, $this->createHandlerContext($this->currentStreamName)); - - return $this; - } - - public function reset(): void - { - $this->streamPositions = []; - - $callback = $this->initCallback; - - if (\is_callable($callback)) { - $result = $callback(); - - if (\is_array($result)) { - $this->state = $result; - - return; - } - } - - $this->state = []; - } - - public function run(): void - { - if (null === $this->query - || (null === $this->handler && empty($this->handlers)) - ) { - throw new Exception\RuntimeException('No handlers configured'); - } - - $this->prepareStreamPositions(); - $singleHandler = null !== $this->handler; - - foreach ($this->streamPositions as $streamName => $position) { - try { - $streamEvents = $this->eventStore->load(new StreamName($streamName), $position + 1, null, $this->metadataMatcher); - } catch (Exception\StreamNotFound $e) { - // ignore - continue; - } - - if ($singleHandler) { - $this->handleStreamWithSingleHandler($streamName, $streamEvents); - } else { - $this->handleStreamWithHandlers($streamName, $streamEvents); - } - - if ($this->isStopped) { - break; - } - if ($this->triggerPcntlSignalDispatch) { - \pcntl_signal_dispatch(); - } - } - } - - public function stop(): void - { - $this->isStopped = true; - } - - public function getState(): array - { - return $this->state; - } - - private function handleStreamWithSingleHandler(string $streamName, Iterator $events): void - { - $this->currentStreamName = $streamName; - $handler = $this->handler; - - foreach ($events as $event) { - if ($this->triggerPcntlSignalDispatch) { - \pcntl_signal_dispatch(); - } - - /* @var Message $event */ - $this->streamPositions[$streamName]++; - - $result = $handler($this->state, $event); - - if (\is_array($result)) { - $this->state = $result; - } - - if ($this->isStopped) { - break; - } - } - } - - private function handleStreamWithHandlers(string $streamName, Iterator $events): void - { - $this->currentStreamName = $streamName; - - foreach ($events as $event) { - if ($this->triggerPcntlSignalDispatch) { - \pcntl_signal_dispatch(); - } - - /* @var Message $event */ - $this->streamPositions[$streamName]++; - - if (! isset($this->handlers[$event->messageName()])) { - continue; - } - - $handler = $this->handlers[$event->messageName()]; - $result = $handler($this->state, $event); - - if (\is_array($result)) { - $this->state = $result; - } - - if ($this->isStopped) { - break; - } - } - } - - private function createHandlerContext(?string &$streamName) - { - return new class($this, $streamName) { - /** - * @var Query - */ - private $query; - - /** - * @var ?string - */ - private $streamName; - - public function __construct(Query $query, ?string &$streamName) - { - $this->query = $query; - $this->streamName = &$streamName; - } - - public function stop(): void - { - $this->query->stop(); - } - - public function streamName(): ?string - { - return $this->streamName; - } - }; - } - - private function prepareStreamPositions(): void - { - $reflectionProperty = new \ReflectionProperty(\get_class($this->innerEventStore), 'streams'); - $reflectionProperty->setAccessible(true); - - $streamPositions = []; - $streams = \array_keys($reflectionProperty->getValue($this->eventStore)); - - if (isset($this->query['all'])) { - foreach ($streams as $stream) { - if (\substr($stream, 0, 1) === '$') { - // ignore internal streams - continue; - } - $streamPositions[$stream] = 0; - } - - $this->streamPositions = \array_merge($streamPositions, $this->streamPositions); - - return; - } - - if (isset($this->query['categories'])) { - foreach ($streams as $stream) { - foreach ($this->query['categories'] as $category) { - if (\substr($stream, 0, \strlen($category) + 1) === $category . '-') { - $streamPositions[$stream] = 0; - break; - } - } - } - - $this->streamPositions = \array_merge($streamPositions, $this->streamPositions); - - return; - } - - // stream names given - foreach ($this->query['streams'] as $stream) { - $streamPositions[$stream] = 0; - } - - $this->streamPositions = \array_merge($streamPositions, $this->streamPositions); - } -} diff --git a/src/Projection/InMemoryEventStoreReadModelProjector.php b/src/Projection/InMemoryEventStoreReadModelProjector.php deleted file mode 100644 index 64c4104f..00000000 --- a/src/Projection/InMemoryEventStoreReadModelProjector.php +++ /dev/null @@ -1,535 +0,0 @@ - - * (c) 2015-2018 Sascha-Oliver Prolic - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace Prooph\EventStore\Projection; - -use Closure; -use Iterator; -use Prooph\Common\Messaging\Message; -use Prooph\EventStore\EventStore; -use Prooph\EventStore\EventStoreDecorator; -use Prooph\EventStore\Exception; -use Prooph\EventStore\InMemoryEventStore; -use Prooph\EventStore\Metadata\MetadataMatcher; -use Prooph\EventStore\NonTransactionalInMemoryEventStore; -use Prooph\EventStore\StreamName; -use Prooph\EventStore\Util\ArrayCache; - -final class InMemoryEventStoreReadModelProjector implements ReadModelProjector -{ - /** - * @var string - */ - private $name; - - /** - * @var ProjectionStatus - */ - private $status; - - /** - * @var EventStore - */ - private $eventStore; - - /** - * @var EventStore - */ - private $innerEventStore; - - /** - * @var ReadModel - */ - private $readModel; - - /** - * @var ArrayCache - */ - private $cachedStreamNames; - - /** - * @var int - */ - private $eventCounter = 0; - - /** - * @var int - */ - private $persistBlockSize; - - /** - * @var array - */ - private $streamPositions = []; - - /** - * @var array - */ - private $state = []; - - /** - * @var callable|null - */ - private $initCallback; - - /** - * @var Closure|null - */ - private $handler; - - /** - * @var array - */ - private $handlers = []; - - /** - * @var boolean - */ - private $isStopped = false; - - /** - * @var ?string - */ - private $currentStreamName = null; - - /** - * @var int - */ - private $sleep; - - /** - * @var bool - */ - private $triggerPcntlSignalDispatch; - - /** - * @var array|null - */ - private $query; - - /** - * @var MetadataMatcher|null - */ - private $metadataMatcher; - - public function __construct( - EventStore $eventStore, - string $name, - ReadModel $readModel, - int $cacheSize, - int $persistBlockSize, - int $sleep, - bool $triggerPcntlSignalDispatch = false - ) { - if ($cacheSize < 1) { - throw new Exception\InvalidArgumentException('cache size must be a positive integer'); - } - - if ($persistBlockSize < 1) { - throw new Exception\InvalidArgumentException('persist block size must be a positive integer'); - } - - if ($sleep < 1) { - throw new Exception\InvalidArgumentException('sleep must be a positive integer'); - } - - if ($triggerPcntlSignalDispatch && ! \extension_loaded('pcntl')) { - throw Exception\ExtensionNotLoadedException::withName('pcntl'); - } - - $this->eventStore = $eventStore; - $this->name = $name; - $this->cachedStreamNames = new ArrayCache($cacheSize); - $this->persistBlockSize = $persistBlockSize; - $this->readModel = $readModel; - $this->sleep = $sleep; - $this->status = ProjectionStatus::IDLE(); - $this->triggerPcntlSignalDispatch = $triggerPcntlSignalDispatch; - - while ($eventStore instanceof EventStoreDecorator) { - $eventStore = $eventStore->getInnerEventStore(); - } - - if ( - ! ( - $eventStore instanceof InMemoryEventStore - || $eventStore instanceof NonTransactionalInMemoryEventStore - ) - ) { - throw new Exception\InvalidArgumentException('Unknown event store instance given'); - } - - $this->innerEventStore = $eventStore; - } - - public function init(Closure $callback): ReadModelProjector - { - if (null !== $this->initCallback) { - throw new Exception\RuntimeException('Projector is already initialized'); - } - - $callback = Closure::bind($callback, $this->createHandlerContext($this->currentStreamName)); - - $result = $callback(); - - if (\is_array($result)) { - $this->state = $result; - } - - $this->initCallback = $callback; - - return $this; - } - - public function fromStream(string $streamName, MetadataMatcher $metadataMatcher = null): ReadModelProjector - { - if (null !== $this->query) { - throw new Exception\RuntimeException('From was already called'); - } - - $this->query['streams'][] = $streamName; - $this->metadataMatcher = $metadataMatcher; - - return $this; - } - - public function fromStreams(string ...$streamNames): ReadModelProjector - { - if (null !== $this->query) { - throw new Exception\RuntimeException('From was already called'); - } - - foreach ($streamNames as $streamName) { - $this->query['streams'][] = $streamName; - } - - return $this; - } - - public function fromCategory(string $name): ReadModelProjector - { - if (null !== $this->query) { - throw new Exception\RuntimeException('From was already called'); - } - - $this->query['categories'][] = $name; - - return $this; - } - - public function fromCategories(string ...$names): ReadModelProjector - { - if (null !== $this->query) { - throw new Exception\RuntimeException('From was already called'); - } - - foreach ($names as $name) { - $this->query['categories'][] = $name; - } - - return $this; - } - - public function fromAll(): ReadModelProjector - { - if (null !== $this->query) { - throw new Exception\RuntimeException('From was already called'); - } - - $this->query['all'] = true; - - return $this; - } - - public function when(array $handlers): ReadModelProjector - { - if (null !== $this->handler || ! empty($this->handlers)) { - throw new Exception\RuntimeException('When was already called'); - } - - foreach ($handlers as $eventName => $handler) { - if (! \is_string($eventName)) { - throw new Exception\InvalidArgumentException('Invalid event name given, string expected'); - } - - if (! $handler instanceof Closure) { - throw new Exception\InvalidArgumentException('Invalid handler given, Closure expected'); - } - - $this->handlers[$eventName] = Closure::bind($handler, $this->createHandlerContext($this->currentStreamName)); - } - - return $this; - } - - public function whenAny(Closure $handler): ReadModelProjector - { - if (null !== $this->handler || ! empty($this->handlers)) { - throw new Exception\RuntimeException('When was already called'); - } - - $this->handler = Closure::bind($handler, $this->createHandlerContext($this->currentStreamName)); - - return $this; - } - - public function readModel(): ReadModel - { - return $this->readModel; - } - - public function delete(bool $deleteProjection): void - { - if ($deleteProjection) { - $this->readModel->delete(); - } - - $this->streamPositions = []; - } - - public function run(bool $keepRunning = true): void - { - if (null === $this->query - || (null === $this->handler && empty($this->handlers)) - ) { - throw new Exception\RuntimeException('No handlers configured'); - } - - $this->prepareStreamPositions(); - $this->isStopped = false; - $this->status = ProjectionStatus::RUNNING(); - - if (! $this->readModel->isInitialized()) { - $this->readModel->init(); - } - - do { - $singleHandler = null !== $this->handler; - - $eventCounter = 0; - - foreach ($this->streamPositions as $streamName => $position) { - try { - $streamEvents = $this->eventStore->load(new StreamName($streamName), $position + 1, null, $this->metadataMatcher); - } catch (Exception\StreamNotFound $e) { - // ignore - continue; - } - - if ($singleHandler) { - $this->handleStreamWithSingleHandler($streamName, $streamEvents); - } else { - $this->handleStreamWithHandlers($streamName, $streamEvents); - } - - if ($this->isStopped) { - break; - } - } - - $this->readModel()->persist(); - - if (0 === $eventCounter) { - \usleep($this->sleep); - } - - if ($this->triggerPcntlSignalDispatch) { - \pcntl_signal_dispatch(); - } - } while ($keepRunning && ! $this->isStopped); - - $this->status = ProjectionStatus::IDLE(); - } - - public function stop(): void - { - $this->readModel()->persist(); - $this->isStopped = true; - } - - public function getState(): array - { - return $this->state; - } - - public function getName(): string - { - return $this->name; - } - - public function reset(): void - { - $this->streamPositions = []; - - $this->state = []; - - $this->readModel->reset(); - - $callback = $this->initCallback; - - if (\is_callable($callback)) { - $result = $callback(); - - if (\is_array($result)) { - $this->state = $result; - } - } - } - - private function handleStreamWithSingleHandler(string $streamName, Iterator $events): void - { - $this->currentStreamName = $streamName; - $handler = $this->handler; - - foreach ($events as $event) { - if ($this->triggerPcntlSignalDispatch) { - \pcntl_signal_dispatch(); - } - - /* @var Message $event */ - $this->streamPositions[$streamName]++; - $this->eventCounter++; - - $result = $handler($this->state, $event); - - if (\is_array($result)) { - $this->state = $result; - } - - if ($this->eventCounter === $this->persistBlockSize) { - $this->readModel()->persist(); - $this->eventCounter = 0; - } - - if ($this->isStopped) { - break; - } - } - } - - private function handleStreamWithHandlers(string $streamName, Iterator $events): void - { - $this->currentStreamName = $streamName; - - foreach ($events as $event) { - if ($this->triggerPcntlSignalDispatch) { - \pcntl_signal_dispatch(); - } - /* @var Message $event */ - $this->streamPositions[$streamName]++; - - if (! isset($this->handlers[$event->messageName()])) { - continue; - } - - $this->eventCounter++; - - $handler = $this->handlers[$event->messageName()]; - $result = $handler($this->state, $event); - - if (\is_array($result)) { - $this->state = $result; - } - - if ($this->eventCounter === $this->persistBlockSize) { - $this->readModel()->persist(); - $this->eventCounter = 0; - } - - if ($this->isStopped) { - break; - } - } - } - - private function createHandlerContext(?string &$streamName) - { - return new class($this, $streamName) { - /** - * @var ReadModelProjector - */ - private $projector; - - /** - * @var ?string - */ - private $streamName; - - public function __construct(ReadModelProjector $projector, ?string &$streamName) - { - $this->projector = $projector; - $this->streamName = &$streamName; - } - - public function stop(): void - { - $this->projector->stop(); - } - - public function readModel(): ReadModel - { - return $this->projector->readModel(); - } - - public function streamName(): ?string - { - return $this->streamName; - } - }; - } - - private function prepareStreamPositions(): void - { - $reflectionProperty = new \ReflectionProperty(\get_class($this->innerEventStore), 'streams'); - $reflectionProperty->setAccessible(true); - - $streamPositions = []; - $streams = \array_keys($reflectionProperty->getValue($this->innerEventStore)); - - if (isset($this->query['all'])) { - foreach ($streams as $stream) { - if (\substr($stream, 0, 1) === '$') { - // ignore internal streams - continue; - } - $streamPositions[$stream] = 0; - } - - $this->streamPositions = \array_merge($streamPositions, $this->streamPositions); - - return; - } - - if (isset($this->query['categories'])) { - foreach ($streams as $stream) { - foreach ($this->query['categories'] as $category) { - if (\substr($stream, 0, \strlen($category) + 1) === $category . '-') { - $streamPositions[$stream] = 0; - break; - } - } - } - - $this->streamPositions = \array_merge($streamPositions, $this->streamPositions); - - return; - } - - // stream names given - foreach ($this->query['streams'] as $stream) { - $streamPositions[$stream] = 0; - } - - $this->streamPositions = \array_merge($streamPositions, $this->streamPositions); - } -} diff --git a/src/Projection/InMemoryProjectionManager.php b/src/Projection/InMemoryProjectionManager.php deleted file mode 100644 index 0eeed7ae..00000000 --- a/src/Projection/InMemoryProjectionManager.php +++ /dev/null @@ -1,214 +0,0 @@ - - * (c) 2015-2018 Sascha-Oliver Prolic - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace Prooph\EventStore\Projection; - -use Prooph\EventStore\EventStore; -use Prooph\EventStore\EventStoreDecorator; -use Prooph\EventStore\Exception; -use Prooph\EventStore\InMemoryEventStore; -use Prooph\EventStore\NonTransactionalInMemoryEventStore; - -final class InMemoryProjectionManager implements ProjectionManager -{ - /** - * @var EventStore - */ - private $eventStore; - - /** - * @var array - * - * key = projector name - * value = projector instance - */ - private $projectors = []; - - public function __construct(EventStore $eventStore) - { - $this->eventStore = $eventStore; - - while ($eventStore instanceof EventStoreDecorator) { - $eventStore = $eventStore->getInnerEventStore(); - } - - if ( - ! ( - $eventStore instanceof InMemoryEventStore - || $eventStore instanceof NonTransactionalInMemoryEventStore - ) - ) { - throw new Exception\InvalidArgumentException('Unknown event store instance given'); - } - } - - public function createQuery(array $options = null): Query - { - return new InMemoryEventStoreQuery( - $this->eventStore, - $options[Query::OPTION_PCNTL_DISPATCH] ?? Query::DEFAULT_PCNTL_DISPATCH - ); - } - - public function createProjection( - string $name, - array $options = null - ): Projector { - $projector = new InMemoryEventStoreProjector( - $this->eventStore, - $name, - $options[Projector::OPTION_CACHE_SIZE] ?? Projector::DEFAULT_CACHE_SIZE, - $options[Projector::OPTION_SLEEP] ?? Projector::DEFAULT_SLEEP, - $options[Projector::OPTION_PCNTL_DISPATCH] ?? Projector::DEFAULT_PCNTL_DISPATCH - ); - - if (! isset($this->projectors[$name])) { - $this->projectors[$name] = $projector; - } - - return $projector; - } - - public function createReadModelProjection( - string $name, - ReadModel $readModel, - array $options = null - ): ReadModelProjector { - $projector = new InMemoryEventStoreReadModelProjector( - $this->eventStore, - $name, - $readModel, - $options[ReadModelProjector::OPTION_CACHE_SIZE] ?? ReadModelProjector::DEFAULT_CACHE_SIZE, - $options[ReadModelProjector::OPTION_PERSIST_BLOCK_SIZE] ?? ReadModelProjector::DEFAULT_PERSIST_BLOCK_SIZE, - $options[ReadModelProjector::OPTION_SLEEP] ?? ReadModelProjector::DEFAULT_SLEEP, - $options[ReadModelProjector::OPTION_PCNTL_DISPATCH] ?? ReadModelProjector::DEFAULT_PCNTL_DISPATCH - ); - - if (! isset($this->projectors[$name])) { - $this->projectors[$name] = $projector; - } - - return $projector; - } - - public function deleteProjection(string $name, bool $deleteEmittedEvents): void - { - throw new Exception\RuntimeException('Deleting a projection is not supported in ' . \get_class($this)); - } - - public function resetProjection(string $name): void - { - throw new Exception\RuntimeException('Resetting a projection is not supported in ' . \get_class($this)); - } - - public function stopProjection(string $name): void - { - throw new Exception\RuntimeException('Stopping a projection is not supported in ' . \get_class($this)); - } - - public function fetchProjectionNames(?string $filter, int $limit = 20, int $offset = 0): array - { - if (1 > $limit) { - throw new Exception\OutOfRangeException( - 'Invalid limit "'.$limit.'" given. Must be greater than 0.' - ); - } - - if (0 > $offset) { - throw new Exception\OutOfRangeException( - 'Invalid offset "'.$offset.'" given. Must be greater or equal than 0.' - ); - } - - if (null === $filter) { - $result = \array_keys($this->projectors); - \sort($result, \SORT_STRING); - - return \array_slice($result, $offset, $limit); - } - - if (isset($this->projectors[$filter])) { - return [$filter]; - } - - return []; - } - - public function fetchProjectionNamesRegex(string $regex, int $limit = 20, int $offset = 0): array - { - if (1 > $limit) { - throw new Exception\OutOfRangeException( - 'Invalid limit "'.$limit.'" given. Must be greater than 0.' - ); - } - - if (0 > $offset) { - throw new Exception\OutOfRangeException( - 'Invalid offset "'.$offset.'" given. Must be greater or equal than 0.' - ); - } - - \set_error_handler(function ($errorNo, $errorMsg): void { - throw new Exception\RuntimeException($errorMsg); - }); - - try { - $result = \preg_grep("/$regex/", \array_keys($this->projectors)); - \sort($result, \SORT_STRING); - - return \array_slice($result, $offset, $limit); - } catch (Exception\RuntimeException $e) { - throw new Exception\InvalidArgumentException('Invalid regex pattern given', 0, $e); - } finally { - \restore_error_handler(); - } - } - - public function fetchProjectionStatus(string $name): ProjectionStatus - { - if (! isset($this->projectors[$name])) { - throw Exception\ProjectionNotFound::withName($name); - } - - $projector = $this->projectors[$name]; - - $ref = new \ReflectionProperty(\get_class($projector), 'status'); - $ref->setAccessible(true); - - return $ref->getValue($projector); - } - - public function fetchProjectionStreamPositions(string $name): array - { - if (! isset($this->projectors[$name])) { - throw Exception\ProjectionNotFound::withName($name); - } - - $projector = $this->projectors[$name]; - - $ref = new \ReflectionProperty(\get_class($projector), 'streamPositions'); - $ref->setAccessible(true); - $value = $ref->getValue($projector); - - return (null === $value) ? [] : $value; - } - - public function fetchProjectionState(string $name): array - { - if (! isset($this->projectors[$name])) { - throw Exception\ProjectionNotFound::withName($name); - } - - return $this->projectors[$name]->getState(); - } -} diff --git a/src/Projection/ProjectionManager.php b/src/Projection/ProjectionManager.php deleted file mode 100644 index d56f7c7e..00000000 --- a/src/Projection/ProjectionManager.php +++ /dev/null @@ -1,72 +0,0 @@ - - * (c) 2015-2018 Sascha-Oliver Prolic - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace Prooph\EventStore\Projection; - -use Prooph\EventStore\Exception\ProjectionNotFound; - -interface ProjectionManager -{ - public function createQuery(): Query; - - public function createProjection( - string $name, - array $options = [] - ): Projector; - - public function createReadModelProjection( - string $name, - ReadModel $readModel, - array $options = [] - ): ReadModelProjector; - - /** - * @throws ProjectionNotFound - */ - public function deleteProjection(string $name, bool $deleteEmittedEvents): void; - - /** - * @throws ProjectionNotFound - */ - public function resetProjection(string $name): void; - - /** - * @throws ProjectionNotFound - */ - public function stopProjection(string $name): void; - - /** - * @return string[] - */ - public function fetchProjectionNames(?string $filter, int $limit = 20, int $offset = 0): array; - - /** - * @return string[] - */ - public function fetchProjectionNamesRegex(string $regex, int $limit = 20, int $offset = 0): array; - - /** - * @throws ProjectionNotFound - */ - public function fetchProjectionStatus(string $name): ProjectionStatus; - - /** - * @throws ProjectionNotFound - */ - public function fetchProjectionStreamPositions(string $name): array; - - /** - * @throws ProjectionNotFound - */ - public function fetchProjectionState(string $name): array; -} diff --git a/src/Projection/ProjectionStatus.php b/src/Projection/ProjectionStatus.php deleted file mode 100644 index 39a488a8..00000000 --- a/src/Projection/ProjectionStatus.php +++ /dev/null @@ -1,34 +0,0 @@ - - * (c) 2015-2018 Sascha-Oliver Prolic - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace Prooph\EventStore\Projection; - -use MabeEnum\Enum; - -/** - * @method static ProjectionStatus RUNNING - * @method static ProjectionStatus STOPPING - * @method static ProjectionStatus DELETING - * @method static ProjectionStatus DELETING_INCL_EMITTED_EVENTS - * @method static ProjectionStatus RESETTING - * @method static ProjectionStatus IDLE - */ -final class ProjectionStatus extends Enum -{ - const RUNNING = 'running'; - const STOPPING = 'stopping'; - const DELETING = 'deleting'; - const DELETING_INCL_EMITTED_EVENTS = 'deleting incl emitted events'; - const RESETTING = 'resetting'; - const IDLE = 'idle'; -} diff --git a/src/Projection/Projector.php b/src/Projection/Projector.php deleted file mode 100644 index 824aaed7..00000000 --- a/src/Projection/Projector.php +++ /dev/null @@ -1,90 +0,0 @@ - - * (c) 2015-2018 Sascha-Oliver Prolic - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace Prooph\EventStore\Projection; - -use Closure; -use Prooph\Common\Messaging\Message; - -interface Projector -{ - public const OPTION_CACHE_SIZE = 'cache_size'; - public const OPTION_SLEEP = 'sleep'; - public const OPTION_PERSIST_BLOCK_SIZE = 'persist_block_size'; - public const OPTION_LOCK_TIMEOUT_MS = 'lock_timeout_ms'; - public const OPTION_PCNTL_DISPATCH = 'trigger_pcntl_dispatch'; - public const OPTION_UPDATE_LOCK_THRESHOLD = 'update_lock_threshold'; - - public const DEFAULT_CACHE_SIZE = 1000; - public const DEFAULT_SLEEP = 100000; - public const DEFAULT_PERSIST_BLOCK_SIZE = 1000; - public const DEFAULT_LOCK_TIMEOUT_MS = 1000; - public const DEFAULT_PCNTL_DISPATCH = false; - public const DEFAULT_UPDATE_LOCK_THRESHOLD = 0; - - /** - * The callback has to return an array - */ - public function init(Closure $callback): Projector; - - public function fromStream(string $streamName): Projector; - - public function fromStreams(string ...$streamNames): Projector; - - public function fromCategory(string $name): Projector; - - public function fromCategories(string ...$names): Projector; - - public function fromAll(): Projector; - - /** - * For example: - * - * when([ - * 'UserCreated' => function (array $state, Message $event) { - * $state['count']++; - * return $state; - * }, - * 'UserDeleted' => function (array $state, Message $event) { - * $state['count']--; - * return $state; - * } - * ]) - */ - public function when(array $handlers): Projector; - - /** - * For example: - * function(array $state, Message $event) { - * $state['count']++; - * return $state; - * } - */ - public function whenAny(Closure $closure): Projector; - - public function reset(): void; - - public function stop(): void; - - public function getState(): array; - - public function getName(): string; - - public function emit(Message $event): void; - - public function linkTo(string $streamName, Message $event): void; - - public function delete(bool $deleteEmittedEvents): void; - - public function run(bool $keepRunning = true): void; -} diff --git a/src/Projection/Query.php b/src/Projection/Query.php deleted file mode 100644 index a8c11e5a..00000000 --- a/src/Projection/Query.php +++ /dev/null @@ -1,71 +0,0 @@ - - * (c) 2015-2018 Sascha-Oliver Prolic - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace Prooph\EventStore\Projection; - -use Closure; - -interface Query -{ - public const OPTION_PCNTL_DISPATCH = 'trigger_pcntl_dispatch'; - - public const DEFAULT_PCNTL_DISPATCH = false; - - /** - * The callback has to return an array - */ - public function init(Closure $callback): Query; - - public function fromStream(string $streamName): Query; - - public function fromStreams(string ...$streamNames): Query; - - public function fromCategory(string $name): Query; - - public function fromCategories(string ...$names): Query; - - public function fromAll(): Query; - - /** - * For example: - * - * when([ - * 'UserCreated' => function (array $state, Message $event) { - * $state->count++; - * return $state; - * }, - * 'UserDeleted' => function (array $state, Message $event) { - * $state->count--; - * return $state; - * } - * ]) - */ - public function when(array $handlers): Query; - - /** - * For example: - * function(array $state, Message $event) { - * $state->count++; - * return $state; - * } - */ - public function whenAny(Closure $closure): Query; - - public function reset(): void; - - public function run(): void; - - public function stop(): void; - - public function getState(): array; -} diff --git a/src/Projection/ReadModelProjector.php b/src/Projection/ReadModelProjector.php deleted file mode 100644 index bd3803f3..00000000 --- a/src/Projection/ReadModelProjector.php +++ /dev/null @@ -1,87 +0,0 @@ - - * (c) 2015-2018 Sascha-Oliver Prolic - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace Prooph\EventStore\Projection; - -use Closure; - -interface ReadModelProjector -{ - public const OPTION_CACHE_SIZE = 'cache_size'; - public const OPTION_SLEEP = 'sleep'; - public const OPTION_PERSIST_BLOCK_SIZE = 'persist_block_size'; - public const OPTION_LOCK_TIMEOUT_MS = 'lock_timeout_ms'; - public const OPTION_PCNTL_DISPATCH = 'trigger_pcntl_dispatch'; - public const OPTION_UPDATE_LOCK_THRESHOLD = 'update_lock_threshold'; - - public const DEFAULT_CACHE_SIZE = 1000; - public const DEFAULT_SLEEP = 100000; - public const DEFAULT_PERSIST_BLOCK_SIZE = 1000; - public const DEFAULT_LOCK_TIMEOUT_MS = 1000; - public const DEFAULT_PCNTL_DISPATCH = false; - public const DEFAULT_UPDATE_LOCK_THRESHOLD = 0; - - /** - * The callback has to return an array - */ - public function init(Closure $callback): ReadModelProjector; - - public function fromStream(string $streamName): ReadModelProjector; - - public function fromStreams(string ...$streamNames): ReadModelProjector; - - public function fromCategory(string $name): ReadModelProjector; - - public function fromCategories(string ...$names): ReadModelProjector; - - public function fromAll(): ReadModelProjector; - - /** - * For example: - * - * when([ - * 'UserCreated' => function (array $state, Message $event) { - * $state->count++; - * return $state; - * }, - * 'UserDeleted' => function (array $state, Message $event) { - * $state->count--; - * return $state; - * } - * ]) - */ - public function when(array $handlers): ReadModelProjector; - - /** - * For example: - * function(array $state, Message $event) { - * $state->count++; - * return $state; - * } - */ - public function whenAny(Closure $closure): ReadModelProjector; - - public function reset(): void; - - public function stop(): void; - - public function getState(): array; - - public function getName(): string; - - public function delete(bool $deleteProjection): void; - - public function run(bool $keepRunning = true): void; - - public function readModel(): ReadModel; -} diff --git a/src/Projections/AsyncProjectionsManager.php b/src/Projections/AsyncProjectionsManager.php new file mode 100644 index 00000000..d90a22ed --- /dev/null +++ b/src/Projections/AsyncProjectionsManager.php @@ -0,0 +1,176 @@ + + * (c) 2015-2018 Sascha-Oliver Prolic + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Prooph\EventStore\Projections; + +use Amp\Promise; +use Prooph\EventStore\UserCredentials; + +interface AsyncProjectionsManager +{ + /** + * Asynchronously enables a projection + */ + public function enableAsync(string $name, ?UserCredentials $userCredentials = null): Promise; + + /** + * Asynchronously aborts and disables a projection without writing a checkpoint + */ + public function disableAsync(string $name, ?UserCredentials $userCredentials = null): Promise; + + /** + * Asynchronously disables a projection + */ + public function abortAsync(string $name, ?UserCredentials $userCredentials = null): Promise; + + /** + * Asynchronously creates a one-time query + */ + public function createOneTimeAsync( + string $query, + ?UserCredentials $userCredentials = null + ): Promise; + + /** + * Asynchronously creates a one-time query + */ + public function createTransientAsync( + string $name, + string $query, + ?UserCredentials $userCredentials = null + ): Promise; + + /** + * Asynchronously creates a continuous projection + */ + public function createContinuousAsync( + string $name, + string $query, + bool $trackEmittedStreams = false, + ?UserCredentials $userCredentials = null + ): Promise; + + /** + * Asynchronously lists all projections + * + * @return Promise + */ + public function listAllAsync(?UserCredentials $userCredentials = null): Promise; + + /** + * Asynchronously lists all one-time projections + * + * @return Promise + */ + public function listOneTimeAsync(?UserCredentials $userCredentials = null): Promise; + + /** + * Asynchronously lists this status of all continuous projections + * + * @return Promise + */ + public function listContinuousAsync(?UserCredentials $userCredentials = null): Promise; + + /** + * Asynchronously gets the status of a projection + * + * returns String of JSON containing projection status + * + * @return Promise + */ + public function getStatusAsync(string $name, ?UserCredentials $userCredentials = null): Promise; + + /** + * Asynchronously gets the state of a projection. + * + * returns String of JSON containing projection state + * + * @return Promise + */ + public function getStateAsync(string $name, ?UserCredentials $userCredentials = null): Promise; + + /** + * Asynchronously gets the state of a projection for a specified partition + * + * returns String of JSON containing projection state + * + * @return Promise + */ + public function getPartitionStateAsync( + string $name, + string $partition, + ?UserCredentials $userCredentials = null + ): Promise; + + /** + * Asynchronously gets the resut of a projection + * + * returns String of JSON containing projection result + * + * @return Promise + */ + public function getResultAsync(string $name, ?UserCredentials $userCredentials = null): Promise; + + /** + * Asynchronously gets the result of a projection for a specified partition + * + * returns String of JSON containing projection result + * + * @return Promise + */ + public function getPartitionResultAsync( + string $name, + string $partition, + ?UserCredentials $userCredentials = null + ): Promise; + + /** + * Asynchronously gets the statistics of a projection + * + * returns String of JSON containing projection statistics + * + * @return Promise + */ + public function getStatisticsAsync(string $name, ?UserCredentials $userCredentials = null): Promise; + + /** + * Asynchronously gets the status of a query + * + * @return Promise + */ + public function getQueryAsync(string $name, ?UserCredentials $userCredentials = null): Promise; + + /** + * Asynchronously updates the definition of a query + */ + public function updateQueryAsync( + string $name, + string $query, + bool $emitEnabled = false, + ?UserCredentials $userCredentials = null + ): Promise; + + /** + * Asynchronously deletes a projection + */ + public function deleteAsync( + string $name, + bool $deleteEmittedStreams = false, + ?UserCredentials $userCredentials = null + ): Promise; + + /** + * Asynchronously resets a projection + */ + public function resetAsync(string $name, ?UserCredentials $userCredentials = null): Promise; +} diff --git a/src/Projections/AsyncQueryManager.php b/src/Projections/AsyncQueryManager.php new file mode 100644 index 00000000..bd0d6b1b --- /dev/null +++ b/src/Projections/AsyncQueryManager.php @@ -0,0 +1,43 @@ + + * (c) 2015-2018 Sascha-Oliver Prolic + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Prooph\EventStore\Projections; + +use Amp\Promise; +use Prooph\EventStore\UserCredentials; + +interface AsyncQueryManager +{ + /** + * Asynchronously executes a query + * + * Creates a new transient projection and polls its status until it is Completed + * + * returns String of JSON containing query result + * + * @param string $name A name for the query + * @param string $query The source code for the query + * @param int $initialPollingDelay Initial time to wait between polling for projection status + * @param int $maximumPollingDelay Maximum time to wait between polling for projection status + * @param UserCredentials|null $userCredentials Credentials for a user with permission to create a query + * + * @return Promise + */ + public function executeAsync( + string $name, + string $query, + int $initialPollingDelay, + int $maximumPollingDelay, + ?UserCredentials $userCredentials = null + ): Promise; +} diff --git a/src/Projections/ProjectionDetails.php b/src/Projections/ProjectionDetails.php new file mode 100644 index 00000000..54b2385c --- /dev/null +++ b/src/Projections/ProjectionDetails.php @@ -0,0 +1,247 @@ + + * (c) 2015-2018 Sascha-Oliver Prolic + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Prooph\EventStore\Projections; + +final class ProjectionDetails +{ + /** @var int */ + private $coreProcessingTime; + /** @var int */ + private $version; + /** @var int */ + private $epoch; + /** @var string */ + private $effectiveName; + /** @var int */ + private $writesInProgress; + /** @var int */ + private $readsInProgress; + /** @var int */ + private $partitionsCached; + /** @var string */ + private $status; + /** @var string */ + private $stateReason; + /** @var string */ + private $name; + /** @var string */ + private $mode; + /** @var string */ + private $position; + /** @var float */ + private $progress; + /** @var string */ + private $lastCheckpoint; + /** @var int */ + private $eventsProcessedAfterRestart; + /** @var string */ + private $statusUrl; + /** @var string */ + private $stateUrl; + /** @var string */ + private $resultUrl; + /** @var string */ + private $queryUrl; + /** @var string */ + private $enableCommandUrl; + /** @var string */ + private $disableCommandUrl; + /** @var string */ + private $checkpointStatus; + /** @var int */ + private $bufferedEvents; + /** @var int */ + private $writePendingEventsBeforeCheckpoint; + /** @var int */ + private $writePendingEventsAfterCheckpoint; + + public function __construct( + int $coreProcessingTime, + int $version, + int $epoch, + string $effectiveName, + int $writesInProgress, + int $readsInProgress, + int $partitionsCached, + string $status, + string $stateReason, + string $name, + string $mode, + string $position, + float $progress, + string $lastCheckpoint, + int $eventsProcessedAfterRestart, + string $statusUrl, + string $stateUrl, + string $resultUrl, + string $queryUrl, + string $enableCommandUrl, + string $disableCommandUrl, + string $checkpointStatus, + int $bufferedEvents, + int $writePendingEventsBeforeCheckpoint, + int $writePendingEventsAfterCheckpoint + ) { + $this->coreProcessingTime = $coreProcessingTime; + $this->version = $version; + $this->epoch = $epoch; + $this->effectiveName = $effectiveName; + $this->writesInProgress = $writesInProgress; + $this->readsInProgress = $readsInProgress; + $this->partitionsCached = $partitionsCached; + $this->status = $status; + $this->stateReason = $stateReason; + $this->name = $name; + $this->mode = $mode; + $this->position = $position; + $this->progress = $progress; + $this->lastCheckpoint = $lastCheckpoint; + $this->eventsProcessedAfterRestart = $eventsProcessedAfterRestart; + $this->statusUrl = $statusUrl; + $this->stateUrl = $stateUrl; + $this->resultUrl = $resultUrl; + $this->queryUrl = $queryUrl; + $this->enableCommandUrl = $enableCommandUrl; + $this->disableCommandUrl = $disableCommandUrl; + $this->checkpointStatus = $checkpointStatus; + $this->bufferedEvents = $bufferedEvents; + $this->writePendingEventsBeforeCheckpoint = $writePendingEventsBeforeCheckpoint; + $this->writePendingEventsAfterCheckpoint = $writePendingEventsAfterCheckpoint; + } + + public function coreProcessingTime(): int + { + return $this->coreProcessingTime; + } + + public function version(): int + { + return $this->version; + } + + public function epoch(): int + { + return $this->epoch; + } + + public function effectiveName(): string + { + return $this->effectiveName; + } + + public function writesInProgress(): int + { + return $this->writesInProgress; + } + + public function readsInProgress(): int + { + return $this->readsInProgress; + } + + public function partitionsCached(): int + { + return $this->partitionsCached; + } + + public function status(): string + { + return $this->status; + } + + public function stateReason(): string + { + return $this->stateReason; + } + + public function name(): string + { + return $this->name; + } + + public function mode(): string + { + return $this->mode; + } + + public function position(): string + { + return $this->position; + } + + public function progress(): float + { + return $this->progress; + } + + public function lastCheckpoint(): string + { + return $this->lastCheckpoint; + } + + public function eventsProcessedAfterRestart(): int + { + return $this->eventsProcessedAfterRestart; + } + + public function statusUrl(): string + { + return $this->statusUrl; + } + + public function stateUrl(): string + { + return $this->stateUrl; + } + + public function resultUrl(): string + { + return $this->resultUrl; + } + + public function queryUrl(): string + { + return $this->queryUrl; + } + + public function enableCommandUrl(): string + { + return $this->enableCommandUrl; + } + + public function disableCommandUrl(): string + { + return $this->disableCommandUrl; + } + + public function checkpointStatus(): string + { + return $this->checkpointStatus; + } + + public function bufferedEvents(): int + { + return $this->bufferedEvents; + } + + public function writePendingEventsBeforeCheckpoint(): int + { + return $this->writePendingEventsBeforeCheckpoint; + } + + public function writePendingEventsAfterCheckpoint(): int + { + return $this->writePendingEventsAfterCheckpoint; + } +} diff --git a/src/Projections/ProjectionsManager.php b/src/Projections/ProjectionsManager.php new file mode 100644 index 00000000..9b6fb582 --- /dev/null +++ b/src/Projections/ProjectionsManager.php @@ -0,0 +1,176 @@ + + * (c) 2015-2018 Sascha-Oliver Prolic + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Prooph\EventStore\Projections; + +use Amp\Promise; +use Prooph\EventStore\UserCredentials; + +interface ProjectionsManager +{ + /** + * Synchronously enables a projection + */ + public function enable(string $name, ?UserCredentials $userCredentials = null): void; + + /** + * Synchronously aborts and disables a projection without writing a checkpoint + */ + public function disable(string $name, ?UserCredentials $userCredentials = null): void; + + /** + * Synchronously disables a projection + */ + public function abort(string $name, ?UserCredentials $userCredentials = null): void; + + /** + * Synchronously creates a one-time query + */ + public function createOneTime( + string $query, + ?UserCredentials $userCredentials = null + ): void; + + /** + * Synchronously creates a one-time query + */ + public function createTransient( + string $name, + string $query, + ?UserCredentials $userCredentials = null + ): void; + + /** + * Synchronously creates a continuous projection + */ + public function createContinuous( + string $name, + string $query, + bool $trackEmittedStreams = false, + ?UserCredentials $userCredentials = null + ): void; + + /** + * Synchronously lists all projections + * + * @return Promise + */ + public function listAll(?UserCredentials $userCredentials = null): void; + + /** + * Synchronously lists all one-time projections + * + * @return Promise + */ + public function listOneTime(?UserCredentials $userCredentials = null): void; + + /** + * Synchronously lists this status of all continuous projections + * + * @return Promise + */ + public function listContinuous(?UserCredentials $userCredentials = null): void; + + /** + * Synchronously gets the status of a projection + * + * returns String of JSON containing projection status + * + * @return Promise + */ + public function getStatus(string $name, ?UserCredentials $userCredentials = null): void; + + /** + * Synchronously gets the state of a projection. + * + * returns String of JSON containing projection state + * + * @return Promise + */ + public function getState(string $name, ?UserCredentials $userCredentials = null): void; + + /** + * Synchronously gets the state of a projection for a specified partition + * + * returns String of JSON containing projection state + * + * @return Promise + */ + public function getPartitionState( + string $name, + string $partition, + ?UserCredentials $userCredentials = null + ): void; + + /** + * Synchronously gets the resut of a projection + * + * returns String of JSON containing projection result + * + * @return Promise + */ + public function getResult(string $name, ?UserCredentials $userCredentials = null): void; + + /** + * Synchronously gets the result of a projection for a specified partition + * + * returns String of JSON containing projection result + * + * @return Promise + */ + public function getPartitionResult( + string $name, + string $partition, + ?UserCredentials $userCredentials = null + ): void; + + /** + * Synchronously gets the statistics of a projection + * + * returns String of JSON containing projection statistics + * + * @return Promise + */ + public function getStatistics(string $name, ?UserCredentials $userCredentials = null): void; + + /** + * Synchronously gets the status of a query + * + * @return Promise + */ + public function getQuery(string $name, ?UserCredentials $userCredentials = null): void; + + /** + * Synchronously updates the definition of a query + */ + public function updateQuery( + string $name, + string $query, + bool $emitEnabled = false, + ?UserCredentials $userCredentials = null + ): void; + + /** + * Synchronously deletes a projection + */ + public function delete( + string $name, + bool $deleteEmittedStreams = false, + ?UserCredentials $userCredentials = null + ): void; + + /** + * Synchronously resets a projection + */ + public function reset(string $name, ?UserCredentials $userCredentials = null): void; +} diff --git a/src/Projections/QueryManager.php b/src/Projections/QueryManager.php new file mode 100644 index 00000000..22904242 --- /dev/null +++ b/src/Projections/QueryManager.php @@ -0,0 +1,42 @@ + + * (c) 2015-2018 Sascha-Oliver Prolic + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Prooph\EventStore\Projections; + +use Prooph\EventStore\UserCredentials; + +interface QueryManager +{ + /** + * Synchronously executes a query + * + * Creates a new transient projection and polls its status until it is Completed + * + * returns String of JSON containing query result + * + * @param string $name A name for the query + * @param string $query The source code for the query + * @param int $initialPollingDelay Initial time to wait between polling for projection status + * @param int $maximumPollingDelay Maximum time to wait between polling for projection status + * @param UserCredentials|null $userCredentials Credentials for a user with permission to create a query + * + * @return string + */ + public function execute( + string $name, + string $query, + int $initialPollingDelay, + int $maximumPollingDelay, + ?UserCredentials $userCredentials = null + ): string; +} diff --git a/src/RawStreamMetadataResult.php b/src/RawStreamMetadataResult.php new file mode 100644 index 00000000..9f4c9c06 --- /dev/null +++ b/src/RawStreamMetadataResult.php @@ -0,0 +1,60 @@ + + * (c) 2015-2018 Sascha-Oliver Prolic + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Prooph\EventStore; + +use Prooph\EventStore\Exception\InvalidArgumentException; + +class RawStreamMetadataResult +{ + /** @var string */ + private $stream; + /** @var bool */ + private $isStreamDeleted; + /** @var int */ + private $metastreamVersion; + /** @var string */ + private $streamMetadata; + + public function __construct(string $stream, bool $isStreamDeleted, int $metastreamVersion, string $streamMetadata) + { + if (empty($stream)) { + throw new InvalidArgumentException('Stream cannot be empty string'); + } + + $this->stream = $stream; + $this->isStreamDeleted = $isStreamDeleted; + $this->metastreamVersion = $metastreamVersion; + $this->streamMetadata = $streamMetadata; + } + + public function stream(): string + { + return $this->stream; + } + + public function isStreamDeleted(): bool + { + return $this->isStreamDeleted; + } + + public function metastreamVersion(): int + { + return $this->metastreamVersion; + } + + public function streamMetadata(): string + { + return $this->streamMetadata; + } +} diff --git a/src/ReadDirection.php b/src/ReadDirection.php new file mode 100644 index 00000000..4c632ba7 --- /dev/null +++ b/src/ReadDirection.php @@ -0,0 +1,86 @@ + + * (c) 2015-2018 Sascha-Oliver Prolic + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Prooph\EventStore; + +use Prooph\EventStore\Exception\InvalidArgumentException; + +class ReadDirection +{ + public const OPTIONS = [ + 'Forward' => 0, + 'Backward' => 1, + ]; + + public const FORWARD = 0; + public const BACKWARD = 1; + + private $name; + private $value; + + private function __construct(string $name) + { + $this->name = $name; + $this->value = self::OPTIONS[$name]; + } + + public static function forward(): self + { + return new self('Forward'); + } + + public static function backward(): self + { + return new self('Backward'); + } + + public static function byName(string $value): self + { + if (! isset(self::OPTIONS[$value])) { + throw new InvalidArgumentException('Unknown enum name given'); + } + + return self::{$value}(); + } + + public static function byValue($value): self + { + foreach (self::OPTIONS as $name => $v) { + if ($v === $value) { + return self::{$name}(); + } + } + + throw new InvalidArgumentException('Unknown enum value given'); + } + + public function equals(ReadDirection $other): bool + { + return \get_class($this) === \get_class($other) && $this->name === $other->name; + } + + public function name(): string + { + return $this->name; + } + + public function value() + { + return $this->value; + } + + public function __toString(): string + { + return $this->name; + } +} diff --git a/src/ReadOnlyEventStore.php b/src/ReadOnlyEventStore.php deleted file mode 100644 index bf4b8c19..00000000 --- a/src/ReadOnlyEventStore.php +++ /dev/null @@ -1,68 +0,0 @@ - - * (c) 2015-2018 Sascha-Oliver Prolic - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace Prooph\EventStore; - -use Iterator; -use Prooph\EventStore\Metadata\MetadataMatcher; - -interface ReadOnlyEventStore -{ - public function fetchStreamMetadata(StreamName $streamName): array; - - public function hasStream(StreamName $streamName): bool; - - public function load( - StreamName $streamName, - int $fromNumber = 1, - int $count = null, - MetadataMatcher $metadataMatcher = null - ): Iterator; - - public function loadReverse( - StreamName $streamName, - int $fromNumber = null, - int $count = null, - MetadataMatcher $metadataMatcher = null - ): Iterator; - - /** - * @return StreamName[] - */ - public function fetchStreamNames( - ?string $filter, - ?MetadataMatcher $metadataMatcher, - int $limit = 20, - int $offset = 0 - ): array; - - /** - * @return StreamName[] - */ - public function fetchStreamNamesRegex( - string $filter, - ?MetadataMatcher $metadataMatcher, - int $limit = 20, - int $offset = 0 - ): array; - - /** - * @return string[] - */ - public function fetchCategoryNames(?string $filter, int $limit = 20, int $offset = 0): array; - - /** - * @return string[] - */ - public function fetchCategoryNamesRegex(string $filter, int $limit = 20, int $offset = 0): array; -} diff --git a/src/ReadOnlyEventStoreWrapper.php b/src/ReadOnlyEventStoreWrapper.php deleted file mode 100644 index d8c0ea88..00000000 --- a/src/ReadOnlyEventStoreWrapper.php +++ /dev/null @@ -1,86 +0,0 @@ - - * (c) 2015-2018 Sascha-Oliver Prolic - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace Prooph\EventStore; - -use Iterator; -use Prooph\EventStore\Metadata\MetadataMatcher; - -final class ReadOnlyEventStoreWrapper implements ReadOnlyEventStore -{ - /** - * @var EventStore - */ - private $eventStore; - - public function __construct(EventStore $eventStore) - { - $this->eventStore = $eventStore; - } - - public function fetchStreamMetadata(StreamName $streamName): array - { - return $this->eventStore->fetchStreamMetadata($streamName); - } - - public function hasStream(StreamName $streamName): bool - { - return $this->eventStore->hasStream($streamName); - } - - public function load( - StreamName $streamName, - int $fromNumber = 1, - int $count = null, - MetadataMatcher $metadataMatcher = null - ): Iterator { - return $this->eventStore->load($streamName, $fromNumber, $count, $metadataMatcher); - } - - public function loadReverse( - StreamName $streamName, - int $fromNumber = null, - int $count = null, - MetadataMatcher $metadataMatcher = null - ): Iterator { - return $this->eventStore->loadReverse($streamName, $fromNumber, $count, $metadataMatcher); - } - - public function fetchStreamNames( - ?string $filter, - ?MetadataMatcher $metadataMatcher, - int $limit = 20, - int $offset = 0 - ): array { - return $this->eventStore->fetchStreamNames($filter, $metadataMatcher, $limit, $offset); - } - - public function fetchStreamNamesRegex( - string $filter, - ?MetadataMatcher $metadataMatcher, - int $limit = 20, - int $offset = 0 - ): array { - return $this->eventStore->fetchStreamNamesRegex($filter, $metadataMatcher, $limit, $offset); - } - - public function fetchCategoryNames(?string $filter, int $limit = 20, int $offset = 0): array - { - return $this->eventStore->fetchCategoryNames($filter, $limit, $offset); - } - - public function fetchCategoryNamesRegex(string $filter, int $limit = 20, int $offset = 0): array - { - return $this->eventStore->fetchCategoryNamesRegex($filter, $limit, $offset); - } -} diff --git a/src/RecordedEvent.php b/src/RecordedEvent.php new file mode 100644 index 00000000..0aa15fea --- /dev/null +++ b/src/RecordedEvent.php @@ -0,0 +1,97 @@ + + * (c) 2015-2018 Sascha-Oliver Prolic + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Prooph\EventStore; + +use DateTimeImmutable; + +class RecordedEvent +{ + /** @var string */ + protected $eventStreamId; + /** @var int */ + protected $eventNumber; + /** @var EventId */ + protected $eventId; + /** @var string */ + protected $eventType; + /** @var bool */ + protected $isJson; + /** @var string */ + protected $data; + /** @var string */ + protected $metadata; + /** @var DateTimeImmutable */ + protected $created; + + /** @internal */ + public function __construct( + string $eventStreamId, + int $eventNumber, + EventId $eventId, + string $eventType, + bool $isJson, + string $data, + string $metadata, + DateTimeImmutable $created + ) { + $this->eventStreamId = $eventStreamId; + $this->eventNumber = $eventNumber; + $this->eventId = $eventId; + $this->eventType = $eventType; + $this->isJson = $isJson; + $this->data = $data; + $this->metadata = $metadata; + $this->created = $created; + } + + public function eventStreamId(): string + { + return $this->eventStreamId; + } + + public function eventNumber(): int + { + return $this->eventNumber; + } + + public function eventId(): EventId + { + return $this->eventId; + } + + public function eventType(): string + { + return $this->eventType; + } + + public function isJson(): bool + { + return $this->isJson; + } + + public function data(): string + { + return $this->data; + } + + public function metadata(): string + { + return $this->metadata; + } + + public function created(): DateTimeImmutable + { + return $this->created; + } +} diff --git a/src/ResolvedEvent.php b/src/ResolvedEvent.php new file mode 100644 index 00000000..ac9015fe --- /dev/null +++ b/src/ResolvedEvent.php @@ -0,0 +1,96 @@ + + * (c) 2015-2018 Sascha-Oliver Prolic + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Prooph\EventStore; + +use Prooph\EventStore\Internal\ResolvedEvent as InternalResolvedEvent; + +/** + * A structure representing a single event or a resolved link event. + */ +class ResolvedEvent implements InternalResolvedEvent +{ + /** + * The event, or the resolved link event if this is a link event + * @var RecordedEvent|null + */ + private $event; + /** + * The link event if this ResolvedEvent is a link event. + * @var RecordedEvent|null + */ + private $link; + /** + * Returns the event that was read or which triggered the subscription. + * + * If this ResolvedEvent represents a link event, the Link + * will be the OriginalEvent otherwise it will be the event. + * @var RecordedEvent|null + */ + private $originalEvent; + /** + * Indicates whether this ResolvedEvent is a resolved link event. + * @var bool + */ + private $isResolved; + /** + * The logical position of the OriginalEvent + * @var Position|null + */ + private $originalPosition; + + /** @internal */ + public function __construct(?RecordedEvent $event, ?RecordedEvent $link, ?Position $originalPosition) + { + $this->event = $event; + $this->link = $link; + $this->originalEvent = $link ?? $event; + $this->isResolved = null !== $link && null !== $event; + $this->originalPosition = $originalPosition; + } + + public function event(): ?RecordedEvent + { + return $this->event; + } + + public function link(): ?RecordedEvent + { + return $this->link; + } + + public function originalEvent(): ?RecordedEvent + { + return $this->originalEvent; + } + + public function isResolved(): bool + { + return $this->isResolved; + } + + public function originalPosition(): ?Position + { + return $this->originalPosition; + } + + public function originalStreamName(): string + { + return $this->originalEvent->eventStreamId(); + } + + public function originalEventNumber(): int + { + return $this->originalEvent->eventNumber(); + } +} diff --git a/src/SliceReadStatus.php b/src/SliceReadStatus.php new file mode 100644 index 00000000..ca0a94b5 --- /dev/null +++ b/src/SliceReadStatus.php @@ -0,0 +1,93 @@ + + * (c) 2015-2018 Sascha-Oliver Prolic + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Prooph\EventStore; + +use Prooph\EventStore\Exception\InvalidArgumentException; + +class SliceReadStatus +{ + public const OPTIONS = [ + 'Success' => 0, + 'StreamNotFound' => 1, + 'StreamDeleted' => 2, + ]; + + public const SUCCESS = 0; + public const STREAM_NOT_FOUND = 1; + public const STREAM_DELETED = 2; + + private $name; + private $value; + + private function __construct(string $name) + { + $this->name = $name; + $this->value = self::OPTIONS[$name]; + } + + public static function success(): self + { + return new self('Success'); + } + + public static function streamNotFound(): self + { + return new self('StreamNotFound'); + } + + public static function streamDeleted(): self + { + return new self('StreamDeleted'); + } + + public static function byName(string $value): self + { + if (! isset(self::OPTIONS[$value])) { + throw new InvalidArgumentException('Unknown enum name given'); + } + + return self::{$value}(); + } + + public static function byValue($value): self + { + foreach (self::OPTIONS as $name => $v) { + if ($v === $value) { + return self::{$name}(); + } + } + + throw new InvalidArgumentException('Unknown enum value given'); + } + + public function equals(SliceReadStatus $other): bool + { + return \get_class($this) === \get_class($other) && $this->name === $other->name; + } + + public function name(): string + { + return $this->name; + } + + public function value() + { + return $this->value; + } + + public function __toString(): string + { + return $this->name; + } +} diff --git a/src/Stream.php b/src/Stream.php deleted file mode 100644 index 6f7adddd..00000000 --- a/src/Stream.php +++ /dev/null @@ -1,56 +0,0 @@ - - * (c) 2015-2018 Sascha-Oliver Prolic - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace Prooph\EventStore; - -use Iterator; - -class Stream -{ - /** - * @var StreamName - */ - protected $streamName; - - /** - * @var Iterator - */ - protected $streamEvents; - - /** - * @var array - */ - protected $metadata = []; - - public function __construct(StreamName $streamName, Iterator $streamEvents, array $metadata = []) - { - $this->streamName = $streamName; - $this->streamEvents = $streamEvents; - $this->metadata = $metadata; - } - - public function streamName(): StreamName - { - return $this->streamName; - } - - public function streamEvents(): Iterator - { - return $this->streamEvents; - } - - public function metadata(): array - { - return $this->metadata; - } -} diff --git a/src/StreamAcl.php b/src/StreamAcl.php new file mode 100644 index 00000000..ac5a8c50 --- /dev/null +++ b/src/StreamAcl.php @@ -0,0 +1,140 @@ + + * (c) 2015-2018 Sascha-Oliver Prolic + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Prooph\EventStore; + +use Prooph\EventStore\Common\SystemMetadata; +use Prooph\EventStore\Exception\InvalidArgumentException; + +class StreamAcl +{ + /** + * Roles and users permitted to read the stream + * @var string[] + */ + private $readRoles; + + /** + * Roles and users permitted to write to the stream + * @var string[] + */ + private $writeRoles; + + /** + * Roles and users permitted to delete the stream + * @var string[] + */ + private $deleteRoles; + + /** + * Roles and users permitted to read stream metadata + * @var string[] + */ + private $metaReadRoles; + + /** + * Roles and users permitted to write stream metadata + * @var string[] + */ + private $metaWriteRoles; + + public function __construct( + array $readRoles = [], + array $writeRoles = [], + array $deleteRoles = [], + array $metaReadRoles = [], + array $metaWriteRoles = [] + ) { + $check = function (array $data): void { + foreach ($data as $value) { + if (! \is_string($value) || '' === $value) { + throw new InvalidArgumentException('Invalid roles given, expected an array of strings'); + } + } + }; + + $check($readRoles); + $check($writeRoles); + $check($deleteRoles); + $check($metaReadRoles); + $check($metaWriteRoles); + + $this->readRoles = $readRoles; + $this->writeRoles = $writeRoles; + $this->deleteRoles = $deleteRoles; + $this->metaReadRoles = $metaReadRoles; + $this->metaWriteRoles = $metaWriteRoles; + } + + /** + * @return string[] + */ + public function readRoles(): array + { + return $this->readRoles; + } + + /** + * @return string[] + */ + public function writeRoles(): array + { + return $this->writeRoles; + } + + /** + * @return string[] + */ + public function deleteRoles(): array + { + return $this->deleteRoles; + } + + /** + * @return string[] + */ + public function metaReadRoles(): array + { + return $this->metaReadRoles; + } + + /** + * @return string[] + */ + public function metaWriteRoles(): array + { + return $this->metaWriteRoles; + } + + public function toArray(): array + { + return [ + SystemMetadata::ACL_READ => $this->readRoles, + SystemMetadata::ACL_WRITE => $this->writeRoles, + SystemMetadata::ACL_DELETE => $this->deleteRoles, + SystemMetadata::ACL_META_READ => $this->metaReadRoles, + SystemMetadata::ACL_META_WRITE => $this->metaWriteRoles, + ]; + } + + public static function fromArray(array $data): StreamAcl + { + return new self( + $data[SystemMetadata::ACL_READ] ?? [], + $data[SystemMetadata::ACL_WRITE] ?? [], + $data[SystemMetadata::ACL_DELETE] ?? [], + $data[SystemMetadata::ACL_META_READ] ?? [], + $data[SystemMetadata::ACL_META_WRITE] ?? [] + ); + } +} diff --git a/src/StreamEventsSlice.php b/src/StreamEventsSlice.php new file mode 100644 index 00000000..a5b72035 --- /dev/null +++ b/src/StreamEventsSlice.php @@ -0,0 +1,98 @@ + + * (c) 2015-2018 Sascha-Oliver Prolic + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Prooph\EventStore; + +class StreamEventsSlice +{ + /** @var SliceReadStatus */ + private $status; + /** @var string */ + private $stream; + /** @var int */ + private $fromEventNumber; + /** @var ReadDirection */ + private $readDirection; + /** @var ResolvedEvent[] */ + private $events; + /** @var int */ + private $nextEventNumber; + /** @var int */ + private $lastEventNumber; + /** @var bool */ + private $isEndOfStream; + + /** @internal */ + public function __construct( + SliceReadStatus $status, + string $stream, + int $fromEventNumber, + ReadDirection $readDirection, + array $events, + int $nextEventNumber, + int $lastEventNumber, + bool $isEndOfStream + ) { + $this->status = $status; + $this->stream = $stream; + $this->fromEventNumber = $fromEventNumber; + $this->readDirection = $readDirection; + $this->events = $events; + $this->nextEventNumber = $nextEventNumber; + $this->lastEventNumber = $lastEventNumber; + $this->isEndOfStream = $isEndOfStream; + } + + public function status(): SliceReadStatus + { + return $this->status; + } + + public function stream(): string + { + return $this->stream; + } + + public function fromEventNumber(): int + { + return $this->fromEventNumber; + } + + public function readDirection(): ReadDirection + { + return $this->readDirection; + } + + /** + * @return ResolvedEvent[] + */ + public function events(): array + { + return $this->events; + } + + public function nextEventNumber(): int + { + return $this->nextEventNumber; + } + + public function lastEventNumber(): int + { + return $this->lastEventNumber; + } + + public function isEndOfStream(): bool + { + return $this->isEndOfStream; + } +} diff --git a/src/StreamIterator/StreamIterator.php b/src/StreamIterator/StreamIterator.php deleted file mode 100644 index 85b3d425..00000000 --- a/src/StreamIterator/StreamIterator.php +++ /dev/null @@ -1,21 +0,0 @@ - - * (c) 2015-2018 Sascha-Oliver Prolic - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace Prooph\EventStore\StreamIterator; - -use Countable; -use Iterator; - -interface StreamIterator extends Countable, Iterator -{ -} diff --git a/src/StreamMetadata.php b/src/StreamMetadata.php new file mode 100644 index 00000000..d05e6ec7 --- /dev/null +++ b/src/StreamMetadata.php @@ -0,0 +1,211 @@ + + * (c) 2015-2018 Sascha-Oliver Prolic + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Prooph\EventStore; + +use JsonSerializable; +use Prooph\EventStore\Common\SystemMetadata; +use Prooph\EventStore\Exception\InvalidArgumentException; +use Prooph\EventStore\Exception\RuntimeException; +use stdClass; + +class StreamMetadata implements JsonSerializable +{ + /** + * The maximum number of events allowed in the stream. + * @var int|null + */ + private $maxCount; + + /** + * The maximum age in seconds for events allowed in the stream. + * @var int|null + */ + private $maxAge; + + /** + * The event number from which previous events can be scavenged. + * This is used to implement soft-deletion of streams. + * @var int|null + */ + private $truncateBefore; + + /** + * The amount of time in seconds for which the stream head is cachable. + * @var int|null + */ + private $cacheControl; + + /** + * The access control list for the stream. + * @var StreamAcl|null + */ + private $acl; + + /** + * key => value pairs of custom metadata + * @var array + */ + private $customMetadata; + + public function __construct( + ?int $maxCount = null, + ?int $maxAge = null, + ?int $truncateBefore = null, + ?int $cacheControl = null, + ?StreamAcl $acl = null, + array $customMetadata = [] + ) { + if (null !== $maxCount && $maxCount <= 0) { + throw new InvalidArgumentException('Max count should be positive value'); + } + + if (null !== $maxAge && $maxAge < 1) { + throw new InvalidArgumentException('Max age should be positive value'); + } + + if (null !== $truncateBefore && $truncateBefore < 0) { + throw new InvalidArgumentException('Truncate before should be non-negative value'); + } + + if (null !== $cacheControl && $cacheControl < 1) { + throw new InvalidArgumentException('Cache control should be positive value'); + } + + foreach ($customMetadata as $key => $value) { + if (! \is_string($key)) { + throw new InvalidArgumentException('CustomMetadata key must be a string'); + } + } + + $this->maxCount = $maxCount; + $this->maxAge = $maxAge; + $this->truncateBefore = $truncateBefore; + $this->cacheControl = $cacheControl; + $this->acl = $acl; + $this->customMetadata = $customMetadata; + } + + public static function create(): StreamMetadataBuilder + { + return new StreamMetadataBuilder(); + } + + public function maxCount(): ?int + { + return $this->maxCount; + } + + public function maxAge(): ?int + { + return $this->maxAge; + } + + public function truncateBefore(): ?int + { + return $this->truncateBefore; + } + + public function cacheControl(): ?int + { + return $this->cacheControl; + } + + public function acl(): ?StreamAcl + { + return $this->acl; + } + + /** + * @return array + */ + public function customMetadata(): array + { + return $this->customMetadata; + } + + /** + * @param string $key + * @return mixed + */ + public function getValue(string $key) + { + if (! \array_key_exists($key, $this->customMetadata)) { + throw new RuntimeException('Key ' . $key . ' not found in custom metadata'); + } + + return $this->customMetadata[$key]; + } + + public function jsonSerialize(): object + { + $object = new stdClass(); + + if (null !== $this->maxCount) { + $object->{SystemMetadata::MAX_COUNT} = $this->maxCount; + } + + if (null !== $this->maxAge) { + $object->{SystemMetadata::MAX_AGE} = $this->maxAge; + } + + if (null !== $this->truncateBefore) { + $object->{SystemMetadata::TRUNCATE_BEFORE} = $this->truncateBefore; + } + + if (null !== $this->cacheControl) { + $object->{SystemMetadata::CACHE_CONTROL} = $this->cacheControl; + } + + if (null !== $this->acl) { + $object->{SystemMetadata::ACL} = $this->acl->toArray(); + } + + foreach ($this->customMetadata as $key => $value) { + $object->{$key} = $value; + } + + return $object; + } + + public static function createFromArray(array $data): StreamMetadata + { + $internals = [ + SystemMetadata::MAX_COUNT, + SystemMetadata::MAX_AGE, + SystemMetadata::TRUNCATE_BEFORE, + SystemMetadata::CACHE_CONTROL, + ]; + + $params = []; + + foreach ($data as $key => $value) { + if (\in_array($key, $internals, true)) { + $params[$key] = $value; + } elseif ($key === SystemMetadata::ACL) { + $params[SystemMetadata::ACL] = StreamAcl::fromArray($value); + } else { + $params['customMetadata'][$key] = $value; + } + } + + return new self( + $params[SystemMetadata::MAX_COUNT] ?? null, + $params[SystemMetadata::MAX_AGE] ?? null, + $params[SystemMetadata::TRUNCATE_BEFORE] ?? null, + $params[SystemMetadata::CACHE_CONTROL] ?? null, + $params[SystemMetadata::ACL] ?? null, + $params['customMetadata'] ?? [] + ); + } +} diff --git a/src/StreamMetadataBuilder.php b/src/StreamMetadataBuilder.php new file mode 100644 index 00000000..090f2c2e --- /dev/null +++ b/src/StreamMetadataBuilder.php @@ -0,0 +1,186 @@ + + * (c) 2015-2018 Sascha-Oliver Prolic + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Prooph\EventStore; + +use Prooph\EventStore\Exception\InvalidArgumentException; + +class StreamMetadataBuilder +{ + /** @var int|null */ + private $maxCount; + /** @var int|null */ + private $maxAge; + /** @var int|null */ + private $truncateBefore; + /** @var int|null*/ + private $cacheControl; + /** @var array */ + private $aclRead; + /** @var array */ + private $aclWrite; + /** @var array */ + private $aclDelete; + /** @var array */ + private $aclMetaRead; + /** @var array */ + private $aclMetaWrite; + /** @var array */ + private $customMetadata; + + public function __construct( + ?int $maxCount = null, + ?int $maxAge = null, + ?int $truncateBefore = null, + ?int $cacheControl = null, + array $aclRead = [], + array $aclWrite = [], + array $aclDelete = [], + array $aclMetaRead = [], + array $aclMetaWrite = [], + array $customMetadata = [] + ) { + $this->maxCount = $maxCount; + $this->maxAge = $maxAge; + $this->truncateBefore = $truncateBefore; + $this->cacheControl = $cacheControl; + $this->aclRead = $aclRead; + $this->aclWrite = $aclWrite; + $this->aclDelete = $aclDelete; + $this->aclMetaRead = $aclMetaRead; + $this->aclMetaWrite = $aclMetaWrite; + $this->customMetadata = $customMetadata; + } + + public function build(): StreamMetadata + { + $acl = null === $this->aclRead + && null === $this->aclWrite + && null === $this->aclDelete + && null === $this->aclMetaRead + && null === $this->aclMetaWrite + ? null + : new StreamAcl($this->aclRead, $this->aclWrite, $this->aclDelete, $this->aclMetaRead, $this->aclMetaWrite); + + return new StreamMetadata( + $this->maxCount, + $this->maxAge, + $this->truncateBefore, + $this->cacheControl, + $acl, + $this->customMetadata + ); + } + + /** Sets the maximum number of events allowed in the stream */ + public function setMaxCount(int $maxCount): StreamMetadataBuilder + { + if ($maxCount < 0) { + throw new InvalidArgumentException('Max count must be positive'); + } + + $this->maxCount = $maxCount; + + return $this; + } + + /** Sets the event number from which previous events can be scavenged */ + public function setMaxAge(int $maxAge): StreamMetadataBuilder + { + if ($maxAge < 0) { + throw new InvalidArgumentException('Max age must be positive'); + } + + $this->maxAge = $maxAge; + + return $this; + } + + /** Sets the event number from which previous events can be scavenged */ + public function setTruncateBefore(int $truncateBefore): StreamMetadataBuilder + { + if ($truncateBefore < 0) { + throw new InvalidArgumentException('Truncate before must be positive'); + } + + $this->truncateBefore = $truncateBefore; + + return $this; + } + + /** Sets the amount of time for which the stream head is cachable */ + public function setCacheControl(int $cacheControl): StreamMetadataBuilder + { + if ($cacheControl < 0) { + throw new InvalidArgumentException('CacheControl must be positive'); + } + + $this->cacheControl = $cacheControl; + + return $this; + } + + /** Sets role names with read permission for the stream */ + public function setReadRoles(string ...$readRole): StreamMetadataBuilder + { + $this->aclRead = $readRole; + + return $this; + } + + /** Sets role names with write permission for the stream */ + public function setWriteRoles(string ...$writeRole): StreamMetadataBuilder + { + $this->aclWrite = $writeRole; + + return $this; + } + + /** Sets role names with delete permission for the stream */ + public function setDeleteRoles(string ...$deleteRole): StreamMetadataBuilder + { + $this->aclDelete = $deleteRole; + + return $this; + } + + /** Sets role names with metadata read permission for the stream */ + public function setMetadataReadRoles(string ...$metaReadRoles): StreamMetadataBuilder + { + $this->aclMetaRead = $metaReadRoles; + + return $this; + } + + /** Sets role names with metadata write permission for the stream */ + public function setMetadataWriteRoles(string ...$metaWriteRoles): StreamMetadataBuilder + { + $this->aclMetaWrite = $metaWriteRoles; + + return $this; + } + + public function setCustomProperty(string $key, $value): StreamMetadataBuilder + { + $this->customMetadata[$key] = $value; + + return $this; + } + + public function removeCustomProperty(string $key): StreamMetadataBuilder + { + unset($this->customMetadata[$key]); + + return $this; + } +} diff --git a/src/StreamMetadataResult.php b/src/StreamMetadataResult.php new file mode 100644 index 00000000..ec8d2b33 --- /dev/null +++ b/src/StreamMetadataResult.php @@ -0,0 +1,65 @@ + + * (c) 2015-2018 Sascha-Oliver Prolic + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Prooph\EventStore; + +use Prooph\EventStore\Exception\InvalidArgumentException; + +class StreamMetadataResult +{ + /** @var string */ + private $stream; + /** @var bool */ + private $isStreamDeleted; + /** @var int */ + private $metastreamVersion; + /** @var StreamMetadata */ + private $streamMetadata; + + /** @internal */ + public function __construct( + string $stream, + bool $isStreamDeleted, + int $metastreamVersion, + StreamMetadata $streamMetadata + ) { + if (empty($stream)) { + throw new InvalidArgumentException('Stream cannot be empty'); + } + + $this->stream = $stream; + $this->isStreamDeleted = $isStreamDeleted; + $this->metastreamVersion = $metastreamVersion; + $this->streamMetadata = $streamMetadata; + } + + public function stream(): string + { + return $this->stream; + } + + public function isStreamDeleted(): bool + { + return $this->isStreamDeleted; + } + + public function metastreamVersion(): int + { + return $this->metastreamVersion; + } + + public function streamMetadata(): StreamMetadata + { + return $this->streamMetadata; + } +} diff --git a/src/StreamPosition.php b/src/StreamPosition.php new file mode 100644 index 00000000..2267832f --- /dev/null +++ b/src/StreamPosition.php @@ -0,0 +1,22 @@ + + * (c) 2015-2018 Sascha-Oliver Prolic + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Prooph\EventStore; + +class StreamPosition +{ + // The first event in a stream + public const START = 0; + // The last event in a stream + public const END = -1; +} diff --git a/src/SubscriptionDropReason.php b/src/SubscriptionDropReason.php new file mode 100644 index 00000000..40380195 --- /dev/null +++ b/src/SubscriptionDropReason.php @@ -0,0 +1,163 @@ + + * (c) 2015-2018 Sascha-Oliver Prolic + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Prooph\EventStore; + +use Prooph\EventStore\Exception\InvalidArgumentException; + +class SubscriptionDropReason +{ + public const OPTIONS = [ + 'UserInitiated' => 0, + 'NotAuthenticated' => 1, + 'AccessDenied' => 2, + 'SubscribingError' => 3, + 'ServerError' => 4, + 'ConnectionClosed' => 5, + 'CatchUpError' => 6, + 'ProcessingQueueOverflow' => 7, + 'EventHandlerException' => 8, + 'MaxSubscribersReached' => 9, + 'PersistentSubscriptionDeleted' => 10, + 'Unknown' => 100, + 'NotFound' => 11, + ]; + + public const USER_INITIATED = 0; + public const NOT_AUTHENTICATED = 1; + public const ACCESS_DENIED = 2; + public const SUBSCRIBING_ERROR = 3; + public const SERVER_ERROR = 4; + public const CONNECTION_CLOSED = 5; + public const CATCH_UP_ERROR = 6; + public const PROCESSING_QUEUE_OVERFLOW = 7; + public const EVENT_HANDLER_EXCEPTION = 8; + public const MAX_SUBSCRIBERS_REACHED = 9; + public const PERSISTENT_SUBSCRIPTION_DELETED = 10; + public const UNKNOWN = 100; + public const NOT_FOUND = 11; + + private $name; + private $value; + + private function __construct(string $name) + { + $this->name = $name; + $this->value = self::OPTIONS[$name]; + } + + public static function userInitiated(): self + { + return new self('UserInitiated'); + } + + public static function notAuthenticated(): self + { + return new self('NotAuthenticated'); + } + + public static function accessDenied(): self + { + return new self('AccessDenied'); + } + + public static function subscribingError(): self + { + return new self('SubscribingError'); + } + + public static function serverError(): self + { + return new self('ServerError'); + } + + public static function connectionClosed(): self + { + return new self('ConnectionClosed'); + } + + public static function catchUpError(): self + { + return new self('CatchUpError'); + } + + public static function processingQueueOverflow(): self + { + return new self('ProcessingQueueOverflow'); + } + + public static function eventHandlerException(): self + { + return new self('EventHandlerException'); + } + + public static function maxSubscribersReached(): self + { + return new self('MaxSubscribersReached'); + } + + public static function persistentSubscriptionDeleted(): self + { + return new self('PersistentSubscriptionDeleted'); + } + + public static function unknown(): self + { + return new self('Unknown'); + } + + public static function notFound(): self + { + return new self('NotFound'); + } + + public static function byName(string $value): self + { + if (! isset(self::OPTIONS[$value])) { + throw new InvalidArgumentException('Unknown enum name given'); + } + + return self::{$value}(); + } + + public static function byValue($value): self + { + foreach (self::OPTIONS as $name => $v) { + if ($v === $value) { + return self::{$name}(); + } + } + + throw new InvalidArgumentException('Unknown enum value given'); + } + + public function equals(SubscriptionDropReason $other): bool + { + return \get_class($this) === \get_class($other) && $this->name === $other->name; + } + + public function name(): string + { + return $this->name; + } + + public function value() + { + return $this->value; + } + + public function __toString(): string + { + return $this->name; + } +} diff --git a/src/SubscriptionDropped.php b/src/SubscriptionDropped.php new file mode 100644 index 00000000..1fcf1b54 --- /dev/null +++ b/src/SubscriptionDropped.php @@ -0,0 +1,25 @@ + + * (c) 2015-2018 Sascha-Oliver Prolic + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Prooph\EventStore; + +use Throwable; + +interface SubscriptionDropped +{ + public function __invoke( + EventStoreSubscription $subscription, + SubscriptionDropReason $reason, + ?Throwable $exception = null + ): void; +} diff --git a/src/SystemSettings.php b/src/SystemSettings.php new file mode 100644 index 00000000..46d0d7bc --- /dev/null +++ b/src/SystemSettings.php @@ -0,0 +1,96 @@ + + * (c) 2015-2018 Sascha-Oliver Prolic + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Prooph\EventStore; + +use JsonSerializable; +use Prooph\EventStore\Common\SystemMetadata; +use Prooph\EventStore\Common\SystemRoles; +use Prooph\EventStore\Exception\InvalidArgumentException; +use stdClass; + +class SystemSettings implements JsonSerializable +{ + /** + * Default access control list for new user streams. + * @var StreamAcl + */ + private $userStreamAcl; + + /** + * Default access control list for new system streams. + * @var StreamAcl + */ + private $systemStreamAcl; + + public static function default(): SystemSettings + { + return new self( + new StreamAcl( + [SystemRoles::ALL], + [SystemRoles::ALL], + [SystemRoles::ALL], + [SystemRoles::ALL], + [SystemRoles::ALL] + ), + new StreamAcl( + [SystemRoles::ALL, SystemRoles::ADMINS], + [SystemRoles::ALL, SystemRoles::ADMINS], + [SystemRoles::ALL, SystemRoles::ADMINS], + [SystemRoles::ALL, SystemRoles::ADMINS], + [SystemRoles::ALL, SystemRoles::ADMINS] + ) + ); + } + + public function __construct(StreamAcl $userStreamAcl, StreamAcl $systemStreamAcl) + { + $this->userStreamAcl = $userStreamAcl; + $this->systemStreamAcl = $systemStreamAcl; + } + + public function userStreamAcl(): StreamAcl + { + return $this->userStreamAcl; + } + + public function systemStreamAcl(): StreamAcl + { + return $this->systemStreamAcl; + } + + public function jsonSerialize(): object + { + $object = new stdClass(); + $object->{SystemMetadata::USER_STREAM_ACL} = $this->userStreamAcl->toArray(); + $object->{SystemMetadata::SYSTEM_STREAM_ACL} = $this->systemStreamAcl->toArray(); + + return $object; + } + + public static function createFromArray(array $data): SystemSettings + { + if (! isset($data[SystemMetadata::USER_STREAM_ACL])) { + throw new InvalidArgumentException(SystemMetadata::USER_STREAM_ACL . ' is missing'); + } + + if (! isset($data[SystemMetadata::SYSTEM_STREAM_ACL])) { + throw new InvalidArgumentException(SystemMetadata::SYSTEM_STREAM_ACL . ' is missing'); + } + + return new self( + StreamAcl::fromArray($data[SystemMetadata::USER_STREAM_ACL]), + StreamAcl::fromArray($data[SystemMetadata::SYSTEM_STREAM_ACL]) + ); + } +} diff --git a/src/TransactionalActionEventEmitterEventStore.php b/src/TransactionalActionEventEmitterEventStore.php deleted file mode 100644 index 147a8545..00000000 --- a/src/TransactionalActionEventEmitterEventStore.php +++ /dev/null @@ -1,126 +0,0 @@ - - * (c) 2015-2018 Sascha-Oliver Prolic - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace Prooph\EventStore; - -use Prooph\Common\Event\ActionEvent; -use Prooph\Common\Event\ActionEventEmitter; -use Prooph\EventStore\Exception\TransactionAlreadyStarted; -use Prooph\EventStore\Exception\TransactionNotStarted; - -class TransactionalActionEventEmitterEventStore extends ActionEventEmitterEventStore implements TransactionalEventStore -{ - public const EVENT_BEGIN_TRANSACTION = 'beginTransaction'; - public const EVENT_COMMIT = 'commit'; - public const EVENT_ROLLBACK = 'rollback'; - - public const ALL_EVENTS = [ - self::EVENT_APPEND_TO, - self::EVENT_CREATE, - self::EVENT_LOAD, - self::EVENT_LOAD_REVERSE, - self::EVENT_DELETE, - self::EVENT_HAS_STREAM, - self::EVENT_FETCH_STREAM_METADATA, - self::EVENT_UPDATE_STREAM_METADATA, - self::EVENT_FETCH_STREAM_NAMES, - self::EVENT_FETCH_STREAM_NAMES_REGEX, - self::EVENT_FETCH_CATEGORY_NAMES, - self::EVENT_FETCH_CATEGORY_NAMES_REGEX, - self::EVENT_BEGIN_TRANSACTION, - self::EVENT_COMMIT, - self::EVENT_ROLLBACK, - ]; - - /** - * @var TransactionalEventStore - */ - protected $eventStore; - - public function __construct(TransactionalEventStore $eventStore, ActionEventEmitter $actionEventEmitter) - { - parent::__construct($eventStore, $actionEventEmitter); - - $actionEventEmitter->attachListener(self::EVENT_BEGIN_TRANSACTION, function (ActionEvent $event): void { - try { - $this->eventStore->beginTransaction(); - } catch (TransactionAlreadyStarted $exception) { - $event->setParam('transactionAlreadyStarted', $exception); - } - }); - - $actionEventEmitter->attachListener(self::EVENT_COMMIT, function (ActionEvent $event): void { - try { - $this->eventStore->commit(); - } catch (TransactionNotStarted $exception) { - $event->setParam('transactionNotStarted', $exception); - } - }); - - $actionEventEmitter->attachListener(self::EVENT_ROLLBACK, function (ActionEvent $event): void { - try { - $this->eventStore->rollback(); - } catch (TransactionNotStarted $exception) { - $event->setParam('transactionNotStarted', $exception); - } - }); - } - - public function beginTransaction(): void - { - $event = $this->actionEventEmitter->getNewActionEvent(self::EVENT_BEGIN_TRANSACTION, $this); - - $this->actionEventEmitter->dispatch($event); - - if ($exception = $event->getParam('transactionAlreadyStarted', false)) { - throw $exception; - } - } - - public function commit(): void - { - $event = $this->actionEventEmitter->getNewActionEvent(self::EVENT_COMMIT, $this); - - $this->actionEventEmitter->dispatch($event); - - if ($exception = $event->getParam('transactionNotStarted', false)) { - throw $exception; - } - } - - public function rollback(): void - { - $event = $this->actionEventEmitter->getNewActionEvent(self::EVENT_ROLLBACK, $this); - - $this->actionEventEmitter->dispatch($event); - - if ($exception = $event->getParam('transactionNotStarted', false)) { - throw $exception; - } - } - - /** - * @throws \Exception - * - * @return mixed - */ - public function transactional(callable $callable) - { - return $this->eventStore->transactional($callable); - } - - public function inTransaction(): bool - { - return $this->eventStore->inTransaction(); - } -} diff --git a/src/TransactionalEventStore.php b/src/TransactionalEventStore.php deleted file mode 100644 index 42b228df..00000000 --- a/src/TransactionalEventStore.php +++ /dev/null @@ -1,35 +0,0 @@ - - * (c) 2015-2018 Sascha-Oliver Prolic - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace Prooph\EventStore; - -/** - * This interfaces describes that an event store implementation allows control of the transaction handling - */ -interface TransactionalEventStore extends EventStore -{ - public function beginTransaction(): void; - - public function commit(): void; - - public function rollback(): void; - - public function inTransaction(): bool; - - /** - * @throws \Exception - * - * @return mixed - */ - public function transactional(callable $callable); -} diff --git a/src/Transport/Http/EndpointExtensions.php b/src/Transport/Http/EndpointExtensions.php new file mode 100644 index 00000000..e747ebe2 --- /dev/null +++ b/src/Transport/Http/EndpointExtensions.php @@ -0,0 +1,61 @@ + + * (c) 2015-2018 Sascha-Oliver Prolic + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Prooph\EventStore\Transport\Http; + +use Prooph\EventStore\EndPoint; + +/** @internal */ +class EndpointExtensions +{ + public const HTTP_SCHEMA = 'http'; + public const HTTPS_SCHEMA = 'https'; + + public static function rawUrlToHttpUrl( + EndPoint $endPoint, + string $schema, + string $rawUrl = '' + ): string { + return self::createHttpUrl( + $schema, + $endPoint->host(), + $endPoint->port(), + \ltrim($rawUrl, '/') + ); + } + + public static function formatStringToHttpUrl( + EndPoint $endPoint, + string $schema, + string $formatString, + ...$args + ): string { + return self::createHttpUrl( + $schema, + $endPoint->host(), + $endPoint->port(), + \sprintf(\ltrim($formatString, '/'), ...$args) + ); + } + + private static function createHttpUrl(string $schema, string $host, int $port, string $path): string + { + return \sprintf( + '%s://%s:%d/%s', + $schema, + $host, + $port, + $path + ); + } +} diff --git a/src/Transport/Http/HttpMethod.php b/src/Transport/Http/HttpMethod.php new file mode 100644 index 00000000..f8fcd477 --- /dev/null +++ b/src/Transport/Http/HttpMethod.php @@ -0,0 +1,26 @@ + + * (c) 2015-2018 Sascha-Oliver Prolic + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Prooph\EventStore\Transport\Http; + +/** @internal */ +class HttpMethod +{ + public const GET = 'GET'; + public const POST = 'POST'; + public const PUT = 'PUT'; + public const DELETE = 'DELETE'; + public const OPTIONS = 'OPTIONS'; + public const HEAD = 'HEAD'; + public const PATCH = 'PATCH'; +} diff --git a/src/Transport/Http/HttpStatusCode.php b/src/Transport/Http/HttpStatusCode.php new file mode 100644 index 00000000..dae40e47 --- /dev/null +++ b/src/Transport/Http/HttpStatusCode.php @@ -0,0 +1,89 @@ + + * (c) 2015-2018 Sascha-Oliver Prolic + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Prooph\EventStore\Transport\Http; + +class HttpStatusCode +{ + // INFORMATIONAL CODES + public const CONTINUE = 100; + public const SWITCHING_PROTOCOLS = 101; + public const PROCESSING = 102; + public const EARLY_HINTS = 103; + // SUCCESS CODES + public const OK = 200; + public const CREATED = 201; + public const ACCEPTED = 202; + public const NON_AUTHORITATIVE_INFORMATION = 203; + public const NO_CONTENT = 204; + public const RESET_CONTENT = 205; + public const PARTIAL_CONTENT = 206; + public const MULTI_STATUS = 207; + public const ALREADY_REPORTED = 208; + public const IM_USED = 226; + // REDIRECTION CODES + public const MULTIPLE_CHOICES = 300; + public const MOVED_PERMANENTLY = 301; + public const FOUND = 302; + public const SEE_OTHER = 303; + public const NOT_MODIFIED = 304; + public const USE_PROXY = 305; + public const SWITCH_PROXY = 306; // Deprecated to 306 => '(Unused)' + public const TEMPORARY_REDIRECT = 307; + public const PERMANENT_REDIRECT = 308; + // CLIENT ERROR + public const BAD_REQUEST = 400; + public const UNAUTHORIZED = 401; + public const PAYMENT_REQUIRED = 402; + public const FORBIDDEN = 403; + public const NOT_FOUND = 404; + public const METHOD_NOT_ALLOWED = 405; + public const NOT_ACCEPTABLE = 406; + public const PROXY_AUTHENTICATION_REQUIRED = 407; + public const REQUEST_TIMEOUT = 408; + public const CONFLICT = 409; + public const GONE = 410; + public const LENGTH_REQUIRED = 411; + public const PRECONDITION_FAILED = 412; + public const PAYLOAD_TOO_LARGE = 413; + public const URI_TOO_LONG = 414; + public const UNSUPPORTED_MEDIA_TYPE = 415; + public const RANGE_NOT_SATISFIABLE = 416; + public const EXPECTATION_FAILED = 417; + public const IM_A_TEAPOT = 418; + public const MISDIRECTED_REQUEST = 421; + public const UNPROCESSABLE_ENTITY = 422; + public const LOCKED = 423; + public const FAILED_DEPENDENCY = 424; + public const TOO_EARLY = 425; + public const UPGRADE_REQUIRED = 426; + public const PRECONDITION_REQUIRED = 428; + public const TOO_MANY_REQUESTS = 429; + public const REQUEST_HEADER_FIELDS_TOO_LARGE = 431; + public const CONNECTION_CLOSED_WITHOUT_RESPONSE = 444; + public const UNAVAILABLE_FOR_LEGAL_REASONS = 451; + // SERVER ERROR + public const CLIENT_CLOSED_REQUEST = 499; + public const INTERNAL_SERVER_ERROR = 500; + public const NOT_IMPLEMENTED = 501; + public const BAD_GATEWAY = 502; + public const SERVICE_UNAVAILABLE = 503; + public const GATEWAY_TIMEOUT = 504; + public const HTTP_VERSION_NOT_SUPPORTED = 505; + public const VARIANT_ALSO_NEGOTIATES = 506; + public const INSUFFICIENT_STORAGE = 507; + public const LOOP_DETECTED = 508; + public const NOT_EXTENDED = 510; + public const NETWORK_AUTHENTICATION_REQUIRED = 511; + public const NETWORK_CONNECT_TIMEOUT_ERROR = 599; +} diff --git a/src/Upcasting/SingleEventUpcaster.php b/src/Upcasting/SingleEventUpcaster.php deleted file mode 100644 index 581dbc98..00000000 --- a/src/Upcasting/SingleEventUpcaster.php +++ /dev/null @@ -1,32 +0,0 @@ - - * (c) 2015-2018 Sascha-Oliver Prolic - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace Prooph\EventStore\Upcasting; - -use Prooph\Common\Messaging\Message; - -abstract class SingleEventUpcaster implements Upcaster -{ - public function upcast(Message $message): array - { - if (! $this->canUpcast($message)) { - return [$message]; - } - - return $this->doUpcast($message); - } - - abstract protected function canUpcast(Message $message): bool; - - abstract protected function doUpcast(Message $message): array; -} diff --git a/src/Upcasting/UpcasterChain.php b/src/Upcasting/UpcasterChain.php deleted file mode 100644 index af9401e2..00000000 --- a/src/Upcasting/UpcasterChain.php +++ /dev/null @@ -1,47 +0,0 @@ - - * (c) 2015-2018 Sascha-Oliver Prolic - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace Prooph\EventStore\Upcasting; - -use Prooph\Common\Messaging\Message; - -final class UpcasterChain implements Upcaster -{ - /** - * @var Upcaster[] - */ - private $upcasters; - - public function __construct(Upcaster ...$upcasters) - { - $this->upcasters = $upcasters; - } - - public function upcast(Message $message): array - { - $result = []; - $messages = [$message]; - - foreach ($this->upcasters as $upcaster) { - $result = []; - - foreach ($messages as $message) { - $result += $upcaster->upcast($message); - } - - $messages = $result; - } - - return $result; - } -} diff --git a/src/Upcasting/UpcastingIterator.php b/src/Upcasting/UpcastingIterator.php deleted file mode 100644 index b9bb6a72..00000000 --- a/src/Upcasting/UpcastingIterator.php +++ /dev/null @@ -1,117 +0,0 @@ - - * (c) 2015-2018 Sascha-Oliver Prolic - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace Prooph\EventStore\Upcasting; - -use Iterator; -use Prooph\Common\Messaging\Message; - -final class UpcastingIterator implements Iterator -{ - /** - * @var Upcaster - */ - private $upcaster; - - /** - * @var Iterator - */ - private $innerIterator; - - /** - * @var array - */ - private $storedMessages = []; - - public function __construct(Upcaster $upcaster, Iterator $iterator) - { - $this->upcaster = $upcaster; - $this->innerIterator = $iterator; - } - - public function current(): ?Message - { - if (! empty($this->storedMessages)) { - return \reset($this->storedMessages); - } - - $current = null; - - if (! $this->innerIterator instanceof \EmptyIterator) { - $current = $this->innerIterator->current(); - } - - if (null === $current) { - return null; - } - - $this->storedMessages = $this->upcaster->upcast($current); - - while (empty($this->storedMessages)) { - $this->innerIterator->next(); - - if (! $this->innerIterator->valid()) { - return null; - } - - $this->storedMessages = $this->upcaster->upcast($this->innerIterator->current()); - } - - return \reset($this->storedMessages); - } - - public function next(): void - { - if (! empty($this->storedMessages)) { - \array_shift($this->storedMessages); - } - - if (! empty($this->storedMessages)) { - return; - } - - while (empty($this->storedMessages)) { - $this->innerIterator->next(); - - if (! $this->innerIterator->valid()) { - return; - } - - $this->storedMessages = $this->upcaster->upcast($this->innerIterator->current()); - } - } - - /** - * @return bool|int - */ - public function key() - { - return $this->innerIterator->key(); - } - - public function valid(): bool - { - if ($this->innerIterator instanceof \EmptyIterator) { - return false; - } - - return null !== $this->current(); - } - - public function rewind(): void - { - $this->storedMessages = []; - $this->innerIterator->rewind(); - $this->position = 0; - } -} diff --git a/src/UserCredentials.php b/src/UserCredentials.php new file mode 100644 index 00000000..3159720b --- /dev/null +++ b/src/UserCredentials.php @@ -0,0 +1,48 @@ + + * (c) 2015-2018 Sascha-Oliver Prolic + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Prooph\EventStore; + +use Prooph\EventStore\Exception\InvalidArgumentException; + +class UserCredentials +{ + /** @var string */ + private $username; + /** @var string */ + private $password; + + public function __construct(string $username, string $password) + { + if (empty($username)) { + throw new InvalidArgumentException('Username cannot be empty'); + } + + if (empty($password)) { + throw new InvalidArgumentException('Password cannot be empty'); + } + + $this->username = $username; + $this->password = $password; + } + + public function username(): string + { + return $this->username; + } + + public function password(): string + { + return $this->password; + } +} diff --git a/src/UserManagement/AsyncUsersManager.php b/src/UserManagement/AsyncUsersManager.php new file mode 100644 index 00000000..10b5fef8 --- /dev/null +++ b/src/UserManagement/AsyncUsersManager.php @@ -0,0 +1,80 @@ + + * (c) 2015-2018 Sascha-Oliver Prolic + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Prooph\EventStore\UserManagement; + +use Amp\Promise; +use Prooph\EventStore\Exception\UserCommandFailedException; +use Prooph\EventStore\UserCredentials; + +interface AsyncUsersManager +{ + public function enableAsync(string $login, ?UserCredentials $userCredentials = null): Promise; + + public function disableAsync(string $login, ?UserCredentials $userCredentials = null): Promise; + + /** @throws UserCommandFailedException */ + public function deleteUserAsync(string $login, ?UserCredentials $userCredentials = null): Promise; + + /** @return Promise */ + public function listAllAsync(?UserCredentials $userCredentials = null): Promise; + + /** @return Promise */ + public function getCurrentUserAsync(?UserCredentials $userCredentials = null): Promise; + + /** @return Promise */ + public function getUserAsync(string $login, ?UserCredentials $userCredentials = null): Promise; + + /** + * @param string $login + * @param string $fullName + * @param string[] $groups + * @param string $password + * @param UserCredentials|null $userCredentials + * @return Promise + */ + public function createUserAsync( + string $login, + string $fullName, + array $groups, + string $password, + ?UserCredentials $userCredentials = null + ): Promise; + + /** + * @param string $login + * @param string $fullName + * @param string[] $groups + * @param UserCredentials|null $userCredentials + * @return Promise + */ + public function updateUserAsync( + string $login, + string $fullName, + array $groups, + ?UserCredentials $userCredentials = null + ): Promise; + + public function changePasswordAsync( + string $login, + string $oldPassword, + string $newPassword, + ?UserCredentials $userCredentials = null + ): Promise; + + public function resetPasswordAsync( + string $login, + string $newPassword, + ?UserCredentials $userCredentials = null + ): Promise; +} diff --git a/src/UserManagement/ChangePasswordDetails.php b/src/UserManagement/ChangePasswordDetails.php new file mode 100644 index 00000000..b8776956 --- /dev/null +++ b/src/UserManagement/ChangePasswordDetails.php @@ -0,0 +1,41 @@ + + * (c) 2015-2018 Sascha-Oliver Prolic + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Prooph\EventStore\UserManagement; + +use JsonSerializable; +use stdClass; + +/** @internal */ +class ChangePasswordDetails implements JsonSerializable +{ + /** @var string */ + private $currentPassword; + /** @var string */ + private $newPassword; + + public function __construct(string $currentPassword, string $newPassword) + { + $this->currentPassword = $currentPassword; + $this->newPassword = $newPassword; + } + + public function jsonSerialize(): object + { + $object = new stdClass(); + $object->currentPassword = $this->currentPassword; + $object->newPassword = $this->newPassword; + + return $object; + } +} diff --git a/src/UserManagement/RelLink.php b/src/UserManagement/RelLink.php new file mode 100644 index 00000000..355e654b --- /dev/null +++ b/src/UserManagement/RelLink.php @@ -0,0 +1,38 @@ + + * (c) 2015-2018 Sascha-Oliver Prolic + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Prooph\EventStore\UserManagement; + +class RelLink +{ + /** @var string */ + private $href; + /** @var string */ + private $rel; + + public function __construct(string $href, string $rel) + { + $this->href = $href; + $this->rel = $rel; + } + + public function href(): string + { + return $this->href; + } + + public function rel(): string + { + return $this->rel; + } +} diff --git a/src/UserManagement/ResetPasswordDetails.php b/src/UserManagement/ResetPasswordDetails.php new file mode 100644 index 00000000..ee566ba6 --- /dev/null +++ b/src/UserManagement/ResetPasswordDetails.php @@ -0,0 +1,37 @@ + + * (c) 2015-2018 Sascha-Oliver Prolic + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Prooph\EventStore\UserManagement; + +use JsonSerializable; +use stdClass; + +/** @internal */ +class ResetPasswordDetails implements JsonSerializable +{ + /** @var string */ + private $newPassword; + + public function __construct(string $newPassword) + { + $this->newPassword = $newPassword; + } + + public function jsonSerialize(): object + { + $object = new stdClass(); + $object->newPassword = $this->newPassword; + + return $object; + } +} diff --git a/src/UserManagement/UserCreationInformation.php b/src/UserManagement/UserCreationInformation.php new file mode 100644 index 00000000..86cc7532 --- /dev/null +++ b/src/UserManagement/UserCreationInformation.php @@ -0,0 +1,48 @@ + + * (c) 2015-2018 Sascha-Oliver Prolic + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Prooph\EventStore\UserManagement; + +use JsonSerializable; +use stdClass; + +final class UserCreationInformation implements JsonSerializable +{ + /** @var string */ + private $loginName; + /** @var string */ + private $fullName; + /** @var string[] */ + private $groups; + /** @var string */ + private $password; + + public function __construct(string $loginName, string $fullName, array $groups, string $password) + { + $this->loginName = $loginName; + $this->fullName = $fullName; + $this->groups = $groups; + $this->password = $password; + } + + public function jsonSerialize(): object + { + $object = new stdClass(); + $object->loginName = $this->loginName; + $object->fullName = $this->fullName; + $object->groups = $this->groups; + $object->password = $this->password; + + return $object; + } +} diff --git a/src/UserManagement/UserDetails.php b/src/UserManagement/UserDetails.php new file mode 100644 index 00000000..9274b3d8 --- /dev/null +++ b/src/UserManagement/UserDetails.php @@ -0,0 +1,109 @@ + + * (c) 2015-2018 Sascha-Oliver Prolic + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Prooph\EventStore\UserManagement; + +use DateTimeImmutable; +use Prooph\EventStore\Exception\RuntimeException; +use Prooph\EventStore\Util\DateTime; + +/** @internal */ +final class UserDetails +{ + /** @var string */ + private $loginName; + /** @var string */ + private $fullName; + /** @var string[] */ + private $groups = []; + /** @var DateTimeImmutable */ + private $dateLastUpdated; + /** @var bool */ + private $disabled; + /** @var RelLink[] */ + private $links = []; + + private function __construct() + { + } + + public static function fromArray(array $data): self + { + $details = new self(); + + $details->loginName = $data['loginName']; + $details->fullName = $data['fullName']; + $details->groups = $data['groups']; + $details->disabled = $data['disabled']; + + $details->dateLastUpdated = isset($data['dateLastUpdated']) + ? DateTime::create($data['dateLastUpdated']) + : null; + + $links = []; + if (isset($data['links'])) { + foreach ($data['links'] as $link) { + $links[] = new RelLink($link['href'], $link['rel']); + } + } + $details->links = $links; + + return $details; + } + + public function loginName(): string + { + return $this->loginName; + } + + public function fullName(): string + { + return $this->fullName; + } + + /** @return string[] */ + public function groups(): array + { + return $this->groups; + } + + public function dateLastUpdated(): ?DateTimeImmutable + { + return $this->dateLastUpdated; + } + + public function disabled(): bool + { + return $this->disabled; + } + + /** @return RelLink[] */ + public function links(): array + { + return $this->links; + } + + /** @throws RuntimeException if rel not found */ + public function getRelLink(string $rel): string + { + $rel = \strtolower($rel); + + foreach ($this->links() as $link) { + if (\strtolower($link->rel()) === $rel) { + return $link->href(); + } + } + + throw new RuntimeException('Rel not found'); + } +} diff --git a/src/UserManagement/UserUpdateInformation.php b/src/UserManagement/UserUpdateInformation.php new file mode 100644 index 00000000..d3d419d0 --- /dev/null +++ b/src/UserManagement/UserUpdateInformation.php @@ -0,0 +1,41 @@ + + * (c) 2015-2018 Sascha-Oliver Prolic + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Prooph\EventStore\UserManagement; + +use JsonSerializable; +use stdClass; + +/** @internal */ +class UserUpdateInformation implements JsonSerializable +{ + /** @var string */ + private $fullName; + /** @var string[] */ + private $groups; + + public function __construct(string $fullName, array $groups) + { + $this->fullName = $fullName; + $this->groups = $groups; + } + + public function jsonSerialize(): object + { + $object = new stdClass(); + $object->fullName = $this->fullName; + $object->groups = $this->groups; + + return $object; + } +} diff --git a/src/UserManagement/UsersManager.php b/src/UserManagement/UsersManager.php new file mode 100644 index 00000000..674ebb85 --- /dev/null +++ b/src/UserManagement/UsersManager.php @@ -0,0 +1,79 @@ + + * (c) 2015-2018 Sascha-Oliver Prolic + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Prooph\EventStore\UserManagement; + +use Prooph\EventStore\Exception\UserCommandFailedException; +use Prooph\EventStore\UserCredentials; + +interface UsersManager +{ + public function enable(string $login, ?UserCredentials $userCredentials = null): void; + + public function disable(string $login, ?UserCredentials $userCredentials = null): void; + + /** @throws UserCommandFailedException */ + public function deleteUser(string $login, ?UserCredentials $userCredentials = null): void; + + /** @return UserDetails[] */ + public function listAll(?UserCredentials $userCredentials = null): array; + + public function getCurrentUser(?UserCredentials $userCredentials = null): UserDetails; + + public function getUser(string $login, ?UserCredentials $userCredentials = null): UserDetails; + + /** + * @param string $login + * @param string $fullName + * @param string[] $groups + * @param string $password + * @param UserCredentials|null $userCredentials + * + * @return void + */ + public function createUser( + string $login, + string $fullName, + array $groups, + string $password, + ?UserCredentials $userCredentials = null + ): void; + + /** + * @param string $login + * @param string $fullName + * @param string[] $groups + * @param UserCredentials|null $userCredentials + * + * @return void + */ + public function updateUser( + string $login, + string $fullName, + array $groups, + ?UserCredentials $userCredentials = null + ): void; + + public function changePassword( + string $login, + string $oldPassword, + string $newPassword, + ?UserCredentials $userCredentials = null + ): void; + + public function resetPassword( + string $login, + string $newPassword, + ?UserCredentials $userCredentials = null + ): void; +} diff --git a/src/Util/ArrayCache.php b/src/Util/ArrayCache.php deleted file mode 100644 index 26eb4222..00000000 --- a/src/Util/ArrayCache.php +++ /dev/null @@ -1,80 +0,0 @@ - - * (c) 2015-2018 Sascha-Oliver Prolic - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace Prooph\EventStore\Util; - -class ArrayCache -{ - /** - * @var array - */ - private $container = []; - - /** - * @var int - */ - private $size; - - /** - * @var int - */ - private $position = -1; - - public function __construct(int $size) - { - if ($size <= 0) { - throw new \InvalidArgumentException('Size must be a positive integer'); - } - - $this->size = $size; - $this->container = \array_fill(0, $size, null); - } - - /** - * @param mixed $value - */ - public function rollingAppend($value): void - { - $this->container[$this->nextPosition()] = $value; - } - - /** - * @param int $position - * @return mixed - */ - public function get(int $position) - { - if ($position >= $this->size - || $position < 0 - ) { - throw new \InvalidArgumentException('Position must be between 0 and ' . ($this->size - 1)); - } - - return $this->container[$position]; - } - - public function has($value): bool - { - return \in_array($value, $this->container, true); - } - - public function size(): int - { - return $this->size; - } - - private function nextPosition(): int - { - return $this->position = ++$this->position % $this->size; - } -} diff --git a/src/Util/Assertion.php b/src/Util/Assertion.php deleted file mode 100644 index 9f47d5df..00000000 --- a/src/Util/Assertion.php +++ /dev/null @@ -1,38 +0,0 @@ - - * (c) 2015-2018 Sascha-Oliver Prolic - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace Prooph\EventStore\Util; - -use Prooph\EventStore\Exception\InvalidArgumentException; - -class Assertion extends \Assert\Assertion -{ - /** - * Exception to throw when an assertion failed. - * - * @var string - */ - protected static $exceptionClass = InvalidArgumentException::class; - - protected static function createException( - $value, - $message, - $code, - $propertyPath = null, - array $constraints = [] - ): InvalidArgumentException { - $exceptionClass = static::$exceptionClass; - - return new $exceptionClass($message, $code); - } -} diff --git a/src/Util/DateTime.php b/src/Util/DateTime.php new file mode 100644 index 00000000..0287f143 --- /dev/null +++ b/src/Util/DateTime.php @@ -0,0 +1,43 @@ + + * (c) 2015-2018 Sascha-Oliver Prolic + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Prooph\EventStore\Util; + +use DateTimeImmutable; +use DateTimeZone; + +class DateTime +{ + public static function utcNow(): DateTimeImmutable + { + return new DateTimeImmutable('now', new DateTimeZone('UTC')); + } + + public static function create(string $dateTimeString): DateTimeImmutable + { + return DateTimeImmutable::createFromFormat( + 'Y-m-d\TH:i:s.uP', + $dateTimeString, + new DateTimeZone('UTC') + ); + } + + public static function format(DateTimeImmutable $dateTime): string + { + return $dateTime->format('Y-m-d\TH:i:s.uP'); + } + + final private function __construct() + { + } +} diff --git a/src/Util/Guid.php b/src/Util/Guid.php new file mode 100644 index 00000000..3b90fd28 --- /dev/null +++ b/src/Util/Guid.php @@ -0,0 +1,67 @@ + + * (c) 2015-2018 Sascha-Oliver Prolic + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Prooph\EventStore\Util; + +use Ramsey\Uuid\FeatureSet; +use Ramsey\Uuid\UuidFactory; +use Ramsey\Uuid\UuidInterface; + +class Guid +{ + /** @var UuidFactory */ + private static $factory; + + public static function generate(): UuidInterface + { + return self::factory()->uuid4(); + } + + public static function generateString(): string + { + return self::generate()->toString(); + } + + public static function generateAsHex(): string + { + return self::generate()->getHex(); + } + + public static function fromString(string $uuid): UuidInterface + { + return self::factory()->fromString($uuid); + } + + public static function fromBytes(string $bytes): UuidInterface + { + return self::factory()->fromBytes($bytes); + } + + public static function empty(): string + { + return '00000000-0000-0000-0000-000000000000'; + } + + final private function __construct() + { + } + + private static function factory(): UuidFactory + { + if (null === self::$factory) { + self::$factory = new UuidFactory(new FeatureSet(true)); + } + + return self::$factory; + } +} diff --git a/src/Util/Json.php b/src/Util/Json.php new file mode 100644 index 00000000..13f99de4 --- /dev/null +++ b/src/Util/Json.php @@ -0,0 +1,55 @@ + + * (c) 2015-2018 Sascha-Oliver Prolic + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Prooph\EventStore\Util; + +use Prooph\EventStore\Exception\JsonException; + +class Json +{ + /** + * @param mixed $value + * @return string + */ + public static function encode($value): string + { + $flags = \JSON_UNESCAPED_UNICODE | \JSON_UNESCAPED_SLASHES | \JSON_PRESERVE_ZERO_FRACTION; + + $string = \json_encode($value, $flags); + + if ($error = \json_last_error()) { + throw new JsonException(\json_last_error_msg(), $error); + } + + return $string; + } + + /** + * @param string $json + * @return mixed + */ + public static function decode(string $json) + { + $data = \json_decode($json, true, 512, \JSON_BIGINT_AS_STRING); + + if ($error = \json_last_error()) { + throw new JsonException(\json_last_error_msg(), $error); + } + + return $data; + } + + final private function __construct() + { + } +} diff --git a/src/WriteResult.php b/src/WriteResult.php new file mode 100644 index 00000000..2099a6fb --- /dev/null +++ b/src/WriteResult.php @@ -0,0 +1,38 @@ + + * (c) 2015-2018 Sascha-Oliver Prolic + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Prooph\EventStore; + +class WriteResult +{ + /** @var int */ + private $nextExpectedVersion; + /** @var Position */ + private $logPosition; + + public function __construct(int $nextExpectedVersion, Position $logPosition) + { + $this->nextExpectedVersion = $nextExpectedVersion; + $this->logPosition = $logPosition; + } + + public function nextExpectedVersion(): int + { + return $this->nextExpectedVersion; + } + + public function logPosition(): Position + { + return $this->logPosition; + } +} diff --git a/tests/AbstractEventStoreTest.php b/tests/AbstractEventStoreTest.php deleted file mode 100644 index 5b205d4f..00000000 --- a/tests/AbstractEventStoreTest.php +++ /dev/null @@ -1,1193 +0,0 @@ - - * (c) 2015-2018 Sascha-Oliver Prolic - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ProophTest\EventStore; - -use ArrayIterator; -use PHPUnit\Framework\TestCase; -use Prooph\EventStore\EventStore; -use Prooph\EventStore\Exception\InvalidArgumentException; -use Prooph\EventStore\Exception\StreamExistsAlready; -use Prooph\EventStore\Exception\StreamNotFound; -use Prooph\EventStore\Metadata\FieldType; -use Prooph\EventStore\Metadata\MetadataMatcher; -use Prooph\EventStore\Metadata\Operator; -use Prooph\EventStore\Stream; -use Prooph\EventStore\StreamIterator\StreamIterator; -use Prooph\EventStore\StreamName; -use ProophTest\EventStore\Mock\TestDomainEvent; -use ProophTest\EventStore\Mock\UserCreated; -use ProophTest\EventStore\Mock\UsernameChanged; - -/** - * Common tests for all event store implementations - */ -abstract class AbstractEventStoreTest extends TestCase -{ - use EventStoreTestStreamTrait; - - /** - * @var EventStore - */ - protected $eventStore; - - /** - * @test - */ - public function it_creates_a_new_stream_and_records_the_stream_events_and_deletes(): void - { - $streamName = new StreamName('Prooph\Model\User'); - - $stream = $this->getTestStream(); - - $this->eventStore->create($stream); - - $streamEvents = $this->eventStore->load($streamName); - - $this->assertCount(1, $streamEvents); - - $this->assertEquals( - [ - 'foo' => 'bar', - ], - $this->eventStore->fetchStreamMetadata($streamName) - ); - - $this->assertTrue($this->eventStore->hasStream($streamName)); - - $this->eventStore->delete($streamName); - - $this->assertFalse($this->eventStore->hasStream($streamName)); - } - - /** - * @test - */ - public function it_appends_events_to_stream_and_records_them(): void - { - $this->eventStore->create($this->getTestStream()); - - $secondStreamEvent = UsernameChanged::with( - ['new_name' => 'John Doe'], - 2 - ); - - $this->eventStore->appendTo(new StreamName('Prooph\Model\User'), new ArrayIterator([$secondStreamEvent])); - - $this->assertCount(2, $this->eventStore->load(new StreamName('Prooph\Model\User'))); - } - - /** - * @test - */ - public function it_cannot_create_a_stream_with_same_name_twice(): void - { - $this->expectException(StreamExistsAlready::class); - - $stream = $this->getTestStream(); - - $this->eventStore->create($stream); - $this->eventStore->create($stream); - } - - /** - * @test - */ - public function it_updates_stream_metadata(): void - { - $stream = $this->getTestStream(); - - $this->eventStore->create($stream); - - $this->eventStore->updateStreamMetadata($stream->streamName(), ['new' => 'values']); - - $this->assertEquals( - [ - 'new' => 'values', - ], - $this->eventStore->fetchStreamMetadata($stream->streamName()) - ); - } - - /** - * @test - */ - public function it_throws_stream_not_found_exception_when_trying_to_update_metadata_on_unknown_stream(): void - { - $this->expectException(StreamNotFound::class); - - $this->eventStore->updateStreamMetadata(new StreamName('unknown'), []); - } - - /** - * @test - */ - public function it_loads_events_from_number(): void - { - $stream = $this->getTestStream(); - - $this->eventStore->create($stream); - - $streamEventVersion2 = UsernameChanged::with( - ['new_name' => 'John Doe'], - 2 - ); - - $streamEventVersion2 = $streamEventVersion2->withAddedMetadata('snapshot', true); - - $streamEventVersion3 = UsernameChanged::with( - ['new_name' => 'Jane Doe'], - 3 - ); - - $streamEventVersion3 = $streamEventVersion3->withAddedMetadata('snapshot', false); - - $this->eventStore->appendTo($stream->streamName(), new ArrayIterator([$streamEventVersion2, $streamEventVersion3])); - - $loadedEvents = $this->eventStore->load($stream->streamName(), 2); - - $this->assertCount(2, $loadedEvents); - - $loadedEvents->rewind(); - - $this->assertTrue($loadedEvents->current()->metadata()['snapshot']); - $loadedEvents->next(); - $this->assertFalse($loadedEvents->current()->metadata()['snapshot']); - - $streamEvents = $this->eventStore->load($stream->streamName(), 2); - - $this->assertCount(2, $streamEvents); - - $streamEvents->rewind(); - - $this->assertTrue($streamEvents->current()->metadata()['snapshot']); - $streamEvents->next(); - $this->assertFalse($streamEvents->current()->metadata()['snapshot']); - } - - /** - * @test - */ - public function it_loads_events_reverse_from_number(): void - { - $stream = $this->getTestStream(); - - $this->eventStore->create($stream); - - $streamEventVersion2 = UsernameChanged::with( - ['new_name' => 'John Doe'], - 2 - ); - - $streamEventVersion2 = $streamEventVersion2->withAddedMetadata('snapshot', true); - - $streamEventVersion3 = UsernameChanged::with( - ['new_name' => 'Jane Doe'], - 3 - ); - - $streamEventVersion3 = $streamEventVersion3->withAddedMetadata('snapshot', false); - - $this->eventStore->appendTo($stream->streamName(), new ArrayIterator([$streamEventVersion2, $streamEventVersion3])); - - $loadedEvents = $this->eventStore->loadReverse($stream->streamName(), null, 2); - - $this->assertCount(2, $loadedEvents); - - $loadedEvents->rewind(); - - $this->assertFalse($loadedEvents->current()->metadata()['snapshot']); - $loadedEvents->next(); - $this->assertTrue($loadedEvents->current()->metadata()['snapshot']); - - $streamEvents = $this->eventStore->loadReverse($stream->streamName(), null, 2); - - $this->assertCount(2, $streamEvents); - - $streamEvents->rewind(); - - $this->assertFalse($streamEvents->current()->metadata()['snapshot']); - $streamEvents->next(); - $this->assertTrue($streamEvents->current()->metadata()['snapshot']); - } - - /** - * @test - */ - public function it_loads_events_from_number_with_count(): void - { - $stream = $this->getTestStream(); - - $this->eventStore->create($stream); - - $streamEventVersion2 = UsernameChanged::with( - ['new_name' => 'John Doe'], - 2 - ); - - $streamEventVersion2 = $streamEventVersion2->withAddedMetadata('snapshot', true); - - $streamEventVersion3 = UsernameChanged::with( - ['new_name' => 'Jane Doe'], - 3 - ); - - $streamEventVersion3 = $streamEventVersion3->withAddedMetadata('snapshot', false); - - $streamEventVersion4 = UsernameChanged::with( - ['new_name' => 'Jane Dole'], - 4 - ); - - $streamEventVersion4 = $streamEventVersion4->withAddedMetadata('snapshot', false); - - $this->eventStore->appendTo($stream->streamName(), new ArrayIterator([ - $streamEventVersion2, - $streamEventVersion3, - $streamEventVersion4, - ])); - - $loadedEvents = $this->eventStore->load($stream->streamName(), 2, 2); - - $this->assertCount(2, $loadedEvents); - - $loadedEvents->rewind(); - - $this->assertTrue($loadedEvents->current()->metadata()['snapshot']); - $loadedEvents->next(); - $this->assertFalse($loadedEvents->current()->metadata()['snapshot']); - - $loadedEvents = $this->eventStore->load($stream->streamName(), 2, 2); - - $this->assertCount(2, $loadedEvents); - - $loadedEvents->rewind(); - - $this->assertTrue($loadedEvents->current()->metadata()['snapshot']); - $loadedEvents->next(); - $this->assertFalse($loadedEvents->current()->metadata()['snapshot']); - } - - /** - * @test - */ - public function it_loads_events_reverse_from_number_with_count(): void - { - $stream = $this->getTestStream(); - - $this->eventStore->create($stream); - - $streamEventVersion2 = UsernameChanged::with( - ['new_name' => 'John Doe'], - 2 - ); - - $streamEventVersion2 = $streamEventVersion2->withAddedMetadata('snapshot', true); - - $streamEventVersion3 = UsernameChanged::with( - ['new_name' => 'Jane Doe'], - 3 - ); - - $streamEventVersion3 = $streamEventVersion3->withAddedMetadata('snapshot', false); - - $streamEventVersion4 = UsernameChanged::with( - ['new_name' => 'Jane Dole'], - 4 - ); - - $streamEventVersion4 = $streamEventVersion4->withAddedMetadata('snapshot', false); - - $this->eventStore->appendTo($stream->streamName(), new ArrayIterator([ - $streamEventVersion2, - $streamEventVersion3, - $streamEventVersion4, - ])); - - $loadedEvents = $this->eventStore->loadReverse($stream->streamName(), 3, 2); - - $this->assertCount(2, $loadedEvents); - - $loadedEvents->rewind(); - - $this->assertFalse($loadedEvents->current()->metadata()['snapshot']); - $loadedEvents->next(); - $this->assertTrue($loadedEvents->current()->metadata()['snapshot']); - - $loadedEvents = $this->eventStore->loadReverse($stream->streamName(), 3, 2); - - $this->assertCount(2, $loadedEvents); - - $loadedEvents->rewind(); - - $this->assertFalse($loadedEvents->current()->metadata()['snapshot']); - $loadedEvents->next(); - $this->assertTrue($loadedEvents->current()->metadata()['snapshot']); - } - - /** - * @test - * @dataProvider getMatchingMetadata - */ - public function it_loads_events_by_matching_metadata(array $metadata): void - { - $stream = $this->getTestStream(); - - $this->eventStore->create($stream); - - $streamEventWithMetadata = TestDomainEvent::with( - ['name' => 'Alex', 'email' => 'contact@prooph.de'], - 2 - ); - - foreach ($metadata as $field => $value) { - $streamEventWithMetadata = $streamEventWithMetadata->withAddedMetadata($field, $value); - } - - $this->eventStore->appendTo($stream->streamName(), new ArrayIterator([$streamEventWithMetadata])); - - $metadataMatcher = new MetadataMatcher(); - - foreach ($metadata as $field => $value) { - $metadataMatcher = $metadataMatcher->withMetadataMatch($field, Operator::EQUALS(), $value); - } - - $streamEvents = $this->eventStore->load($stream->streamName(), 1, null, $metadataMatcher); - - $this->assertCount(1, $streamEvents); - - $streamEvents->rewind(); - - $currentMetadata = $streamEvents->current()->metadata(); - - foreach ($metadata as $field => $value) { - $this->assertEquals($value, $currentMetadata[$field]); - } - } - - /** - * @test - * @dataProvider getMatchingMetadata - */ - public function it_loads_events_reverse_by_matching_metadata(array $metadata): void - { - $stream = $this->getTestStream(); - - $this->eventStore->create($stream); - - $streamEventWithMetadata = TestDomainEvent::with( - ['name' => 'Alex', 'email' => 'contact@prooph.de'], - 2 - ); - - foreach ($metadata as $field => $value) { - $streamEventWithMetadata = $streamEventWithMetadata->withAddedMetadata($field, $value); - } - - $this->eventStore->appendTo($stream->streamName(), new ArrayIterator([$streamEventWithMetadata])); - - $metadataMatcher = new MetadataMatcher(); - - foreach ($metadata as $field => $value) { - $metadataMatcher = $metadataMatcher->withMetadataMatch($field, Operator::EQUALS(), $value); - } - - $streamEvents = $this->eventStore->loadReverse($stream->streamName(), 2, null, $metadataMatcher); - - $this->assertCount(1, $streamEvents); - - $streamEvents->rewind(); - - $currentMetadata = $streamEvents->current()->metadata(); - - foreach ($metadata as $field => $value) { - $this->assertEquals($value, $currentMetadata[$field]); - } - } - - /** - * @test - */ - public function it_returns_only_matched_metadata(): void - { - $event = UserCreated::with(['name' => 'John'], 1); - $event = $event->withAddedMetadata('foo', 'bar'); - $event = $event->withAddedMetadata('int', 5); - $event = $event->withAddedMetadata('int2', 4); - $event = $event->withAddedMetadata('int3', 6); - $event = $event->withAddedMetadata('int4', 7); - - $uuid = $event->uuid()->toString(); - $before = $event->createdAt()->modify('-5 secs')->format('Y-m-d\TH:i:s.u'); - $later = $event->createdAt()->modify('+5 secs')->format('Y-m-d\TH:i:s.u'); - - $stream = new Stream(new StreamName('Prooph\Model\User'), new ArrayIterator([$event])); - - $this->eventStore->create($stream); - - $metadataMatcher = new MetadataMatcher(); - $metadataMatcher = $metadataMatcher->withMetadataMatch('foo', Operator::EQUALS(), 'bar'); - $metadataMatcher = $metadataMatcher->withMetadataMatch('foo', Operator::NOT_EQUALS(), 'baz'); - $metadataMatcher = $metadataMatcher->withMetadataMatch('int', Operator::GREATER_THAN(), 4); - $metadataMatcher = $metadataMatcher->withMetadataMatch('int2', Operator::GREATER_THAN_EQUALS(), 4); - $metadataMatcher = $metadataMatcher->withMetadataMatch('int', Operator::IN(), [4, 5, 6]); - $metadataMatcher = $metadataMatcher->withMetadataMatch('int3', Operator::LOWER_THAN(), 7); - $metadataMatcher = $metadataMatcher->withMetadataMatch('int4', Operator::LOWER_THAN_EQUALS(), 7); - $metadataMatcher = $metadataMatcher->withMetadataMatch('int', Operator::NOT_IN(), [4, 6]); - $metadataMatcher = $metadataMatcher->withMetadataMatch('foo', Operator::REGEX(), '^b[a]r$'); - - $metadataMatcher = $metadataMatcher->withMetadataMatch('uuid', Operator::EQUALS(), $uuid, FieldType::MESSAGE_PROPERTY()); - $metadataMatcher = $metadataMatcher->withMetadataMatch('uuid', Operator::NOT_EQUALS(), 'baz', FieldType::MESSAGE_PROPERTY()); - $metadataMatcher = $metadataMatcher->withMetadataMatch('createdAt', Operator::GREATER_THAN(), $before, FieldType::MESSAGE_PROPERTY()); - $metadataMatcher = $metadataMatcher->withMetadataMatch('createdAt', Operator::GREATER_THAN_EQUALS(), $before, FieldType::MESSAGE_PROPERTY()); - $metadataMatcher = $metadataMatcher->withMetadataMatch('uuid', Operator::IN(), [$uuid, 2, 3], FieldType::MESSAGE_PROPERTY()); - $metadataMatcher = $metadataMatcher->withMetadataMatch('createdAt', Operator::LOWER_THAN(), $later, FieldType::MESSAGE_PROPERTY()); - $metadataMatcher = $metadataMatcher->withMetadataMatch('createdAt', Operator::LOWER_THAN_EQUALS(), $later, FieldType::MESSAGE_PROPERTY()); - $metadataMatcher = $metadataMatcher->withMetadataMatch('createdAt', Operator::NOT_IN(), [$before, $later], FieldType::MESSAGE_PROPERTY()); - $metadataMatcher = $metadataMatcher->withMetadataMatch('messageName', Operator::REGEX(), '.+UserCreated$', FieldType::MESSAGE_PROPERTY()); - - $streamEvents = $this->eventStore->load($stream->streamName(), 1, null, $metadataMatcher); - - $this->assertCount(1, $streamEvents); - } - - /** - * @test - */ - public function it_returns_only_matched_metadata_2(): void - { - $event = UserCreated::with(['name' => 'John'], 1); - $event = $event->withAddedMetadata('foo', 'bar'); - $event = $event->withAddedMetadata('int', 5); - $event = $event->withAddedMetadata('int2', 4); - $event = $event->withAddedMetadata('int3', 6); - $event = $event->withAddedMetadata('int4', 7); - - $streamName = $this->prophesize(StreamName::class); - $streamName->toString()->willReturn('Prooph\Model\User')->shouldBeCalled(); - $streamName = $streamName->reveal(); - - $stream = $this->prophesize(Stream::class); - $stream->streamName()->willReturn($streamName)->shouldBeCalled(); - $stream->metadata()->willReturn([])->shouldBeCalled(); - $stream->streamEvents()->willReturn(new \ArrayIterator([$event]))->shouldBeCalled(); - - $this->eventStore->create($stream->reveal()); - - $metadataMatcher = new MetadataMatcher(); - $metadataMatcher = $metadataMatcher->withMetadataMatch('foo', Operator::EQUALS(), 'baz'); - - $result = $this->eventStore->load($streamName, 1, null, $metadataMatcher); - - $this->assertFalse($result->valid()); - - $metadataMatcher = new MetadataMatcher(); - $metadataMatcher = $metadataMatcher->withMetadataMatch('foo', Operator::NOT_EQUALS(), 'bar'); - - $result = $this->eventStore->load($streamName, 1, null, $metadataMatcher); - - $this->assertFalse($result->valid()); - - $metadataMatcher = new MetadataMatcher(); - $metadataMatcher = $metadataMatcher->withMetadataMatch('int', Operator::GREATER_THAN(), 9); - - $result = $this->eventStore->load($streamName, 1, null, $metadataMatcher); - - $this->assertFalse($result->valid()); - - $metadataMatcher = new MetadataMatcher(); - $metadataMatcher = $metadataMatcher->withMetadataMatch('int2', Operator::GREATER_THAN_EQUALS(), 10); - - $result = $this->eventStore->load($streamName, 1, null, $metadataMatcher); - - $this->assertFalse($result->valid()); - - $metadataMatcher = new MetadataMatcher(); - $metadataMatcher = $metadataMatcher->withMetadataMatch('int3', Operator::LOWER_THAN(), 1); - - $result = $this->eventStore->load($streamName, 1, null, $metadataMatcher); - - $this->assertFalse($result->valid()); - - $metadataMatcher = new MetadataMatcher(); - $metadataMatcher = $metadataMatcher->withMetadataMatch('int4', Operator::LOWER_THAN_EQUALS(), 1); - - $result = $this->eventStore->load($streamName, 1, null, $metadataMatcher); - - $this->assertFalse($result->valid()); - } - - /** - * @test - */ - public function it_returns_only_matched_metadata_reverse(): void - { - $event = UserCreated::with(['name' => 'John'], 1); - $event = $event->withAddedMetadata('foo', 'bar'); - $event = $event->withAddedMetadata('int', 5); - $event = $event->withAddedMetadata('int2', 4); - $event = $event->withAddedMetadata('int3', 6); - $event = $event->withAddedMetadata('int4', 7); - - $uuid = $event->uuid()->toString(); - $before = $event->createdAt()->modify('-5 secs')->format('Y-m-d\TH:i:s.u'); - $later = $event->createdAt()->modify('+5 secs')->format('Y-m-d\TH:i:s.u'); - - $streamName = new StreamName('Prooph\Model\User'); - - $stream = new Stream($streamName, new ArrayIterator([$event])); - - $this->eventStore->create($stream); - - $metadataMatcher = new MetadataMatcher(); - $metadataMatcher = $metadataMatcher->withMetadataMatch('foo', Operator::EQUALS(), 'bar'); - $metadataMatcher = $metadataMatcher->withMetadataMatch('foo', Operator::NOT_EQUALS(), 'baz'); - $metadataMatcher = $metadataMatcher->withMetadataMatch('int', Operator::GREATER_THAN(), 4); - $metadataMatcher = $metadataMatcher->withMetadataMatch('int2', Operator::GREATER_THAN_EQUALS(), 4); - $metadataMatcher = $metadataMatcher->withMetadataMatch('int', Operator::IN(), [4, 5, 6]); - $metadataMatcher = $metadataMatcher->withMetadataMatch('int3', Operator::LOWER_THAN(), 7); - $metadataMatcher = $metadataMatcher->withMetadataMatch('int4', Operator::LOWER_THAN_EQUALS(), 7); - $metadataMatcher = $metadataMatcher->withMetadataMatch('int', Operator::NOT_IN(), [4, 6]); - $metadataMatcher = $metadataMatcher->withMetadataMatch('foo', Operator::REGEX(), '^b[a]r$'); - - $metadataMatcher = $metadataMatcher->withMetadataMatch('uuid', Operator::EQUALS(), $uuid, FieldType::MESSAGE_PROPERTY()); - $metadataMatcher = $metadataMatcher->withMetadataMatch('uuid', Operator::NOT_EQUALS(), 'baz', FieldType::MESSAGE_PROPERTY()); - $metadataMatcher = $metadataMatcher->withMetadataMatch('createdAt', Operator::GREATER_THAN(), $before, FieldType::MESSAGE_PROPERTY()); - $metadataMatcher = $metadataMatcher->withMetadataMatch('createdAt', Operator::GREATER_THAN_EQUALS(), $before, FieldType::MESSAGE_PROPERTY()); - $metadataMatcher = $metadataMatcher->withMetadataMatch('uuid', Operator::IN(), [$uuid, 2, 3], FieldType::MESSAGE_PROPERTY()); - $metadataMatcher = $metadataMatcher->withMetadataMatch('createdAt', Operator::LOWER_THAN(), $later, FieldType::MESSAGE_PROPERTY()); - $metadataMatcher = $metadataMatcher->withMetadataMatch('createdAt', Operator::LOWER_THAN_EQUALS(), $later, FieldType::MESSAGE_PROPERTY()); - $metadataMatcher = $metadataMatcher->withMetadataMatch('createdAt', Operator::NOT_IN(), [$before, $later], FieldType::MESSAGE_PROPERTY()); - $metadataMatcher = $metadataMatcher->withMetadataMatch('messageName', Operator::REGEX(), '.+UserCreated$', FieldType::MESSAGE_PROPERTY()); - - $streamEvents = $this->eventStore->loadReverse($stream->streamName(), 1, null, $metadataMatcher); - - $this->assertCount(1, $streamEvents); - } - - /** - * @test - */ - public function it_returns_only_matched_metadata_2_reverse(): void - { - $event = UserCreated::with(['name' => 'John'], 1); - $event = $event->withAddedMetadata('foo', 'bar'); - $event = $event->withAddedMetadata('int', 5); - $event = $event->withAddedMetadata('int2', 4); - $event = $event->withAddedMetadata('int3', 6); - $event = $event->withAddedMetadata('int4', 7); - - $streamName = $this->prophesize(StreamName::class); - $streamName->toString()->willReturn('Prooph\Model\User')->shouldBeCalled(); - $streamName = $streamName->reveal(); - - $stream = $this->prophesize(Stream::class); - $stream->streamName()->willReturn($streamName)->shouldBeCalled(); - $stream->metadata()->willReturn([])->shouldBeCalled(); - $stream->streamEvents()->willReturn(new \ArrayIterator([$event]))->shouldBeCalled(); - - $this->eventStore->create($stream->reveal()); - - $metadataMatcher = new MetadataMatcher(); - $metadataMatcher = $metadataMatcher->withMetadataMatch('foo', Operator::EQUALS(), 'baz'); - - $result = $this->eventStore->loadReverse($streamName, null, null, $metadataMatcher); - - $this->assertFalse($result->valid()); - - $metadataMatcher = new MetadataMatcher(); - $metadataMatcher = $metadataMatcher->withMetadataMatch('foo', Operator::NOT_EQUALS(), 'bar'); - - $result = $this->eventStore->loadReverse($streamName, null, null, $metadataMatcher); - - $this->assertFalse($result->valid()); - - $metadataMatcher = new MetadataMatcher(); - $metadataMatcher = $metadataMatcher->withMetadataMatch('int', Operator::GREATER_THAN(), 9); - - $result = $this->eventStore->loadReverse($streamName, null, null, $metadataMatcher); - - $this->assertFalse($result->valid()); - - $metadataMatcher = new MetadataMatcher(); - $metadataMatcher = $metadataMatcher->withMetadataMatch('int2', Operator::GREATER_THAN_EQUALS(), 10); - - $result = $this->eventStore->loadReverse($streamName, null, null, $metadataMatcher); - - $this->assertFalse($result->valid()); - - $metadataMatcher = new MetadataMatcher(); - $metadataMatcher = $metadataMatcher->withMetadataMatch('int3', Operator::LOWER_THAN(), 1); - - $this->eventStore->loadReverse($streamName, null, null, $metadataMatcher); - - $this->assertFalse($result->valid()); - - $metadataMatcher = new MetadataMatcher(); - $metadataMatcher = $metadataMatcher->withMetadataMatch('int4', Operator::LOWER_THAN_EQUALS(), 1); - - $result = $this->eventStore->loadReverse($streamName, null, null, $metadataMatcher); - - $this->assertFalse($result->valid()); - - $metadataMatcher = new MetadataMatcher(); - $metadataMatcher = $metadataMatcher->withMetadataMatch('int', Operator::IN(), [4, 5, 6]); - - $streamEvents = $this->eventStore->loadReverse($streamName, null, null, $metadataMatcher); - - $this->assertCount(1, $streamEvents); - - $metadataMatcher = new MetadataMatcher(); - $metadataMatcher = $metadataMatcher->withMetadataMatch('int', Operator::IN(), [4, 6]); - - $streamEvents = $this->eventStore->loadReverse($streamName, null, null, $metadataMatcher); - - $this->assertFalse($streamEvents->valid()); - - $metadataMatcher = new MetadataMatcher(); - $metadataMatcher = $metadataMatcher->withMetadataMatch('int', Operator::NOT_IN(), [4, 5, 6]); - - $streamEvents = $this->eventStore->loadReverse($streamName, null, null, $metadataMatcher); - - $this->assertFalse($streamEvents->valid()); - - $metadataMatcher = new MetadataMatcher(); - $metadataMatcher = $metadataMatcher->withMetadataMatch('foo', Operator::REGEX(), '^b[a]r$'); - - $streamEvents = $this->eventStore->loadReverse($streamName, null, null, $metadataMatcher); - - $this->assertCount(1, $streamEvents); - - $metadataMatcher = new MetadataMatcher(); - $metadataMatcher = $metadataMatcher->withMetadataMatch('foo', Operator::REGEX(), '^b[a]z$'); - - $streamEvents = $this->eventStore->loadReverse($streamName, null, null, $metadataMatcher); - - $this->assertFalse($streamEvents->valid()); - - $this->expectException(InvalidArgumentException::class); - - $value = new \stdClass(); - $value->foo = 'bar'; - - $metadataMatcher = new MetadataMatcher(); - $metadataMatcher->withMetadataMatch('meta', Operator::EQUALS(), $value); - } - - /** - * @test - */ - public function it_returns_only_matched_message_property(): void - { - $event = UserCreated::with(['name' => 'John'], 1); - $event = $event->withAddedMetadata('foo', 'bar'); - $event = $event->withAddedMetadata('int', 5); - $event = $event->withAddedMetadata('int2', 4); - $event = $event->withAddedMetadata('int3', 6); - $event = $event->withAddedMetadata('int4', 7); - - $uuid = $event->uuid()->toString(); - $createdAt = $event->createdAt()->format('Y-m-d\TH:i:s.u'); - $messageName = $event->messageName(); - - $before = $event->createdAt()->modify('-5 secs')->format('Y-m-d\TH:i:s.u'); - $later = $event->createdAt()->modify('+5 secs')->format('Y-m-d\TH:i:s.u'); - - $streamName = new StreamName('Prooph\Model\User'); - - $stream = new Stream($streamName, new ArrayIterator([$event])); - - $this->eventStore->create($stream); - - $metadataMatcher = new MetadataMatcher(); - $metadataMatcher = $metadataMatcher->withMetadataMatch('uuid', Operator::EQUALS(), 'baz', FieldType::MESSAGE_PROPERTY()); - - $result = $this->eventStore->load($streamName, 1, null, $metadataMatcher); - - $this->assertFalse($result->valid()); - - $metadataMatcher = new MetadataMatcher(); - $metadataMatcher = $metadataMatcher->withMetadataMatch('uuid', Operator::NOT_EQUALS(), $uuid, FieldType::MESSAGE_PROPERTY()); - - $result = $this->eventStore->load($streamName, 1, null, $metadataMatcher); - - $this->assertFalse($result->valid()); - - $metadataMatcher = new MetadataMatcher(); - $metadataMatcher = $metadataMatcher->withMetadataMatch('createdAt', Operator::GREATER_THAN(), $later, FieldType::MESSAGE_PROPERTY()); - - $result = $this->eventStore->load($streamName, 1, null, $metadataMatcher); - - $this->assertFalse($result->valid()); - - $metadataMatcher = new MetadataMatcher(); - $metadataMatcher = $metadataMatcher->withMetadataMatch('created_at', Operator::GREATER_THAN_EQUALS(), $later, FieldType::MESSAGE_PROPERTY()); - - $result = $this->eventStore->load($streamName, 1, null, $metadataMatcher); - - $this->assertFalse($result->valid()); - - $metadataMatcher = new MetadataMatcher(); - $metadataMatcher = $metadataMatcher->withMetadataMatch('created_at', Operator::IN(), [$before, $later], FieldType::MESSAGE_PROPERTY()); - - $result = $this->eventStore->load($streamName, 1, null, $metadataMatcher); - - $this->assertFalse($result->valid()); - - $metadataMatcher = new MetadataMatcher(); - $metadataMatcher = $metadataMatcher->withMetadataMatch('createdAt', Operator::LOWER_THAN(), $before, FieldType::MESSAGE_PROPERTY()); - - $result = $this->eventStore->load($streamName, 1, null, $metadataMatcher); - - $this->assertFalse($result->valid()); - - $metadataMatcher = new MetadataMatcher(); - $metadataMatcher = $metadataMatcher->withMetadataMatch('createdAt', Operator::LOWER_THAN_EQUALS(), $before, FieldType::MESSAGE_PROPERTY()); - - $result = $this->eventStore->load($streamName, 1, null, $metadataMatcher); - - $this->assertFalse($result->valid()); - - $metadataMatcher = new MetadataMatcher(); - $metadataMatcher = $metadataMatcher->withMetadataMatch('created_at', Operator::NOT_IN(), [$before, $createdAt, $later], FieldType::MESSAGE_PROPERTY()); - - $result = $this->eventStore->load($streamName, 1, null, $metadataMatcher); - - $this->assertFalse($result->valid()); - - $metadataMatcher = new MetadataMatcher(); - $metadataMatcher = $metadataMatcher->withMetadataMatch('message_name', Operator::REGEX(), 'foobar', FieldType::MESSAGE_PROPERTY()); - - $result = $this->eventStore->load($streamName, 1, null, $metadataMatcher); - - $this->assertFalse($result->valid()); - } - - /** - * @test - */ - public function it_returns_only_matched_message_property_reverse(): void - { - $event = UserCreated::with(['name' => 'John'], 1); - $event = $event->withAddedMetadata('foo', 'bar'); - $event = $event->withAddedMetadata('int', 5); - $event = $event->withAddedMetadata('int2', 4); - $event = $event->withAddedMetadata('int3', 6); - $event = $event->withAddedMetadata('int4', 7); - - $uuid = $event->uuid()->toString(); - $createdAt = $event->createdAt()->format('Y-m-d\TH:i:s.u'); - $before = $event->createdAt()->modify('-5 secs')->format('Y-m-d\TH:i:s.u'); - $later = $event->createdAt()->modify('+5 secs')->format('Y-m-d\TH:i:s.u'); - - $streamName = new StreamName('Prooph\Model\User'); - $stream = new Stream($streamName, new ArrayIterator([$event])); - - $this->eventStore->create($stream); - - $metadataMatcher = new MetadataMatcher(); - $metadataMatcher = $metadataMatcher->withMetadataMatch('uuid', Operator::EQUALS(), 'baz', FieldType::MESSAGE_PROPERTY()); - - $result = $this->eventStore->loadReverse($streamName, 1, null, $metadataMatcher); - - $this->assertFalse($result->valid()); - - $metadataMatcher = new MetadataMatcher(); - $metadataMatcher = $metadataMatcher->withMetadataMatch('uuid', Operator::NOT_EQUALS(), $uuid, FieldType::MESSAGE_PROPERTY()); - - $result = $this->eventStore->loadReverse($streamName, 1, null, $metadataMatcher); - - $this->assertFalse($result->valid()); - - $metadataMatcher = new MetadataMatcher(); - $metadataMatcher = $metadataMatcher->withMetadataMatch('createdAt', Operator::GREATER_THAN(), $later, FieldType::MESSAGE_PROPERTY()); - - $result = $this->eventStore->loadReverse($streamName, 1, null, $metadataMatcher); - - $this->assertFalse($result->valid()); - - $metadataMatcher = new MetadataMatcher(); - $metadataMatcher = $metadataMatcher->withMetadataMatch('created_at', Operator::GREATER_THAN_EQUALS(), $later, FieldType::MESSAGE_PROPERTY()); - - $result = $this->eventStore->loadReverse($streamName, 1, null, $metadataMatcher); - - $this->assertFalse($result->valid()); - - $metadataMatcher = new MetadataMatcher(); - $metadataMatcher = $metadataMatcher->withMetadataMatch('created_at', Operator::IN(), [$before, $later], FieldType::MESSAGE_PROPERTY()); - - $result = $this->eventStore->loadReverse($streamName, 1, null, $metadataMatcher); - - $this->assertFalse($result->valid()); - - $metadataMatcher = new MetadataMatcher(); - $metadataMatcher = $metadataMatcher->withMetadataMatch('createdAt', Operator::LOWER_THAN(), $before, FieldType::MESSAGE_PROPERTY()); - - $result = $this->eventStore->loadReverse($streamName, 1, null, $metadataMatcher); - - $this->assertFalse($result->valid()); - - $metadataMatcher = new MetadataMatcher(); - $metadataMatcher = $metadataMatcher->withMetadataMatch('createdAt', Operator::LOWER_THAN_EQUALS(), $before, FieldType::MESSAGE_PROPERTY()); - - $result = $this->eventStore->loadReverse($streamName, 1, null, $metadataMatcher); - - $this->assertFalse($result->valid()); - - $metadataMatcher = new MetadataMatcher(); - $metadataMatcher = $metadataMatcher->withMetadataMatch('created_at', Operator::NOT_IN(), [$before, $createdAt, $later], FieldType::MESSAGE_PROPERTY()); - - $result = $this->eventStore->loadReverse($streamName, 1, null, $metadataMatcher); - - $this->assertFalse($result->valid()); - - $metadataMatcher = new MetadataMatcher(); - $metadataMatcher = $metadataMatcher->withMetadataMatch('message_name', Operator::REGEX(), 'foobar', FieldType::MESSAGE_PROPERTY()); - - $result = $this->eventStore->loadReverse($streamName, 1, null, $metadataMatcher); - - $this->assertFalse($result->valid()); - } - - /** - * @test - */ - public function it_loads_empty_stream(): void - { - $streamName = new StreamName('Prooph\Model\User'); - - $this->eventStore->create(new Stream($streamName, new ArrayIterator())); - - $it = $this->eventStore->load($streamName); - - $this->assertFalse($it->valid()); - } - - /** - * @test - */ - public function it_loads_reverse_empty_stream(): void - { - $streamName = new StreamName('Prooph\Model\User'); - - $this->eventStore->create(new Stream($streamName, new ArrayIterator())); - - $it = $this->eventStore->loadReverse($streamName); - - $this->assertFalse($it->valid()); - } - - /** - * @test - */ - public function it_throws_stream_not_found_exception_if_it_loads_nothing(): void - { - $this->expectException(StreamNotFound::class); - - $stream = $this->getTestStream(); - - $this->eventStore->load($stream->streamName()); - } - - /** - * @test - */ - public function it_throws_stream_not_found_exception_if_it_loads_nothing_reverse(): void - { - $this->expectException(StreamNotFound::class); - - $stream = $this->getTestStream(); - - $this->eventStore->loadReverse($stream->streamName()); - } - - /** - * @test - */ - public function it_throws_exception_when_asked_for_unknown_stream_metadata(): void - { - $this->expectException(StreamNotFound::class); - - $this->eventStore->fetchStreamMetadata(new StreamName('unknown')); - } - - /** - * @test - */ - public function it_returns_metadata_when_asked_for_stream_metadata(): void - { - $stream = new Stream(new StreamName('Prooph\Model\User'), new ArrayIterator(), ['foo' => 'bar']); - - $this->eventStore->create($stream); - - $this->assertEquals(['foo' => 'bar'], $this->eventStore->fetchStreamMetadata($stream->streamName())); - } - - /** - * @test - */ - public function it_throws_exception_when_trying_to_delete_unknown_stream(): void - { - $this->expectException(StreamNotFound::class); - - $this->eventStore->delete(new StreamName('unknown')); - } - - /** - * @test - */ - public function it_throws_exception_when_trying_to_append_on_non_existing_stream(): void - { - $this->expectException(StreamNotFound::class); - - $event = UserCreated::with(['name' => 'Alex'], 1); - - $this->eventStore->appendTo(new StreamName('unknown'), new ArrayIterator([$event])); - } - - /** - * @test - */ - public function it_throws_exception_when_trying_to_load_non_existing_stream(): void - { - $this->expectException(StreamNotFound::class); - - $streamName = $this->prophesize(StreamName::class); - $streamName->toString()->willReturn('test')->shouldBeCalled(); - - $this->eventStore->load($streamName->reveal()); - } - - /** - * @test - */ - public function it_deletes_stream(): void - { - $stream = $this->getTestStream(); - - $this->eventStore->create($stream); - - $this->eventStore->delete($stream->streamName()); - - $this->assertFalse($this->eventStore->hasStream($stream->streamName())); - } - - /** - * @test - */ - public function it_can_check_for_stream_existence(): void - { - $streamName = new StreamName('Prooph\Model\User'); - - $this->assertFalse($this->eventStore->hasStream($streamName)); - - $this->eventStore->create($this->getTestStream()); - - $this->assertTrue($this->eventStore->hasStream($streamName)); - } - - /** - * @test - */ - public function it_fetches_stream_names(): void - { - $streamNames = []; - - try { - for ($i = 0; $i < 50; $i++) { - $streamNames[] = 'user-' . $i; - $streamNames[] = 'admin-' . $i; - $this->eventStore->create(new Stream(new StreamName('user-' . $i), new \EmptyIterator(), ['foo' => 'bar'])); - $this->eventStore->create(new Stream(new StreamName('admin-' . $i), new \EmptyIterator(), ['foo' => 'bar'])); - } - - for ($i = 0; $i < 20; $i++) { - $streamName = \uniqid('rand'); - $streamNames[] = $streamName; - $this->eventStore->create(new Stream(new StreamName($streamName), new \EmptyIterator())); - } - - $this->assertCount(1, $this->eventStore->fetchStreamNames('user-0', null, 200, 0)); - $this->assertCount(120, $this->eventStore->fetchStreamNames(null, null, 200, 0)); - $this->assertCount(0, $this->eventStore->fetchStreamNames(null, null, 200, 200)); - $this->assertCount(10, $this->eventStore->fetchStreamNames(null, null, 10, 0)); - $this->assertCount(10, $this->eventStore->fetchStreamNames(null, null, 10, 10)); - $this->assertCount(5, $this->eventStore->fetchStreamNames(null, null, 10, 115)); - - for ($i = 0; $i < 50; $i++) { - $this->assertStringStartsWith('admin-', $this->eventStore->fetchStreamNames(null, null, 1, $i)[0]->toString()); - } - - for ($i = 50; $i < 70; $i++) { - $this->assertStringStartsWith('rand', $this->eventStore->fetchStreamNames(null, null, 1, $i)[0]->toString()); - } - - for ($i = 0; $i < 50; $i++) { - $this->assertStringStartsWith('user-', $this->eventStore->fetchStreamNames(null, null, 1, $i + 70)[0]->toString()); - } - - $this->assertCount(30, $this->eventStore->fetchStreamNamesRegex('s.*er-', null, 30, 0)); - $this->assertCount(20, $this->eventStore->fetchStreamNamesRegex('s.*er-', null, 20, 10)); - $this->assertCount(30, $this->eventStore->fetchStreamNamesRegex('n.*-', (new MetadataMatcher())->withMetadataMatch('foo', Operator::EQUALS(), 'bar'), 30, 0)); - $this->assertCount(0, $this->eventStore->fetchStreamNamesRegex('n.*-', (new MetadataMatcher())->withMetadataMatch('foo', Operator::NOT_EQUALS(), 'bar'), 30, 0)); - $this->assertCount(0, $this->eventStore->fetchStreamNames(null, (new MetadataMatcher())->withMetadataMatch('foo', Operator::NOT_EQUALS(), 'bar'), 30, 0)); - } finally { - foreach ($streamNames as $streamName) { - $this->eventStore->delete(new StreamName($streamName)); - } - } - } - - /** - * @test - */ - public function it_throws_exception_when_fetching_stream_names_using_invalid_regex(): void - { - $this->expectException(InvalidArgumentException::class); - $this->expectExceptionMessage('Invalid regex pattern given'); - - $this->eventStore->fetchStreamNamesRegex('/invalid)/', null, 10, 0); - } - - /** - * @test - */ - public function it_fetches_stream_categories(): void - { - $streamNames = []; - - try { - for ($i = 0; $i < 5; $i++) { - $streamNames[] = 'foo-' . $i; - $streamNames[] = 'bar-' . $i; - $streamNames[] = 'baz-' . $i; - $streamNames[] = 'bam-' . $i; - $streamNames[] = 'foobar-' . $i; - $streamNames[] = 'foobaz-' . $i; - $streamNames[] = 'foobam-' . $i; - $this->eventStore->create(new Stream(new StreamName('foo-' . $i), new \EmptyIterator())); - $this->eventStore->create(new Stream(new StreamName('bar-' . $i), new \EmptyIterator())); - $this->eventStore->create(new Stream(new StreamName('baz-' . $i), new \EmptyIterator())); - $this->eventStore->create(new Stream(new StreamName('bam-' . $i), new \EmptyIterator())); - $this->eventStore->create(new Stream(new StreamName('foobar-' . $i), new \EmptyIterator())); - $this->eventStore->create(new Stream(new StreamName('foobaz-' . $i), new \EmptyIterator())); - $this->eventStore->create(new Stream(new StreamName('foobam-' . $i), new \EmptyIterator())); - } - - for ($i = 0; $i < 20; $i++) { - $streamName = \uniqid('rand'); - $streamNames[] = $streamName; - $this->eventStore->create(new Stream(new StreamName($streamName), new \EmptyIterator())); - } - - $this->assertCount(7, $this->eventStore->fetchCategoryNames(null, 20, 0)); - $this->assertCount(0, $this->eventStore->fetchCategoryNames(null, 20, 20)); - $this->assertCount(3, $this->eventStore->fetchCategoryNames(null, 3, 0)); - $this->assertCount(3, $this->eventStore->fetchCategoryNames(null, 3, 3)); - $this->assertCount(5, $this->eventStore->fetchCategoryNames(null, 10, 2)); - - $this->assertCount(1, $this->eventStore->fetchCategoryNames('foo', 20, 0)); - $this->assertCount(4, $this->eventStore->fetchCategoryNamesRegex('^foo', 20, 0)); - $this->assertCount(2, $this->eventStore->fetchCategoryNamesRegex('^foo', 2, 2)); - } finally { - foreach ($streamNames as $streamName) { - $this->eventStore->delete(new StreamName($streamName)); - } - } - } - - /** - * @test - */ - public function it_throws_exception_when_fetching_stream_categories_using_invalid_regex(): void - { - $this->expectException(InvalidArgumentException::class); - $this->expectExceptionMessage('Invalid regex pattern given'); - - $this->eventStore->fetchCategoryNamesRegex('invalid)', 10, 0); - } - - /** - * @test - */ - public function it_throws_exception_given_invalid_metadata_value(): void - { - $this->expectException(InvalidArgumentException::class); - - $value = new \stdClass(); - $value->foo = 'bar'; - - $metadataMatcher = new MetadataMatcher(); - $metadataMatcher->withMetadataMatch('key', Operator::EQUALS(), $value); - } - - /** - * @test - */ - public function it_throws_on_invalid_field_for_message_property(): void - { - $this->expectException(\UnexpectedValueException::class); - - $event = UserCreated::with(['name' => 'John'], 1); - $event = $event->withAddedMetadata('foo', 'bar'); - $event = $event->withAddedMetadata('int', 5); - $event = $event->withAddedMetadata('int2', 4); - $event = $event->withAddedMetadata('int3', 6); - $event = $event->withAddedMetadata('int4', 7); - - $streamName = new StreamName('Prooph\Model\User'); - $stream = new Stream($streamName, new ArrayIterator([$event])); - - $this->eventStore->create($stream); - - $metadataMatcher = $this->prophesize(MetadataMatcher::class); - $metadataMatcher->data()->willReturn([[ - 'field' => 'foo', - 'value' => 'bar', - 'operator' => Operator::EQUALS(), - 'fieldType' => FieldType::MESSAGE_PROPERTY(), - ]])->shouldBeCalled(); - - $this->eventStore->load($streamName, 1, null, $metadataMatcher->reveal()); - } - - public function getMatchingMetadata(): array - { - return [ - [['snapshot' => true]], - [['some_id' => 123]], - [['foo' => 'bar']], - [['snapshot' => true, 'some_id' => 123, 'foo' => 'bar']], - ]; - } - - /** - * @test - */ - public function it_return_stream_iterator_for_load(): void - { - $this->eventStore->create($this->getTestStream()); - - $this->assertInstanceOf(StreamIterator::class, $this->eventStore->load(new StreamName('Prooph\Model\User'))); - } - - /** - * @test - */ - public function it_return_stream_iterator_for_load_reversed(): void - { - $this->eventStore->create($this->getTestStream()); - - $this->assertInstanceOf(StreamIterator::class, $this->eventStore->loadReverse(new StreamName('Prooph\Model\User'))); - } -} diff --git a/tests/ActionEventEmitterEventStoreTest.php b/tests/ActionEventEmitterEventStoreTest.php deleted file mode 100644 index a0835073..00000000 --- a/tests/ActionEventEmitterEventStoreTest.php +++ /dev/null @@ -1,560 +0,0 @@ - - * (c) 2015-2018 Sascha-Oliver Prolic - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ProophTest\EventStore; - -use ArrayIterator; -use Prooph\Common\Event\ActionEvent; -use Prooph\Common\Event\ProophActionEventEmitter; -use Prooph\EventStore\ActionEventEmitterEventStore; -use Prooph\EventStore\EventStore; -use Prooph\EventStore\Exception\ConcurrencyException; -use Prooph\EventStore\Exception\StreamExistsAlready; -use Prooph\EventStore\Exception\StreamNotFound; -use Prooph\EventStore\Metadata\MetadataMatcher; -use Prooph\EventStore\Metadata\Operator; -use Prooph\EventStore\StreamName; -use ProophTest\EventStore\Mock\UsernameChanged; - -class ActionEventEmitterEventStoreTest extends ActionEventEmitterEventStoreTestCase -{ - use EventStoreTestStreamTrait; - - /** - * @test - */ - public function it_breaks_loading_a_stream_when_listener_stops_propagation_but_does_not_provide_a_stream(): void - { - $this->expectException(StreamNotFound::class); - - $stream = $this->getTestStream(); - - $this->eventStore->create($stream); - - $this->eventStore->attach( - 'load', - function (ActionEvent $event): void { - $event->stopPropagation(true); - }, - 1000 - ); - - $this->eventStore->load(new StreamName('Prooph\Model\User')); - } - - /** - * @test - */ - public function it_cannot_create_a_stream_with_same_name_twice(): void - { - $this->expectException(StreamExistsAlready::class); - - $stream = $this->getTestStream(); - - $this->eventStore->create($stream); - $this->eventStore->create($stream); - } - - /** - * @test - */ - public function it_throws_exception_when_trying_to_append_to_non_existing_stream(): void - { - $this->expectException(StreamNotFound::class); - - $streamName = $this->prophesize(StreamName::class); - $streamName->toString()->willReturn('test'); - - $this->eventStore->appendTo($streamName->reveal(), new \ArrayIterator()); - } - - /** - * @test - */ - public function it_throws_concurrency_exception_when_it_happens(): void - { - $this->expectException(ConcurrencyException::class); - - $eventStore = $this->prophesize(EventStore::class); - $eventEmitter = new ProophActionEventEmitter(ActionEventEmitterEventStore::ALL_EVENTS); - - $actionEventStore = new ActionEventEmitterEventStore($eventStore->reveal(), $eventEmitter); - - $streamName = new StreamName('test'); - $events = new ArrayIterator(); - - $eventStore->appendTo($streamName, $events)->willThrow(ConcurrencyException::class)->shouldBeCalled(); - - $actionEventStore->appendTo($streamName, $events); - } - - /** - * @test - */ - public function it_throws_exception_when_trying_to_load_non_existing_stream(): void - { - $this->expectException(StreamNotFound::class); - - $streamName = $this->prophesize(StreamName::class); - $streamName->toString()->willReturn('test'); - - $this->assertNull($this->eventStore->load($streamName->reveal())); - } - - /** - * @test - */ - public function it_throws_exception_when_trying_to_load_reverse_non_existing_stream(): void - { - $this->expectException(StreamNotFound::class); - - $streamName = $this->prophesize(StreamName::class); - $streamName->toString()->willReturn('test'); - - $this->assertNull($this->eventStore->loadReverse($streamName->reveal())); - } - - /** - * @test - */ - public function it_loads_events_in_reverse_order(): void - { - $stream = $this->getTestStream(); - - $this->eventStore->create($stream); - - $streamEventVersion2 = UsernameChanged::with( - ['new_name' => 'John Doe'], - 2 - ); - - $streamEventVersion2 = $streamEventVersion2->withAddedMetadata('snapshot', true); - - $streamEventVersion3 = UsernameChanged::with( - ['new_name' => 'Jane Doe'], - 3 - ); - - $streamEventVersion3 = $streamEventVersion3->withAddedMetadata('snapshot', false); - - $streamEventVersion4 = UsernameChanged::with( - ['new_name' => 'Jane Dole'], - 4 - ); - - $streamEventVersion4 = $streamEventVersion4->withAddedMetadata('snapshot', false); - - $this->eventStore->appendTo($stream->streamName(), new ArrayIterator([ - $streamEventVersion2, - $streamEventVersion3, - $streamEventVersion4, - ])); - - $loadedEvents = $this->eventStore->loadReverse($stream->streamName(), 3, 2); - - $this->assertCount(2, $loadedEvents); - - $loadedEvents->rewind(); - - $this->assertFalse($loadedEvents->current()->metadata()['snapshot']); - $loadedEvents->next(); - $this->assertTrue($loadedEvents->current()->metadata()['snapshot']); - } - - /** - * @test - */ - public function it_throws_exception_when_listener_stops_loading_events_and_does_not_provide_loaded_events(): void - { - $this->expectException(StreamNotFound::class); - - $stream = $this->getTestStream(); - - $this->eventStore->create($stream); - - $this->eventStore->attach( - 'load', - function (ActionEvent $event): void { - $event->stopPropagation(true); - }, - 1000 - ); - - $this->eventStore->load($stream->streamName()); - } - - /** - * @test - */ - public function it_throws_exception_when_listener_stops_loading_events_and_does_not_provide_loaded_events_reverse(): void - { - $this->expectException(StreamNotFound::class); - - $stream = $this->getTestStream(); - - $this->eventStore->create($stream); - - $this->eventStore->attach( - 'loadReverse', - function (ActionEvent $event): void { - $event->stopPropagation(true); - }, - 1000 - ); - - $this->eventStore->loadReverse($stream->streamName()); - } - - /** - * @test - */ - public function it_throws_exception_when_trying_to_delete_unknown_stream(): void - { - $this->expectException(StreamNotFound::class); - - $streamName = $this->prophesize(StreamName::class); - $streamName->toString()->willReturn('foo')->shouldBeCalled(); - - $this->eventStore->delete($streamName->reveal()); - } - - /** - * @test - */ - public function it_does_not_append_events_when_listener_stops_propagation(): void - { - $recordedEvents = []; - - $this->eventStore->attach( - 'create', - function (ActionEvent $event) use (&$recordedEvents): void { - foreach ($event->getParam('recordedEvents', new \ArrayIterator()) as $recordedEvent) { - $recordedEvents[] = $recordedEvent; - } - }, - -1000 - ); - - $this->eventStore->attach( - 'appendTo', - function (ActionEvent $event) use (&$recordedEvents): void { - foreach ($event->getParam('recordedEvents', new \ArrayIterator()) as $recordedEvent) { - $recordedEvents[] = $recordedEvent; - } - }, - -1000 - ); - - $this->eventStore->create($this->getTestStream()); - - $this->eventStore->attach( - 'appendTo', - function (ActionEvent $event): void { - $event->stopPropagation(true); - }, - 1000 - ); - - $secondStreamEvent = UsernameChanged::with( - ['new_name' => 'John Doe'], - 2 - ); - - $this->eventStore->appendTo(new StreamName('Prooph\Model\User'), new ArrayIterator([$secondStreamEvent])); - - $this->assertCount(1, $this->eventStore->load(new StreamName('Prooph\Model\User'))); - } - - /** - * @test - */ - public function it_uses_stream_provided_by_listener_when_listener_stops_propagation(): void - { - $stream = $this->getTestStream(); - - $this->eventStore->create($stream); - - $this->eventStore->attach( - 'load', - function (ActionEvent $event): void { - $event->setParam('streamEvents', new ArrayIterator()); - $event->stopPropagation(true); - }, - 1000 - ); - - $emptyStream = $this->eventStore->load($stream->streamName()); - - $this->assertCount(0, $emptyStream); - } - - /** - * @test - */ - public function it_returns_listener_events_when_listener_stops_loading_events_and_provide_loaded_events(): void - { - $stream = $this->getTestStream(); - - $this->eventStore->create($stream); - - $streamEventWithMetadata = UsernameChanged::with( - ['new_name' => 'John Doe'], - 2 - ); - - $streamEventWithMetadata = $streamEventWithMetadata->withAddedMetadata('snapshot', true); - - $this->eventStore->appendTo($stream->streamName(), new ArrayIterator([$streamEventWithMetadata])); - - $this->eventStore->attach( - 'load', - function (ActionEvent $event): void { - $streamEventWithMetadataButOtherUuid = UsernameChanged::with( - ['new_name' => 'John Doe'], - 2 - ); - - $streamEventWithMetadataButOtherUuid = $streamEventWithMetadataButOtherUuid->withAddedMetadata('snapshot', true); - - $event->setParam('streamEvents', new ArrayIterator([$streamEventWithMetadataButOtherUuid])); - $event->stopPropagation(true); - }, - 1000 - ); - - $metadataMatcher = new MetadataMatcher(); - $metadataMatcher = $metadataMatcher->withMetadataMatch('snapshot', Operator::EQUALS(), true); - - $loadedEvents = $this->eventStore->load($stream->streamName(), 1, null, $metadataMatcher); - - $this->assertCount(1, $loadedEvents); - - $loadedEvents->rewind(); - - $this->assertNotEquals($streamEventWithMetadata->uuid()->toString(), $loadedEvents->current()->uuid()->toString()); - } - - /** - * @test - */ - public function it_appends_events_to_stream_and_records_them(): void - { - $recordedEvents = []; - - $this->eventStore->attach( - 'create', - function (ActionEvent $event) use (&$recordedEvents): void { - $stream = $event->getParam('stream'); - - foreach ($stream->streamEvents() as $recordedEvent) { - $recordedEvents[] = $recordedEvent; - } - }, - -1000 - ); - - $this->eventStore->attach( - 'appendTo', - function (ActionEvent $event) use (&$recordedEvents): void { - foreach ($event->getParam('streamEvents', new \ArrayIterator()) as $recordedEvent) { - $recordedEvents[] = $recordedEvent; - } - }, - -1000 - ); - - $this->eventStore->create($this->getTestStream()); - - $secondStreamEvent = UsernameChanged::with( - ['new_name' => 'John Doe'], - 2 - ); - - $this->eventStore->appendTo(new StreamName('Prooph\Model\User'), new ArrayIterator([$secondStreamEvent])); - - $this->assertCount(2, $recordedEvents); - } - - /** - * @test - */ - public function it_creates_a_new_stream_and_records_the_stream_events_and_deletes(): void - { - $recordedEvents = []; - - $streamName = new StreamName('Prooph\Model\User'); - - $this->eventStore->attach( - 'create', - function (ActionEvent $event) use (&$recordedEvents): void { - $stream = $event->getParam('stream'); - - foreach ($stream->streamEvents() as $recordedEvent) { - $recordedEvents[] = $recordedEvent; - } - }, - -1000 - ); - - $stream = $this->getTestStream(); - - $this->eventStore->create($stream); - - $streamEvents = $this->eventStore->load($streamName); - - $this->assertCount(1, $streamEvents); - - $this->assertCount(1, $recordedEvents); - - $this->assertEquals( - [ - 'foo' => 'bar', - ], - $this->eventStore->fetchStreamMetadata($streamName) - ); - - $this->assertTrue($this->eventStore->hasStream($streamName)); - - $this->eventStore->delete($streamName); - - $this->assertFalse($this->eventStore->hasStream($streamName)); - } - - /** - * @test - */ - public function it_throws_exception_when_asked_for_unknown_stream_metadata(): void - { - $this->expectException(StreamNotFound::class); - - $streamName = $this->prophesize(StreamName::class); - $streamName->toString()->willReturn('unknown')->shouldBeCalled(); - - $this->eventStore->fetchStreamMetadata($streamName->reveal()); - } - - /** - * @test - */ - public function it_throws_exception_when_asked_for_stream_metadata_and_event_gets_stopped(): void - { - $this->expectException(StreamNotFound::class); - - $streamName = $this->prophesize(StreamName::class); - $streamName->toString()->willReturn('test')->shouldBeCalled(); - - $this->eventStore->attach( - ActionEventEmitterEventStore::EVENT_FETCH_STREAM_METADATA, - function (ActionEvent $event) { - $event->stopPropagation(); - }, - 1000 - ); - - $this->eventStore->fetchStreamMetadata($streamName->reveal()); - } - - /** - * @test - */ - public function it_updates_stream_metadata(): void - { - $stream = $this->getTestStream(); - - $this->eventStore->create($stream); - - $this->eventStore->updateStreamMetadata($stream->streamName(), ['new' => 'values']); - - $this->assertEquals( - [ - 'new' => 'values', - ], - $this->eventStore->fetchStreamMetadata($stream->streamName()) - ); - } - - /** - * @test - */ - public function it_throws_stream_not_found_exception_when_trying_to_update_metadata_on_unknown_stream(): void - { - $this->expectException(StreamNotFound::class); - - $this->eventStore->updateStreamMetadata(new StreamName('unknown'), []); - } - - /** - * @test - */ - public function it_fetches_stream_names(): void - { - $eventStore = $this->prophesize(EventStore::class); - $eventStore->fetchStreamNames('foo', null, 10, 20)->shouldBeCalled(); - - $wrapper = new ActionEventEmitterEventStore($eventStore->reveal(), new ProophActionEventEmitter()); - - $wrapper->fetchStreamNames('foo', null, 10, 20); - } - - /** - * @test - */ - public function it_fetches_stream_names_regex(): void - { - $eventStore = $this->prophesize(EventStore::class); - $eventStore->fetchStreamNamesRegex('foo', null, 10, 20)->shouldBeCalled(); - - $wrapper = new ActionEventEmitterEventStore($eventStore->reveal(), new ProophActionEventEmitter()); - - $wrapper->fetchStreamNamesRegex('foo', null, 10, 20); - } - - /** - * @test - */ - public function it_fetches_category_names(): void - { - $eventStore = $this->prophesize(EventStore::class); - $eventStore->fetchCategoryNames('foo', 10, 20)->shouldBeCalled(); - - $wrapper = new ActionEventEmitterEventStore($eventStore->reveal(), new ProophActionEventEmitter()); - - $wrapper->fetchCategoryNames('foo', 10, 20); - } - - /** - * @test - */ - public function it_fetches_category_names_regex(): void - { - $eventStore = $this->prophesize(EventStore::class); - $eventStore->fetchCategoryNamesRegex('foo', 10, 20)->shouldBeCalled(); - - $wrapper = new ActionEventEmitterEventStore($eventStore->reveal(), new ProophActionEventEmitter()); - - $wrapper->fetchCategoryNamesRegex('foo', 10, 20); - } - - /** - * @test - */ - public function it_returns_inner_event_store(): void - { - $eventStore = $this->prophesize(EventStore::class); - $eventStore = $eventStore->reveal(); - - $wrapper = new ActionEventEmitterEventStore($eventStore, new ProophActionEventEmitter()); - - $this->assertSame($eventStore, $wrapper->getInnerEventStore()); - } -} diff --git a/tests/ActionEventEmitterEventStoreTestCase.php b/tests/ActionEventEmitterEventStoreTestCase.php deleted file mode 100644 index b7569324..00000000 --- a/tests/ActionEventEmitterEventStoreTestCase.php +++ /dev/null @@ -1,34 +0,0 @@ - - * (c) 2015-2018 Sascha-Oliver Prolic - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ProophTest\EventStore; - -use PHPUnit\Framework\TestCase; -use Prooph\Common\Event\ProophActionEventEmitter; -use Prooph\EventStore\ActionEventEmitterEventStore; -use Prooph\EventStore\InMemoryEventStore; - -abstract class ActionEventEmitterEventStoreTestCase extends TestCase -{ - /** - * @var ActionEventEmitterEventStore - */ - protected $eventStore; - - protected function setUp(): void - { - $eventEmitter = new ProophActionEventEmitter(ActionEventEmitterEventStore::ALL_EVENTS); - - $this->eventStore = new ActionEventEmitterEventStore(new InMemoryEventStore(), $eventEmitter); - } -} diff --git a/tests/Container/InMemoryEventStoreFactoryTest.php b/tests/Container/InMemoryEventStoreFactoryTest.php deleted file mode 100644 index bc196788..00000000 --- a/tests/Container/InMemoryEventStoreFactoryTest.php +++ /dev/null @@ -1,266 +0,0 @@ - - * (c) 2015-2018 Sascha-Oliver Prolic - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ProophTest\EventStore\Container; - -use PHPUnit\Framework\TestCase; -use Prooph\Common\Event\ActionEventEmitter; -use Prooph\Common\Messaging\Message; -use Prooph\EventStore\ActionEventEmitterEventStore; -use Prooph\EventStore\Container\InMemoryEventStoreFactory; -use Prooph\EventStore\Exception\ConfigurationException; -use Prooph\EventStore\Exception\InvalidArgumentException; -use Prooph\EventStore\InMemoryEventStore; -use Prooph\EventStore\Metadata\MetadataEnricher; -use Prooph\EventStore\NonTransactionalInMemoryEventStore; -use Prooph\EventStore\Plugin\Plugin; -use Prooph\EventStore\ReadOnlyEventStoreWrapper; -use Prooph\EventStore\Stream; -use Prooph\EventStore\StreamName; -use Prooph\EventStore\TransactionalActionEventEmitterEventStore; -use ProophTest\EventStore\Mock\UserCreated; -use ProophTest\EventStore\Mock\UsernameChanged; -use Prophecy\Argument; -use Psr\Container\ContainerInterface; - -class InMemoryEventStoreFactoryTest extends TestCase -{ - /** - * @test - */ - public function it_creates_event_store_with_default_event_emitter(): void - { - $config['prooph']['event_store']['default'] = []; - - $containerMock = $this->getMockForAbstractClass(ContainerInterface::class); - $containerMock->expects($this->at(0))->method('get')->with('config')->willReturn($config); - - $factory = new InMemoryEventStoreFactory(); - $eventStore = $factory($containerMock); - - $this->assertInstanceOf(TransactionalActionEventEmitterEventStore::class, $eventStore); - } - - /** - * @test - */ - public function it_creates_event_store_without_event_emitter(): void - { - $config['prooph']['event_store']['default'] = ['wrap_action_event_emitter' => false]; - - $containerMock = $this->getMockForAbstractClass(ContainerInterface::class); - $containerMock->expects($this->at(0))->method('get')->with('config')->willReturn($config); - - $factory = new InMemoryEventStoreFactory(); - $eventStore = $factory($containerMock); - - $this->assertInstanceOf(InMemoryEventStore::class, $eventStore); - } - - /** - * @test - */ - public function it_creates_non_transactional_event_store_without_event_emitter(): void - { - $config['prooph']['event_store']['default'] = ['wrap_action_event_emitter' => false, 'transactional' => false]; - - $containerMock = $this->getMockForAbstractClass(ContainerInterface::class); - $containerMock->expects($this->at(0))->method('get')->with('config')->willReturn($config); - - $factory = new InMemoryEventStoreFactory(); - $eventStore = $factory($containerMock); - - $this->assertInstanceOf(NonTransactionalInMemoryEventStore::class, $eventStore); - } - - /** - * @test - */ - public function it_creates_read_only_event_store(): void - { - $config['prooph']['event_store']['default'] = ['wrap_action_event_emitter' => false, 'read_only' => true]; - - $containerMock = $this->getMockForAbstractClass(ContainerInterface::class); - $containerMock->expects($this->at(0))->method('get')->with('config')->willReturn($config); - - $factory = new InMemoryEventStoreFactory(); - $eventStore = $factory($containerMock); - - $this->assertInstanceOf(ReadOnlyEventStoreWrapper::class, $eventStore); - } - - /** - * @test - */ - public function it_creates_event_store_with_default_event_emitter_via_callstatic(): void - { - $config['prooph']['event_store']['another'] = []; - - $containerMock = $this->getMockForAbstractClass(ContainerInterface::class); - $containerMock->expects($this->at(0))->method('get')->with('config')->willReturn($config); - - $type = 'another'; - $eventStore = InMemoryEventStoreFactory::$type($containerMock); - - $this->assertInstanceOf(TransactionalActionEventEmitterEventStore::class, $eventStore); - } - - /** - * @test - */ - public function it_creates_non_transactional_event_store_with_non_transactional_event_emitter_via_callstatic(): void - { - $config['prooph']['event_store']['another'] = ['transactional' => false]; - - $containerMock = $this->getMockForAbstractClass(ContainerInterface::class); - $containerMock->expects($this->at(0))->method('get')->with('config')->willReturn($config); - - $type = 'another'; - $eventStore = InMemoryEventStoreFactory::$type($containerMock); - - $this->assertInstanceOf(ActionEventEmitterEventStore::class, $eventStore); - } - - /** - * @test - */ - public function it_injects_custom_event_emitter(): void - { - $config['prooph']['event_store']['default']['event_emitter'] = 'event_emitter'; - - $eventEmitterMock = $this->getMockForAbstractClass(ActionEventEmitter::class); - - $containerMock = $this->getMockForAbstractClass(ContainerInterface::class); - $containerMock->expects($this->at(0))->method('get')->with('config')->willReturn($config); - $containerMock->expects($this->at(1))->method('get')->with('event_emitter')->willReturn($eventEmitterMock); - - $factory = new InMemoryEventStoreFactory(); - $eventStore = $factory($containerMock); - - $this->assertInstanceOf(TransactionalActionEventEmitterEventStore::class, $eventStore); - } - - /** - * @test - */ - public function it_injects_plugins(): void - { - $config['prooph']['event_store']['default']['plugins'][] = 'plugin'; - - $featureMock = $this->prophesize(Plugin::class); - $featureMock->attachToEventStore(Argument::type(TransactionalActionEventEmitterEventStore::class))->shouldBeCalled(); - - $containerMock = $this->getMockForAbstractClass(ContainerInterface::class); - $containerMock->expects($this->at(0))->method('get')->with('config')->willReturn($config); - $containerMock->expects($this->at(1))->method('get')->with('plugin')->willReturn($featureMock->reveal()); - - $factory = new InMemoryEventStoreFactory(); - $eventStore = $factory($containerMock); - - $this->assertInstanceOf(TransactionalActionEventEmitterEventStore::class, $eventStore); - } - - /** - * @test - */ - public function it_throws_exception_when_invalid_plugin_configured(): void - { - $this->expectException(ConfigurationException::class); - $this->expectExceptionMessage('Plugin plugin does not implement the Plugin interface'); - - $config['prooph']['event_store']['default']['plugins'][] = 'plugin'; - - $featureMock = 'foo'; - - $containerMock = $this->getMockForAbstractClass(ContainerInterface::class); - $containerMock->expects($this->at(0))->method('get')->with('config')->willReturn($config); - $containerMock->expects($this->at(1))->method('get')->with('plugin')->willReturn($featureMock); - - $factory = new InMemoryEventStoreFactory(); - $factory($containerMock); - } - - /** - * @test - */ - public function it_injects_metadata_enrichers(): void - { - $config['prooph']['event_store']['default']['metadata_enrichers'][] = 'metadata_enricher1'; - $config['prooph']['event_store']['default']['metadata_enrichers'][] = 'metadata_enricher2'; - - $metadataEnricher1 = $this->prophesize(MetadataEnricher::class); - $metadataEnricher2 = $this->prophesize(MetadataEnricher::class); - - $container = $this->prophesize(ContainerInterface::class); - $container->get('config')->willReturn($config); - $container->get('metadata_enricher1')->willReturn($metadataEnricher1->reveal()); - $container->get('metadata_enricher2')->willReturn($metadataEnricher2->reveal()); - - $factory = new InMemoryEventStoreFactory(); - $eventStore = $factory($container->reveal()); - - $this->assertInstanceOf(TransactionalActionEventEmitterEventStore::class, $eventStore); - - // Some events to inject into the event store - $events = [ - UserCreated::with(['name' => 'John'], 1), - UsernameChanged::with(['name' => 'Jane'], 2), - ]; - - // The metadata enrichers should be called as many - // times as there are events - $metadataEnricher1 - ->enrich(Argument::type(Message::class)) - ->shouldBeCalledTimes(\count($events)) - ->willReturnArgument(0); - - $metadataEnricher2 - ->enrich(Argument::type(Message::class)) - ->shouldBeCalledTimes(\count($events)) - ->willReturnArgument(0); - - $stream = new Stream(new StreamName('test'), new \ArrayIterator($events)); - - /* @var InMemoryEventStore $eventStore */ - $eventStore->create($stream); - } - - /** - * @test - */ - public function it_throws_exception_when_invalid_metadata_enricher_configured(): void - { - $this->expectException(ConfigurationException::class); - $this->expectExceptionMessage('Metadata enricher foobar does not implement the MetadataEnricher interface'); - - $config['prooph']['event_store']['default']['metadata_enrichers'][] = 'foobar'; - - $container = $this->prophesize(ContainerInterface::class); - $container->get('config')->willReturn($config); - $container->get('foobar')->willReturn(new \stdClass()); - - $factory = new InMemoryEventStoreFactory(); - $factory($container->reveal()); - } - - /** - * @test - */ - public function it_throws_exception_when_invalid_container_given_to_callstatic(): void - { - $this->expectException(InvalidArgumentException::class); - - $type = 'another'; - InMemoryEventStoreFactory::$type('invalid container'); - } -} diff --git a/tests/Container/InMemoryProjectionManagerFactoryTest.php b/tests/Container/InMemoryProjectionManagerFactoryTest.php deleted file mode 100644 index 9a635634..00000000 --- a/tests/Container/InMemoryProjectionManagerFactoryTest.php +++ /dev/null @@ -1,77 +0,0 @@ - - * (c) 2015-2018 Sascha-Oliver Prolic - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ProophTest\EventStore\Container; - -use PHPUnit\Framework\TestCase; -use Prooph\EventStore\Container\InMemoryProjectionManagerFactory; -use Prooph\EventStore\Exception\InvalidArgumentException; -use Prooph\EventStore\InMemoryEventStore; -use Prooph\EventStore\Projection\InMemoryProjectionManager; -use Psr\Container\ContainerInterface; - -class InMemoryProjectionManagerFactoryTest extends TestCase -{ - /** - * @test - */ - public function it_creates_projection_manager(): void - { - $config['prooph']['projection_manager']['default'] = [ - 'event_store' => 'my_event_store', - ]; - - $eventStore = new InMemoryEventStore(); - - $container = $this->prophesize(ContainerInterface::class); - $container->get('config')->willReturn($config)->shouldBeCalled(); - $container->get('my_event_store')->willReturn($eventStore)->shouldBeCalled(); - - $factory = new InMemoryProjectionManagerFactory(); - $projectionManager = $factory($container->reveal()); - - $this->assertInstanceOf(InMemoryProjectionManager::class, $projectionManager); - } - - /** - * @test - */ - public function it_creates_projection_manager_via_callstatic(): void - { - $config['prooph']['projection_manager']['default'] = [ - 'event_store' => 'my_event_store', - ]; - - $eventStore = new InMemoryEventStore(); - - $container = $this->prophesize(ContainerInterface::class); - $container->get('config')->willReturn($config)->shouldBeCalled(); - $container->get('my_event_store')->willReturn($eventStore)->shouldBeCalled(); - - $name = 'default'; - $projectionManager = InMemoryProjectionManagerFactory::$name($container->reveal()); - - $this->assertInstanceOf(InMemoryProjectionManager::class, $projectionManager); - } - - /** - * @test - */ - public function it_throws_exception_when_invalid_container_given_to_callstatic(): void - { - $this->expectException(InvalidArgumentException::class); - - $type = 'another'; - InMemoryProjectionManagerFactory::$type('invalid container'); - } -} diff --git a/tests/EventStoreTestStreamTrait.php b/tests/EventStoreTestStreamTrait.php deleted file mode 100644 index 38867e21..00000000 --- a/tests/EventStoreTestStreamTrait.php +++ /dev/null @@ -1,32 +0,0 @@ - - * (c) 2015-2018 Sascha-Oliver Prolic - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ProophTest\EventStore; - -use ArrayIterator; -use Prooph\EventStore\Stream; -use Prooph\EventStore\StreamName; -use ProophTest\EventStore\Mock\UserCreated; - -trait EventStoreTestStreamTrait -{ - protected function getTestStream(): Stream - { - $streamEvent = UserCreated::with( - ['name' => 'Alex', 'email' => 'contact@prooph.de'], - 1 - ); - - return new Stream(new StreamName('Prooph\Model\User'), new ArrayIterator([$streamEvent]), ['foo' => 'bar']); - } -} diff --git a/tests/Example/QuickStartTest.php b/tests/Example/QuickStartTest.php deleted file mode 100644 index 1ef9c564..00000000 --- a/tests/Example/QuickStartTest.php +++ /dev/null @@ -1,40 +0,0 @@ - - * (c) 2015-2018 Sascha-Oliver Prolic - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ProophTest\EventStore\Example; - -use PHPUnit\Framework\TestCase; - -class QuickStartTest extends TestCase -{ - /** - * @test - */ - public function it_provides_the_correct_example_output(): void - { - $pattern = \sprintf( - '~^Event with name Prooph\\\\EventStore\\\\QuickStart\\\\Event\\\\QuickStartSucceeded was recorded\. It occurred on %s ///\n\nIt works$~', - '\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}' - ); - - $this->assertRegExp($pattern, $this->getQuickstartOutput()); - } - - private function getQuickstartOutput(): string - { - \ob_start(); - include __DIR__ . '/../../examples/quickstart.php'; - - return \ob_get_clean(); - } -} diff --git a/tests/InMemoryEventStoreTest.php b/tests/InMemoryEventStoreTest.php deleted file mode 100644 index c2aa705f..00000000 --- a/tests/InMemoryEventStoreTest.php +++ /dev/null @@ -1,32 +0,0 @@ - - * (c) 2015-2018 Sascha-Oliver Prolic - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ProophTest\EventStore; - -use Prooph\EventStore\InMemoryEventStore; - -class InMemoryEventStoreTest extends AbstractEventStoreTest -{ - use EventStoreTestStreamTrait; - use TransactionalEventStoreTestTrait; - - /** - * @var InMemoryEventStore - */ - protected $eventStore; - - protected function setUp(): void - { - $this->eventStore = new InMemoryEventStore(); - } -} diff --git a/tests/Metadata/MetadataEnricherAggregateTest.php b/tests/Metadata/MetadataEnricherAggregateTest.php deleted file mode 100644 index e6522e7f..00000000 --- a/tests/Metadata/MetadataEnricherAggregateTest.php +++ /dev/null @@ -1,82 +0,0 @@ - - * (c) 2015-2018 Sascha-Oliver Prolic - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ProophTest\EventStore\Metadata; - -use PHPUnit\Framework\TestCase; -use Prooph\Common\Messaging\Message; -use Prooph\EventStore\Exception\InvalidArgumentException; -use Prooph\EventStore\Metadata\MetadataEnricher; -use Prooph\EventStore\Metadata\MetadataEnricherAggregate; -use ProophTest\EventStore\Mock\TestDomainEvent; -use Prophecy\Argument; - -class MetadataEnricherAggregateTest extends TestCase -{ - /** - * @test - */ - public function it_aggregates_metadata_enrichers(): void - { - // Mocks - $metadataEnricher1 = $this->prophesize(MetadataEnricher::class); - $metadataEnricher2 = $this->prophesize(MetadataEnricher::class); - - // Class under test - $metadataEnricherAgg = new MetadataEnricherAggregate([ - $metadataEnricher1->reveal(), - $metadataEnricher2->reveal(), - ]); - - // Initial payload and expected data - $originalEvent = TestDomainEvent::with(['foo' => 'bar'], 1); - $eventAfterEnricher1 = $originalEvent->withAddedMetadata('meta1', 'data1'); - $eventAfterEnricher2 = $eventAfterEnricher1->withAddedMetadata('meta2', 'data2'); - - // Prepare mock - $metadataEnricher1 - ->enrich(Argument::type(Message::class)) - ->shouldBeCalledTimes(1) - ->willReturn($eventAfterEnricher1); - - $metadataEnricher2 - ->enrich(Argument::type(Message::class)) - ->shouldBeCalledTimes(1) - ->willReturn($eventAfterEnricher2); - - // Call method under test - $enrichedEvent = $metadataEnricherAgg->enrich($originalEvent); - - // Assertions - $this->assertEquals($originalEvent->payload(), $enrichedEvent->payload()); - $this->assertEquals($originalEvent->version(), $enrichedEvent->version()); - $this->assertEquals($originalEvent->createdAt(), $enrichedEvent->createdAt()); - - $expectedMetadata = ['meta1' => 'data1', 'meta2' => 'data2', '_aggregate_version' => 1]; - $this->assertEquals($expectedMetadata, $enrichedEvent->metadata()); - } - - /** - * @test - */ - public function it_only_accept_correct_instances(): void - { - $this->expectException(InvalidArgumentException::class); - - new MetadataEnricherAggregate([ - $this->prophesize(MetadataEnricher::class)->reveal(), - new \stdClass(), - $this->prophesize(MetadataEnricher::class)->reveal(), - ]); - } -} diff --git a/tests/Metadata/MetadataEnricherPluginTest.php b/tests/Metadata/MetadataEnricherPluginTest.php deleted file mode 100644 index 0fa68b8a..00000000 --- a/tests/Metadata/MetadataEnricherPluginTest.php +++ /dev/null @@ -1,135 +0,0 @@ - - * (c) 2015-2018 Sascha-Oliver Prolic - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ProophTest\EventStore\Metadata; - -use PHPUnit\Framework\TestCase; -use Prooph\Common\Event\DefaultActionEvent; -use Prooph\Common\Event\ProophActionEventEmitter; -use Prooph\Common\Messaging\Message; -use Prooph\EventStore\ActionEventEmitterEventStore; -use Prooph\EventStore\InMemoryEventStore; -use Prooph\EventStore\Metadata\MetadataEnricher; -use Prooph\EventStore\Metadata\MetadataEnricherPlugin; -use Prooph\EventStore\Stream; -use Prooph\EventStore\StreamName; -use ProophTest\EventStore\Mock\TestDomainEvent; -use Prophecy\Argument; - -class MetadataEnricherPluginTest extends TestCase -{ - /** - * @test - */ - public function it_enrich_metadata_on_stream_create(): void - { - $metadataEnricher = new class() implements MetadataEnricher { - public function enrich(Message $message): Message - { - return $message->withAddedMetadata('foo', 'bar'); - } - }; - - $eventStore = new ActionEventEmitterEventStore(new InMemoryEventStore(), new ProophActionEventEmitter()); - - $plugin = new MetadataEnricherPlugin($metadataEnricher); - $plugin->attachToEventStore($eventStore); - - $eventStore->create(new Stream(new StreamName('foo'), new \ArrayIterator([new TestDomainEvent(['foo' => 'bar'])]))); - - $streamEvents = $eventStore->load(new StreamName('foo')); - - $this->assertEquals( - ['foo' => 'bar'], - $streamEvents->current()->metadata() - ); - } - - /** - * @test - */ - public function it_does_not_enrich_metadata_on_create_if_stream_is_not_set(): void - { - $metadataEnricher = $this->prophesize(MetadataEnricher::class); - $metadataEnricher->enrich(Argument::any())->shouldNotBeCalled(); - - $actionEvent = new DefaultActionEvent('create'); - - $plugin = new MetadataEnricherPlugin($metadataEnricher->reveal()); - $plugin->onEventStoreCreateStream($actionEvent); - } - - /** - * @test - */ - public function it_enrich_metadata_on_stream_appendTo(): void - { - $metadataEnricher = new class() implements MetadataEnricher { - public function enrich(Message $message): Message - { - return $message->withAddedMetadata('foo', 'bar'); - } - }; - - $eventStore = new ActionEventEmitterEventStore(new InMemoryEventStore(), new ProophActionEventEmitter()); - - $eventStore->create(new Stream(new StreamName('foo'), new \ArrayIterator())); - - $plugin = new MetadataEnricherPlugin($metadataEnricher); - $plugin->attachToEventStore($eventStore); - - $eventStore->appendTo(new StreamName('foo'), new \ArrayIterator([new TestDomainEvent(['foo' => 'bar'])])); - - $streamEvents = $eventStore->load(new StreamName('foo')); - - $this->assertEquals( - ['foo' => 'bar'], - $streamEvents->current()->metadata() - ); - } - - /** - * @test - */ - public function it_does_not_enrich_metadata_on_appendTo_if_stream_is_not_set(): void - { - $metadataEnricher = $this->prophesize(MetadataEnricher::class); - $metadataEnricher->enrich(Argument::any())->shouldNotBeCalled(); - - $actionEvent = new DefaultActionEvent('appendTo'); - - $plugin = new MetadataEnricherPlugin($metadataEnricher->reveal()); - $plugin->onEventStoreAppendToStream($actionEvent); - } - - /** - * @test - */ - public function it_detaches_from_event_store(): void - { - $metadataEnricher = $this->prophesize(MetadataEnricher::class); - $metadataEnricher->enrich(Argument::any())->shouldNotBeCalled(); - - $eventStore = new ActionEventEmitterEventStore(new InMemoryEventStore(), new ProophActionEventEmitter()); - - $plugin = new MetadataEnricherPlugin($metadataEnricher->reveal()); - $plugin->attachToEventStore($eventStore); - $plugin->detachFromEventStore($eventStore); - - $eventStore->create(new Stream(new StreamName('foo'), new \ArrayIterator([new TestDomainEvent(['foo' => 'bar'])]))); - - $stream = $eventStore->load(new StreamName('foo')); - - $this->assertEmpty($stream->current()->metadata()); - } -} diff --git a/tests/Metadata/MetadataMatcherTest.php b/tests/Metadata/MetadataMatcherTest.php deleted file mode 100644 index 3a8ee2b6..00000000 --- a/tests/Metadata/MetadataMatcherTest.php +++ /dev/null @@ -1,71 +0,0 @@ - - * (c) 2015-2018 Sascha-Oliver Prolic - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ProophTest\EventStore\Metadata; - -use PHPUnit\Framework\TestCase; -use Prooph\EventStore\Exception\InvalidArgumentException; -use Prooph\EventStore\Metadata\FieldType; -use Prooph\EventStore\Metadata\MetadataMatcher; -use Prooph\EventStore\Metadata\Operator; - -class MetadataMatcherTest extends TestCase -{ - /** - * @test - */ - public function it_throws_on_invalid_value_for_in_operator(): void - { - $this->expectException(InvalidArgumentException::class); - $this->expectExceptionMessage('Value must be an array for the operator IN'); - - $metadataMatcher = new MetadataMatcher(); - $metadataMatcher->withMetadataMatch('foo', Operator::IN(), 'bar', FieldType::METADATA()); - } - - /** - * @test - */ - public function it_throws_on_invalid_value_for_not_in_operator(): void - { - $this->expectException(InvalidArgumentException::class); - $this->expectExceptionMessage('Value must be an array for the operator NOT_IN.'); - - $metadataMatcher = new MetadataMatcher(); - $metadataMatcher->withMetadataMatch('foo', Operator::NOT_IN(), 'bar', FieldType::METADATA()); - } - - /** - * @test - */ - public function it_throws_on_invalid_value_for_regex_operator(): void - { - $this->expectException(InvalidArgumentException::class); - $this->expectExceptionMessage('Value must be a string for the regex operator.'); - - $metadataMatcher = new MetadataMatcher(); - $metadataMatcher->withMetadataMatch('foo', Operator::REGEX(), false, FieldType::METADATA()); - } - - /** - * @test - */ - public function it_throws_on_invalid_value_for_equals_operator(): void - { - $this->expectException(InvalidArgumentException::class); - $this->expectExceptionMessage('Value must have a scalar type for the operator EQUALS.'); - - $metadataMatcher = new MetadataMatcher(); - $metadataMatcher->withMetadataMatch('foo', Operator::EQUALS(), ['bar' => 'baz'], FieldType::METADATA()); - } -} diff --git a/tests/Mock/EventLoggerPlugin.php b/tests/Mock/EventLoggerPlugin.php deleted file mode 100644 index 4bf018a9..00000000 --- a/tests/Mock/EventLoggerPlugin.php +++ /dev/null @@ -1,56 +0,0 @@ - - * (c) 2015-2018 Sascha-Oliver Prolic - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ProophTest\EventStore\Mock; - -use Iterator; -use Prooph\Common\Event\ActionEvent; -use Prooph\EventStore\ActionEventEmitterEventStore; -use Prooph\EventStore\Plugin\AbstractPlugin; - -class EventLoggerPlugin extends AbstractPlugin -{ - /** - * @var Iterator - */ - protected $loggedStreamEvents; - - public function __construct() - { - $this->loggedStreamEvents = new \ArrayIterator(); - } - - public function attachToEventStore(ActionEventEmitterEventStore $eventStore): void - { - $this->listenerHandlers[] = $eventStore->attach( - ActionEventEmitterEventStore::EVENT_CREATE, - function (ActionEvent $event): void { - $this->loggedStreamEvents = $event->getParam('stream')->streamEvents(); - }, - -10000 - ); - - $this->listenerHandlers[] = $eventStore->attach( - ActionEventEmitterEventStore::EVENT_APPEND_TO, - function (ActionEvent $event): void { - $this->loggedStreamEvents = $event->getParam('streamEvents', new \ArrayIterator()); - }, - -10000 - ); - } - - public function getLoggedStreamEvents(): Iterator - { - return $this->loggedStreamEvents; - } -} diff --git a/tests/Mock/Post.php b/tests/Mock/Post.php deleted file mode 100644 index e3d9bdc7..00000000 --- a/tests/Mock/Post.php +++ /dev/null @@ -1,139 +0,0 @@ - - * (c) 2015-2018 Sascha-Oliver Prolic - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ProophTest\EventStore\Mock; - -use Prooph\Common\Messaging\DomainEvent; -use Prooph\Common\Messaging\Message; -use Ramsey\Uuid\Uuid; - -class Post -{ - /** - * @var Uuid - */ - private $postId; - - /** - * @var string - */ - private $text; - - /** - * @var string - */ - private $email; - - /** - * @var DomainEvent[] - */ - private $recordedEvents; - - /** - * @var int - */ - private $version = 0; - - public static function create(string $text, string $email): Post - { - $self = new self(); - - $self->recordThat(PostCreated::with( - [ - 'post_id' => Uuid::uuid4()->toString(), - 'text' => $text, - 'email' => $email, - ], - $self->nextVersion() - )); - - return $self; - } - - /** - * @param Message[] $historyEvents - * @return Post - */ - public static function reconstituteFromHistory(array $historyEvents): Post - { - $self = new self(); - - $self->replay($historyEvents); - - return $self; - } - - private function __construct() - { - } - - /** - * @return Uuid - */ - public function getId(): Uuid - { - return $this->postId; - } - - public function text(): string - { - return $this->text; - } - - private function recordThat(TestDomainEvent $domainEvent): void - { - $this->recordedEvents[] = $domainEvent; - $this->apply($domainEvent); - } - - public function apply(TestDomainEvent $event): void - { - if ($event instanceof PostCreated) { - $this->whenPostCreated($event); - } - } - - private function whenPostCreated(PostCreated $postCreated): void - { - $payload = $postCreated->payload(); - - $this->postId = Uuid::fromString($payload['post_id']); - $this->name = $payload['name']; - $this->email = $payload['email']; - } - - public function popRecordedEvents(): array - { - $recordedEvents = $this->recordedEvents; - - $this->recordedEvents = []; - - return $recordedEvents; - } - - /** - * @param DomainEvent[] $streamEvents - */ - private function replay($streamEvents): void - { - foreach ($streamEvents as $streamEvent) { - $this->apply($streamEvent); - $this->version = $streamEvent->version(); - } - } - - private function nextVersion(): int - { - return ++$this->version; - } -} diff --git a/tests/Mock/ReadModelMock.php b/tests/Mock/ReadModelMock.php deleted file mode 100644 index 91efcbef..00000000 --- a/tests/Mock/ReadModelMock.php +++ /dev/null @@ -1,71 +0,0 @@ - - * (c) 2015-2018 Sascha-Oliver Prolic - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ProophTest\EventStore\Mock; - -use Prooph\EventStore\Exception\InvalidArgumentException; -use Prooph\EventStore\Projection\AbstractReadModel; - -class ReadModelMock extends AbstractReadModel -{ - private $storage; - - /** - * @var array - */ - private $stack = []; - - public function insert(string $key, $value): void - { - $this->storage[$key] = $value; - } - - public function update(string $key, $value): void - { - if (! \array_key_exists($key, $this->storage)) { - throw new InvalidArgumentException('Invalid key given'); - } - - $this->storage[$key] = $value; - } - - public function hasKey(string $key): bool - { - return \is_array($this->storage) && \array_key_exists($key, $this->storage); - } - - public function read(string $key) - { - return $this->storage[$key]; - } - - public function init(): void - { - $this->storage = []; - } - - public function isInitialized(): bool - { - return \is_array($this->storage); - } - - public function reset(): void - { - $this->storage = []; - } - - public function delete(): void - { - $this->storage = null; - } -} diff --git a/tests/Mock/TestDomainEvent.php b/tests/Mock/TestDomainEvent.php deleted file mode 100644 index 9f3125e2..00000000 --- a/tests/Mock/TestDomainEvent.php +++ /dev/null @@ -1,48 +0,0 @@ - - * (c) 2015-2018 Sascha-Oliver Prolic - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ProophTest\EventStore\Mock; - -use Prooph\Common\Messaging\DomainEvent; -use Prooph\Common\Messaging\PayloadConstructable; -use Prooph\Common\Messaging\PayloadTrait; - -class TestDomainEvent extends DomainEvent implements PayloadConstructable -{ - use PayloadTrait; - - public static function with(array $payload, int $version): TestDomainEvent - { - $event = new static($payload); - - return $event->withVersion($version); - } - - public static function withPayloadAndSpecifiedCreatedAt(array $payload, int $version, \DateTimeImmutable $createdAt): TestDomainEvent - { - $event = new static($payload); - $event->createdAt = $createdAt; - - return $event->withVersion($version); - } - - public function withVersion(int $version): TestDomainEvent - { - return $this->withAddedMetadata('_aggregate_version', $version); - } - - public function version(): int - { - return $this->metadata['_aggregate_version']; - } -} diff --git a/tests/Mock/TestIteratorAggregate.php b/tests/Mock/TestIteratorAggregate.php deleted file mode 100644 index a8a7500b..00000000 --- a/tests/Mock/TestIteratorAggregate.php +++ /dev/null @@ -1,25 +0,0 @@ - - * (c) 2015-2018 Sascha-Oliver Prolic - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ProophTest\EventStore\Mock; - -use ArrayIterator; -use IteratorAggregate; - -final class TestIteratorAggregate implements IteratorAggregate -{ - public function getIterator(): ArrayIterator - { - return new ArrayIterator(); - } -} diff --git a/tests/Mock/User.php b/tests/Mock/User.php deleted file mode 100644 index 5f671cce..00000000 --- a/tests/Mock/User.php +++ /dev/null @@ -1,163 +0,0 @@ - - * (c) 2015-2018 Sascha-Oliver Prolic - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ProophTest\EventStore\Mock; - -use Iterator; -use Prooph\Common\Messaging\DomainEvent; -use Ramsey\Uuid\Uuid; - -class User -{ - /** - * @var Uuid - */ - private $userId; - - /** - * @var string - */ - private $name; - - /** - * @var string - */ - private $email; - - /** - * @var DomainEvent[] - */ - private $recordedEvents; - - /** - * @var int - */ - private $version = 0; - - public static function create(string $name, string $email): User - { - $self = new self(); - - $self->recordThat(UserCreated::with( - [ - 'user_id' => Uuid::uuid4()->toString(), - 'name' => $name, - 'email' => $email, - ], - $self->nextVersion() - )); - - return $self; - } - - public static function reconstituteFromHistory(\Iterator $historyEvents): User - { - $self = new self(); - - $self->replay($historyEvents); - - return $self; - } - - private function __construct() - { - } - - public function getVersion(): int - { - return $this->version; - } - - public function getId(): Uuid - { - return $this->userId; - } - - public function name(): string - { - return $this->name; - } - - public function email(): string - { - return $this->email; - } - - public function changeName(string $newName) - { - $this->recordThat(UsernameChanged::with( - [ - 'old_name' => $this->name, - 'new_name' => $newName, - ], - $this->nextVersion() - )); - } - - private function recordThat(TestDomainEvent $domainEvent): void - { - $this->version += 1; - $this->recordedEvents[] = $domainEvent; - $this->apply($domainEvent); - } - - public function apply(TestDomainEvent $event): void - { - if ($event instanceof UserCreated) { - $this->whenUserCreated($event); - } - - if ($event instanceof UsernameChanged) { - $this->whenUsernameChanged($event); - } - } - - private function whenUserCreated(UserCreated $userCreated): void - { - $payload = $userCreated->payload(); - - $this->userId = Uuid::fromString($payload['user_id']); - $this->name = $payload['name']; - $this->email = $payload['email']; - } - - private function whenUsernameChanged(UsernameChanged $usernameChanged): void - { - $this->name = $usernameChanged->payload()['new_name']; - } - - public function popRecordedEvents(): array - { - $recordedEvents = $this->recordedEvents; - - $this->recordedEvents = []; - - return $recordedEvents; - } - - /** - * @param DomainEvent[] $streamEvents - */ - public function replay(Iterator $streamEvents): void - { - foreach ($streamEvents as $streamEvent) { - $this->apply($streamEvent); - $this->version = $streamEvent->version(); - } - } - - private function nextVersion(): int - { - return $this->version + 1; - } -} diff --git a/tests/Mock/UsernameChanged.php b/tests/Mock/UsernameChanged.php deleted file mode 100644 index aad1675e..00000000 --- a/tests/Mock/UsernameChanged.php +++ /dev/null @@ -1,18 +0,0 @@ - - * (c) 2015-2018 Sascha-Oliver Prolic - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ProophTest\EventStore\Mock; - -class UsernameChanged extends TestDomainEvent -{ -} diff --git a/tests/NonTransactionalInMemoryEventStoreTest.php b/tests/NonTransactionalInMemoryEventStoreTest.php deleted file mode 100644 index 9106bb5e..00000000 --- a/tests/NonTransactionalInMemoryEventStoreTest.php +++ /dev/null @@ -1,32 +0,0 @@ - - * (c) 2015-2018 Sascha-Oliver Prolic - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ProophTest\EventStore; - -use Prooph\EventStore\InMemoryEventStore; -use Prooph\EventStore\NonTransactionalInMemoryEventStore; - -class NonTransactionalInMemoryEventStoreTest extends AbstractEventStoreTest -{ - use EventStoreTestStreamTrait; - - /** - * @var InMemoryEventStore - */ - protected $eventStore; - - protected function setUp(): void - { - $this->eventStore = new NonTransactionalInMemoryEventStore(); - } -} diff --git a/tests/Plugin/PluginManagerTest.php b/tests/Plugin/PluginManagerTest.php deleted file mode 100644 index a356c6d4..00000000 --- a/tests/Plugin/PluginManagerTest.php +++ /dev/null @@ -1,56 +0,0 @@ - - * (c) 2015-2018 Sascha-Oliver Prolic - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ProophTest\EventStore\Plugin; - -use Prooph\EventStore\Stream; -use Prooph\EventStore\StreamName; -use ProophTest\EventStore\ActionEventEmitterEventStoreTestCase; -use ProophTest\EventStore\Mock\EventLoggerPlugin; -use ProophTest\EventStore\Mock\UserCreated; -use Psr\Container\ContainerInterface; - -class PluginManagerTest extends ActionEventEmitterEventStoreTestCase -{ - /** - * @test - */ - public function an_invokable_plugin_is_loaded_by_plugin_manager_and_attached_to_event_store_by_configuration(): void - { - $container = $this->prophesize(ContainerInterface::class); - $container->get('eventlogger')->willReturn(new EventLoggerPlugin())->shouldBeCalled(); - $container = $container->reveal(); - - $logger = $container->get('eventlogger'); - $logger->attachToEventStore($this->eventStore); - - $this->eventStore->create( - new Stream( - new StreamName('user'), - new \ArrayIterator([ - UserCreated::with( - [ - 'name' => 'Alex', - 'email' => 'contact@prooph.de', - ], - 1 - ), - ]) - ) - ); - - $loggedStreamEvents = $container->get('eventlogger')->getLoggedStreamEvents(); - - $this->assertEquals(1, \count($loggedStreamEvents)); - } -} diff --git a/tests/Plugin/UpcastingPluginTest.php b/tests/Plugin/UpcastingPluginTest.php deleted file mode 100644 index 7d75ff5c..00000000 --- a/tests/Plugin/UpcastingPluginTest.php +++ /dev/null @@ -1,101 +0,0 @@ - - * (c) 2015-2018 Sascha-Oliver Prolic - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ProophTest\EventStore\Plugin; - -use Prooph\Common\Messaging\Message; -use Prooph\EventStore\Exception\StreamNotFound; -use Prooph\EventStore\Plugin\UpcastingPlugin; -use Prooph\EventStore\Stream; -use Prooph\EventStore\StreamName; -use Prooph\EventStore\Upcasting\NoOpEventUpcaster; -use Prooph\EventStore\Upcasting\SingleEventUpcaster; -use ProophTest\EventStore\ActionEventEmitterEventStoreTestCase; -use ProophTest\EventStore\Mock\UserCreated; -use ProophTest\EventStore\Mock\UsernameChanged; - -class UpcastingPluginTest extends ActionEventEmitterEventStoreTestCase -{ - /** - * @test - */ - public function it_attaches_to_event_store(): void - { - $upcaster = new class() extends SingleEventUpcaster { - protected function canUpcast(Message $message): bool - { - return true; - } - - protected function doUpcast(Message $message): array - { - return [$message->withAddedMetadata('key', 'value')]; - } - }; - - $plugin = new UpcastingPlugin($upcaster); - $plugin->attachToEventStore($this->eventStore); - - $streamName = new StreamName('user'); - - $this->eventStore->create( - new Stream( - $streamName, - new \ArrayIterator([ - UserCreated::with( - [ - 'name' => 'Alex', - 'email' => 'contact@prooph.de', - ], - 1 - ), - UsernameChanged::with( - [ - 'name' => 'Sascha', - ], - 2 - ), - ]) - ) - ); - - $iterator = $this->eventStore->load($streamName); - - $this->assertTrue($iterator->valid()); - $this->assertEquals(['key' => 'value', '_aggregate_version' => 1], $iterator->current()->metadata()); - $iterator->next(); - $this->assertTrue($iterator->valid()); - $this->assertEquals(['key' => 'value', '_aggregate_version' => 2], $iterator->current()->metadata()); - - $iterator = $this->eventStore->loadReverse($streamName); - - $this->assertTrue($iterator->valid()); - $this->assertEquals(['key' => 'value', '_aggregate_version' => 2], $iterator->current()->metadata()); - $iterator->next(); - $this->assertTrue($iterator->valid()); - $this->assertEquals(['key' => 'value', '_aggregate_version' => 1], $iterator->current()->metadata()); - } - - /** - * @test - */ - public function it_ignores_when_no_iterator_in_result(): void - { - $this->expectException(StreamNotFound::class); - - $plugin = new UpcastingPlugin(new NoOpEventUpcaster()); - $plugin->attachToEventStore($this->eventStore); - - $this->eventStore->load(new StreamName('user')); - } -} diff --git a/tests/Projection/AbstractEventStoreProjectorTest.php b/tests/Projection/AbstractEventStoreProjectorTest.php deleted file mode 100644 index 1fd51e88..00000000 --- a/tests/Projection/AbstractEventStoreProjectorTest.php +++ /dev/null @@ -1,1058 +0,0 @@ - - * (c) 2015-2018 Sascha-Oliver Prolic - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ProophTest\EventStore\Projection; - -use ArrayIterator; -use PHPUnit\Framework\TestCase; -use Prooph\Common\Messaging\Message; -use Prooph\EventStore\EventStore; -use Prooph\EventStore\Exception\InvalidArgumentException; -use Prooph\EventStore\Exception\RuntimeException; -use Prooph\EventStore\Exception\StreamNotFound; -use Prooph\EventStore\Metadata\MetadataMatcher; -use Prooph\EventStore\Metadata\Operator; -use Prooph\EventStore\Projection\ProjectionManager; -use Prooph\EventStore\Projection\ProjectionStatus; -use Prooph\EventStore\Projection\Projector; -use Prooph\EventStore\Stream; -use Prooph\EventStore\StreamName; -use ProophTest\EventStore\Mock\UserCreated; -use ProophTest\EventStore\Mock\UsernameChanged; - -/** - * Common tests for all event store projector implementations - */ -abstract class AbstractEventStoreProjectorTest extends TestCase -{ - /** - * @var ProjectionManager - */ - protected $projectionManager; - - /** - * @var EventStore - */ - protected $eventStore; - - /** - * @test - */ - public function it_can_project_from_stream_and_reset(): void - { - $this->prepareEventStream('user-123'); - - $testCase = $this; - $projectionManager = $this->projectionManager; - $projection = $projectionManager->createProjection('test_projection'); - - $projection - ->init(function (): array { - return ['count' => 0]; - }) - ->fromStream('user-123') - ->when([ - UsernameChanged::class => function (array $state, UsernameChanged $event) use ($projectionManager, $testCase): array { - $testCase->assertSame(ProjectionStatus::RUNNING(), $projectionManager->fetchProjectionStatus('test_projection')); - $state['count']++; - - return $state; - }, - ]) - ->run(false); - - $this->assertEquals(49, $projection->getState()['count']); - - $projection->reset(); - - $projection->run(false); - - $projection->run(false); - - $this->assertEquals(49, $projection->getState()['count']); - - $this->assertEquals($projection->getState(), $projectionManager->fetchProjectionState('test_projection')); - - $this->assertEquals( - [ - 'user-123' => 50, - ], - $projectionManager->fetchProjectionStreamPositions('test_projection') - ); - } - - /** - * @test - */ - public function it_can_be_stopped_while_processing(): void - { - $this->prepareEventStream('user-123'); - - $projection = $this->projectionManager->createProjection('test_projection'); - - $projection - ->init(function (): array { - return ['count' => 0]; - }) - ->fromStream('user-123') - ->whenAny(function (array $state, Message $event): array { - $state['count']++; - - if ($state['count'] === 10) { - $this->stop(); - } - - return $state; - }) - ->run(false); - - $this->assertEquals(10, $projection->getState()['count']); - } - - /** - * @test - */ - public function it_can_query_from_streams(): void - { - $this->prepareEventStream('user-123'); - $this->prepareEventStream('user-234'); - - $projection = $this->projectionManager->createProjection('test_projection'); - - $projection - ->init(function (): array { - return ['count' => 0]; - }) - ->fromStreams('user-123', 'user-234') - ->whenAny( - function (array $state, Message $event): array { - $state['count']++; - - return $state; - } - ) - ->run(false); - - $this->assertEquals(100, $projection->getState()['count']); - } - - /** - * @test - */ - public function it_can_query_from_stream_and_filter_with_metadata_matcher(): void - { - $this->prepareEventStream('user-123'); - - $projection = $this->projectionManager->createProjection('test_projection'); - $metadataMatcher = (new MetadataMatcher()) - ->withMetadataMatch('_aggregate_version', Operator::EQUALS(), 10); - - $projection - ->init(function (): array { - return ['count' => 0, 'version' => null]; - }) - ->fromStream('user-123', $metadataMatcher) - ->whenAny( - function (array $state, Message $event): array { - $state['count']++; - $state['version'] = $event->metadata()['_aggregate_version']; - - return $state; - } - ) - ->run(false); - - $this->assertEquals(1, $projection->getState()['count']); - $this->assertEquals(10, $projection->getState()['version']); - } - - /** - * @test - */ - public function it_can_query_from_all_ignoring_internal_streams(): void - { - $this->prepareEventStream('user-123'); - $this->prepareEventStream('user-234'); - $this->prepareEventStream('$iternal-345'); - - $testCase = $this; - - $projection = $this->projectionManager->createProjection('test_projection'); - - $projection - ->init(function (): array { - return ['count' => 0]; - }) - ->fromAll() - ->whenAny( - function (array $state, Message $event) use ($testCase): array { - $state['count']++; - if ($state['count'] < 51) { - $testCase->assertEquals('user-123', $this->streamName()); - } else { - $testCase->assertEquals('user-234', $this->streamName()); - } - - return $state; - } - ) - ->run(false); - - $this->assertEquals(100, $projection->getState()['count']); - } - - /** - * @test - */ - public function it_can_query_from_category_with_when_any(): void - { - $this->prepareEventStream('user-123'); - $this->prepareEventStream('user-234'); - - $projection = $this->projectionManager->createProjection('test_projection'); - - $projection - ->init(function (): array { - return ['count' => 0]; - }) - ->fromCategory('user') - ->whenAny( - function (array $state, Message $event): array { - $state['count']++; - - return $state; - } - ) - ->run(false); - - $this->assertEquals(100, $projection->getState()['count']); - } - - /** - * @test - */ - public function it_can_query_from_categories_with_when(): void - { - $this->prepareEventStream('user-123'); - $this->prepareEventStream('user-234'); - $this->prepareEventStream('guest-345'); - $this->prepareEventStream('guest-456'); - - $projection = $this->projectionManager->createProjection('test_projection'); - - $projection - ->init(function (): array { - return ['count' => 0]; - }) - ->fromCategories('user', 'guest') - ->when([ - UserCreated::class => function (array $state, Message $event): array { - $state['count']++; - - return $state; - }, - ]) - ->run(false); - - $this->assertEquals(4, $projection->getState()['count']); - } - - /** - * @test - */ - public function it_resumes_projection_from_position(): void - { - $this->prepareEventStream('user-123'); - - $projection = $this->projectionManager->createProjection('test_projection'); - - $projection - ->init(function (): array { - return ['count' => 0]; - }) - ->fromStreams('user-123', 'user-234') - ->when([ - UsernameChanged::class => function (array $state, Message $event): array { - $state['count']++; - - return $state; - }, - ]) - ->run(false); - - $this->assertEquals(49, $projection->getState()['count']); - - $events = []; - for ($i = 51; $i <= 100; $i++) { - $events[] = UsernameChanged::with([ - 'name' => \uniqid('name_'), - ], $i); - } - - $this->eventStore->appendTo(new StreamName('user-123'), new ArrayIterator($events)); - - $this->prepareEventStream('user-234'); - - $projection->run(false); - - $this->assertEquals(148, $projection->getState()['count']); - } - - /** - * @test - */ - public function it_ignores_error_on_delete_of_not_created_stream_projections(): void - { - $this->prepareEventStream('user-123'); - - $projection = $this->projectionManager->createProjection('test_projection'); - - $projection - ->fromStream('user-123') - ->when([ - UserCreated::class => function (array $state, UserCreated $event): array { - $this->stop(); - - return $state; - }, - ]) - ->run(); - - $projection->delete(true); - - $this->assertEmpty($this->projectionManager->fetchProjectionNames('test-projection')); - } - - /** - * @test - */ - public function it_throws_exception_when_trying_to_run_two_projections_at_the_same_time(): void - { - $this->expectException(\Prooph\EventStore\Exception\RuntimeException::class); - $this->expectExceptionMessage('Another projection process is already running'); - - $this->prepareEventStream('user-123'); - - $projection = $this->projectionManager->createProjection('test_projection'); - - $projectionManager = $this->projectionManager; - - $projection - ->fromStream('user-123') - ->whenAny( - function (array $state, Message $event) use ($projectionManager): array { - $projection = $projectionManager->createProjection('test_projection'); - - $projection - ->fromStream('user-123') - ->whenAny( - function (array $state, Message $event): array { - $this->linkTo('foo', $event); - - return $state; - } - ) - ->run(); - } - ) - ->run(); - } - - /** - * @test - */ - public function it_deletes_projection_before_start_when_it_was_deleted_from_outside(): void - { - $this->prepareEventStream('user-123'); - - $calledTimes = 0; - - $projection = $this->projectionManager->createProjection('test_projection', [ - Projector::OPTION_PERSIST_BLOCK_SIZE => 10, - ]); - - $projection - ->init(function (): array { - return ['count' => 0]; - }) - ->fromStream('user-123') - ->when([ - UsernameChanged::class => function (array $state, UsernameChanged $event) use (&$calledTimes): array { - $state['count']++; - $calledTimes++; - - return $state; - }, - ]) - ->run(false); - - $events = []; - for ($i = 51; $i <= 100; $i++) { - $events[] = UsernameChanged::with([ - 'name' => \uniqid('name_'), - ], $i); - } - - $this->eventStore->appendTo(new StreamName('user-123'), new ArrayIterator($events)); - - $this->projectionManager->deleteProjection('test_projection', false); - - $projection->run(false); - - $this->assertEquals(0, $projection->getState()['count']); - $this->assertEquals(49, $calledTimes); - } - - /** - * @test - */ - public function it_deletes_projection_incl_emitting_events_before_start_when_it_was_deleted_from_outside(): void - { - $this->prepareEventStream('user-123'); - - $calledTimes = 0; - - $projection = $this->projectionManager->createProjection('test_projection', [ - Projector::OPTION_PERSIST_BLOCK_SIZE => 10, - ]); - - $projection - ->init(function (): array { - return ['count' => 0]; - }) - ->fromStream('user-123') - ->when([ - UsernameChanged::class => function (array $state, UsernameChanged $event) use (&$calledTimes): array { - $state['count']++; - $calledTimes++; - - return $state; - }, - ]) - ->run(false); - - $events = []; - for ($i = 51; $i <= 100; $i++) { - $events[] = UsernameChanged::with([ - 'name' => \uniqid('name_'), - ], $i); - } - - $this->eventStore->appendTo(new StreamName('user-123'), new ArrayIterator($events)); - - $this->projectionManager->deleteProjection('test_projection', true); - - $projection->run(false); - - $this->assertEquals(0, $projection->getState()['count']); - $this->assertEquals(49, $calledTimes); - } - - /** - * @test - */ - public function it_deletes_projection_during_run_when_it_was_deleted_from_outside(): void - { - $this->prepareEventStream('user-123'); - - $calledTimes = 0; - - $projectionManager = $this->projectionManager; - - $projection = $this->projectionManager->createProjection('test_projection', [ - Projector::OPTION_PERSIST_BLOCK_SIZE => 5, - ]); - - $projection - ->init(function (): array { - return ['count' => 0]; - }) - ->fromStream('user-123') - ->when([ - UsernameChanged::class => function (array $state, UsernameChanged $event) use (&$calledTimes, $projectionManager): array { - static $wasReset = false; - - if (! $wasReset) { - $projectionManager->deleteProjection('test_projection', false); - $wasReset = true; - } - - $state['count']++; - $calledTimes++; - - return $state; - }, - ]) - ->run(false); - - $this->assertEquals(0, $projection->getState()['count']); - $this->assertEquals(5, $calledTimes); - $this->assertEquals([], $projectionManager->fetchProjectionNames('test_projection')); - } - - /** - * @test - */ - public function it_deletes_projection_incl_emitted_events_during_run_when_it_was_deleted_from_outside(): void - { - $this->prepareEventStream('user-123'); - - $calledTimes = 0; - - $projectionManager = $this->projectionManager; - - $projection = $this->projectionManager->createProjection('test_projection', [ - Projector::OPTION_PERSIST_BLOCK_SIZE => 5, - ]); - - $projection - ->init(function (): array { - return ['count' => 0]; - }) - ->fromStream('user-123') - ->when([ - UsernameChanged::class => function (array $state, UsernameChanged $event) use (&$calledTimes, $projectionManager): array { - static $wasReset = false; - - if (! $wasReset) { - $projectionManager->deleteProjection('test_projection', true); - $wasReset = true; - } - - $state['count']++; - $calledTimes++; - - return $state; - }, - ]) - ->run(false); - - $this->assertEquals(0, $projection->getState()['count']); - $this->assertEquals(5, $calledTimes); - $this->assertEquals([], $projectionManager->fetchProjectionNames('test_projection')); - } - - /** - * @test - */ - public function it_resets_projection_before_start_when_it_was_reset_from_outside(): void - { - $this->prepareEventStream('user-123'); - - $calledTimes = 0; - - $projection = $this->projectionManager->createProjection('test_projection'); - - $projection - ->init(function (): array { - return ['count' => 0]; - }) - ->fromStream('user-123') - ->when([ - UsernameChanged::class => function (array $state, UsernameChanged $event) use (&$calledTimes): array { - $state['count']++; - $calledTimes++; - - return $state; - }, - ]) - ->run(false); - - $events = []; - for ($i = 51; $i <= 100; $i++) { - $events[] = UsernameChanged::with([ - 'name' => \uniqid('name_'), - ], $i); - } - - $this->eventStore->appendTo(new StreamName('user-123'), new ArrayIterator($events)); - - $this->projectionManager->resetProjection('test_projection'); - - $projection->run(false); - - $this->assertEquals(99, $projection->getState()['count']); - $this->assertEquals(148, $calledTimes); - } - - /** - * @test - */ - public function it_resets_projection_during_run_when_it_was_reset_from_outside(): void - { - $this->prepareEventStream('user-123'); - - $calledTimes = 0; - - $projectionManager = $this->projectionManager; - - $projection = $this->projectionManager->createProjection('test_projection'); - - $projection - ->init(function (): array { - return ['count' => 0]; - }) - ->fromStream('user-123') - ->when([ - UsernameChanged::class => function (array $state, UsernameChanged $event) use (&$calledTimes, $projectionManager): array { - static $wasReset = false; - - if (! $wasReset) { - $projectionManager->resetProjection('test_projection'); - $wasReset = true; - } - - $state['count']++; - $calledTimes++; - - return $state; - }, - ]) - ->run(false); - - $projection->run(false); - - $this->assertEquals(49, $projection->getState()['count']); - $this->assertEquals(98, $calledTimes); - } - - /** - * @test - */ - public function it_stops_when_projection_before_start_when_it_was_stopped_from_outside(): void - { - $this->prepareEventStream('user-123'); - - $calledTimes = 0; - - $projection = $this->projectionManager->createProjection('test_projection'); - - $projection - ->init(function (): array { - return ['count' => 0]; - }) - ->fromStream('user-123') - ->when([ - UsernameChanged::class => function (array $state, UsernameChanged $event) use (&$calledTimes): array { - $state['count']++; - $calledTimes++; - - return $state; - }, - ]) - ->run(false); - - $events = []; - for ($i = 51; $i <= 100; $i++) { - $events[] = UsernameChanged::with([ - 'name' => \uniqid('name_'), - ], $i); - } - - $this->eventStore->appendTo(new StreamName('user-123'), new ArrayIterator($events)); - - $this->projectionManager->stopProjection('test_projection'); - - $projection->run(false); - - $this->assertEquals(49, $projection->getState()['count']); - $this->assertEquals(49, $calledTimes); - } - - /** - * @test - */ - public function it_stops_projection_during_run_when_it_was_stopped_from_outside(): void - { - $this->prepareEventStream('user-123'); - - $calledTimes = 0; - - $projectionManager = $this->projectionManager; - - $projection = $this->projectionManager->createProjection('test_projection'); - - $projection - ->init(function (): array { - return ['count' => 0]; - }) - ->fromStream('user-123') - ->when([ - UsernameChanged::class => function (array $state, UsernameChanged $event) use (&$calledTimes, $projectionManager): array { - static $wasReset = false; - - if (! $wasReset) { - $projectionManager->stopProjection('test_projection'); - $wasReset = true; - } - - $state['count']++; - $calledTimes++; - - return $state; - }, - ]) - ->run(false); - - $projection->run(false); - - $this->assertEquals(49, $projection->getState()['count']); - $this->assertEquals(49, $calledTimes); - } - - /** - * @test - */ - public function it_resets_to_empty_array(): void - { - $projection = $this->projectionManager->createProjection('test_projection'); - - $state = $projection->getState(); - - $this->assertInternalType('array', $state); - - $projection->reset(); - - $state2 = $projection->getState(); - - $this->assertInternalType('array', $state2); - } - - /** - * @test - */ - public function it_throws_exception_when_init_callback_provided_twice(): void - { - $this->expectException(RuntimeException::class); - - $projection = $this->projectionManager->createProjection('test_projection'); - - $projection->init(function (): array { - return []; - }); - $projection->init(function (): array { - return []; - }); - } - - /** - * @test - */ - public function it_throws_exception_when_from_called_twice(): void - { - $this->expectException(RuntimeException::class); - - $projection = $this->projectionManager->createProjection('test_projection'); - - $projection->fromStream('foo'); - $projection->fromStream('bar'); - } - - /** - * @test - */ - public function it_throws_exception_when_from_called_twice_2(): void - { - $this->expectException(RuntimeException::class); - - $projection = $this->projectionManager->createProjection('test_projection'); - - $projection->fromStreams('foo'); - $projection->fromCategory('bar'); - } - - /** - * @test - */ - public function it_throws_exception_when_from_called_twice_3(): void - { - $this->expectException(RuntimeException::class); - - $projection = $this->projectionManager->createProjection('test_projection'); - - $projection->fromCategory('foo'); - $projection->fromStreams('bar'); - } - - /** - * @test - */ - public function it_throws_exception_when_from_called_twice_4(): void - { - $this->expectException(RuntimeException::class); - - $projection = $this->projectionManager->createProjection('test_projection'); - - $projection->fromCategories('foo'); - $projection->fromCategories('bar'); - } - - /** - * @test - */ - public function it_throws_exception_when_from_called_twice_5(): void - { - $this->expectException(RuntimeException::class); - - $projection = $this->projectionManager->createProjection('test_projection'); - - $projection->fromCategories('foo'); - $projection->fromAll('bar'); - } - - /** - * @test - */ - public function it_throws_exception_when_when_called_twice_(): void - { - $this->expectException(RuntimeException::class); - - $projection = $this->projectionManager->createProjection('test_projection'); - - $projection->when(['foo' => function (): void { - }]); - $projection->when(['foo' => function (): void { - }]); - } - - /** - * @test - */ - public function it_throws_exception_when_invalid_handlers_configured(): void - { - $this->expectException(InvalidArgumentException::class); - - $projection = $this->projectionManager->createProjection('test_projection'); - - $projection->when(['1' => function (): void { - }]); - } - - /** - * @test - */ - public function it_throws_exception_when_invalid_handlers_configured_2(): void - { - $this->expectException(InvalidArgumentException::class); - - $projection = $this->projectionManager->createProjection('test_projection'); - - $projection->when(['foo' => 'invalid']); - } - - /** - * @test - */ - public function it_throws_exception_when_whenAny_called_twice(): void - { - $this->expectException(RuntimeException::class); - - $projection = $this->projectionManager->createProjection('test_projection'); - - $projection->whenAny(function (): void { - }); - $projection->whenAny(function (): void { - }); - } - - /** - * @test - */ - public function it_throws_exception_on_run_when_nothing_configured(): void - { - $this->expectException(RuntimeException::class); - - $projection = $this->projectionManager->createProjection('test_projection'); - $projection->run(); - } - - /** - * @test - */ - public function it_links_to_and_loads_and_continues_again(): void - { - $this->prepareEventStream('user-123'); - - $projection = $this->projectionManager->createProjection('test_projection'); - - $projection - ->fromStream('user-123') - ->whenAny( - function (array $state, Message $event): array { - $this->linkTo('foo', $event); - - if ($event->metadata()['_aggregate_version'] === 50 - || $event->metadata()['_aggregate_version'] === 100 - ) { - $this->stop(); - } - - return $state; - } - ) - ->run(); - - $events = $this->eventStore->load(new StreamName('foo')); - - $this->assertCount(50, $events); - - $events = []; - for ($i = 51; $i < 100; $i++) { - $events[] = UsernameChanged::with([ - 'name' => \uniqid('name_'), - ], $i); - } - $events[] = UsernameChanged::with([ - 'name' => 'Oliver', - ], 100); - - $this->eventStore->appendTo(new StreamName('user-123'), new ArrayIterator($events)); - - $projection->run(); - - $events = $this->eventStore->load(new StreamName('foo')); - - $this->assertCount(100, $events); - } - - /** - * @test - */ - public function it_emits_events_and_resets(): void - { - $this->prepareEventStream('user-123'); - - $testCase = $this; - - $projection = $this->projectionManager->createProjection('test_projection'); - $projection - ->fromStream('user-123') - ->when([ - UserCreated::class => function (array $state, UserCreated $event) use ($testCase): void { - $testCase->assertEquals('user-123', $this->streamName()); - $this->emit($event); - $this->stop(); - }, - ]) - ->run(); - - $events = $this->eventStore->load(new StreamName('test_projection')); - - $this->assertCount(1, $events); - $this->assertEquals('Alex', $events->current()->payload()['name']); - - $projection->reset(); - $this->assertEquals('test_projection', $projection->getName()); - - $this->expectException(StreamNotFound::class); - $this->eventStore->load(new StreamName('test_projection')); - } - - /** - * @test - */ - public function it_emits_events_and_deletes(): void - { - $this->prepareEventStream('user-123'); - - $projection = $this->projectionManager->createProjection('test_projection'); - $projection - ->fromStream('user-123') - ->when([ - UserCreated::class => function (array $state, UserCreated $event): array { - $this->emit($event); - - return $state; - }, - ]) - ->run(false); - - $events = $this->eventStore->load(new StreamName('test_projection')); - - $this->assertCount(1, $events); - $this->assertEquals('Alex', $events->current()->payload()['name']); - - $projection->delete(true); - - $this->expectException(StreamNotFound::class); - $this->eventStore->load(new StreamName('test_projection')); - } - - /** - * @test - */ - public function it_persists_in_single_handler(): void - { - $this->prepareEventStream('user-123'); - - $projection = $this->projectionManager->createProjection('test_projection'); - - $projection - ->init(function (): array { - return ['count' => 0]; - }) - ->fromCategories('user', 'guest') - ->whenAny(function (array $state, Message $event): array { - $state['count']++; - - return $state; - }) - ->run(false); - - $this->assertEquals(50, $projection->getState()['count']); - } - - /** - * @test - */ - public function it_persists_in_handlers(): void - { - $this->prepareEventStream('user-123'); - - $projection = $this->projectionManager->createProjection('test_projection'); - - $projection - ->init(function (): array { - return ['count' => 0]; - }) - ->fromCategories('user', 'guest') - ->when([ - UsernameChanged::class => function (array $state, Message $event): array { - $state['count']++; - - return $state; - }, - ]) - ->run(false); - - $this->assertEquals(49, $projection->getState()['count']); - } - - protected function prepareEventStream(string $name): void - { - $events = []; - $events[] = UserCreated::with([ - 'name' => 'Alex', - ], 1); - for ($i = 2; $i < 50; $i++) { - $events[] = UsernameChanged::with([ - 'name' => \uniqid('name_'), - ], $i); - } - $events[] = UsernameChanged::with([ - 'name' => 'Sascha', - ], 50); - - $this->eventStore->create(new Stream(new StreamName($name), new ArrayIterator($events))); - } -} diff --git a/tests/Projection/AbstractEventStoreQueryTest.php b/tests/Projection/AbstractEventStoreQueryTest.php deleted file mode 100644 index 79af5db2..00000000 --- a/tests/Projection/AbstractEventStoreQueryTest.php +++ /dev/null @@ -1,477 +0,0 @@ - - * (c) 2015-2018 Sascha-Oliver Prolic - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ProophTest\EventStore\Projection; - -use ArrayIterator; -use PHPUnit\Framework\TestCase; -use Prooph\Common\Messaging\Message; -use Prooph\EventStore\EventStore; -use Prooph\EventStore\Exception\InvalidArgumentException; -use Prooph\EventStore\Exception\RuntimeException; -use Prooph\EventStore\Metadata\MetadataMatcher; -use Prooph\EventStore\Metadata\Operator; -use Prooph\EventStore\Projection\ProjectionManager; -use Prooph\EventStore\Stream; -use Prooph\EventStore\StreamName; -use ProophTest\EventStore\Mock\UserCreated; -use ProophTest\EventStore\Mock\UsernameChanged; - -/** - * Common tests for all event store query implementations - */ -abstract class AbstractEventStoreQueryTest extends TestCase -{ - /** - * @var ProjectionManager - */ - protected $projectionManager; - - /** - * @var EventStore - */ - protected $eventStore; - - /** - * @test - */ - public function it_can_query_from_stream_and_reset(): void - { - $this->prepareEventStream('user-123'); - - $query = $this->projectionManager->createQuery(); - - $query - ->init(function (): array { - return ['count' => 0]; - }) - ->fromStream('user-123') - ->when([ - UsernameChanged::class => function (array $state, UsernameChanged $event): array { - $state['count']++; - - return $state; - }, - ]) - ->run(); - - $this->assertEquals(49, $query->getState()['count']); - - $query->reset(); - - $query->run(); - - $this->assertEquals(49, $query->getState()['count']); - } - - /** - * @test - */ - public function it_can_be_stopped_while_processing(): void - { - $this->prepareEventStream('user-123'); - - $query = $this->projectionManager->createQuery(); - - $query - ->init(function (): array { - return ['count' => 0]; - }) - ->fromStream('user-123') - ->whenAny(function (array $state, Message $event): array { - $state['count']++; - - if ($state['count'] === 10) { - $this->stop(); - } - - return $state; - }) - ->run(); - - $this->assertEquals(10, $query->getState()['count']); - } - - /** - * @test - */ - public function it_can_query_from_streams(): void - { - $this->prepareEventStream('user-123'); - $this->prepareEventStream('user-234'); - - $query = $this->projectionManager->createQuery(); - - $query - ->init(function (): array { - return ['count' => 0]; - }) - ->fromStreams('user-123', 'user-234') - ->whenAny( - function (array $state, Message $event): array { - $state['count']++; - - return $state; - } - ) - ->run(); - - $this->assertEquals(100, $query->getState()['count']); - } - - /** - * @test - */ - public function it_can_query_from_stream_and_filter_with_metadata_matcher(): void - { - $this->prepareEventStream('user-123'); - - $projection = $this->projectionManager->createQuery(); - $metadataMatcher = (new MetadataMatcher()) - ->withMetadataMatch('_aggregate_version', Operator::EQUALS(), 10); - - $projection - ->init(function (): array { - return ['count' => 0, 'version' => null]; - }) - ->fromStream('user-123', $metadataMatcher) - ->whenAny( - function (array $state, Message $event): array { - $state['count']++; - $state['version'] = $event->metadata()['_aggregate_version']; - - return $state; - } - ) - ->run(); - - $this->assertEquals(1, $projection->getState()['count']); - $this->assertEquals(10, $projection->getState()['version']); - } - - /** - * @test - */ - public function it_can_query_from_all_ignoring_internal_streams(): void - { - $this->prepareEventStream('user-123'); - $this->prepareEventStream('user-234'); - $this->prepareEventStream('$iternal-345'); - - $testCase = $this; - - $query = $this->projectionManager->createQuery(); - - $query - ->init(function (): array { - return ['count' => 0]; - }) - ->fromAll() - ->whenAny( - function (array $state, Message $event) use ($testCase): array { - $state['count']++; - if ($state['count'] < 51) { - $testCase->assertEquals('user-123', $this->streamName()); - } else { - $testCase->assertEquals('user-234', $this->streamName()); - } - - return $state; - } - ) - ->run(); - - $this->assertEquals(100, $query->getState()['count']); - } - - /** - * @test - */ - public function it_can_query_from_category_with_when_any(): void - { - $this->prepareEventStream('user-123'); - $this->prepareEventStream('user-234'); - - $query = $this->projectionManager->createQuery(); - - $query - ->init(function (): array { - return ['count' => 0]; - }) - ->fromCategory('user') - ->whenAny( - function (array $state, Message $event): array { - $state['count']++; - - return $state; - } - ) - ->run(); - - $this->assertEquals(100, $query->getState()['count']); - } - - /** - * @test - */ - public function it_can_query_from_categories_with_when(): void - { - $this->prepareEventStream('user-123'); - $this->prepareEventStream('user-234'); - $this->prepareEventStream('guest-345'); - $this->prepareEventStream('guest-456'); - - $query = $this->projectionManager->createQuery(); - - $query - ->init(function (): array { - return ['count' => 0]; - }) - ->fromCategories('user', 'guest') - ->when([ - UserCreated::class => function (array $state, Message $event): array { - $state['count']++; - - return $state; - }, - ]) - ->run(); - - $this->assertEquals(4, $query->getState()['count']); - } - - /** - * @test - */ - public function it_resumes_query_from_position(): void - { - $this->prepareEventStream('user-123'); - - $query = $this->projectionManager->createQuery(); - - $query - ->init(function (): array { - return ['count' => 0]; - }) - ->fromStreams('user-123', 'user-234') - ->when([ - UsernameChanged::class => function (array $state, Message $event): array { - $state['count']++; - - return $state; - }, - ]) - ->run(); - - $this->assertEquals(49, $query->getState()['count']); - - $events = []; - for ($i = 51; $i <= 100; $i++) { - $events[] = UsernameChanged::with([ - 'name' => \uniqid('name_'), - ], $i); - } - - $this->eventStore->appendTo(new StreamName('user-123'), new ArrayIterator($events)); - - $this->prepareEventStream('user-234'); - - $query->run(); - - $this->assertEquals(148, $query->getState()['count']); - } - - /** - * @test - */ - public function it_resets_to_empty_array(): void - { - $query = $this->projectionManager->createQuery(); - - $state = $query->getState(); - - $this->assertInternalType('array', $state); - - $query->reset(); - - $state2 = $query->getState(); - - $this->assertInternalType('array', $state2); - } - - /** - * @test - */ - public function it_throws_exception_when_init_callback_provided_twice(): void - { - $this->expectException(RuntimeException::class); - - $query = $this->projectionManager->createQuery(); - - $query->init(function (): array { - return []; - }); - $query->init(function (): array { - return []; - }); - } - - /** - * @test - */ - public function it_throws_exception_when_from_called_twice(): void - { - $this->expectException(RuntimeException::class); - - $query = $this->projectionManager->createQuery(); - - $query->fromStream('foo'); - $query->fromStream('bar'); - } - - /** - * @test - */ - public function it_throws_exception_when_from_called_twice_2(): void - { - $this->expectException(RuntimeException::class); - - $query = $this->projectionManager->createQuery(); - - $query->fromStreams('foo'); - $query->fromCategory('bar'); - } - - /** - * @test - */ - public function it_throws_exception_when_from_called_twice_3(): void - { - $this->expectException(RuntimeException::class); - - $query = $this->projectionManager->createQuery(); - - $query->fromCategory('foo'); - $query->fromStreams('bar'); - } - - /** - * @test - */ - public function it_throws_exception_when_from_called_twice_4(): void - { - $this->expectException(RuntimeException::class); - - $query = $this->projectionManager->createQuery(); - - $query->fromCategories('foo'); - $query->fromCategories('bar'); - } - - /** - * @test - */ - public function it_throws_exception_when_from_called_twice_5(): void - { - $this->expectException(RuntimeException::class); - - $query = $this->projectionManager->createQuery(); - - $query->fromCategories('foo'); - $query->fromAll('bar'); - } - - /** - * @test - */ - public function it_throws_exception_when_when_called_twice(): void - { - $this->expectException(RuntimeException::class); - - $query = $this->projectionManager->createQuery(); - - $query->when(['foo' => function (): void { - }]); - $query->when(['foo' => function (): void { - }]); - } - - /** - * @test - */ - public function it_throws_exception_when_invalid_handlers_configured(): void - { - $this->expectException(InvalidArgumentException::class); - - $query = $this->projectionManager->createQuery(); - - $query->when(['1' => function (): void { - }]); - } - - /** - * @test - */ - public function it_throws_exception_when_invalid_handlers_configured_2(): void - { - $this->expectException(InvalidArgumentException::class); - - $query = $this->projectionManager->createQuery(); - - $query->when(['foo' => 'invalid']); - } - - /** - * @test - */ - public function it_throws_exception_when_whenAny_called_twice(): void - { - $this->expectException(RuntimeException::class); - - $query = $this->projectionManager->createQuery(); - - $query->whenAny(function (): void { - }); - $query->whenAny(function (): void { - }); - } - - /** - * @test - */ - public function it_throws_exception_on_run_when_nothing_configured(): void - { - $this->expectException(RuntimeException::class); - - $query = $this->projectionManager->createQuery(); - $query->run(); - } - - protected function prepareEventStream(string $name): void - { - $events = []; - $events[] = UserCreated::with([ - 'name' => 'Alex', - ], 1); - for ($i = 2; $i < 50; $i++) { - $events[] = UsernameChanged::with([ - 'name' => \uniqid('name_'), - ], $i); - } - $events[] = UsernameChanged::with([ - 'name' => 'Sascha', - ], 50); - - $this->eventStore->create(new Stream(new StreamName($name), new ArrayIterator($events))); - } -} diff --git a/tests/Projection/AbstractEventStoreReadModelProjectorTest.php b/tests/Projection/AbstractEventStoreReadModelProjectorTest.php deleted file mode 100644 index 682cc80c..00000000 --- a/tests/Projection/AbstractEventStoreReadModelProjectorTest.php +++ /dev/null @@ -1,1090 +0,0 @@ - - * (c) 2015-2018 Sascha-Oliver Prolic - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ProophTest\EventStore\Projection; - -use ArrayIterator; -use PHPUnit\Framework\TestCase; -use Prooph\Common\Messaging\Message; -use Prooph\EventStore\EventStore; -use Prooph\EventStore\Exception\InvalidArgumentException; -use Prooph\EventStore\Exception\RuntimeException; -use Prooph\EventStore\Metadata\MetadataMatcher; -use Prooph\EventStore\Metadata\Operator; -use Prooph\EventStore\Projection\ProjectionManager; -use Prooph\EventStore\Projection\ReadModel; -use Prooph\EventStore\Projection\ReadModelProjector; -use Prooph\EventStore\Stream; -use Prooph\EventStore\StreamName; -use ProophTest\EventStore\Mock\ReadModelMock; -use ProophTest\EventStore\Mock\UserCreated; -use ProophTest\EventStore\Mock\UsernameChanged; - -/** - * Common tests for all event store read model projector implementations - */ -abstract class AbstractEventStoreReadModelProjectorTest extends TestCase -{ - /** - * @var ProjectionManager - */ - protected $projectionManager; - - /** - * @var EventStore - */ - protected $eventStore; - - /** - * @test - */ - public function it_can_project_from_stream_and_reset(): void - { - $this->prepareEventStream('user-123'); - - $readModel = new ReadModelMock(); - - $projection = $this->projectionManager->createReadModelProjection('test_projection', $readModel); - - $this->assertEquals('test_projection', $projection->getName()); - - $projection - ->init(function (): array { - return ['count' => 0]; - }) - ->fromStream('user-123') - ->when([ - UsernameChanged::class => function (array $state, UsernameChanged $event): array { - $state['count']++; - - return $state; - }, - ]) - ->run(false); - - $this->assertEquals(49, $projection->getState()['count']); - - $projection->reset(); - - $projection->run(false); - - $projection->run(false); - - $this->assertEquals(49, $projection->getState()['count']); - } - - /** - * @test - */ - public function it_can_project_from_stream_and_delete(): void - { - $this->prepareEventStream('user-123'); - - $readModel = new ReadModelMock(); - - $projection = $this->projectionManager->createReadModelProjection('test_projection', $readModel); - - $this->assertEquals('test_projection', $projection->getName()); - - $projection - ->init(function (): array { - return ['count' => 0]; - }) - ->fromStream('user-123') - ->when([ - UsernameChanged::class => function (array $state, UsernameChanged $event): array { - $state['count']++; - - return $state; - }, - ]) - ->run(false); - - $this->assertEquals(49, $projection->getState()['count']); - - $projection->delete(true); - - $this->assertFalse($readModel->isInitialized()); - } - - /** - * @test - */ - public function it_can_be_stopped_while_processing(): void - { - $this->prepareEventStream('user-123'); - - $readModel = new ReadModelMock(); - - $projection = $this->projectionManager->createReadModelProjection('test_projection', $readModel); - - $projection - ->init(function (): array { - return ['count' => 0]; - }) - ->fromStream('user-123') - ->whenAny(function (array $state, Message $event): array { - $state['count']++; - - if ($state['count'] === 10) { - $this->stop(); - } - - return $state; - }) - ->run(false); - - $this->assertEquals(10, $projection->getState()['count']); - } - - /** - * @test - */ - public function it_can_query_from_streams(): void - { - $this->prepareEventStream('user-123'); - $this->prepareEventStream('user-234'); - - $readModel = new ReadModelMock(); - - $projection = $this->projectionManager->createReadModelProjection('test_projection', $readModel); - - $projection - ->init(function (): array { - return ['count' => 0]; - }) - ->fromStreams('user-123', 'user-234') - ->whenAny( - function (array $state, Message $event): array { - $state['count']++; - - return $state; - } - ) - ->run(false); - - $this->assertEquals(100, $projection->getState()['count']); - } - - /** - * @test - */ - public function it_can_query_from_stream_and_filter_with_metadata_matcher(): void - { - $this->prepareEventStream('user-123'); - - $readModel = new ReadModelMock(); - - $projection = $this->projectionManager->createReadModelProjection('test_projection', $readModel); - $metadataMatcher = (new MetadataMatcher()) - ->withMetadataMatch('_aggregate_version', Operator::EQUALS(), 10); - - $projection - ->init(function (): array { - return ['count' => 0, 'version' => null]; - }) - ->fromStream('user-123', $metadataMatcher) - ->whenAny( - function (array $state, Message $event): array { - $state['count']++; - $state['version'] = $event->metadata()['_aggregate_version']; - - return $state; - } - ) - ->run(false); - - $this->assertEquals(1, $projection->getState()['count']); - $this->assertEquals(10, $projection->getState()['version']); - } - - /** - * @test - */ - public function it_can_query_from_all_ignoring_internal_streams(): void - { - $this->prepareEventStream('user-123'); - $this->prepareEventStream('user-234'); - $this->prepareEventStream('$iternal-345'); - - $testCase = $this; - - $readModel = new ReadModelMock(); - - $projection = $this->projectionManager->createReadModelProjection('test_projection', $readModel); - - $projection - ->init(function (): array { - return ['count' => 0]; - }) - ->fromAll() - ->whenAny( - function (array $state, Message $event) use ($testCase): array { - $state['count']++; - if ($state['count'] < 51) { - $testCase->assertEquals('user-123', $this->streamName()); - } else { - $testCase->assertEquals('user-234', $this->streamName()); - } - - return $state; - } - ) - ->run(false); - - $this->assertEquals(100, $projection->getState()['count']); - } - - /** - * @test - */ - public function it_can_query_from_category_with_when_any(): void - { - $this->prepareEventStream('user-123'); - $this->prepareEventStream('user-234'); - - $readModel = new ReadModelMock(); - - $projection = $this->projectionManager->createReadModelProjection('test_projection', $readModel); - - $projection - ->init(function (): array { - return ['count' => 0]; - }) - ->fromCategory('user') - ->whenAny( - function (array $state, Message $event): array { - $state['count']++; - - return $state; - } - ) - ->run(false); - - $this->assertEquals(100, $projection->getState()['count']); - } - - /** - * @test - */ - public function it_can_query_from_categories_with_when(): void - { - $this->prepareEventStream('user-123'); - $this->prepareEventStream('user-234'); - $this->prepareEventStream('guest-345'); - $this->prepareEventStream('guest-456'); - - $readModel = new ReadModelMock(); - - $projection = $this->projectionManager->createReadModelProjection('test_projection', $readModel); - - $projection - ->init(function (): array { - return ['count' => 0]; - }) - ->fromCategories('user', 'guest') - ->when([ - UserCreated::class => function (array $state, Message $event): array { - $state['count']++; - - return $state; - }, - ]) - ->run(false); - - $this->assertEquals(4, $projection->getState()['count']); - } - - /** - * @test - */ - public function it_resumes_projection_from_position(): void - { - $this->prepareEventStream('user-123'); - - $readModel = new ReadModelMock(); - - $projection = $this->projectionManager->createReadModelProjection('test_projection', $readModel); - - $projection - ->init(function (): array { - return ['count' => 0]; - }) - ->fromStreams('user-123', 'user-234') - ->when([ - UsernameChanged::class => function (array $state, Message $event): array { - $state['count']++; - - return $state; - }, - ]) - ->run(false); - - $this->assertEquals(49, $projection->getState()['count']); - - $events = []; - for ($i = 51; $i <= 100; $i++) { - $events[] = UsernameChanged::with([ - 'name' => \uniqid('name_'), - ], $i); - } - - $this->eventStore->appendTo(new StreamName('user-123'), new ArrayIterator($events)); - - $this->prepareEventStream('user-234'); - - $projection->run(false); - - $this->assertEquals(148, $projection->getState()['count']); - } - - /** - * @test - */ - public function it_resets_to_empty_array(): void - { - $readModel = new ReadModelMock(); - - $projection = $this->projectionManager->createReadModelProjection('test_projection', $readModel); - - $state = $projection->getState(); - - $this->assertInternalType('array', $state); - - $projection->reset(); - - $state2 = $projection->getState(); - - $this->assertInternalType('array', $state2); - } - - /** - * @test - */ - public function it_throws_exception_when_init_callback_provided_twice(): void - { - $this->expectException(RuntimeException::class); - - $readModel = new ReadModelMock(); - - $projection = $this->projectionManager->createReadModelProjection('test_projection', $readModel); - - $projection->init(function (): array { - return []; - }); - $projection->init(function (): array { - return []; - }); - } - - /** - * @test - */ - public function it_throws_exception_when_from_called_twice(): void - { - $this->expectException(RuntimeException::class); - - $readModel = new ReadModelMock(); - - $projection = $this->projectionManager->createReadModelProjection('test_projection', $readModel); - - $projection->fromStream('foo'); - $projection->fromStream('bar'); - } - - /** - * @test - */ - public function it_throws_exception_when_from_called_twice_2(): void - { - $this->expectException(RuntimeException::class); - - $readModel = new ReadModelMock(); - - $projection = $this->projectionManager->createReadModelProjection('test_projection', $readModel); - - $projection->fromStreams('foo'); - $projection->fromCategory('bar'); - } - - /** - * @test - */ - public function it_throws_exception_when_from_called_twice_3(): void - { - $this->expectException(RuntimeException::class); - - $readModel = new ReadModelMock(); - - $projection = $this->projectionManager->createReadModelProjection('test_projection', $readModel); - - $projection->fromCategory('foo'); - $projection->fromStreams('bar'); - } - - /** - * @test - */ - public function it_throws_exception_when_from_called_twice_4(): void - { - $this->expectException(RuntimeException::class); - - $readModel = new ReadModelMock(); - - $projection = $this->projectionManager->createReadModelProjection('test_projection', $readModel); - - $projection->fromCategories('foo'); - $projection->fromCategories('bar'); - } - - /** - * @test - */ - public function it_throws_exception_when_from_called_twice_5(): void - { - $this->expectException(RuntimeException::class); - - $readModel = new ReadModelMock(); - - $projection = $this->projectionManager->createReadModelProjection('test_projection', $readModel); - - $projection->fromCategories('foo'); - $projection->fromAll('bar'); - } - - /** - * @test - */ - public function it_throws_exception_when_when_called_twice_(): void - { - $this->expectException(RuntimeException::class); - - $readModel = new ReadModelMock(); - - $projection = $this->projectionManager->createReadModelProjection('test_projection', $readModel); - - $projection->when(['foo' => function (): void { - }]); - $projection->when(['foo' => function (): void { - }]); - } - - /** - * @test - */ - public function it_throws_exception_when_invalid_handlers_configured(): void - { - $this->expectException(InvalidArgumentException::class); - - $readModel = new ReadModelMock(); - - $projection = $this->projectionManager->createReadModelProjection('test_projection', $readModel); - - $projection->when(['1' => function (): void { - }]); - } - - /** - * @test - */ - public function it_throws_exception_when_invalid_handlers_configured_2(): void - { - $this->expectException(InvalidArgumentException::class); - - $readModel = new ReadModelMock(); - - $projection = $this->projectionManager->createReadModelProjection('test_projection', $readModel); - - $projection->when(['foo' => 'invalid']); - } - - /** - * @test - */ - public function it_throws_exception_when_whenAny_called_twice(): void - { - $this->expectException(RuntimeException::class); - - $readModel = new ReadModelMock(); - - $projection = $this->projectionManager->createReadModelProjection('test_projection', $readModel); - - $projection->whenAny(function (): void { - }); - $projection->whenAny(function (): void { - }); - } - - /** - * @test - */ - public function it_throws_exception_on_run_when_nothing_configured(): void - { - $this->expectException(RuntimeException::class); - - $readModel = new ReadModelMock(); - - $projection = $this->projectionManager->createReadModelProjection('test_projection', $readModel); - $projection->run(); - } - - /** - * @test - */ - public function it_updates_read_model_using_when(): void - { - $this->prepareEventStream('user-123'); - - $testCase = $this; - - $readModel = new ReadModelMock(); - - $projection = $this->projectionManager->createReadModelProjection('test_projection', $readModel); - - $projection - ->fromAll() - ->when([ - UserCreated::class => function ($state, Message $event) use ($testCase): void { - $testCase->assertEquals('user-123', $this->streamName()); - $this->readModel()->stack('insert', 'name', $event->payload()['name']); - }, - UsernameChanged::class => function ($state, Message $event) use ($testCase): void { - $testCase->assertEquals('user-123', $this->streamName()); - $this->readModel()->stack('update', 'name', $event->payload()['name']); - - if ($event->payload()['name'] === 'Sascha') { - $this->stop(); - } - }, - ]) - ->run(); - - $this->assertEquals('Sascha', $readModel->read('name')); - - $projection->reset(); - - $this->assertFalse($readModel->hasKey('name')); - } - - /** - * @test - */ - public function it_updates_read_model_using_when_any(): void - { - $this->prepareEventStream('user-123'); - - $readModel = new ReadModelMock(); - - $projection = $this->projectionManager->createReadModelProjection('test_projection', $readModel); - - $projection - ->init(function (): void { - $this->readModel()->stack('insert', 'name', null); - }) - ->fromStream('user-123') - ->whenAny(function ($state, Message $event): void { - $this->readModel()->stack('update', 'name', $event->payload()['name']); - - if ($event->payload()['name'] === 'Sascha') { - $this->stop(); - } - }) - ->run(); - - $this->assertEquals('Sascha', $readModel->read('name')); - } - - /** - * @test - */ - public function it_throws_exception_when_trying_to_run_two_projections_at_the_same_time(): void - { - $this->expectException(\Prooph\EventStore\Exception\RuntimeException::class); - $this->expectExceptionMessage('Another projection process is already running'); - - $this->prepareEventStream('user-123'); - - $projectionManager = $this->projectionManager; - $projection = $this->projectionManager->createReadModelProjection('test_projection', new ReadModelMock()); - - $projection - ->fromStream('user-123') - ->whenAny( - function (array $state, Message $event) use ($projectionManager): array { - $projection = $projectionManager->createReadModelProjection('test_projection', new ReadModelMock()); - - $projection - ->fromStream('user-123') - ->whenAny( - function (array $state, Message $event): void { - } - ) - ->run(); - } - ) - ->run(); - } - - /** - * @test - */ - public function it_updates_projection_and_deletes(): void - { - $this->prepareEventStream('user-123'); - - $readModel = new ReadModelMock(); - - $projection = $this->projectionManager->createReadModelProjection('test_projection', $readModel); - - $projection - ->fromStream('user-123') - ->when([ - UserCreated::class => function (array $state, UserCreated $event): array { - $this->readModel()->stack('insert', 'name', $event->payload()['name']); - $this->stop(); - - return $state; - }, - ]) - ->run(false); - - $this->assertEquals('Alex', $readModel->read('name')); - - $projection->delete(true); - - $this->assertFalse($readModel->isInitialized()); - } - - /** - * @test - */ - public function it_persists_using_single_handler(): void - { - $this->prepareEventStream('user-123'); - - $readModel = new ReadModelMock(); - - $projection = $this->projectionManager->createReadModelProjection('test_projection', $readModel, [ - ReadModelProjector::OPTION_PERSIST_BLOCK_SIZE => 10, - ]); - - $projection - ->init(function (): array { - return ['count' => 0]; - }) - ->fromCategories('user', 'guest') - ->whenAny(function (array $state, Message $event): array { - $state['count']++; - - return $state; - }) - ->run(false); - - $this->assertEquals(50, $projection->getState()['count']); - } - - /** - * @test - */ - public function it_persists_in_handlers(): void - { - $this->prepareEventStream('user-123'); - - $readModel = new ReadModelMock(); - - $projection = $this->projectionManager->createReadModelProjection('test_projection', $readModel, [ - ReadModelProjector::OPTION_PERSIST_BLOCK_SIZE => 10, - ]); - - $projection - ->init(function (): array { - return ['count' => 0]; - }) - ->fromCategories('user', 'guest') - ->when([ - UsernameChanged::class => function (array $state, Message $event): array { - $state['count']++; - - return $state; - }, - ]) - ->run(false); - - $this->assertEquals(49, $projection->getState()['count']); - } - - /** - * @test - */ - public function it_deletes_projection_before_start_when_it_was_deleted_from_outside(): void - { - $this->prepareEventStream('user-123'); - - $calledTimes = 0; - - $projection = $this->projectionManager->createReadModelProjection('test_projection', new ReadModelMock()); - - $projection - ->init(function (): array { - return ['count' => 0]; - }) - ->fromStream('user-123') - ->when([ - UsernameChanged::class => function (array $state, UsernameChanged $event) use (&$calledTimes): array { - $state['count']++; - $calledTimes++; - - return $state; - }, - ]) - ->run(false); - - $events = []; - for ($i = 51; $i <= 100; $i++) { - $events[] = UsernameChanged::with([ - 'name' => \uniqid('name_'), - ], $i); - } - - $this->eventStore->appendTo(new StreamName('user-123'), new ArrayIterator($events)); - - $this->projectionManager->deleteProjection('test_projection', false); - - $projection->run(false); - - $this->assertEquals(0, $projection->getState()['count']); - $this->assertEquals(49, $calledTimes); - } - - /** - * @test - */ - public function it_deletes_projection_incl_emitted_events_before_start_when_it_was_deleted_from_outside(): void - { - $this->prepareEventStream('user-123'); - - $calledTimes = 0; - - $projection = $this->projectionManager->createReadModelProjection('test_projection', new ReadModelMock()); - - $projection - ->init(function (): array { - return ['count' => 0]; - }) - ->fromStream('user-123') - ->when([ - UsernameChanged::class => function (array $state, UsernameChanged $event) use (&$calledTimes): array { - $state['count']++; - $calledTimes++; - - return $state; - }, - ]) - ->run(false); - - $events = []; - for ($i = 51; $i <= 100; $i++) { - $events[] = UsernameChanged::with([ - 'name' => \uniqid('name_'), - ], $i); - } - - $this->eventStore->appendTo(new StreamName('user-123'), new ArrayIterator($events)); - - $this->projectionManager->deleteProjection('test_projection', true); - - $projection->run(false); - - $this->assertEquals(0, $projection->getState()['count']); - $this->assertEquals(49, $calledTimes); - } - - /** - * @test - */ - public function it_deletes_projection_during_run_when_it_was_deleted_from_outside(): void - { - $this->prepareEventStream('user-123'); - - $calledTimes = 0; - - $projectionManager = $this->projectionManager; - $projection = $this->projectionManager->createReadModelProjection('test_projection', new ReadModelMock()); - - $projection - ->init(function (): array { - return ['count' => 0]; - }) - ->fromStream('user-123') - ->when([ - UsernameChanged::class => function (array $state, UsernameChanged $event) use (&$calledTimes, $projectionManager): array { - static $wasReset = false; - - if (! $wasReset) { - $projectionManager->deleteProjection('test_projection', false); - $wasReset = true; - } - - $state['count']++; - $calledTimes++; - - return $state; - }, - ]) - ->run(false); - - $this->assertEquals(0, $projection->getState()['count']); - $this->assertEquals(49, $calledTimes); - $this->assertEquals([], $projectionManager->fetchProjectionNames('test_projection')); - } - - /** - * @test - */ - public function it_deletes_projection_incl_emitted_events_during_run_when_it_was_deleted_from_outside(): void - { - $this->prepareEventStream('user-123'); - - $calledTimes = 0; - - $projectionManager = $this->projectionManager; - $projection = $this->projectionManager->createReadModelProjection('test_projection', new ReadModelMock()); - - $projection - ->init(function (): array { - return ['count' => 0]; - }) - ->fromStream('user-123') - ->when([ - UsernameChanged::class => function (array $state, UsernameChanged $event) use (&$calledTimes, $projectionManager): array { - static $wasReset = false; - - if (! $wasReset) { - $projectionManager->deleteProjection('test_projection', true); - $wasReset = true; - } - - $state['count']++; - $calledTimes++; - - return $state; - }, - ]) - ->run(false); - - $this->assertEquals(0, $projection->getState()['count']); - $this->assertEquals(49, $calledTimes); - $this->assertEquals([], $projectionManager->fetchProjectionNames('test_projection')); - } - - /** - * @test - */ - public function it_resets_projection_before_start_when_it_was_reset_from_outside(): void - { - $this->prepareEventStream('user-123'); - - $calledTimes = 0; - - $projection = $this->projectionManager->createReadModelProjection('test_projection', new ReadModelMock()); - - $projection - ->init(function (): array { - return ['count' => 0]; - }) - ->fromStream('user-123') - ->when([ - UsernameChanged::class => function (array $state, UsernameChanged $event) use (&$calledTimes): array { - $state['count']++; - $calledTimes++; - - return $state; - }, - ]) - ->run(false); - - $events = []; - for ($i = 51; $i <= 100; $i++) { - $events[] = UsernameChanged::with([ - 'name' => \uniqid('name_'), - ], $i); - } - - $this->eventStore->appendTo(new StreamName('user-123'), new ArrayIterator($events)); - - $this->projectionManager->resetProjection('test_projection'); - - $projection->run(false); - - $this->assertEquals(99, $projection->getState()['count']); - $this->assertEquals(148, $calledTimes); - } - - /** - * @test - */ - public function it_resets_projection_during_run_when_it_was_reset_from_outside(): void - { - $this->prepareEventStream('user-123'); - - $calledTimes = 0; - - $projectionManager = $this->projectionManager; - $projection = $this->projectionManager->createReadModelProjection('test_projection', new ReadModelMock()); - - $projection - ->init(function (): array { - return ['count' => 0]; - }) - ->fromStream('user-123') - ->when([ - UsernameChanged::class => function (array $state, UsernameChanged $event) use (&$calledTimes, $projectionManager): array { - static $wasReset = false; - - if (! $wasReset) { - $projectionManager->resetProjection('test_projection'); - $wasReset = true; - } - - $state['count']++; - $calledTimes++; - - return $state; - }, - ]) - ->run(false); - - $projection->run(false); - - $this->assertEquals(49, $projection->getState()['count']); - $this->assertEquals(98, $calledTimes); - } - - /** - * @test - */ - public function it_stops_when_projection_before_start_when_it_was_stopped_from_outside(): void - { - $this->prepareEventStream('user-123'); - - $calledTimes = 0; - - $projection = $this->projectionManager->createReadModelProjection('test_projection', new ReadModelMock()); - - $projection - ->init(function (): array { - return ['count' => 0]; - }) - ->fromStream('user-123') - ->when([ - UsernameChanged::class => function (array $state, UsernameChanged $event) use (&$calledTimes): array { - $state['count']++; - $calledTimes++; - - return $state; - }, - ]) - ->run(false); - - $events = []; - for ($i = 51; $i <= 100; $i++) { - $events[] = UsernameChanged::with([ - 'name' => \uniqid('name_'), - ], $i); - } - - $this->eventStore->appendTo(new StreamName('user-123'), new ArrayIterator($events)); - - $this->projectionManager->stopProjection('test_projection'); - - $projection->run(false); - - $this->assertEquals(49, $projection->getState()['count']); - $this->assertEquals(49, $calledTimes); - } - - /** - * @test - */ - public function it_stops_projection_during_run_when_it_was_stopped_from_outside(): void - { - $this->prepareEventStream('user-123'); - - $calledTimes = 0; - - $projectionManager = $this->projectionManager; - $projection = $this->projectionManager->createReadModelProjection('test_projection', new ReadModelMock()); - - $projection - ->init(function (): array { - return ['count' => 0]; - }) - ->fromStream('user-123') - ->when([ - UsernameChanged::class => function (array $state, UsernameChanged $event) use (&$calledTimes, $projectionManager): array { - static $wasReset = false; - - if (! $wasReset) { - $projectionManager->stopProjection('test_projection'); - $wasReset = true; - } - - $state['count']++; - $calledTimes++; - - return $state; - }, - ]) - ->run(false); - - $projection->run(false); - - $this->assertEquals(49, $projection->getState()['count']); - $this->assertEquals(49, $calledTimes); - } - - /** - * @test - */ - public function it_calls_reset_projection_also_if_init_callback_returns_state() - { - $readModel = $this->prophesize(ReadModel::class); - $readModel->reset()->shouldBeCalled(); - - $readModelProjection = $this->projectionManager->createReadModelProjection('test-projection', $readModel->reveal()); - - $readModelProjection->init(function () { - return ['state' => 'some value']; - }); - - $readModelProjection->reset(); - } - - protected function prepareEventStream(string $name): void - { - $events = []; - $events[] = UserCreated::with([ - 'name' => 'Alex', - ], 1); - for ($i = 2; $i < 50; $i++) { - $events[] = UsernameChanged::with([ - 'name' => \uniqid('name_'), - ], $i); - } - $events[] = UsernameChanged::with([ - 'name' => 'Sascha', - ], 50); - - $this->eventStore->create(new Stream(new StreamName($name), new ArrayIterator($events))); - } -} diff --git a/tests/Projection/AbstractProjectionManagerTest.php b/tests/Projection/AbstractProjectionManagerTest.php deleted file mode 100644 index 7e37c288..00000000 --- a/tests/Projection/AbstractProjectionManagerTest.php +++ /dev/null @@ -1,386 +0,0 @@ - - * (c) 2015-2018 Sascha-Oliver Prolic - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ProophTest\EventStore\Projection; - -use PHPUnit\Framework\TestCase; -use Prooph\EventStore\Exception\InvalidArgumentException; -use Prooph\EventStore\Exception\OutOfRangeException; -use Prooph\EventStore\Exception\ProjectionNotFound; -use Prooph\EventStore\Projection\ProjectionManager; -use Prooph\EventStore\Projection\ProjectionStatus; - -/** - * Common tests for all projection manager implementations - */ -abstract class AbstractProjectionManagerTest extends TestCase -{ - /** - * @var ProjectionManager - */ - protected $projectionManager; - - /** - * @test - */ - public function it_fetches_projection_names(): void - { - $projections = []; - - try { - for ($i = 0; $i < 50; $i++) { - $projection = $this->projectionManager->createProjection('user-' . $i); - $projection->fromAll()->whenAny(function (): void { - })->run(false); - $projections[] = $projection; - } - - for ($i = 0; $i < 20; $i++) { - $projection = $this->projectionManager->createProjection(\uniqid('rand')); - $projection->fromAll()->whenAny(function (): void { - })->run(false); - $projections[] = $projection; - } - - $this->assertCount(20, $this->projectionManager->fetchProjectionNames(null)); - $this->assertCount(70, $this->projectionManager->fetchProjectionNames(null, 200, 0)); - $this->assertCount(0, $this->projectionManager->fetchProjectionNames(null, 200, 100)); - $this->assertCount(10, $this->projectionManager->fetchProjectionNames(null, 10, 0)); - $this->assertCount(10, $this->projectionManager->fetchProjectionNames(null, 10, 10)); - $this->assertCount(5, $this->projectionManager->fetchProjectionNames(null, 10, 65)); - - for ($i = 0; $i < 20; $i++) { - $this->assertStringStartsWith('rand', $this->projectionManager->fetchProjectionNames(null, 1, $i)[0]); - } - - $this->assertCount(30, $this->projectionManager->fetchProjectionNamesRegex('ser-', 30, 0)); - $this->assertCount(0, $this->projectionManager->fetchProjectionNamesRegex('n-', 30, 0)); - } finally { - foreach ($projections as $projection) { - $projection->delete(false); - } - } - } - - /** - * @test - */ - public function it_fetches_projection_names_with_filter(): void - { - $projection = $this->projectionManager->createProjection('user-1'); - $projection->fromAll()->whenAny(function (): void { - })->run(false); - - $projection = $this->projectionManager->createProjection('user-2'); - $projection->fromAll()->whenAny(function (): void { - })->run(false); - - $projection = $this->projectionManager->createProjection('rand-1'); - $projection->fromAll()->whenAny(function (): void { - })->run(false); - - $projection = $this->projectionManager->createProjection('user-3'); - $projection->fromAll()->whenAny(function (): void { - })->run(false); - - $this->assertSame(['user-1'], $this->projectionManager->fetchProjectionNames('user-1')); - $this->assertSame(['user-2'], $this->projectionManager->fetchProjectionNames('user-2', 2)); - $this->assertSame(['rand-1'], $this->projectionManager->fetchProjectionNames('rand-1', 5)); - - $this->assertSame([], $this->projectionManager->fetchProjectionNames('foo')); - $this->assertSame([], $this->projectionManager->fetchProjectionNames('foo', 5)); - $this->assertSame([], $this->projectionManager->fetchProjectionNames('foo', 10, 100)); - } - - /** - * @test - */ - public function it_fetches_projection_names_sorted(): void - { - $projection = $this->projectionManager->createProjection('user-100'); - $projection->fromAll()->whenAny(function (): void { - })->run(false); - - $projection = $this->projectionManager->createProjection('user-21'); - $projection->fromAll()->whenAny(function (): void { - })->run(false); - - $projection = $this->projectionManager->createProjection('rand-5'); - $projection->fromAll()->whenAny(function (): void { - })->run(false); - - $projection = $this->projectionManager->createProjection('user-10'); - $projection->fromAll()->whenAny(function (): void { - })->run(false); - - $projection = $this->projectionManager->createProjection('user-1'); - $projection->fromAll()->whenAny(function (): void { - })->run(false); - - $this->assertEquals( - ['rand-5', 'user-1', 'user-10', 'user-100', 'user-21'], - $this->projectionManager->fetchProjectionNames(null) - ); - } - - /** - * @test - */ - public function it_throws_exception_when_fetching_projection_names_using_invalid_limit(): void - { - $this->expectException(OutOfRangeException::class); - $this->expectExceptionMessage('Invalid limit "-1" given. Must be greater than 0.'); - - $this->projectionManager->fetchProjectionNames(null, -1, 0); - } - - /** - * @test - */ - public function it_throws_exception_when_fetching_projection_names_using_invalid_offset(): void - { - $this->expectException(OutOfRangeException::class); - $this->expectExceptionMessage('Invalid offset "-1" given. Must be greater or equal than 0.'); - - $this->projectionManager->fetchProjectionNames(null, 1, -1); - } - - /** - * @test - */ - public function it_fetches_projection_names_using_regex(): void - { - for ($i = 0; $i < 50; $i++) { - $projection = $this->projectionManager->createProjection('user-' . $i); - $projection->fromAll()->whenAny(function (): void { - })->run(false); - } - - for ($i = 0; $i < 20; $i++) { - $projection = $this->projectionManager->createProjection(\uniqid('rand')); - $projection->fromAll()->whenAny(function (): void { - })->run(false); - } - - $this->assertCount(20, $this->projectionManager->fetchProjectionNamesRegex('user')); - $this->assertCount(50, $this->projectionManager->fetchProjectionNamesRegex('user', 100)); - $this->assertCount(30, $this->projectionManager->fetchProjectionNamesRegex('ser-', 30, 0)); - $this->assertCount(0, $this->projectionManager->fetchProjectionNamesRegex('n-', 30, 0)); - $this->assertCount(5, $this->projectionManager->fetchProjectionNamesRegex('rand', 100, 15)); - } - - /** - * @test - */ - public function it_fetches_projection_names_sorted_using_regex(): void - { - $projection = $this->projectionManager->createProjection('user-100'); - $projection->fromAll()->whenAny(function (): void { - })->run(false); - - $projection = $this->projectionManager->createProjection('user-21'); - $projection->fromAll()->whenAny(function (): void { - })->run(false); - - $projection = $this->projectionManager->createProjection('rand-5'); - $projection->fromAll()->whenAny(function (): void { - })->run(false); - - $projection = $this->projectionManager->createProjection('user-10'); - $projection->fromAll()->whenAny(function (): void { - })->run(false); - - $projection = $this->projectionManager->createProjection('user-1'); - $projection->fromAll()->whenAny(function (): void { - })->run(false); - - $this->assertEquals( - \json_encode(['user-1', 'user-10', 'user-100', 'user-21']), - \json_encode($this->projectionManager->fetchProjectionNamesRegex('ser-')) - ); - } - - /** - * @test - */ - public function it_throws_exception_when_fetching_projection_names_using_regex_with_invalid_limit(): void - { - $this->expectException(OutOfRangeException::class); - $this->expectExceptionMessage('Invalid limit "-1" given. Must be greater than 0.'); - - $this->projectionManager->fetchProjectionNamesRegex('foo', -1, 0); - } - - /** - * @test - */ - public function it_throws_exception_when_fetching_projection_names_using_regex_with_invalid_offset(): void - { - $this->expectException(OutOfRangeException::class); - $this->expectExceptionMessage('Invalid offset "-1" given. Must be greater or equal than 0.'); - - $this->projectionManager->fetchProjectionNamesRegex('bar', 1, -1); - } - - /** - * @test - */ - public function it_throws_exception_when_fetching_projection_names_using_invalid_regex(): void - { - $this->expectException(InvalidArgumentException::class); - $this->expectExceptionMessage('Invalid regex pattern given'); - - $this->projectionManager->fetchProjectionNamesRegex('invalid)', 10, 0); - } - - /** - * @test - */ - public function it_throws_exception_when_asked_for_unknown_projection_status(): void - { - $this->expectException(ProjectionNotFound::class); - - $this->projectionManager->fetchProjectionStatus('unkown'); - } - - /** - * @test - */ - public function it_throws_exception_when_asked_for_unknown_projection_stream_positions(): void - { - $this->expectException(ProjectionNotFound::class); - - $this->projectionManager->fetchProjectionStreamPositions('unkown'); - } - - /** - * @test - */ - public function it_throws_exception_when_asked_for_unknown_projection_state(): void - { - $this->expectException(ProjectionNotFound::class); - - $this->projectionManager->fetchProjectionState('unkown'); - } - - /** - * @test - */ - public function it_fetches_projection_status(): void - { - $projection = $this->projectionManager->createProjection('test-projection'); - $projection->fromAll()->whenAny(function (): void { - })->run(false); - - $this->assertSame(ProjectionStatus::IDLE(), $this->projectionManager->fetchProjectionStatus('test-projection')); - } - - /** - * @test - */ - public function it_fetches_projection_stream_positions(): void - { - $projection = $this->projectionManager->createProjection('test-projection'); - $projection->fromAll()->whenAny(function (): void { - })->run(false); - - $this->assertSame([], $this->projectionManager->fetchProjectionStreamPositions('test-projection')); - } - - /** - * @test - */ - public function it_fetches_projection_state(): void - { - $projection = $this->projectionManager->createProjection('test-projection'); - $projection->fromAll()->whenAny(function (): void { - })->run(false); - - $this->assertSame([], $this->projectionManager->fetchProjectionState('test-projection')); - } - - /** - * @test - */ - public function it_throws_exception_when_trying_to_delete_non_existing_projection(): void - { - $this->expectException(ProjectionNotFound::class); - - $this->projectionManager->deleteProjection('unknown', false); - } - - /** - * @test - */ - public function it_throws_exception_when_trying_to_reset_non_existing_projection(): void - { - $this->expectException(ProjectionNotFound::class); - - $this->projectionManager->resetProjection('unknown'); - } - - /** - * @test - */ - public function it_throws_exception_when_trying_to_stop_non_existing_projection(): void - { - $this->expectException(ProjectionNotFound::class); - - $this->projectionManager->stopProjection('unknown'); - } - - /** - * @test - */ - public function it_does_not_fail_deleting_twice(): void - { - $projection = $this->projectionManager->createProjection('test-projection'); - $projection->fromAll()->whenAny(function (): void { - })->run(false); - - $this->projectionManager->deleteProjection('test-projection', false); - $this->projectionManager->deleteProjection('test-projection', false); - - $this->assertTrue($this->projectionManager->fetchProjectionStatus('test-projection')->is(ProjectionStatus::DELETING())); - } - - /** - * @test - */ - public function it_does_not_fail_resetting_twice(): void - { - $projection = $this->projectionManager->createProjection('test-projection'); - $projection->fromAll()->whenAny(function (): void { - })->run(false); - - $this->projectionManager->resetProjection('test-projection'); - $this->projectionManager->resetProjection('test-projection'); - - $this->assertTrue($this->projectionManager->fetchProjectionStatus('test-projection')->is(ProjectionStatus::RESETTING())); - } - - /** - * @test - */ - public function it_does_not_fail_stopping_twice(): void - { - $projection = $this->projectionManager->createProjection('test-projection'); - $projection->fromAll()->whenAny(function (): void { - })->run(false); - - $this->projectionManager->stopProjection('test-projection'); - $this->projectionManager->stopProjection('test-projection'); - - $this->assertTrue($this->projectionManager->fetchProjectionStatus('test-projection')->is(ProjectionStatus::STOPPING())); - } -} diff --git a/tests/Projection/InMemoryEventStoreProjectorTest.php b/tests/Projection/InMemoryEventStoreProjectorTest.php deleted file mode 100644 index 98671d61..00000000 --- a/tests/Projection/InMemoryEventStoreProjectorTest.php +++ /dev/null @@ -1,239 +0,0 @@ - - * (c) 2015-2018 Sascha-Oliver Prolic - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ProophTest\EventStore\Projection; - -use Prooph\EventStore\EventStore; -use Prooph\EventStore\EventStoreDecorator; -use Prooph\EventStore\Exception\InvalidArgumentException; -use Prooph\EventStore\InMemoryEventStore; -use Prooph\EventStore\NonTransactionalInMemoryEventStore; -use Prooph\EventStore\Projection\InMemoryEventStoreProjector; -use Prooph\EventStore\Projection\InMemoryProjectionManager; - -class InMemoryEventStoreProjectorTest extends AbstractEventStoreProjectorTest -{ - /** - * @var InMemoryProjectionManager - */ - protected $projectionManager; - - /** - * @var InMemoryEventStore - */ - protected $eventStore; - - protected function setUp(): void - { - $this->eventStore = new InMemoryEventStore(); - $this->projectionManager = new InMemoryProjectionManager($this->eventStore); - } - - /** - * @test - */ - public function it_throws_exception_when_trying_to_run_two_projections_at_the_same_time(): void - { - $this->markTestSkipped('InMemoryProjectionManager cannot guard agains concurrent projections'); - } - - /** - * @test - */ - public function it_deletes_projection_during_run_when_it_was_deleted_from_outside(): void - { - $this->markTestSkipped('InMemoryProjectionManager cannot delete projections'); - } - - /** - * @test - */ - public function it_deletes_projection_before_start_when_it_was_deleted_from_outside(): void - { - $this->markTestSkipped('InMemoryProjectionManager cannot delete projections'); - } - - /** - * @test - */ - public function it_deletes_projection_incl_emitting_events_before_start_when_it_was_deleted_from_outside(): void - { - $this->markTestSkipped('InMemoryProjectionManager cannot delete projections'); - } - - /** - * @test - */ - public function it_deletes_projection_incl_emitted_events_during_run_when_it_was_deleted_from_outside(): void - { - $this->markTestSkipped('InMemoryProjectionManager cannot delete projections'); - } - - /** - * @test - */ - public function it_resets_projection_before_start_when_it_was_reset_from_outside(): void - { - $this->markTestSkipped('InMemoryProjectionManager cannot reset projections'); - } - - /** - * @test - */ - public function it_resets_projection_during_run_when_it_was_reset_from_outside(): void - { - $this->markTestSkipped('InMemoryProjectionManager cannot reset projections'); - } - - /** - * @test - */ - public function it_stops_when_projection_before_start_when_it_was_stopped_from_outside(): void - { - $this->markTestSkipped('InMemoryProjectionManager cannot stop projections'); - } - - /** - * @test - */ - public function it_stops_projection_during_run_when_it_was_stopped_from_outside(): void - { - $this->markTestSkipped('InMemoryProjectionManager cannot stop projections'); - } - - /** - * @test - */ - public function it_throws_exception_when_unknown_event_store_instance_passed(): void - { - $this->expectException(InvalidArgumentException::class); - - $eventStore = $this->prophesize(EventStore::class); - - new InMemoryEventStoreProjector($eventStore->reveal(), 'test_projection', 10, 10); - } - - /** - * @test - */ - public function it_throws_exception_when_invalid_wrapped_event_store_instance_passed(): void - { - $this->expectException(InvalidArgumentException::class); - - $eventStore = $this->prophesize(EventStore::class); - $wrappedEventStore = $this->prophesize(EventStoreDecorator::class); - $wrappedEventStore->getInnerEventStore()->willReturn($eventStore->reveal())->shouldBeCalled(); - - new InMemoryEventStoreProjector($wrappedEventStore->reveal(), 'test_projection', 1, 1); - } - - /** - * @test - */ - public function it_allows_non_transactional_event_store_instance(): void - { - $eventStore = new NonTransactionalInMemoryEventStore(); - $projector = new InMemoryEventStoreProjector($eventStore, 'test_projection', 1, 1); - - $this->assertInstanceOf(InMemoryEventStoreProjector::class, $projector); - } - - /** - * @test - */ - public function it_throws_exception_when_invalid_cache_size_given(): void - { - $this->expectException(InvalidArgumentException::class); - - new InMemoryEventStoreProjector($this->eventStore, 'test_projection', -1, 1); - } - - /** - * @test - */ - public function it_throws_exception_when_invalid_sleep_given(): void - { - $this->expectException(InvalidArgumentException::class); - - new InMemoryEventStoreProjector($this->eventStore, 'test_projection', 1, -1); - } - - /** - * @test - */ - public function it_dispatches_pcntl_signals_when_enabled(): void - { - if (! \extension_loaded('pcntl')) { - $this->markTestSkipped('The PCNTL extension is not available.'); - - return; - } - - $command = 'exec php ' . \realpath(__DIR__) . '/isolated-projection.php'; - $descriptorSpec = [ - 0 => ['pipe', 'r'], - 1 => ['pipe', 'w'], - 2 => ['pipe', 'w'], - ]; - /** - * Created process inherits env variables from this process. - * Script returns with non-standard code SIGUSR1 from the handler and -1 else - */ - $projectionProcess = \proc_open($command, $descriptorSpec, $pipes); - $processDetails = \proc_get_status($projectionProcess); - \sleep(1); - \posix_kill($processDetails['pid'], SIGQUIT); - \sleep(1); - - $processDetails = \proc_get_status($projectionProcess); - $this->assertEquals( - SIGUSR1, - $processDetails['exitcode'] - ); - } - - /** - * @test - * @small - */ - public function it_stops_immediately_after_pcntl_signal_was_received(): void - { - if (! \extension_loaded('pcntl')) { - $this->markTestSkipped('The PCNTL extension is not available.'); - - return; - } - - $command = 'exec php ' . \realpath(__DIR__) . '/isolated-long-running-projection.php'; - $descriptorSpec = [ - 0 => ['pipe', 'r'], - 1 => ['pipe', 'w'], - 2 => ['pipe', 'w'], - ]; - /** - * Created process inherits env variables from this process. - * Script returns with non-standard code SIGUSR1 from the handler and -1 else - */ - $projectionProcess = \proc_open($command, $descriptorSpec, $pipes); - $processDetails = \proc_get_status($projectionProcess); - \usleep(500000); - \posix_kill($processDetails['pid'], SIGQUIT); - \usleep(500000); - - $processDetails = \proc_get_status($projectionProcess); - $this->assertEquals( - SIGUSR1, - $processDetails['exitcode'] - ); - } -} diff --git a/tests/Projection/InMemoryEventStoreQueryTest.php b/tests/Projection/InMemoryEventStoreQueryTest.php deleted file mode 100644 index e17f3174..00000000 --- a/tests/Projection/InMemoryEventStoreQueryTest.php +++ /dev/null @@ -1,113 +0,0 @@ - - * (c) 2015-2018 Sascha-Oliver Prolic - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ProophTest\EventStore\Projection; - -use Prooph\EventStore\EventStore; -use Prooph\EventStore\EventStoreDecorator; -use Prooph\EventStore\Exception\InvalidArgumentException; -use Prooph\EventStore\InMemoryEventStore; -use Prooph\EventStore\NonTransactionalInMemoryEventStore; -use Prooph\EventStore\Projection\InMemoryEventStoreQuery; -use Prooph\EventStore\Projection\InMemoryProjectionManager; - -class InMemoryEventStoreQueryTest extends AbstractEventStoreQueryTest -{ - /** - * @var InMemoryProjectionManager - */ - protected $projectionManager; - - /** - * @var InMemoryEventStore - */ - protected $eventStore; - - protected function setUp(): void - { - $this->eventStore = new InMemoryEventStore(); - $this->projectionManager = new InMemoryProjectionManager($this->eventStore); - } - - /** - * @test - */ - public function it_throws_exception_when_unknown_event_store_instance_passed(): void - { - $this->expectException(InvalidArgumentException::class); - - $eventStore = $this->prophesize(EventStore::class); - - new InMemoryEventStoreQuery($eventStore->reveal()); - } - - /** - * @test - */ - public function it_throws_exception_when_invalid_wrapped_event_store_instance_passed(): void - { - $this->expectException(InvalidArgumentException::class); - - $eventStore = $this->prophesize(EventStore::class); - $wrappedEventStore = $this->prophesize(EventStoreDecorator::class); - $wrappedEventStore->getInnerEventStore()->willReturn($eventStore->reveal())->shouldBeCalled(); - - new InMemoryEventStoreQuery($wrappedEventStore->reveal()); - } - - /** - * @test - */ - public function it_allows_non_transactional_event_store_instance(): void - { - $eventStore = new NonTransactionalInMemoryEventStore(); - $query = new InMemoryEventStoreQuery($eventStore); - - $this->assertInstanceOf(InMemoryEventStoreQuery::class, $query); - } - - /** - * @test - * @small - */ - public function it_stops_immediately_after_pcntl_signal_was_received(): void - { - if (! \extension_loaded('pcntl')) { - $this->markTestSkipped('The PCNTL extension is not available.'); - - return; - } - - $command = 'exec php ' . \realpath(__DIR__) . '/isolated-long-running-query.php'; - $descriptorSpec = [ - 0 => ['pipe', 'r'], - 1 => ['pipe', 'w'], - 2 => ['pipe', 'w'], - ]; - /** - * Created process inherits env variables from this process. - * Script returns with non-standard code SIGUSR1 from the handler and -1 else - */ - $projectionProcess = \proc_open($command, $descriptorSpec, $pipes); - $processDetails = \proc_get_status($projectionProcess); - \usleep(500000); - \posix_kill($processDetails['pid'], SIGQUIT); - \usleep(500000); - - $processDetails = \proc_get_status($projectionProcess); - $this->assertEquals( - SIGUSR1, - $processDetails['exitcode'] - ); - } -} diff --git a/tests/Projection/InMemoryEventStoreReadModelProjectorTest.php b/tests/Projection/InMemoryEventStoreReadModelProjectorTest.php deleted file mode 100644 index 136be977..00000000 --- a/tests/Projection/InMemoryEventStoreReadModelProjectorTest.php +++ /dev/null @@ -1,250 +0,0 @@ - - * (c) 2015-2018 Sascha-Oliver Prolic - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ProophTest\EventStore\Projection; - -use Prooph\EventStore\EventStore; -use Prooph\EventStore\EventStoreDecorator; -use Prooph\EventStore\Exception\InvalidArgumentException; -use Prooph\EventStore\InMemoryEventStore; -use Prooph\EventStore\NonTransactionalInMemoryEventStore; -use Prooph\EventStore\Projection\InMemoryEventStoreReadModelProjector; -use Prooph\EventStore\Projection\InMemoryProjectionManager; -use ProophTest\EventStore\Mock\ReadModelMock; - -class InMemoryEventStoreReadModelProjectorTest extends AbstractEventStoreReadModelProjectorTest -{ - /** - * @var InMemoryProjectionManager - */ - protected $projectionManager; - - /** - * @var InMemoryEventStore - */ - protected $eventStore; - - protected function setUp(): void - { - $this->eventStore = new InMemoryEventStore(); - $this->projectionManager = new InMemoryProjectionManager($this->eventStore); - } - - /** - * @test - */ - public function it_throws_exception_when_trying_to_run_two_projections_at_the_same_time(): void - { - $this->markTestSkipped('InMemoryProjectionManager cannot guard agains concurrent projections'); - } - - /** - * @test - */ - public function it_deletes_projection_during_run_when_it_was_deleted_from_outside(): void - { - $this->markTestSkipped('InMemoryProjectionManager cannot delete projections'); - } - - /** - * @test - */ - public function it_deletes_projection_before_start_when_it_was_deleted_from_outside(): void - { - $this->markTestSkipped('InMemoryProjectionManager cannot delete projections'); - } - - /** - * @test - */ - public function it_deletes_projection_incl_emitted_events_before_start_when_it_was_deleted_from_outside(): void - { - $this->markTestSkipped('InMemoryProjectionManager cannot delete projections'); - } - - /** - * @test - */ - public function it_deletes_projection_incl_emitted_events_during_run_when_it_was_deleted_from_outside(): void - { - $this->markTestSkipped('InMemoryProjectionManager cannot delete projections'); - } - - /** - * @test - */ - public function it_resets_projection_before_start_when_it_was_reset_from_outside(): void - { - $this->markTestSkipped('InMemoryProjectionManager cannot reset projections'); - } - - /** - * @test - */ - public function it_resets_projection_during_run_when_it_was_reset_from_outside(): void - { - $this->markTestSkipped('InMemoryProjectionManager cannot reset projections'); - } - - /** - * @test - */ - public function it_stops_when_projection_before_start_when_it_was_stopped_from_outside(): void - { - $this->markTestSkipped('InMemoryProjectionManager cannot stop projections'); - } - - /** - * @test - */ - public function it_stops_projection_during_run_when_it_was_stopped_from_outside(): void - { - $this->markTestSkipped('InMemoryProjectionManager cannot stop projections'); - } - - /** - * @test - */ - public function it_throws_exception_when_invalid_cache_size_given(): void - { - $this->expectException(InvalidArgumentException::class); - - new InMemoryEventStoreReadModelProjector($this->eventStore, 'test_projection', new ReadModelMock(), -1, 1, 1); - } - - /** - * @test - */ - public function it_throws_exception_when_invalid_persist_block_size_given(): void - { - $this->expectException(InvalidArgumentException::class); - - new InMemoryEventStoreReadModelProjector($this->eventStore, 'test_projection', new ReadModelMock(), 1, -1, 25); - } - - /** - * @test - */ - public function it_throws_exception_when_invalid_sleep_given(): void - { - $this->expectException(InvalidArgumentException::class); - - new InMemoryEventStoreReadModelProjector($this->eventStore, 'test_projection', new ReadModelMock(), 1, 1, -1); - } - - /** - * @test - */ - public function it_throws_exception_when_unknown_event_store_instance_passed(): void - { - $this->expectException(InvalidArgumentException::class); - - $eventStore = $this->prophesize(EventStore::class); - - new InMemoryEventStoreReadModelProjector($eventStore->reveal(), 'test_projection', new ReadModelMock(), 1, 1, 1); - } - - /** - * @test - */ - public function it_throws_exception_when_invalid_wrapped_event_store_instance_passed(): void - { - $this->expectException(InvalidArgumentException::class); - - $eventStore = $this->prophesize(EventStore::class); - $wrappedEventStore = $this->prophesize(EventStoreDecorator::class); - $wrappedEventStore->getInnerEventStore()->willReturn($eventStore->reveal())->shouldBeCalled(); - - new InMemoryEventStoreReadModelProjector($wrappedEventStore->reveal(), 'test_projection', new ReadModelMock(), 1, 1, 1); - } - - /** - * @test - */ - public function it_allows_non_transactional_event_store_instance(): void - { - $eventStore = new NonTransactionalInMemoryEventStore(); - $projector = new InMemoryEventStoreReadModelProjector($eventStore, 'test_projection', new ReadModelMock(), 1, 1, 1); - - $this->assertInstanceOf(InMemoryEventStoreReadModelProjector::class, $projector); - } - - /** - * @test - */ - public function it_dispatches_pcntl_signals_when_enabled(): void - { - if (! \extension_loaded('pcntl')) { - $this->markTestSkipped('The PCNTL extension is not available.'); - - return; - } - - $command = 'exec php ' . \realpath(__DIR__) . '/isolated-read-model-projection.php'; - $descriptorSpec = [ - 0 => ['pipe', 'r'], - 1 => ['pipe', 'w'], - 2 => ['pipe', 'w'], - ]; - /** - * Created process inherits env variables from this process. - * Script returns with non-standard code SIGUSR1 from the handler and -1 else - */ - $projectionProcess = \proc_open($command, $descriptorSpec, $pipes); - $processDetails = \proc_get_status($projectionProcess); - \sleep(1); - \posix_kill($processDetails['pid'], SIGQUIT); - \sleep(1); - - $processDetails = \proc_get_status($projectionProcess); - $this->assertEquals( - SIGUSR1, - $processDetails['exitcode'] - ); - } - - /** - * @test - * @small - */ - public function it_stops_immediately_after_pcntl_signal_was_received(): void - { - if (! \extension_loaded('pcntl')) { - $this->markTestSkipped('The PCNTL extension is not available.'); - - return; - } - - $command = 'exec php ' . \realpath(__DIR__) . '/isolated-long-running-read-model-projection.php'; - $descriptorSpec = [ - 0 => ['pipe', 'r'], - 1 => ['pipe', 'w'], - 2 => ['pipe', 'w'], - ]; - /** - * Created process inherits env variables from this process. - * Script returns with non-standard code SIGUSR1 from the handler and -1 else - */ - $projectionProcess = \proc_open($command, $descriptorSpec, $pipes); - $processDetails = \proc_get_status($projectionProcess); - \usleep(500000); - \posix_kill($processDetails['pid'], SIGQUIT); - \usleep(500000); - - $processDetails = \proc_get_status($projectionProcess); - $this->assertEquals( - SIGUSR1, - $processDetails['exitcode'] - ); - } -} diff --git a/tests/Projection/InMemoryProjectionManagerTest.php b/tests/Projection/InMemoryProjectionManagerTest.php deleted file mode 100644 index 10789c14..00000000 --- a/tests/Projection/InMemoryProjectionManagerTest.php +++ /dev/null @@ -1,150 +0,0 @@ - - * (c) 2015-2018 Sascha-Oliver Prolic - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ProophTest\EventStore\Projection; - -use Prooph\EventStore\EventStore; -use Prooph\EventStore\EventStoreDecorator; -use Prooph\EventStore\Exception\InvalidArgumentException; -use Prooph\EventStore\Exception\RuntimeException; -use Prooph\EventStore\InMemoryEventStore; -use Prooph\EventStore\NonTransactionalInMemoryEventStore; -use Prooph\EventStore\Projection\InMemoryProjectionManager; - -class InMemoryProjectionManagerTest extends AbstractProjectionManagerTest -{ - /** - * @var InMemoryProjectionManager - */ - protected $projectionManager; - - protected function setUp() - { - $this->projectionManager = new InMemoryProjectionManager(new InMemoryEventStore()); - } - - /** - * @test - */ - public function it_throws_exception_when_invalid_event_store_instance_passed(): void - { - $this->expectException(InvalidArgumentException::class); - - $eventStore = $this->prophesize(EventStore::class); - - new InMemoryProjectionManager($eventStore->reveal()); - } - - /** - * @test - */ - public function it_throws_exception_when_invalid_wrapped_event_store_instance_passed(): void - { - $this->expectException(InvalidArgumentException::class); - - $eventStore = $this->prophesize(EventStore::class); - $wrappedEventStore = $this->prophesize(EventStoreDecorator::class); - $wrappedEventStore->getInnerEventStore()->willReturn($eventStore->reveal())->shouldBeCalled(); - - new InMemoryProjectionManager($wrappedEventStore->reveal()); - } - - /** - * @test - */ - public function it_allows_non_transactional_event_store_instance(): void - { - $eventStore = new NonTransactionalInMemoryEventStore(); - $manager = new InMemoryProjectionManager($eventStore); - - $this->assertInstanceOf(InMemoryProjectionManager::class, $manager); - } - - /** - * @test - */ - public function it_cannot_delete_projections(): void - { - $this->expectException(RuntimeException::class); - - $this->projectionManager->deleteProjection('foo', true); - } - - /** - * @test - */ - public function it_cannot_reset_projections(): void - { - $this->expectException(RuntimeException::class); - - $this->projectionManager->resetProjection('foo'); - } - - /** - * @test - */ - public function it_cannot_stop_projections(): void - { - $this->expectException(RuntimeException::class); - - $this->projectionManager->stopProjection('foo'); - } - - /** - * @test - */ - public function it_throws_exception_when_trying_to_delete_non_existing_projection(): void - { - $this->markTestSkipped('Deleting a projection is not supported in ' . InMemoryProjectionManager::class); - } - - /** - * @test - */ - public function it_throws_exception_when_trying_to_reset_non_existing_projection(): void - { - $this->markTestSkipped('Resetting a projection is not supported in ' . InMemoryProjectionManager::class); - } - - /** - * @test - */ - public function it_throws_exception_when_trying_to_stop_non_existing_projection(): void - { - $this->markTestSkipped('Stopping a projection is not supported in ' . InMemoryProjectionManager::class); - } - - /** - * @test - */ - public function it_does_not_fail_deleting_twice(): void - { - $this->markTestSkipped('Deleting a projection is not supported in ' . InMemoryProjectionManager::class); - } - - /** - * @test - */ - public function it_does_not_fail_resetting_twice(): void - { - $this->markTestSkipped('Resetting a projection is not supported in ' . InMemoryProjectionManager::class); - } - - /** - * @test - */ - public function it_does_not_fail_stopping_twice(): void - { - $this->markTestSkipped('Stopping a projection is not supported in ' . InMemoryProjectionManager::class); - } -} diff --git a/tests/Projection/isolated-long-running-projection.php b/tests/Projection/isolated-long-running-projection.php deleted file mode 100755 index f5bc5ce0..00000000 --- a/tests/Projection/isolated-long-running-projection.php +++ /dev/null @@ -1,49 +0,0 @@ - - * (c) 2015-2018 Sascha-Oliver Prolic - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -use Prooph\EventStore\InMemoryEventStore; -use Prooph\EventStore\Projection\InMemoryProjectionManager; -use Prooph\EventStore\Projection\Projector; -use Prooph\EventStore\Stream; -use Prooph\EventStore\StreamName; -use ProophTest\EventStore\Mock\TestDomainEvent; - -require __DIR__ . '/../../vendor/autoload.php'; - -$eventStore = new InMemoryEventStore(); -$events = []; - -for ($i = 0; $i < 100; $i++) { - $events[] = TestDomainEvent::with(['test' => 1], $i); - $i++; -} - -$eventStore->create(new Stream(new StreamName('user-123'), new ArrayIterator($events))); - -$projectionManager = new InMemoryProjectionManager($eventStore); -$projection = $projectionManager->createProjection( - 'test_projection', - [ - Projector::OPTION_PCNTL_DISPATCH => true, - ] -); -\pcntl_signal(SIGQUIT, function () use ($projection) { - $projection->stop(); - exit(SIGUSR1); -}); -$projection - ->fromStream('user-123') - ->whenAny(function () { - \usleep(500000); - }) - ->run(); diff --git a/tests/Projection/isolated-long-running-query.php b/tests/Projection/isolated-long-running-query.php deleted file mode 100755 index 06b0d239..00000000 --- a/tests/Projection/isolated-long-running-query.php +++ /dev/null @@ -1,50 +0,0 @@ - - * (c) 2015-2018 Sascha-Oliver Prolic - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -use Prooph\EventStore\InMemoryEventStore; -use Prooph\EventStore\Projection\InMemoryProjectionManager; -use Prooph\EventStore\Projection\Query; -use Prooph\EventStore\Stream; -use Prooph\EventStore\StreamName; -use ProophTest\EventStore\Mock\TestDomainEvent; - -require __DIR__ . '/../../vendor/autoload.php'; - -$eventStore = new InMemoryEventStore(); -$events = []; - -for ($i = 0; $i < 100; $i++) { - $events[] = TestDomainEvent::with(['test' => 1], $i); - $i++; -} - -$eventStore->create(new Stream(new StreamName('user-123'), new ArrayIterator($events))); - -$projectionManager = new InMemoryProjectionManager($eventStore); -$query = $projectionManager->createQuery( - [ - Query::OPTION_PCNTL_DISPATCH => true, - ] -); - -\pcntl_signal(SIGQUIT, function () use ($query) { - $query->stop(); - exit(SIGUSR1); -}); - -$query - ->fromStreams('user-123') - ->whenAny(function () { - \usleep(500000); - }) - ->run(); diff --git a/tests/Projection/isolated-long-running-read-model-projection.php b/tests/Projection/isolated-long-running-read-model-projection.php deleted file mode 100755 index 03148687..00000000 --- a/tests/Projection/isolated-long-running-read-model-projection.php +++ /dev/null @@ -1,78 +0,0 @@ - - * (c) 2015-2018 Sascha-Oliver Prolic - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -use Prooph\EventStore\InMemoryEventStore; -use Prooph\EventStore\Projection\InMemoryProjectionManager; -use Prooph\EventStore\Projection\ReadModel; -use Prooph\EventStore\Projection\ReadModelProjector; -use Prooph\EventStore\Stream; -use Prooph\EventStore\StreamName; -use ProophTest\EventStore\Mock\TestDomainEvent; - -require __DIR__ . '/../../vendor/autoload.php'; - -$readModel = new class() implements ReadModel { - public function init(): void - { - } - - public function isInitialized(): bool - { - return true; - } - - public function reset(): void - { - } - - public function delete(): void - { - } - - public function stack(string $operation, ...$args): void - { - } - - public function persist(): void - { - } -}; - -$eventStore = new InMemoryEventStore(); -$events = []; - -for ($i = 0; $i < 100; $i++) { - $events[] = TestDomainEvent::with(['test' => 1], $i); - $i++; -} - -$eventStore->create(new Stream(new StreamName('user-123'), new ArrayIterator($events))); - -$projectionManager = new InMemoryProjectionManager($eventStore); -$projection = $projectionManager->createReadModelProjection( - 'test_projection', - $readModel, - [ - ReadModelProjector::OPTION_PCNTL_DISPATCH => true, - ] -); -\pcntl_signal(SIGQUIT, function () use ($projection) { - $projection->stop(); - exit(SIGUSR1); -}); -$projection - ->fromStream('user-123') - ->whenAny(function () { - \usleep(500000); - }) - ->run(); diff --git a/tests/Projection/isolated-projection.php b/tests/Projection/isolated-projection.php deleted file mode 100755 index 5ac23003..00000000 --- a/tests/Projection/isolated-projection.php +++ /dev/null @@ -1,40 +0,0 @@ - - * (c) 2015-2018 Sascha-Oliver Prolic - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -use Prooph\EventStore\InMemoryEventStore; -use Prooph\EventStore\Projection\InMemoryProjectionManager; -use Prooph\EventStore\Projection\Projector; -use Prooph\EventStore\Stream; -use Prooph\EventStore\StreamName; - -require __DIR__ . '/../../vendor/autoload.php'; - -$eventStore = new InMemoryEventStore(); -$eventStore->create(new Stream(new StreamName('user-123'), new ArrayIterator([]))); - -$projectionManager = new InMemoryProjectionManager($eventStore); -$projection = $projectionManager->createProjection( - 'test_projection', - [ - Projector::OPTION_PCNTL_DISPATCH => true, - ] -); -\pcntl_signal(SIGQUIT, function () use ($projection) { - $projection->stop(); - exit(SIGUSR1); -}); -$projection - ->fromStream('user-123') - ->whenAny(function () { - }) - ->run(); diff --git a/tests/Projection/isolated-read-model-projection.php b/tests/Projection/isolated-read-model-projection.php deleted file mode 100755 index 400cddd4..00000000 --- a/tests/Projection/isolated-read-model-projection.php +++ /dev/null @@ -1,69 +0,0 @@ - - * (c) 2015-2018 Sascha-Oliver Prolic - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -use Prooph\EventStore\InMemoryEventStore; -use Prooph\EventStore\Projection\InMemoryProjectionManager; -use Prooph\EventStore\Projection\ReadModel; -use Prooph\EventStore\Projection\ReadModelProjector; -use Prooph\EventStore\Stream; -use Prooph\EventStore\StreamName; - -require __DIR__ . '/../../vendor/autoload.php'; - -$readModel = new class() implements ReadModel { - public function init(): void - { - } - - public function isInitialized(): bool - { - return true; - } - - public function reset(): void - { - } - - public function delete(): void - { - } - - public function stack(string $operation, ...$args): void - { - } - - public function persist(): void - { - } -}; - -$eventStore = new InMemoryEventStore(); -$eventStore->create(new Stream(new StreamName('user-123'), new ArrayIterator([]))); - -$projectionManager = new InMemoryProjectionManager($eventStore); -$projection = $projectionManager->createReadModelProjection( - 'test_projection', - $readModel, - [ - ReadModelProjector::OPTION_PCNTL_DISPATCH => true, - ] -); -\pcntl_signal(SIGQUIT, function () use ($projection) { - $projection->stop(); - exit(SIGUSR1); -}); -$projection - ->fromStream('user-123') - ->whenAny(function () { - }) - ->run(); diff --git a/tests/ReadOnlyEventStoreWrapperTest.php b/tests/ReadOnlyEventStoreWrapperTest.php deleted file mode 100644 index 1fd3203b..00000000 --- a/tests/ReadOnlyEventStoreWrapperTest.php +++ /dev/null @@ -1,52 +0,0 @@ - - * (c) 2015-2018 Sascha-Oliver Prolic - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ProophTest\EventStore; - -use PHPUnit\Framework\TestCase; -use Prooph\EventStore\EventStore; -use Prooph\EventStore\ReadOnlyEventStoreWrapper; -use Prooph\EventStore\StreamName; -use Prophecy\Argument; - -class ReadOnlyEventStoreWrapperTest extends TestCase -{ - /** - * @test - */ - public function it_delegates_method_calls_to_internal_event_store(): void - { - $eventStore = $this->prophesize(EventStore::class); - $eventStore->fetchStreamMetadata(Argument::type(StreamName::class))->willReturn([])->shouldBeCalled(); - $eventStore->hasStream(Argument::type(StreamName::class))->willReturn(true)->shouldBeCalled(); - $eventStore->load(Argument::type(StreamName::class), 0, 10, null)->willReturn(new \ArrayIterator())->shouldBeCalled(); - $eventStore->loadReverse(Argument::type(StreamName::class), 0, 10, null)->willReturn(new \ArrayIterator())->shouldBeCalled(); - $eventStore->fetchStreamNames('foo', null, 0, 10)->willReturn(['foobar', 'foobaz'])->shouldBeCalled(); - $eventStore->fetchStreamNamesRegex('^foo', null, 0, 10)->willReturn(['foobar', 'foobaz'])->shouldBeCalled(); - $eventStore->fetchCategoryNames('foo', 0, 10)->willReturn(['foo-1', 'foo-2'])->shouldBeCalled(); - $eventStore->fetchCategoryNamesRegex('^foo', 0, 10)->willReturn(['foo-1', 'foo-2'])->shouldBeCalled(); - - $testStream = new StreamName('foo'); - - $readOnlyEventStore = new ReadOnlyEventStoreWrapper($eventStore->reveal()); - - $this->assertEmpty($readOnlyEventStore->fetchStreamMetadata($testStream)); - $this->assertTrue($readOnlyEventStore->hasStream($testStream)); - $this->assertInstanceOf(\ArrayIterator::class, $readOnlyEventStore->load($testStream, 0, 10)); - $this->assertInstanceOf(\ArrayIterator::class, $readOnlyEventStore->loadReverse($testStream, 0, 10)); - $this->assertSame(['foobar', 'foobaz'], $readOnlyEventStore->fetchStreamNames('foo', null, 0, 10)); - $this->assertSame(['foobar', 'foobaz'], $readOnlyEventStore->fetchStreamNamesRegex('^foo', null, 0, 10)); - $this->assertSame(['foo-1', 'foo-2'], $readOnlyEventStore->fetchCategoryNames('foo', 0, 10)); - $this->assertSame(['foo-1', 'foo-2'], $readOnlyEventStore->fetchCategoryNamesRegex('^foo', 0, 10)); - } -} diff --git a/tests/StreamIterator/AbstractStreamIteratorTest.php b/tests/StreamIterator/AbstractStreamIteratorTest.php deleted file mode 100644 index e13d28c0..00000000 --- a/tests/StreamIterator/AbstractStreamIteratorTest.php +++ /dev/null @@ -1,40 +0,0 @@ - - * (c) 2015-2018 Sascha-Oliver Prolic - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ProophTest\EventStore\StreamIterator; - -use PHPUnit\Framework\TestCase; -use Prooph\EventStore\StreamIterator\StreamIterator; - -abstract class AbstractStreamIteratorTest extends TestCase -{ - /** - * @test - */ - public function it_implements_iterator(): void - { - $iterator = $this->getMockBuilder(StreamIterator::class)->getMock(); - - $this->assertInstanceOf(\Iterator::class, $iterator); - } - - /** - * @test - */ - public function it_implements_countable(): void - { - $iterator = $this->getMockBuilder(StreamIterator::class)->getMock(); - - $this->assertInstanceOf(\Countable::class, $iterator); - } -} diff --git a/tests/StreamIterator/EmptyStreamIteratorTest.php b/tests/StreamIterator/EmptyStreamIteratorTest.php deleted file mode 100644 index ba08d7a7..00000000 --- a/tests/StreamIterator/EmptyStreamIteratorTest.php +++ /dev/null @@ -1,50 +0,0 @@ - - * (c) 2015-2018 Sascha-Oliver Prolic - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ProophTest\EventStore\StreamIterator; - -use Prooph\EventStore\StreamIterator\EmptyStreamIterator; -use Prooph\EventStore\StreamIterator\StreamIterator; - -class EmptyStreamIteratorTest extends AbstractStreamIteratorTest -{ - /** - * @test - */ - public function it_implements_stream_iterator(): void - { - $iterator = new EmptyStreamIterator(); - - $this->assertInstanceOf(StreamIterator::class, $iterator); - } - - /** - * @test - */ - public function it_implements_empty_iterator(): void - { - $iterator = new EmptyStreamIterator(); - - $this->assertInstanceOf(\EmptyIterator::class, $iterator); - } - - /** - * @test - */ - public function it_counts_correct(): void - { - $iterator = new EmptyStreamIterator(); - - $this->assertEquals(0, $iterator->count()); - } -} diff --git a/tests/StreamIterator/InMemoryStreamIteratorTest.php b/tests/StreamIterator/InMemoryStreamIteratorTest.php deleted file mode 100644 index f9ec59fe..00000000 --- a/tests/StreamIterator/InMemoryStreamIteratorTest.php +++ /dev/null @@ -1,40 +0,0 @@ - - * (c) 2015-2018 Sascha-Oliver Prolic - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ProophTest\EventStore\StreamIterator; - -use Prooph\EventStore\StreamIterator\InMemoryStreamIterator; -use Prooph\EventStore\StreamIterator\StreamIterator; - -class InMemoryStreamIteratorTest extends AbstractStreamIteratorTest -{ - /** - * @test - */ - public function it_implements_stream_iterator(): void - { - $iterator = new InMemoryStreamIterator(); - - $this->assertInstanceOf(StreamIterator::class, $iterator); - } - - /** - * @test - */ - public function it_implements_array_iterator(): void - { - $iterator = new InMemoryStreamIterator(); - - $this->assertInstanceOf(\ArrayIterator::class, $iterator); - } -} diff --git a/tests/StreamNameTest.php b/tests/StreamNameTest.php deleted file mode 100644 index e721b108..00000000 --- a/tests/StreamNameTest.php +++ /dev/null @@ -1,29 +0,0 @@ - - * (c) 2015-2018 Sascha-Oliver Prolic - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ProophTest\EventStore; - -use PHPUnit\Framework\TestCase; -use Prooph\EventStore\StreamName; - -class StreamNameTest extends TestCase -{ - /** - * @test - */ - public function it_delegates_to_string(): void - { - $streamName = new StreamName('foo'); - $this->assertEquals('foo', (string) $streamName); - } -} diff --git a/tests/TransactionalActionEventEmitterEventStoreTest.php b/tests/TransactionalActionEventEmitterEventStoreTest.php deleted file mode 100644 index e656ac0f..00000000 --- a/tests/TransactionalActionEventEmitterEventStoreTest.php +++ /dev/null @@ -1,137 +0,0 @@ - - * (c) 2015-2018 Sascha-Oliver Prolic - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ProophTest\EventStore; - -use PHPUnit\Framework\TestCase; -use Prooph\Common\Event\ProophActionEventEmitter; -use Prooph\EventStore\Exception\TransactionAlreadyStarted; -use Prooph\EventStore\Exception\TransactionNotStarted; -use Prooph\EventStore\InMemoryEventStore; -use Prooph\EventStore\Stream; -use Prooph\EventStore\StreamName; -use Prooph\EventStore\TransactionalActionEventEmitterEventStore; - -class TransactionalActionEventEmitterEventStoreTest extends TestCase -{ - use EventStoreTestStreamTrait; - - /** - * @var TransactionalActionEventEmitterEventStore - */ - protected $eventStore; - - protected function setUp(): void - { - $eventEmitter = new ProophActionEventEmitter(TransactionalActionEventEmitterEventStore::ALL_EVENTS); - - $this->eventStore = new TransactionalActionEventEmitterEventStore(new InMemoryEventStore(), $eventEmitter); - } - - /** - * @test - */ - public function it_works_transactional(): void - { - $streamName = $this->prophesize(StreamName::class); - $streamName->toString()->willReturn('test')->shouldBeCalled(); - $streamName = $streamName->reveal(); - - $stream = $this->prophesize(Stream::class); - $stream->streamName()->willReturn($streamName); - $stream->metadata()->willReturn(['foo' => 'bar'])->shouldBeCalled(); - $stream->streamEvents()->willReturn(new \ArrayIterator()); - - $this->eventStore->beginTransaction(); - - $this->eventStore->create($stream->reveal()); - - $this->assertFalse($this->eventStore->hasStream($streamName)); - - $this->eventStore->commit(); - - $this->assertTrue($this->eventStore->hasStream($streamName)); - } - - /** - * @test - */ - public function it_rolls_back_transaction(): void - { - $streamName = $this->prophesize(StreamName::class); - $streamName->toString()->willReturn('test')->shouldBeCalled(); - $streamName = $streamName->reveal(); - - $stream = $this->prophesize(Stream::class); - $stream->streamName()->willReturn($streamName); - $stream->metadata()->willReturn(['foo' => 'bar'])->shouldBeCalled(); - $stream->streamEvents()->willReturn(new \ArrayIterator()); - - $this->eventStore->beginTransaction(); - - $this->assertTrue($this->eventStore->inTransaction()); - - $this->eventStore->create($stream->reveal()); - - $this->assertFalse($this->eventStore->hasStream($streamName)); - - $this->eventStore->rollback(); - - $this->assertFalse($this->eventStore->hasStream($streamName)); - } - - /** - * @test - */ - public function it_throws_exception_when_no_transaction_started_on_commit(): void - { - $this->expectException(TransactionNotStarted::class); - - $this->eventStore->commit(); - } - - /** - * @test - */ - public function it_throws_exception_when_no_transaction_started_on_rollback(): void - { - $this->expectException(TransactionNotStarted::class); - - $this->eventStore->rollback(); - } - - /** - * @test - */ - public function it_throws_exception_when_transaction_already_started(): void - { - $this->expectException(TransactionAlreadyStarted::class); - - $this->eventStore->beginTransaction(); - $this->eventStore->beginTransaction(); - } - - /** - * @test - */ - public function it_wraps_up_code_in_transaction_properly(): void - { - $transactionResult = $this->eventStore->transactional(function () { - $this->eventStore->create($this->getTestStream()); - - return 'Result'; - }); - - $this->assertSame('Result', $transactionResult); - } -} diff --git a/tests/TransactionalEventStoreTestTrait.php b/tests/TransactionalEventStoreTestTrait.php deleted file mode 100644 index 4ed1d12a..00000000 --- a/tests/TransactionalEventStoreTestTrait.php +++ /dev/null @@ -1,246 +0,0 @@ - - * (c) 2015-2018 Sascha-Oliver Prolic - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ProophTest\EventStore; - -use ArrayIterator; -use Prooph\EventStore\EventStore; -use Prooph\EventStore\Exception\StreamNotFound; -use Prooph\EventStore\Exception\TransactionAlreadyStarted; -use Prooph\EventStore\Exception\TransactionNotStarted; -use Prooph\EventStore\Stream; -use Prooph\EventStore\StreamName; -use Prooph\EventStore\TransactionalEventStore; -use ProophTest\EventStore\Mock\UsernameChanged; - -/** - * Common tests for all transactional event store implementations - */ -trait TransactionalEventStoreTestTrait -{ - /** - * @var TransactionalEventStore - */ - protected $eventStore; - - /** - * @test - */ - public function it_works_transactional(): void - { - $streamName = $this->prophesize(StreamName::class); - $streamName->toString()->willReturn('Prooph\Model\User')->shouldBeCalled(); - $streamName = $streamName->reveal(); - - $stream = $this->prophesize(Stream::class); - $stream->streamName()->willReturn($streamName); - $stream->metadata()->willReturn(['foo' => 'bar'])->shouldBeCalled(); - $stream->streamEvents()->willReturn(new \ArrayIterator()); - - $this->eventStore->beginTransaction(); - - $this->eventStore->create($stream->reveal()); - - $this->eventStore->commit(); - - $this->assertTrue($this->eventStore->hasStream($streamName)); - } - - /** - * @test - */ - public function it_wraps_up_code_in_transaction_properly(): void - { - $transactionResult = $this->eventStore->transactional(function (EventStore $eventStore) { - $this->eventStore->create($this->getTestStream()); - $this->assertSame($this->eventStore, $eventStore); - - return 'Result'; - }); - - $this->assertSame('Result', $transactionResult); - - $secondStreamEvent = UsernameChanged::with( - ['new_name' => 'John Doe'], - 2 - ); - - $transactionResult = $this->eventStore->transactional(function (EventStore $eventStore) use ($secondStreamEvent) { - $this->eventStore->appendTo(new StreamName('Prooph\Model\User'), new ArrayIterator([$secondStreamEvent])); - $this->assertSame($this->eventStore, $eventStore); - - return 'Second Result'; - }); - - $this->assertSame('Second Result', $transactionResult); - - $streamEvents = $this->eventStore->load(new StreamName('Prooph\Model\User'), 1); - - $this->assertCount(2, $streamEvents); - } - - /** - * @test - */ - public function it_rolls_back_transaction(): void - { - $streamName = $this->prophesize(StreamName::class); - $streamName->toString()->willReturn('test')->shouldBeCalled(); - $streamName = $streamName->reveal(); - - $stream = $this->prophesize(Stream::class); - $stream->streamName()->willReturn($streamName); - $stream->metadata()->willReturn(['foo' => 'bar'])->shouldBeCalled(); - $stream->streamEvents()->willReturn(new \ArrayIterator()); - - $this->eventStore->beginTransaction(); - - $this->assertTrue($this->eventStore->inTransaction()); - - $this->eventStore->create($stream->reveal()); - - $this->eventStore->rollback(); - - $this->assertFalse($this->eventStore->hasStream($streamName)); - } - - /** - * @test - */ - public function it_should_rollback_and_throw_exception_in_case_of_transaction_fail(): void - { - $this->expectException(\Exception::class); - $this->expectExceptionMessage('Transaction failed'); - - $eventStore = $this->eventStore; - - $this->eventStore->transactional(function (EventStore $es) use ($eventStore) { - $this->assertSame($es, $eventStore); - throw new \Exception('Transaction failed'); - }); - } - - /** - * @test - */ - public function it_should_return_true_by_default_if_transaction_is_used(): void - { - $transactionResult = $this->eventStore->transactional(function (EventStore $eventStore) { - $this->eventStore->create($this->getTestStream()); - $this->assertSame($this->eventStore, $eventStore); - }); - $this->assertTrue($transactionResult); - } - - /** - * @test - */ - public function it_throws_exception_when_transaction_already_started(): void - { - $this->expectException(TransactionAlreadyStarted::class); - - $this->eventStore->beginTransaction(); - $this->eventStore->beginTransaction(); - } - - /** - * @test - */ - public function it_can_commit_empty_transaction(): void - { - $this->eventStore->beginTransaction(); - $this->eventStore->commit(); - - $this->assertFalse($this->eventStore->inTransaction()); - } - - /** - * @test - */ - public function it_cannot_commit_twice(): void - { - $this->expectException(TransactionNotStarted::class); - - $this->eventStore->beginTransaction(); - $this->eventStore->commit(); - $this->eventStore->commit(); - } - - /** - * @test - */ - public function it_can_rollback_empty_transaction(): void - { - $this->assertFalse($this->eventStore->inTransaction()); - $this->eventStore->beginTransaction(); - $this->assertTrue($this->eventStore->inTransaction()); - $this->eventStore->rollback(); - $this->assertFalse($this->eventStore->inTransaction()); - } - - /** - * @test - */ - public function it_cannot_rollback_twice(): void - { - $this->expectException(TransactionNotStarted::class); - - $this->eventStore->beginTransaction(); - $this->eventStore->rollback(); - $this->eventStore->rollback(); - } - - /** - * @test - */ - public function it_throws_exception_when_no_transaction_started_on_commit(): void - { - $this->expectException(TransactionNotStarted::class); - - $this->eventStore->commit(); - } - - /** - * @test - */ - public function it_throws_exception_when_no_transaction_started_on_rollback(): void - { - $this->expectException(TransactionNotStarted::class); - - $this->eventStore->rollback(); - } - - /** - * @test - */ - public function it_loads_and_saves_within_one_transaction(): void - { - $testStream = $this->getTestStream(); - - $this->eventStore->beginTransaction(); - - $streamNotFound = false; - - try { - $this->eventStore->load($testStream->streamName()); - } catch (StreamNotFound $e) { - $streamNotFound = true; - } - - $this->assertTrue($streamNotFound); - - $this->eventStore->create($testStream); - - $this->eventStore->commit(); - } -} diff --git a/tests/Upcasting/NoOpEventUpcasterTest.php b/tests/Upcasting/NoOpEventUpcasterTest.php deleted file mode 100644 index e15baec0..00000000 --- a/tests/Upcasting/NoOpEventUpcasterTest.php +++ /dev/null @@ -1,37 +0,0 @@ - - * (c) 2015-2018 Sascha-Oliver Prolic - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ProophTest\EventStore\Upcasting; - -use PHPUnit\Framework\TestCase; -use Prooph\Common\Messaging\Message; -use Prooph\EventStore\Upcasting\NoOpEventUpcaster; - -class NoOpEventUpcasterTest extends TestCase -{ - /** - * @test - */ - public function it_does_nothing_during_upcast(): void - { - $message = $this->prophesize(Message::class); - $message = $message->reveal(); - - $upcaster = new NoOpEventUpcaster(); - - $messages = $upcaster->upcast($message); - $this->assertInternalType('array', $messages); - $this->assertNotEmpty($messages); - $this->assertSame($message, $messages[0]); - } -} diff --git a/tests/Upcasting/SingleEventUpcasterTest.php b/tests/Upcasting/SingleEventUpcasterTest.php deleted file mode 100644 index b27db4b0..00000000 --- a/tests/Upcasting/SingleEventUpcasterTest.php +++ /dev/null @@ -1,90 +0,0 @@ - - * (c) 2015-2018 Sascha-Oliver Prolic - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ProophTest\EventStore\Upcasting; - -use PHPUnit\Framework\TestCase; -use Prooph\Common\Messaging\Message; -use Prooph\EventStore\Upcasting\SingleEventUpcaster; - -class SingleEventUpcasterTest extends TestCase -{ - /** - * @test - */ - public function it_upcasts(): void - { - $upcastedMessage = $this->prophesize(Message::class); - $upcastedMessage = $upcastedMessage->reveal(); - - $message = $this->prophesize(Message::class); - $message->withAddedMetadata('key', 'value')->willReturn($upcastedMessage)->shouldBeCalled(); - $message = $message->reveal(); - - $upcaster = $this->createUpcasterWhoCanUpcast(); - - $messages = $upcaster->upcast($message); - - $this->assertInternalType('array', $messages); - $this->assertNotEmpty($messages); - $this->assertSame($upcastedMessage, $messages[0]); - } - - /** - * @test - */ - public function it_does_not_upcast_when_impossible(): void - { - $message = $this->prophesize(Message::class); - $message->withAddedMetadata('key', 'value')->shouldNotBeCalled(); - $message = $message->reveal(); - - $upcaster = $this->createUpcasterWhoCannotUpcast(); - - $messages = $upcaster->upcast($message); - - $this->assertInternalType('array', $messages); - $this->assertNotEmpty($messages); - $this->assertSame($message, $messages[0]); - } - - protected function createUpcasterWhoCanUpcast(): SingleEventUpcaster - { - return new class() extends SingleEventUpcaster { - protected function canUpcast(Message $message): bool - { - return true; - } - - protected function doUpcast(Message $message): array - { - return [$message->withAddedMetadata('key', 'value')]; - } - }; - } - - protected function createUpcasterWhoCannotUpcast(): SingleEventUpcaster - { - return new class() extends SingleEventUpcaster { - protected function canUpcast(Message $message): bool - { - return false; - } - - protected function doUpcast(Message $message): array - { - return [$message->withAddedMetadata('key', 'value')]; - } - }; - } -} diff --git a/tests/Upcasting/UpcasterChainTest.php b/tests/Upcasting/UpcasterChainTest.php deleted file mode 100644 index 56713b2f..00000000 --- a/tests/Upcasting/UpcasterChainTest.php +++ /dev/null @@ -1,87 +0,0 @@ - - * (c) 2015-2018 Sascha-Oliver Prolic - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ProophTest\EventStore\Upcasting; - -use PHPUnit\Framework\TestCase; -use Prooph\Common\Messaging\Message; -use Prooph\EventStore\Upcasting\NoOpEventUpcaster; -use Prooph\EventStore\Upcasting\SingleEventUpcaster; -use Prooph\EventStore\Upcasting\UpcasterChain; - -class UpcasterChainTest extends TestCase -{ - /** - * @test - */ - public function it_chains_upcasts(): void - { - $upcastedMessage3 = $this->prophesize(Message::class); - $upcastedMessage3 = $upcastedMessage3->reveal(); - - $upcastedMessage2 = $this->prophesize(Message::class); - $upcastedMessage2 = $upcastedMessage2->reveal(); - - $upcastedMessage1 = $this->prophesize(Message::class); - $upcastedMessage1->withAddedMetadata('key', 'other_value')->willReturn($upcastedMessage2)->shouldBeCalled(); - $upcastedMessage1->withAddedMetadata('key', 'yet_another_value')->willReturn($upcastedMessage3)->shouldBeCalled(); - $upcastedMessage1 = $upcastedMessage1->reveal(); - - $message = $this->prophesize(Message::class); - $message->withAddedMetadata('key', 'value')->willReturn($upcastedMessage1)->shouldBeCalled(); - $message = $message->reveal(); - - $upcasterOne = $this->createUpcasterWhoCanUpcast(); - $upcasterTwo = new NoOpEventUpcaster(); - $upcasterThree = $this->createUpcasterWhoCanAlsoUpcast(); - - $upcasterChain = new UpcasterChain($upcasterOne, $upcasterTwo, $upcasterThree); - - $messages = $upcasterChain->upcast($message); - - $this->assertInternalType('array', $messages); - $this->assertNotEmpty($messages); - $this->assertSame($upcastedMessage2, $messages[0]); - $this->assertSame($upcastedMessage3, $messages[1]); - } - - protected function createUpcasterWhoCanUpcast(): SingleEventUpcaster - { - return new class() extends SingleEventUpcaster { - protected function canUpcast(Message $message): bool - { - return true; - } - - protected function doUpcast(Message $message): array - { - return [$message->withAddedMetadata('key', 'value')]; - } - }; - } - - protected function createUpcasterWhoCanAlsoUpcast(): SingleEventUpcaster - { - return new class() extends SingleEventUpcaster { - protected function canUpcast(Message $message): bool - { - return true; - } - - protected function doUpcast(Message $message): array - { - return [$message->withAddedMetadata('key', 'other_value'), $message->withAddedMetadata('key', 'yet_another_value')]; - } - }; - } -} diff --git a/tests/Upcasting/UpcastingIteratorTest.php b/tests/Upcasting/UpcastingIteratorTest.php deleted file mode 100644 index 6aa0743b..00000000 --- a/tests/Upcasting/UpcastingIteratorTest.php +++ /dev/null @@ -1,150 +0,0 @@ - - * (c) 2015-2018 Sascha-Oliver Prolic - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ProophTest\EventStore\Upcasting; - -use PHPUnit\Framework\TestCase; -use Prooph\Common\Messaging\Message; -use Prooph\EventStore\Upcasting\SingleEventUpcaster; -use Prooph\EventStore\Upcasting\UpcastingIterator; - -class UpcastingIteratorTest extends TestCase -{ - /** - * @test - */ - public function it_iterates(): void - { - $upcastedMessage1 = $this->prophesize(Message::class); - $upcastedMessage1 = $upcastedMessage1->reveal(); - - $upcastedMessage2 = $this->prophesize(Message::class); - $upcastedMessage2 = $upcastedMessage2->reveal(); - - $message1 = $this->prophesize(Message::class); - $message1->metadata()->willReturn(['foo' => 'baz'])->shouldBeCalled(); - $message1 = $message1->reveal(); - - $message2 = $this->prophesize(Message::class); - $message2->metadata()->willReturn([])->shouldBeCalled(); - $message2->withAddedMetadata('key', 'value')->willReturn($upcastedMessage1)->shouldBeCalled(); - $message2->withAddedMetadata('key', 'another_value')->willReturn($upcastedMessage2)->shouldBeCalled(); - $message2 = $message2->reveal(); - - $message3 = $this->prophesize(Message::class); - $message3->metadata()->willReturn(['foo' => 'bar'])->shouldBeCalled(); - $message3->withAddedMetadata('key', 'value')->shouldNotBeCalled(); - $message3->withAddedMetadata('key', 'another_value')->shouldNotBeCalled(); - $message3 = $message3->reveal(); - - $message4 = $this->prophesize(Message::class); - $message4->metadata()->willReturn(['foo' => 'baz'])->shouldBeCalled(); - $message4 = $message4->reveal(); - - $iterator = new \ArrayIterator([ - $message1, - $message2, - $message3, - $message4, - ]); - - $upcastingIterator = new UpcastingIterator($this->createUpcaster(), $iterator); - - $this->assertEquals(0, $upcastingIterator->key()); - $this->assertTrue($upcastingIterator->valid()); - $this->assertSame($upcastedMessage1, $upcastingIterator->current()); - $upcastingIterator->next(); - $this->assertEquals(1, $upcastingIterator->key()); - $this->assertSame($upcastedMessage2, $upcastingIterator->current()); - $upcastingIterator->next(); - $this->assertEquals(2, $upcastingIterator->key()); - $this->assertSame($message3, $upcastingIterator->current()); - $upcastingIterator->rewind(); - $this->assertEquals(0, $upcastingIterator->key()); - $this->assertTrue($upcastingIterator->valid()); - $this->assertSame($upcastedMessage1, $upcastingIterator->current()); - $upcastingIterator->next(); - $this->assertEquals(1, $upcastingIterator->key()); - $this->assertSame($upcastedMessage2, $upcastingIterator->current()); - $upcastingIterator->next(); - $this->assertEquals(2, $upcastingIterator->key()); - $this->assertSame($message3, $upcastingIterator->current()); - $upcastingIterator->next(); - $this->assertFalse($upcastingIterator->valid()); - $this->assertNull($upcastingIterator->current()); - } - - /** - * @test - */ - public function it_iterates_on_iterator_with_removed_messages_only(): void - { - $message = $this->prophesize(Message::class); - $message->metadata()->willReturn(['foo' => 'baz'])->shouldBeCalled(); - $message = $message->reveal(); - - $iterator = new \ArrayIterator([$message]); - - $upcastingIterator = new UpcastingIterator($this->createUpcaster(), $iterator); - - $this->assertEquals(0, $upcastingIterator->key()); - $this->assertFalse($upcastingIterator->valid()); - $this->assertNull($upcastingIterator->current()); - } - - /** - * @test - */ - public function it_iterates_over_array_iterator(): void - { - $iterator = new \ArrayIterator(); - - $upcastingIterator = new UpcastingIterator($this->createUpcaster(), $iterator); - - $this->assertEquals(0, $upcastingIterator->key()); - $this->assertFalse($upcastingIterator->valid()); - $this->assertNull($upcastingIterator->current()); - } - - /** - * @test - */ - public function it_iterates_over_empty_iterator(): void - { - $iterator = new \EmptyIterator(); - - $upcastingIterator = new UpcastingIterator($this->createUpcaster(), $iterator); - - $this->assertFalse($upcastingIterator->valid()); - $this->assertNull($upcastingIterator->current()); - } - - protected function createUpcaster(): SingleEventUpcaster - { - return new class() extends SingleEventUpcaster { - protected function canUpcast(Message $message): bool - { - return $message->metadata() !== ['foo' => 'bar']; - } - - protected function doUpcast(Message $message): array - { - if ($message->metadata() === ['foo' => 'baz']) { - return []; - } - - return [$message->withAddedMetadata('key', 'value'), $message->withAddedMetadata('key', 'another_value')]; - } - }; - } -} diff --git a/tests/Util/ArrayCacheTest.php b/tests/Util/ArrayCacheTest.php deleted file mode 100644 index e77bfe2f..00000000 --- a/tests/Util/ArrayCacheTest.php +++ /dev/null @@ -1,82 +0,0 @@ - - * (c) 2015-2018 Sascha-Oliver Prolic - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ProophTest\EventStore\Util; - -use PHPUnit\Framework\TestCase; -use Prooph\EventStore\Util\ArrayCache; - -class ArrayCacheTest extends TestCase -{ - /** - * @test - */ - public function it_throws_exception_when_invalid_size_given(): void - { - $this->expectException(\InvalidArgumentException::class); - - new ArrayCache(-1); - } - - /** - * @test - */ - public function it_throws_exception_when_too_high_position_given(): void - { - $this->expectException(\InvalidArgumentException::class); - - $cache = new ArrayCache(100); - - $this->assertEquals(100, $cache->size()); - - $cache->get(101); - } - - /** - * @test - */ - public function it_throws_exception_when_too_low_position_given(): void - { - $this->expectException(\InvalidArgumentException::class); - - $cache = new ArrayCache(100); - - $cache->get(-1); - } - - /** - * @test - */ - public function it_gets_checks_for_values(): void - { - $cache = new ArrayCache(4); - - $this->assertNull($cache->get(0)); - - $cache->rollingAppend(1); - $cache->rollingAppend(2); - $cache->rollingAppend(3); - $cache->rollingAppend(4); - - $this->assertTrue($cache->has(4)); - $this->assertEquals(3, $cache->get(2)); - - $cache->rollingAppend(5); - $cache->rollingAppend(6); - $cache->rollingAppend(7); - - $this->assertTrue($cache->has(7)); - $this->assertEquals(7, $cache->get(2)); - $this->assertEquals(4, $cache->get(3)); - } -}