CompiledFile.php 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195
  1. <?php
  2. /**
  3. * @package Grav\Common\File
  4. *
  5. * @copyright Copyright (c) 2015 - 2022 Trilby Media, LLC. All rights reserved.
  6. * @license MIT License; see LICENSE file for details.
  7. */
  8. namespace Grav\Common\File;
  9. use Exception;
  10. use Grav\Common\Debugger;
  11. use Grav\Common\Grav;
  12. use Grav\Common\Utils;
  13. use RocketTheme\Toolbox\File\PhpFile;
  14. use RuntimeException;
  15. use Throwable;
  16. use function function_exists;
  17. use function get_class;
  18. /**
  19. * Trait CompiledFile
  20. * @package Grav\Common\File
  21. */
  22. trait CompiledFile
  23. {
  24. /**
  25. * Get/set parsed file contents.
  26. *
  27. * @param mixed $var
  28. * @return array
  29. */
  30. public function content($var = null)
  31. {
  32. try {
  33. $filename = $this->filename;
  34. // If nothing has been loaded, attempt to get pre-compiled version of the file first.
  35. if ($var === null && $this->raw === null && $this->content === null) {
  36. $key = md5($filename);
  37. $file = PhpFile::instance(CACHE_DIR . "compiled/files/{$key}{$this->extension}.php");
  38. $modified = $this->modified();
  39. if (!$modified) {
  40. try {
  41. return $this->decode($this->raw());
  42. } catch (Throwable $e) {
  43. // If the compiled file is broken, we can safely ignore the error and continue.
  44. }
  45. }
  46. $class = get_class($this);
  47. $size = filesize($filename);
  48. $cache = $file->exists() ? $file->content() : null;
  49. // Load real file if cache isn't up to date (or is invalid).
  50. if (!isset($cache['@class'])
  51. || $cache['@class'] !== $class
  52. || $cache['modified'] !== $modified
  53. || ($cache['size'] ?? null) !== $size
  54. || $cache['filename'] !== $filename
  55. ) {
  56. // Attempt to lock the file for writing.
  57. try {
  58. $locked = $file->lock(false);
  59. } catch (Exception $e) {
  60. $locked = false;
  61. /** @var Debugger $debugger */
  62. $debugger = Grav::instance()['debugger'];
  63. $debugger->addMessage(sprintf('%s(): Cannot obtain a lock for compiling cache file for %s: %s', __METHOD__, $this->filename, $e->getMessage()), 'warning');
  64. }
  65. // Decode RAW file into compiled array.
  66. $data = (array)$this->decode($this->raw());
  67. $cache = [
  68. '@class' => $class,
  69. 'filename' => $filename,
  70. 'modified' => $modified,
  71. 'size' => $size,
  72. 'data' => $data
  73. ];
  74. // If compiled file wasn't already locked by another process, save it.
  75. if ($locked) {
  76. $file->save($cache);
  77. $file->unlock();
  78. // Compile cached file into bytecode cache
  79. if (function_exists('opcache_invalidate') && filter_var(ini_get('opcache.enable'), \FILTER_VALIDATE_BOOLEAN)) {
  80. $lockName = $file->filename();
  81. // Silence error if function exists, but is restricted.
  82. @opcache_invalidate($lockName, true);
  83. @opcache_compile_file($lockName);
  84. }
  85. }
  86. }
  87. $file->free();
  88. $this->content = $cache['data'];
  89. }
  90. } catch (Exception $e) {
  91. throw new RuntimeException(sprintf('Failed to read %s: %s', Utils::basename($filename), $e->getMessage()), 500, $e);
  92. }
  93. return parent::content($var);
  94. }
  95. /**
  96. * Save file.
  97. *
  98. * @param mixed $data Optional data to be saved, usually array.
  99. * @return void
  100. * @throws RuntimeException
  101. */
  102. public function save($data = null)
  103. {
  104. // Make sure that the cache file is always up to date!
  105. $key = md5($this->filename);
  106. $file = PhpFile::instance(CACHE_DIR . "compiled/files/{$key}{$this->extension}.php");
  107. try {
  108. $locked = $file->lock();
  109. } catch (Exception $e) {
  110. $locked = false;
  111. /** @var Debugger $debugger */
  112. $debugger = Grav::instance()['debugger'];
  113. $debugger->addMessage(sprintf('%s(): Cannot obtain a lock for compiling cache file for %s: %s', __METHOD__, $this->filename, $e->getMessage()), 'warning');
  114. }
  115. parent::save($data);
  116. if ($locked) {
  117. $modified = $this->modified();
  118. $filename = $this->filename;
  119. $class = get_class($this);
  120. $size = filesize($filename);
  121. // windows doesn't play nicely with this as it can't read when locked
  122. if (!Utils::isWindows()) {
  123. // Reload data from the filesystem. This ensures that we always cache the correct data (see issue #2282).
  124. $this->raw = $this->content = null;
  125. $data = (array)$this->decode($this->raw());
  126. }
  127. // Decode data into compiled array.
  128. $cache = [
  129. '@class' => $class,
  130. 'filename' => $filename,
  131. 'modified' => $modified,
  132. 'size' => $size,
  133. 'data' => $data
  134. ];
  135. $file->save($cache);
  136. $file->unlock();
  137. // Compile cached file into bytecode cache
  138. if (function_exists('opcache_invalidate') && filter_var(ini_get('opcache.enable'), \FILTER_VALIDATE_BOOLEAN)) {
  139. $lockName = $file->filename();
  140. // Silence error if function exists, but is restricted.
  141. @opcache_invalidate($lockName, true);
  142. @opcache_compile_file($lockName);
  143. }
  144. }
  145. }
  146. /**
  147. * Serialize file.
  148. *
  149. * @return array
  150. */
  151. public function __sleep()
  152. {
  153. return [
  154. 'filename',
  155. 'extension',
  156. 'raw',
  157. 'content',
  158. 'settings'
  159. ];
  160. }
  161. /**
  162. * Unserialize file.
  163. */
  164. public function __wakeup()
  165. {
  166. if (!isset(static::$instances[$this->filename])) {
  167. static::$instances[$this->filename] = $this;
  168. }
  169. }
  170. }