Skip to content

Commit 8ea28cb

Browse files
authored
Merge pull request #138 from php-school/lazy-container-listeners
Lazy container listeners
2 parents f015cbe + 4c61d8d commit 8ea28cb

11 files changed

+498
-428
lines changed

.travis.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ before_install:
1717
- if [ "$SYMFONY_VERSION" != "" ]; then composer require --dev --no-update symfony/symfony=$SYMFONY_VERSION; fi
1818

1919
install:
20-
- composer update $COMPOSER_FLAGS --prefer-source --optimize-autoloader
20+
- composer update $COMPOSER_FLAGS --prefer-dist --optimize-autoloader
2121

2222
before_script:
2323
- mkdir -p build/logs

app/config.php

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
use Colors\Color;
44
use function DI\object;
55
use function DI\factory;
6+
use function PhpSchool\PhpWorkshop\Event\containerListener;
67
use Interop\Container\ContainerInterface;
78
use League\CommonMark\DocParser;
89
use League\CommonMark\Environment;
@@ -264,9 +265,38 @@
264265
'@chris3ailey' => 'Chris Bailey'
265266
],
266267
'appContributors' => [],
267-
'eventListeners' => [
268-
'route.pre.resolve.args' => [
269-
CheckExerciseAssignedListener::class
268+
'eventListeners' => [
269+
'check-exercise-assigned' => [
270+
'route.pre.resolve.args' => [
271+
containerListener(CheckExerciseAssignedListener::class)
272+
],
273+
],
274+
'prepare-solution' => [
275+
'verify.start' => [
276+
containerListener(PrepareSolutionListener::class),
277+
],
278+
'run.start' => [
279+
containerListener(PrepareSolutionListener::class),
280+
],
281+
],
282+
'code-patcher' => [
283+
'run.start' => [
284+
containerListener(CodePatchListener::class, 'patch'),
285+
],
286+
'verify.pre.execute' => [
287+
containerListener(CodePatchListener::class, 'patch'),
288+
],
289+
'verify.post.execute' => [
290+
containerListener(CodePatchListener::class, 'revert'),
291+
],
292+
'run.finish' => [
293+
containerListener(CodePatchListener::class, 'revert'),
294+
],
270295
],
271-
]
296+
'self-check' => [
297+
'verify.post.check' => [
298+
containerListener(SelfCheckListener::class)
299+
],
300+
],
301+
],
272302
];

