HookLoader::__construct()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 8
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 5
c 1
b 0
f 0
nc 1
nop 1
dl 0
loc 8
rs 10
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Churn\Event;
6
7
use Churn\Event\Hook\AfterAnalysisHook;
8
use Churn\Event\Hook\AfterFileAnalysisHook;
9
use Churn\Event\Hook\BeforeAnalysisHook;
10
use Churn\Event\Subscriber\AfterAnalysisHookDecorator;
11
use Churn\Event\Subscriber\AfterFileAnalysisHookDecorator;
12
use Churn\Event\Subscriber\BeforeAnalysisHookDecorator;
13
use Churn\File\FileHelper;
14
use InvalidArgumentException;
15
16
/**
17
 * @internal
18
 */
19
final class HookLoader
20
{
21
    /**
22
     * @var array<string, string>
23
     * @psalm-var array<
24
     * class-string<\Churn\Event\Subscriber\HookDecorator>,
25
     * class-string<AfterAnalysisHook>|class-string<AfterFileAnalysisHook>|class-string<BeforeAnalysisHook>
26
     * >
27
     */
28
    private $decorators;
29
30
    /**
31
     * @var string
32
     */
33
    private $basePath;
34
35
    /**
36
     * @param string $basePath The base path for the hooks files.
37
     */
38
    public function __construct(string $basePath)
39
    {
40
        $this->decorators = [
41
            AfterAnalysisHookDecorator::class => AfterAnalysisHook::class,
42
            AfterFileAnalysisHookDecorator::class => AfterFileAnalysisHook::class,
43
            BeforeAnalysisHookDecorator::class => BeforeAnalysisHook::class,
44
        ];
45
        $this->basePath = $basePath;
46
    }
47
48
    /**
49
     * @param array<string> $hooks The list of hooks to attach.
50
     * @param Broker $broker The event broker.
51
     * @throws InvalidArgumentException If a hook is invalid.
52
     */
53
    public function attachHooks(array $hooks, Broker $broker): void
54
    {
55
        if ([] === $hooks) {
56
            return;
57
        }
58
59
        foreach ($hooks as $hook) {
60
            if (!$this->attach($hook, $broker)) {
61
                throw new InvalidArgumentException('Invalid hook: ' . $hook);
62
            }
63
        }
64
    }
65
66
    /**
67
     * @param string $hookPath The class name or the file path of the hook.
68
     * @param Broker $broker The event broker.
69
     */
70
    public function attach(string $hookPath, Broker $broker): bool
71
    {
72
        $foundSubscriber = false;
73
        $subscribers = [];
74
75
        foreach ($this->loadHooks($hookPath) as $hookName) {
76
            $subscribers = \array_merge($subscribers, $this->hookToSubscribers($hookName));
77
        }
78
79
        foreach ($subscribers as $subscriber) {
80
            $broker->subscribe($subscriber);
81
            $foundSubscriber = true;
82
        }
83
84
        return $foundSubscriber;
85
    }
86
87
    /**
88
     * @param string $hookPath The class name or the file path of the hook.
89
     * @return array<string>
90
     * @psalm-return array<class-string>
91
     */
92
    private function loadHooks(string $hookPath): array
93
    {
94
        if (\class_exists($hookPath)) {
95
            return [$hookPath];
96
        }
97
98
        $hookPath = FileHelper::toAbsolutePath($hookPath, $this->basePath);
99
100
        if (!\is_file($hookPath)) {
101
            return [];
102
        }
103
104
        $numberOfClasses = \count(\get_declared_classes());
105
        \Churn\Event\includeOnce($hookPath);
106
107
        return \array_slice(\get_declared_classes(), $numberOfClasses);
108
    }
109
110
    /**
111
     * @param string $hookName The class name of the hook.
112
     * @return array<\Churn\Event\Subscriber\HookDecorator>
113
     * @psalm-param class-string $hookName
114
     */
115
    private function hookToSubscribers(string $hookName): array
116
    {
117
        $subscribers = [];
118
119
        foreach ($this->decorators as $decorator => $hook) {
120
            if (!\is_subclass_of($hookName, $hook)) {
121
                continue;
122
            }
123
124
            $subscribers[] = new $decorator($hookName);
125
        }
126
127
        return $subscribers;
128
    }
129
}
130
131
/**
132
 * Scope isolated include.
133
 *
134
 * Prevents access to $this/self from included files.
135
 *
136
 * @param string $file The PHP file to include.
137
 */
138
function includeOnce(string $file): void
139
{
140
    include_once $file;
141
}
142