Lines 97.14% 34 / 35
Methods 75.00% 3 / 4
Classes 0.00% 0 / 1
Covered by tests of size
Name Lines Methods CRAP
 __construct 100.00% 4 / 4 100.00% 1 / 1 2
 getContainer 100.00% 25 / 25 100.00% 1 / 1 9
 containerClass 100.00% 1 / 1 100.00% 1 / 1 1
 writeCache 80.00% 4 / 5 0.00% 0 / 1 2.03
30final class ContainerCacheManager
31{
32    private readonly string $cacheDir;
33
34    /** @var callable(string): void */
35    private $logError;
36
37    /**
38     * @param callable(string): void|null $logError Defaults to error_log(); the container
39     *                                              is not built yet, so no PSR logger exists.
40     */
41    public function __construct(string $cacheDir, ?callable $logError = null)
42    {
43        $this->cacheDir = rtrim(string: $cacheDir, characters: '/');
44        $this->logError = $logError ?? error_log(...);
45
46        if (!is_dir($this->cacheDir)) {
47            mkdir(directory: $this->cacheDir, permissions: 0o755, recursive: true);
48        }
49    }
50
51    /**
52     * Returns the compiled container, dumping it to the cache on first use. Every service
53     * and alias is marked public before compiling because phpMyFAQ resolves services from
54     * the container at runtime. If compiling or dumping fails, the container falls back to
55     * a fresh, uncompiled builder, so the application keeps working without the cache.
56     *
57     * @param callable(): ContainerBuilder $containerBuilderFactory
58     */
59    public function getContainer(callable $containerBuilderFactory): ContainerInterface
60    {
61        $containerClass = $this->containerClass();
62        $cacheFile = $this->cacheDir . '/' . $containerClass . '.php';
63
64        if (!class_exists($containerClass, autoload: false) && is_file($cacheFile)) {
65            require_once $cacheFile;
66        }
67
68        if (class_exists($containerClass, autoload: false)) {
69            /* @mago-expect analysis:unknown-class-instantiation - the dumped container class only exists at runtime */
70            $container = new $containerClass();
71            if ($container instanceof ContainerInterface) {
72                return $container;
73            }
74        }
75
76        try {
77            $containerBuilder = $containerBuilderFactory();
78
79            foreach ($containerBuilder->getDefinitions() as $definition) {
80                $definition->setPublic(true);
81            }
82
83            foreach ($containerBuilder->getAliases() as $alias) {
84                $alias->setPublic(true);
85            }
86
87            $containerBuilder->compile();
88
89            $dump = new PhpDumper($containerBuilder)->dump(['class' => $containerClass]);
90            if (is_string($dump)) {
91                $this->writeCache($cacheFile, $dump);
92            }
93
94            return $containerBuilder;
95        } catch (Throwable $throwable) {
96            ($this->logError)(sprintf(
97                'phpMyFAQ: cannot compile the DI container (%s), falling back to the uncompiled container: %s',
98                $throwable::class,
99                $throwable->getMessage(),
100            ));
101
102            return $containerBuilderFactory();
103        }
104    }
105
106    /**
107     * The class name embeds a cache-directory hash, so containers dumped into
108     * different directories never collide inside one PHP process.
109     */
110    private function containerClass(): string
111    {
112        return 'PMFCompiledContainer_' . substr(md5($this->cacheDir), offset: 0, length: 12);
113    }
114
115    private function writeCache(string $cacheFile, string $dump): void
116    {
117        $temporaryFile = tempnam($this->cacheDir, prefix: 'container');
118        if ($temporaryFile === false) {
119            return;
120        }
121
122        file_put_contents($temporaryFile, $dump);
123        rename($temporaryFile, $cacheFile);
124    }
125}