Skip to content

Commit ca7f0c1

Browse files
authored
chore(release): 2.0.0 — Realtime mode, hardened API, UX & i18n
### Features - feat(js): add realtime polling toggle with cookie persistence; prevent duplicate intervals; poll opcache state via axios GET with form_key. - feat(js): native Magento 2 alerts after actions (reset, invalidate, bulk invalidate) with consistent success/error messages. - feat(api): `Amadeco\OpcacheGui\Controller\Adminhtml\Api\State` now implements HttpGetActionInterface and returns structured JSON `{ ok, data|error }`. ### Security / Hardening - fix(api): validate CSRF form key on every request; enforce action whitelist (`poll|reset|invalidate|invalidate_searched`). - fix(api): sanitize and scope parameters before invoking Amnuts service; safely stage temporary `$_GET` and restore afterwards. ### Refactor / Maintainability - refactor(block): move OPcache health checks and view logic from `gui.phtml` into `Block\Adminhtml\Gui` helpers; expose typed getters and a serialized config for JS bootstrap. - refactor(phpdoc): normalize English PHPDoc across block and controller; add types and exceptions where applicable. - refactor(js): cache CSS variables once; avoid inline closures where not needed; guard redundant `setState` in pagination. ### UX / Performance - perf(js): debounce filtering; stable sorting; lighter rendering in Tabs; small accessibility fixes (roles/aria-controls). - ui(js): “Invalidate all matching files” now uses the API when realtime is enabled; per-file invalidation keeps navigation in place. ### Internationalization - i18n(fr_FR): add French translations for new/updated UI strings (CSV updated). ### Notes / Compatibility - If you scripted against the previous POST API, switch to GET and include `form_key` and an `action` parameter. - No template markup changes required in custom themes; JS options still bootstrap from `Gui::getSerializedConfig()`. Files touched (high level): - `view/adminhtml/web/js/*` (React/AMD interface) - `Controller/Adminhtml/Api/State.php` (API, security, JSON payload) - `Block/Adminhtml/Gui.php` (config + health checks + PHPDoc) - `i18n/fr_FR.csv` (new translations)
2 parents 357a193 + c5218f2 commit ca7f0c1

File tree

17 files changed

+2417
-355
lines changed

17 files changed

+2417
-355
lines changed

Block/Adminhtml/Gui.php

Lines changed: 333 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,333 @@
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

Comments
 (0)