├── .gitignore ├── tests ├── bootstrap.php ├── Fakes │ ├── Projectors │ │ ├── NoModeProjector.php │ │ ├── BrokenProjector.php │ │ ├── NewTestProjector.php │ │ ├── RunFromLaunch.php │ │ ├── RunFromStart.php │ │ ├── RunOnce.php │ │ └── BaseTestProjector.php │ └── Services │ │ └── EventLog │ │ └── ThingHappened.php ├── projectors.php ├── Integration │ ├── Infrastructure │ │ └── Service │ │ │ ├── InMemory │ │ │ ├── EventLogTest.php │ │ │ ├── ProjectorPositionRepositoryTest.php │ │ │ └── EventStreamTest.php │ │ │ └── Redis │ │ │ └── ProjectorPositionRepositoryTest.php │ └── Service │ │ ├── EventLogTest.php │ │ ├── EventLog │ │ └── EventStreamTest.php │ │ └── ProjectorPositionRepositoryTest.php ├── Unit │ ├── Strategy │ │ ├── EventHandler │ │ │ ├── ClassNameTest.php │ │ │ └── MetaTypePropertyTest.php │ │ └── ProjectorPlayerTest.php │ ├── ValueObjects │ │ ├── ProjectorReferenceTest.php │ │ ├── ProjectorPositionCollectionTest.php │ │ ├── ProjectorReferenceCollectionTest.php │ │ └── ProjectorPositionTest.php │ ├── Fakes │ │ └── ProjectorTest.php │ └── Services │ │ └── ProjectorQueryableTest.php └── Acceptance │ ├── ProjectionistPlayProjectorsTest.php │ └── ProjectionistBootProjectorsTest.php ├── src ├── Domain │ ├── Services │ │ ├── ProjectorException.php │ │ ├── EventWrapper.php │ │ ├── EventStream.php │ │ ├── EventLog.php │ │ ├── ProjectorPositionLedger.php │ │ └── ProjectorQueryable.php │ ├── Strategy │ │ ├── EventHandler.php │ │ ├── ProjectorSkipper.php │ │ └── ProjectorPlayer.php │ └── ValueObjects │ │ ├── ProjectorMode.php │ │ ├── ProjectorStatus.php │ │ ├── ProjectorReference.php │ │ ├── ProjectorPosition.php │ │ ├── ProjectorPositionCollection.php │ │ └── ProjectorReferenceCollection.php ├── App │ ├── ConfigFactory │ │ ├── InMemory.php │ │ └── Redis.php │ └── Config.php ├── Infra │ ├── EventWrapper │ │ └── Identifiable.php │ ├── EventStream │ │ └── InMemory.php │ ├── EventHandler │ │ └── ClassName.php │ ├── EventLog │ │ └── InMemory.php │ └── ProjectorPositionLedger │ │ ├── InMemory.php │ │ └── Redis.php └── Projectionist.php ├── composer.json ├── phpunit.xml ├── .circleci └── config.yml ├── benchmarks └── DefinedSpeedTest.php ├── README.md └── composer.lock /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | vendor/ 3 | .DS_Store -------------------------------------------------------------------------------- /tests/bootstrap.php: -------------------------------------------------------------------------------- 1 | id = $id; 10 | } 11 | 12 | public function id() 13 | { 14 | return $this->id; 15 | } 16 | } -------------------------------------------------------------------------------- /tests/Integration/Infrastructure/Service/InMemory/EventLogTest.php: -------------------------------------------------------------------------------- 1 | wrapped_event = $event; 10 | } 11 | 12 | public function id() 13 | { 14 | return $this->wrapped_event->id(); 15 | } 16 | 17 | public function wrappedEvent() 18 | { 19 | return $this->wrapped_event; 20 | } 21 | } -------------------------------------------------------------------------------- /tests/Integration/Infrastructure/Service/InMemory/ProjectorPositionRepositoryTest.php: -------------------------------------------------------------------------------- 1 | makeEventLog(); 14 | 15 | $stream = $event_log->getStream(self::EVENT_ID); 16 | 17 | $this->assertInstanceOf(\Projectionist\Domain\Services\EventStream::class, $stream); 18 | } 19 | } -------------------------------------------------------------------------------- /tests/Integration/Infrastructure/Service/InMemory/EventStreamTest.php: -------------------------------------------------------------------------------- 1 | projectorPositionLedger = $projector_position_ledger; 13 | } 14 | 15 | public function make(): Config 16 | { 17 | $eventLog = new Infra\EventLog\InMemory(); 18 | $eventHandler = new Infra\EventHandler\ClassName(); 19 | 20 | return new Config($this->projectorPositionLedger, $eventLog, $eventHandler); 21 | } 22 | } -------------------------------------------------------------------------------- /src/Infra/EventStream/InMemory.php: -------------------------------------------------------------------------------- 1 | events = new Collection($events); 13 | } 14 | 15 | /** @return \Projectionist\Domain\Services\EventWrapper|null */ 16 | public function next() 17 | { 18 | $event = $this->events->shift(); 19 | if ($event) { 20 | return new Identifiable($event); 21 | } 22 | return null; 23 | } 24 | } -------------------------------------------------------------------------------- /src/Domain/Services/ProjectorPositionLedger.php: -------------------------------------------------------------------------------- 1 | handlerFunctionName($this->className($event)); 10 | 11 | if (method_exists($projector, $method)) { 12 | $projector->$method($event); 13 | } 14 | } 15 | 16 | private function className($event) 17 | { 18 | $namespaces = explode('\\', get_class($event)); 19 | return last($namespaces); 20 | } 21 | 22 | private function handlerFunctionName(string $type): string 23 | { 24 | return "when".$type; 25 | } 26 | } -------------------------------------------------------------------------------- /tests/Unit/Strategy/EventHandler/ClassNameTest.php: -------------------------------------------------------------------------------- 1 | prophesize(BaseTestProjector::class); 12 | $event = new ThingHappened(''); 13 | 14 | $projector->whenThingHappened($event)->shouldBeCalled(); 15 | 16 | $event_handler = new \Projectionist\Infra\EventHandler\ClassName(); 17 | $event_handler->handle($event, $projector->reveal()); 18 | } 19 | } -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 12 | 13 | 14 | ./tests 15 | 16 | 17 | 18 | 19 | app/ 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /tests/Integration/Service/EventLog/EventStreamTest.php: -------------------------------------------------------------------------------- 1 | makeEventStream(); 13 | 14 | $event = $stream->next(); 15 | 16 | $this->assertInstanceOf(EventWrapper::class, $event); 17 | } 18 | 19 | public function test_returns_null_when_no_more_events() 20 | { 21 | $stream = $this->makeEventStream(); 22 | 23 | $stream->next(); 24 | 25 | $eventB = $stream->next(); 26 | 27 | $this->assertNull($eventB); 28 | } 29 | } -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | # PHP CircleCI 2.0 configuration file 2 | # 3 | # Check https://circleci.com/docs/2.0/language-php/ for more details 4 | # 5 | version: 2 6 | jobs: 7 | build: 8 | docker: 9 | # specify the version you desire here 10 | - image: tarrynn/circleci:latest 11 | 12 | working_directory: ~/repo 13 | 14 | steps: 15 | - checkout 16 | 17 | # Download and cache dependencies 18 | - restore_cache: 19 | keys: 20 | - v1-dependencies-{{ checksum "composer.json" }}x 21 | - v1-dependencies- 22 | 23 | - run: composer install -n --prefer-dist 24 | 25 | - run: /etc/init.d/redis-server restart 26 | 27 | - save_cache: 28 | paths: 29 | - ./vendor 30 | key: v1-dependencies-{{ checksum "composer.json" }} 31 | 32 | # run tests! 33 | - run: ./vendor/bin/phpunit -------------------------------------------------------------------------------- /tests/Unit/ValueObjects/ProjectorReferenceTest.php: -------------------------------------------------------------------------------- 1 | assertEquals(ProjectorMode::RUN_ONCE, $ref->mode); 15 | } 16 | 17 | public function test_gives_default_mode_if_none_set() 18 | { 19 | $ref = ProjectorReference::makeFromProjector(new NoModeProjector); 20 | 21 | $this->assertEquals(ProjectorMode::RUN_FROM_START, $ref->mode); 22 | } 23 | } -------------------------------------------------------------------------------- /src/Infra/EventLog/InMemory.php: -------------------------------------------------------------------------------- 1 | id()] = true; 16 | } 17 | 18 | public static function hasProjectedEvent(string $event_id): bool 19 | { 20 | return isset(static::$projected_events[$event_id]); 21 | } 22 | 23 | public static function projectedEvents() 24 | { 25 | return static::$projected_events; 26 | } 27 | 28 | public static function reset() 29 | { 30 | return static::$projected_events = []; 31 | } 32 | 33 | public static function version() 34 | { 35 | return static::VERSION; 36 | } 37 | } -------------------------------------------------------------------------------- /src/App/Config.php: -------------------------------------------------------------------------------- 1 | projectorPositionLedger = $projectorPositionLedger; 19 | $this->eventLog = $eventLog; 20 | $this->eventHandler = $eventHandler; 21 | } 22 | 23 | public function projectorPositionLedger(): ProjectorPositionLedger 24 | { 25 | return $this->projectorPositionLedger; 26 | } 27 | 28 | public function eventLog(): EventLog 29 | { 30 | return $this->eventLog; 31 | } 32 | 33 | public function eventHandler(): EventHandler 34 | { 35 | return $this->eventHandler; 36 | } 37 | } -------------------------------------------------------------------------------- /tests/Unit/Strategy/EventHandler/MetaTypePropertyTest.php: -------------------------------------------------------------------------------- 1 | prophesize(MetaTypeProjector::class); 13 | 14 | $snapshot = new class { 15 | public $event = 'event-data'; 16 | 17 | public function type(){ 18 | return 'type.of.event'; 19 | } 20 | }; 21 | 22 | $projector->when_type_of_event('event-data', $snapshot)->shouldBeCalled(); 23 | 24 | $event_handler = new MetaTypeProperty(); 25 | $event_handler->handle($snapshot, $projector->reveal()); 26 | } 27 | } 28 | 29 | interface MetaTypeProjector { 30 | public function when_type_of_event($event, $snapshot); 31 | } -------------------------------------------------------------------------------- /src/Domain/Services/ProjectorQueryable.php: -------------------------------------------------------------------------------- 1 | projectorPositionLedger = $projectorPositionLedger; 15 | $this->projectorReferences = $projectorReferences; 16 | } 17 | 18 | public function newOrBrokenProjectors(): ProjectorReferenceCollection 19 | { 20 | $projector_positions = $this->projectorPositionLedger->fetchCollection($this->projectorReferences); 21 | 22 | return $this->projectorReferences->extractNewOrFailedProjectors($projector_positions); 23 | } 24 | 25 | public function allProjectors(): ProjectorReferenceCollection 26 | { 27 | return $this->projectorReferences; 28 | } 29 | } -------------------------------------------------------------------------------- /src/Domain/ValueObjects/ProjectorStatus.php: -------------------------------------------------------------------------------- 1 | value = $value; 18 | } 19 | 20 | public function is(string $value): bool 21 | { 22 | return $this->value == $value; 23 | } 24 | 25 | public function __toString() 26 | { 27 | return $this->value; 28 | } 29 | 30 | public static function new(): ProjectorStatus 31 | { 32 | return new self(self::NEW); 33 | } 34 | 35 | public static function working(): ProjectorStatus 36 | { 37 | return new self(self::WORKING); 38 | } 39 | 40 | public static function broken(): ProjectorStatus 41 | { 42 | return new self(self::BROKEN); 43 | } 44 | 45 | public static function stalled(): ProjectorStatus 46 | { 47 | return new self(self::STALLED); 48 | } 49 | } -------------------------------------------------------------------------------- /tests/Unit/Fakes/ProjectorTest.php: -------------------------------------------------------------------------------- 1 | event = new ThingHappened(self::EVENT_ID); 16 | } 17 | 18 | public function test_can_check_if_fake_projector_got_event() 19 | { 20 | $projector_a = new RunFromStart(); 21 | $projector_b = new RunFromLaunch(); 22 | $projector_a->reset(); 23 | $projector_b->reset(); 24 | 25 | $projector_a->whenThingHappened($this->event); 26 | 27 | $this->assertTrue($projector_a->hasProjectedEvent(self::EVENT_ID)); 28 | $this->assertFalse($projector_b->hasProjectedEvent(self::EVENT_ID)); 29 | } 30 | 31 | public function test_can_reset_fake_projectors() 32 | { 33 | $projector_a = new RunFromStart(); 34 | 35 | $projector_a->whenThingHappened($this->event); 36 | 37 | $projector_a->reset(); 38 | 39 | $this->assertFalse($projector_a->hasProjectedEvent(self::EVENT_ID)); 40 | } 41 | } -------------------------------------------------------------------------------- /src/Infra/ProjectorPositionLedger/InMemory.php: -------------------------------------------------------------------------------- 1 | store = []; 15 | } 16 | 17 | public function clear() 18 | { 19 | $this->store = []; 20 | } 21 | 22 | public function store(ProjectorPosition $projectorPosition) 23 | { 24 | $ref = $projectorPosition->projector_reference; 25 | $key = $ref->class_path.'-'.$ref->version; 26 | $this->store[$key] = $projectorPosition; 27 | } 28 | 29 | public function fetch(ProjectorReference $projectorReference) 30 | { 31 | $key = $projectorReference->class_path.'-'.$projectorReference->version; 32 | if (!isset($this->store[$key])) { 33 | return null; 34 | } 35 | return $this->store[$key]; 36 | } 37 | 38 | public function fetchCollection(ProjectorReferenceCollection $references): ProjectorPositionCollection 39 | { 40 | $positions = array_map(function(ProjectorReference $ref) { 41 | return $this->fetch($ref); 42 | }, $references->toArray()); 43 | 44 | return new ProjectorPositionCollection(array_filter($positions)); 45 | } 46 | } -------------------------------------------------------------------------------- /src/Domain/Strategy/ProjectorSkipper.php: -------------------------------------------------------------------------------- 1 | projectorPositionLedger = $adapter->projectorPositionLedger(); 17 | $this->eventLog = $adapter->eventLog(); 18 | } 19 | 20 | public function skip(ProjectorReferenceCollection $projectorRefs) 21 | { 22 | $latest_event = $this->eventLog->latestEvent(); 23 | if ($latest_event == null) { 24 | return; 25 | } 26 | foreach ($projectorRefs as $projectorRef) { 27 | $this->skipProjectorToEvent($projectorRef, $latest_event); 28 | } 29 | } 30 | 31 | private function skipProjectorToEvent(ProjectorReference $projectorReference, EventWrapper $latest_event) 32 | { 33 | $projectorPosition = $this->projectorPositionLedger->fetch($projectorReference); 34 | if (!$projectorPosition) { 35 | $projectorPosition = ProjectorPosition::makeNewUnplayed($projectorReference); 36 | } 37 | if ($latest_event) { 38 | $projectorPosition = $projectorPosition->played($latest_event); 39 | } 40 | 41 | $this->projectorPositionLedger->store($projectorPosition); 42 | } 43 | } -------------------------------------------------------------------------------- /tests/Unit/ValueObjects/ProjectorPositionCollectionTest.php: -------------------------------------------------------------------------------- 1 | assertTrue($collection->hasReference($ref_1)); 23 | $this->assertTrue($collection->hasReference($ref_2)); 24 | $this->assertFalse($collection->hasReference($ref_1_bumped)); 25 | } 26 | 27 | public function test_will_not_allow_duplicate_references() 28 | { 29 | $this->expectException(\Exception::class); 30 | $this->expectExceptionMessage("Duplicate projector references, not allowed"); 31 | 32 | $ref_1 = ProjectorReference::makeFromProjector(new RunFromStart); 33 | new ProjectorPositionCollection([ 34 | ProjectorPosition::makeNewUnplayed($ref_1), 35 | ProjectorPosition::makeNewUnplayed($ref_1), 36 | ]); 37 | } 38 | } -------------------------------------------------------------------------------- /tests/Unit/ValueObjects/ProjectorReferenceCollectionTest.php: -------------------------------------------------------------------------------- 1 | expectException(\Exception::class); 14 | $this->expectExceptionMessage("Duplicate projector references, not allowed"); 15 | 16 | $ref_1 = ProjectorReference::makeFromProjector(new RunFromStart); 17 | new ProjectorReferenceCollection([$ref_1, $ref_1]); 18 | } 19 | 20 | public function test_excluding_by_mode() 21 | { 22 | $ref_1 = ProjectorReference::makeFromProjector(new RunFromStart); 23 | $ref_2 = ProjectorReference::makeFromProjector(new RunOnce); 24 | $collection = new ProjectorReferenceCollection([$ref_1, $ref_2]); 25 | 26 | $excluded = $collection->exclude(ProjectorMode::RUN_ONCE); 27 | 28 | $this->assertEquals([$ref_1], $excluded->all()); 29 | } 30 | 31 | public function test_extracting_by_mode() 32 | { 33 | $ref_1 = ProjectorReference::makeFromProjector(new RunFromStart); 34 | $ref_2 = ProjectorReference::makeFromProjector(new RunOnce); 35 | $collection = new ProjectorReferenceCollection([$ref_1, $ref_2]); 36 | 37 | $extracted = $collection->extract(ProjectorMode::RUN_ONCE); 38 | 39 | $this->assertEquals([$ref_2], $extracted->all()); 40 | } 41 | } -------------------------------------------------------------------------------- /src/Infra/ProjectorPositionLedger/Redis.php: -------------------------------------------------------------------------------- 1 | redis = $redis; 19 | } 20 | 21 | public function clear() 22 | { 23 | $this->redis->del([self::STORE]); 24 | } 25 | 26 | public function store(ProjectorPosition $projectorPosition) 27 | { 28 | $field = $projectorPosition->projector_reference->toString(); 29 | $value = serialize($projectorPosition); 30 | $this->redis->hset(self::STORE, $field, $value); 31 | } 32 | 33 | /** @return ProjectorPosition */ 34 | public function fetch(ProjectorReference $projectorReference) 35 | { 36 | $field = $projectorReference->toString(); 37 | $serialized = $this->redis->hget(self::STORE, $field); 38 | if (!$serialized) { 39 | return null; 40 | } 41 | return unserialize($serialized); 42 | } 43 | 44 | public function fetchCollection(ProjectorReferenceCollection $references): ProjectorPositionCollection 45 | { 46 | $fields = $references->toStrings(); 47 | 48 | $positionsSerialized = $this->redis->hmget(self::STORE, $fields); 49 | 50 | $positionsSerialized = array_filter($positionsSerialized); 51 | 52 | return new ProjectorPositionCollection(array_map(function($positionSerialized){ 53 | return unserialize($positionSerialized); 54 | }, $positionsSerialized)); 55 | } 56 | } -------------------------------------------------------------------------------- /benchmarks/DefinedSpeedTest.php: -------------------------------------------------------------------------------- 1 | runFunction1000Times(function(){ 26 | return true; 27 | }); 28 | var_dump("Simple true: $bool_total"); 29 | 30 | $defined_total = $this->runFunction1000Times(function(){ 31 | return defined(RunOnce::class."::MODE"); 32 | }); 33 | $defined_ratio = round($defined_total/$bool_total, 1); 34 | var_dump("Defined const: $defined_total ($defined_ratio)"); 35 | 36 | $undefined_total = $this->runFunction1000Times(function(){ 37 | return defined(NoModeProjector::class."::MODE"); 38 | }); 39 | $undefined_ratio = round($undefined_total/$bool_total, 1); 40 | var_dump("Undefined const: $undefined_total ($undefined_ratio)"); 41 | } 42 | 43 | private function runFunction1000Times(Closure $func): float 44 | { 45 | $total = 0; 46 | for ($i = 0; $i < 1000; $i++) { 47 | $start = microtime(true); 48 | if ($func()) { 49 | $end = microtime(true); 50 | } else { 51 | $end = microtime(true); 52 | } 53 | 54 | $total += $end - $start; 55 | } 56 | return $total; 57 | } 58 | } -------------------------------------------------------------------------------- /src/Projectionist.php: -------------------------------------------------------------------------------- 1 | adapter = $adapter; 19 | $this->projectorPlayer = new ProjectorPlayer($adapter); 20 | $this->projectorSkipper = new ProjectorSkipper($adapter); 21 | } 22 | 23 | public function boot(array $projectors) 24 | { 25 | $projectorRefs = ProjectorReferenceCollection::fromProjectors($projectors); 26 | 27 | $projectorQueryable = $this->makeQueryable($projectorRefs); 28 | 29 | $newProjectors = $projectorQueryable->newOrBrokenProjectors(); 30 | 31 | $playToNowProjectors = $newProjectors->exclude(ProjectorMode::RUN_FROM_LAUNCH); 32 | $this->projectorPlayer->boot($playToNowProjectors); 33 | 34 | $skipToNowProjectors = $newProjectors->extract(ProjectorMode::RUN_FROM_LAUNCH); 35 | $this->projectorSkipper->skip($skipToNowProjectors); 36 | } 37 | 38 | public function play(array $projectors) 39 | { 40 | $projectorRefs = ProjectorReferenceCollection::fromProjectors($projectors); 41 | 42 | $projectorQueryable = $this->makeQueryable($projectorRefs); 43 | 44 | $projectors = $projectorQueryable->allProjectors(); 45 | 46 | $activeProjectors = $projectors->exclude(ProjectorMode::RUN_ONCE); 47 | 48 | $this->projectorPlayer->play($activeProjectors); 49 | } 50 | 51 | private function makeQueryable(ProjectorReferenceCollection $projectorRefs): ProjectorQueryable 52 | { 53 | return new ProjectorQueryable($this->adapter->projectorPositionLedger(), $projectorRefs); 54 | } 55 | } -------------------------------------------------------------------------------- /tests/Unit/ValueObjects/ProjectorPositionTest.php: -------------------------------------------------------------------------------- 1 | assertTrue($reference->equals($actual->projector_reference)); 18 | $this->assertEmpty(0, $actual->processed_events); 19 | } 20 | 21 | public function test_marking_a_position_as_broken() 22 | { 23 | $ref = ProjectorReference::makeFromProjectorWithVersion(new RunOnce, 1); 24 | $processed_events = 2; 25 | $occurred_at = date('Y-m-d H:i:s'); 26 | $last_event_id = '6c040404-80fd-4a4d-98d6-547344d4873a'; 27 | $position = new ProjectorPosition($ref, $processed_events, $occurred_at, $last_event_id, ProjectorStatus::broken()); 28 | 29 | $broken = $position->broken(); 30 | 31 | $this->assertIsFailing($broken); 32 | $this->assertLastValidEventIdIsStillSet($last_event_id, $broken); 33 | } 34 | 35 | private function assertIsFailing(ProjectorPosition $actual) 36 | { 37 | $this->assertTrue($actual->isFailing()); 38 | } 39 | 40 | private function assertLastValidEventIdIsStillSet($expected, ProjectorPosition $actual) 41 | { 42 | $this->assertEquals($expected, $actual->last_position); 43 | } 44 | 45 | public function test_marking_an_event_as_processed() 46 | { 47 | $ref = ProjectorReference::makeFromProjectorWithVersion(new RunOnce, 1); 48 | $event_id = '6c040404-80fd-4a4d-98d6-547344d4873a'; 49 | $position = ProjectorPosition::makeNewUnplayed($ref); 50 | 51 | $position = $position->played(new Identifiable(new ThingHappened($event_id))); 52 | 53 | $this->assertEquals($event_id, $position->last_position); 54 | $this->assertEquals(1, $position->processed_events); 55 | } 56 | } -------------------------------------------------------------------------------- /src/Domain/ValueObjects/ProjectorReference.php: -------------------------------------------------------------------------------- 1 | projector = $projector; 33 | $this->class_path = empty($class_path) ? get_class($projector): $class_path; 34 | $this->version = $version; 35 | $this->mode = $this->mode($projector); 36 | } 37 | 38 | public function equals(ProjectorReference $reference) 39 | { 40 | return $this->class_path == $reference->class_path && $this->version == $reference->version; 41 | } 42 | 43 | public function toString() 44 | { 45 | return get_class($this->projector)."-".$this->version; 46 | } 47 | 48 | public function projector() 49 | { 50 | return $this->projector; 51 | } 52 | 53 | const DEFAULT_MODE = ProjectorMode::RUN_FROM_START; 54 | 55 | private function mode($projector) 56 | { 57 | return self::getConst($projector, 'MODE', self::DEFAULT_MODE); 58 | } 59 | 60 | const DEFAULT_VERSION = 1; 61 | 62 | private static function getConst($projector, string $name, $default) 63 | { 64 | $class = is_string($projector) 65 | ? $projector 66 | : get_class($projector); 67 | 68 | $name = "$class::$name"; 69 | if (defined($name)) { 70 | return constant($name); 71 | } 72 | 73 | return $default; 74 | } 75 | } -------------------------------------------------------------------------------- /src/Domain/ValueObjects/ProjectorPosition.php: -------------------------------------------------------------------------------- 1 | projector_reference = $projector_reference; 22 | $this->processed_events = $processed_events; 23 | $this->last_position = $last_position; 24 | $this->occurred_at = $occurred_at; 25 | $this->status = $status; 26 | } 27 | 28 | public static function makeNewUnplayed(ProjectorReference $projector_reference): ProjectorPosition 29 | { 30 | return new ProjectorPosition( 31 | $projector_reference, 32 | 0, 33 | '', 34 | '', 35 | ProjectorStatus::new() 36 | ); 37 | } 38 | 39 | public function played(EventWrapper $event): ProjectorPosition 40 | { 41 | $event_count = $this->processed_events + 1; 42 | 43 | return new ProjectorPosition( 44 | $this->projector_reference, 45 | $event_count, 46 | date('Y-m-d H:i:s'), 47 | $event->id(), 48 | ProjectorStatus::working() 49 | ); 50 | } 51 | 52 | public function broken(): ProjectorPosition 53 | { 54 | return new ProjectorPosition( 55 | $this->projector_reference, 56 | $this->processed_events, 57 | date('Y-m-d H:i:s'), 58 | $this->last_position, 59 | ProjectorStatus::broken() 60 | ); 61 | } 62 | 63 | public function stalled(): ProjectorPosition 64 | { 65 | return new ProjectorPosition( 66 | $this->projector_reference, 67 | $this->processed_events, 68 | date('Y-m-d H:i:s'), 69 | $this->last_position, 70 | ProjectorStatus::stalled() 71 | ); 72 | } 73 | 74 | public function isSame(ProjectorReference $current_projector) 75 | { 76 | return $this->projector_reference->equals($current_projector); 77 | } 78 | 79 | public function isFailing() 80 | { 81 | return $this->status->is(ProjectorStatus::BROKEN) || $this->status->is(ProjectorStatus::STALLED); 82 | } 83 | } -------------------------------------------------------------------------------- /src/Domain/ValueObjects/ProjectorPositionCollection.php: -------------------------------------------------------------------------------- 1 | projector_reference->toString(); 13 | }, $items); 14 | 15 | $unique_references = array_unique($references); 16 | 17 | if (count($unique_references) != count($references)) { 18 | throw new \Exception("Duplicate projector references, not allowed"); 19 | } 20 | } 21 | 22 | public function hasReference(ProjectorReference $projectorReference): bool 23 | { 24 | return $this->getByReference($projectorReference) != null; 25 | } 26 | 27 | /** 28 | * @param ProjectorReference $projectorReference 29 | * @return ProjectorPosition 30 | */ 31 | public function getByReference(ProjectorReference $projectorReference) 32 | { 33 | return $this->first(function(ProjectorPosition $position) use ($projectorReference){ 34 | return $position->projector_reference->equals($projectorReference); 35 | }); 36 | } 37 | 38 | public function references(): ProjectorReferenceCollection 39 | { 40 | return new ProjectorReferenceCollection(array_map(function(ProjectorPosition $pos){ 41 | return $pos->projector_reference; 42 | }, $this->all())); 43 | } 44 | 45 | public function filterOutFailing(): ProjectorPositionCollection 46 | { 47 | return $this->filter(function(ProjectorPosition $projectorPosition){ 48 | return $projectorPosition->isFailing() === false; 49 | }); 50 | } 51 | 52 | public function markUnbrokenAsStalled() 53 | { 54 | return $this->map(function(ProjectorPosition $position){ 55 | if ($position->isFailing()) { 56 | return $position; 57 | } 58 | return $position->stalled(); 59 | }); 60 | } 61 | 62 | public function groupByLastPosition(): Collection 63 | { 64 | $groupByPosition = []; 65 | foreach ($this as $position) { 66 | $groupByPosition[$position->last_position][] = $position; 67 | } 68 | 69 | return (new Collection($groupByPosition)) 70 | ->map(function($groupedPositions){ 71 | return new ProjectorPositionCollection($groupedPositions); 72 | }); 73 | } 74 | } -------------------------------------------------------------------------------- /src/Domain/ValueObjects/ProjectorReferenceCollection.php: -------------------------------------------------------------------------------- 1 | toString(); 24 | }, $items); 25 | 26 | $uniqueReferences = array_unique($references); 27 | 28 | if (count($uniqueReferences) != count($references)) { 29 | throw new \Exception("Duplicate projector references, not allowed"); 30 | } 31 | } 32 | 33 | public function extract(string $mode): ProjectorReferenceCollection 34 | { 35 | return $this->filter(function(ProjectorReference $projectorReference) use ($mode) { 36 | return $projectorReference->mode == $mode; 37 | })->values(); 38 | } 39 | 40 | public function exclude(string $mode): ProjectorReferenceCollection 41 | { 42 | return $this->filter(function(ProjectorReference $projectorReference) use ($mode) { 43 | return $projectorReference->mode != $mode; 44 | })->values(); 45 | } 46 | 47 | public function extractNewOrFailedProjectors(ProjectorPositionCollection $projectorPositions) 48 | { 49 | return $this->filter(function(ProjectorReference $projectorReference) use ($projectorPositions){ 50 | $projectorPosition = $projectorPositions->getByReference($projectorReference); 51 | if (!$projectorPosition) { 52 | return true; 53 | } 54 | return $projectorPosition->isFailing(); 55 | })->values(); 56 | } 57 | 58 | public function projectors(): array 59 | { 60 | return array_map(function(ProjectorReference $reference){ 61 | return $reference->projector(); 62 | }, $this->toArray()); 63 | } 64 | 65 | public function toStrings(): array 66 | { 67 | return array_map(function(ProjectorReference $reference){ 68 | return $reference->toString(); 69 | }, $this->toArray()); 70 | } 71 | } -------------------------------------------------------------------------------- /tests/Integration/Service/ProjectorPositionRepositoryTest.php: -------------------------------------------------------------------------------- 1 | repo = $this->makeRepository(); 22 | $this->repo->clear(); 23 | $this->ref = ProjectorReference::makeFromProjector(new RunFromStart); 24 | $this->position = ProjectorPosition::makeNewUnplayed($this->ref); 25 | } 26 | 27 | abstract protected function makeRepository(): ProjectorPositionLedger; 28 | 29 | public function test_fetching_unstored_returns_null() 30 | { 31 | $this->assertNull($this->repo->fetch($this->ref)); 32 | } 33 | 34 | public function test_can_fetch() 35 | { 36 | $this->assertNull($this->repo->fetch($this->ref)); 37 | 38 | $this->repo->store($this->position); 39 | 40 | $actual = $this->repo->fetch($this->ref); 41 | 42 | $this->assertEquals($this->position, $actual); 43 | } 44 | 45 | private function makePosition($projector) 46 | { 47 | return ProjectorPosition::makeNewUnplayed( 48 | ProjectorReference::makeFromProjector($projector) 49 | ); 50 | } 51 | 52 | public function test_can_get_all() 53 | { 54 | $pos_1 = $this->makePosition(new RunFromStart); 55 | $pos_2 = $this->makePosition(new RunFromLaunch); 56 | $pos_3 = $this->makePosition(new RunOnce); 57 | 58 | $this->repo->store($pos_1); 59 | $this->repo->store($pos_2); 60 | $this->repo->store($pos_3); 61 | 62 | $references = new ProjectorReferenceCollection([$pos_1->projector_reference, $pos_2->projector_reference, $pos_3->projector_reference]); 63 | 64 | $expected = new ProjectorPositionCollection([$pos_1, $pos_2, $pos_3]); 65 | 66 | $actual = $this->repo->fetchCollection($references); 67 | 68 | $this->assertEquals($expected, $actual); 69 | } 70 | 71 | public function test_stores_by_reference_and_version() 72 | { 73 | $pos_1 = $this->makePosition(new RunFromStart); 74 | $pos_1_bumped_version = ProjectorPosition::makeNewUnplayed( 75 | ProjectorReference::makeFromProjectorWithVersion(new RunFromStart, 2) 76 | ); 77 | 78 | $this->repo->store($pos_1); 79 | $this->repo->store($pos_1_bumped_version); 80 | 81 | $references = new ProjectorReferenceCollection([$pos_1->projector_reference, $pos_1_bumped_version->projector_reference]); 82 | 83 | $expected = new ProjectorPositionCollection([$pos_1, $pos_1_bumped_version]); 84 | 85 | $actual = $this->repo->fetchCollection($references); 86 | 87 | $this->assertEquals($expected, $actual); 88 | } 89 | } -------------------------------------------------------------------------------- /tests/Unit/Services/ProjectorQueryableTest.php: -------------------------------------------------------------------------------- 1 | repo = $this->prophesize(ProjectorPositionLedger::class); 26 | } 27 | 28 | private function makeQueryable(array $projectors): ProjectorQueryable 29 | { 30 | $references = ProjectorReferenceCollection::fromProjectors($projectors); 31 | return new ProjectorQueryable($this->repo->reveal(), $references); 32 | } 33 | 34 | public function test_registered_but_not_stored_projectors_are_considered_new() 35 | { 36 | $projector = new RunFromStart; 37 | $ref = ProjectorReference::makeFromProjector($projector); 38 | 39 | $references = new ProjectorReferenceCollection([$ref]); 40 | 41 | $this->repo->fetchCollection($references)->willReturn(new ProjectorPositionCollection([])); 42 | 43 | $actual = $this->makeQueryable([$projector])->newOrBrokenProjectors(); 44 | 45 | $this->assertEquals($references, $actual); 46 | } 47 | 48 | public function test_that_stored_projectors_are_not_considered_new() 49 | { 50 | $ref_1 = ProjectorReference::makeFromProjector(new RunFromStart); 51 | $ref_2 = ProjectorReference::makeFromProjector(new RunOnce); 52 | $ref_3 = ProjectorReference::makeFromProjector(new RunFromLaunch); 53 | 54 | $pos_1 = ProjectorPosition::makeNewUnplayed($ref_1); 55 | 56 | $projectors = [new RunFromStart, new RunOnce, new RunFromLaunch]; 57 | $references = ProjectorReferenceCollection::fromProjectors($projectors); 58 | 59 | $this->repo->fetchCollection($references)->willReturn(new ProjectorPositionCollection([$pos_1])); 60 | 61 | $expected = new ProjectorReferenceCollection([$ref_2, $ref_3]); 62 | 63 | $actual = $this->makeQueryable($projectors)->newOrBrokenProjectors(); 64 | 65 | $this->assertEquals($expected, $actual); 66 | } 67 | 68 | public function test_broken_projectors_are_returned() 69 | { 70 | $ref_1 = ProjectorReference::makeFromProjector(new RunFromStart); 71 | $pos_1 = ProjectorPosition::makeNewUnplayed($ref_1)->broken(); 72 | 73 | $references = new ProjectorReferenceCollection([$ref_1]); 74 | 75 | $this->repo->fetchCollection($references)->willReturn(new ProjectorPositionCollection([$pos_1])); 76 | 77 | $expected = new ProjectorReferenceCollection([$ref_1]); 78 | 79 | $actual = $this->makeQueryable([$ref_1->projector()])->newOrBrokenProjectors(); 80 | 81 | $this->assertEquals($expected, $actual); 82 | } 83 | 84 | public function test_projectors_with_a_higher_version_than_stored_are_considered_new() 85 | { 86 | $projector = new RunOnce; 87 | $ref = ProjectorReference::makeFromProjectorWithVersion($projector, 1); 88 | $ref_higher_version = ProjectorReference::makeFromProjectorWithVersion($projector, 2); 89 | 90 | $processed_events = 2; 91 | $occurred_at = date('Y-m-d H:i:s'); 92 | $last_event_id = '6c040404-80fd-4a4d-98d6-547344d4873a'; 93 | $pos_1 = new ProjectorPosition($ref, $processed_events, $occurred_at, $last_event_id, ProjectorStatus::broken()); 94 | 95 | $references = ProjectorReferenceCollection::fromProjectors([$projector]); 96 | 97 | $this->repo->fetchCollection($references)->willReturn(new ProjectorPositionCollection([$pos_1])); 98 | 99 | $expected = new ProjectorReferenceCollection([$ref_higher_version]); 100 | 101 | $actual = $this->makeQueryable([$projector])->newOrBrokenProjectors(); 102 | 103 | $this->assertEquals($expected, $actual); 104 | } 105 | } -------------------------------------------------------------------------------- /tests/Unit/Strategy/ProjectorPlayerTest.php: -------------------------------------------------------------------------------- 1 | prophesize(ProjectorPositionLedger::class); 27 | $event = new ThingHappened(''); 28 | $adapter = $this->makeAdapter($event_handler, $position_ledger->reveal(), [$event]); 29 | 30 | // Expectations 31 | $broken_projector = new BrokenProjector(); 32 | $ref = ProjectorReference::makeFromProjector($broken_projector); 33 | $projector_position = ProjectorPosition::makeNewUnplayed($ref); 34 | 35 | $position_ledger->fetch($ref)->willReturn(null); 36 | $position_ledger->store($projector_position->broken())->shouldBeCalled(); 37 | $this->expectException(ProjectorException::class); 38 | 39 | // Run 40 | $projector_player = new ProjectorPlayer($adapter); 41 | $projector_refs = ProjectorReferenceCollection::fromProjectors([$broken_projector]); 42 | $projector_player->play($projector_refs); 43 | } 44 | 45 | public function test_boot_attempts_to_play_broken_projectors() 46 | { 47 | $event_handler = $this->prophesize(EventHandler::class); 48 | $position_ledger = $this->prophesize(ProjectorPositionLedger::class); 49 | 50 | $projector = new RunFromStart; 51 | $ref = ProjectorReference::makeFromProjector($projector); 52 | $position = ProjectorPosition::makeNewUnplayed($ref)->broken(); 53 | 54 | $references = ProjectorReferenceCollection::fromProjectors([$projector]); 55 | 56 | $position_ledger->fetch($ref)->willReturn($position); 57 | $position_ledger->store(Argument::cetera())->shouldBeCalled(); 58 | $position_ledger->fetchCollection($references)->willReturn(new ProjectorPositionCollection([$position])); 59 | 60 | $event = new ThingHappened(''); 61 | 62 | $adapter = $this->makeAdapter($event_handler->reveal(), $position_ledger->reveal(), [$event]); 63 | 64 | $projector_player = new ProjectorPlayer($adapter); 65 | 66 | $projector_player->boot($references); 67 | 68 | $event_handler->handle(Argument::cetera())->shouldHaveBeenCalled(); 69 | } 70 | 71 | public function test_play_ignores_broken_projectors() 72 | { 73 | $player = $this->prophesize(EventHandler::class); 74 | $position_ledger = $this->prophesize(ProjectorPositionLedger::class); 75 | 76 | $ref = ProjectorReference::makeFromProjector(new BrokenProjector); 77 | $position = ProjectorPosition::makeNewUnplayed($ref)->broken(); 78 | $position_ledger->fetch($ref)->willReturn($position); 79 | 80 | $adapter = $this->makeAdapter($player->reveal(), $position_ledger->reveal()); 81 | 82 | $projector_player = new ProjectorPlayer($adapter); 83 | 84 | $refs = ProjectorReferenceCollection::fromProjectors([new BrokenProjector]); 85 | 86 | $projector_player->play($refs); 87 | 88 | $player->handle(Argument::cetera())->shouldNotHaveBeenCalled(); 89 | } 90 | 91 | private function makeAdapter(EventHandler $player, ProjectorPositionLedger $ledger, $events=[]): Config 92 | { 93 | $adapter = $this->prophesize(Config::class); 94 | 95 | $event_stream = new EventStream\InMemory($events); 96 | 97 | $event_log = $this->prophesize(EventLog::class); 98 | $event_log->latestEvent()->willReturn(new Identifiable(last($events))); 99 | $event_log->getStream("")->willReturn($event_stream); 100 | 101 | $adapter->eventHandler()->willReturn($player); 102 | $adapter->projectorPositionLedger()->willReturn($ledger); 103 | $adapter->eventLog()->willReturn($event_log); 104 | 105 | return $adapter->reveal(); 106 | } 107 | } -------------------------------------------------------------------------------- /src/Domain/Strategy/ProjectorPlayer.php: -------------------------------------------------------------------------------- 1 | projectorPositionLedger = $adapter->projectorPositionLedger(); 20 | $this->eventLog = $adapter->eventLog(); 21 | $this->eventHandler = $adapter->eventHandler(); 22 | } 23 | 24 | public function boot(ProjectorReferenceCollection $projector_references) 25 | { 26 | $positions = $this->getProjectorPositions($projector_references); 27 | 28 | $positions = $this->playProjectors($positions); 29 | 30 | if ($this->thereWasAFailure()) { 31 | $positions = $positions->markUnbrokenAsStalled(); 32 | } 33 | 34 | $this->storeProjectorPositions($positions); 35 | 36 | if ($this->thereWasAFailure()) { 37 | $this->reportFailure(); 38 | } 39 | } 40 | 41 | public function play(ProjectorReferenceCollection $projector_references) 42 | { 43 | $positions = $this->getProjectorPositions($projector_references)->filterOutFailing(); 44 | 45 | $positions = $this->playProjectors($positions); 46 | 47 | $this->storeProjectorPositions($positions); 48 | 49 | if ($this->thereWasAFailure()) { 50 | $this->reportFailure(); 51 | } 52 | } 53 | 54 | // TODO: Extract into it's own class/concept 55 | private function getProjectorPositions(ProjectorReferenceCollection $projector_references): ProjectorPositionCollection 56 | { 57 | return new ProjectorPositionCollection( 58 | array_map(function(ProjectorReference $ref){ 59 | $position = $this->projectorPositionLedger->fetch($ref); 60 | 61 | if ($position) { 62 | return $position; 63 | } 64 | 65 | return ProjectorPosition::makeNewUnplayed($ref); 66 | }, $projector_references->toArray()) 67 | ); 68 | } 69 | 70 | private function storeProjectorPositions(ProjectorPositionCollection $positions) 71 | { 72 | foreach ($positions as $position) { 73 | $this->projectorPositionLedger->store($position); 74 | } 75 | } 76 | 77 | private function playProjectors(ProjectorPositionCollection $positions): ProjectorPositionCollection 78 | { 79 | $groupedByPosition = $positions->groupByLastPosition(); 80 | 81 | $groupedByPosition = $groupedByPosition->map(function($groupedPositions, $last_position){ 82 | if ($this->thereWasAFailure()) { 83 | return $groupedPositions; 84 | } 85 | return $this->playProjectorsFromPosition($groupedPositions, $last_position); 86 | }); 87 | 88 | return new ProjectorPositionCollection($groupedByPosition->flatten()->toArray()); 89 | } 90 | 91 | private function playProjectorsFromPosition(ProjectorPositionCollection $positions, $last_position): ProjectorPositionCollection 92 | { 93 | $event_stream = $this->eventLog->getStream($last_position); 94 | 95 | while ($event = $event_stream->next()) { 96 | $positions = $positions->map(function(ProjectorPosition $position) use ($event) { 97 | return $this->playEventIntoProjector($event, $position); 98 | }); 99 | 100 | if ($this->thereWasAFailure()) { 101 | return $positions; 102 | } 103 | } 104 | return $positions; 105 | } 106 | 107 | private function playEventIntoProjector(EventWrapper $event, ProjectorPosition $projectorPosition): ProjectorPosition 108 | { 109 | if ($this->thereWasAFailure()) { 110 | return $projectorPosition; 111 | } 112 | $projector = $projectorPosition->projector_reference->projector(); 113 | try { 114 | $this->eventHandler->handle($event->wrappedEvent(), $projector); 115 | return $projectorPosition->played($event); 116 | } catch (\Throwable $t) { 117 | $this->catchFailure($t); 118 | return $projectorPosition->broken(); 119 | } 120 | } 121 | 122 | private $brokenProjectorException; 123 | 124 | private function thereWasAFailure() 125 | { 126 | return $this->brokenProjectorException != null; 127 | } 128 | 129 | private function reportFailure() 130 | { 131 | $exception = $this->brokenProjectorException; 132 | $this->brokenProjectorException = null; 133 | throw new ProjectorException( 134 | "A projector had an unexpected failure", 135 | $exception->getCode(), 136 | $exception 137 | ); 138 | } 139 | 140 | private function catchFailure(\Throwable $t) 141 | { 142 | $this->brokenProjectorException = $t; 143 | } 144 | } -------------------------------------------------------------------------------- /tests/Acceptance/ProjectionistPlayProjectorsTest.php: -------------------------------------------------------------------------------- 1 | make(); 32 | $config->projectorPositionLedger()->clear(); 33 | $this->projectionist = new Projectionist($config); 34 | 35 | $this->projector_position_ledger = $config->projectorPositionLedger(); 36 | $this->event_log = $config->eventLog(); 37 | $this->event_log->reset(); 38 | } 39 | 40 | const EVENT_1_ID = '94ae0b60-ddb4-4cf0-bb75-4b588fea3c3c'; 41 | const EVENT_2_ID = '359e43d8-025e-49ec-a017-3a99c1ce89ba'; 42 | 43 | private function seedEvent(string $event_id) 44 | { 45 | $event = new ThingHappened($event_id); 46 | $this->event_log->appendEvent($event); 47 | } 48 | 49 | public function test_plays_projectors_up_till_the_latest_event() 50 | { 51 | $this->seedEvent(self::EVENT_1_ID); 52 | $projectors = [new RunFromLaunch, new RunFromStart]; 53 | $projectorRefs = ProjectorReferenceCollection::fromProjectors($projectors); 54 | 55 | $this->projectionist->play($projectorRefs->projectors()); 56 | 57 | $stored_projector_positions = $this->projector_position_ledger->fetchCollection($projectorRefs); 58 | 59 | $this->assertProjectorsAreAtPosition($projectorRefs, self::EVENT_1_ID, $stored_projector_positions); 60 | } 61 | 62 | private function assertProjectorsAreAtPosition(ProjectorReferenceCollection $projectorRefs, string $expectedPosition, ProjectorPositionCollection $positions) 63 | { 64 | $this->assertCount(count($projectorRefs->projectors()), $positions); 65 | $this->assertProjectorsHaveProcessedEvent($projectorRefs, $expectedPosition); 66 | $positions->each(function(ProjectorPosition $position) use ($expectedPosition) { 67 | $this->assertEquals($expectedPosition, $position->last_position); 68 | $this->assertEquals(ProjectorStatus::working(), $position->status); 69 | }); 70 | } 71 | 72 | private function assertProjectorsHaveProcessedEvent(ProjectorReferenceCollection $projectorRefs, string $event_id) 73 | { 74 | foreach ($projectorRefs->projectors() as $projector) { 75 | $this->assertTrue($projector::hasProjectedEvent($event_id)); 76 | } 77 | } 78 | 79 | public function test_does_not_play_run_once_projectors() 80 | { 81 | $this->seedEvent(self::EVENT_1_ID); 82 | $projectors = [new RunFromLaunch, new RunFromStart, new RunOnce()]; 83 | $projectorRefs = ProjectorReferenceCollection::fromProjectors($projectors); 84 | 85 | $this->projectionist->play($projectorRefs->projectors()); 86 | 87 | $this->assertFalse(RunOnce::hasProjectedEvent(self::EVENT_1_ID)); 88 | } 89 | 90 | public function test_playing_a_broken_projector_fails_elegantly() 91 | { 92 | $this->seedEvent(self::EVENT_1_ID); 93 | $projectors = [new RunFromLaunch, new RunFromStart, new BrokenProjector()]; 94 | $projectorRefs = ProjectorReferenceCollection::fromProjectors($projectors); 95 | 96 | $this->expectException(ProjectorException::class); 97 | 98 | $this->projectionist->play($projectorRefs->projectors()); 99 | } 100 | 101 | public function test_playing_after_a_failure_continues_normally() 102 | { 103 | $this->seedEvent(self::EVENT_1_ID); 104 | 105 | $projectors = [new RunFromLaunch, new RunFromStart, new BrokenProjector()]; 106 | $projectorRefs = ProjectorReferenceCollection::fromProjectors($projectors); 107 | 108 | $first_play_failed = false; 109 | try { 110 | $this->projectionist->play($projectorRefs->projectors()); 111 | } catch (\Throwable $e) { 112 | $first_play_failed = true; 113 | } 114 | 115 | $this->assertTrue($first_play_failed); 116 | 117 | $this->seedEvent(self::EVENT_2_ID); 118 | 119 | $this->projectionist->play($projectorRefs->projectors()); 120 | 121 | $expectedProjectors = [new RunFromLaunch, new RunFromStart]; 122 | $expectedProjectorRefs = ProjectorReferenceCollection::fromProjectors($expectedProjectors); 123 | $stored_projector_positions = $this->projector_position_ledger->fetchCollection($expectedProjectorRefs); 124 | 125 | $this->assertProjectorsAreAtPosition($expectedProjectorRefs, self::EVENT_2_ID, $stored_projector_positions); 126 | } 127 | } -------------------------------------------------------------------------------- /tests/Acceptance/ProjectionistBootProjectorsTest.php: -------------------------------------------------------------------------------- 1 | make(); 37 | $this->eventLog = $config->eventLog(); 38 | $this->eventLog->reset(); 39 | $this->projectorPositionRepo = $config->projectorPositionLedger(); 40 | $this->projectionist = new Projectionist($config); 41 | 42 | $projectors = [new RunFromLaunch, new RunFromStart, new RunOnce]; 43 | $this->projectorRefs = ProjectorReferenceCollection::fromProjectors($projectors); 44 | 45 | $this->seedEvent(self::EVENT_ID_1); 46 | } 47 | 48 | private function seedEvent(string $event_id) 49 | { 50 | $event = new ThingHappened($event_id); 51 | $this->eventLog->appendEvent($event); 52 | } 53 | 54 | public function tests_boots_all_projectors_if_none_has_been_stored() 55 | { 56 | $this->assertEmpty($this->projectorPositionRepo->fetchCollection($this->projectorRefs)); 57 | 58 | $this->projectionist->boot($this->projectorRefs->projectors()); 59 | 60 | $stored_projector_positions = $this->projectorPositionRepo->fetchCollection($this->projectorRefs); 61 | 62 | $actual = $stored_projector_positions->references(); 63 | 64 | $this->assertEquals($this->projectorRefs, $actual); 65 | $this->assertProjectorsAreAtPosition($this->projectorRefs, self::EVENT_ID_1, $stored_projector_positions); 66 | } 67 | 68 | private function assertProjectorsAreAtPosition( 69 | ProjectorReferenceCollection $projectorRefs, 70 | string $expectedPosition, 71 | ProjectorPositionCollection $positions 72 | ) 73 | { 74 | $this->assertCount(count($projectorRefs->projectors()), $positions); 75 | $positions->each(function(ProjectorPosition $position) use ($expectedPosition) { 76 | $this->assertEquals($expectedPosition, $position->last_position); // TODO: Change to getMethod 77 | }); 78 | } 79 | 80 | public function test_boot_does_not_play_events_into_run_from_launch_projectors() 81 | { 82 | $this->projectionist->boot($this->projectorRefs->projectors()); 83 | 84 | $this->assertTrue(RunFromStart::hasProjectedEvent(self::EVENT_ID_1)); 85 | $this->assertTrue(RunOnce::hasProjectedEvent(self::EVENT_ID_1)); 86 | $this->assertFalse(RunFromLaunch::hasProjectedEvent(self::EVENT_ID_1)); 87 | } 88 | 89 | public function test_booting_a_broken_projectors_marks_other_projectors_as_stalled() 90 | { 91 | $runFromStart = new RunFromStart; 92 | $broken = new BrokenProjector; 93 | $runOnce = new RunOnce; 94 | 95 | $projectorRefs = ProjectorReferenceCollection::fromProjectors([ 96 | $runFromStart, 97 | $broken, 98 | $runOnce 99 | ]); 100 | 101 | $this->assertProjectionistFailsOnBoot($projectorRefs); 102 | 103 | $stored_projector_positions = $this->projectorPositionRepo->fetchCollection($projectorRefs); 104 | 105 | $runOncePos = $stored_projector_positions->getByReference( ProjectorReference::makeFromProjector($runOnce) ); 106 | $brokenPos = $stored_projector_positions->getByReference( ProjectorReference::makeFromProjector($broken) ); 107 | $runFromStartPos = $stored_projector_positions->getByReference( ProjectorReference::makeFromProjector($runFromStart) ); 108 | 109 | $this->assertProjectorIsBroken($brokenPos); 110 | $this->assertProjectorIsStalled($runOncePos); 111 | $this->assertProjectorIsStalled($runFromStartPos); 112 | } 113 | 114 | private function assertProjectorIsBroken(ProjectorPosition $position) 115 | { 116 | $this->assertTrue($position->status->is(ProjectorStatus::BROKEN), "Projector ".$position->projector_reference->class_path." should be broken"); 117 | } 118 | 119 | public function assertProjectorIsStalled(ProjectorPosition $position) 120 | { 121 | $this->assertTrue($position->status->is(ProjectorStatus::STALLED), "Projector ".$position->projector_reference->class_path." should be stalled"); 122 | } 123 | 124 | private function assertProjectionistFailsOnBoot(ProjectorReferenceCollection $projectorRefs) 125 | { 126 | $first_boot_failed = false; 127 | try { 128 | $this->projectionist->boot($projectorRefs->projectors()); 129 | } catch (ProjectorException $e) { 130 | $first_boot_failed = true; 131 | } 132 | 133 | $this->assertTrue($first_boot_failed); 134 | } 135 | 136 | public function test_booting_fails_if_a_projector_is_broken() 137 | { 138 | $projectorRefs = ProjectorReferenceCollection::fromProjectors([new BrokenProjector()]); 139 | 140 | $this->assertProjectionistFailsOnBoot($projectorRefs); 141 | 142 | $this->expectException(ProjectorException::class); 143 | 144 | $this->projectionist->boot($projectorRefs->projectors()); 145 | } 146 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # The Projectionist: Projector management and versioning system 2 | 3 | [![CircleCI](https://circleci.com/gh/barryosull/the-projectionist/tree/master.svg?style=svg)](https://circleci.com/gh/barryosull/the-projectionist/tree/master) 4 | 5 | If you are building an EventSourced/CQRS system in PHP, you need a solid system to handle building your projections. Enter the "projectionist". 6 | 7 | This is a library that makes it easy to consume events and manage the lifecycle of projectors in PHP, keeping track of where each projector is in the event stream. It is currently a **WIP** so I wouldn't advise using it right now, but feel free to have a look around (and read this readme, lots of details here). 8 | 9 | It's based on a lot of trial and error from building these kinds of systems, so my hope is that it will allow others to leapfrog us and gain from our mistakes. 10 | 11 | ![Projectionist in action](https://res.cloudinary.com/practicaldev/image/fetch/s--0Wje2n09--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://thepracticaldev.s3.amazonaws.com/i/ea3uvjpnhca5wokt6tnx.png) 12 | 13 | ## How it works 14 | The projectonist is given a collection of projectors. It can either boot or play these projectors. 15 | 16 | Booting projectors prepares new projectors for launch. If they're `run_from_launch`, they will just get set to the latest event, and no events will be played. Other projectors will be played up to the most recent event. This should be run as part of your release script, ensuring that all new projectors and played and up to date before you make the app live. 17 | 18 | Playing projectors takes all the active projectors that aren't `run_once`, and plays them to the latest event. This is intended to run in the background when the system is live, constantly listening for new events and attempting to apply them. 19 | 20 | ## How to use this library 21 | This how you create a projectionist. 22 | 23 | ```php 24 | // Define the config for the projectionist system 25 | $config = new Projectionist\App\ConfigFactory\InMemory(); 26 | 27 | // Create the projectionist 28 | $projectionist = new Projectionist($config); 29 | 30 | // Load all your projectors into an array of Projector references 31 | $projectors = [new RunFromLaunch, new RunFromStart]; 32 | 33 | // Boot the projectors 34 | $projectionist->boot($projectors); 35 | 36 | // Play the projectors 37 | $projectionist->play($projectors); 38 | ``` 39 | 40 | That's it. The tricky part is in the details though. To use this library, you must write integrations for your system. We don't know what how you've implemented your system, so instead we've made it easy to integrate with this system, no matter what your implementation. 41 | 42 | To do this, you have to implement the config for the projectonist. 43 | 44 | Config is an interface that outputs three adapters and one strategy, these also need to be implemented. 45 | 46 | - Adapters: 47 | - EventLog - Check if there are events, get the latest event, or get a stream of events after an event ID. 48 | - EventStream - Get events one at a time, until there are none left 49 | - EventWrapper - Wraps your event, you just need to implement how you get the id, so the projectionist can keep track of the projectors position. 50 | 51 | The adapters act as integration points to your application, allowing your system and the projectionist to work together, no matter what the implementation. 52 | 53 | You can also choose to override the default event handler by defining your own EventHandler Strategy. 54 | - Strategy: 55 | - EventHandler - Play an event into a projector. Simple to write, gives you full flexibility. 56 | 57 | It allows you define how your projectors work. By default it uses a class name based handler, which handle projectors with this handler style. 58 | 59 | ```php 60 | handlerFunctionName($this->className($event)); 85 | 86 | if (method_exists($projector, $method)) { 87 | $projector->$method($event); 88 | } 89 | } 90 | 91 | private function className($event) 92 | { 93 | $namespaces = explode('\\', get_class($event)); 94 | return last($namespaces); 95 | } 96 | 97 | private function handlerFunctionName(string $type): string 98 | { 99 | return "when".$type; 100 | } 101 | } 102 | ``` 103 | 104 | However, you can write your own version however you want. This means you're not stuck with the handler system we've implemented. 105 | 106 | ## Using a different storage engine for projector positions 107 | By default this system uses redis to keep track of projector positions. If you'd like to use another implementation, you'll need to write your own. This isn't too hard though, just create an adapter that implements the interface and passes the integration tests. 108 | You probably needs more detail, but the easiest way to figure out how to do this is look at the Redis implementation and the integration test for it. 109 | 110 | ## Modes 111 | Projectors tend to have three distinct modes, which control how each behaves when booted, or played. 112 | 1. `run_from_start`: Start at the first event and play as normal 113 | 3. `run_from_launch`: Start at most recent event, only play forward from there 114 | 2. `run_once`: Start at the first event, only run once 115 | 116 | These modes allow for various usecases. Most projectors will be `run_from_start`, which is the default (ie. you don't need to define a mode), while `run_from_launch` is useful when you only want the projector triggered by new events, not existing ones. `run_once` is useful for the opposite reason, only run through existing events, ignore new ones. 117 | 118 | These can be configured by add a `MODE` const to your projector, and setting the appropriate mode. 119 | ```php 120 | =5.3,<8.0-DEV" 93 | }, 94 | "require-dev": { 95 | "athletic/athletic": "~0.1.8", 96 | "ext-pdo": "*", 97 | "ext-phar": "*", 98 | "phpunit/phpunit": "~4.0", 99 | "squizlabs/php_codesniffer": "~2.0" 100 | }, 101 | "type": "library", 102 | "extra": { 103 | "branch-alias": { 104 | "dev-master": "1.0.x-dev" 105 | } 106 | }, 107 | "autoload": { 108 | "psr-4": { 109 | "Doctrine\\Instantiator\\": "src/Doctrine/Instantiator/" 110 | } 111 | }, 112 | "notification-url": "https://packagist.org/downloads/", 113 | "license": [ 114 | "MIT" 115 | ], 116 | "authors": [ 117 | { 118 | "name": "Marco Pivetta", 119 | "email": "ocramius@gmail.com", 120 | "homepage": "http://ocramius.github.com/" 121 | } 122 | ], 123 | "description": "A small, lightweight utility to instantiate objects in PHP without invoking their constructors", 124 | "homepage": "https://github.com/doctrine/instantiator", 125 | "keywords": [ 126 | "constructor", 127 | "instantiate" 128 | ], 129 | "time": "2015-06-14 21:17:01" 130 | }, 131 | { 132 | "name": "illuminate/container", 133 | "version": "v5.5.36", 134 | "source": { 135 | "type": "git", 136 | "url": "https://github.com/illuminate/container.git", 137 | "reference": "25821159f2bb12dc9e5720a4b13ea3372ffa8216" 138 | }, 139 | "dist": { 140 | "type": "zip", 141 | "url": "https://api.github.com/repos/illuminate/container/zipball/25821159f2bb12dc9e5720a4b13ea3372ffa8216", 142 | "reference": "25821159f2bb12dc9e5720a4b13ea3372ffa8216", 143 | "shasum": "" 144 | }, 145 | "require": { 146 | "illuminate/contracts": "5.5.*", 147 | "php": ">=7.0", 148 | "psr/container": "~1.0" 149 | }, 150 | "type": "library", 151 | "extra": { 152 | "branch-alias": { 153 | "dev-master": "5.5-dev" 154 | } 155 | }, 156 | "autoload": { 157 | "psr-4": { 158 | "Illuminate\\Container\\": "" 159 | } 160 | }, 161 | "notification-url": "https://packagist.org/downloads/", 162 | "license": [ 163 | "MIT" 164 | ], 165 | "authors": [ 166 | { 167 | "name": "Taylor Otwell", 168 | "email": "taylor@laravel.com" 169 | } 170 | ], 171 | "description": "The Illuminate Container package.", 172 | "homepage": "https://laravel.com", 173 | "time": "2018-01-19 17:58:33" 174 | }, 175 | { 176 | "name": "illuminate/contracts", 177 | "version": "v5.5.36", 178 | "source": { 179 | "type": "git", 180 | "url": "https://github.com/illuminate/contracts.git", 181 | "reference": "eb9a0171866fca0669c9acab6c7441e19b4694ca" 182 | }, 183 | "dist": { 184 | "type": "zip", 185 | "url": "https://api.github.com/repos/illuminate/contracts/zipball/eb9a0171866fca0669c9acab6c7441e19b4694ca", 186 | "reference": "eb9a0171866fca0669c9acab6c7441e19b4694ca", 187 | "shasum": "" 188 | }, 189 | "require": { 190 | "php": ">=7.0", 191 | "psr/container": "~1.0", 192 | "psr/simple-cache": "~1.0" 193 | }, 194 | "type": "library", 195 | "extra": { 196 | "branch-alias": { 197 | "dev-master": "5.5-dev" 198 | } 199 | }, 200 | "autoload": { 201 | "psr-4": { 202 | "Illuminate\\Contracts\\": "" 203 | } 204 | }, 205 | "notification-url": "https://packagist.org/downloads/", 206 | "license": [ 207 | "MIT" 208 | ], 209 | "authors": [ 210 | { 211 | "name": "Taylor Otwell", 212 | "email": "taylor@laravel.com" 213 | } 214 | ], 215 | "description": "The Illuminate Contracts package.", 216 | "homepage": "https://laravel.com", 217 | "time": "2018-01-19 17:59:58" 218 | }, 219 | { 220 | "name": "illuminate/support", 221 | "version": "v5.5.36", 222 | "source": { 223 | "type": "git", 224 | "url": "https://github.com/illuminate/support.git", 225 | "reference": "c10c2fcabe8cb907ebb2dcae4f50e7c83a872055" 226 | }, 227 | "dist": { 228 | "type": "zip", 229 | "url": "https://api.github.com/repos/illuminate/support/zipball/c10c2fcabe8cb907ebb2dcae4f50e7c83a872055", 230 | "reference": "c10c2fcabe8cb907ebb2dcae4f50e7c83a872055", 231 | "shasum": "" 232 | }, 233 | "require": { 234 | "doctrine/inflector": "~1.1", 235 | "ext-mbstring": "*", 236 | "illuminate/contracts": "5.5.*", 237 | "nesbot/carbon": "^1.20", 238 | "php": ">=7.0" 239 | }, 240 | "replace": { 241 | "tightenco/collect": "<5.5.33" 242 | }, 243 | "suggest": { 244 | "illuminate/filesystem": "Required to use the composer class (5.2.*).", 245 | "symfony/process": "Required to use the composer class (~3.3).", 246 | "symfony/var-dumper": "Required to use the dd function (~3.3)." 247 | }, 248 | "type": "library", 249 | "extra": { 250 | "branch-alias": { 251 | "dev-master": "5.5-dev" 252 | } 253 | }, 254 | "autoload": { 255 | "psr-4": { 256 | "Illuminate\\Support\\": "" 257 | }, 258 | "files": [ 259 | "helpers.php" 260 | ] 261 | }, 262 | "notification-url": "https://packagist.org/downloads/", 263 | "license": [ 264 | "MIT" 265 | ], 266 | "authors": [ 267 | { 268 | "name": "Taylor Otwell", 269 | "email": "taylor@laravel.com" 270 | } 271 | ], 272 | "description": "The Illuminate Support package.", 273 | "homepage": "https://laravel.com", 274 | "time": "2018-02-14 15:12:47" 275 | }, 276 | { 277 | "name": "myclabs/deep-copy", 278 | "version": "1.7.0", 279 | "source": { 280 | "type": "git", 281 | "url": "https://github.com/myclabs/DeepCopy.git", 282 | "reference": "3b8a3a99ba1f6a3952ac2747d989303cbd6b7a3e" 283 | }, 284 | "dist": { 285 | "type": "zip", 286 | "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/3b8a3a99ba1f6a3952ac2747d989303cbd6b7a3e", 287 | "reference": "3b8a3a99ba1f6a3952ac2747d989303cbd6b7a3e", 288 | "shasum": "" 289 | }, 290 | "require": { 291 | "php": "^5.6 || ^7.0" 292 | }, 293 | "require-dev": { 294 | "doctrine/collections": "^1.0", 295 | "doctrine/common": "^2.6", 296 | "phpunit/phpunit": "^4.1" 297 | }, 298 | "type": "library", 299 | "autoload": { 300 | "psr-4": { 301 | "DeepCopy\\": "src/DeepCopy/" 302 | }, 303 | "files": [ 304 | "src/DeepCopy/deep_copy.php" 305 | ] 306 | }, 307 | "notification-url": "https://packagist.org/downloads/", 308 | "license": [ 309 | "MIT" 310 | ], 311 | "description": "Create deep copies (clones) of your objects", 312 | "keywords": [ 313 | "clone", 314 | "copy", 315 | "duplicate", 316 | "object", 317 | "object graph" 318 | ], 319 | "time": "2017-10-19 19:58:43" 320 | }, 321 | { 322 | "name": "nesbot/carbon", 323 | "version": "1.23.0", 324 | "source": { 325 | "type": "git", 326 | "url": "https://github.com/briannesbitt/Carbon.git", 327 | "reference": "4a874a39b2b00d7e0146cd46fab6f47c41ce9e65" 328 | }, 329 | "dist": { 330 | "type": "zip", 331 | "url": "https://api.github.com/repos/briannesbitt/Carbon/zipball/4a874a39b2b00d7e0146cd46fab6f47c41ce9e65", 332 | "reference": "4a874a39b2b00d7e0146cd46fab6f47c41ce9e65", 333 | "shasum": "" 334 | }, 335 | "require": { 336 | "php": ">=5.3.0", 337 | "symfony/translation": "~2.6 || ~3.0 || ~4.0" 338 | }, 339 | "require-dev": { 340 | "friendsofphp/php-cs-fixer": "~2", 341 | "phpunit/phpunit": "^4.8.35 || ^5.7" 342 | }, 343 | "type": "library", 344 | "extra": { 345 | "branch-alias": { 346 | "dev-master": "1.23-dev" 347 | } 348 | }, 349 | "autoload": { 350 | "psr-4": { 351 | "Carbon\\": "src/Carbon/" 352 | } 353 | }, 354 | "notification-url": "https://packagist.org/downloads/", 355 | "license": [ 356 | "MIT" 357 | ], 358 | "authors": [ 359 | { 360 | "name": "Brian Nesbitt", 361 | "email": "brian@nesbot.com", 362 | "homepage": "http://nesbot.com" 363 | } 364 | ], 365 | "description": "A simple API extension for DateTime.", 366 | "homepage": "http://carbon.nesbot.com", 367 | "keywords": [ 368 | "date", 369 | "datetime", 370 | "time" 371 | ], 372 | "time": "2018-02-28 09:22:05" 373 | }, 374 | { 375 | "name": "phar-io/manifest", 376 | "version": "1.0.1", 377 | "source": { 378 | "type": "git", 379 | "url": "https://github.com/phar-io/manifest.git", 380 | "reference": "2df402786ab5368a0169091f61a7c1e0eb6852d0" 381 | }, 382 | "dist": { 383 | "type": "zip", 384 | "url": "https://api.github.com/repos/phar-io/manifest/zipball/2df402786ab5368a0169091f61a7c1e0eb6852d0", 385 | "reference": "2df402786ab5368a0169091f61a7c1e0eb6852d0", 386 | "shasum": "" 387 | }, 388 | "require": { 389 | "ext-dom": "*", 390 | "ext-phar": "*", 391 | "phar-io/version": "^1.0.1", 392 | "php": "^5.6 || ^7.0" 393 | }, 394 | "type": "library", 395 | "extra": { 396 | "branch-alias": { 397 | "dev-master": "1.0.x-dev" 398 | } 399 | }, 400 | "autoload": { 401 | "classmap": [ 402 | "src/" 403 | ] 404 | }, 405 | "notification-url": "https://packagist.org/downloads/", 406 | "license": [ 407 | "BSD-3-Clause" 408 | ], 409 | "authors": [ 410 | { 411 | "name": "Arne Blankerts", 412 | "email": "arne@blankerts.de", 413 | "role": "Developer" 414 | }, 415 | { 416 | "name": "Sebastian Heuer", 417 | "email": "sebastian@phpeople.de", 418 | "role": "Developer" 419 | }, 420 | { 421 | "name": "Sebastian Bergmann", 422 | "email": "sebastian@phpunit.de", 423 | "role": "Developer" 424 | } 425 | ], 426 | "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)", 427 | "time": "2017-03-05 18:14:27" 428 | }, 429 | { 430 | "name": "phar-io/version", 431 | "version": "1.0.1", 432 | "source": { 433 | "type": "git", 434 | "url": "https://github.com/phar-io/version.git", 435 | "reference": "a70c0ced4be299a63d32fa96d9281d03e94041df" 436 | }, 437 | "dist": { 438 | "type": "zip", 439 | "url": "https://api.github.com/repos/phar-io/version/zipball/a70c0ced4be299a63d32fa96d9281d03e94041df", 440 | "reference": "a70c0ced4be299a63d32fa96d9281d03e94041df", 441 | "shasum": "" 442 | }, 443 | "require": { 444 | "php": "^5.6 || ^7.0" 445 | }, 446 | "type": "library", 447 | "autoload": { 448 | "classmap": [ 449 | "src/" 450 | ] 451 | }, 452 | "notification-url": "https://packagist.org/downloads/", 453 | "license": [ 454 | "BSD-3-Clause" 455 | ], 456 | "authors": [ 457 | { 458 | "name": "Arne Blankerts", 459 | "email": "arne@blankerts.de", 460 | "role": "Developer" 461 | }, 462 | { 463 | "name": "Sebastian Heuer", 464 | "email": "sebastian@phpeople.de", 465 | "role": "Developer" 466 | }, 467 | { 468 | "name": "Sebastian Bergmann", 469 | "email": "sebastian@phpunit.de", 470 | "role": "Developer" 471 | } 472 | ], 473 | "description": "Library for handling version information and constraints", 474 | "time": "2017-03-05 17:38:23" 475 | }, 476 | { 477 | "name": "phpdocumentor/reflection-common", 478 | "version": "1.0.1", 479 | "source": { 480 | "type": "git", 481 | "url": "https://github.com/phpDocumentor/ReflectionCommon.git", 482 | "reference": "21bdeb5f65d7ebf9f43b1b25d404f87deab5bfb6" 483 | }, 484 | "dist": { 485 | "type": "zip", 486 | "url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/21bdeb5f65d7ebf9f43b1b25d404f87deab5bfb6", 487 | "reference": "21bdeb5f65d7ebf9f43b1b25d404f87deab5bfb6", 488 | "shasum": "" 489 | }, 490 | "require": { 491 | "php": ">=5.5" 492 | }, 493 | "require-dev": { 494 | "phpunit/phpunit": "^4.6" 495 | }, 496 | "type": "library", 497 | "extra": { 498 | "branch-alias": { 499 | "dev-master": "1.0.x-dev" 500 | } 501 | }, 502 | "autoload": { 503 | "psr-4": { 504 | "phpDocumentor\\Reflection\\": [ 505 | "src" 506 | ] 507 | } 508 | }, 509 | "notification-url": "https://packagist.org/downloads/", 510 | "license": [ 511 | "MIT" 512 | ], 513 | "authors": [ 514 | { 515 | "name": "Jaap van Otterdijk", 516 | "email": "opensource@ijaap.nl" 517 | } 518 | ], 519 | "description": "Common reflection classes used by phpdocumentor to reflect the code structure", 520 | "homepage": "http://www.phpdoc.org", 521 | "keywords": [ 522 | "FQSEN", 523 | "phpDocumentor", 524 | "phpdoc", 525 | "reflection", 526 | "static analysis" 527 | ], 528 | "time": "2017-09-11 18:02:19" 529 | }, 530 | { 531 | "name": "phpdocumentor/reflection-docblock", 532 | "version": "4.3.0", 533 | "source": { 534 | "type": "git", 535 | "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", 536 | "reference": "94fd0001232e47129dd3504189fa1c7225010d08" 537 | }, 538 | "dist": { 539 | "type": "zip", 540 | "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/94fd0001232e47129dd3504189fa1c7225010d08", 541 | "reference": "94fd0001232e47129dd3504189fa1c7225010d08", 542 | "shasum": "" 543 | }, 544 | "require": { 545 | "php": "^7.0", 546 | "phpdocumentor/reflection-common": "^1.0.0", 547 | "phpdocumentor/type-resolver": "^0.4.0", 548 | "webmozart/assert": "^1.0" 549 | }, 550 | "require-dev": { 551 | "doctrine/instantiator": "~1.0.5", 552 | "mockery/mockery": "^1.0", 553 | "phpunit/phpunit": "^6.4" 554 | }, 555 | "type": "library", 556 | "extra": { 557 | "branch-alias": { 558 | "dev-master": "4.x-dev" 559 | } 560 | }, 561 | "autoload": { 562 | "psr-4": { 563 | "phpDocumentor\\Reflection\\": [ 564 | "src/" 565 | ] 566 | } 567 | }, 568 | "notification-url": "https://packagist.org/downloads/", 569 | "license": [ 570 | "MIT" 571 | ], 572 | "authors": [ 573 | { 574 | "name": "Mike van Riel", 575 | "email": "me@mikevanriel.com" 576 | } 577 | ], 578 | "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.", 579 | "time": "2017-11-30 07:14:17" 580 | }, 581 | { 582 | "name": "phpdocumentor/type-resolver", 583 | "version": "0.4.0", 584 | "source": { 585 | "type": "git", 586 | "url": "https://github.com/phpDocumentor/TypeResolver.git", 587 | "reference": "9c977708995954784726e25d0cd1dddf4e65b0f7" 588 | }, 589 | "dist": { 590 | "type": "zip", 591 | "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/9c977708995954784726e25d0cd1dddf4e65b0f7", 592 | "reference": "9c977708995954784726e25d0cd1dddf4e65b0f7", 593 | "shasum": "" 594 | }, 595 | "require": { 596 | "php": "^5.5 || ^7.0", 597 | "phpdocumentor/reflection-common": "^1.0" 598 | }, 599 | "require-dev": { 600 | "mockery/mockery": "^0.9.4", 601 | "phpunit/phpunit": "^5.2||^4.8.24" 602 | }, 603 | "type": "library", 604 | "extra": { 605 | "branch-alias": { 606 | "dev-master": "1.0.x-dev" 607 | } 608 | }, 609 | "autoload": { 610 | "psr-4": { 611 | "phpDocumentor\\Reflection\\": [ 612 | "src/" 613 | ] 614 | } 615 | }, 616 | "notification-url": "https://packagist.org/downloads/", 617 | "license": [ 618 | "MIT" 619 | ], 620 | "authors": [ 621 | { 622 | "name": "Mike van Riel", 623 | "email": "me@mikevanriel.com" 624 | } 625 | ], 626 | "time": "2017-07-14 14:27:02" 627 | }, 628 | { 629 | "name": "phpspec/prophecy", 630 | "version": "1.7.5", 631 | "source": { 632 | "type": "git", 633 | "url": "https://github.com/phpspec/prophecy.git", 634 | "reference": "dfd6be44111a7c41c2e884a336cc4f461b3b2401" 635 | }, 636 | "dist": { 637 | "type": "zip", 638 | "url": "https://api.github.com/repos/phpspec/prophecy/zipball/dfd6be44111a7c41c2e884a336cc4f461b3b2401", 639 | "reference": "dfd6be44111a7c41c2e884a336cc4f461b3b2401", 640 | "shasum": "" 641 | }, 642 | "require": { 643 | "doctrine/instantiator": "^1.0.2", 644 | "php": "^5.3|^7.0", 645 | "phpdocumentor/reflection-docblock": "^2.0|^3.0.2|^4.0", 646 | "sebastian/comparator": "^1.1|^2.0", 647 | "sebastian/recursion-context": "^1.0|^2.0|^3.0" 648 | }, 649 | "require-dev": { 650 | "phpspec/phpspec": "^2.5|^3.2", 651 | "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.5" 652 | }, 653 | "type": "library", 654 | "extra": { 655 | "branch-alias": { 656 | "dev-master": "1.7.x-dev" 657 | } 658 | }, 659 | "autoload": { 660 | "psr-0": { 661 | "Prophecy\\": "src/" 662 | } 663 | }, 664 | "notification-url": "https://packagist.org/downloads/", 665 | "license": [ 666 | "MIT" 667 | ], 668 | "authors": [ 669 | { 670 | "name": "Konstantin Kudryashov", 671 | "email": "ever.zet@gmail.com", 672 | "homepage": "http://everzet.com" 673 | }, 674 | { 675 | "name": "Marcello Duarte", 676 | "email": "marcello.duarte@gmail.com" 677 | } 678 | ], 679 | "description": "Highly opinionated mocking framework for PHP 5.3+", 680 | "homepage": "https://github.com/phpspec/prophecy", 681 | "keywords": [ 682 | "Double", 683 | "Dummy", 684 | "fake", 685 | "mock", 686 | "spy", 687 | "stub" 688 | ], 689 | "time": "2018-02-19 10:16:54" 690 | }, 691 | { 692 | "name": "phpunit/php-code-coverage", 693 | "version": "5.3.0", 694 | "source": { 695 | "type": "git", 696 | "url": "https://github.com/sebastianbergmann/php-code-coverage.git", 697 | "reference": "661f34d0bd3f1a7225ef491a70a020ad23a057a1" 698 | }, 699 | "dist": { 700 | "type": "zip", 701 | "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/661f34d0bd3f1a7225ef491a70a020ad23a057a1", 702 | "reference": "661f34d0bd3f1a7225ef491a70a020ad23a057a1", 703 | "shasum": "" 704 | }, 705 | "require": { 706 | "ext-dom": "*", 707 | "ext-xmlwriter": "*", 708 | "php": "^7.0", 709 | "phpunit/php-file-iterator": "^1.4.2", 710 | "phpunit/php-text-template": "^1.2.1", 711 | "phpunit/php-token-stream": "^2.0.1", 712 | "sebastian/code-unit-reverse-lookup": "^1.0.1", 713 | "sebastian/environment": "^3.0", 714 | "sebastian/version": "^2.0.1", 715 | "theseer/tokenizer": "^1.1" 716 | }, 717 | "require-dev": { 718 | "phpunit/phpunit": "^6.0" 719 | }, 720 | "suggest": { 721 | "ext-xdebug": "^2.5.5" 722 | }, 723 | "type": "library", 724 | "extra": { 725 | "branch-alias": { 726 | "dev-master": "5.3.x-dev" 727 | } 728 | }, 729 | "autoload": { 730 | "classmap": [ 731 | "src/" 732 | ] 733 | }, 734 | "notification-url": "https://packagist.org/downloads/", 735 | "license": [ 736 | "BSD-3-Clause" 737 | ], 738 | "authors": [ 739 | { 740 | "name": "Sebastian Bergmann", 741 | "email": "sebastian@phpunit.de", 742 | "role": "lead" 743 | } 744 | ], 745 | "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.", 746 | "homepage": "https://github.com/sebastianbergmann/php-code-coverage", 747 | "keywords": [ 748 | "coverage", 749 | "testing", 750 | "xunit" 751 | ], 752 | "time": "2017-12-06 09:29:45" 753 | }, 754 | { 755 | "name": "phpunit/php-file-iterator", 756 | "version": "1.4.5", 757 | "source": { 758 | "type": "git", 759 | "url": "https://github.com/sebastianbergmann/php-file-iterator.git", 760 | "reference": "730b01bc3e867237eaac355e06a36b85dd93a8b4" 761 | }, 762 | "dist": { 763 | "type": "zip", 764 | "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/730b01bc3e867237eaac355e06a36b85dd93a8b4", 765 | "reference": "730b01bc3e867237eaac355e06a36b85dd93a8b4", 766 | "shasum": "" 767 | }, 768 | "require": { 769 | "php": ">=5.3.3" 770 | }, 771 | "type": "library", 772 | "extra": { 773 | "branch-alias": { 774 | "dev-master": "1.4.x-dev" 775 | } 776 | }, 777 | "autoload": { 778 | "classmap": [ 779 | "src/" 780 | ] 781 | }, 782 | "notification-url": "https://packagist.org/downloads/", 783 | "license": [ 784 | "BSD-3-Clause" 785 | ], 786 | "authors": [ 787 | { 788 | "name": "Sebastian Bergmann", 789 | "email": "sb@sebastian-bergmann.de", 790 | "role": "lead" 791 | } 792 | ], 793 | "description": "FilterIterator implementation that filters files based on a list of suffixes.", 794 | "homepage": "https://github.com/sebastianbergmann/php-file-iterator/", 795 | "keywords": [ 796 | "filesystem", 797 | "iterator" 798 | ], 799 | "time": "2017-11-27 13:52:08" 800 | }, 801 | { 802 | "name": "phpunit/php-text-template", 803 | "version": "1.2.1", 804 | "source": { 805 | "type": "git", 806 | "url": "https://github.com/sebastianbergmann/php-text-template.git", 807 | "reference": "31f8b717e51d9a2afca6c9f046f5d69fc27c8686" 808 | }, 809 | "dist": { 810 | "type": "zip", 811 | "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/31f8b717e51d9a2afca6c9f046f5d69fc27c8686", 812 | "reference": "31f8b717e51d9a2afca6c9f046f5d69fc27c8686", 813 | "shasum": "" 814 | }, 815 | "require": { 816 | "php": ">=5.3.3" 817 | }, 818 | "type": "library", 819 | "autoload": { 820 | "classmap": [ 821 | "src/" 822 | ] 823 | }, 824 | "notification-url": "https://packagist.org/downloads/", 825 | "license": [ 826 | "BSD-3-Clause" 827 | ], 828 | "authors": [ 829 | { 830 | "name": "Sebastian Bergmann", 831 | "email": "sebastian@phpunit.de", 832 | "role": "lead" 833 | } 834 | ], 835 | "description": "Simple template engine.", 836 | "homepage": "https://github.com/sebastianbergmann/php-text-template/", 837 | "keywords": [ 838 | "template" 839 | ], 840 | "time": "2015-06-21 13:50:34" 841 | }, 842 | { 843 | "name": "phpunit/php-timer", 844 | "version": "1.0.9", 845 | "source": { 846 | "type": "git", 847 | "url": "https://github.com/sebastianbergmann/php-timer.git", 848 | "reference": "3dcf38ca72b158baf0bc245e9184d3fdffa9c46f" 849 | }, 850 | "dist": { 851 | "type": "zip", 852 | "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/3dcf38ca72b158baf0bc245e9184d3fdffa9c46f", 853 | "reference": "3dcf38ca72b158baf0bc245e9184d3fdffa9c46f", 854 | "shasum": "" 855 | }, 856 | "require": { 857 | "php": "^5.3.3 || ^7.0" 858 | }, 859 | "require-dev": { 860 | "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.0" 861 | }, 862 | "type": "library", 863 | "extra": { 864 | "branch-alias": { 865 | "dev-master": "1.0-dev" 866 | } 867 | }, 868 | "autoload": { 869 | "classmap": [ 870 | "src/" 871 | ] 872 | }, 873 | "notification-url": "https://packagist.org/downloads/", 874 | "license": [ 875 | "BSD-3-Clause" 876 | ], 877 | "authors": [ 878 | { 879 | "name": "Sebastian Bergmann", 880 | "email": "sb@sebastian-bergmann.de", 881 | "role": "lead" 882 | } 883 | ], 884 | "description": "Utility class for timing", 885 | "homepage": "https://github.com/sebastianbergmann/php-timer/", 886 | "keywords": [ 887 | "timer" 888 | ], 889 | "time": "2017-02-26 11:10:40" 890 | }, 891 | { 892 | "name": "phpunit/php-token-stream", 893 | "version": "2.0.2", 894 | "source": { 895 | "type": "git", 896 | "url": "https://github.com/sebastianbergmann/php-token-stream.git", 897 | "reference": "791198a2c6254db10131eecfe8c06670700904db" 898 | }, 899 | "dist": { 900 | "type": "zip", 901 | "url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/791198a2c6254db10131eecfe8c06670700904db", 902 | "reference": "791198a2c6254db10131eecfe8c06670700904db", 903 | "shasum": "" 904 | }, 905 | "require": { 906 | "ext-tokenizer": "*", 907 | "php": "^7.0" 908 | }, 909 | "require-dev": { 910 | "phpunit/phpunit": "^6.2.4" 911 | }, 912 | "type": "library", 913 | "extra": { 914 | "branch-alias": { 915 | "dev-master": "2.0-dev" 916 | } 917 | }, 918 | "autoload": { 919 | "classmap": [ 920 | "src/" 921 | ] 922 | }, 923 | "notification-url": "https://packagist.org/downloads/", 924 | "license": [ 925 | "BSD-3-Clause" 926 | ], 927 | "authors": [ 928 | { 929 | "name": "Sebastian Bergmann", 930 | "email": "sebastian@phpunit.de" 931 | } 932 | ], 933 | "description": "Wrapper around PHP's tokenizer extension.", 934 | "homepage": "https://github.com/sebastianbergmann/php-token-stream/", 935 | "keywords": [ 936 | "tokenizer" 937 | ], 938 | "time": "2017-11-27 05:48:46" 939 | }, 940 | { 941 | "name": "phpunit/phpunit", 942 | "version": "6.5.7", 943 | "source": { 944 | "type": "git", 945 | "url": "https://github.com/sebastianbergmann/phpunit.git", 946 | "reference": "6bd77b57707c236833d2b57b968e403df060c9d9" 947 | }, 948 | "dist": { 949 | "type": "zip", 950 | "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/6bd77b57707c236833d2b57b968e403df060c9d9", 951 | "reference": "6bd77b57707c236833d2b57b968e403df060c9d9", 952 | "shasum": "" 953 | }, 954 | "require": { 955 | "ext-dom": "*", 956 | "ext-json": "*", 957 | "ext-libxml": "*", 958 | "ext-mbstring": "*", 959 | "ext-xml": "*", 960 | "myclabs/deep-copy": "^1.6.1", 961 | "phar-io/manifest": "^1.0.1", 962 | "phar-io/version": "^1.0", 963 | "php": "^7.0", 964 | "phpspec/prophecy": "^1.7", 965 | "phpunit/php-code-coverage": "^5.3", 966 | "phpunit/php-file-iterator": "^1.4.3", 967 | "phpunit/php-text-template": "^1.2.1", 968 | "phpunit/php-timer": "^1.0.9", 969 | "phpunit/phpunit-mock-objects": "^5.0.5", 970 | "sebastian/comparator": "^2.1", 971 | "sebastian/diff": "^2.0", 972 | "sebastian/environment": "^3.1", 973 | "sebastian/exporter": "^3.1", 974 | "sebastian/global-state": "^2.0", 975 | "sebastian/object-enumerator": "^3.0.3", 976 | "sebastian/resource-operations": "^1.0", 977 | "sebastian/version": "^2.0.1" 978 | }, 979 | "conflict": { 980 | "phpdocumentor/reflection-docblock": "3.0.2", 981 | "phpunit/dbunit": "<3.0" 982 | }, 983 | "require-dev": { 984 | "ext-pdo": "*" 985 | }, 986 | "suggest": { 987 | "ext-xdebug": "*", 988 | "phpunit/php-invoker": "^1.1" 989 | }, 990 | "bin": [ 991 | "phpunit" 992 | ], 993 | "type": "library", 994 | "extra": { 995 | "branch-alias": { 996 | "dev-master": "6.5.x-dev" 997 | } 998 | }, 999 | "autoload": { 1000 | "classmap": [ 1001 | "src/" 1002 | ] 1003 | }, 1004 | "notification-url": "https://packagist.org/downloads/", 1005 | "license": [ 1006 | "BSD-3-Clause" 1007 | ], 1008 | "authors": [ 1009 | { 1010 | "name": "Sebastian Bergmann", 1011 | "email": "sebastian@phpunit.de", 1012 | "role": "lead" 1013 | } 1014 | ], 1015 | "description": "The PHP Unit Testing framework.", 1016 | "homepage": "https://phpunit.de/", 1017 | "keywords": [ 1018 | "phpunit", 1019 | "testing", 1020 | "xunit" 1021 | ], 1022 | "time": "2018-02-26 07:01:09" 1023 | }, 1024 | { 1025 | "name": "phpunit/phpunit-mock-objects", 1026 | "version": "5.0.6", 1027 | "source": { 1028 | "type": "git", 1029 | "url": "https://github.com/sebastianbergmann/phpunit-mock-objects.git", 1030 | "reference": "33fd41a76e746b8fa96d00b49a23dadfa8334cdf" 1031 | }, 1032 | "dist": { 1033 | "type": "zip", 1034 | "url": "https://api.github.com/repos/sebastianbergmann/phpunit-mock-objects/zipball/33fd41a76e746b8fa96d00b49a23dadfa8334cdf", 1035 | "reference": "33fd41a76e746b8fa96d00b49a23dadfa8334cdf", 1036 | "shasum": "" 1037 | }, 1038 | "require": { 1039 | "doctrine/instantiator": "^1.0.5", 1040 | "php": "^7.0", 1041 | "phpunit/php-text-template": "^1.2.1", 1042 | "sebastian/exporter": "^3.1" 1043 | }, 1044 | "conflict": { 1045 | "phpunit/phpunit": "<6.0" 1046 | }, 1047 | "require-dev": { 1048 | "phpunit/phpunit": "^6.5" 1049 | }, 1050 | "suggest": { 1051 | "ext-soap": "*" 1052 | }, 1053 | "type": "library", 1054 | "extra": { 1055 | "branch-alias": { 1056 | "dev-master": "5.0.x-dev" 1057 | } 1058 | }, 1059 | "autoload": { 1060 | "classmap": [ 1061 | "src/" 1062 | ] 1063 | }, 1064 | "notification-url": "https://packagist.org/downloads/", 1065 | "license": [ 1066 | "BSD-3-Clause" 1067 | ], 1068 | "authors": [ 1069 | { 1070 | "name": "Sebastian Bergmann", 1071 | "email": "sebastian@phpunit.de", 1072 | "role": "lead" 1073 | } 1074 | ], 1075 | "description": "Mock Object library for PHPUnit", 1076 | "homepage": "https://github.com/sebastianbergmann/phpunit-mock-objects/", 1077 | "keywords": [ 1078 | "mock", 1079 | "xunit" 1080 | ], 1081 | "time": "2018-01-06 05:45:45" 1082 | }, 1083 | { 1084 | "name": "predis/predis", 1085 | "version": "v1.1.1", 1086 | "source": { 1087 | "type": "git", 1088 | "url": "https://github.com/nrk/predis.git", 1089 | "reference": "f0210e38881631afeafb56ab43405a92cafd9fd1" 1090 | }, 1091 | "dist": { 1092 | "type": "zip", 1093 | "url": "https://api.github.com/repos/nrk/predis/zipball/f0210e38881631afeafb56ab43405a92cafd9fd1", 1094 | "reference": "f0210e38881631afeafb56ab43405a92cafd9fd1", 1095 | "shasum": "" 1096 | }, 1097 | "require": { 1098 | "php": ">=5.3.9" 1099 | }, 1100 | "require-dev": { 1101 | "phpunit/phpunit": "~4.8" 1102 | }, 1103 | "suggest": { 1104 | "ext-curl": "Allows access to Webdis when paired with phpiredis", 1105 | "ext-phpiredis": "Allows faster serialization and deserialization of the Redis protocol" 1106 | }, 1107 | "type": "library", 1108 | "autoload": { 1109 | "psr-4": { 1110 | "Predis\\": "src/" 1111 | } 1112 | }, 1113 | "notification-url": "https://packagist.org/downloads/", 1114 | "license": [ 1115 | "MIT" 1116 | ], 1117 | "authors": [ 1118 | { 1119 | "name": "Daniele Alessandri", 1120 | "email": "suppakilla@gmail.com", 1121 | "homepage": "http://clorophilla.net" 1122 | } 1123 | ], 1124 | "description": "Flexible and feature-complete Redis client for PHP and HHVM", 1125 | "homepage": "http://github.com/nrk/predis", 1126 | "keywords": [ 1127 | "nosql", 1128 | "predis", 1129 | "redis" 1130 | ], 1131 | "time": "2016-06-16 16:22:20" 1132 | }, 1133 | { 1134 | "name": "psr/container", 1135 | "version": "1.0.0", 1136 | "source": { 1137 | "type": "git", 1138 | "url": "https://github.com/php-fig/container.git", 1139 | "reference": "b7ce3b176482dbbc1245ebf52b181af44c2cf55f" 1140 | }, 1141 | "dist": { 1142 | "type": "zip", 1143 | "url": "https://api.github.com/repos/php-fig/container/zipball/b7ce3b176482dbbc1245ebf52b181af44c2cf55f", 1144 | "reference": "b7ce3b176482dbbc1245ebf52b181af44c2cf55f", 1145 | "shasum": "" 1146 | }, 1147 | "require": { 1148 | "php": ">=5.3.0" 1149 | }, 1150 | "type": "library", 1151 | "extra": { 1152 | "branch-alias": { 1153 | "dev-master": "1.0.x-dev" 1154 | } 1155 | }, 1156 | "autoload": { 1157 | "psr-4": { 1158 | "Psr\\Container\\": "src/" 1159 | } 1160 | }, 1161 | "notification-url": "https://packagist.org/downloads/", 1162 | "license": [ 1163 | "MIT" 1164 | ], 1165 | "authors": [ 1166 | { 1167 | "name": "PHP-FIG", 1168 | "homepage": "http://www.php-fig.org/" 1169 | } 1170 | ], 1171 | "description": "Common Container Interface (PHP FIG PSR-11)", 1172 | "homepage": "https://github.com/php-fig/container", 1173 | "keywords": [ 1174 | "PSR-11", 1175 | "container", 1176 | "container-interface", 1177 | "container-interop", 1178 | "psr" 1179 | ], 1180 | "time": "2017-02-14 16:28:37" 1181 | }, 1182 | { 1183 | "name": "psr/simple-cache", 1184 | "version": "1.0.0", 1185 | "source": { 1186 | "type": "git", 1187 | "url": "https://github.com/php-fig/simple-cache.git", 1188 | "reference": "753fa598e8f3b9966c886fe13f370baa45ef0e24" 1189 | }, 1190 | "dist": { 1191 | "type": "zip", 1192 | "url": "https://api.github.com/repos/php-fig/simple-cache/zipball/753fa598e8f3b9966c886fe13f370baa45ef0e24", 1193 | "reference": "753fa598e8f3b9966c886fe13f370baa45ef0e24", 1194 | "shasum": "" 1195 | }, 1196 | "require": { 1197 | "php": ">=5.3.0" 1198 | }, 1199 | "type": "library", 1200 | "extra": { 1201 | "branch-alias": { 1202 | "dev-master": "1.0.x-dev" 1203 | } 1204 | }, 1205 | "autoload": { 1206 | "psr-4": { 1207 | "Psr\\SimpleCache\\": "src/" 1208 | } 1209 | }, 1210 | "notification-url": "https://packagist.org/downloads/", 1211 | "license": [ 1212 | "MIT" 1213 | ], 1214 | "authors": [ 1215 | { 1216 | "name": "PHP-FIG", 1217 | "homepage": "http://www.php-fig.org/" 1218 | } 1219 | ], 1220 | "description": "Common interfaces for simple caching", 1221 | "keywords": [ 1222 | "cache", 1223 | "caching", 1224 | "psr", 1225 | "psr-16", 1226 | "simple-cache" 1227 | ], 1228 | "time": "2017-01-02 13:31:39" 1229 | }, 1230 | { 1231 | "name": "sebastian/code-unit-reverse-lookup", 1232 | "version": "1.0.1", 1233 | "source": { 1234 | "type": "git", 1235 | "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git", 1236 | "reference": "4419fcdb5eabb9caa61a27c7a1db532a6b55dd18" 1237 | }, 1238 | "dist": { 1239 | "type": "zip", 1240 | "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/4419fcdb5eabb9caa61a27c7a1db532a6b55dd18", 1241 | "reference": "4419fcdb5eabb9caa61a27c7a1db532a6b55dd18", 1242 | "shasum": "" 1243 | }, 1244 | "require": { 1245 | "php": "^5.6 || ^7.0" 1246 | }, 1247 | "require-dev": { 1248 | "phpunit/phpunit": "^5.7 || ^6.0" 1249 | }, 1250 | "type": "library", 1251 | "extra": { 1252 | "branch-alias": { 1253 | "dev-master": "1.0.x-dev" 1254 | } 1255 | }, 1256 | "autoload": { 1257 | "classmap": [ 1258 | "src/" 1259 | ] 1260 | }, 1261 | "notification-url": "https://packagist.org/downloads/", 1262 | "license": [ 1263 | "BSD-3-Clause" 1264 | ], 1265 | "authors": [ 1266 | { 1267 | "name": "Sebastian Bergmann", 1268 | "email": "sebastian@phpunit.de" 1269 | } 1270 | ], 1271 | "description": "Looks up which function or method a line of code belongs to", 1272 | "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/", 1273 | "time": "2017-03-04 06:30:41" 1274 | }, 1275 | { 1276 | "name": "sebastian/comparator", 1277 | "version": "2.1.3", 1278 | "source": { 1279 | "type": "git", 1280 | "url": "https://github.com/sebastianbergmann/comparator.git", 1281 | "reference": "34369daee48eafb2651bea869b4b15d75ccc35f9" 1282 | }, 1283 | "dist": { 1284 | "type": "zip", 1285 | "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/34369daee48eafb2651bea869b4b15d75ccc35f9", 1286 | "reference": "34369daee48eafb2651bea869b4b15d75ccc35f9", 1287 | "shasum": "" 1288 | }, 1289 | "require": { 1290 | "php": "^7.0", 1291 | "sebastian/diff": "^2.0 || ^3.0", 1292 | "sebastian/exporter": "^3.1" 1293 | }, 1294 | "require-dev": { 1295 | "phpunit/phpunit": "^6.4" 1296 | }, 1297 | "type": "library", 1298 | "extra": { 1299 | "branch-alias": { 1300 | "dev-master": "2.1.x-dev" 1301 | } 1302 | }, 1303 | "autoload": { 1304 | "classmap": [ 1305 | "src/" 1306 | ] 1307 | }, 1308 | "notification-url": "https://packagist.org/downloads/", 1309 | "license": [ 1310 | "BSD-3-Clause" 1311 | ], 1312 | "authors": [ 1313 | { 1314 | "name": "Jeff Welch", 1315 | "email": "whatthejeff@gmail.com" 1316 | }, 1317 | { 1318 | "name": "Volker Dusch", 1319 | "email": "github@wallbash.com" 1320 | }, 1321 | { 1322 | "name": "Bernhard Schussek", 1323 | "email": "bschussek@2bepublished.at" 1324 | }, 1325 | { 1326 | "name": "Sebastian Bergmann", 1327 | "email": "sebastian@phpunit.de" 1328 | } 1329 | ], 1330 | "description": "Provides the functionality to compare PHP values for equality", 1331 | "homepage": "https://github.com/sebastianbergmann/comparator", 1332 | "keywords": [ 1333 | "comparator", 1334 | "compare", 1335 | "equality" 1336 | ], 1337 | "time": "2018-02-01 13:46:46" 1338 | }, 1339 | { 1340 | "name": "sebastian/diff", 1341 | "version": "2.0.1", 1342 | "source": { 1343 | "type": "git", 1344 | "url": "https://github.com/sebastianbergmann/diff.git", 1345 | "reference": "347c1d8b49c5c3ee30c7040ea6fc446790e6bddd" 1346 | }, 1347 | "dist": { 1348 | "type": "zip", 1349 | "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/347c1d8b49c5c3ee30c7040ea6fc446790e6bddd", 1350 | "reference": "347c1d8b49c5c3ee30c7040ea6fc446790e6bddd", 1351 | "shasum": "" 1352 | }, 1353 | "require": { 1354 | "php": "^7.0" 1355 | }, 1356 | "require-dev": { 1357 | "phpunit/phpunit": "^6.2" 1358 | }, 1359 | "type": "library", 1360 | "extra": { 1361 | "branch-alias": { 1362 | "dev-master": "2.0-dev" 1363 | } 1364 | }, 1365 | "autoload": { 1366 | "classmap": [ 1367 | "src/" 1368 | ] 1369 | }, 1370 | "notification-url": "https://packagist.org/downloads/", 1371 | "license": [ 1372 | "BSD-3-Clause" 1373 | ], 1374 | "authors": [ 1375 | { 1376 | "name": "Kore Nordmann", 1377 | "email": "mail@kore-nordmann.de" 1378 | }, 1379 | { 1380 | "name": "Sebastian Bergmann", 1381 | "email": "sebastian@phpunit.de" 1382 | } 1383 | ], 1384 | "description": "Diff implementation", 1385 | "homepage": "https://github.com/sebastianbergmann/diff", 1386 | "keywords": [ 1387 | "diff" 1388 | ], 1389 | "time": "2017-08-03 08:09:46" 1390 | }, 1391 | { 1392 | "name": "sebastian/environment", 1393 | "version": "3.1.0", 1394 | "source": { 1395 | "type": "git", 1396 | "url": "https://github.com/sebastianbergmann/environment.git", 1397 | "reference": "cd0871b3975fb7fc44d11314fd1ee20925fce4f5" 1398 | }, 1399 | "dist": { 1400 | "type": "zip", 1401 | "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/cd0871b3975fb7fc44d11314fd1ee20925fce4f5", 1402 | "reference": "cd0871b3975fb7fc44d11314fd1ee20925fce4f5", 1403 | "shasum": "" 1404 | }, 1405 | "require": { 1406 | "php": "^7.0" 1407 | }, 1408 | "require-dev": { 1409 | "phpunit/phpunit": "^6.1" 1410 | }, 1411 | "type": "library", 1412 | "extra": { 1413 | "branch-alias": { 1414 | "dev-master": "3.1.x-dev" 1415 | } 1416 | }, 1417 | "autoload": { 1418 | "classmap": [ 1419 | "src/" 1420 | ] 1421 | }, 1422 | "notification-url": "https://packagist.org/downloads/", 1423 | "license": [ 1424 | "BSD-3-Clause" 1425 | ], 1426 | "authors": [ 1427 | { 1428 | "name": "Sebastian Bergmann", 1429 | "email": "sebastian@phpunit.de" 1430 | } 1431 | ], 1432 | "description": "Provides functionality to handle HHVM/PHP environments", 1433 | "homepage": "http://www.github.com/sebastianbergmann/environment", 1434 | "keywords": [ 1435 | "Xdebug", 1436 | "environment", 1437 | "hhvm" 1438 | ], 1439 | "time": "2017-07-01 08:51:00" 1440 | }, 1441 | { 1442 | "name": "sebastian/exporter", 1443 | "version": "3.1.0", 1444 | "source": { 1445 | "type": "git", 1446 | "url": "https://github.com/sebastianbergmann/exporter.git", 1447 | "reference": "234199f4528de6d12aaa58b612e98f7d36adb937" 1448 | }, 1449 | "dist": { 1450 | "type": "zip", 1451 | "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/234199f4528de6d12aaa58b612e98f7d36adb937", 1452 | "reference": "234199f4528de6d12aaa58b612e98f7d36adb937", 1453 | "shasum": "" 1454 | }, 1455 | "require": { 1456 | "php": "^7.0", 1457 | "sebastian/recursion-context": "^3.0" 1458 | }, 1459 | "require-dev": { 1460 | "ext-mbstring": "*", 1461 | "phpunit/phpunit": "^6.0" 1462 | }, 1463 | "type": "library", 1464 | "extra": { 1465 | "branch-alias": { 1466 | "dev-master": "3.1.x-dev" 1467 | } 1468 | }, 1469 | "autoload": { 1470 | "classmap": [ 1471 | "src/" 1472 | ] 1473 | }, 1474 | "notification-url": "https://packagist.org/downloads/", 1475 | "license": [ 1476 | "BSD-3-Clause" 1477 | ], 1478 | "authors": [ 1479 | { 1480 | "name": "Jeff Welch", 1481 | "email": "whatthejeff@gmail.com" 1482 | }, 1483 | { 1484 | "name": "Volker Dusch", 1485 | "email": "github@wallbash.com" 1486 | }, 1487 | { 1488 | "name": "Bernhard Schussek", 1489 | "email": "bschussek@2bepublished.at" 1490 | }, 1491 | { 1492 | "name": "Sebastian Bergmann", 1493 | "email": "sebastian@phpunit.de" 1494 | }, 1495 | { 1496 | "name": "Adam Harvey", 1497 | "email": "aharvey@php.net" 1498 | } 1499 | ], 1500 | "description": "Provides the functionality to export PHP variables for visualization", 1501 | "homepage": "http://www.github.com/sebastianbergmann/exporter", 1502 | "keywords": [ 1503 | "export", 1504 | "exporter" 1505 | ], 1506 | "time": "2017-04-03 13:19:02" 1507 | }, 1508 | { 1509 | "name": "sebastian/global-state", 1510 | "version": "2.0.0", 1511 | "source": { 1512 | "type": "git", 1513 | "url": "https://github.com/sebastianbergmann/global-state.git", 1514 | "reference": "e8ba02eed7bbbb9e59e43dedd3dddeff4a56b0c4" 1515 | }, 1516 | "dist": { 1517 | "type": "zip", 1518 | "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/e8ba02eed7bbbb9e59e43dedd3dddeff4a56b0c4", 1519 | "reference": "e8ba02eed7bbbb9e59e43dedd3dddeff4a56b0c4", 1520 | "shasum": "" 1521 | }, 1522 | "require": { 1523 | "php": "^7.0" 1524 | }, 1525 | "require-dev": { 1526 | "phpunit/phpunit": "^6.0" 1527 | }, 1528 | "suggest": { 1529 | "ext-uopz": "*" 1530 | }, 1531 | "type": "library", 1532 | "extra": { 1533 | "branch-alias": { 1534 | "dev-master": "2.0-dev" 1535 | } 1536 | }, 1537 | "autoload": { 1538 | "classmap": [ 1539 | "src/" 1540 | ] 1541 | }, 1542 | "notification-url": "https://packagist.org/downloads/", 1543 | "license": [ 1544 | "BSD-3-Clause" 1545 | ], 1546 | "authors": [ 1547 | { 1548 | "name": "Sebastian Bergmann", 1549 | "email": "sebastian@phpunit.de" 1550 | } 1551 | ], 1552 | "description": "Snapshotting of global state", 1553 | "homepage": "http://www.github.com/sebastianbergmann/global-state", 1554 | "keywords": [ 1555 | "global state" 1556 | ], 1557 | "time": "2017-04-27 15:39:26" 1558 | }, 1559 | { 1560 | "name": "sebastian/object-enumerator", 1561 | "version": "3.0.3", 1562 | "source": { 1563 | "type": "git", 1564 | "url": "https://github.com/sebastianbergmann/object-enumerator.git", 1565 | "reference": "7cfd9e65d11ffb5af41198476395774d4c8a84c5" 1566 | }, 1567 | "dist": { 1568 | "type": "zip", 1569 | "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/7cfd9e65d11ffb5af41198476395774d4c8a84c5", 1570 | "reference": "7cfd9e65d11ffb5af41198476395774d4c8a84c5", 1571 | "shasum": "" 1572 | }, 1573 | "require": { 1574 | "php": "^7.0", 1575 | "sebastian/object-reflector": "^1.1.1", 1576 | "sebastian/recursion-context": "^3.0" 1577 | }, 1578 | "require-dev": { 1579 | "phpunit/phpunit": "^6.0" 1580 | }, 1581 | "type": "library", 1582 | "extra": { 1583 | "branch-alias": { 1584 | "dev-master": "3.0.x-dev" 1585 | } 1586 | }, 1587 | "autoload": { 1588 | "classmap": [ 1589 | "src/" 1590 | ] 1591 | }, 1592 | "notification-url": "https://packagist.org/downloads/", 1593 | "license": [ 1594 | "BSD-3-Clause" 1595 | ], 1596 | "authors": [ 1597 | { 1598 | "name": "Sebastian Bergmann", 1599 | "email": "sebastian@phpunit.de" 1600 | } 1601 | ], 1602 | "description": "Traverses array structures and object graphs to enumerate all referenced objects", 1603 | "homepage": "https://github.com/sebastianbergmann/object-enumerator/", 1604 | "time": "2017-08-03 12:35:26" 1605 | }, 1606 | { 1607 | "name": "sebastian/object-reflector", 1608 | "version": "1.1.1", 1609 | "source": { 1610 | "type": "git", 1611 | "url": "https://github.com/sebastianbergmann/object-reflector.git", 1612 | "reference": "773f97c67f28de00d397be301821b06708fca0be" 1613 | }, 1614 | "dist": { 1615 | "type": "zip", 1616 | "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/773f97c67f28de00d397be301821b06708fca0be", 1617 | "reference": "773f97c67f28de00d397be301821b06708fca0be", 1618 | "shasum": "" 1619 | }, 1620 | "require": { 1621 | "php": "^7.0" 1622 | }, 1623 | "require-dev": { 1624 | "phpunit/phpunit": "^6.0" 1625 | }, 1626 | "type": "library", 1627 | "extra": { 1628 | "branch-alias": { 1629 | "dev-master": "1.1-dev" 1630 | } 1631 | }, 1632 | "autoload": { 1633 | "classmap": [ 1634 | "src/" 1635 | ] 1636 | }, 1637 | "notification-url": "https://packagist.org/downloads/", 1638 | "license": [ 1639 | "BSD-3-Clause" 1640 | ], 1641 | "authors": [ 1642 | { 1643 | "name": "Sebastian Bergmann", 1644 | "email": "sebastian@phpunit.de" 1645 | } 1646 | ], 1647 | "description": "Allows reflection of object attributes, including inherited and non-public ones", 1648 | "homepage": "https://github.com/sebastianbergmann/object-reflector/", 1649 | "time": "2017-03-29 09:07:27" 1650 | }, 1651 | { 1652 | "name": "sebastian/recursion-context", 1653 | "version": "3.0.0", 1654 | "source": { 1655 | "type": "git", 1656 | "url": "https://github.com/sebastianbergmann/recursion-context.git", 1657 | "reference": "5b0cd723502bac3b006cbf3dbf7a1e3fcefe4fa8" 1658 | }, 1659 | "dist": { 1660 | "type": "zip", 1661 | "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/5b0cd723502bac3b006cbf3dbf7a1e3fcefe4fa8", 1662 | "reference": "5b0cd723502bac3b006cbf3dbf7a1e3fcefe4fa8", 1663 | "shasum": "" 1664 | }, 1665 | "require": { 1666 | "php": "^7.0" 1667 | }, 1668 | "require-dev": { 1669 | "phpunit/phpunit": "^6.0" 1670 | }, 1671 | "type": "library", 1672 | "extra": { 1673 | "branch-alias": { 1674 | "dev-master": "3.0.x-dev" 1675 | } 1676 | }, 1677 | "autoload": { 1678 | "classmap": [ 1679 | "src/" 1680 | ] 1681 | }, 1682 | "notification-url": "https://packagist.org/downloads/", 1683 | "license": [ 1684 | "BSD-3-Clause" 1685 | ], 1686 | "authors": [ 1687 | { 1688 | "name": "Jeff Welch", 1689 | "email": "whatthejeff@gmail.com" 1690 | }, 1691 | { 1692 | "name": "Sebastian Bergmann", 1693 | "email": "sebastian@phpunit.de" 1694 | }, 1695 | { 1696 | "name": "Adam Harvey", 1697 | "email": "aharvey@php.net" 1698 | } 1699 | ], 1700 | "description": "Provides functionality to recursively process PHP variables", 1701 | "homepage": "http://www.github.com/sebastianbergmann/recursion-context", 1702 | "time": "2017-03-03 06:23:57" 1703 | }, 1704 | { 1705 | "name": "sebastian/resource-operations", 1706 | "version": "1.0.0", 1707 | "source": { 1708 | "type": "git", 1709 | "url": "https://github.com/sebastianbergmann/resource-operations.git", 1710 | "reference": "ce990bb21759f94aeafd30209e8cfcdfa8bc3f52" 1711 | }, 1712 | "dist": { 1713 | "type": "zip", 1714 | "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/ce990bb21759f94aeafd30209e8cfcdfa8bc3f52", 1715 | "reference": "ce990bb21759f94aeafd30209e8cfcdfa8bc3f52", 1716 | "shasum": "" 1717 | }, 1718 | "require": { 1719 | "php": ">=5.6.0" 1720 | }, 1721 | "type": "library", 1722 | "extra": { 1723 | "branch-alias": { 1724 | "dev-master": "1.0.x-dev" 1725 | } 1726 | }, 1727 | "autoload": { 1728 | "classmap": [ 1729 | "src/" 1730 | ] 1731 | }, 1732 | "notification-url": "https://packagist.org/downloads/", 1733 | "license": [ 1734 | "BSD-3-Clause" 1735 | ], 1736 | "authors": [ 1737 | { 1738 | "name": "Sebastian Bergmann", 1739 | "email": "sebastian@phpunit.de" 1740 | } 1741 | ], 1742 | "description": "Provides a list of PHP built-in functions that operate on resources", 1743 | "homepage": "https://www.github.com/sebastianbergmann/resource-operations", 1744 | "time": "2015-07-28 20:34:47" 1745 | }, 1746 | { 1747 | "name": "sebastian/version", 1748 | "version": "2.0.1", 1749 | "source": { 1750 | "type": "git", 1751 | "url": "https://github.com/sebastianbergmann/version.git", 1752 | "reference": "99732be0ddb3361e16ad77b68ba41efc8e979019" 1753 | }, 1754 | "dist": { 1755 | "type": "zip", 1756 | "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/99732be0ddb3361e16ad77b68ba41efc8e979019", 1757 | "reference": "99732be0ddb3361e16ad77b68ba41efc8e979019", 1758 | "shasum": "" 1759 | }, 1760 | "require": { 1761 | "php": ">=5.6" 1762 | }, 1763 | "type": "library", 1764 | "extra": { 1765 | "branch-alias": { 1766 | "dev-master": "2.0.x-dev" 1767 | } 1768 | }, 1769 | "autoload": { 1770 | "classmap": [ 1771 | "src/" 1772 | ] 1773 | }, 1774 | "notification-url": "https://packagist.org/downloads/", 1775 | "license": [ 1776 | "BSD-3-Clause" 1777 | ], 1778 | "authors": [ 1779 | { 1780 | "name": "Sebastian Bergmann", 1781 | "email": "sebastian@phpunit.de", 1782 | "role": "lead" 1783 | } 1784 | ], 1785 | "description": "Library that helps with managing the version number of Git-hosted PHP projects", 1786 | "homepage": "https://github.com/sebastianbergmann/version", 1787 | "time": "2016-10-03 07:35:21" 1788 | }, 1789 | { 1790 | "name": "symfony/polyfill-mbstring", 1791 | "version": "v1.7.0", 1792 | "source": { 1793 | "type": "git", 1794 | "url": "https://github.com/symfony/polyfill-mbstring.git", 1795 | "reference": "78be803ce01e55d3491c1397cf1c64beb9c1b63b" 1796 | }, 1797 | "dist": { 1798 | "type": "zip", 1799 | "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/78be803ce01e55d3491c1397cf1c64beb9c1b63b", 1800 | "reference": "78be803ce01e55d3491c1397cf1c64beb9c1b63b", 1801 | "shasum": "" 1802 | }, 1803 | "require": { 1804 | "php": ">=5.3.3" 1805 | }, 1806 | "suggest": { 1807 | "ext-mbstring": "For best performance" 1808 | }, 1809 | "type": "library", 1810 | "extra": { 1811 | "branch-alias": { 1812 | "dev-master": "1.7-dev" 1813 | } 1814 | }, 1815 | "autoload": { 1816 | "psr-4": { 1817 | "Symfony\\Polyfill\\Mbstring\\": "" 1818 | }, 1819 | "files": [ 1820 | "bootstrap.php" 1821 | ] 1822 | }, 1823 | "notification-url": "https://packagist.org/downloads/", 1824 | "license": [ 1825 | "MIT" 1826 | ], 1827 | "authors": [ 1828 | { 1829 | "name": "Nicolas Grekas", 1830 | "email": "p@tchwork.com" 1831 | }, 1832 | { 1833 | "name": "Symfony Community", 1834 | "homepage": "https://symfony.com/contributors" 1835 | } 1836 | ], 1837 | "description": "Symfony polyfill for the Mbstring extension", 1838 | "homepage": "https://symfony.com", 1839 | "keywords": [ 1840 | "compatibility", 1841 | "mbstring", 1842 | "polyfill", 1843 | "portable", 1844 | "shim" 1845 | ], 1846 | "time": "2018-01-30 19:27:44" 1847 | }, 1848 | { 1849 | "name": "symfony/translation", 1850 | "version": "v3.3.6", 1851 | "source": { 1852 | "type": "git", 1853 | "url": "https://github.com/symfony/translation.git", 1854 | "reference": "35dd5fb003c90e8bd4d8cabdf94bf9c96d06fdc3" 1855 | }, 1856 | "dist": { 1857 | "type": "zip", 1858 | "url": "https://api.github.com/repos/symfony/translation/zipball/35dd5fb003c90e8bd4d8cabdf94bf9c96d06fdc3", 1859 | "reference": "35dd5fb003c90e8bd4d8cabdf94bf9c96d06fdc3", 1860 | "shasum": "" 1861 | }, 1862 | "require": { 1863 | "php": ">=5.5.9", 1864 | "symfony/polyfill-mbstring": "~1.0" 1865 | }, 1866 | "conflict": { 1867 | "symfony/config": "<2.8", 1868 | "symfony/yaml": "<3.3" 1869 | }, 1870 | "require-dev": { 1871 | "psr/log": "~1.0", 1872 | "symfony/config": "~2.8|~3.0", 1873 | "symfony/intl": "^2.8.18|^3.2.5", 1874 | "symfony/yaml": "~3.3" 1875 | }, 1876 | "suggest": { 1877 | "psr/log": "To use logging capability in translator", 1878 | "symfony/config": "", 1879 | "symfony/yaml": "" 1880 | }, 1881 | "type": "library", 1882 | "extra": { 1883 | "branch-alias": { 1884 | "dev-master": "3.3-dev" 1885 | } 1886 | }, 1887 | "autoload": { 1888 | "psr-4": { 1889 | "Symfony\\Component\\Translation\\": "" 1890 | }, 1891 | "exclude-from-classmap": [ 1892 | "/Tests/" 1893 | ] 1894 | }, 1895 | "notification-url": "https://packagist.org/downloads/", 1896 | "license": [ 1897 | "MIT" 1898 | ], 1899 | "authors": [ 1900 | { 1901 | "name": "Fabien Potencier", 1902 | "email": "fabien@symfony.com" 1903 | }, 1904 | { 1905 | "name": "Symfony Community", 1906 | "homepage": "https://symfony.com/contributors" 1907 | } 1908 | ], 1909 | "description": "Symfony Translation Component", 1910 | "homepage": "https://symfony.com", 1911 | "time": "2017-06-24 16:45:30" 1912 | }, 1913 | { 1914 | "name": "theseer/tokenizer", 1915 | "version": "1.1.0", 1916 | "source": { 1917 | "type": "git", 1918 | "url": "https://github.com/theseer/tokenizer.git", 1919 | "reference": "cb2f008f3f05af2893a87208fe6a6c4985483f8b" 1920 | }, 1921 | "dist": { 1922 | "type": "zip", 1923 | "url": "https://api.github.com/repos/theseer/tokenizer/zipball/cb2f008f3f05af2893a87208fe6a6c4985483f8b", 1924 | "reference": "cb2f008f3f05af2893a87208fe6a6c4985483f8b", 1925 | "shasum": "" 1926 | }, 1927 | "require": { 1928 | "ext-dom": "*", 1929 | "ext-tokenizer": "*", 1930 | "ext-xmlwriter": "*", 1931 | "php": "^7.0" 1932 | }, 1933 | "type": "library", 1934 | "autoload": { 1935 | "classmap": [ 1936 | "src/" 1937 | ] 1938 | }, 1939 | "notification-url": "https://packagist.org/downloads/", 1940 | "license": [ 1941 | "BSD-3-Clause" 1942 | ], 1943 | "authors": [ 1944 | { 1945 | "name": "Arne Blankerts", 1946 | "email": "arne@blankerts.de", 1947 | "role": "Developer" 1948 | } 1949 | ], 1950 | "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", 1951 | "time": "2017-04-07 12:08:54" 1952 | }, 1953 | { 1954 | "name": "webmozart/assert", 1955 | "version": "1.3.0", 1956 | "source": { 1957 | "type": "git", 1958 | "url": "https://github.com/webmozart/assert.git", 1959 | "reference": "0df1908962e7a3071564e857d86874dad1ef204a" 1960 | }, 1961 | "dist": { 1962 | "type": "zip", 1963 | "url": "https://api.github.com/repos/webmozart/assert/zipball/0df1908962e7a3071564e857d86874dad1ef204a", 1964 | "reference": "0df1908962e7a3071564e857d86874dad1ef204a", 1965 | "shasum": "" 1966 | }, 1967 | "require": { 1968 | "php": "^5.3.3 || ^7.0" 1969 | }, 1970 | "require-dev": { 1971 | "phpunit/phpunit": "^4.6", 1972 | "sebastian/version": "^1.0.1" 1973 | }, 1974 | "type": "library", 1975 | "extra": { 1976 | "branch-alias": { 1977 | "dev-master": "1.3-dev" 1978 | } 1979 | }, 1980 | "autoload": { 1981 | "psr-4": { 1982 | "Webmozart\\Assert\\": "src/" 1983 | } 1984 | }, 1985 | "notification-url": "https://packagist.org/downloads/", 1986 | "license": [ 1987 | "MIT" 1988 | ], 1989 | "authors": [ 1990 | { 1991 | "name": "Bernhard Schussek", 1992 | "email": "bschussek@gmail.com" 1993 | } 1994 | ], 1995 | "description": "Assertions to validate method input/output with nice error messages.", 1996 | "keywords": [ 1997 | "assert", 1998 | "check", 1999 | "validate" 2000 | ], 2001 | "time": "2018-01-29 19:49:41" 2002 | } 2003 | ], 2004 | "packages-dev": [], 2005 | "aliases": [], 2006 | "minimum-stability": "stable", 2007 | "stability-flags": [], 2008 | "prefer-stable": false, 2009 | "prefer-lowest": false, 2010 | "platform": { 2011 | "php": "~7.0" 2012 | }, 2013 | "platform-dev": [] 2014 | } 2015 | --------------------------------------------------------------------------------