PageRenderTime 26ms CodeModel.GetById 21ms RepoModel.GetById 0ms app.codeStats 0ms

/lib/Autoconfig/AutoconfigGenerator.php

https://github.com/ICanBoogie/ICanBoogie
PHP | 520 lines | 313 code | 88 blank | 119 comment | 8 complexity | 73263a92e868beac5819f9021f70d468 MD5 | raw file
  1. <?php
  2. /*
  3. * This file is part of the ICanBoogie package.
  4. *
  5. * (c) Olivier Laviale <olivier.laviale@gmail.com>
  6. *
  7. * For the full copyright and license information, please view the LICENSE
  8. * file that was distributed with this source code.
  9. */
  10. namespace ICanBoogie\Autoconfig;
  11. use Composer\Package\Package;
  12. use Composer\Package\PackageInterface;
  13. use Composer\Package\RootPackage;
  14. use Composer\Package\RootPackageInterface;
  15. use Composer\Util\Filesystem;
  16. use ICanBoogie\Accessor\AccessorTrait;
  17. use Throwable;
  18. use function array_keys;
  19. use function array_map;
  20. use function array_merge;
  21. use function explode;
  22. use function file_put_contents;
  23. use function getcwd;
  24. use function implode;
  25. use function ksort;
  26. use function realpath;
  27. /**
  28. * @codeCoverageIgnore
  29. *
  30. * @property-read array<string, Package> $packages
  31. * @property-read RootPackageInterface $root_package
  32. */
  33. final class AutoconfigGenerator
  34. {
  35. use AccessorTrait;
  36. /**
  37. * @var Package[]
  38. */
  39. private $packages;
  40. /**
  41. * @return array<string, Package>
  42. */
  43. private function get_packages(): iterable
  44. {
  45. foreach ($this->packages as [ $package, $pathname ])
  46. {
  47. if (!$pathname)
  48. {
  49. $pathname = getcwd();
  50. }
  51. yield $pathname => $package;
  52. }
  53. }
  54. private function get_root_package(): ?RootPackageInterface
  55. {
  56. foreach ($this->packages as [ $package ])
  57. {
  58. if ($package instanceof RootPackageInterface)
  59. {
  60. return $package;
  61. }
  62. }
  63. return null;
  64. }
  65. /**
  66. * @var string
  67. */
  68. private $destination;
  69. /**
  70. * @var Filesystem
  71. */
  72. private $filesystem;
  73. /**
  74. * @var array<string, mixed>
  75. */
  76. private $fragments = [];
  77. /**
  78. * @var array<string, int>
  79. */
  80. private $weights = [];
  81. /**
  82. * @var ExtensionAbstract[]
  83. */
  84. private $extensions = [];
  85. /**
  86. * @param Package[] $packages
  87. *
  88. * @uses get_root_package
  89. */
  90. public function __construct(array $packages, string $destination)
  91. {
  92. $this->packages = $packages;
  93. $this->destination = $destination;
  94. $this->filesystem = new Filesystem;
  95. }
  96. /**
  97. * Search for autoconfig fragments defined by the packages and create the autoconfig file.
  98. *
  99. * @throws Throwable
  100. */
  101. public function __invoke(): void
  102. {
  103. [ $fragments, $weights ] = $this->collect_fragments();
  104. $this->fragments = $fragments;
  105. $this->weights = $weights;
  106. $this->extensions = $this->collect_extensions($fragments);
  107. $this->validate_fragments($fragments);
  108. $this->write();
  109. }
  110. public function find_shortest_path_code(string $to): string
  111. {
  112. return $this->filesystem->findShortestPathCode($this->destination, $to);
  113. }
  114. public function render_entry(string $key, string $value): string
  115. {
  116. return <<<EOT
  117. '$key' => $value,
  118. EOT;
  119. }
  120. /**
  121. * @param array<string, mixed> $items
  122. */
  123. public function render_array_entry(string $key, array $items, callable $renderer): string
  124. {
  125. $rendered_items = implode(array_map(function ($item, $key) use ($renderer) {
  126. return "\t\t" . $renderer($item, $key) . ",\n";
  127. }, $items, array_keys($items)));
  128. return <<<EOT
  129. '$key' => [
  130. $rendered_items
  131. ],
  132. EOT;
  133. }
  134. /**
  135. * Collect autoconfig fragments from packages.
  136. *
  137. * @return array{0: array<string, mixed>, 1: array<string, int>} An array with the collected fragments and their weights.
  138. */
  139. private function collect_fragments(): array
  140. {
  141. $fragments = [];
  142. $weights = [];
  143. foreach ($this->get_packages() as $pathname => $package)
  144. {
  145. $pathname = realpath($pathname);
  146. assert(is_string($pathname));
  147. $fragment = $this->find_fragment($package);
  148. if (!$fragment)
  149. {
  150. continue;
  151. }
  152. $fragments[$pathname] = $fragment;
  153. $weights[$pathname] = $this->resolve_config_weight($package, $fragment);
  154. }
  155. return [ $fragments, $weights ];
  156. }
  157. /**
  158. * Try to find autoconfig fragment of package.
  159. *
  160. * @return array<string, mixed>|null The autoconfig fragment, or `null` if the package doesn't define one.
  161. */
  162. private function find_fragment(PackageInterface $package): ?array
  163. {
  164. return $package->getExtra()['icanboogie'] ?? null;
  165. }
  166. /**
  167. * @param array<string, mixed> $fragments
  168. *
  169. * @return ExtensionAbstract[]
  170. */
  171. private function collect_extensions(array $fragments): array
  172. {
  173. $extensions = [];
  174. foreach ($fragments as $fragment)
  175. {
  176. if (empty($fragment[SchemaOptions::AUTOCONFIG_EXTENSION]))
  177. {
  178. continue;
  179. }
  180. $class = $fragment[SchemaOptions::AUTOCONFIG_EXTENSION];
  181. $extensions[] = new $class($this);
  182. }
  183. return $extensions;
  184. }
  185. /**
  186. * Validate fragments against schema.
  187. *
  188. * @param array<string, mixed> $fragments
  189. *
  190. * @throws Throwable
  191. */
  192. private function validate_fragments(array $fragments): void
  193. {
  194. $data = Schema::read(__DIR__ . '/schema.json');
  195. $properties = &$data->properties;
  196. $set_property = function ($property, array $data) use (&$properties): void
  197. {
  198. $properties->$property = (object) $data;
  199. };
  200. foreach ($this->extensions as $extension)
  201. {
  202. $extension->alter_schema($set_property);
  203. }
  204. $schema = new Schema($data);
  205. foreach ($fragments as $pathname => $fragment)
  206. {
  207. $schema->validate(Schema::normalize_data($fragment), $pathname);
  208. }
  209. }
  210. /**
  211. * @param array<string, mixed> $fragment
  212. */
  213. private function resolve_config_weight(PackageInterface $package, array $fragment): int
  214. {
  215. if (isset($fragment[SchemaOptions::CONFIG_WEIGHT]))
  216. {
  217. return $fragment[SchemaOptions::CONFIG_WEIGHT];
  218. }
  219. if ($package instanceof RootPackage)
  220. {
  221. return Autoconfig::CONFIG_WEIGHT_APP;
  222. }
  223. return Autoconfig::CONFIG_WEIGHT_FRAMEWORK;
  224. }
  225. /**
  226. * Synthesize the autoconfig fragments into a single array.
  227. *
  228. * @return array<string, mixed>
  229. */
  230. private function synthesize(): array
  231. {
  232. static $mapping = [
  233. SchemaOptions::CONFIG_CONSTRUCTOR => Autoconfig::CONFIG_CONSTRUCTOR,
  234. SchemaOptions::CONFIG_PATH => Autoconfig::CONFIG_PATH,
  235. SchemaOptions::LOCALE_PATH => Autoconfig::LOCALE_PATH,
  236. SchemaOptions::AUTOCONFIG_FILTERS => Autoconfig::AUTOCONFIG_FILTERS,
  237. SchemaOptions::APP_PATHS => Autoconfig::APP_PATHS,
  238. ];
  239. $config = [
  240. Autoconfig::CONFIG_CONSTRUCTOR => [],
  241. Autoconfig::CONFIG_PATH => [],
  242. Autoconfig::LOCALE_PATH => [],
  243. Autoconfig::AUTOCONFIG_FILTERS => [],
  244. Autoconfig::APP_PATHS => []
  245. ];
  246. foreach ($this->fragments as $path => $fragment)
  247. {
  248. foreach ($fragment as $key => $value)
  249. {
  250. switch ($key)
  251. {
  252. case SchemaOptions::CONFIG_CONSTRUCTOR:
  253. case SchemaOptions::AUTOCONFIG_FILTERS:
  254. case SchemaOptions::APP_PATHS:
  255. $key = $mapping[$key];
  256. $config[$key] = array_merge($config[$key], (array) $value);
  257. break;
  258. case SchemaOptions::CONFIG_PATH:
  259. foreach ((array) $value as $v)
  260. {
  261. $config[Autoconfig::CONFIG_PATH][] = [
  262. $this->find_shortest_path_code("$path/$v"),
  263. $this->weights[$path]
  264. ];
  265. }
  266. break;
  267. case SchemaOptions::LOCALE_PATH:
  268. $key = $mapping[$key];
  269. foreach ((array) $value as $v)
  270. {
  271. $config[$key][] = $this->find_shortest_path_code("$path/$v");
  272. }
  273. break;
  274. }
  275. }
  276. }
  277. foreach ($this->extensions as $extension)
  278. {
  279. $extension->synthesize($config);
  280. }
  281. return $config;
  282. }
  283. /**
  284. * Render the synthesized autoconfig into a string.
  285. *
  286. * @param array<string, mixed> $config Synthesized config.
  287. */
  288. private function render(array $config = []): string
  289. {
  290. if (!$config)
  291. {
  292. $config = $this->synthesize();
  293. }
  294. $class = __CLASS__;
  295. $rendered_entries = [
  296. $this->render_entry(
  297. Autoconfig::BASE_PATH,
  298. 'getcwd()'
  299. ),
  300. $this->render_entry(
  301. Autoconfig::APP_PATH,
  302. 'getcwd() . DIRECTORY_SEPARATOR . "' . Autoconfig::DEFAULT_APP_DIRECTORY . '"'
  303. ),
  304. $this->render_app_paths($config),
  305. $this->render_locale_paths($config),
  306. $this->render_config_constructor($config),
  307. $this->render_filters($config),
  308. $this->render_config_path($config),
  309. ];
  310. foreach ($this->extensions as $extension)
  311. {
  312. $rendered_entries[] = $extension->render();
  313. }
  314. $extension_render = implode(array_map(function ($rendered_entry) {
  315. return "\n$rendered_entry\n";
  316. }, $rendered_entries));
  317. return <<<EOT
  318. <?php
  319. /*
  320. * DO NOT EDIT THIS FILE
  321. *
  322. * @generated by $class
  323. * @see https://icanboogie.org/docs/4.0/autoconfig
  324. */
  325. return [
  326. $extension_render
  327. ];
  328. EOT;
  329. }
  330. /**
  331. * Render the {@link Autoconfig::APP_PATHS} part of the autoconfig.
  332. *
  333. * @param array<string, mixed> $config
  334. */
  335. private function render_app_paths(array $config): string
  336. {
  337. return $this->render_array_entry(
  338. Autoconfig::APP_PATHS,
  339. $config[Autoconfig::APP_PATHS],
  340. function ($item): string
  341. {
  342. return (string) $item;
  343. }
  344. );
  345. }
  346. /**
  347. * Render the {@link Autoconfig::LOCALE_PATH} part of the autoconfig.
  348. *
  349. * @param array<string, mixed> $config
  350. */
  351. private function render_locale_paths(array $config): string
  352. {
  353. return $this->render_array_entry(
  354. Autoconfig::LOCALE_PATH,
  355. $config[Autoconfig::LOCALE_PATH],
  356. function ($item): string
  357. {
  358. return (string) $item;
  359. }
  360. );
  361. }
  362. /**
  363. * Render the {@link Autoconfig::CONFIG_CONSTRUCTOR} part of the autoconfig.
  364. *
  365. * @param array<string, mixed> $config
  366. */
  367. private function render_config_constructor(array $config): string
  368. {
  369. $synthesized = $config[Autoconfig::CONFIG_CONSTRUCTOR];
  370. ksort($synthesized);
  371. return $this->render_array_entry(
  372. Autoconfig::CONFIG_CONSTRUCTOR,
  373. $synthesized,
  374. function ($constructor, $name): string
  375. {
  376. [ $callback, $from ] = explode('#', $constructor) + [ 1 => null ];
  377. return "'$name' => [ '$callback'" . ($from ? ", '$from'" : '') . " ]";
  378. }
  379. );
  380. }
  381. /**
  382. * Render the {@link Autoconfig::CONFIG_PATH} part of the autoconfig.
  383. *
  384. * @param array<string, mixed> $config
  385. */
  386. private function render_config_path(array $config): string
  387. {
  388. return $this->render_array_entry(
  389. Autoconfig::CONFIG_PATH,
  390. $config[Autoconfig::CONFIG_PATH],
  391. function ($item): string
  392. {
  393. [ $path_code, $weight ] = $item;
  394. return "$path_code => $weight";
  395. }
  396. );
  397. }
  398. /**
  399. * Render the {@link Autoconfig::AUTOCONFIG_FILTERS} part of the autoconfig.
  400. *
  401. * @param array<string, mixed> $config
  402. */
  403. private function render_filters(array $config): string
  404. {
  405. return $this->render_array_entry(
  406. Autoconfig::AUTOCONFIG_FILTERS,
  407. $config[Autoconfig::AUTOCONFIG_FILTERS],
  408. function ($callable): string
  409. {
  410. return "'$callable'";
  411. }
  412. );
  413. }
  414. /**
  415. * Write the autoconfig file.
  416. *
  417. * @throws Throwable
  418. */
  419. private function write(): void
  420. {
  421. try
  422. {
  423. file_put_contents($this->destination, $this->render());
  424. echo "Created Autoconfig in $this->destination\n";
  425. }
  426. catch (Throwable $e)
  427. {
  428. echo $e;
  429. throw $e;
  430. }
  431. }
  432. }