Skip to content

Lazy container listeners #138

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Nov 21, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
38 changes: 34 additions & 4 deletions app/config.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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)
],
],
],
];
5 changes: 4 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,10 @@
"autoload" : {
"psr-4" : {
"PhpSchool\\PhpWorkshop\\": "src"
}
},
"files": [
"src/Event/functions.php"
]
},
"autoload-dev": {
"psr-4": {
Expand Down
45 changes: 45 additions & 0 deletions src/Event/ContainerListenerHelper.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<?php

namespace PhpSchool\PhpWorkshop\Event;

/**
* @author Aydin Hassan <[email protected]>
*/
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;
}
}
16 changes: 16 additions & 0 deletions src/Event/functions.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?php

namespace PhpSchool\PhpWorkshop\Event;

if (!function_exists('PhpSchool\PhpWorkshop\Event\containerListener')) {

/**
* @param string $service
* @param string $method
* @return ContainerListenerHelper
*/
function containerListener($service, $method = '__invoke')
{
return new ContainerListenerHelper($service, $method);
}
}
92 changes: 60 additions & 32 deletions src/Factory/EventDispatcherFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,10 @@

use Interop\Container\ContainerInterface;
use PhpSchool\PhpWorkshop\Event\EventDispatcher;
use PhpSchool\PhpWorkshop\Event\ContainerListenerHelper;
use PhpSchool\PhpWorkshop\Exception\InvalidArgumentException;
use PhpSchool\PhpWorkshop\Listener\CodePatchListener;
use PhpSchool\PhpWorkshop\Listener\PrepareSolutionListener;
use PhpSchool\PhpWorkshop\Listener\SelfCheckListener;
use PhpSchool\PhpWorkshop\ResultAggregator;
use PhpSchool\PhpWorkshop\Utils\Collection;

/**
* Class EventDispatcherFactory
Expand All @@ -27,36 +26,59 @@ public function __invoke(ContainerInterface $container)
{
$dispatcher = new EventDispatcher($container->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
Expand All @@ -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);
});
}
Expand Down
85 changes: 84 additions & 1 deletion src/Utils/ArrayObject.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand All @@ -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.
*
Expand Down Expand Up @@ -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.
*
Expand Down
11 changes: 11 additions & 0 deletions src/Utils/Collection.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?php

namespace PhpSchool\PhpWorkshop\Utils;

/**
* @author Aydin Hassan <[email protected]>
*/
class Collection extends ArrayObject
{

}
Loading