1+ <?php
2+ /**
3+ * OPcache GUI block
4+ *
5+ * Responsible for preparing server-side data for the admin UI:
6+ * - Exposes configuration for the React UI
7+ * - Computes performance/warning messages for OPcache status card
8+ *
9+ * @author Ilan Parmentier
10+ * @license MIT License
11+ */
12+ declare (strict_types=1 );
13+
14+ namespace Amadeco \OpcacheGui \Block \Adminhtml ;
15+
16+ use Amnuts \Opcache \Service as OpcacheService ;
17+ use Magento \Backend \Block \Template ;
18+ use Magento \Backend \Block \Template \Context ;
19+ use Magento \Framework \Serialize \Serializer \Json as JsonSerializer ;
20+
21+ class Gui extends Template
22+ {
23+ /** Minimum free memory (bytes) before emitting a low-memory warning. */
24+ private const MIN_FREE_MEMORY_BYTES = 32 * 1024 * 1024 ; // 32MB
25+
26+ /** Recommended minimum for Magento 2 projects. */
27+ private const RECOMMENDED_MAX_FILES = 50000 ;
28+ private const RECOMMENDED_MEMORY_MB = 256 ;
29+
30+ /** @var JsonSerializer */
31+ private JsonSerializer $ serializer ;
32+
33+ /** @var OpcacheService */
34+ private OpcacheService $ opcacheService ;
35+
36+ /**
37+ * Cached service instance to avoid re-running handle() during a request.
38+ *
39+ * @var OpcacheService|null
40+ */
41+ private ?OpcacheService $ handled = null ;
42+
43+ /**
44+ * Cached raw OPcache status (single collection per request).
45+ *
46+ * @var array<string,mixed>|null
47+ */
48+ private ?array $ opcacheStatus = null ;
49+
50+ /**
51+ * Cached raw OPcache configuration (single collection per request).
52+ *
53+ * @var array<string,mixed>|null
54+ */
55+ private ?array $ opcacheConfig = null ;
56+
57+ /**
58+ * @param Context $context
59+ * @param JsonSerializer $serializer
60+ * @param OpcacheService $opcacheService
61+ * @param array<mixed> $data
62+ */
63+ public function __construct (
64+ Context $ context ,
65+ JsonSerializer $ serializer ,
66+ OpcacheService $ opcacheService ,
67+ array $ data = []
68+ ) {
69+ parent ::__construct ($ context , $ data );
70+ $ this ->serializer = $ serializer ;
71+ $ this ->opcacheService = $ opcacheService ;
72+ }
73+
74+ /**
75+ * Prepare block data (exposes service in $block->getData('opcache')).
76+ *
77+ * @return $this
78+ */
79+ protected function _beforeToHtml ()
80+ {
81+ $ this ->setData ('opcache ' , $ this ->getOpcache ());
82+ return parent ::_beforeToHtml ();
83+ }
84+
85+ /**
86+ * Return the Opcache service (handled only once per request).
87+ *
88+ * @return OpcacheService
89+ */
90+ public function getOpcache (): OpcacheService
91+ {
92+ if ($ this ->handled === null ) {
93+ $ this ->handled = $ this ->opcacheService ->handle ();
94+ }
95+ return $ this ->handled ;
96+ }
97+
98+ /**
99+ * Shortcut to read a service option by key.
100+ *
101+ * @param string $key
102+ * @return mixed
103+ */
104+ public function getOpcacheOption (string $ key )
105+ {
106+ return $ this ->getOpcache ()->getOption ($ key );
107+ }
108+
109+ /**
110+ * Return the full UI config consumed by the React application.
111+ *
112+ * @return array<string,mixed>
113+ */
114+ public function getConfig (): array
115+ {
116+ return [
117+ 'api ' => [
118+ 'stateUrl ' => $ this ->getUrl ('opcache_gui/api/state ' ),
119+ ],
120+ 'allow ' => [
121+ 'filelist ' => (bool )$ this ->getOpcacheOption ('allow_filelist ' ),
122+ 'invalidate ' => (bool )$ this ->getOpcacheOption ('allow_invalidate ' ),
123+ 'reset ' => (bool )$ this ->getOpcacheOption ('allow_reset ' ),
124+ 'realtime ' => (bool )$ this ->getOpcacheOption ('allow_realtime ' ),
125+ ],
126+ 'cookie ' => [
127+ 'name ' => (string )$ this ->getOpcacheOption ('cookie_name ' ),
128+ 'ttl ' => (int )$ this ->getOpcacheOption ('cookie_ttl ' ),
129+ ],
130+ 'opstate ' => $ this ->getOpcache ()->getData (),
131+ 'useCharts ' => (bool )$ this ->getOpcacheOption ('charts ' ),
132+ 'highlight ' => (array )$ this ->getOpcacheOption ('highlight ' ),
133+ 'debounceRate ' => (int )$ this ->getOpcacheOption ('debounce_rate ' ),
134+ 'perPageLimit ' => (int )$ this ->getOpcacheOption ('per_page ' ),
135+ 'realtimeRefresh ' => (int )$ this ->getOpcacheOption ('refresh_time ' ),
136+ 'language ' => $ this ->getOpcacheOption ('language_pack ' ),
137+ ];
138+ }
139+
140+ /**
141+ * JSON-encoded version of the React app config.
142+ *
143+ * @return string
144+ */
145+ public function getSerializedConfig (): string
146+ {
147+ return $ this ->serializer ->serialize ($ this ->getConfig ());
148+ }
149+
150+ /**
151+ * Compute performance data for the "PHP OPcache Status Check" card.
152+ * Returns a normalized structure with availability, warnings, errors and metrics.
153+ *
154+ * @return array{
155+ * available: bool,
156+ * errors: string[],
157+ * warnings: string[],
158+ * metrics: array<int, array{label:string,value:string}>
159+ * }
160+ */
161+ public function getPerformanceData (): array
162+ {
163+ $ data = [
164+ 'available ' => $ this ->isOpcacheAvailable (),
165+ 'errors ' => [],
166+ 'warnings ' => [],
167+ 'metrics ' => [],
168+ ];
169+
170+ if (!$ data ['available ' ]) {
171+ $ data ['errors ' ][] = (string )__ ('Zend OPcache extension is not loaded. OPcache is not working. ' );
172+ return $ data ;
173+ }
174+
175+ try {
176+ $ status = $ this ->getOpcacheStatus ();
177+ $ conf = $ this ->getOpcacheConfig ();
178+
179+ if (!is_array ($ status ) || !is_array ($ conf )) {
180+ throw new \RuntimeException ('Invalid OPcache status or configuration data ' );
181+ }
182+
183+ if (isset ($ status ['opcache_enabled ' ]) && !$ status ['opcache_enabled ' ]) {
184+ $ data ['errors ' ][] = (string )__ ('Zend OPcache is disabled. ' );
185+ return $ data ;
186+ }
187+
188+ // Memory figures
189+ $ used = (float )($ status ['memory_usage ' ]['used_memory ' ] ?? 0.0 );
190+ $ free = (float )($ status ['memory_usage ' ]['free_memory ' ] ?? 0.0 );
191+ $ wasted = (float )($ status ['memory_usage ' ]['wasted_memory ' ] ?? 0.0 );
192+ $ total = $ used + $ free + $ wasted ;
193+ $ util = $ total > 0 ? ($ used / $ total ) * 100 : 0.0 ;
194+
195+ // Key directives
196+ $ validateTimestamps = $ conf ['directives ' ]['opcache.validate_timestamps ' ] ?? null ;
197+ $ maxAcceleratedFiles = $ conf ['directives ' ]['opcache.max_accelerated_files ' ] ?? null ;
198+ $ memoryConsumptionMb = $ conf ['directives ' ]['opcache.memory_consumption ' ] ?? null ;
199+
200+ // Stats
201+ $ hitRate = isset ($ status ['opcache_statistics ' ]['hit_rate ' ]) ? (float )$ status ['opcache_statistics ' ]['hit_rate ' ] : null ;
202+ $ numCachedScripts = isset ($ status ['opcache_statistics ' ]['num_cached_scripts ' ]) ? (int )$ status ['opcache_statistics ' ]['num_cached_scripts ' ] : null ;
203+
204+ // Warnings
205+ if (extension_loaded ('xdebug ' )) {
206+ $ data ['warnings ' ][] = (string )__ (
207+ 'Xdebug is enabled. This causes significant performance overhead. Disable Xdebug on production servers. '
208+ );
209+ }
210+ if ($ free < self ::MIN_FREE_MEMORY_BYTES ) {
211+ $ data ['warnings ' ][] = (string )__ (
212+ 'Low OPcache free memory. Consider increasing opcache.memory_consumption. Current free memory: %1MB ' ,
213+ $ this ->formatNumber ($ free / 1048576 , 2 )
214+ );
215+ }
216+ if ($ validateTimestamps === true ) {
217+ $ data ['warnings ' ][] = (string )__ (
218+ 'Timestamp validation is enabled. For optimal performance in production, disable opcache.validate_timestamps in php.ini. '
219+ );
220+ }
221+ if ($ maxAcceleratedFiles !== null && (int )$ maxAcceleratedFiles < self ::RECOMMENDED_MAX_FILES ) {
222+ $ data ['warnings ' ][] = (string )__ (
223+ 'Low max_accelerated_files setting. For Magento 2, at least %1 is recommended. Current setting: %2 ' ,
224+ self ::RECOMMENDED_MAX_FILES ,
225+ number_format ((int )$ maxAcceleratedFiles )
226+ );
227+ }
228+ if ($ memoryConsumptionMb !== null && (int )$ memoryConsumptionMb < self ::RECOMMENDED_MEMORY_MB ) {
229+ $ data ['warnings ' ][] = (string )__ (
230+ 'Low memory_consumption setting. For Magento 2, at least %1MB is recommended. Current setting: %2MB ' ,
231+ self ::RECOMMENDED_MEMORY_MB ,
232+ (int )$ memoryConsumptionMb
233+ );
234+ }
235+
236+ // Metrics
237+ $ data ['metrics ' ][] = [
238+ 'label ' => (string )__ ('Memory Usage: ' ),
239+ 'value ' => sprintf (
240+ '%sMB %s, %sMB %s, %sMB %s (%s %s) ' ,
241+ $ this ->formatNumber ($ used / 1048576 , 2 ), (string )__ ('used ' ),
242+ $ this ->formatNumber ($ free / 1048576 , 2 ), (string )__ ('free ' ),
243+ $ this ->formatNumber ($ wasted / 1048576 , 2 ), (string )__ ('wasted ' ),
244+ $ this ->formatNumber ($ util , 1 ) . '% ' , (string )__ ('utilization ' )
245+ ),
246+ ];
247+
248+ if ($ hitRate !== null ) {
249+ $ data ['metrics ' ][] = [
250+ 'label ' => (string )__ ('Hit Rate: ' ),
251+ 'value ' => $ this ->formatNumber ($ hitRate , 2 ) . '% ' ,
252+ ];
253+ }
254+
255+ if ($ memoryConsumptionMb !== null ) {
256+ $ data ['metrics ' ][] = [
257+ 'label ' => (string )__ ('Memory Allocation: ' ),
258+ 'value ' => (int )$ memoryConsumptionMb . 'MB ' ,
259+ ];
260+ }
261+
262+ if ($ maxAcceleratedFiles !== null ) {
263+ $ data ['metrics ' ][] = [
264+ 'label ' => (string )__ ('Max Accelerated Files: ' ),
265+ 'value ' => number_format ((int )$ maxAcceleratedFiles ),
266+ ];
267+ }
268+
269+ if ($ numCachedScripts !== null ) {
270+ $ data ['metrics ' ][] = [
271+ 'label ' => (string )__ ('Cached Scripts: ' ),
272+ 'value ' => number_format ($ numCachedScripts ),
273+ ];
274+ }
275+ } catch (\Throwable $ e ) {
276+ $ data ['errors ' ][] = (string )__ ('Error: %1 ' , $ e ->getMessage ());
277+ }
278+
279+ return $ data ;
280+ }
281+
282+ /**
283+ * Check if OPcache APIs are available.
284+ *
285+ * @return bool
286+ */
287+ public function isOpcacheAvailable (): bool
288+ {
289+ return extension_loaded ('Zend OPcache ' )
290+ && function_exists ('opcache_get_status ' )
291+ && function_exists ('opcache_get_configuration ' );
292+ }
293+
294+ /**
295+ * Fetch and cache OPcache status (no script list).
296+ *
297+ * @return array<string,mixed>
298+ */
299+ private function getOpcacheStatus (): array
300+ {
301+ if ($ this ->opcacheStatus === null ) {
302+ $ status = opcache_get_status (false );
303+ $ this ->opcacheStatus = is_array ($ status ) ? $ status : [];
304+ }
305+ return $ this ->opcacheStatus ;
306+ }
307+
308+ /**
309+ * Fetch and cache OPcache configuration.
310+ *
311+ * @return array<string,mixed>
312+ */
313+ private function getOpcacheConfig (): array
314+ {
315+ if ($ this ->opcacheConfig === null ) {
316+ $ config = opcache_get_configuration ();
317+ $ this ->opcacheConfig = is_array ($ config ) ? $ config : [];
318+ }
319+ return $ this ->opcacheConfig ;
320+ }
321+
322+ /**
323+ * Format a number with a dot decimal separator (for consistent admin display).
324+ *
325+ * @param float $value
326+ * @param int $decimals
327+ * @return string
328+ */
329+ private function formatNumber (float $ value , int $ decimals = 0 ): string
330+ {
331+ return number_format ($ value , $ decimals , '. ' , '' );
332+ }
333+ }
0 commit comments