├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── appveyor.yml ├── composer.json ├── phpunit.xml.dist ├── spec └── Isolate │ └── UnitOfWork │ ├── Command │ ├── EditCommandSpec.php │ ├── NewCommandSpec.php │ └── RemoveCommandSpec.php │ ├── CommandBus │ └── SilentBusSpec.php │ ├── Entity │ ├── ChangeBuilderSpec.php │ ├── ClassNameSpec.php │ ├── ComparerSpec.php │ ├── Definition │ │ ├── IdentificationStrategy │ │ │ └── PropertyValueSpec.php │ │ ├── IdentitySpec.php │ │ ├── PropertySpec.php │ │ └── Repository │ │ │ └── InMemorySpec.php │ ├── DefinitionSpec.php │ ├── Identifier │ │ └── EntityIdentifierSpec.php │ ├── Property │ │ ├── PHPUnitComparator │ │ │ └── StrictScalarComparatorSpec.php │ │ └── PHPUnitValueComparerSpec.php │ └── Value │ │ ├── Change │ │ └── ScalarChangeSpec.php │ │ └── ChangeSetSpec.php │ ├── Object │ ├── InMemoryRegistrySpec.php │ ├── PropertyAccessorSpec.php │ └── PropertyClonerSpec.php │ └── UnitOfWorkSpec.php ├── src └── Isolate │ └── UnitOfWork │ ├── Command │ ├── Command.php │ ├── EditCommand.php │ ├── EditCommandHandler.php │ ├── NewCommand.php │ ├── NewCommandHandler.php │ ├── RemoveCommand.php │ └── RemoveCommandHandler.php │ ├── CommandBus.php │ ├── CommandBus │ └── SilentBus.php │ ├── Entity │ ├── ChangeBuilder.php │ ├── ClassName.php │ ├── Comparer.php │ ├── Definition.php │ ├── Definition │ │ ├── Association.php │ │ ├── IdentificationStrategy.php │ │ ├── IdentificationStrategy │ │ │ └── PropertyValue.php │ │ ├── Identity.php │ │ ├── Property.php │ │ ├── Repository.php │ │ └── Repository │ │ │ └── InMemory.php │ ├── Identifier.php │ ├── Identifier │ │ └── EntityIdentifier.php │ ├── Property │ │ ├── PHPUnitComparator │ │ │ ├── Factory.php │ │ │ └── StrictScalarComparator.php │ │ ├── PHPUnitValueComparer.php │ │ └── ValueComparer.php │ └── Value │ │ ├── Change.php │ │ ├── Change │ │ ├── AssociatedCollection.php │ │ ├── EditedEntity.php │ │ ├── NewEntity.php │ │ ├── RemovedEntity.php │ │ └── ScalarChange.php │ │ └── ChangeSet.php │ ├── EntityStates.php │ ├── Exception │ ├── Exception.php │ ├── InvalidArgumentException.php │ ├── NotExistingPropertyException.php │ └── RuntimeException.php │ ├── Factory.php │ ├── Object │ ├── InMemoryRegistry.php │ ├── PropertyAccessor.php │ ├── PropertyCloner.php │ ├── Registry.php │ ├── SnapshotMaker.php │ └── SnapshotMaker │ │ └── Adapter │ │ └── DeepCopy │ │ └── SnapshotMaker.php │ └── UnitOfWork.php └── tests ├── Isolate └── UnitOfWork │ └── Tests │ ├── Double │ ├── AssociatedEntityFake.php │ ├── EditCommandHandlerMock.php │ ├── EntityFake.php │ ├── EntityFakeChild.php │ ├── NewCommandHandlerMock.php │ ├── ProtectedEntity.php │ └── RemoveCommandHandlerMock.php │ └── UnitOfWorkTest.php └── bootstrap.php /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor/ 2 | /bin/ 3 | composer.lock 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | php: 4 | - 5.4 5 | - 5.5 6 | - 5.6 7 | 8 | env: 9 | - COMPOSER_OPTIONS='install --prefer-source' 10 | 11 | matrix: 12 | include: 13 | - php: 5.4 14 | env: COMPOSER_OPTIONS='update --prefer-lowest --prefer-source' 15 | 16 | before_install: 17 | - composer self-update 18 | 19 | before_script: 20 | - COMPOSER_ROOT_VERSION=dev-master composer $COMPOSER_OPTIONS 21 | 22 | script: 23 | - ./bin/phpspec run --format=dot 24 | - ./bin/phpunit 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 Norbert Orzechowicz 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is furnished 8 | to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Unit of Work 2 | 3 | [![Build Status](https://travis-ci.org/isolate-org/unit-of-work.svg?branch=master)](https://travis-ci.org/isolate-org/unit-of-work) 4 | [![Build status](https://ci.appveyor.com/api/projects/status/a65da0k2dxd8yt79?svg=true)](https://ci.appveyor.com/project/norzechowicz/unit-of-work) 5 | [![Latest Stable Version](https://poser.pugx.org/isolate/unit-of-work/v/stable.svg)](https://packagist.org/packages/isolate/unit-of-work) 6 | [![Total Downloads](https://poser.pugx.org/isolate/unit-of-work/downloads.svg)](https://packagist.org/packages/isolate/unit-of-work) 7 | [![Latest Unstable Version](https://poser.pugx.org/isolate/unit-of-work/v/unstable.svg)](https://packagist.org/packages/isolate/unit-of-work) 8 | [![License](https://poser.pugx.org/isolate/unit-of-work/license.svg)](https://packagist.org/packages/isolate/unit-of-work) 9 | [![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/isolate-org/unit-of-work/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/isolate-org/unit-of-work/?branch=master) 10 | 11 | ## Documentation 12 | 13 | To find out how to use Unit of Work follow [Documentation] 14 | 15 | ## Support 16 | 17 | If you have any problems or questions your can always contact us on Twitter, just tweet to [@isolate_php] 18 | 19 | ## License 20 | 21 | All Isolate components are released under [MIT license] 22 | 23 | [Documentation]: http://docs.isolate-project.org/en/latest/framework/unit-of-work/getting-started/index.html 24 | [@isolate_php]: https://twitter.com/isolate_php 25 | [MIT license]: LICENSE 26 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | build: false 2 | shallow_clone: true 3 | platform: x86 4 | clone_folder: c:\projects\isolate\unit-of-works 5 | 6 | init: 7 | - SET PATH=C:\Program Files\OpenSSL;c:\tools\php;%PATH% 8 | - SET COMPOSER_NO_INTERACTION=1 9 | 10 | install: 11 | - cinst -y OpenSSL.Light 12 | - cinst -y php 13 | - cd c:\tools\php 14 | - copy php.ini-production php.ini 15 | - echo date.timezone="UTC" >> php.ini 16 | - echo extension=php_curl.dll >> php.ini 17 | - echo extension_dir=ext >> php.ini 18 | - echo extension=php_openssl.dll >> php.ini 19 | - cd c:\projects\isolate\unit-of-works 20 | - appveyor DownloadFile https://getcomposer.org/composer.phar 21 | - php composer.phar install --prefer-dist --no-progress --ansi 22 | 23 | test_script: 24 | - cd c:\projects\isolate\unit-of-works 25 | - bin\phpunit.bat 26 | - bin\phpspec run --format=dot --ansi 27 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "isolate/unit-of-work", 3 | "description": "Unit of Work", 4 | "keywords": ["isolate", "unit of work", "uow", "unit", "work"], 5 | "require": { 6 | "php": ">=5.4.0", 7 | "myclabs/deep-copy": "^1.5", 8 | "sebastian/comparator": "~1.0" 9 | }, 10 | "require-dev": { 11 | "phpunit/phpunit": "^4.0", 12 | "phpspec/phpspec": "^2.1", 13 | "fzaninotto/faker": "1.4.*" 14 | }, 15 | "autoload": { 16 | "psr-0": { 17 | "Isolate\\UnitOfWork": "src/", 18 | "Isolate\\UnitOfWork\\Tests": "tests/" 19 | } 20 | }, 21 | "extra": { 22 | "branch-alias": { 23 | "dev-master": "1.0-dev" 24 | } 25 | }, 26 | "config": { 27 | "bin-dir": "bin" 28 | }, 29 | "license": "MIT", 30 | "authors": [ 31 | { 32 | "name": "Norbert Orzechowicz", 33 | "email": "norbert@orzechowicz.pl" 34 | }, 35 | { 36 | "name": "Dawid Sajdak", 37 | "email": "sajdak.dawid@gmail.com" 38 | } 39 | ] 40 | } 41 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 13 | 14 | 15 | ./tests/ 16 | 17 | 18 | 19 | 20 | ./src/Isolate/UnitOfWork/ 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /spec/Isolate/UnitOfWork/Command/EditCommandSpec.php: -------------------------------------------------------------------------------- 1 | beConstructedWith($entity, $changes); 20 | $this->getEntity()->shouldReturn($entity); 21 | $this->getChanges()->shouldReturn($changes); 22 | } 23 | 24 | function it_throws_exception_when_created_for_not_a_object_value() 25 | { 26 | $this->shouldThrow(new InvalidArgumentException("Edit command require object \"string\" type passed.")) 27 | ->during("__construct", ["this is string", new ChangeSet(), 1]); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /spec/Isolate/UnitOfWork/Command/NewCommandSpec.php: -------------------------------------------------------------------------------- 1 | beConstructedWith($entity); 16 | $this->getEntity()->shouldReturn($entity); 17 | } 18 | 19 | function it_throws_exception_when_created_for_not_a_object_value() 20 | { 21 | $this->shouldThrow(new InvalidArgumentException("New command require object \"string\" type passed.")) 22 | ->during("__construct", ["this is string", 1]); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /spec/Isolate/UnitOfWork/Command/RemoveCommandSpec.php: -------------------------------------------------------------------------------- 1 | beConstructedWith($entity); 16 | $this->getEntity()->shouldReturn($entity); 17 | } 18 | 19 | function it_throws_exception_when_created_for_not_a_object_value() 20 | { 21 | $this->shouldThrow(new InvalidArgumentException("Remove command require object \"string\" type passed.")) 22 | ->during("__construct", ["this is string", 1]); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /spec/Isolate/UnitOfWork/CommandBus/SilentBusSpec.php: -------------------------------------------------------------------------------- 1 | getDefinition(Argument::type(EntityFake::getClassName())) 23 | ->willReturn($this->createEntityDefinition()); 24 | 25 | $this->beConstructedWith($definitions); 26 | } 27 | 28 | function it_is_command_bus() 29 | { 30 | $this->shouldImplement('Isolate\UnitOfWork\CommandBus'); 31 | } 32 | 33 | function it_dispatch_handler_for_new_command_if_exists(Definition\Repository $definitions, NewCommandHandler $handler) 34 | { 35 | $definition = $this->createEntityDefinition(); 36 | $definition->setNewCommandHandler($handler->getWrappedObject()); 37 | $entity = new EntityFake(); 38 | $command = new NewCommand($entity); 39 | $definitions->getDefinition($entity)->willReturn($definition); 40 | 41 | $handler->handle($command)->willReturn(true); 42 | 43 | $this->dispatch($command)->shouldReturn(true); 44 | } 45 | 46 | function it_dispatch_handler_for_edit_command_if_exists(Definition\Repository $definitions, EditCommandHandler $handler) 47 | { 48 | $definition = $this->createEntityDefinition(); 49 | $definition->setEditCommandHandler($handler->getWrappedObject()); 50 | $entity = new EntityFake(1); 51 | $command = new EditCommand($entity, new ChangeSet()); 52 | $definitions->getDefinition($entity)->willReturn($definition); 53 | 54 | $handler->handle($command)->willReturn(true); 55 | 56 | $this->dispatch($command)->shouldReturn(true); 57 | } 58 | 59 | function it_dispatch_handler_for_remove_command_if_exists(Definition\Repository $definitions, RemoveCommandHandler $handler) 60 | { 61 | $definition = $this->createEntityDefinition(); 62 | $definition->setRemoveCommandHandler($handler->getWrappedObject()); 63 | $entity = new EntityFake(1); 64 | $command = new RemoveCommand($entity); 65 | $definitions->getDefinition($entity)->willReturn($definition); 66 | 67 | $handler->handle($command)->willReturn(true); 68 | 69 | $this->dispatch($command)->shouldReturn(true); 70 | } 71 | 72 | function it_returns_null_when_there_is_no_handler_for_command_in_entity_definition() 73 | { 74 | $entity = new EntityFake(1); 75 | $command = new NewCommand($entity); 76 | $this->dispatch($command)->shouldReturn(null); 77 | } 78 | 79 | /** 80 | * @return Definition 81 | */ 82 | private function createEntityDefinition() 83 | { 84 | $definition = new Definition( 85 | new ClassName(EntityFake::getClassName()), 86 | new Definition\Identity("id") 87 | ); 88 | $definition->addToObserved(new Definition\Property("firstName")); 89 | $definition->addToObserved(new Definition\Property("lastName")); 90 | 91 | return $definition; 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /spec/Isolate/UnitOfWork/Entity/ChangeBuilderSpec.php: -------------------------------------------------------------------------------- 1 | getDefinition(Argument::type(EntityFake::getClassName())) 19 | ->willReturn($this->createEntityDefinition()); 20 | $definitions->getDefinition(Argument::type(AssociatedEntityFake::getClassName())) 21 | ->willReturn($this->createAssociatedEntityDefinition()); 22 | 23 | $identifier->isPersisted(Argument::any())->will(function ($args) { 24 | return !is_null($args[0]->getId()); 25 | }); 26 | 27 | $identifier->getIdentity(Argument::any())->will(function ($args) { 28 | return $args[0]->getId(); 29 | }); 30 | 31 | $this->beConstructedWith($definitions, $identifier); 32 | } 33 | 34 | function it_build_change_for_different_objects() 35 | { 36 | $sourceObject = new EntityFake(1, "Norbert"); 37 | $editedObject = clone($sourceObject); 38 | $editedObject->changeFirstName("Michal"); 39 | 40 | $changeSet = $this->buildChanges($sourceObject, $editedObject); 41 | 42 | $changeSet->hasChangeFor('firstName')->shouldReturn(true); 43 | $changeSet->getChangeFor('firstName')->getOriginValue()->shouldReturn("Norbert"); 44 | $changeSet->getChangeFor('firstName')->shouldBeAnInstanceOf("Isolate\\UnitOfWork\\Entity\\Value\\Change\\ScalarChange"); 45 | $changeSet->shouldBeAnInstanceOf("Isolate\\UnitOfWork\\Entity\\Value\\ChangeSet"); 46 | } 47 | 48 | function it_throws_exception_when_new_entity_in_associated_property_does_not_match_target_class() 49 | { 50 | $sourceObject = new AssociatedEntityFake(1); 51 | $editedObject = clone($sourceObject); 52 | $editedObject->setParent(new EntityFake()); 53 | 54 | $this->shouldThrow(new RuntimeException( 55 | sprintf("Property \"parent\" expects instanceof \"%s\" as a value.", AssociatedEntityFake::getClassName()) 56 | ))->during("buildChanges", [$sourceObject, $editedObject]); 57 | } 58 | 59 | function it_build_single_association_change_for_new_entity() 60 | { 61 | $parent = new AssociatedEntityFake(100); 62 | 63 | $sourceObject = new AssociatedEntityFake(1); 64 | $editedObject = clone($sourceObject); 65 | $editedObject->setParent($parent); 66 | 67 | $changeSet = $this->buildChanges($sourceObject, $editedObject); 68 | 69 | $changeSet->hasChangeFor("parent")->shouldReturn(true); 70 | $changeSet->getChangeFor("parent")->getNewValue()->shouldReturn($parent); 71 | $changeSet->getChangeFor("parent")->isPersisted()->shouldReturn(true); 72 | $changeSet->getChangeFor("parent")->shouldBeAnInstanceOf("Isolate\\UnitOfWork\\Entity\\Value\\Change\\NewEntity"); 73 | } 74 | 75 | function it_build_change_when_single_associated_entity_was_removed() 76 | { 77 | $parent = new AssociatedEntityFake(100); 78 | 79 | $sourceObject = new AssociatedEntityFake(1, null, $parent); 80 | $editedObject = clone($sourceObject); 81 | $editedObject->removeParent(); 82 | 83 | $changeSet = $this->buildChanges($sourceObject, $editedObject); 84 | 85 | $changeSet->hasChangeFor("parent")->shouldReturn(true); 86 | $changeSet->getChangeFor("parent")->getOriginValue()->shouldReturn($parent); 87 | $changeSet->getChangeFor("parent")->shouldBeAnInstanceOf("Isolate\\UnitOfWork\\Entity\\Value\\Change\\RemovedEntity"); 88 | } 89 | 90 | function it_build_single_association_change_for_edited_entity() 91 | { 92 | $sourceObject = new AssociatedEntityFake(1, null, new AssociatedEntityFake(100, "Norbert")); 93 | $editedObject = new AssociatedEntityFake(1, null, new AssociatedEntityFake(100, "Norbert")); 94 | $editedObject->getParent()->setName("Dawid"); 95 | 96 | $changeSet = $this->buildChanges($sourceObject, $editedObject); 97 | 98 | $changeSet->hasChangeFor("parent")->shouldReturn(true); 99 | $changeSet->getChangeFor("parent")->shouldBeAnInstanceOf("Isolate\\UnitOfWork\\Entity\\Value\\Change\\EditedEntity"); 100 | $changeSet->getChangeFor("parent")->getChangeSet()->getChangeFor("name")->shouldBeAnInstanceOf( 101 | "Isolate\\UnitOfWork\\Entity\\Value\\Change\\ScalarChange" 102 | ); 103 | $changeSet->getChangeFor("parent")->getChangeSet()->getChangeFor("name")->getOriginValue()->shouldReturn("Norbert"); 104 | $changeSet->getChangeFor("parent")->getChangeSet()->getChangeFor("name")->getNewValue()->shouldReturn("Dawid"); 105 | } 106 | 107 | function it_throws_exception_when_new_associated_collection_is_not_valid_array() 108 | { 109 | $sourceObject = new AssociatedEntityFake(1, null, null); 110 | $editedObject = new AssociatedEntityFake(1, null, null, "test"); 111 | 112 | $this->shouldThrow( 113 | new RuntimeException("Property \"children\" is marked as associated with many entities and require new value to be traversable collection.") 114 | )->during("buildChanges", [$sourceObject, $editedObject]); 115 | } 116 | 117 | function it_build_change_for_many_entities_association_that_knows_which_entities_were_added() 118 | { 119 | $sourceObject = new AssociatedEntityFake(1, null, null, []); 120 | $editedObject = new AssociatedEntityFake(1, null, null, [new AssociatedEntityFake()]); 121 | 122 | $changeSet = $this->buildChanges($sourceObject, $editedObject); 123 | 124 | $changeSet->hasChangeFor("children")->shouldReturn(true); 125 | $change = $changeSet->getChangeFor("children"); 126 | $change->shouldBeAnInstanceOf("Isolate\\UnitOfWork\\Entity\\Value\\Change\\AssociatedCollection"); 127 | $change->getChangeForNewEntities()->shouldHaveCount(1); 128 | $change->getChangesForRemovedEntities()->shouldHaveCount(0); 129 | $change->getChangesForEditedEntities()->shouldHaveCount(0); 130 | } 131 | 132 | function it_build_change_for_many_entities_association_that_knows_which_entities_were_added_even_persisted() 133 | { 134 | $sourceObject = new AssociatedEntityFake(1, null, null, []); 135 | $editedObject = new AssociatedEntityFake(1, null, null, [new AssociatedEntityFake(100)]); 136 | 137 | $changeSet = $this->buildChanges($sourceObject, $editedObject); 138 | 139 | $changeSet->hasChangeFor("children")->shouldReturn(true); 140 | $change = $changeSet->getChangeFor("children"); 141 | $change->shouldBeAnInstanceOf("Isolate\\UnitOfWork\\Entity\\Value\\Change\\AssociatedCollection"); 142 | $change->getChangeForNewEntities()->shouldHaveCount(1); 143 | $change->getChangesForRemovedEntities()->shouldHaveCount(0); 144 | $change->getChangesForEditedEntities()->shouldHaveCount(0); 145 | } 146 | 147 | function it_build_change_for_many_entities_association_that_knows_which_entities_were_removed() 148 | { 149 | $sourceObject = new AssociatedEntityFake(1, null, null, [new AssociatedEntityFake(100)]); 150 | $editedObject = new AssociatedEntityFake(1, null, null, []); 151 | 152 | $changeSet = $this->buildChanges($sourceObject, $editedObject); 153 | 154 | $changeSet->hasChangeFor("children")->shouldReturn(true); 155 | $change = $changeSet->getChangeFor("children"); 156 | $change->shouldBeAnInstanceOf("Isolate\\UnitOfWork\\Entity\\Value\\Change\\AssociatedCollection"); 157 | $change->getChangeForNewEntities()->shouldHaveCount(0); 158 | $change->getChangesForRemovedEntities()->shouldHaveCount(1); 159 | $change->getChangesForEditedEntities()->shouldHaveCount(0); 160 | } 161 | 162 | function it_build_change_for_many_entities_association_that_knows_which_entities_were_edited() 163 | { 164 | $sourceObject = new AssociatedEntityFake(1, null, null, [new AssociatedEntityFake(100)]); 165 | $editedObject = new AssociatedEntityFake(1, null, null, [new AssociatedEntityFake(100, "Norbert")]); 166 | 167 | $changeSet = $this->buildChanges($sourceObject, $editedObject); 168 | 169 | $changeSet->hasChangeFor("children")->shouldReturn(true); 170 | $change = $changeSet->getChangeFor("children"); 171 | $change->shouldBeAnInstanceOf("Isolate\\UnitOfWork\\Entity\\Value\\Change\\AssociatedCollection"); 172 | $change->getChangeForNewEntities()->shouldHaveCount(0); 173 | $change->getChangesForRemovedEntities()->shouldHaveCount(0); 174 | $change->getChangesForEditedEntities()->shouldHaveCount(1); 175 | } 176 | 177 | function it_build_change_for_many_entities_association_that_has_new_edited_and_removed_elements() 178 | { 179 | $sourceObject = new AssociatedEntityFake(1, null, null, [ 180 | new AssociatedEntityFake(100), 181 | new AssociatedEntityFake(101), 182 | ]); 183 | $editedObject = new AssociatedEntityFake(1, null, null, [ 184 | new AssociatedEntityFake(101, "Norbert"), 185 | new AssociatedEntityFake() 186 | ]); 187 | 188 | $changeSet = $this->buildChanges($sourceObject, $editedObject); 189 | 190 | $changeSet->hasChangeFor("children")->shouldReturn(true); 191 | $change = $changeSet->getChangeFor("children"); 192 | $change->shouldBeAnInstanceOf("Isolate\\UnitOfWork\\Entity\\Value\\Change\\AssociatedCollection"); 193 | $change->getChangeForNewEntities()->shouldHaveCount(1); 194 | $change->getChangesForRemovedEntities()->shouldHaveCount(1); 195 | $change->getChangeForNewEntities()->shouldHaveCount(1); 196 | } 197 | 198 | /** 199 | * @return Definition 200 | */ 201 | private function createEntityDefinition() 202 | { 203 | $definition = new Definition( 204 | new ClassName(EntityFake::getClassName()), 205 | new Definition\Identity("id") 206 | ); 207 | $definition->addToObserved(new Definition\Property("firstName")); 208 | 209 | return $definition; 210 | } 211 | 212 | /** 213 | * @return Definition 214 | */ 215 | private function createAssociatedEntityDefinition() 216 | { 217 | $definition = new Definition( 218 | new ClassName(AssociatedEntityFake::getClassName()), 219 | new Definition\Identity("id") 220 | ); 221 | $parentAssociation = new Definition\Association( 222 | new ClassName(AssociatedEntityFake::getClassName()), 223 | Definition\Association::TO_SINGLE_ENTITY 224 | ); 225 | 226 | $definition->setObserved([ 227 | new Definition\Property("parent", $parentAssociation), 228 | new Definition\Property("children", $this->createChildrenAssociation()), 229 | new Definition\Property("name") 230 | ]); 231 | 232 | return $definition; 233 | } 234 | 235 | /** 236 | * @return Definition\Association 237 | */ 238 | private function createChildrenAssociation() 239 | { 240 | return new Definition\Association( 241 | new ClassName(AssociatedEntityFake::getClassName()), 242 | Definition\Association::TO_MANY_ENTITIES 243 | ); 244 | } 245 | } 246 | 247 | -------------------------------------------------------------------------------- /spec/Isolate/UnitOfWork/Entity/ClassNameSpec.php: -------------------------------------------------------------------------------- 1 | shouldThrow(new InvalidArgumentException("Class name must be a valid string.")) 14 | ->during("__construct", [new \stdClass()]); 15 | } 16 | 17 | function it_throws_exception_if_class_not_exists() 18 | { 19 | $this->shouldThrow(new InvalidArgumentException("Class \"Coduo\" does not exists.")) 20 | ->during("__construct", ["Coduo"]); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /spec/Isolate/UnitOfWork/Entity/ComparerSpec.php: -------------------------------------------------------------------------------- 1 | getDefinition(Argument::type(EntityFake::getClassName())) 16 | ->willReturn($this->createEntityDefinition()); 17 | 18 | $this->beConstructedWith($definitions); 19 | } 20 | 21 | function it_returns_true_when_values_in_observed_properties_are_equal() 22 | { 23 | $firstObject = new EntityFake(1, "Norbert", "Orzechowicz"); 24 | $secondObject = clone $firstObject; 25 | 26 | $this->areEqual($firstObject, $secondObject)->shouldReturn(true); 27 | } 28 | 29 | function it_returns_false_when_values_in_at_least_one_observed_property_is_different() 30 | { 31 | $firstObject = new EntityFake(1, "Norbert", "Orzechowicz"); 32 | $secondObject = clone $firstObject; 33 | $secondObject->changeLastName('Sajdak'); 34 | 35 | $this->areEqual($firstObject, $secondObject)->shouldReturn(false); 36 | } 37 | 38 | function it_ignores_not_observed_properties() 39 | { 40 | $firstObject = new EntityFake(1, "Norbert", "Orzechowicz"); 41 | $secondObject = clone $firstObject; 42 | $secondObject->setItems(["Foo", "Bar"]); 43 | 44 | $this->areEqual($firstObject, $secondObject)->shouldReturn(true); 45 | } 46 | 47 | /** 48 | * @return Definition 49 | */ 50 | private function createEntityDefinition() 51 | { 52 | $definition = new Definition( 53 | new ClassName(EntityFake::getClassName()), 54 | new Definition\Identity("id") 55 | ); 56 | $definition->addToObserved(new Definition\Property("firstName")); 57 | $definition->addToObserved(new Definition\Property("lastName")); 58 | 59 | return $definition; 60 | } 61 | } 62 | 63 | -------------------------------------------------------------------------------- /spec/Isolate/UnitOfWork/Entity/Definition/IdentificationStrategy/PropertyValueSpec.php: -------------------------------------------------------------------------------- 1 | beConstructedWith(new Identity("id")); 16 | } 17 | 18 | function it_is_identification_strategy() 19 | { 20 | $this->shouldHaveType('Isolate\UnitOfWork\Entity\Definition\IdentificationStrategy'); 21 | } 22 | 23 | function it_identify_entities_with_not_empty_value_in_identity_property() 24 | { 25 | $this->isIdentified(new EntityFake(1))->shouldReturn(true); 26 | } 27 | 28 | function it_identify_entities_with_properties_that_contains_0_as_a_value() 29 | { 30 | $this->isIdentified(new EntityFake(0))->shouldReturn(true); 31 | } 32 | 33 | function it_gets_identity_from_entity() 34 | { 35 | $this->getIdentity(new EntityFake(1))->shouldReturn(1); 36 | } 37 | 38 | function it_throws_exception_when_entity_does_not_have_identity() 39 | { 40 | $this->shouldThrow( 41 | new RuntimeException("Can't get identity from not identified entity.") 42 | )->during("getIdentity", [new EntityFake()]); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /spec/Isolate/UnitOfWork/Entity/Definition/IdentitySpec.php: -------------------------------------------------------------------------------- 1 | shouldThrow(new InvalidArgumentException("Property name must be a valid string.")) 14 | ->during("__construct", [null]); 15 | } 16 | 17 | function it_throw_exception_if_created_with_empty_property_path() 18 | { 19 | $this->shouldThrow(new InvalidArgumentException("Property name can't be empty.")) 20 | ->during("__construct", [""]); 21 | } 22 | 23 | function it_returns_property_path() 24 | { 25 | $this->beConstructedWith("id"); 26 | $this->getPropertyName()->shouldReturn("id"); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /spec/Isolate/UnitOfWork/Entity/Definition/PropertySpec.php: -------------------------------------------------------------------------------- 1 | shouldThrow(new InvalidArgumentException("Property name can't be empty.")) 14 | ->during("__construct", [""]); 15 | 16 | $this->shouldThrow(new InvalidArgumentException("Property name must be a valid string.")) 17 | ->during("__construct", [new \stdClass()]); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /spec/Isolate/UnitOfWork/Entity/Definition/Repository/InMemorySpec.php: -------------------------------------------------------------------------------- 1 | shouldImplement('Isolate\UnitOfWork\Entity\Definition\Repository'); 19 | } 20 | 21 | function it_throws_exception_when_crated_with_non_traversable_collection() 22 | { 23 | $this->shouldThrow( 24 | new InvalidArgumentException("Entity definition repository require array od traversable collection of entity definitions.") 25 | )->during("__construct", [new \stdClass()]); 26 | } 27 | 28 | function it_throws_exception_when_crated_with_non_entity_definition_in_collection() 29 | { 30 | $this->shouldThrow( 31 | new InvalidArgumentException("Each element in collection needs to be an instance of Isolate\\UnitOfWork\\Entity\\Definition") 32 | )->during("__construct", [[new \stdClass()]]); 33 | } 34 | 35 | function it_throws_exception_when_checking_if_definition_exists_for_non_object() 36 | { 37 | $this->shouldThrow( 38 | new InvalidArgumentException("Entity definition repository require objects as arguments for methods.") 39 | )->during("hasDefinition", ["string"]); 40 | } 41 | 42 | function it_knows_when_entity_definition_exists() 43 | { 44 | $definition = new Definition( 45 | new ClassName(EntityFake::getClassName()), 46 | new Definition\Identity("id") 47 | ); 48 | 49 | $this->beConstructedWith([$definition]); 50 | 51 | $entity = new EntityFake(); 52 | 53 | $this->hasDefinition($entity)->shouldReturn(true); 54 | } 55 | 56 | function it_throws_exception_when_there_is_no_definition_for_entity() 57 | { 58 | $entity = new EntityFake(); 59 | 60 | $this->shouldThrow( 61 | new RuntimeException(sprintf("Entity definition for \"%s\" does not exists.", EntityFake::getClassName())) 62 | )->during("getDefinition", [$entity]); 63 | } 64 | 65 | function it_returns_definition_for_entity() 66 | { 67 | $definition = new Definition( 68 | new ClassName(EntityFake::getClassName()), 69 | new Definition\Identity("id") 70 | ); 71 | $this->beConstructedWith([$definition]); 72 | 73 | $entity = new EntityFake(); 74 | 75 | $this->getDefinition($entity)->shouldReturn($definition); 76 | } 77 | 78 | function it_throws_exception_when_associated_entity_is_not_defied() 79 | { 80 | $definition = new Definition( 81 | new ClassName(EntityFake::getClassName()), 82 | new Definition\Identity("not_exists") 83 | ); 84 | $association = new Definition\Association(new ClassName(EntityFakeChild::getClassName()), Definition\Association::TO_MANY_ENTITIES); 85 | $definition->addToObserved(new Definition\Property("children", $association)); 86 | 87 | $this->shouldThrow( 88 | new InvalidArgumentException("Entity class \"Isolate\\UnitOfWork\\Tests\\Double\\EntityFakeChild\" used in association of \"Isolate\\UnitOfWork\\Tests\\Double\\EntityFake\" entity does not have definition.") 89 | )->during("__construct", [[$definition]]); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /spec/Isolate/UnitOfWork/Entity/DefinitionSpec.php: -------------------------------------------------------------------------------- 1 | beConstructedWith($className, new Identity("id")); 22 | 23 | $this->shouldThrow(new InvalidArgumentException("Id definition property path can't be between observer properties.")) 24 | ->during("addToObserved", [new Property("id")]); 25 | } 26 | 27 | function it_force_observed_properties_to_be_unique() 28 | { 29 | $className = new ClassName(EntityFake::getClassName()); 30 | $this->beConstructedWith($className, new Identity("id")); 31 | 32 | $this->setObserved([new Property("firstName"), new Property("firstName")]); 33 | 34 | $this->getObservedProperties()->shouldHaveCount(1); 35 | 36 | $this->setObserved([]); 37 | $this->addToObserved(new Property("firstName")); 38 | $this->addToObserved(new Property("firstName")); 39 | $this->getObservedProperties()->shouldHaveCount(1); 40 | } 41 | 42 | function it_returns_class_name() 43 | { 44 | $className = new ClassName(EntityFake::getClassName()); 45 | $this->beConstructedWith($className, new Identity("id")); 46 | $this->getClassName()->shouldReturn($className); 47 | } 48 | 49 | function it_returns_id_definition() 50 | { 51 | $idDefinition = new Identity("id"); 52 | $this->beConstructedWith(new ClassName(EntityFake::getClassName()), $idDefinition); 53 | $this->getIdDefinition()->shouldReturn($idDefinition); 54 | } 55 | 56 | function it_fits_for_entity_that_is_an_instance_of_class() 57 | { 58 | $this->beConstructedWith(new ClassName(EntityFake::getClassName()), new Identity("id")); 59 | $this->fitsFor(new EntityFake())->shouldReturn(true); 60 | } 61 | 62 | function it_can_have_new_command_handler(NewCommandHandler $commandHandler) 63 | { 64 | $this->beConstructedWith(new ClassName(EntityFake::getClassName()), new Identity("id")); 65 | $this->setNewCommandHandler($commandHandler); 66 | $this->hasNewCommandHandler()->shouldReturn(true); 67 | } 68 | 69 | function it_can_have_edit_command_handler(EditCommandHandler $commandHandler) 70 | { 71 | $this->beConstructedWith(new ClassName(EntityFake::getClassName()), new Identity("id")); 72 | $this->setEditCommandHandler($commandHandler); 73 | $this->hasEditCommandHandler()->shouldReturn(true); 74 | } 75 | 76 | function it_can_have_remove_command_handler(RemoveCommandHandler $commandHandler) 77 | { 78 | $this->beConstructedWith(new ClassName(EntityFake::getClassName()), new Identity("id")); 79 | $this->setRemoveCommandHandler($commandHandler); 80 | $this->hasRemoveCommandHandler()->shouldReturn(true); 81 | } 82 | 83 | function it_has_default_identification_strategy() 84 | { 85 | $this->beConstructedWith(new ClassName(EntityFake::getClassName()), new Identity("id")); 86 | $this->getIdentityStrategy()->shouldReturnAnInstanceOf( 87 | 'Isolate\UnitOfWork\Entity\Definition\IdentificationStrategy' 88 | ); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /spec/Isolate/UnitOfWork/Entity/Identifier/EntityIdentifierSpec.php: -------------------------------------------------------------------------------- 1 | hasDefinition(Argument::type('stdClass'))->willReturn(false); 18 | $this->beConstructedWith($definitions); 19 | } 20 | 21 | function it_is_identifier() 22 | { 23 | $this->shouldImplement("Isolate\\UnitOfWork\\Entity\\Identifier"); 24 | } 25 | 26 | function it_throw_exception_during_verification_of_non_defined_entity() 27 | { 28 | $this->shouldThrow(new RuntimeException("Class \"stdClass\" does not have definition.")) 29 | ->during("isPersisted", [new \stdClass()]); 30 | } 31 | 32 | function it_throw_exception_during_identification_of_non_defined_entity() 33 | { 34 | $this->shouldThrow(new RuntimeException("Class \"stdClass\" does not have definition.")) 35 | ->during("getIdentity", [new \stdClass()]); 36 | } 37 | 38 | function it_use_entity_definition_to_tells_if_entity_was_persisted(Definition\Repository $definitions, Definition\IdentificationStrategy $identificationStrategy) 39 | { 40 | $entity = new EntityFake(1); 41 | $identificationStrategy->isIdentified($entity)->willReturn(true); 42 | $definitions->hasDefinition($entity)->willReturn(true); 43 | $definitions->getDefinition($entity)->willReturn( 44 | new Definition(new ClassName(EntityFake::getClassName()),new Definition\Identity("id"), $identificationStrategy->getWrappedObject()) 45 | ); 46 | 47 | $this->isPersisted($entity)->shouldReturn(true); 48 | } 49 | 50 | function it_throws_exception_during_persist_check_when_property_does_not_exists(Definition\Repository $definitions) 51 | { 52 | $entity = new EntityFake(); 53 | $definitions->hasDefinition($entity)->willReturn(true); 54 | $definitions->getDefinition($entity)->willReturn( 55 | new Definition(new ClassName(EntityFake::getClassName()),new Definition\Identity("not_exists")) 56 | ); 57 | 58 | $this->shouldThrow( 59 | new NotExistingPropertyException("Property \"not_exists\" does not exists in \"Isolate\\UnitOfWork\\Tests\\Double\\EntityFake\" class.") 60 | )->during("isPersisted", [$entity]); 61 | } 62 | 63 | function it_gets_identity_from_entity(Definition\Repository $definitions, Definition\IdentificationStrategy $identificationStrategy) 64 | { 65 | $entity = new EntityFake(1); 66 | $identificationStrategy->getIdentity($entity)->willReturn(1); 67 | $definitions->hasDefinition($entity)->willReturn(true); 68 | $definitions->getDefinition($entity)->willReturn( 69 | new Definition(new ClassName(EntityFake::getClassName()),new Definition\Identity("id"), $identificationStrategy->getWrappedObject()) 70 | ); 71 | 72 | $this->getIdentity($entity)->shouldReturn(1); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /spec/Isolate/UnitOfWork/Entity/Property/PHPUnitComparator/StrictScalarComparatorSpec.php: -------------------------------------------------------------------------------- 1 | shouldThrow($exception)->during( 14 | "assertEquals", [0, null] 15 | ); 16 | } 17 | } -------------------------------------------------------------------------------- /spec/Isolate/UnitOfWork/Entity/Property/PHPUnitValueComparerSpec.php: -------------------------------------------------------------------------------- 1 | hasDifferentValue(new Property("firstName"), $firstObject, $secondObject)->shouldReturn(false); 20 | } 21 | 22 | function it_returns_true_when_there_is_any_difference_between_same_property_in_two_objects() 23 | { 24 | $firstObject = new EntityFake(1, "Norbert"); 25 | $secondObject = new EntityFake(1, "Michal"); 26 | 27 | $this->hasDifferentValue(new Property("firstName"), $firstObject, $secondObject)->shouldReturn(true); 28 | } 29 | 30 | function it_returns_true_when_values_are_different_in_property_that_holds_array() 31 | { 32 | $firstObject = new EntityFake(1); 33 | $firstObject->setItems([new EntityFake(5), new EntityFake(6)]); 34 | 35 | $secondObject = new EntityFake(1); 36 | $secondObject->setItems([new EntityFake(5), new EntityFake(7)]); 37 | 38 | $this->hasDifferentValue(new Property("items"), $firstObject, $secondObject)->shouldReturn(true); 39 | } 40 | 41 | function it_throws_exception_when_at_least_one_of_compared_values_is_not_an_object() 42 | { 43 | $this->shouldThrow(new InvalidArgumentException("Compared values need to be a valid objects.")) 44 | ->during("hasDifferentValue", [new Property("firstName"), "fakeEntity", new EntityFake()]); 45 | 46 | $this->shouldThrow(new InvalidArgumentException("Compared values need to be a valid objects.")) 47 | ->during("hasDifferentValue", [new Property("firstName"), new EntityFake(), "fakeEntity"]); 48 | } 49 | 50 | function it_throws_exception_when_compared_objects_have_different_classes() 51 | { 52 | $this->shouldThrow(new InvalidArgumentException("Compared values need to be an instances of the same class.")) 53 | ->during("hasDifferentValue", [new Property("firstName"), new ProtectedEntity(), new EntityFake()]); 54 | } 55 | 56 | function it_throws_exception_when_property_does_not_exists() 57 | { 58 | $firstObject = $secondObject = new EntityFake(1, "Norbert", "Orzechowicz"); 59 | 60 | $this->shouldThrow(new NotExistingPropertyException("Property \"title\" does not exists in \"Isolate\\UnitOfWork\\Tests\\Double\\EntityFake\" class.")) 61 | ->during("hasDifferentValue", [new Property("title"), $firstObject, $secondObject]); 62 | } 63 | } 64 | 65 | -------------------------------------------------------------------------------- /spec/Isolate/UnitOfWork/Entity/Value/Change/ScalarChangeSpec.php: -------------------------------------------------------------------------------- 1 | beConstructedWith(new Property('property'), "Foo", "Bar"); 14 | } 15 | 16 | function it_have_new_value_old_value_and_property_name() 17 | { 18 | $property = new Property('property'); 19 | $this->beConstructedWith($property, "Foo", "Bar"); 20 | $this->getOriginValue()->shouldReturn("Foo"); 21 | $this->getNewValue()->shouldReturn("Bar"); 22 | $this->getProperty()->shouldReturn($property); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /spec/Isolate/UnitOfWork/Entity/Value/ChangeSetSpec.php: -------------------------------------------------------------------------------- 1 | shouldBeAnInstanceOf('ArrayObject'); 16 | } 17 | 18 | function it_has_information_about_changes_for_specific_property_name() 19 | { 20 | $change = new ScalarChange(new Property("firstName"), "Michal", "Norbert"); 21 | $this->beConstructedWith([$change]); 22 | 23 | $this->hasChangeFor("firstName")->shouldReturn(true); 24 | $this->getChangeFor("firstName")->shouldReturn($change); 25 | } 26 | 27 | function it_throws_exception_when_there_are_no_changes_for_property() 28 | { 29 | $this->beConstructedWith([]); 30 | 31 | $this->shouldThrow(new RuntimeException("There are no changes for \"firstName\" property.")) 32 | ->during('getChangeFor', ["firstName"]); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /spec/Isolate/UnitOfWork/Object/InMemoryRegistrySpec.php: -------------------------------------------------------------------------------- 1 | makeSnapshotOf(Argument::type('object'))->will(function ($args) { 16 | $object = $args[0]; 17 | return clone $object; 18 | }); 19 | 20 | $this->beConstructedWith($cloner, $recoveryPoint); 21 | } 22 | 23 | function it_knows_when_object_was_not_registered() 24 | { 25 | $this->isRegistered(new EntityFake())->shouldReturn(false); 26 | } 27 | 28 | function it_knows_when_object_was_registered() 29 | { 30 | $object = new EntityFake(); 31 | $this->register($object); 32 | $this->isRegistered($object)->shouldReturn(true); 33 | } 34 | 35 | function it_contains_snapshots_of_registered_objects() 36 | { 37 | $object = new EntityFake(1, "Norbert", "Orzechowicz"); 38 | $this->register($object); 39 | $object->changeFirstName("Dawid"); 40 | $object->changeLastName("Sajdak"); 41 | 42 | $snapshot = $this->getSnapshot($object); 43 | $snapshot->getFirstName()->shouldReturn("Norbert"); 44 | $snapshot->getLastName()->shouldReturn("Orzechowicz"); 45 | } 46 | 47 | function it_make_new_snapshots_of_registered_objects() 48 | { 49 | $object = new EntityFake(1, "Norbert", "Orzechowicz"); 50 | $this->register($object); 51 | $object->changeFirstName("Dawid"); 52 | $object->changeLastName("Sajdak"); 53 | 54 | $this->makeNewSnapshots(); 55 | 56 | $snapshot = $this->getSnapshot($object); 57 | $snapshot->getFirstName()->shouldReturn("Dawid"); 58 | $snapshot->getLastName()->shouldReturn("Sajdak"); 59 | } 60 | 61 | function it_knows_when_object_should_not_be_removed() 62 | { 63 | $object = new EntityFake(1, "Norbert", "Orzechowicz"); 64 | $this->isRemoved($object)->shouldReturn(false); 65 | } 66 | 67 | function it_knows_when_object_should_be_removed() 68 | { 69 | $entity = new EntityFake(1, "Norbert", "Orzechowicz"); 70 | $this->remove($entity); 71 | 72 | $this->isRemoved($entity)->shouldReturn(true); 73 | } 74 | 75 | function it_automatically_register_objects_that_should_be_removed() 76 | { 77 | $object = new EntityFake(1, "Norbert", "Orzechowicz"); 78 | 79 | $this->isRegistered($object)->shouldReturn(false); 80 | $this->remove($object); 81 | 82 | $this->isRegistered($object)->shouldReturn(true); 83 | $this->isRemoved($object)->shouldReturn(true); 84 | } 85 | 86 | function it_cleans_removed_objects() 87 | { 88 | $object = new EntityFake(1, "Norbert", "Orzechowicz"); 89 | $this->remove($object); 90 | 91 | $this->cleanRemoved(); 92 | 93 | $this->isRegistered($object)->shouldReturn(false); 94 | $this->isRemoved($object)->shouldReturn(false); 95 | } 96 | 97 | function it_returns_all_objects_as_array() 98 | { 99 | $object = new EntityFake(1, "Norbert", "Orzechowicz"); 100 | $this->register($object); 101 | 102 | $this->all()->shouldReturn([ 103 | $object 104 | ]); 105 | } 106 | 107 | function it_resets_objects_to_states_from_snapshots(SnapshotMaker $cloner, PropertyCloner $recoveryPoint) 108 | { 109 | $object = new EntityFake(1, "Norbert", "Orzechowicz"); 110 | $objectSnapshot = new EntityFake(1, "Norbert", "Orzechowicz"); 111 | $cloner->makeSnapshotOf($object)->willReturn($objectSnapshot); 112 | 113 | $this->register($object); 114 | $object->changeFirstName("Dawid"); 115 | $object->changeLastName("Sajdak"); 116 | $objectToRemove = new EntityFake(2); 117 | $this->remove($objectToRemove); 118 | $this->reset(); 119 | 120 | $recoveryPoint->cloneProperties($object, $objectSnapshot)->shouldHaveBeenCalled(); 121 | $this->isRemoved($objectToRemove)->shouldReturn(false); 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /spec/Isolate/UnitOfWork/Object/PropertyAccessorSpec.php: -------------------------------------------------------------------------------- 1 | shouldThrow( 16 | new InvalidArgumentException("PropertyAccessor require object to access property, \"array\" passed.") 17 | )->during("getValue", [[], "property"]); 18 | } 19 | 20 | function it_throw_exception_when_accessing_not_existing_property() 21 | { 22 | $object = new ProtectedEntity(); 23 | $this->shouldThrow( 24 | new NotExistingPropertyException("Property \"notExistingPropertyName\" does not exists in \"Isolate\\UnitOfWork\\Tests\\Double\\ProtectedEntity\" class.") 25 | )->during("getValue", [$object, "notExistingPropertyName"]); 26 | } 27 | 28 | function it_can_read_value_from_private_property() 29 | { 30 | $object = new ProtectedEntity(124); 31 | $this->getValue($object, "privateProperty")->shouldReturn(124); 32 | } 33 | 34 | function it_can_read_value_from_protected_property() 35 | { 36 | $object = new ProtectedEntity(124, 256); 37 | $this->getValue($object, "protectedProperty")->shouldReturn(256); 38 | } 39 | 40 | function it_can_read_value_from_public_property() 41 | { 42 | $object = new ProtectedEntity(); 43 | $object->publicProperty = 64; 44 | $this->getValue($object, "publicProperty")->shouldReturn(64); 45 | } 46 | 47 | 48 | function it_throw_exception_on_attempt_to_set_property_not_on_object() 49 | { 50 | $this->shouldThrow( 51 | new InvalidArgumentException("PropertyAccessor require object to access property, \"array\" passed.") 52 | )->during("setValue", [[], "property", 64]); 53 | } 54 | 55 | function it_throw_exception_when_setting_not_existing_property() 56 | { 57 | $object = new ProtectedEntity(); 58 | $this->shouldThrow( 59 | new NotExistingPropertyException("Property \"notExistingPropertyName\" does not exists in \"Isolate\\UnitOfWork\\Tests\\Double\\ProtectedEntity\" class.") 60 | )->during("setValue", [$object, "notExistingPropertyName", 64]); 61 | } 62 | 63 | function it_can_set_value_from_private_property() 64 | { 65 | $object = new ProtectedEntity(); 66 | $this->setValue($object, "privateProperty", 124); 67 | $this->getValue($object, "privateProperty")->shouldReturn(124); 68 | } 69 | 70 | function it_can_set_value_from_protected_property() 71 | { 72 | $object = new ProtectedEntity(); 73 | $this->setValue($object, "protectedProperty", 256); 74 | $this->getValue($object, "protectedProperty")->shouldReturn(256); 75 | } 76 | 77 | function it_can_set_value_from_public_property() 78 | { 79 | $object = new ProtectedEntity(); 80 | $this->setValue($object, "publicProperty", 64); 81 | $this->getValue($object, "publicProperty")->shouldReturn(64); 82 | } 83 | 84 | } 85 | -------------------------------------------------------------------------------- /spec/Isolate/UnitOfWork/Object/PropertyClonerSpec.php: -------------------------------------------------------------------------------- 1 | shouldThrow(new InvalidArgumentException("Compared values need to be a valid objects.")) 18 | ->during("cloneProperties", ["fakeEntity", new EntityFake()]); 19 | 20 | $this->shouldThrow(new InvalidArgumentException("Compared values need to be a valid objects.")) 21 | ->during("cloneProperties", [new EntityFake(), "fakeEntity"]); 22 | } 23 | 24 | function it_throws_exception_when_target_and_source_are_different_types() 25 | { 26 | $this->shouldThrow(new InvalidArgumentException("Compared values need to be an instances of the same class.")) 27 | ->during("cloneProperties", [new ProtectedEntity(), new EntityFake()]); 28 | } 29 | 30 | function it_clones_properties_from_source_into_target() 31 | { 32 | $object = new EntityFake(1, "Norbert", "Orzechowicz"); 33 | $sourceObject = new EntityFake(1, "Dawid", "Sajdak"); 34 | 35 | $this->cloneProperties($object, $sourceObject); 36 | 37 | if ($object->getFirstName() !== "Dawid") { 38 | throw new ExampleException("Invalid object first name"); 39 | } 40 | if ($object->getLastName() !== "Sajdak") { 41 | throw new ExampleException("Invalid object last name"); 42 | } 43 | } 44 | 45 | function it_clone_single_property_from_source_into_target() 46 | { 47 | $object = new EntityFake(1, "Norbert", "Orzechowicz"); 48 | $sourceObject = new EntityFake(1, "Dawid", "Sajdak"); 49 | 50 | $this->cloneProperty($object, $sourceObject, "firstName"); 51 | 52 | if ($object->getFirstName() !== "Dawid") { 53 | throw new ExampleException("Invalid object first name"); 54 | } 55 | if ($object->getLastName() !== "Orzechowicz") { 56 | throw new ExampleException("Invalid object last name"); 57 | } 58 | } 59 | 60 | function it_throws_exception_when_property_does_not_exists() 61 | { 62 | $this->shouldThrow(new NotExistingPropertyException()) 63 | ->during("cloneProperty", [new EntityFake(), new EntityFake(), "notExistingProperty"]); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /spec/Isolate/UnitOfWork/UnitOfWorkSpec.php: -------------------------------------------------------------------------------- 1 | beConstructedWith( 21 | $registry, 22 | $identifier, 23 | new ChangeBuilder($definitions->getWrappedObject(), $identifier->getWrappedObject()), 24 | $comparer, 25 | $commandBus 26 | ); 27 | } 28 | 29 | function it_throw_exception_during_non_object_registration() 30 | { 31 | $this->shouldThrow(new InvalidArgumentException("Only objects can be registered in Unit of Work.")) 32 | ->during("register", ["Coduo"]); 33 | } 34 | 35 | function it_throw_exception_during_non_entity_registration() 36 | { 37 | $this->shouldThrow(new InvalidArgumentException("Only entities can be registered in Unit of Work.")) 38 | ->during("register", [new \stdClass()]); 39 | } 40 | 41 | function it_tells_when_entity_was_registered(Registry $registry, Identifier $identifier) 42 | { 43 | $entity = new EntityFake(); 44 | $registry->isRegistered($entity)->willReturn(true); 45 | $identifier->isEntity($entity)->willReturn(true); 46 | $registry->register($entity)->willReturn(); 47 | 48 | $this->register($entity); 49 | 50 | $this->isRegistered($entity)->shouldReturn(true); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Isolate/UnitOfWork/Command/Command.php: -------------------------------------------------------------------------------- 1 | entity = $entity; 35 | $this->changeSet = $changeSet; 36 | } 37 | 38 | /** 39 | * @return mixed 40 | * 41 | * @api 42 | */ 43 | public function getEntity() 44 | { 45 | return $this->entity; 46 | } 47 | 48 | /** 49 | * @return ChangeSet 50 | * 51 | * @api 52 | */ 53 | public function getChanges() 54 | { 55 | return $this->changeSet; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/Isolate/UnitOfWork/Command/EditCommandHandler.php: -------------------------------------------------------------------------------- 1 | entity = $entity; 28 | } 29 | 30 | /** 31 | * @return mixed 32 | * 33 | * @api 34 | */ 35 | public function getEntity() 36 | { 37 | return $this->entity; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Isolate/UnitOfWork/Command/NewCommandHandler.php: -------------------------------------------------------------------------------- 1 | entity = $entity; 28 | } 29 | 30 | /** 31 | * @return mixed 32 | * 33 | * @api 34 | */ 35 | public function getEntity() 36 | { 37 | return $this->entity; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Isolate/UnitOfWork/Command/RemoveCommandHandler.php: -------------------------------------------------------------------------------- 1 | definitions = $definitions; 25 | } 26 | 27 | /** 28 | * @param Command $command 29 | */ 30 | public function dispatch(Command $command) 31 | { 32 | $definition = $this->definitions->getDefinition($command->getEntity()); 33 | 34 | if ($command instanceof NewCommand && $definition->hasNewCommandHandler()) { 35 | return $definition->getNewCommandHandler()->handle($command); 36 | } 37 | 38 | if ($command instanceof EditCommand && $definition->hasEditCommandHandler()) { 39 | return $definition->getEditCommandHandler()->handle($command); 40 | } 41 | 42 | if ($command instanceof RemoveCommand && $definition->hasRemoveCommandHandler()) { 43 | return $definition->getRemoveCommandHandler()->handle($command); 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/Isolate/UnitOfWork/Entity/ChangeBuilder.php: -------------------------------------------------------------------------------- 1 | propertyAccessor = new PropertyAccessor(); 50 | $this->propertyValueComparer = new PHPUnitValueComparer(); 51 | $this->definitions = $definitions; 52 | $this->identifier = $identifier; 53 | } 54 | 55 | /** 56 | * @param $oldEntity 57 | * @param $newEntity 58 | * @return ChangeSet 59 | * @throws RuntimeException 60 | * 61 | * @api 62 | */ 63 | public function buildChanges($oldEntity, $newEntity) 64 | { 65 | $changes = []; 66 | $entityDefinition = $this->definitions->getDefinition($oldEntity); 67 | foreach ($entityDefinition->getObservedProperties() as $property) { 68 | if ($this->isDifferent($property, $oldEntity, $newEntity)) { 69 | $oldValue = $this->propertyAccessor->getValue($oldEntity, $property->getName()); 70 | $newValue = $this->propertyAccessor->getValue($newEntity, $property->getName()); 71 | 72 | $changes[] = $this->buildChange($property, $oldValue, $newValue); 73 | } 74 | } 75 | 76 | return new ChangeSet($changes); 77 | } 78 | 79 | /** 80 | * @param Property $property 81 | * @param $oldEntity 82 | * @param $newEntity 83 | * @return bool 84 | */ 85 | private function isDifferent(Property $property, $oldEntity, $newEntity) 86 | { 87 | return $this->propertyValueComparer->hasDifferentValue($property, $newEntity, $oldEntity); 88 | } 89 | 90 | /** 91 | * @param Property $property 92 | * @param $oldValue 93 | * @param $newValue 94 | * @return \Isolate\UnitOfWork\Entity\Value\Change\ScalarChange 95 | * @throws RuntimeException 96 | */ 97 | private function buildChange(Property $property, $oldValue, $newValue) 98 | { 99 | if ($property->isAssociated()) { 100 | $association = $property->getAssociation(); 101 | switch ($association->getType()) { 102 | case Association::TO_SINGLE_ENTITY: 103 | return $this->buildAssociationToSingleEntityChange($property, $oldValue, $newValue); 104 | case Association::TO_MANY_ENTITIES; 105 | return $this->buildAssociationToManyEntitiesChange($property, $oldValue, $newValue); 106 | } 107 | } 108 | 109 | return new ScalarChange($property, $oldValue, $newValue); 110 | } 111 | 112 | /** 113 | * @param Property $property 114 | * @param $oldValue 115 | * @param $newValue 116 | * @return EditedEntity|NewEntity|RemovedEntity 117 | * @throws RuntimeException 118 | */ 119 | private function buildAssociationToSingleEntityChange(Property $property, $oldValue, $newValue) 120 | { 121 | if (is_null($newValue)) { 122 | return new RemovedEntity($property, $oldValue); 123 | } 124 | 125 | if (is_null($oldValue)) { 126 | $this->validateAssociatedEntity($property, $newValue); 127 | 128 | return new NewEntity($property, $newValue, $this->identifier->isPersisted($newValue)); 129 | } 130 | 131 | return new EditedEntity( 132 | $property, 133 | $this->buildChanges($oldValue, $newValue), 134 | $oldValue, 135 | $newValue 136 | ); 137 | } 138 | 139 | /** 140 | * @param Property $property 141 | * @param $oldValue 142 | * @param $newValue 143 | * @return AssociatedCollection 144 | * @throws RuntimeException 145 | */ 146 | private function buildAssociationToManyEntitiesChange(Property $property, $oldValue, $newValue) 147 | { 148 | if (!$this->isTraversableArray($newValue)) { 149 | throw new RuntimeException( 150 | sprintf( 151 | "Property \"%s\" is marked as associated with many entities and require new value to be traversable collection.", 152 | $property->getName() 153 | ) 154 | ); 155 | } 156 | 157 | 158 | $oldPersistedArray = $this->toPersistedArray($oldValue); 159 | $newPersistedArray = []; 160 | $changes = []; 161 | 162 | foreach ($newValue as $newElement) { 163 | $this->validateAssociatedEntity($property, $newElement); 164 | 165 | if (!$this->identifier->isPersisted($newElement)) { 166 | $changes[] = new NewEntity($property, $newElement, false); 167 | continue; 168 | } 169 | 170 | $identity = $this->identifier->getIdentity($newElement); 171 | $newPersistedArray[$identity] = $newElement; 172 | 173 | if (array_key_exists($identity, $oldPersistedArray)) { 174 | $oldElement = $oldPersistedArray[$identity]; 175 | $changeSet = $this->buildChanges($oldElement, $newElement); 176 | 177 | if ($changeSet->count()) { 178 | $changes[] = new EditedEntity($property, $changeSet, $oldElement, $newElement); 179 | } 180 | 181 | continue; 182 | } 183 | 184 | $changes[] = new NewEntity($property, $newElement, true); 185 | } 186 | 187 | foreach ($oldPersistedArray as $identity => $oldElement) { 188 | if (!array_key_exists($identity, $newPersistedArray)) { 189 | $changes[] = new RemovedEntity($property, $oldElement); 190 | } 191 | } 192 | 193 | return new AssociatedCollection($property, $oldValue, $newValue, $changes); 194 | } 195 | 196 | /** 197 | * @param $traversableArray 198 | * @return array 199 | */ 200 | private function toPersistedArray($traversableArray) 201 | { 202 | if (!$this->isTraversableArray($traversableArray)) { 203 | return []; 204 | } 205 | 206 | $result = []; 207 | foreach ($traversableArray as $valueElement) { 208 | $result[$this->identifier->getIdentity($valueElement)] = $valueElement; 209 | } 210 | 211 | return $result; 212 | } 213 | 214 | /** 215 | * @param Property $property 216 | * @param $newElement 217 | * @throws RuntimeException 218 | */ 219 | private function validateAssociatedEntity(Property $property, $newElement) 220 | { 221 | if (!is_object($newElement) || !$property->getAssociation()->getTargetClassName()->isClassOf($newElement)) { 222 | throw new RuntimeException( 223 | sprintf( 224 | "Property \"%s\" expects instanceof \"%s\" as a value.", 225 | $property->getName(), 226 | (string) $property->getAssociation()->getTargetClassName() 227 | ) 228 | ); 229 | } 230 | } 231 | 232 | /** 233 | * @param $newValue 234 | * @return bool 235 | */ 236 | private function isTraversableArray($newValue) 237 | { 238 | return is_array($newValue) || ($newValue instanceof \Traversable && $newValue instanceof \ArrayAccess); 239 | } 240 | } 241 | -------------------------------------------------------------------------------- /src/Isolate/UnitOfWork/Entity/ClassName.php: -------------------------------------------------------------------------------- 1 | className = $className; 32 | } 33 | 34 | public function __toString() 35 | { 36 | return $this->className; 37 | } 38 | 39 | /** 40 | * @param $entity 41 | * @return bool 42 | * 43 | * @api 44 | */ 45 | public function isClassOf($entity) 46 | { 47 | return $entity instanceof $this->className; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/Isolate/UnitOfWork/Entity/Comparer.php: -------------------------------------------------------------------------------- 1 | propertyValueComparer = new PHPUnitValueComparer(); 30 | $this->definitions = $definitions; 31 | } 32 | 33 | /** 34 | * @param $firstEntity 35 | * @param $secondEntity 36 | * @return bool 37 | * @throws InvalidArgumentException 38 | * 39 | * @api 40 | */ 41 | public function areEqual($firstEntity, $secondEntity) 42 | { 43 | $entityDefinition = $this->definitions->getDefinition($firstEntity); 44 | 45 | if (!$entityDefinition->fitsFor($secondEntity)) { 46 | throw new InvalidArgumentException("You can't compare entities of different type."); 47 | } 48 | 49 | foreach ($entityDefinition->getObservedProperties() as $property) { 50 | if ($this->propertyValueComparer->hasDifferentValue($property, $firstEntity, $secondEntity)) { 51 | return false; 52 | } 53 | } 54 | 55 | return true; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/Isolate/UnitOfWork/Entity/Definition.php: -------------------------------------------------------------------------------- 1 | className = $className; 62 | $this->idDefinition = $idDefinition; 63 | $this->observedProperties = []; 64 | $this->identificationStrategy = $identificationStrategy ?: new IdentificationStrategy\PropertyValue($idDefinition); 65 | } 66 | 67 | /** 68 | * @param Property[] $properties 69 | * @throws InvalidArgumentException 70 | * @throws NotExistingPropertyException 71 | * 72 | * @api 73 | */ 74 | public function setObserved(array $properties) 75 | { 76 | foreach ($properties as $property) { 77 | if (!$property instanceof Property) { 78 | throw new InvalidArgumentException("Each observed property needs to be an instance of Property"); 79 | } 80 | } 81 | $this->validatePropertyPaths((string) $this->className, $this->idDefinition, $properties); 82 | $this->observedProperties = array_unique($properties); 83 | } 84 | 85 | /** 86 | * @param Property $property 87 | * @throws InvalidArgumentException 88 | * @throws NotExistingPropertyException 89 | * 90 | * @api 91 | */ 92 | public function addToObserved(Property $property) 93 | { 94 | $this->validatePropertyPaths((string) $this->className, $this->idDefinition, [$property]); 95 | $this->observedProperties[] = $property; 96 | $this->observedProperties = array_unique($this->observedProperties); 97 | } 98 | 99 | /** 100 | * @return Property[]|array 101 | * 102 | * @api 103 | */ 104 | public function getObservedProperties() 105 | { 106 | return $this->observedProperties; 107 | } 108 | 109 | /** 110 | * @return ClassName 111 | * 112 | * @api 113 | */ 114 | public function getClassName() 115 | { 116 | return $this->className; 117 | } 118 | 119 | /** 120 | * @return Identity 121 | * 122 | * @api 123 | */ 124 | public function getIdDefinition() 125 | { 126 | return $this->idDefinition; 127 | } 128 | 129 | /** 130 | * @param $entity 131 | * @return bool 132 | * 133 | * @api 134 | */ 135 | public function fitsFor($entity) 136 | { 137 | return $this->className->isClassOf($entity); 138 | } 139 | 140 | /** 141 | * @param NewCommandHandler $commandHandler 142 | * 143 | * @api 144 | */ 145 | public function setNewCommandHandler(NewCommandHandler $commandHandler) 146 | { 147 | $this->newCommandHandler = $commandHandler; 148 | } 149 | 150 | /** 151 | * @return bool 152 | * 153 | * @api 154 | */ 155 | public function hasNewCommandHandler() 156 | { 157 | return isset($this->newCommandHandler); 158 | } 159 | 160 | /** 161 | * @return NewCommandHandler|null 162 | * 163 | * @api 164 | */ 165 | public function getNewCommandHandler() 166 | { 167 | return $this->newCommandHandler; 168 | } 169 | 170 | /** 171 | * @param EditCommandHandler $commandHandler 172 | * 173 | * @api 174 | */ 175 | public function setEditCommandHandler(EditCommandHandler $commandHandler) 176 | { 177 | $this->editCommandHandler = $commandHandler; 178 | } 179 | 180 | /** 181 | * @return bool 182 | * 183 | * @api 184 | */ 185 | public function hasEditCommandHandler() 186 | { 187 | return isset($this->editCommandHandler); 188 | } 189 | 190 | /** 191 | * @return EditCommandHandler|null 192 | * 193 | * @api 194 | */ 195 | public function getEditCommandHandler() 196 | { 197 | return $this->editCommandHandler; 198 | } 199 | 200 | /** 201 | * @param RemoveCommandHandler $commandHandler 202 | * 203 | * @api 204 | */ 205 | public function setRemoveCommandHandler(RemoveCommandHandler $commandHandler) 206 | { 207 | $this->removeCommandHandler = $commandHandler; 208 | } 209 | 210 | /** 211 | * @return bool 212 | * 213 | * @api 214 | */ 215 | public function hasRemoveCommandHandler() 216 | { 217 | return isset($this->removeCommandHandler); 218 | } 219 | 220 | /** 221 | * @return RemoveCommandHandler|null 222 | * 223 | * @api 224 | */ 225 | public function getRemoveCommandHandler() 226 | { 227 | return $this->removeCommandHandler; 228 | } 229 | 230 | /** 231 | * @return IdentificationStrategy|IdentificationStrategy 232 | * 233 | * @api 234 | */ 235 | public function getIdentityStrategy() 236 | { 237 | return $this->identificationStrategy; 238 | } 239 | 240 | /** 241 | * @param $className 242 | * @param Identity $idDefinition 243 | * @param array $observedProperties 244 | * @throws InvalidArgumentException 245 | * @throws NotExistingPropertyException 246 | */ 247 | private function validatePropertyPaths($className, Identity $idDefinition, array $observedProperties) 248 | { 249 | $reflection = new \ReflectionClass($className); 250 | foreach ($observedProperties as $property) { 251 | $propertyName = $property->getName(); 252 | if ($idDefinition->isEqual($propertyName)) { 253 | throw new InvalidArgumentException("Id definition property path can't be between observer properties."); 254 | } 255 | 256 | if (!$reflection->hasProperty($propertyName)) { 257 | throw new NotExistingPropertyException(sprintf( 258 | "Property \"%s\" does not exists in \"%s\" class.", 259 | $propertyName, 260 | $className 261 | )); 262 | } 263 | } 264 | } 265 | } 266 | -------------------------------------------------------------------------------- /src/Isolate/UnitOfWork/Entity/Definition/Association.php: -------------------------------------------------------------------------------- 1 | validateAssociationType($type); 41 | 42 | $this->target = $target; 43 | $this->type = $type; 44 | } 45 | 46 | /** 47 | * @return ClassName 48 | * 49 | * @api 50 | */ 51 | public function getTargetClassName() 52 | { 53 | return $this->target; 54 | } 55 | 56 | /** 57 | * @return int 58 | * 59 | * @api 60 | */ 61 | public function getType() 62 | { 63 | return $this->type; 64 | } 65 | 66 | /** 67 | * @param $type 68 | * @throws InvalidArgumentException 69 | */ 70 | private function validateAssociationType($type) 71 | { 72 | if ($type != self::TO_SINGLE_ENTITY && $type != self::TO_MANY_ENTITIES) { 73 | throw new InvalidArgumentException("Unknown association type."); 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/Isolate/UnitOfWork/Entity/Definition/IdentificationStrategy.php: -------------------------------------------------------------------------------- 1 | identity = $identity; 28 | $this->propertyAccessor = new PropertyAccessor(); 29 | } 30 | 31 | /** 32 | * @param mixed $entity 33 | * @return boolean 34 | */ 35 | public function isIdentified($entity) 36 | { 37 | $identity = $this->propertyAccessor->getValue($entity, $this->identity->getPropertyName()); 38 | 39 | return !empty($identity) || $identity === 0; 40 | } 41 | 42 | /** 43 | * @param $entity 44 | * @return mixed 45 | * @throws RuntimeException 46 | */ 47 | public function getIdentity($entity) 48 | { 49 | $identity = $this->propertyAccessor->getValue($entity, $this->identity->getPropertyName()); 50 | 51 | if (empty($identity) && $identity !== 0) { 52 | throw new RuntimeException("Can't get identity from not identified entity."); 53 | } 54 | 55 | return $identity; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/Isolate/UnitOfWork/Entity/Definition/Identity.php: -------------------------------------------------------------------------------- 1 | propertyName = $propertyName; 32 | } 33 | 34 | /** 35 | * @return string 36 | * 37 | * @api 38 | */ 39 | public function getPropertyName() 40 | { 41 | return $this->propertyName; 42 | } 43 | 44 | /** 45 | * @param string $propertyPath 46 | * @return bool 47 | * 48 | * @api 49 | */ 50 | public function isEqual($propertyPath) 51 | { 52 | return $this->propertyName === $propertyPath; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Isolate/UnitOfWork/Entity/Definition/Property.php: -------------------------------------------------------------------------------- 1 | name = $name; 38 | $this->association = $association; 39 | } 40 | 41 | public function __toString() 42 | { 43 | return $this->name; 44 | } 45 | 46 | /** 47 | * @return string 48 | * 49 | * @api 50 | */ 51 | public function getName() 52 | { 53 | return $this->name; 54 | } 55 | 56 | /** 57 | * @return bool 58 | * 59 | * @api 60 | */ 61 | public function isAssociated() 62 | { 63 | return !is_null($this->association); 64 | } 65 | 66 | /** 67 | * @return null|Association 68 | * 69 | * @api 70 | */ 71 | public function getAssociation() 72 | { 73 | return $this->association; 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/Isolate/UnitOfWork/Entity/Definition/Repository.php: -------------------------------------------------------------------------------- 1 | entityDefinitions = []; 25 | 26 | if (!is_array($entityDefinitions) && !$entityDefinitions instanceof \Traversable) { 27 | throw new InvalidArgumentException("Entity definition repository require array od traversable collection of entity definitions."); 28 | } 29 | 30 | foreach ($entityDefinitions as $definition) { 31 | if (!$definition instanceof Definition) { 32 | throw new InvalidArgumentException("Each element in collection needs to be an instance of Isolate\\UnitOfWork\\Entity\\Definition"); 33 | } 34 | 35 | $this->addDefinition($definition); 36 | } 37 | 38 | $this->validateAssociations(); 39 | } 40 | 41 | /** 42 | * @param Definition $entityDefinition 43 | */ 44 | public function addDefinition(Definition $entityDefinition) 45 | { 46 | $this->entityDefinitions[(string) $entityDefinition->getClassName()] = $entityDefinition; 47 | } 48 | 49 | /** 50 | * @param $entity 51 | * @return bool 52 | * @throws InvalidArgumentException 53 | */ 54 | public function hasDefinition($entity) 55 | { 56 | if (!is_object($entity)) { 57 | throw new InvalidArgumentException("Entity definition repository require objects as arguments for methods."); 58 | } 59 | 60 | return array_key_exists(get_class($entity), $this->entityDefinitions); 61 | } 62 | 63 | /** 64 | * @param $entity 65 | * @return Definition 66 | * @throws RuntimeException 67 | */ 68 | public function getDefinition($entity) 69 | { 70 | if (!$this->hasDefinition($entity)) { 71 | throw new RuntimeException(sprintf("Entity definition for \"%s\" does not exists.", get_class($entity))); 72 | } 73 | 74 | return $this->entityDefinitions[get_class($entity)]; 75 | } 76 | 77 | /** 78 | * @throws InvalidArgumentException 79 | */ 80 | private function validateAssociations() 81 | { 82 | foreach ($this->entityDefinitions as $definition) { 83 | foreach ($definition->getObservedProperties() as $property) { 84 | $this->validateAssociation($definition, $property); 85 | } 86 | } 87 | } 88 | 89 | /** 90 | * @param Definition $definition 91 | * @param Property $property 92 | * @throws InvalidArgumentException 93 | */ 94 | private function validateAssociation(Definition $definition, Property $property) 95 | { 96 | if ($property->isAssociated()) { 97 | $targetClass = (string) $property->getAssociation()->getTargetClassName(); 98 | if (!array_key_exists($targetClass, $this->entityDefinitions)) { 99 | throw new InvalidArgumentException( 100 | sprintf( 101 | "Entity class \"%s\" used in association of \"%s\" entity does not have definition.", 102 | $targetClass, 103 | (string)$definition->getClassName() 104 | ) 105 | ); 106 | } 107 | } 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/Isolate/UnitOfWork/Entity/Identifier.php: -------------------------------------------------------------------------------- 1 | definitions = $definitions; 28 | $this->propertyAccessor = new PropertyAccessor(); 29 | } 30 | 31 | /** 32 | * @param $object 33 | * @return boolean 34 | */ 35 | public function isEntity($object) 36 | { 37 | return $this->definitions->hasDefinition($object); 38 | } 39 | 40 | /** 41 | * @param mixed $entity 42 | * @return bool 43 | * @throws RuntimeException 44 | */ 45 | public function isPersisted($entity) 46 | { 47 | $this->validateEntity($entity); 48 | $entityDefinition = $this->definitions->getDefinition($entity); 49 | 50 | return $entityDefinition->getIdentityStrategy()->isIdentified($entity); 51 | 52 | } 53 | 54 | /** 55 | * @param $entity 56 | * @return mixed 57 | * @throws RuntimeException 58 | */ 59 | public function getIdentity($entity) 60 | { 61 | $this->validateEntity($entity); 62 | $entityDefinition = $this->definitions->getDefinition($entity); 63 | 64 | return $entityDefinition->getIdentityStrategy()->getIdentity($entity); 65 | } 66 | 67 | /** 68 | * @param $entity 69 | * @throws RuntimeException 70 | */ 71 | private function validateEntity($entity) 72 | { 73 | if (!$this->definitions->hasDefinition($entity)) { 74 | throw new RuntimeException(sprintf("Class \"%s\" does not have definition.", get_class($entity))); 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/Isolate/UnitOfWork/Entity/Property/PHPUnitComparator/Factory.php: -------------------------------------------------------------------------------- 1 | register(new TypeComparator); 25 | $this->register(new StrictScalarComparator()); 26 | $this->register(new NumericComparator); 27 | $this->register(new DoubleComparator); 28 | $this->register(new ArrayComparator); 29 | $this->register(new ResourceComparator); 30 | $this->register(new ObjectComparator); 31 | $this->register(new ExceptionComparator); 32 | $this->register(new SplObjectStorageComparator); 33 | $this->register(new DOMNodeComparator); 34 | $this->register(new DateTimeComparator); 35 | } 36 | } -------------------------------------------------------------------------------- /src/Isolate/UnitOfWork/Entity/Property/PHPUnitComparator/StrictScalarComparator.php: -------------------------------------------------------------------------------- 1 | throwComparisonFailureException($expected, $actual); 52 | } 53 | } 54 | 55 | if ($expectedToCompare != $actualToCompare) { 56 | if (is_string($expected) && is_string($actual)) { 57 | throw new ComparisonFailure( 58 | $expected, 59 | $actual, 60 | $this->exporter->export($expected), 61 | $this->exporter->export($actual), 62 | false, 63 | 'Failed asserting that two strings are equal.' 64 | ); 65 | } 66 | 67 | $this->throwComparisonFailureException($expected, $actual); 68 | } 69 | } 70 | 71 | /** 72 | * @param $expected 73 | * @param $actual 74 | */ 75 | private function throwComparisonFailureException($expected, $actual) 76 | { 77 | throw new ComparisonFailure( 78 | $expected, 79 | $actual, 80 | $this->exporter->export($expected), 81 | $this->exporter->export($actual), 82 | false, 83 | sprintf( 84 | 'Failed asserting that %s matches expected %s.', 85 | $this->exporter->export($actual), 86 | $this->exporter->export($expected) 87 | ) 88 | ); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/Isolate/UnitOfWork/Entity/Property/PHPUnitValueComparer.php: -------------------------------------------------------------------------------- 1 | comparatorFactory = new Factory(); 26 | $this->propertyAccessor = new PropertyAccessor(); 27 | } 28 | 29 | /** 30 | * @param Property $property 31 | * @param $firstObject 32 | * @param $secondObject 33 | * @return bool 34 | * @throws InvalidArgumentException 35 | */ 36 | public function hasDifferentValue(Property $property, $firstObject, $secondObject) 37 | { 38 | $this->validaObjects($firstObject, $secondObject); 39 | 40 | $firstValue = $this->propertyAccessor->getValue($firstObject, $property->getName()); 41 | $secondValue = $this->propertyAccessor->getValue($secondObject, $property->getName()); 42 | 43 | 44 | $comparator = $this->comparatorFactory->getComparatorFor($firstValue, $secondValue); 45 | 46 | try { 47 | $comparator->assertEquals($firstValue, $secondValue); 48 | return false; 49 | } catch (ComparisonFailure $exception) { 50 | return true; 51 | } 52 | } 53 | 54 | /** 55 | * @param $firstObject 56 | * @param $secondObject 57 | * @throws InvalidArgumentException 58 | */ 59 | private function validaObjects($firstObject, $secondObject) 60 | { 61 | if (!is_object($firstObject) || !is_object($secondObject)) { 62 | throw new InvalidArgumentException("Compared values need to be a valid objects."); 63 | } 64 | 65 | if (get_class($firstObject) !== get_class($secondObject)) { 66 | throw new InvalidArgumentException("Compared values need to be an instances of the same class."); 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/Isolate/UnitOfWork/Entity/Property/ValueComparer.php: -------------------------------------------------------------------------------- 1 | property = $property; 41 | $this->originValue = $originValue; 42 | $this->newValue = $newValue; 43 | $this->changes = $changes; 44 | } 45 | 46 | /** 47 | * @return mixed 48 | */ 49 | public function getOriginValue() 50 | { 51 | return $this->originValue; 52 | } 53 | 54 | /** 55 | * @return mixed 56 | */ 57 | public function getNewValue() 58 | { 59 | return $this->newValue; 60 | } 61 | 62 | /** 63 | * @return Property 64 | */ 65 | public function getProperty() 66 | { 67 | return $this->property; 68 | } 69 | 70 | /** 71 | * @return array|NewEntity[] 72 | * 73 | * @api 74 | */ 75 | public function getChangeForNewEntities() 76 | { 77 | $new = []; 78 | foreach ($this->changes as $change) { 79 | if ($change instanceof NewEntity) { 80 | $new[] = $change; 81 | } 82 | } 83 | 84 | return $new; 85 | } 86 | 87 | /** 88 | * @return array|RemovedEntity[] 89 | * 90 | * @api 91 | */ 92 | public function getChangesForRemovedEntities() 93 | { 94 | $removed = []; 95 | foreach ($this->changes as $change) { 96 | if ($change instanceof RemovedEntity) { 97 | $removed[] = $change; 98 | } 99 | } 100 | 101 | return $removed; 102 | } 103 | 104 | /** 105 | * @return array|EditedEntity[] 106 | * 107 | * @api 108 | */ 109 | public function getChangesForEditedEntities() 110 | { 111 | $edited = []; 112 | foreach ($this->changes as $change) { 113 | if ($change instanceof EditedEntity) { 114 | $edited[] = $change; 115 | } 116 | } 117 | 118 | return $edited; 119 | } 120 | 121 | /** 122 | * @param $propertyName 123 | * @return bool 124 | */ 125 | public function isFor($propertyName) 126 | { 127 | return $this->property->getName() === $propertyName; 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /src/Isolate/UnitOfWork/Entity/Value/Change/EditedEntity.php: -------------------------------------------------------------------------------- 1 | property = $property; 40 | $this->originValue = $originValue; 41 | $this->newValue = $newValue; 42 | $this->changeSet = $changeSet; 43 | } 44 | 45 | /** 46 | * @return mixed 47 | */ 48 | public function getOriginValue() 49 | { 50 | return $this->originValue; 51 | } 52 | 53 | /** 54 | * @return mixed 55 | */ 56 | public function getNewValue() 57 | { 58 | return $this->newValue; 59 | } 60 | 61 | /** 62 | * @return Property 63 | */ 64 | public function getProperty() 65 | { 66 | return $this->property; 67 | } 68 | 69 | /** 70 | * @return ChangeSet 71 | * 72 | * @api 73 | */ 74 | public function getChangeSet() 75 | { 76 | return $this->changeSet; 77 | } 78 | 79 | /** 80 | * @param $propertyName 81 | * @return bool 82 | */ 83 | public function isFor($propertyName) 84 | { 85 | return $this->property->getName() === $propertyName; 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/Isolate/UnitOfWork/Entity/Value/Change/NewEntity.php: -------------------------------------------------------------------------------- 1 | property = $property; 34 | $this->newValue = $newValue; 35 | $this->isPersisted = (boolean) $isPersisted; 36 | } 37 | 38 | /** 39 | * @return mixed 40 | */ 41 | public function getOriginValue() 42 | { 43 | return null; 44 | } 45 | 46 | /** 47 | * @return mixed 48 | */ 49 | public function getNewValue() 50 | { 51 | return $this->newValue; 52 | } 53 | 54 | /** 55 | * @return Property 56 | */ 57 | public function getProperty() 58 | { 59 | return $this->property; 60 | } 61 | 62 | /** 63 | * @return boolean 64 | * 65 | * @api 66 | */ 67 | public function isPersisted() 68 | { 69 | return $this->isPersisted; 70 | } 71 | 72 | /** 73 | * @param $propertyName 74 | * @return bool 75 | */ 76 | public function isFor($propertyName) 77 | { 78 | return $this->property->getName() === $propertyName; 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/Isolate/UnitOfWork/Entity/Value/Change/RemovedEntity.php: -------------------------------------------------------------------------------- 1 | property = $property; 27 | $this->originValue = $originValue; 28 | } 29 | 30 | /** 31 | * @return mixed 32 | */ 33 | public function getOriginValue() 34 | { 35 | return $this->originValue; 36 | } 37 | 38 | /** 39 | * @return mixed 40 | */ 41 | public function getNewValue() 42 | { 43 | return null; 44 | } 45 | 46 | /** 47 | * @return Property 48 | */ 49 | public function getProperty() 50 | { 51 | return $this->property; 52 | } 53 | 54 | /** 55 | * @param $propertyName 56 | * @return bool 57 | */ 58 | public function isFor($propertyName) 59 | { 60 | return $this->property->getName() === $propertyName; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/Isolate/UnitOfWork/Entity/Value/Change/ScalarChange.php: -------------------------------------------------------------------------------- 1 | property = $property; 33 | $this->originValue = $originValue; 34 | $this->newValue = $newValue; 35 | } 36 | 37 | /** 38 | * @return mixed 39 | */ 40 | public function getOriginValue() 41 | { 42 | return $this->originValue; 43 | } 44 | 45 | /** 46 | * @return mixed 47 | */ 48 | public function getNewValue() 49 | { 50 | return $this->newValue; 51 | } 52 | 53 | /** 54 | * @return Property 55 | */ 56 | public function getProperty() 57 | { 58 | return $this->property; 59 | } 60 | 61 | /** 62 | * @param $propertyName 63 | * @return bool 64 | */ 65 | public function isFor($propertyName) 66 | { 67 | return $this->property->getName() === $propertyName; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/Isolate/UnitOfWork/Entity/Value/ChangeSet.php: -------------------------------------------------------------------------------- 1 | getIterator() as $change) { 22 | /* @var ScalarChange $change */ 23 | if ($change->isFor($propertyName)) { 24 | return true; 25 | } 26 | } 27 | 28 | return false; 29 | } 30 | 31 | /** 32 | * @param $propertyName 33 | * @return Change 34 | * @throws RuntimeException 35 | * 36 | * @api 37 | */ 38 | public function getChangeFor($propertyName) 39 | { 40 | foreach ($this->getIterator() as $change) { 41 | /* @var \Isolate\UnitOfWork\Entity\Value\Change\ScalarChange $change */ 42 | if ($change->isFor($propertyName)) { 43 | return $change; 44 | } 45 | } 46 | 47 | throw new RuntimeException(sprintf("There are no changes for \"%s\" property.", $propertyName)); 48 | } 49 | 50 | /** 51 | * @param array $properties 52 | * @return bool 53 | * 54 | * @api 55 | */ 56 | public function hasChangesForAny(array $properties = []) 57 | { 58 | foreach ($properties as $propertyName) { 59 | if ($this->hasChangeFor($propertyName)) { 60 | return true; 61 | } 62 | } 63 | 64 | return false; 65 | } 66 | 67 | /** 68 | * @return ScalarChange[] 69 | * 70 | * @api 71 | */ 72 | public function all() 73 | { 74 | return $this->getIterator(); 75 | } 76 | 77 | /** 78 | * @param mixed $index 79 | * 80 | * @return Change 81 | */ 82 | public function offsetGet($index) 83 | { 84 | parent::offsetGet($index); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/Isolate/UnitOfWork/EntityStates.php: -------------------------------------------------------------------------------- 1 | snapshotMaker = $snapshotMaker; 39 | $this->recoveryPoint = $recoveryPoint; 40 | $this->objects = []; 41 | $this->snapshots = []; 42 | $this->removed = []; 43 | } 44 | 45 | /** 46 | * {@inheritdoc} 47 | */ 48 | public function isRegistered($object) 49 | { 50 | return array_key_exists($this->getId($object), $this->objects); 51 | } 52 | 53 | /** 54 | * {@inheritdoc} 55 | */ 56 | public function register($object) 57 | { 58 | $this->objects[$this->getId($object)] = $object; 59 | $this->snapshots[$this->getId($object)] = $this->snapshotMaker->makeSnapshotOf($object); 60 | } 61 | 62 | /** 63 | * {@inheritdoc} 64 | */ 65 | public function getSnapshot($object) 66 | { 67 | return $this->snapshots[$this->getId($object)]; 68 | } 69 | 70 | /** 71 | * {@inheritdoc} 72 | */ 73 | public function makeNewSnapshots() 74 | { 75 | foreach ($this->objects as $id => $entity) { 76 | $this->snapshots[$id] = $this->snapshotMaker->makeSnapshotOf($entity); 77 | } 78 | } 79 | 80 | /** 81 | * {@inheritdoc} 82 | */ 83 | public function isRemoved($object) 84 | { 85 | return array_key_exists($this->getId($object), $this->removed); 86 | } 87 | 88 | /** 89 | * {@inheritdoc} 90 | */ 91 | public function remove($object) 92 | { 93 | if (!$this->isRegistered($object)) { 94 | $this->register($object); 95 | } 96 | 97 | $this->removed[$this->getId($object)] = true; 98 | } 99 | 100 | /** 101 | * {@inheritdoc} 102 | */ 103 | public function cleanRemoved() 104 | { 105 | foreach ($this->removed as $id => $object) { 106 | unset($this->snapshots[$id]); 107 | unset($this->objects[$id]); 108 | } 109 | 110 | $this->removed = []; 111 | } 112 | 113 | /** 114 | * {@inheritdoc} 115 | */ 116 | public function all() 117 | { 118 | return array_values($this->objects); 119 | } 120 | 121 | /** 122 | * {@inheritdoc} 123 | */ 124 | public function reset() 125 | { 126 | $this->removed = []; 127 | 128 | foreach ($this->snapshots as $id => $objectSnapshot) { 129 | $this->recoveryPoint->cloneProperties($this->objects[$id], $objectSnapshot); 130 | } 131 | } 132 | 133 | /** 134 | * @param $entity 135 | * @return string 136 | */ 137 | private function getId($entity) 138 | { 139 | return spl_object_hash($entity); 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /src/Isolate/UnitOfWork/Object/PropertyAccessor.php: -------------------------------------------------------------------------------- 1 | validateObject($object); 25 | $reflection = new \ReflectionClass($object); 26 | $this->validateProperty($reflection, $object, $propertyName); 27 | $property = $reflection->getProperty($propertyName); 28 | $property->setAccessible(true); 29 | 30 | return $property->getValue($object); 31 | } 32 | 33 | /** 34 | * @param $object 35 | * @param $propertyName 36 | * @param $value 37 | * @throws InvalidArgumentException 38 | * @throws NotExistingPropertyException 39 | * 40 | * @api 41 | */ 42 | public function setValue($object, $propertyName, $value) 43 | { 44 | $this->validateObject($object); 45 | $reflection = new \ReflectionClass($object); 46 | $this->validateProperty($reflection, $object, $propertyName); 47 | $property = $reflection->getProperty($propertyName); 48 | $property->setAccessible(true); 49 | 50 | $property->setValue($object, $value); 51 | } 52 | 53 | /** 54 | * @param $value 55 | * @throws InvalidArgumentException 56 | */ 57 | private function validateObject($value) 58 | { 59 | if (!is_object($value)) { 60 | throw new InvalidArgumentException(sprintf( 61 | "PropertyAccessor require object to access property, \"%s\" passed.", 62 | gettype($value) 63 | )); 64 | } 65 | } 66 | 67 | /** 68 | * @param \ReflectionClass $reflection 69 | * @param $object 70 | * @param $propertyName 71 | * @throws NotExistingPropertyException 72 | */ 73 | private function validateProperty(\ReflectionClass $reflection, $object, $propertyName) 74 | { 75 | if (!$reflection->hasProperty($propertyName)) { 76 | throw new NotExistingPropertyException(sprintf( 77 | "Property \"%s\" does not exists in \"%s\" class.", 78 | $propertyName, 79 | get_class($object) 80 | )); 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/Isolate/UnitOfWork/Object/PropertyCloner.php: -------------------------------------------------------------------------------- 1 | propertyAccessor = new PropertyAccessor(); 21 | } 22 | 23 | /** 24 | * @param $target 25 | * @param $source 26 | * 27 | * @api 28 | */ 29 | public function cloneProperties($target, $source) 30 | { 31 | $this->validaObjects($target, $source); 32 | 33 | $reflection = new \ReflectionClass($target); 34 | foreach ($reflection->getProperties() as $property) { 35 | 36 | $this->propertyAccessor->setValue( 37 | $target, 38 | $property->getName(), 39 | $this->propertyAccessor->getValue($source, $property->getName()) 40 | ); 41 | } 42 | } 43 | 44 | /** 45 | * @param $target 46 | * @param $source 47 | * @param $propertyName 48 | * @throws InvalidArgumentException 49 | * @throws NotExistingPropertyException 50 | * 51 | * @api 52 | */ 53 | public function cloneProperty($target, $source, $propertyName) 54 | { 55 | $this->validaObjects($target, $source); 56 | 57 | $reflection = new \ReflectionClass($target); 58 | if (!$reflection->hasProperty($propertyName)) { 59 | throw new NotExistingPropertyException(); 60 | } 61 | 62 | $this->propertyAccessor->setValue( 63 | $target, 64 | $propertyName, 65 | $this->propertyAccessor->getValue($source, $propertyName) 66 | ); 67 | } 68 | 69 | /** 70 | * @param $firstObject 71 | * @param $secondObject 72 | * @throws InvalidArgumentException 73 | */ 74 | private function validaObjects($firstObject, $secondObject) 75 | { 76 | if (!is_object($firstObject) || !is_object($secondObject)) { 77 | throw new InvalidArgumentException("Compared values need to be a valid objects."); 78 | } 79 | 80 | if (get_class($firstObject) !== get_class($secondObject)) { 81 | throw new InvalidArgumentException("Compared values need to be an instances of the same class."); 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/Isolate/UnitOfWork/Object/Registry.php: -------------------------------------------------------------------------------- 1 | cloner = new DeepCopy(); 19 | } 20 | 21 | /** 22 | * @param mixed $object 23 | * @return mixed 24 | * @throws InvalidArgumentException 25 | */ 26 | public function makeSnapshotOf($object) 27 | { 28 | return $this->cloner->copy($object); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Isolate/UnitOfWork/UnitOfWork.php: -------------------------------------------------------------------------------- 1 | registry = $registry; 61 | $this->identifier = $identifier; 62 | $this->changeBuilder = $changeBuilder; 63 | $this->comparer = $entityComparer; 64 | $this->commandBus = $commandBus; 65 | } 66 | 67 | /** 68 | * @param $entity 69 | * @throws InvalidArgumentException 70 | * @throws RuntimeException 71 | * 72 | * @api 73 | */ 74 | public function register($entity) 75 | { 76 | if (!is_object($entity)) { 77 | throw new InvalidArgumentException("Only objects can be registered in Unit of Work."); 78 | } 79 | 80 | if (!$this->identifier->isEntity($entity)) { 81 | throw new InvalidArgumentException("Only entities can be registered in Unit of Work."); 82 | } 83 | 84 | $this->registry->register($entity); 85 | } 86 | 87 | /** 88 | * @param $entity 89 | * @return bool 90 | * 91 | * @api 92 | */ 93 | public function isRegistered($entity) 94 | { 95 | return $this->registry->isRegistered($entity); 96 | } 97 | 98 | /** 99 | * @param $entity 100 | * @throws InvalidArgumentException 101 | * @throws RuntimeException 102 | * 103 | * @api 104 | */ 105 | public function remove($entity) 106 | { 107 | if (!is_object($entity)) { 108 | throw new InvalidArgumentException("Only objects can be registered in Unit of Work."); 109 | } 110 | 111 | if (!$this->isRegistered($entity)) { 112 | if (!$this->identifier->isPersisted($entity)) { 113 | throw new RuntimeException("Unit of Work can't remove not persisted entities."); 114 | } 115 | } 116 | 117 | $this->registry->remove($entity); 118 | } 119 | 120 | /** 121 | * @throws InvalidArgumentException 122 | * @throws RuntimeException 123 | * 124 | * @api 125 | */ 126 | public function commit() 127 | { 128 | foreach ($this->registry->all() as $entity) { 129 | switch($this->getEntityState($entity)) { 130 | case EntityStates::NEW_ENTITY: 131 | $this->commandBus->dispatch(new NewCommand($entity)); 132 | break; 133 | case EntityStates::EDITED_ENTITY: 134 | $changeSet = $this->changeBuilder->buildChanges($this->registry->getSnapshot($entity), $entity); 135 | $this->commandBus->dispatch(new EditCommand($entity, $changeSet)); 136 | break; 137 | case EntityStates::REMOVED_ENTITY: 138 | $this->commandBus->dispatch(new RemoveCommand($entity)); 139 | break; 140 | } 141 | } 142 | 143 | $this->registry->cleanRemoved(); 144 | $this->registry->makeNewSnapshots(); 145 | } 146 | 147 | /** 148 | * @api 149 | */ 150 | public function rollback() 151 | { 152 | $this->registry->reset(); 153 | } 154 | 155 | /** 156 | * @param $entity 157 | * @return int 158 | * @throws InvalidArgumentException 159 | * @throws RuntimeException 160 | */ 161 | private function getEntityState($entity) 162 | { 163 | if (!is_object($entity)) { 164 | throw new InvalidArgumentException("Only objects can be registered in Unit of Work."); 165 | } 166 | 167 | if (!$this->isRegistered($entity)) { 168 | throw new RuntimeException("Object need to be registered first in the Unit of Work."); 169 | } 170 | 171 | if ($this->registry->isRemoved($entity)) { 172 | return EntityStates::REMOVED_ENTITY; 173 | } 174 | 175 | if (!$this->identifier->isPersisted($entity)) { 176 | return EntityStates::NEW_ENTITY; 177 | } 178 | 179 | if ($this->isChanged($entity)) { 180 | return EntityStates::EDITED_ENTITY; 181 | } 182 | 183 | return EntityStates::PERSISTED_ENTITY; 184 | } 185 | 186 | /** 187 | * @param $entity 188 | * @return bool 189 | * @throws RuntimeException 190 | */ 191 | private function isChanged($entity) 192 | { 193 | return !$this->comparer->areEqual( 194 | $entity, 195 | $this->registry->getSnapshot($entity) 196 | ); 197 | } 198 | } 199 | -------------------------------------------------------------------------------- /tests/Isolate/UnitOfWork/Tests/Double/AssociatedEntityFake.php: -------------------------------------------------------------------------------- 1 | id = $id; 35 | $this->children = []; 36 | $this->parent = $parent; 37 | $this->children = $children; 38 | $this->name = $name; 39 | } 40 | 41 | /** 42 | * @return null 43 | */ 44 | public function getId() 45 | { 46 | return $this->id; 47 | } 48 | 49 | /** 50 | * @param $id 51 | */ 52 | public function setId($id) 53 | { 54 | $this->id = $id; 55 | } 56 | 57 | /** 58 | * @return null 59 | */ 60 | public function getName() 61 | { 62 | return $this->name; 63 | } 64 | 65 | /** 66 | * @param null $name 67 | */ 68 | public function setName($name) 69 | { 70 | $this->name = $name; 71 | } 72 | 73 | /** 74 | * @param AssociatedEntityFake $child 75 | */ 76 | public function addChild(AssociatedEntityFake $child) 77 | { 78 | $this->children[] = $child; 79 | } 80 | 81 | /** 82 | * @param $parent 83 | */ 84 | public function setParent($parent) 85 | { 86 | $this->parent = $parent; 87 | } 88 | 89 | public function removeParent() 90 | { 91 | $this->parent = null; 92 | } 93 | 94 | /** 95 | * @return AssociatedEntityFake 96 | */ 97 | public function getParent() 98 | { 99 | return $this->parent; 100 | } 101 | 102 | /** 103 | * @return array|EntityFakeChild[] 104 | */ 105 | public function getChildren() 106 | { 107 | return $this->children; 108 | } 109 | 110 | public static function getClassName() 111 | { 112 | return __CLASS__; 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /tests/Isolate/UnitOfWork/Tests/Double/EditCommandHandlerMock.php: -------------------------------------------------------------------------------- 1 | persistedEntities[] = $command->getEntity(); 20 | $this->persistedEntitiesChanges[] = $command->getChanges(); 21 | } 22 | 23 | public function entityWasPersisted($entity) 24 | { 25 | foreach ($this->persistedEntities as $persistedEntity) { 26 | if ($persistedEntity === $entity) { 27 | return true; 28 | } 29 | } 30 | 31 | return false; 32 | } 33 | 34 | public function getPersistedEntityChanges($entity) 35 | { 36 | foreach ($this->persistedEntities as $index => $persistedEntity) { 37 | if ($persistedEntity === $entity) { 38 | return $this->persistedEntitiesChanges[$index]; 39 | } 40 | } 41 | 42 | throw new \RuntimeException("Object was not handled"); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /tests/Isolate/UnitOfWork/Tests/Double/EntityFake.php: -------------------------------------------------------------------------------- 1 | id = $id; 37 | $this->firstName = $firstName; 38 | $this->lastName = $lastName; 39 | $this->items = $items; 40 | $this->children = []; 41 | } 42 | 43 | /** 44 | * @return null 45 | */ 46 | public function getId() 47 | { 48 | return $this->id; 49 | } 50 | 51 | /** 52 | * @param $id 53 | */ 54 | public function setId($id) 55 | { 56 | $this->id = $id; 57 | } 58 | 59 | /** 60 | * @param $newLastName 61 | */ 62 | public function changeLastName($newLastName) 63 | { 64 | $this->lastName = $newLastName; 65 | } 66 | 67 | /** 68 | * @return null 69 | */ 70 | public function getLastName() 71 | { 72 | return $this->lastName; 73 | } 74 | 75 | /** 76 | * @param $newName 77 | */ 78 | public function changeFirstName($newName) 79 | { 80 | $this->firstName = $newName; 81 | } 82 | 83 | /** 84 | * @return null 85 | */ 86 | public function getFirstName() 87 | { 88 | return $this->firstName; 89 | } 90 | 91 | /** 92 | * @return array 93 | */ 94 | public function getItems() 95 | { 96 | return $this->items; 97 | } 98 | 99 | /** 100 | * @param array $items 101 | */ 102 | public function setItems(array $items = []) 103 | { 104 | $this->items = $items; 105 | } 106 | 107 | public function addItem($item) 108 | { 109 | $this->items[] = $item; 110 | } 111 | 112 | public static function getClassName() 113 | { 114 | return __CLASS__; 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /tests/Isolate/UnitOfWork/Tests/Double/EntityFakeChild.php: -------------------------------------------------------------------------------- 1 | id = $id; 18 | } 19 | 20 | /** 21 | * @return null 22 | */ 23 | public function getId() 24 | { 25 | return $this->id; 26 | } 27 | 28 | /** 29 | * @param $id 30 | */ 31 | public function setId($id) 32 | { 33 | $this->id = $id; 34 | } 35 | 36 | public static function getClassName() 37 | { 38 | return __CLASS__; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /tests/Isolate/UnitOfWork/Tests/Double/NewCommandHandlerMock.php: -------------------------------------------------------------------------------- 1 | propertyAccessor = new PropertyAccessor(); 21 | } 22 | 23 | /** 24 | * @param NewCommand $command 25 | */ 26 | public function handle(NewCommand $command) 27 | { 28 | $this->persistedEntities[] = $command->getEntity(); 29 | 30 | //After persisting entity we need to give it some unique identity 31 | $this->propertyAccessor->setValue($command->getEntity(), "id", time()); 32 | } 33 | 34 | public function entityWasPersisted($entity) 35 | { 36 | foreach ($this->persistedEntities as $persistedObject) { 37 | if ($persistedObject === $entity) { 38 | return true; 39 | } 40 | } 41 | 42 | return false; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /tests/Isolate/UnitOfWork/Tests/Double/ProtectedEntity.php: -------------------------------------------------------------------------------- 1 | privateProperty = $privateValue; 16 | $this->protectedProperty = $protectedValue; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /tests/Isolate/UnitOfWork/Tests/Double/RemoveCommandHandlerMock.php: -------------------------------------------------------------------------------- 1 | removedEntities[] = $command->getEntity(); 18 | } 19 | 20 | public function entityWasRemoved($entity) 21 | { 22 | foreach ($this->removedEntities as $persistedObject) { 23 | if ($persistedObject === $entity) { 24 | return true; 25 | } 26 | } 27 | 28 | return false; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /tests/Isolate/UnitOfWork/Tests/UnitOfWorkTest.php: -------------------------------------------------------------------------------- 1 | editCommandHandler = new EditCommandHandlerMock(); 44 | $this->newCommandHandler = new NewCommandHandlerMock(); 45 | $this->removeCommandHandler = new RemoveCommandHandlerMock(); 46 | } 47 | 48 | function test_commit_of_new_entity() 49 | { 50 | $unitOfWork = $this->createUnitOfWork(); 51 | 52 | $entity = new EntityFake(); 53 | $unitOfWork->register($entity); 54 | 55 | $unitOfWork->commit(); 56 | $this->assertTrue($this->newCommandHandler->entityWasPersisted($entity)); 57 | } 58 | 59 | function test_commit_of_edited_and_persisted_entity() 60 | { 61 | $unitOfWork = $this->createUnitOfWork(); 62 | 63 | $entity = new EntityFake(1, "Norbert", "Orzechowicz", [new EntityFake(2)]); 64 | $unitOfWork->register($entity); 65 | 66 | $entity->changeFirstName("Michal"); 67 | $entity->changeLastName("Dabrowski"); 68 | 69 | $unitOfWork->commit(); 70 | 71 | $this->assertTrue($this->editCommandHandler->entityWasPersisted($entity)); 72 | $this->assertEquals( 73 | new ChangeSet([ 74 | new ScalarChange(new Property("firstName"), "Norbert", "Michal"), 75 | new ScalarChange(new Property("lastName"), "Orzechowicz", "Dabrowski") 76 | ]), 77 | $this->editCommandHandler->getPersistedEntityChanges($entity) 78 | ); 79 | } 80 | 81 | function test_commit_of_edited_and_persisted_entity_with_changes_in_property_that_contains_array() 82 | { 83 | $unitOfWork = $this->createUnitOfWork(); 84 | 85 | $entity = new EntityFake(1, "Norbert", "Orzechowicz", [new EntityFake(2, "Dawid", "Sajdak")]); 86 | $unitOfWork->register($entity); 87 | 88 | $items = $entity->getItems(); 89 | $items[0]->changeFirstName("Michal"); 90 | $items[0]->changeLastName("Dabrowski"); 91 | 92 | $unitOfWork->commit(); 93 | 94 | $this->assertTrue($this->editCommandHandler->entityWasPersisted($entity)); 95 | $this->assertEquals( 96 | new ChangeSet([new ScalarChange( 97 | new Property("items"), 98 | [new EntityFake(2, "Dawid", "Sajdak")], 99 | [new EntityFake(2, "Michal", "Dabrowski")] 100 | )]), 101 | $this->editCommandHandler->getPersistedEntityChanges($entity) 102 | ); 103 | } 104 | 105 | function test_commit_of_removed_and_persisted_entity() 106 | { 107 | $unitOfWork = $this->createUnitOfWork(); 108 | $entity = new EntityFake(1, "Dawid", "Sajdak"); 109 | 110 | $unitOfWork->register($entity); 111 | $unitOfWork->remove($entity); 112 | $unitOfWork->commit(); 113 | 114 | $this->assertTrue($this->removeCommandHandler->entityWasRemoved($entity)); 115 | $this->assertFalse($unitOfWork->isRegistered($entity)); 116 | } 117 | 118 | function test_commits_after_persist_and_update_entity() 119 | { 120 | $unitOfWork = $this->createUnitOfWork(); 121 | $entity = new EntityFake(); 122 | $unitOfWork->register($entity); 123 | $unitOfWork->commit(); 124 | $this->assertTrue($this->newCommandHandler->entityWasPersisted($entity)); 125 | 126 | $entity->changeFirstName('Norbert'); 127 | $unitOfWork->commit(); 128 | 129 | $this->assertTrue($this->editCommandHandler->entityWasPersisted($entity)); 130 | } 131 | 132 | function test_rollback_entity_before_commit() 133 | { 134 | $unitOfWork = $this->createUnitOfWork(); 135 | $entity = new EntityFake(1, "Dawid", "Sajdak"); 136 | $unitOfWork->register($entity); 137 | $entity->changeFirstName("Norbert"); 138 | $entity->changeLastName("Orzechowicz"); 139 | 140 | $unitOfWork->rollback(); 141 | 142 | $this->assertSame("Dawid", $entity->getFirstName()); 143 | $this->assertSame("Sajdak", $entity->getLastName()); 144 | } 145 | 146 | function test_that_rollback_after_successful_commit_have_no_affect_for_entities() 147 | { 148 | $unitOfWork = $this->createUnitOfWork(); 149 | $entity = new EntityFake(1, "Dawid", "Sajdak"); 150 | $unitOfWork->register($entity); 151 | $entity->changeFirstName("Norbert"); 152 | $entity->changeLastName("Orzechowicz"); 153 | $unitOfWork->commit(); 154 | 155 | $unitOfWork->rollback(); 156 | 157 | $this->assertSame("Norbert", $entity->getFirstName()); 158 | $this->assertSame("Orzechowicz", $entity->getLastName()); 159 | } 160 | 161 | function test_definition_registration_after_unit_of_work_is_created() 162 | { 163 | $definitions = new Definition\Repository\InMemory([]); 164 | $unitOfWork = $this->createUnitOfWork($definitions); 165 | $definitions->addDefinition($this->createFakeEntityDefinition()); 166 | 167 | $entity = new EntityFake(); 168 | $unitOfWork->register($entity); 169 | $unitOfWork->commit(); 170 | $this->assertTrue($this->newCommandHandler->entityWasPersisted($entity)); 171 | 172 | } 173 | 174 | /** 175 | * @return UnitOfWork 176 | */ 177 | private function createUnitOfWork(Definition\Repository $definitions = null) 178 | { 179 | $definitions = (is_null($definitions)) 180 | ? new Definition\Repository\InMemory([$this->createFakeEntityDefinition()]) 181 | : $definitions; 182 | 183 | $identifier = new EntityIdentifier($definitions); 184 | 185 | return new UnitOfWork( 186 | new InMemoryRegistry(new SnapshotMaker(), new PropertyCloner()), 187 | $identifier, 188 | new ChangeBuilder($definitions, $identifier), 189 | new Comparer($definitions), 190 | new SilentBus($definitions) 191 | ); 192 | } 193 | 194 | /** 195 | * @return \Isolate\UnitOfWork\Entity\Definition 196 | */ 197 | private function createFakeEntityDefinition() 198 | { 199 | $definition = new Definition(new ClassName(EntityFake::getClassName()), new Identity("id")); 200 | $definition->setObserved([ 201 | new Property("firstName"), 202 | new Property("lastName"), 203 | new Property("items")] 204 | ); 205 | $definition->setNewCommandHandler($this->newCommandHandler); 206 | $definition->setEditCommandHandler($this->editCommandHandler); 207 | $definition->setRemoveCommandHandler($this->removeCommandHandler); 208 | 209 | return $definition; 210 | } 211 | } 212 | -------------------------------------------------------------------------------- /tests/bootstrap.php: -------------------------------------------------------------------------------- 1 |