Deprecation.php 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312
  1. <?php
  2. declare(strict_types=1);
  3. namespace Doctrine\Deprecations;
  4. use Psr\Log\LoggerInterface;
  5. use function array_key_exists;
  6. use function array_reduce;
  7. use function assert;
  8. use function debug_backtrace;
  9. use function sprintf;
  10. use function strpos;
  11. use function strrpos;
  12. use function substr;
  13. use function trigger_error;
  14. use const DEBUG_BACKTRACE_IGNORE_ARGS;
  15. use const DIRECTORY_SEPARATOR;
  16. use const E_USER_DEPRECATED;
  17. /**
  18. * Manages Deprecation logging in different ways.
  19. *
  20. * By default triggered exceptions are not logged.
  21. *
  22. * To enable different deprecation logging mechanisms you can call the
  23. * following methods:
  24. *
  25. * - Minimal collection of deprecations via getTriggeredDeprecations()
  26. * \Doctrine\Deprecations\Deprecation::enableTrackingDeprecations();
  27. *
  28. * - Uses @trigger_error with E_USER_DEPRECATED
  29. * \Doctrine\Deprecations\Deprecation::enableWithTriggerError();
  30. *
  31. * - Sends deprecation messages via a PSR-3 logger
  32. * \Doctrine\Deprecations\Deprecation::enableWithPsrLogger($logger);
  33. *
  34. * Packages that trigger deprecations should use the `trigger()` or
  35. * `triggerIfCalledFromOutside()` methods.
  36. */
  37. class Deprecation
  38. {
  39. private const TYPE_NONE = 0;
  40. private const TYPE_TRACK_DEPRECATIONS = 1;
  41. private const TYPE_TRIGGER_ERROR = 2;
  42. private const TYPE_PSR_LOGGER = 4;
  43. /** @var int-mask-of<self::TYPE_*>|null */
  44. private static $type;
  45. /** @var LoggerInterface|null */
  46. private static $logger;
  47. /** @var array<string,bool> */
  48. private static $ignoredPackages = [];
  49. /** @var array<string,int> */
  50. private static $triggeredDeprecations = [];
  51. /** @var array<string,bool> */
  52. private static $ignoredLinks = [];
  53. /** @var bool */
  54. private static $deduplication = true;
  55. /**
  56. * Trigger a deprecation for the given package and identfier.
  57. *
  58. * The link should point to a Github issue or Wiki entry detailing the
  59. * deprecation. It is additionally used to de-duplicate the trigger of the
  60. * same deprecation during a request.
  61. *
  62. * @param float|int|string $args
  63. */
  64. public static function trigger(string $package, string $link, string $message, ...$args): void
  65. {
  66. $type = self::$type ?? self::getTypeFromEnv();
  67. if ($type === self::TYPE_NONE) {
  68. return;
  69. }
  70. if (isset(self::$ignoredLinks[$link])) {
  71. return;
  72. }
  73. if (array_key_exists($link, self::$triggeredDeprecations)) {
  74. self::$triggeredDeprecations[$link]++;
  75. } else {
  76. self::$triggeredDeprecations[$link] = 1;
  77. }
  78. if (self::$deduplication === true && self::$triggeredDeprecations[$link] > 1) {
  79. return;
  80. }
  81. if (isset(self::$ignoredPackages[$package])) {
  82. return;
  83. }
  84. $backtrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2);
  85. $message = sprintf($message, ...$args);
  86. self::delegateTriggerToBackend($message, $backtrace, $link, $package);
  87. }
  88. /**
  89. * Trigger a deprecation for the given package and identifier when called from outside.
  90. *
  91. * "Outside" means we assume that $package is currently installed as a
  92. * dependency and the caller is not a file in that package. When $package
  93. * is installed as a root package then deprecations triggered from the
  94. * tests folder are also considered "outside".
  95. *
  96. * This deprecation method assumes that you are using Composer to install
  97. * the dependency and are using the default /vendor/ folder and not a
  98. * Composer plugin to change the install location. The assumption is also
  99. * that $package is the exact composer packge name.
  100. *
  101. * Compared to {@link trigger()} this method causes some overhead when
  102. * deprecation tracking is enabled even during deduplication, because it
  103. * needs to call {@link debug_backtrace()}
  104. *
  105. * @param float|int|string $args
  106. */
  107. public static function triggerIfCalledFromOutside(string $package, string $link, string $message, ...$args): void
  108. {
  109. $type = self::$type ?? self::getTypeFromEnv();
  110. if ($type === self::TYPE_NONE) {
  111. return;
  112. }
  113. $backtrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2);
  114. // first check that the caller is not from a tests folder, in which case we always let deprecations pass
  115. if (isset($backtrace[1]['file'], $backtrace[0]['file']) && strpos($backtrace[1]['file'], DIRECTORY_SEPARATOR . 'tests' . DIRECTORY_SEPARATOR) === false) {
  116. $path = DIRECTORY_SEPARATOR . 'vendor' . DIRECTORY_SEPARATOR . $package . DIRECTORY_SEPARATOR;
  117. if (strpos($backtrace[0]['file'], $path) === false) {
  118. return;
  119. }
  120. if (strpos($backtrace[1]['file'], $path) !== false) {
  121. return;
  122. }
  123. }
  124. if (isset(self::$ignoredLinks[$link])) {
  125. return;
  126. }
  127. if (array_key_exists($link, self::$triggeredDeprecations)) {
  128. self::$triggeredDeprecations[$link]++;
  129. } else {
  130. self::$triggeredDeprecations[$link] = 1;
  131. }
  132. if (self::$deduplication === true && self::$triggeredDeprecations[$link] > 1) {
  133. return;
  134. }
  135. if (isset(self::$ignoredPackages[$package])) {
  136. return;
  137. }
  138. $message = sprintf($message, ...$args);
  139. self::delegateTriggerToBackend($message, $backtrace, $link, $package);
  140. }
  141. /**
  142. * @param list<array{function: string, line?: int, file?: string, class?: class-string, type?: string, args?: mixed[], object?: object}> $backtrace
  143. */
  144. private static function delegateTriggerToBackend(string $message, array $backtrace, string $link, string $package): void
  145. {
  146. $type = self::$type ?? self::getTypeFromEnv();
  147. if (($type & self::TYPE_PSR_LOGGER) > 0) {
  148. $context = [
  149. 'file' => $backtrace[0]['file'] ?? null,
  150. 'line' => $backtrace[0]['line'] ?? null,
  151. 'package' => $package,
  152. 'link' => $link,
  153. ];
  154. assert(self::$logger !== null);
  155. self::$logger->notice($message, $context);
  156. }
  157. if (! (($type & self::TYPE_TRIGGER_ERROR) > 0)) {
  158. return;
  159. }
  160. $message .= sprintf(
  161. ' (%s:%d called by %s:%d, %s, package %s)',
  162. self::basename($backtrace[0]['file'] ?? 'native code'),
  163. $backtrace[0]['line'] ?? 0,
  164. self::basename($backtrace[1]['file'] ?? 'native code'),
  165. $backtrace[1]['line'] ?? 0,
  166. $link,
  167. $package
  168. );
  169. @trigger_error($message, E_USER_DEPRECATED);
  170. }
  171. /**
  172. * A non-local-aware version of PHPs basename function.
  173. */
  174. private static function basename(string $filename): string
  175. {
  176. $pos = strrpos($filename, DIRECTORY_SEPARATOR);
  177. if ($pos === false) {
  178. return $filename;
  179. }
  180. return substr($filename, $pos + 1);
  181. }
  182. public static function enableTrackingDeprecations(): void
  183. {
  184. self::$type = self::$type ?? 0;
  185. self::$type |= self::TYPE_TRACK_DEPRECATIONS;
  186. }
  187. public static function enableWithTriggerError(): void
  188. {
  189. self::$type = self::$type ?? 0;
  190. self::$type |= self::TYPE_TRIGGER_ERROR;
  191. }
  192. public static function enableWithPsrLogger(LoggerInterface $logger): void
  193. {
  194. self::$type = self::$type ?? 0;
  195. self::$type |= self::TYPE_PSR_LOGGER;
  196. self::$logger = $logger;
  197. }
  198. public static function withoutDeduplication(): void
  199. {
  200. self::$deduplication = false;
  201. }
  202. public static function disable(): void
  203. {
  204. self::$type = self::TYPE_NONE;
  205. self::$logger = null;
  206. self::$deduplication = true;
  207. self::$ignoredLinks = [];
  208. foreach (self::$triggeredDeprecations as $link => $count) {
  209. self::$triggeredDeprecations[$link] = 0;
  210. }
  211. }
  212. public static function ignorePackage(string $packageName): void
  213. {
  214. self::$ignoredPackages[$packageName] = true;
  215. }
  216. public static function ignoreDeprecations(string ...$links): void
  217. {
  218. foreach ($links as $link) {
  219. self::$ignoredLinks[$link] = true;
  220. }
  221. }
  222. public static function getUniqueTriggeredDeprecationsCount(): int
  223. {
  224. return array_reduce(self::$triggeredDeprecations, static function (int $carry, int $count) {
  225. return $carry + $count;
  226. }, 0);
  227. }
  228. /**
  229. * Returns each triggered deprecation link identifier and the amount of occurrences.
  230. *
  231. * @return array<string,int>
  232. */
  233. public static function getTriggeredDeprecations(): array
  234. {
  235. return self::$triggeredDeprecations;
  236. }
  237. /**
  238. * @return int-mask-of<self::TYPE_*>
  239. */
  240. private static function getTypeFromEnv(): int
  241. {
  242. switch ($_SERVER['DOCTRINE_DEPRECATIONS'] ?? $_ENV['DOCTRINE_DEPRECATIONS'] ?? null) {
  243. case 'trigger':
  244. self::$type = self::TYPE_TRIGGER_ERROR;
  245. break;
  246. case 'track':
  247. self::$type = self::TYPE_TRACK_DEPRECATIONS;
  248. break;
  249. default:
  250. self::$type = self::TYPE_NONE;
  251. break;
  252. }
  253. return self::$type;
  254. }
  255. }