Deprecation.php 9.3 KB

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