1: <?php
2:
3: declare(strict_types=1);
4:
5: namespace LaravelUi5\OData\Console;
6:
7: use Illuminate\Console\Command;
8: use Illuminate\Filesystem\Filesystem;
9: use LaravelUi5\OData\Console\Concerns\ResolvesServices;
10: use LaravelUi5\OData\Service\Cache\EdmxWriter;
11: use LaravelUi5\OData\Service\Cache\ResolverMapWriter;
12: use LaravelUi5\OData\Service\Contracts\ODataServiceRegistryInterface;
13: use ReflectionClass;
14:
15: class CacheCommand extends Command
16: {
17: use ResolvesServices;
18:
19: protected $signature = 'odata:cache {--class= : Comma-separated FQCNs of additional OData services to cache (route-composed/bound services not in the registry)}';
20:
21: protected $description = 'Generate cached Edm PHP classes for OData services — the registry, plus any --class services (dev only)';
22:
23: public function handle(ODataServiceRegistryInterface $registry): int
24: {
25: if (app()->environment('production', 'staging')) {
26: $this->error('odata:cache must not be run in production or staging.');
27: $this->error('The generated Edm/ cache is committed to version control and deployed as-is.');
28: $this->error('Run odata:cache on your development machine, commit the result, then deploy.');
29:
30: return self::FAILURE;
31: }
32:
33: $services = $this->resolveServices($registry);
34:
35: if ($services === null) {
36: return self::FAILURE;
37: }
38:
39: // Pre-pass: each service must own its cache directory. Two services sharing a
40: // namespace would overwrite each other's Edm/ — and the warm path would then serve
41: // the wrong schema. Fail loud BEFORE writing anything, rather than silently.
42: $claimedBy = [];
43: foreach ($services as $service) {
44: $dir = dirname((new ReflectionClass($service))->getFileName()) . '/Edm';
45: if (isset($claimedBy[$dir])) {
46: $this->error(sprintf(
47: 'Cache directory collision: %s and %s both map to %s.',
48: $service::class,
49: $claimedBy[$dir],
50: $dir,
51: ));
52: $this->error('Each OData service must live in its own namespace/directory.');
53:
54: return self::FAILURE;
55: }
56: $claimedBy[$dir] = $service::class;
57: }
58:
59: $fs = new Filesystem();
60:
61: foreach ($services as $service) {
62: $reflected = new ReflectionClass($service);
63: $outputDir = dirname($reflected->getFileName()) . '/Edm';
64: $namespace = $reflected->getNamespaceName() . '\\Edm';
65:
66: // Remove stale cache so schema() takes the cold path.
67: if ($fs->isDirectory($outputDir)) {
68: $fs->deleteDirectory($outputDir);
69: $this->info("Cleared {$outputDir}");
70: }
71:
72: $edmx = $service->schema()->getEdmx();
73:
74: $writer = new EdmxWriter(
75: edmx: $edmx,
76: outputDir: $outputDir,
77: namespace: $namespace,
78: output: fn(string $line) => $this->info($line),
79: );
80:
81: $writer->write();
82:
83: $resolverMap = $service->resolverMap();
84: ResolverMapWriter::write($service, $resolverMap);
85:
86: $this->info("Cached: {$reflected->getName()}");
87: }
88:
89: // Refresh autoloader so the newly generated Edm classes are discoverable.
90: // Skipped under tests — it shells out to composer and would regenerate the
91: // package's own autoloader.
92: if (!app()->runningUnitTests()) {
93: $this->info('Refreshing autoloader...');
94: passthru('composer dump-autoload 2>&1');
95: }
96:
97: $this->info('OData schema cached successfully.');
98:
99: return self::SUCCESS;
100: }
101: }
102: