Skip to content

Commit eb5edb9

Browse files
authored
Merge pull request #2466 from elcreator/fix-cache-full-clear
fix(cache): document saves no longer reset opcache or purge compiled views
2 parents ff08a37 + a595704 commit eb5edb9

14 files changed

Lines changed: 528 additions & 43 deletions

‎core/src/Controllers/MoveDocument.php‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ protected function handle()
114114
]);
115115

116116
// empty cache & sync site
117-
$this->managerTheme->getCore()->clearCache('full');
117+
$this->managerTheme->getCore()->clearCache('document');
118118

119119
header('Location: index.php?a=3&id=' . $document->getKey() . '&r=9');
120120
}

‎core/src/Core.php‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4627,7 +4627,7 @@ public function getSnippetName()
46274627
/**
46284628
* Clear the cache of MODX.
46294629
*
4630-
* @param string $type
4630+
* @param string|array $type '' page caches, 'full', 'document' (no opcache reset / compiled views) or a document id
46314631
* @param bool $report
46324632
* @return void
46334633
*/
@@ -4636,7 +4636,7 @@ public function clearCache($type = '', $report = false)
46364636
$cache_dir = $this->bootstrapPath();
46374637

46384638
/*$this['command.view.clear']->handle();*/
4639-
$path = $this['config']['view.compiled'];
4639+
$path = $type === 'document' ? '' : $this['config']['view.compiled'];
46404640
if ($path) {
46414641
foreach ($this['files']->glob("{$path}/*") as $view) {
46424642
$this['files']->delete($view);
@@ -4652,6 +4652,10 @@ public function clearCache($type = '', $report = false)
46524652
$sync->setCachepath($cache_dir);
46534653
$sync->setReport($report);
46544654
$sync->emptyCache();
4655+
} elseif ($type === 'document') {
4656+
$sync = new Legacy\Cache();
4657+
$sync->setCachepath($cache_dir);
4658+
$sync->refreshDocumentCache($this);
46554659
} elseif (preg_match('@^[1-9]\d*$@', $type)) {
46564660
$key = ($this->getConfig('cache_type') == 2) ? $this->makePageCacheKey($type) : $type;
46574661
$file_name = "docid_" . $key . "_*.pageCache.php";

‎core/src/Legacy/Cache.php‎

Lines changed: 106 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
<?php namespace EvolutionCMS\Legacy;
22

3+
use EvolutionCMS\Interfaces;
34
use EvolutionCMS\Models;
45

56
/**
@@ -111,30 +112,13 @@ public function getParents($id, $path = '')
111112
*/
112113
public function emptyCache($evo = null)
113114
{
114-
if (!($evo instanceof Interfaces\CoreInterface)) {
115-
$evo = $GLOBALS['evo'];
116-
}
117-
if (!isset($this->cachePath)) {
118-
$evo->getService('ExceptionHandler')->messageQuit("Cache path not set.");
119-
}
115+
$evo = $this->resolveCore($evo);
120116
\Illuminate\Support\Facades\Cache::flush();
121117
Models\UserSetting::query()->whereIn('setting_name', ['password', 'password_confirmation', 'clearPassword'])->delete();
122-
$files = glob(realpath($this->cachePath) . '/*.pageCache.php');
123-
$filesincache = count($files);
124-
$deletedfiles = [];
125-
while ($file = array_shift($files)) {
126-
$name = basename($file);
127-
clearstatcache();
128-
if (is_file($file)) {
129-
if (unlink($file)) {
130-
$deletedfiles[] = $name;
131-
}
132-
}
133-
}
134-
$opcache_restrict_api = trim(ini_get('opcache.restrict_api'));
135-
$opcache_restrict_api = $opcache_restrict_api && mb_stripos(__FILE__, $opcache_restrict_api) !== 0;
118+
$filesincache = count(glob(realpath($this->cachePath) . '/*.pageCache.php') ?: []);
119+
$deletedfiles = $this->clearPageCache();
136120

137-
if (!$opcache_restrict_api && function_exists('opcache_get_status')) {
121+
if ($this->opcacheApiAllowed() && function_exists('opcache_get_status')) {
138122
$opcache = opcache_get_status();
139123
if (!empty($opcache['opcache_enabled'])) {
140124
opcache_reset();
@@ -163,6 +147,83 @@ public function emptyCache($evo = null)
163147
}
164148
}
165149

150+
/**
151+
* Document save/publish/move: drops page caches and rebuilds the site cache
152+
* without resetting opcache or touching compiled views.
153+
* @param null|Interfaces\CoreInterface $evo
154+
* @since 3.5.8
155+
*/
156+
public function refreshDocumentCache($evo = null)
157+
{
158+
$evo = $this->resolveCore($evo);
159+
\Illuminate\Support\Facades\Cache::flush();
160+
$this->clearPageCache();
161+
$this->buildCache($evo);
162+
$this->publishTimeConfig();
163+
}
164+
165+
/**
166+
* @return string[] deleted file names
167+
*/
168+
public function clearPageCache(): array
169+
{
170+
$deleted = [];
171+
foreach (glob(realpath($this->cachePath) . '/*.pageCache.php') ?: [] as $file) {
172+
clearstatcache(false, $file);
173+
if (is_file($file) && @unlink($file)) {
174+
$deleted[] = basename($file);
175+
}
176+
}
177+
178+
return $deleted;
179+
}
180+
181+
/**
182+
* @param mixed $evo
183+
* @return Interfaces\CoreInterface
184+
*/
185+
protected function resolveCore($evo)
186+
{
187+
if (!($evo instanceof Interfaces\CoreInterface)) {
188+
$evo = $GLOBALS['evo'];
189+
}
190+
if (!isset($this->cachePath)) {
191+
$evo->getService('ExceptionHandler')->messageQuit("Cache path not set.");
192+
}
193+
194+
return $evo;
195+
}
196+
197+
protected function opcacheApiAllowed(): bool
198+
{
199+
$restrict = trim((string) ini_get('opcache.restrict_api'));
200+
201+
return !($restrict && mb_stripos(__FILE__, $restrict) !== 0);
202+
}
203+
204+
/**
205+
* Writes through a temp file + rename so concurrent readers never include a partial file,
206+
* then drops the stale opcache entry (needed with opcache.validate_timestamps=0).
207+
*/
208+
protected function writeCacheFile(string $filename, string $content): bool
209+
{
210+
$tmp = $filename . '.' . uniqid('', true) . '.tmp';
211+
if (@file_put_contents($tmp, $content) === false) {
212+
return false;
213+
}
214+
if (!@rename($tmp, $filename)) {
215+
@unlink($tmp);
216+
if (@file_put_contents($filename, $content) === false) {
217+
return false;
218+
}
219+
}
220+
if ($this->opcacheApiAllowed() && function_exists('opcache_invalidate')) {
221+
@opcache_invalidate($filename, true);
222+
}
223+
224+
return true;
225+
}
226+
166227
/**
167228
* @param string|int $cacheRefreshTime
168229
*/
@@ -179,15 +240,10 @@ public function publishTimeConfig($cacheRefreshTime = '')
179240
$content .= '$recent_update=\'' . $this->request_time . '\';' . "\n";
180241
$content .= '$cacheRefreshTime=\'' . $cacheRefreshTime . '\';' . "\n";
181242

182-
$filename = evo()->getSitePublishingFilePath();
183-
if (!$handle = fopen($filename, 'w')) {
184-
exit("Cannot open file ({$filename}");
185-
}
186-
187243
$content .= "\n";
188244

189-
// Write $somecontent to our opened file.
190-
if (fwrite($handle, $content) === false) {
245+
$filename = evo()->getSitePublishingFilePath();
246+
if (!$this->writeCacheFile($filename, $content)) {
191247
exit("Cannot write publishing info file! Make sure the {$filename} and its directory is writable!");
192248
}
193249
}
@@ -233,6 +289,27 @@ public function getCacheRefreshTime()
233289
* @return boolean success
234290
*/
235291
public function buildCache($evo)
292+
{
293+
// serialise concurrent rebuilds so the last writer always reflects the latest DB state
294+
$lock = @fopen($evo->getSiteCacheFilePath() . '.lock', 'c');
295+
if ($lock) {
296+
flock($lock, LOCK_EX);
297+
}
298+
try {
299+
return $this->writeSiteCache($evo);
300+
} finally {
301+
if ($lock) {
302+
flock($lock, LOCK_UN);
303+
fclose($lock);
304+
}
305+
}
306+
}
307+
308+
/**
309+
* @param Interfaces\CoreInterface $evo
310+
* @return boolean success
311+
*/
312+
protected function writeSiteCache($evo)
236313
{
237314
$content = "<?php\n";
238315

@@ -392,7 +469,7 @@ public function buildCache($evo)
392469
// invoke OnBeforeCacheUpdate event
393470
$evo->invokeEvent('OnBeforeCacheUpdate');
394471

395-
if (@file_put_contents($filename, $content) === false) {
472+
if (!$this->writeCacheFile($filename, $content)) {
396473
exit("Cannot write $filename! Make sure file or its directory is writable!");
397474
}
398475

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
<?php
2+
3+
namespace Tests\Mocks;
4+
5+
use Illuminate\Cache\ArrayStore;
6+
use Illuminate\Cache\Repository;
7+
use Illuminate\Container\Container;
8+
use Illuminate\Database\Capsule\Manager as Capsule;
9+
use Illuminate\Database\Eloquent\Model;
10+
use Illuminate\Database\Schema\Blueprint;
11+
use Illuminate\Events\Dispatcher;
12+
use Illuminate\Support\Facades\Facade;
13+
14+
/**
15+
* Stand-in for the core: only what Legacy\Cache touches while rebuilding the site cache.
16+
*/
17+
class CacheRefreshHarness
18+
{
19+
public array $config = ['server_offset_time' => 0];
20+
public array $events = [];
21+
public string $dir;
22+
23+
public function getConfig($name = '', $default = null)
24+
{
25+
return $this->config[$name] ?? $default;
26+
}
27+
28+
public function parseProperties($props, $name = null, $type = null): array
29+
{
30+
return [];
31+
}
32+
33+
public function invokeEvent($name, $params = [])
34+
{
35+
$this->events[] = $name;
36+
return false;
37+
}
38+
39+
public function getSiteCacheFilePath(): string
40+
{
41+
return $this->dir . '/siteCache.idx.php';
42+
}
43+
44+
public function getSitePublishingFilePath(): string
45+
{
46+
return $this->dir . '/sitePublishing.idx.php';
47+
}
48+
49+
/** Boots Eloquent (with model events) and the Cache facade against the given sqlite database. */
50+
public static function boot(string $database = ':memory:'): Capsule
51+
{
52+
foreach (['IN_MANAGER_MODE', 'IN_INSTALL_MODE'] as $c) {
53+
defined($c) || define($c, false);
54+
}
55+
defined('EVO_API_MODE') || define('EVO_API_MODE', true);
56+
defined('EVO_CLASS') || define('EVO_CLASS', self::class);
57+
require_once dirname(__DIR__, 2) . '/functions/preload.php';
58+
59+
$database === ':memory:' || is_file($database) || touch($database);
60+
$capsule = new Capsule();
61+
$capsule->addConnection(['driver' => 'sqlite', 'database' => $database, 'prefix' => '']);
62+
$capsule->setEventDispatcher(new Dispatcher(new Container()));
63+
$capsule->setAsGlobal();
64+
$capsule->bootEloquent();
65+
Model::setConnectionResolver($capsule->getDatabaseManager());
66+
Model::clearBootedModels();
67+
68+
$container = new Container();
69+
$container->instance('cache', new Repository(new ArrayStore()));
70+
Facade::setFacadeApplication($container);
71+
72+
return $capsule;
73+
}
74+
75+
public static function schema(Capsule $capsule): void
76+
{
77+
$schema = $capsule->getConnection()->getSchemaBuilder();
78+
$schema->create('site_content', function (Blueprint $t) {
79+
$t->increments('id');
80+
$t->string('type', 20)->default('document');
81+
$t->string('contentType', 50)->default('text/html');
82+
$t->string('pagetitle')->default('');
83+
$t->string('alias', 245)->nullable()->default('');
84+
$t->unsignedInteger('published')->default(0);
85+
$t->unsignedInteger('pub_date')->default(0);
86+
$t->unsignedInteger('unpub_date')->default(0);
87+
$t->unsignedInteger('parent')->default(0);
88+
$t->unsignedInteger('isfolder')->default(0);
89+
$t->unsignedInteger('menuindex')->default(0);
90+
$t->unsignedInteger('createdon')->default(0);
91+
$t->unsignedInteger('editedon')->default(0);
92+
$t->unsignedInteger('deleted')->default(0);
93+
$t->unsignedInteger('deletedon')->default(0);
94+
$t->unsignedInteger('alias_visible')->default(1);
95+
});
96+
$schema->create('site_content_closure', function (Blueprint $t) {
97+
$t->increments('closure_id');
98+
$t->unsignedInteger('ancestor');
99+
$t->unsignedInteger('descendant');
100+
$t->unsignedInteger('depth');
101+
});
102+
$schema->create('system_settings', function (Blueprint $t) {
103+
$t->string('setting_name', 50)->primary();
104+
$t->text('setting_value')->nullable();
105+
});
106+
$schema->create('site_htmlsnippets', function (Blueprint $t) {
107+
$t->increments('id');
108+
$t->string('name')->default('');
109+
$t->text('snippet')->nullable();
110+
$t->boolean('disabled')->default(0);
111+
});
112+
foreach (['site_snippets' => 'snippet', 'site_plugins' => 'plugincode'] as $table => $code) {
113+
$schema->create($table, function (Blueprint $t) use ($code) {
114+
$t->increments('id');
115+
$t->string('name')->default('');
116+
$t->text($code)->nullable();
117+
$t->text('properties')->nullable();
118+
$t->string('moduleguid', 32)->default('');
119+
$t->boolean('disabled')->default(0);
120+
});
121+
}
122+
$schema->create('site_modules', function (Blueprint $t) {
123+
$t->increments('id');
124+
$t->string('guid', 32)->default('');
125+
$t->text('properties')->nullable();
126+
});
127+
$schema->create('system_eventnames', function (Blueprint $t) {
128+
$t->increments('id');
129+
$t->string('name', 50)->default('');
130+
});
131+
$schema->create('site_plugin_events', function (Blueprint $t) {
132+
$t->unsignedInteger('pluginid');
133+
$t->unsignedInteger('evtid');
134+
$t->integer('priority')->default(0);
135+
});
136+
$schema->create('user_settings', function (Blueprint $t) {
137+
$t->unsignedInteger('user');
138+
$t->string('setting_name', 50);
139+
$t->text('setting_value')->nullable();
140+
});
141+
$capsule->table('system_settings')->insert([
142+
['setting_name' => 'friendly_urls', 'setting_value' => '1'],
143+
['setting_name' => 'use_alias_path', 'setting_value' => '1'],
144+
]);
145+
}
146+
147+
/** Includes a generated site cache the way Core::recoverySiteCache() does. */
148+
public static function listing(string $file): array
149+
{
150+
$reader = new class {
151+
public $config = [], $aliasListing = [], $documentListing = [], $documentMap = [], $contentTypes = [], $chunkCache = [], $snippetCache = [], $pluginCache = [], $pluginEvent = [];
152+
public function load(string $file): void { include $file; }
153+
};
154+
$reader->load($file);
155+
return ['aliasListing' => $reader->aliasListing, 'documentListing' => $reader->documentListing, 'config' => $reader->config];
156+
}
157+
}

0 commit comments

Comments
 (0)