Commit | Line | Data |
---|---|---|
b233b6ca TO |
1 | <?php |
2 | ||
3 | /** | |
4 | * The Mixlib class is a utility for downloading/listing available mixins. | |
5 | * It is used by some test-infra and by civix. | |
6 | */ | |
7 | class Mixlib { | |
8 | ||
9 | /** | |
10 | * Local path to the mixlib folder. | |
11 | * | |
12 | * @var string|null | |
13 | */ | |
14 | private $mixlibDir; | |
15 | ||
16 | /** | |
17 | * Public URL of the mixlib folder. | |
18 | * | |
19 | * @var string|null | |
20 | */ | |
21 | private $mixlibUrl; | |
22 | ||
23 | private $cache = []; | |
24 | ||
25 | /** | |
26 | * Mixlib constructor. | |
27 | * | |
28 | * @param string|NULL $mixlibDir | |
29 | * Ex: Civi::paths()->getPath('[civicrm.root]/mixin') | |
30 | * @param string|NULL $mixlibUrl | |
31 | * Ex: "https://raw.githubusercontent.com/civicrm/civicrm-core/5.45/mixin" | |
32 | * Ex: "https://raw.githubusercontent.com/totten/civicrm-core/master-mix-dec/mixin" | |
33 | */ | |
34 | public function __construct(?string $mixlibDir = NULL, ?string $mixlibUrl = NULL) { | |
35 | $this->mixlibDir = $mixlibDir ?: dirname(__DIR__, 3) . '/mixin'; | |
36 | $this->mixlibUrl = $mixlibUrl; | |
37 | } | |
38 | ||
39 | public function getList(): array { | |
40 | if ($this->mixlibDir === NULL || !file_exists($this->mixlibDir)) { | |
41 | throw new \RuntimeException("Cannot get list of available mixins"); | |
42 | } | |
43 | ||
44 | if (isset($this->cache['getList'])) { | |
45 | return $this->cache['getList']; | |
46 | } | |
47 | ||
48 | $dirs = (array) glob($this->mixlibDir . '/*@*'); | |
49 | $mixinNames = []; | |
50 | foreach ($dirs as $dir) { | |
51 | if (is_dir($dir)) { | |
52 | $mixinNames[] = basename($dir); | |
53 | } | |
54 | } | |
55 | sort($mixinNames); | |
56 | $this->cache['getList'] = $mixinNames; | |
57 | return $mixinNames; | |
58 | } | |
59 | ||
60 | /** | |
61 | * @param string $mixin | |
62 | * | |
63 | * @return array | |
64 | * Item with keys: | |
65 | * - mixinName: string, eg 'mgd-php' | |
66 | * - mixinVersion: string, eg '1.0.2' | |
67 | * - mixinConstraint: string, eg 'mgd-php@1.0.2' | |
68 | * - mixinFile: string, eg 'mgd-php@1.0.2.mixin.php' | |
69 | * - src: string, unevaluated PHP source | |
70 | */ | |
71 | public function get(string $mixin) { | |
72 | if (isset($this->cache["parsed:$mixin"])) { | |
73 | return $this->cache["parsed:$mixin"]; | |
74 | } | |
75 | ||
76 | $phpCode = $this->getSourceCode($mixin); | |
77 | $mixinSpec = $this->parseString($phpCode); | |
78 | $mixinSpec['mixinName'] = $mixinSpec['mixinName'] ?? preg_replace(';@.*$;', '', $mixin); | |
79 | ||
80 | $parts = explode('@', $mixin); | |
81 | $effectiveVersion = !empty($mixinSpec['mixinVersion']) ? $mixinSpec['mixinVersion'] : ($parts[1] ?? ''); | |
82 | if ($effectiveVersion) { | |
83 | $mixinSpec = array_merge([ | |
84 | 'mixinConstraint' => $mixinSpec['mixinName'] . '@' . $effectiveVersion, | |
85 | 'mixinFile' => $mixinSpec['mixinName'] . '@' . $effectiveVersion . '.mixin.php', | |
86 | ], $mixinSpec); | |
87 | } | |
88 | else { | |
89 | $mixinSpec = array_merge([ | |
90 | 'mixinFile' => $mixinSpec['mixinName'] . '.mixin.php', | |
91 | ], $mixinSpec); | |
92 | } | |
93 | $mixinSpec['src'] = $phpCode; | |
94 | $this->cache["parsed:$mixin"] = $mixinSpec; | |
95 | ||
96 | return $this->cache["parsed:$mixin"]; | |
97 | } | |
98 | ||
99 | /** | |
100 | * Consolidate and retrieve the listed mixins. | |
101 | * | |
102 | * @param array $mixinConstraints | |
103 | * Ex: ['foo@1.0', 'bar@1.2', 'bar@1.3'] | |
104 | * @return array | |
105 | * Ex: ['foo@1.0' => array, 'bar@1.3' => array] | |
106 | */ | |
107 | public function consolidate(array $mixinConstraints): array { | |
108 | // Find and remove duplicate constraints. Pick tightest constraint. | |
109 | // array(string $mixinName => string $mixinVersion) | |
110 | $preferredVersions = []; | |
111 | foreach ($mixinConstraints as $mixinName) { | |
112 | [$name, $version] = explode('@', $mixinName); | |
113 | if (!isset($preferredVersions[$name])) { | |
114 | $preferredVersions[$name] = $version; | |
115 | } | |
116 | elseif (version_compare($version, $preferredVersions[$name], '>=')) { | |
117 | $preferredVersions[$name] = $version; | |
118 | } | |
119 | } | |
120 | ||
121 | // Resolve current versions matching constraint. | |
122 | $result = []; | |
123 | foreach ($preferredVersions as $mixinName => $mixinVersion) { | |
124 | $result[] = $mixinName . '@' . $mixinVersion; | |
125 | } | |
126 | sort($result); | |
127 | return $result; | |
128 | } | |
129 | ||
130 | /** | |
131 | * Consolidate and retrieve the listed mixins. | |
132 | * | |
133 | * @param array $mixinConstraints | |
134 | * Ex: ['foo@1.0', 'bar@1.2', 'bar@1.3'] | |
135 | * @return array | |
136 | * Ex: ['foo@1.0' => array, 'bar@1.3' => array] | |
137 | */ | |
138 | public function resolve(array $mixinConstraints): array { | |
139 | $mixinConstraints = $this->consolidate($mixinConstraints); | |
140 | ||
141 | $result = []; | |
142 | foreach ($mixinConstraints as $mixinConstraint) { | |
143 | [$expectName, $expectVersion] = explode('@', $mixinConstraint); | |
144 | $mixin = $this->get($mixinConstraint); | |
145 | $this->assertValid($mixin); | |
146 | if (!version_compare($mixin['mixinVersion'], $expectVersion, '>=') || $mixin['mixinName'] !== $expectName) { | |
147 | throw new \RuntimeException(sprintf("Received incompatible version (expected=\"%s@%s\", actual=\"%s@%s\")", $expectName, $expectVersion, $mixin['mixinName'], $mixin['mixinVersion'])); | |
148 | } | |
149 | $result[$mixin['mixinConstraint']] = $mixin; | |
150 | } | |
151 | return $result; | |
152 | } | |
153 | ||
154 | /** | |
155 | * @param string $mixin | |
156 | * Ex: 'foo@1.2.3', 'foo-bar@4.5.6', 'polyfill', | |
157 | * @return string | |
158 | */ | |
159 | protected function getSourceCode(string $mixin): string { | |
160 | if ($mixin === 'polyfill') { | |
161 | $file = 'polyfill.php'; | |
162 | } | |
163 | elseif (preg_match(';^([-\w]+)@(\d+)([\.\d]+)?;', $mixin, $m)) { | |
164 | // Get the last revision within the major series. | |
165 | $file = sprintf('%s@%s/mixin.php', $m[1], $m[2]); | |
166 | } | |
167 | else { | |
168 | throw new \RuntimeException("Failed to parse mixin name ($mixin)"); | |
169 | } | |
170 | ||
171 | if ($this->mixlibDir && file_exists($this->mixlibDir . '/' . $file)) { | |
172 | return file_get_contents($this->mixlibDir . '/' . $file); | |
173 | } | |
174 | ||
175 | if ($this->mixlibUrl) { | |
176 | $url = $this->mixlibUrl . '/' . $file; | |
177 | $download = file_get_contents($url); | |
178 | if (!empty($download)) { | |
179 | $this->cache["src:$mixin"] = $download; | |
180 | return $download; | |
181 | } | |
182 | } | |
183 | ||
184 | throw new \RuntimeException("Failed to locate $file (mixlibDir={$this->mixlibDir}, mixlibUrl={$this->mixlibUrl})"); | |
185 | } | |
186 | ||
187 | public function assertValid(array $mixin): array { | |
188 | if (empty($mixin['mixinVersion'])) { | |
189 | throw new \RuntimeException("Invalid {$mixin["file"]}. There is no @mixinVersion annotation."); | |
190 | } | |
191 | if (empty($mixin['mixinVersion'])) { | |
192 | throw new \RuntimeException("Invalid {$mixin["file"]}. There is no @mixinName annotation."); | |
193 | } | |
194 | return $mixin; | |
195 | } | |
196 | ||
197 | /** | |
198 | * @param string $phpCode | |
199 | * @return array | |
200 | */ | |
201 | protected function parseString(string $phpCode): array { | |
202 | $commmentTokens = [T_DOC_COMMENT, T_COMMENT, T_FUNC_C, T_METHOD_C, T_TRAIT_C, T_CLASS_C]; | |
203 | $mixinSpec = []; | |
204 | foreach (token_get_all($phpCode) as $token) { | |
205 | if (is_array($token) && in_array($token[0], $commmentTokens)) { | |
206 | $mixinSpec = $this->parseComment($token[1]); | |
207 | break; | |
208 | } | |
209 | } | |
210 | return $mixinSpec; | |
211 | } | |
212 | ||
213 | protected function parseComment(string $comment): array { | |
214 | $info = []; | |
215 | $param = NULL; | |
216 | foreach (preg_split("/((\r?\n)|(\r\n?))/", $comment) as $num => $line) { | |
217 | if (!$num || strpos($line, '*/') !== FALSE) { | |
218 | continue; | |
219 | } | |
220 | $line = ltrim(trim($line), '*'); | |
221 | if (strlen($line) && $line[0] === ' ') { | |
222 | $line = substr($line, 1); | |
223 | } | |
224 | if (strpos(ltrim($line), '@') === 0) { | |
225 | $words = explode(' ', ltrim($line, ' @')); | |
226 | $key = array_shift($words); | |
227 | $param = NULL; | |
228 | if ($key == 'var') { | |
229 | $info['type'] = explode('|', $words[0]); | |
230 | } | |
231 | elseif ($key == 'return') { | |
232 | $info['return'] = explode('|', $words[0]); | |
233 | } | |
234 | elseif ($key == 'options' || $key == 'ui_join_filters') { | |
235 | $val = str_replace(', ', ',', implode(' ', $words)); | |
236 | $info[$key] = explode(',', $val); | |
237 | } | |
238 | elseif ($key == 'throws' || $key == 'see') { | |
239 | $info[$key][] = implode(' ', $words); | |
240 | } | |
241 | elseif ($key == 'param' && $words) { | |
242 | $type = $words[0][0] !== '$' ? explode('|', array_shift($words)) : NULL; | |
243 | $param = rtrim(array_shift($words), '-:()/'); | |
244 | $info['params'][$param] = [ | |
245 | 'type' => $type, | |
246 | 'description' => $words ? ltrim(implode(' ', $words), '-: ') : '', | |
247 | 'comment' => '', | |
248 | ]; | |
249 | } | |
250 | else { | |
251 | // Unrecognized annotation, but we'll duly add it to the info array | |
252 | $val = implode(' ', $words); | |
253 | $info[$key] = strlen($val) ? $val : TRUE; | |
254 | } | |
255 | } | |
256 | elseif ($param) { | |
257 | $info['params'][$param]['comment'] .= $line . "\n"; | |
258 | } | |
259 | elseif ($num == 1) { | |
260 | $info['description'] = ucfirst($line); | |
261 | } | |
262 | elseif (!$line) { | |
263 | if (isset($info['comment'])) { | |
264 | $info['comment'] .= "\n"; | |
265 | } | |
266 | else { | |
267 | $info['comment'] = NULL; | |
268 | } | |
269 | } | |
270 | // For multi-line description. | |
271 | elseif (count($info) === 1 && isset($info['description']) && substr($info['description'], -1) !== '.') { | |
272 | $info['description'] .= ' ' . $line; | |
273 | } | |
274 | else { | |
275 | $info['comment'] = isset($info['comment']) ? "{$info['comment']}\n$line" : $line; | |
276 | } | |
277 | } | |
278 | if (isset($info['comment'])) { | |
279 | $info['comment'] = rtrim($info['comment']); | |
280 | } | |
281 | return $info; | |
282 | } | |
283 | ||
284 | } |