Deprecation.php 7.7 KB

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