diff --git a/.travis.yml b/.travis.yml index 4b1163cc..a06eef16 100644 --- a/.travis.yml +++ b/.travis.yml @@ -17,7 +17,7 @@ before_install: - if [ "$SYMFONY_VERSION" != "" ]; then composer require --dev --no-update symfony/symfony=$SYMFONY_VERSION; fi install: - - composer update $COMPOSER_FLAGS --prefer-source --optimize-autoloader + - composer update $COMPOSER_FLAGS --prefer-dist --optimize-autoloader before_script: - mkdir -p build/logs diff --git a/app/config.php b/app/config.php index aaad2245..ace0a1a3 100644 --- a/app/config.php +++ b/app/config.php @@ -3,6 +3,7 @@ use Colors\Color; use function DI\object; use function DI\factory; +use function PhpSchool\PhpWorkshop\Event\containerListener; use Interop\Container\ContainerInterface; use League\CommonMark\DocParser; use League\CommonMark\Environment; @@ -264,9 +265,38 @@ '@chris3ailey' => 'Chris Bailey' ], 'appContributors' => [], - 'eventListeners' => [ - 'route.pre.resolve.args' => [ - CheckExerciseAssignedListener::class + 'eventListeners' => [ + 'check-exercise-assigned' => [ + 'route.pre.resolve.args' => [ + containerListener(CheckExerciseAssignedListener::class) + ], + ], + 'prepare-solution' => [ + 'verify.start' => [ + containerListener(PrepareSolutionListener::class), + ], + 'run.start' => [ + containerListener(PrepareSolutionListener::class), + ], + ], + 'code-patcher' => [ + 'run.start' => [ + containerListener(CodePatchListener::class, 'patch'), + ], + 'verify.pre.execute' => [ + containerListener(CodePatchListener::class, 'patch'), + ], + 'verify.post.execute' => [ + containerListener(CodePatchListener::class, 'revert'), + ], + 'run.finish' => [ + containerListener(CodePatchListener::class, 'revert'), + ], ], - ] + 'self-check' => [ + 'verify.post.check' => [ + containerListener(SelfCheckListener::class) + ], + ], + ], ]; diff --git a/composer.json b/composer.json index 24e67c79..69ef113c 100644 --- a/composer.json +++ b/composer.json @@ -40,7 +40,10 @@ "autoload" : { "psr-4" : { "PhpSchool\\PhpWorkshop\\": "src" - } + }, + "files": [ + "src/Event/functions.php" + ] }, "autoload-dev": { "psr-4": { diff --git a/src/Event/ContainerListenerHelper.php b/src/Event/ContainerListenerHelper.php new file mode 100644 index 00000000..4daa190a --- /dev/null +++ b/src/Event/ContainerListenerHelper.php @@ -0,0 +1,45 @@ + + */ +class ContainerListenerHelper +{ + /** + * @var string + */ + private $service; + + /** + * @var string + */ + private $method; + + /** + * @param $service + * @param string $method + */ + public function __construct($service, $method = '__invoke') + { + $this->service = $service; + $this->method = $method; + } + + /** + * @return string + */ + public function getService() + { + return $this->service; + } + + /** + * @return string + */ + public function getMethod() + { + return $this->method; + } +} diff --git a/src/Event/functions.php b/src/Event/functions.php new file mode 100644 index 00000000..878d8e12 --- /dev/null +++ b/src/Event/functions.php @@ -0,0 +1,16 @@ +get(ResultAggregator::class)); - $prepareSolutionListener = $container->get(PrepareSolutionListener::class); - $dispatcher->listen('verify.start', $prepareSolutionListener); - $dispatcher->listen('run.start', $prepareSolutionListener); - - $codePatcherListener = $container->get(CodePatchListener::class); - $dispatcher->listen('verify.pre.execute', [$codePatcherListener, 'patch']); - $dispatcher->listen('verify.post.execute', [$codePatcherListener, 'revert']); - $dispatcher->listen('run.start', [$codePatcherListener, 'patch']); - $dispatcher->listen('run.finish', [$codePatcherListener, 'revert']); - - $dispatcher->listen('verify.post.check', $container->get(SelfCheckListener::class)); - //add listeners from config $eventListeners = $container->has('eventListeners') ? $container->get('eventListeners') : []; if (!is_array($eventListeners)) { throw InvalidArgumentException::typeMisMatch('array', $eventListeners); } - - array_walk($eventListeners, function ($listeners, $eventName) use ($dispatcher, $container) { - if (!is_array($listeners)) { - throw InvalidArgumentException::typeMisMatch('array', $listeners); + + array_walk($eventListeners, function ($events) { + if (!is_array($events)) { + throw InvalidArgumentException::typeMisMatch('array', $events); } + }); + $eventListeners = $this->mergeListenerGroups($eventListeners); + + array_walk($eventListeners, function ($listeners, $eventName) use ($dispatcher, $container) { $this->attachListeners($eventName, $listeners, $container, $dispatcher); }); return $dispatcher; } + /** + * @param array $listeners + * @return array + */ + private function mergeListenerGroups(array $listeners) + { + $listeners = new Collection($listeners); + + return $listeners + ->keys() + ->reduce(function (Collection $carry, $listenerGroup) use ($listeners) { + $events = new Collection($listeners->get($listenerGroup)); + + return $events + ->keys() + ->reduce(function (Collection $carry, $event) use ($events) { + $listeners = $events->get($event); + + if (!is_array($listeners)) { + throw InvalidArgumentException::typeMisMatch('array', $listeners); + } + + return $carry->set( + $event, + array_merge($carry->get($event, []), $listeners) + ); + }, $carry); + }, new Collection) + ->getArrayCopy(); + } + /** * @param string $eventName * @param array $listeners @@ -71,26 +93,32 @@ private function attachListeners( EventDispatcher $dispatcher ) { array_walk($listeners, function ($listener) use ($eventName, $dispatcher, $container) { - if (is_callable($listener)) { - return $dispatcher->listen($eventName, $listener); + if ($listener instanceof ContainerListenerHelper) { + if (!$container->has($listener->getService())) { + throw new InvalidArgumentException( + sprintf('Container has no entry named: "%s"', $listener->getService()) + ); + } + + return $dispatcher->listen($eventName, function (...$args) use ($container, $listener) { + $service = $container->get($listener->getService()); + + if (!method_exists($service, $listener->getMethod())) { + throw new InvalidArgumentException( + sprintf('Method "%s" does not exist on "%s"', $listener->getMethod(), get_class($service)) + ); + } + + $service->{$listener->getMethod()}(...$args); + }); } - if (!is_string($listener)) { + if (!is_callable($listener)) { throw new InvalidArgumentException( sprintf('Listener must be a callable or a container entry for a callable service.') ); } - if (!$container->has($listener)) { - throw new InvalidArgumentException(sprintf('Container has no entry named: "%s"', $listener)); - } - - $listener = $container->get($listener); - - if (!is_callable($listener)) { - throw InvalidArgumentException::typeMisMatch('callable', $listener); - } - return $dispatcher->listen($eventName, $listener); }); } diff --git a/src/Utils/ArrayObject.php b/src/Utils/ArrayObject.php index d62ef31e..638cb373 100644 --- a/src/Utils/ArrayObject.php +++ b/src/Utils/ArrayObject.php @@ -25,7 +25,7 @@ class ArrayObject implements IteratorAggregate, Countable * * @param array $array */ - public function __construct(array $array) + public function __construct(array $array = []) { $this->array = $array; } @@ -42,6 +42,59 @@ public function map(callable $callback) return new static (array_map($callback, $this->array)); } + /** + * Run a callable over each item in the array and flatten the results by one level returning a new instance of + * `ArrayObject` with the flattened items. + * + * @param callable $callback + * @return static + */ + public function flatMap(callable $callback) + { + return $this->map($callback)->collapse(); + } + + /** + * Collapse an array of arrays into a single array returning a new instance of `ArrayObject` + * with the collapsed items. + * + * @return static + */ + public function collapse() + { + $results = []; + + foreach ($this->array as $item) { + if (!is_array($item)) { + continue; + } + + $results = array_merge($results, $item); + } + + return new static($results); + } + + /** + * Reduce the items to a single value. + * + * @param callable $callback + * @param mixed $initial + * @return mixed + */ + public function reduce(callable $callback, $initial = null) + { + return array_reduce($this->array, $callback, $initial); + } + + /** + * @return static + */ + public function keys() + { + return new static(array_keys($this->array)); + } + /** * Implode each item together using the provided glue. * @@ -75,6 +128,36 @@ public function append($value) return new static(array_merge($this->array, [$value])); } + /** + * Get an item at the given key. + * + * @param mixed $key + * @param mixed $default + * @return mixed + */ + public function get($key, $default = null) + { + if (isset($this->array[$key])) { + return $this->array[$key]; + } + + return $default; + } + + /** + * Set the item at a given offset and return a new instance. + * + * @param mixed $key + * @param mixed $value + * @return static + */ + public function set($key, $value) + { + $items = $this->array; + $items[$key] = $value; + return new static($items); + } + /** * Return an iterator containing all the items. Allows to `foreach` over. * diff --git a/src/Utils/Collection.php b/src/Utils/Collection.php new file mode 100644 index 00000000..c9ccf3f2 --- /dev/null +++ b/src/Utils/Collection.php @@ -0,0 +1,11 @@ + + */ +class Collection extends ArrayObject +{ + +} diff --git a/test/Event/ContainerListenerHelperTest.php b/test/Event/ContainerListenerHelperTest.php new file mode 100644 index 00000000..497135ea --- /dev/null +++ b/test/Event/ContainerListenerHelperTest.php @@ -0,0 +1,28 @@ + + */ +class ContainerListenerHelperTest extends PHPUnit_Framework_TestCase +{ + public function testDefaultMethodIsInvoke() + { + $helper = new ContainerListenerHelper('Some\Object'); + + $this->assertEquals('Some\Object', $helper->getService()); + $this->assertEquals('__invoke', $helper->getMethod()); + } + + public function testWithCustomMethod() + { + $helper = new ContainerListenerHelper('Some\Object', 'myMethod'); + + $this->assertEquals('Some\Object', $helper->getService()); + $this->assertEquals('myMethod', $helper->getMethod()); + } +} diff --git a/test/Factory/EventDispatcherFactoryTest.php b/test/Factory/EventDispatcherFactoryTest.php index ff92f6ba..4e0b47e7 100644 --- a/test/Factory/EventDispatcherFactoryTest.php +++ b/test/Factory/EventDispatcherFactoryTest.php @@ -2,13 +2,13 @@ namespace PhpSchool\PhpWorkshopTest\Factory; +use DI\ContainerBuilder; +use PhpSchool\PhpWorkshop\Event\Event; +use function PhpSchool\PhpWorkshop\Event\containerListener; use Interop\Container\ContainerInterface; use PhpSchool\PhpWorkshop\Event\EventDispatcher; use PhpSchool\PhpWorkshop\Exception\InvalidArgumentException; use PhpSchool\PhpWorkshop\Factory\EventDispatcherFactory; -use PhpSchool\PhpWorkshop\Listener\CodePatchListener; -use PhpSchool\PhpWorkshop\Listener\PrepareSolutionListener; -use PhpSchool\PhpWorkshop\Listener\SelfCheckListener; use PhpSchool\PhpWorkshop\ResultAggregator; use PHPUnit_Framework_TestCase; @@ -20,375 +20,122 @@ class EventDispatcherFactoryTest extends PHPUnit_Framework_TestCase { - public function testCreate() + public function testCreateWithNoConfig() { - $c = $this->createMock(ContainerInterface::class); + $c = $this->prophesize(ContainerInterface::class); + $c->get(ResultAggregator::class)->willReturn(new ResultAggregator); + $c->has('eventListeners')->willReturn(false); - $c->expects($this->at(0)) - ->method('get') - ->with(ResultAggregator::class) - ->will($this->returnValue(new ResultAggregator)); - - $prepareSolutionListener = new PrepareSolutionListener; - - $c->expects($this->at(1)) - ->method('get') - ->with(PrepareSolutionListener::class) - ->will($this->returnValue($prepareSolutionListener)); - - $codePatchListener = $this->createMock(CodePatchListener::class); - - $c->expects($this->at(2)) - ->method('get') - ->with(CodePatchListener::class) - ->will($this->returnValue($codePatchListener)); - - $selfCheckListener = new SelfCheckListener(new ResultAggregator); - - $c->expects($this->at(3)) - ->method('get') - ->with(SelfCheckListener::class) - ->will($this->returnValue($selfCheckListener)); - - $dispatcher = (new EventDispatcherFactory)->__invoke($c); + $dispatcher = (new EventDispatcherFactory)->__invoke($c->reveal()); $this->assertInstanceOf(EventDispatcher::class, $dispatcher); - $this->assertSame( - [ - 'verify.start' => [ - $prepareSolutionListener - ], - 'run.start' => [ - $prepareSolutionListener, - [$codePatchListener, 'patch'], - ], - 'verify.pre.execute' => [ - [$codePatchListener, 'patch'], - ], - 'verify.post.execute' => [ - [$codePatchListener, 'revert'], - ], - 'run.finish' => [ - [$codePatchListener, 'revert'], - ], - 'verify.post.check' => [ - $selfCheckListener - ] - ], - $this->readAttribute($dispatcher, 'listeners') - ); + $this->assertSame([], $this->readAttribute($dispatcher, 'listeners')); } - public function testConfigEventListenersThrowsExceptionIfEventsNotArray() + public function testExceptionIsThrownIfEventListenerGroupsNotArray() { - $c = $this->createMock(ContainerInterface::class); - - $c->expects($this->at(0)) - ->method('get') - ->with(ResultAggregator::class) - ->will($this->returnValue(new ResultAggregator)); - - $prepareSolutionListener = new PrepareSolutionListener; - - $c->expects($this->at(1)) - ->method('get') - ->with(PrepareSolutionListener::class) - ->will($this->returnValue($prepareSolutionListener)); - - $codePatchListener = $this->createMock(CodePatchListener::class); - - $c->expects($this->at(2)) - ->method('get') - ->with(CodePatchListener::class) - ->will($this->returnValue($codePatchListener)); - - $selfCheckListener = new SelfCheckListener(new ResultAggregator); - - $c->expects($this->at(3)) - ->method('get') - ->with(SelfCheckListener::class) - ->will($this->returnValue($selfCheckListener)); - - $c->expects($this->at(4)) - ->method('has') - ->with('eventListeners') - ->willReturn(true); - - $c->expects($this->at(5)) - ->method('get') - ->with('eventListeners') - ->will($this->returnValue(new \stdClass)); + $c = $this->prophesize(ContainerInterface::class); + $c->get(ResultAggregator::class)->willReturn(new ResultAggregator); + $c->has('eventListeners')->willReturn(true); + $c->get('eventListeners')->willReturn(new \stdClass); $this->expectException(InvalidArgumentException::class); $this->expectExceptionMessage('Expected: "array" Received: "stdClass"'); - (new EventDispatcherFactory)->__invoke($c); + (new EventDispatcherFactory)->__invoke($c->reveal()); } - public function testConfigEventListenersThrowsExceptionIfEventsListenersNotArray() + public function testExceptionIsThrownIfEventsNotArray() { - $c = $this->createMock(ContainerInterface::class); - - $c->expects($this->at(0)) - ->method('get') - ->with(ResultAggregator::class) - ->will($this->returnValue(new ResultAggregator)); - - $prepareSolutionListener = new PrepareSolutionListener; - - $c->expects($this->at(1)) - ->method('get') - ->with(PrepareSolutionListener::class) - ->will($this->returnValue($prepareSolutionListener)); - - $codePatchListener = $this->createMock(CodePatchListener::class); - - $c->expects($this->at(2)) - ->method('get') - ->with(CodePatchListener::class) - ->will($this->returnValue($codePatchListener)); - - $selfCheckListener = new SelfCheckListener(new ResultAggregator); - - $c->expects($this->at(3)) - ->method('get') - ->with(SelfCheckListener::class) - ->will($this->returnValue($selfCheckListener)); - - $c->expects($this->at(4)) - ->method('has') - ->with('eventListeners') - ->willReturn(true); - - $c->expects($this->at(5)) - ->method('get') - ->with('eventListeners') - ->will($this->returnValue([ 'someEvent' => new \stdClass])); + $c = $this->prophesize(ContainerInterface::class); + $c->get(ResultAggregator::class)->willReturn(new ResultAggregator); + $c->has('eventListeners')->willReturn(true); + $c->get('eventListeners')->willReturn(['my-group' => new \stdClass]); $this->expectException(InvalidArgumentException::class); $this->expectExceptionMessage('Expected: "array" Received: "stdClass"'); - (new EventDispatcherFactory)->__invoke($c); + (new EventDispatcherFactory)->__invoke($c->reveal()); } - public function testConfigEventListenersThrowsExceptionIfEventsListenerNotCallableOrString() + public function testExceptionIsThrownIfEventListenersNotArray() { - $c = $this->createMock(ContainerInterface::class); - - $c->expects($this->at(0)) - ->method('get') - ->with(ResultAggregator::class) - ->will($this->returnValue(new ResultAggregator)); - - $prepareSolutionListener = new PrepareSolutionListener; - - $c->expects($this->at(1)) - ->method('get') - ->with(PrepareSolutionListener::class) - ->will($this->returnValue($prepareSolutionListener)); - - $codePatchListener = $this->createMock(CodePatchListener::class); - - $c->expects($this->at(2)) - ->method('get') - ->with(CodePatchListener::class) - ->will($this->returnValue($codePatchListener)); + $eventConfig = [ + 'my-group' => [ + 'someEvent' => new \stdClass + ] + ]; - $selfCheckListener = new SelfCheckListener(new ResultAggregator); - - $c->expects($this->at(3)) - ->method('get') - ->with(SelfCheckListener::class) - ->will($this->returnValue($selfCheckListener)); - - $c->expects($this->at(4)) - ->method('has') - ->with('eventListeners') - ->willReturn(true); - - $c->expects($this->at(5)) - ->method('get') - ->with('eventListeners') - ->will($this->returnValue([ 'someEvent' => [new \stdClass]])); + $c = $this->prophesize(ContainerInterface::class); + $c->get(ResultAggregator::class)->willReturn(new ResultAggregator); + $c->has('eventListeners')->willReturn(true); + $c->get('eventListeners')->willReturn($eventConfig); $this->expectException(InvalidArgumentException::class); - $this->expectExceptionMessage('Listener must be a callable or a container entry for a callable service.'); + $this->expectExceptionMessage('Expected: "array" Received: "stdClass"'); - (new EventDispatcherFactory)->__invoke($c); + (new EventDispatcherFactory)->__invoke($c->reveal()); } - public function testConfigEventListenersThrowsExceptionIfEventsListenerContainerEntryNotExist() + public function testExceptionIsThrownIfListenerNotCallable() { - $c = $this->createMock(ContainerInterface::class); - - $c->expects($this->at(0)) - ->method('get') - ->with(ResultAggregator::class) - ->will($this->returnValue(new ResultAggregator)); - - $prepareSolutionListener = new PrepareSolutionListener; - - $c->expects($this->at(1)) - ->method('get') - ->with(PrepareSolutionListener::class) - ->will($this->returnValue($prepareSolutionListener)); - - $codePatchListener = $this->createMock(CodePatchListener::class); - - $c->expects($this->at(2)) - ->method('get') - ->with(CodePatchListener::class) - ->will($this->returnValue($codePatchListener)); - - $selfCheckListener = new SelfCheckListener(new ResultAggregator); - - $c->expects($this->at(3)) - ->method('get') - ->with(SelfCheckListener::class) - ->will($this->returnValue($selfCheckListener)); + $eventConfig = [ + 'my-group' => [ + 'someEvent' => [new \stdClass] + ] + ]; - $c->expects($this->at(4)) - ->method('has') - ->with('eventListeners') - ->willReturn(true); - - $c->expects($this->at(5)) - ->method('get') - ->with('eventListeners') - ->will($this->returnValue([ 'someEvent' => ['nonExistingContainerEntry']])); - - $c->expects($this->at(6)) - ->method('has') - ->with('nonExistingContainerEntry') - ->will($this->returnValue(false)); + $c = $this->prophesize(ContainerInterface::class); + $c->get(ResultAggregator::class)->willReturn(new ResultAggregator); + $c->has('eventListeners')->willReturn(true); + $c->get('eventListeners')->willReturn($eventConfig); $this->expectException(InvalidArgumentException::class); - $this->expectExceptionMessage('Container has no entry named: "nonExistingContainerEntry"'); + $this->expectExceptionMessage('Listener must be a callable or a container entry for a callable service.'); - (new EventDispatcherFactory)->__invoke($c); + (new EventDispatcherFactory)->__invoke($c->reveal()); } - public function testConfigEventListenersThrowsExceptionIfEventsListenerContainerEntryNotCallable() + public function testExceptionIsThrownIfEventsListenerContainerEntryNotExist() { - $c = $this->createMock(ContainerInterface::class); - - $c->expects($this->at(0)) - ->method('get') - ->with(ResultAggregator::class) - ->will($this->returnValue(new ResultAggregator)); - - $prepareSolutionListener = new PrepareSolutionListener; - - $c->expects($this->at(1)) - ->method('get') - ->with(PrepareSolutionListener::class) - ->will($this->returnValue($prepareSolutionListener)); - - $codePatchListener = $this->createMock(CodePatchListener::class); - - $c->expects($this->at(2)) - ->method('get') - ->with(CodePatchListener::class) - ->will($this->returnValue($codePatchListener)); - - $selfCheckListener = new SelfCheckListener(new ResultAggregator); - - $c->expects($this->at(3)) - ->method('get') - ->with(SelfCheckListener::class) - ->will($this->returnValue($selfCheckListener)); - - $c->expects($this->at(4)) - ->method('has') - ->with('eventListeners') - ->willReturn(true); + $eventConfig = [ + 'my-group' => [ + 'someEvent' => [containerListener('nonExistingContainerEntry')] + ] + ]; - $c->expects($this->at(5)) - ->method('get') - ->with('eventListeners') - ->will($this->returnValue([ 'someEvent' => ['notCallableEntry']])); + $c = $this->prophesize(ContainerInterface::class); + $c->get(ResultAggregator::class)->willReturn(new ResultAggregator); + $c->has('eventListeners')->willReturn(true); + $c->get('eventListeners')->willReturn($eventConfig); - $c->expects($this->at(6)) - ->method('has') - ->with('notCallableEntry') - ->will($this->returnValue(true)); - - $c->expects($this->at(7)) - ->method('get') - ->with('notCallableEntry') - ->will($this->returnValue(null)); + $c->has('nonExistingContainerEntry')->willReturn(false); $this->expectException(InvalidArgumentException::class); - $this->expectExceptionMessage('Expected: "callable" Received: "NULL"'); + $this->expectExceptionMessage('Container has no entry named: "nonExistingContainerEntry"'); - (new EventDispatcherFactory)->__invoke($c); + (new EventDispatcherFactory)->__invoke($c->reveal()); } public function testConfigEventListenersWithAnonymousFunction() { - $c = $this->createMock(ContainerInterface::class); - - $c->expects($this->at(0)) - ->method('get') - ->with(ResultAggregator::class) - ->will($this->returnValue(new ResultAggregator)); - - $prepareSolutionListener = new PrepareSolutionListener; - - $c->expects($this->at(1)) - ->method('get') - ->with(PrepareSolutionListener::class) - ->will($this->returnValue($prepareSolutionListener)); - - $codePatchListener = $this->createMock(CodePatchListener::class); - - $c->expects($this->at(2)) - ->method('get') - ->with(CodePatchListener::class) - ->will($this->returnValue($codePatchListener)); - - $selfCheckListener = new SelfCheckListener(new ResultAggregator); - - $c->expects($this->at(3)) - ->method('get') - ->with(SelfCheckListener::class) - ->will($this->returnValue($selfCheckListener)); - $callback = function () { }; - $c->expects($this->at(4)) - ->method('has') - ->with('eventListeners') - ->willReturn(true); + $eventConfig = [ + 'my-group' => [ + 'someEvent' => [$callback] + ] + ]; - $c->expects($this->at(5)) - ->method('get') - ->with('eventListeners') - ->will($this->returnValue([ 'someEvent' => [$callback]])); + $c = $this->prophesize(ContainerInterface::class); + $c->get(ResultAggregator::class)->willReturn(new ResultAggregator); + $c->has('eventListeners')->willReturn(true); + $c->get('eventListeners')->willReturn($eventConfig); - $dispatcher = (new EventDispatcherFactory)->__invoke($c); + $dispatcher = (new EventDispatcherFactory)->__invoke($c->reveal()); $this->assertInstanceOf(EventDispatcher::class, $dispatcher); $this->assertSame( [ - 'verify.start' => [ - $prepareSolutionListener - ], - 'run.start' => [ - $prepareSolutionListener, - [$codePatchListener, 'patch'], - ], - 'verify.pre.execute' => [ - [$codePatchListener, 'patch'], - ], - 'verify.post.execute' => [ - [$codePatchListener, 'revert'], - ], - 'run.finish' => [ - [$codePatchListener, 'revert'], - ], - 'verify.post.check' => [ - $selfCheckListener - ], 'someEvent' => [ $callback ] @@ -397,87 +144,94 @@ public function testConfigEventListenersWithAnonymousFunction() ); } - public function testConfigEventListenersWithContainerEntry() + public function testListenerFromContainerIsNotFetchedDuringAttaching() { - $c = $this->createMock(ContainerInterface::class); - - $c->expects($this->at(0)) - ->method('get') - ->with(ResultAggregator::class) - ->will($this->returnValue(new ResultAggregator)); + $eventConfig = [ + 'my-group' => [ + 'someEvent' => [containerListener('containerEntry')] + ] + ]; - $prepareSolutionListener = new PrepareSolutionListener; + $c = $this->prophesize(ContainerInterface::class); - $c->expects($this->at(1)) - ->method('get') - ->with(PrepareSolutionListener::class) - ->will($this->returnValue($prepareSolutionListener)); + $c->get(ResultAggregator::class)->willReturn(new ResultAggregator); + $c->has('eventListeners')->willReturn(true); + $c->get('eventListeners')->willReturn($eventConfig); + $c->has('containerEntry')->willReturn(true); - $codePatchListener = $this->createMock(CodePatchListener::class); - $c->expects($this->at(2)) - ->method('get') - ->with(CodePatchListener::class) - ->will($this->returnValue($codePatchListener)); + $dispatcher = (new EventDispatcherFactory)->__invoke($c->reveal()); + $this->assertInstanceOf(EventDispatcher::class, $dispatcher); + $this->assertArrayHasKey('someEvent', $this->readAttribute($dispatcher, 'listeners')); - $selfCheckListener = new SelfCheckListener(new ResultAggregator); + $c->get('containerEntry')->shouldNotHaveBeenCalled(); + } - $c->expects($this->at(3)) - ->method('get') - ->with(SelfCheckListener::class) - ->will($this->returnValue($selfCheckListener)); + public function testListenerFromContainerIsFetchedWhenEventDispatched() + { + $eventConfig = [ + 'my-group' => [ + 'someEvent' => [containerListener('containerEntry')] + ] + ]; + + $c = $this->prophesize(ContainerInterface::class); + + $c->get(ResultAggregator::class)->willReturn(new ResultAggregator); + $c->has('eventListeners')->willReturn(true); + $c->get('eventListeners')->willReturn($eventConfig); + $c->has('containerEntry')->willReturn(true); + $c->get('containerEntry')->willReturn(function () { + }); + + $dispatcher = (new EventDispatcherFactory)->__invoke($c->reveal()); + $this->assertInstanceOf(EventDispatcher::class, $dispatcher); + $this->assertArrayHasKey('someEvent', $this->readAttribute($dispatcher, 'listeners')); - $c->expects($this->at(4)) - ->method('has') - ->with('eventListeners') - ->willReturn(true); + $dispatcher->dispatch(new Event('someEvent')); + } - $c->expects($this->at(5)) - ->method('get') - ->with('eventListeners') - ->will($this->returnValue([ 'someEvent' => ['containerEntry']])); + public function testExceptionIsThrownIfMethodDoesNotExistOnContainerEntry() + { + $eventConfig = [ + 'my-group' => [ + 'someEvent' => [containerListener('containerEntry', 'notHere')] + ] + ]; - $c->expects($this->at(6)) - ->method('has') - ->with('containerEntry') - ->will($this->returnValue(true)); + $c = $this->prophesize(ContainerInterface::class); - $callback = function () { - }; + $c->get(ResultAggregator::class)->willReturn(new ResultAggregator); + $c->has('eventListeners')->willReturn(true); + $c->get('eventListeners')->willReturn($eventConfig); + $c->has('containerEntry')->willReturn(true); + $c->get('containerEntry')->willReturn(new \stdClass); - $c->expects($this->at(7)) - ->method('get') - ->with('containerEntry') - ->will($this->returnValue($callback)); + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Method "notHere" does not exist on "stdClass"'); - $dispatcher = (new EventDispatcherFactory)->__invoke($c); + $dispatcher = (new EventDispatcherFactory)->__invoke($c->reveal()); $this->assertInstanceOf(EventDispatcher::class, $dispatcher); - $this->assertSame( - [ - 'verify.start' => [ - $prepareSolutionListener - ], - 'run.start' => [ - $prepareSolutionListener, - [$codePatchListener, 'patch'], - ], - 'verify.pre.execute' => [ - [$codePatchListener, 'patch'], - ], - 'verify.post.execute' => [ - [$codePatchListener, 'revert'], - ], - 'run.finish' => [ - [$codePatchListener, 'revert'], - ], - 'verify.post.check' => [ - $selfCheckListener - ], - 'someEvent' => [ - $callback - ] - ], - $this->readAttribute($dispatcher, 'listeners') - ); + + $dispatcher->dispatch(new Event('someEvent')); + } + + public function testDefaultListenersAreRegisteredFromConfig() + { + $containerBuilder = new ContainerBuilder; + $containerBuilder->addDefinitions(__DIR__ . '/../../app/config.php'); + + $container = $containerBuilder->build(); + + $dispatcher = (new EventDispatcherFactory)->__invoke($container); + + $listeners = $this->readAttribute($dispatcher, 'listeners'); + + $this->assertArrayHasKey('verify.start', $listeners); + $this->assertArrayHasKey('run.start', $listeners); + $this->assertArrayHasKey('verify.pre.execute', $listeners); + $this->assertArrayHasKey('verify.post.execute', $listeners); + $this->assertArrayHasKey('run.finish', $listeners); + $this->assertArrayHasKey('verify.post.check', $listeners); } } diff --git a/test/Util/ArrayObjectTest.php b/test/Util/ArrayObjectTest.php index 8076485e..b8450fde 100644 --- a/test/Util/ArrayObjectTest.php +++ b/test/Util/ArrayObjectTest.php @@ -58,4 +58,76 @@ public function testGetArrayCopy() $arrayObject = new ArrayObject([1, 2, 3]); $this->assertSame([1, 2, 3], $arrayObject->getArrayCopy()); } + + public function testFlatMap() + { + $arrayObject = new ArrayObject([ + ['name' => 'Aydin', 'pets' => ['rat', 'raccoon', 'binturong']], + ['name' => 'Caroline', 'pets' => ['rabbit', 'squirrel', 'dog']], + ]); + $new = $arrayObject->flatMap(function (array $item) { + return $item['pets']; + }); + + $this->assertSame(['rat', 'raccoon', 'binturong', 'rabbit', 'squirrel', 'dog'], $new->getArrayCopy()); + } + + public function testCollapse() + { + $arrayObject = new ArrayObject([[1, 2], [3, 4, 5], [6, 7, 8]]); + $new = $arrayObject->collapse(); + + $this->assertSame([1, 2, 3, 4, 5, 6, 7, 8], $new->getArrayCopy()); + + //with non array elements (should be skipped) + $arrayObject = new ArrayObject([[1, 2], [3, 4, 5], [6, 7, 8], 9, 10]); + $new = $arrayObject->collapse(); + + $this->assertSame([1, 2, 3, 4, 5, 6, 7, 8], $new->getArrayCopy()); + } + + public function testReduce() + { + $arrayObject = new ArrayObject([6, 3, 1]); + $total = $arrayObject->reduce(function ($carry, $item) { + return $carry + $item; + }, 0); + + $this->assertEquals(10, $total); + } + + public function testKeys() + { + $arrayObject = new ArrayObject(['one' => 1, 'two' => 2, 'three' => 3]); + $new = $arrayObject->keys(); + + $this->assertSame(['one', 'two', 'three'], $new->getArrayCopy()); + } + + public function testGetReturnsDefaultIfNotSet() + { + $arrayObject = new ArrayObject(['one' => 1, 'two' => 2, 'three' => 3]); + $this->assertEquals(4, $arrayObject->get('four', 4)); + } + + public function testGet() + { + $arrayObject = new ArrayObject(['one' => 1, 'two' => 2, 'three' => 3]); + $this->assertEquals(3, $arrayObject->get('three')); + } + + public function testSet() + { + $arrayObject = new ArrayObject([1, 2, 3]); + $new = $arrayObject->set(3, 4); + + $this->assertNotSame($new, $arrayObject); + $this->assertSame([1, 2, 3, 4], $new->getArrayCopy()); + + $arrayObject = new ArrayObject(['one' => 1, 'two' => 2, 'three' => 3]); + $new = $arrayObject->set('three', 4); + + $this->assertNotSame($new, $arrayObject); + $this->assertSame(['one' => 1, 'two' => 2, 'three' => 4], $new->getArrayCopy()); + } }