composer.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,10 @@
4040
"autoload" : {
4141
"psr-4" : {
4242
"PhpSchool\\PhpWorkshop\\": "src"
43-
}
43+
},
44+
"files": [
45+
"src/Event/functions.php"
46+
]
4447
},
4548
"autoload-dev": {
4649
"psr-4": {

src/Event/ContainerListenerHelper.php

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
<?php
2+
3+
namespace PhpSchool\PhpWorkshop\Event;
4+
5+
/**
6+
* @author Aydin Hassan <[email protected]>
7+
*/
8+
class ContainerListenerHelper
9+
{
10+
/**
11+
* @var string
12+
*/
13+
private $service;
14+
15+
/**
16+
* @var string
17+
*/
18+
private $method;
19+
20+
/**
21+
* @param $service
22+
* @param string $method
23+
*/
24+
public function __construct($service, $method = '__invoke')
25+
{
26+
$this->service = $service;
27+
$this->method = $method;
28+
}
29+
30+
/**
31+
* @return string
32+
*/
33+
public function getService()
34+
{
35+
return $this->service;
36+
}
37+
38+
/**
39+
* @return string
40+
*/
41+
public function getMethod()
42+
{
43+
return $this->method;
44+
}
45+
}

src/Event/functions.php

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<?php
2+
3+
namespace PhpSchool\PhpWorkshop\Event;
4+
5+
if (!function_exists('PhpSchool\PhpWorkshop\Event\containerListener')) {
6+
7+
/**
8+
* @param string $service
9+
* @param string $method
10+
* @return ContainerListenerHelper
11+
*/
12+
function containerListener($service, $method = '__invoke')
13+
{
14+
return new ContainerListenerHelper($service, $method);
15+
}
16+
}

src/Factory/EventDispatcherFactory.php

Lines changed: 60 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,10 @@
44

55
use Interop\Container\ContainerInterface;
66
use PhpSchool\PhpWorkshop\Event\EventDispatcher;
7+
use PhpSchool\PhpWorkshop\Event\ContainerListenerHelper;
78
use PhpSchool\PhpWorkshop\Exception\InvalidArgumentException;
8-
use PhpSchool\PhpWorkshop\Listener\CodePatchListener;
9-
use PhpSchool\PhpWorkshop\Listener\PrepareSolutionListener;
10-
use PhpSchool\PhpWorkshop\Listener\SelfCheckListener;
119
use PhpSchool\PhpWorkshop\ResultAggregator;
10+
use PhpSchool\PhpWorkshop\Utils\Collection;
1211

1312
/**
1413
* Class EventDispatcherFactory
@@ -27,36 +26,59 @@ public function __invoke(ContainerInterface $container)
2726
{
2827
$dispatcher = new EventDispatcher($container->get(ResultAggregator::class));
2928

30-
$prepareSolutionListener = $container->get(PrepareSolutionListener::class);
31-
$dispatcher->listen('verify.start', $prepareSolutionListener);
32-
$dispatcher->listen('run.start', $prepareSolutionListener);
33-
34-
$codePatcherListener = $container->get(CodePatchListener::class);
35-
$dispatcher->listen('verify.pre.execute', [$codePatcherListener, 'patch']);
36-
$dispatcher->listen('verify.post.execute', [$codePatcherListener, 'revert']);
37-
$dispatcher->listen('run.start', [$codePatcherListener, 'patch']);
38-
$dispatcher->listen('run.finish', [$codePatcherListener, 'revert']);
39-
40-
$dispatcher->listen('verify.post.check', $container->get(SelfCheckListener::class));
41-
4229
//add listeners from config
4330
$eventListeners = $container->has('eventListeners') ? $container->get('eventListeners') : [];
4431

4532
if (!is_array($eventListeners)) {
4633
throw InvalidArgumentException::typeMisMatch('array', $eventListeners);
4734
}
48-
49-
array_walk($eventListeners, function ($listeners, $eventName) use ($dispatcher, $container) {
50-
if (!is_array($listeners)) {
51-
throw InvalidArgumentException::typeMisMatch('array', $listeners);
35+
36+
array_walk($eventListeners, function ($events) {
37+
if (!is_array($events)) {
38+
throw InvalidArgumentException::typeMisMatch('array', $events);
5239
}
40+
});
5341

42+
$eventListeners = $this->mergeListenerGroups($eventListeners);
43+
44+
array_walk($eventListeners, function ($listeners, $eventName) use ($dispatcher, $container) {
5445
$this->attachListeners($eventName, $listeners, $container, $dispatcher);
5546
});
5647

5748
return $dispatcher;
5849
}
5950

51+
/**
52+
* @param array $listeners
53+
* @return array
54+
*/
55+
private function mergeListenerGroups(array $listeners)
56+
{
57+
$listeners = new Collection($listeners);
58+
59+
return $listeners
60+
->keys()
61+
->reduce(function (Collection $carry, $listenerGroup) use ($listeners) {
62+
$events = new Collection($listeners->get($listenerGroup));
63+
64+
return $events
65+
->keys()
66+
->reduce(function (Collection $carry, $event) use ($events) {
67+
$listeners = $events->get($event);
68+
69+
if (!is_array($listeners)) {
70+
throw InvalidArgumentException::typeMisMatch('array', $listeners);
71+
}
72+
73+
return $carry->set(
74+
$event,
75+
array_merge($carry->get($event, []), $listeners)
76+
);
77+
}, $carry);
78+
}, new Collection)
79+
->getArrayCopy();
80+
}
81+
6082
/**
6183
* @param string $eventName
6284
* @param array $listeners
@@ -71,26 +93,32 @@ private function attachListeners(
7193
EventDispatcher $dispatcher
7294
) {
7395
array_walk($listeners, function ($listener) use ($eventName, $dispatcher, $container) {
74-
if (is_callable($listener)) {
75-
return $dispatcher->listen($eventName, $listener);
96+
if ($listener instanceof ContainerListenerHelper) {
97+
if (!$container->has($listener->getService())) {
98+
throw new InvalidArgumentException(
99+
sprintf('Container has no entry named: "%s"', $listener->getService())
100+
);
101+
}
102+
103+
return $dispatcher->listen($eventName, function (...$args) use ($container, $listener) {
104+
$service = $container->get($listener->getService());
105+
106+
if (!method_exists($service, $listener->getMethod())) {
107+
throw new InvalidArgumentException(
108+
sprintf('Method "%s" does not exist on "%s"', $listener->getMethod(), get_class($service))
109+
);
110+
}
111+
112+
$service->{$listener->getMethod()}(...$args);
113+
});
76114
}
77115

78-
if (!is_string($listener)) {
116+
if (!is_callable($listener)) {
79117
throw new InvalidArgumentException(
80118
sprintf('Listener must be a callable or a container entry for a callable service.')
81119
);
82120
}
83121

84-
if (!$container->has($listener)) {
85-
throw new InvalidArgumentException(sprintf('Container has no entry named: "%s"', $listener));
86-
}
87-
88-
$listener = $container->get($listener);
89-
90-
if (!is_callable($listener)) {
91-
throw InvalidArgumentException::typeMisMatch('callable', $listener);
92-
}
93-
94122
return $dispatcher->listen($eventName, $listener);
95123
});
96124
}

src/Utils/ArrayObject.php

Lines changed: 84 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ class ArrayObject implements IteratorAggregate, Countable
2525
*
2626
* @param array $array
2727
*/
28-
public function __construct(array $array)
28+
public function __construct(array $array = [])
2929
{
3030
$this->array = $array;
3131
}
@@ -42,6 +42,59 @@ public function map(callable $callback)
4242
return new static (array_map($callback, $this->array));
4343
}
4444

45+
/**
46+
* Run a callable over each item in the array and flatten the results by one level returning a new instance of
47+
* `ArrayObject` with the flattened items.
48+
*
49+
* @param callable $callback
50+
* @return static
51+
*/
52+
public function flatMap(callable $callback)
53+
{
54+
return $this->map($callback)->collapse();
55+
}
56+
57+
/**
58+
* Collapse an array of arrays into a single array returning a new instance of `ArrayObject`
59+
* with the collapsed items.
60+
*
61+
* @return static
62+
*/
63+
public function collapse()
64+
{
65+
$results = [];
66+
67+
foreach ($this->array as $item) {
68+
if (!is_array($item)) {
69+
continue;
70+
}
71+
72+
$results = array_merge($results, $item);
73+
}
74+
75+
return new static($results);
76+
}
77+
78+
/**
79+
* Reduce the items to a single value.
80+
*
81+
* @param callable $callback
82+
* @param mixed $initial
83+
* @return mixed
84+
*/
85+
public function reduce(callable $callback, $initial = null)
86+
{
87+
return array_reduce($this->array, $callback, $initial);
88+
}
89+
90+
/**
91+
* @return static
92+
*/
93+
public function keys()
94+
{
95+
return new static(array_keys($this->array));
96+
}
97+
4598
/**
4699
* Implode each item together using the provided glue.
47100
*
@@ -75,6 +128,36 @@ public function append($value)
75128
return new static(array_merge($this->array, [$value]));
76129
}
77130

131+
/**
132+
* Get an item at the given key.
133+
*
134+
* @param mixed $key
135+
* @param mixed $default
136+
* @return mixed
137+
*/
138+
public function get($key, $default = null)
139+
{
140+
if (isset($this->array[$key])) {
141+
return $this->array[$key];
142+
}
143+
144+
return $default;
145+
}
146+
147+
/**
148+
* Set the item at a given offset and return a new instance.
149+
*
150+
* @param mixed $key
151+
* @param mixed $value
152+
* @return static
153+
*/
154+
public function set($key, $value)
155+
{
156+
$items = $this->array;
157+
$items[$key] = $value;
158+
return new static($items);
159+
}
160+
78161
/**
79162
* Return an iterator containing all the items. Allows to `foreach` over.
80163
*

src/Utils/Collection.php

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<?php
2+
3+
namespace PhpSchool\PhpWorkshop\Utils;
4+
5+
/**
6+
* @author Aydin Hassan <[email protected]>
7+
*/
8+
class Collection extends ArrayObject
9+
{
10+
11+
}

0 commit comments

Comments
 (0)