Deprecation.php 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294
  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 self::TYPE_*|null */
  43. private static $type;
  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. $type = self::$type ?? self::getTypeFromEnv();
  64. if ($type === self::TYPE_NONE) {
  65. return;
  66. }
  67. if (array_key_exists($link, self::$ignoredLinks)) {
  68. self::$ignoredLinks[$link]++;
  69. } else {
  70. self::$ignoredLinks[$link] = 1;
  71. }
  72. if (self::$deduplication === true && self::$ignoredLinks[$link] > 1) {
  73. return;
  74. }
  75. if (isset(self::$ignoredPackages[$package])) {
  76. return;
  77. }
  78. $backtrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2);
  79. $message = sprintf($message, ...$args);
  80. self::delegateTriggerToBackend($message, $backtrace, $link, $package);
  81. }
  82. /**
  83. * Trigger a deprecation for the given package and identifier when called from outside.
  84. *
  85. * "Outside" means we assume that $package is currently installed as a
  86. * dependency and the caller is not a file in that package. When $package
  87. * is installed as a root package then deprecations triggered from the
  88. * tests folder are also considered "outside".
  89. *
  90. * This deprecation method assumes that you are using Composer to install
  91. * the dependency and are using the default /vendor/ folder and not a
  92. * Composer plugin to change the install location. The assumption is also
  93. * that $package is the exact composer packge name.
  94. *
  95. * Compared to {@link trigger()} this method causes some overhead when
  96. * deprecation tracking is enabled even during deduplication, because it
  97. * needs to call {@link debug_backtrace()}
  98. *
  99. * @param mixed $args
  100. */
  101. public static function triggerIfCalledFromOutside(string $package, string $link, string $message, ...$args): void
  102. {
  103. $type = self::$type ?? self::getTypeFromEnv();
  104. if ($type === self::TYPE_NONE) {
  105. return;
  106. }
  107. $backtrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2);
  108. // first check that the caller is not from a tests folder, in which case we always let deprecations pass
  109. if (strpos($backtrace[1]['file'], DIRECTORY_SEPARATOR . 'tests' . DIRECTORY_SEPARATOR) === false) {
  110. $path = DIRECTORY_SEPARATOR . 'vendor' . DIRECTORY_SEPARATOR . $package . DIRECTORY_SEPARATOR;
  111. if (strpos($backtrace[0]['file'], $path) === false) {
  112. return;
  113. }
  114. if (strpos($backtrace[1]['file'], $path) !== false) {
  115. return;
  116. }
  117. }
  118. if (array_key_exists($link, self::$ignoredLinks)) {
  119. self::$ignoredLinks[$link]++;
  120. } else {
  121. self::$ignoredLinks[$link] = 1;
  122. }
  123. if (self::$deduplication === true && self::$ignoredLinks[$link] > 1) {
  124. return;
  125. }
  126. if (isset(self::$ignoredPackages[$package])) {
  127. return;
  128. }
  129. $message = sprintf($message, ...$args);
  130. self::delegateTriggerToBackend($message, $backtrace, $link, $package);
  131. }
  132. /**
  133. * @param array<mixed> $backtrace
  134. */
  135. private static function delegateTriggerToBackend(string $message, array $backtrace, string $link, string $package): void
  136. {
  137. $type = self::$type ?? self::getTypeFromEnv();
  138. if (($type & self::TYPE_PSR_LOGGER) > 0) {
  139. $context = [
  140. 'file' => $backtrace[0]['file'],
  141. 'line' => $backtrace[0]['line'],
  142. 'package' => $package,
  143. 'link' => $link,
  144. ];
  145. self::$logger->notice($message, $context);
  146. }
  147. if (! (($type & self::TYPE_TRIGGER_ERROR) > 0)) {
  148. return;
  149. }
  150. $message .= sprintf(
  151. ' (%s:%d called by %s:%d, %s, package %s)',
  152. self::basename($backtrace[0]['file']),
  153. $backtrace[0]['line'],
  154. self::basename($backtrace[1]['file']),
  155. $backtrace[1]['line'],
  156. $link,
  157. $package
  158. );
  159. @trigger_error($message, E_USER_DEPRECATED);
  160. }
  161. /**
  162. * A non-local-aware version of PHPs basename function.
  163. */
  164. private static function basename(string $filename): string
  165. {
  166. $pos = strrpos($filename, DIRECTORY_SEPARATOR);
  167. if ($pos === false) {
  168. return $filename;
  169. }
  170. return substr($filename, $pos + 1);
  171. }
  172. public static function enableTrackingDeprecations(): void
  173. {
  174. self::$type |= self::TYPE_TRACK_DEPRECATIONS;
  175. }
  176. public static function enableWithTriggerError(): void
  177. {
  178. self::$type |= self::TYPE_TRIGGER_ERROR;
  179. }
  180. public static function enableWithPsrLogger(LoggerInterface $logger): void
  181. {
  182. self::$type |= self::TYPE_PSR_LOGGER;
  183. self::$logger = $logger;
  184. }
  185. public static function withoutDeduplication(): void
  186. {
  187. self::$deduplication = false;
  188. }
  189. public static function disable(): void
  190. {
  191. self::$type = self::TYPE_NONE;
  192. self::$logger = null;
  193. self::$deduplication = true;
  194. foreach (self::$ignoredLinks as $link => $count) {
  195. self::$ignoredLinks[$link] = 0;
  196. }
  197. }
  198. public static function ignorePackage(string $packageName): void
  199. {
  200. self::$ignoredPackages[$packageName] = true;
  201. }
  202. public static function ignoreDeprecations(string ...$links): void
  203. {
  204. foreach ($links as $link) {
  205. self::$ignoredLinks[$link] = 0;
  206. }
  207. }
  208. public static function getUniqueTriggeredDeprecationsCount(): int
  209. {
  210. return array_reduce(self::$ignoredLinks, static function (int $carry, int $count) {
  211. return $carry + $count;
  212. }, 0);
  213. }
  214. /**
  215. * Returns each triggered deprecation link identifier and the amount of occurrences.
  216. *
  217. * @return array<string,int>
  218. */
  219. public static function getTriggeredDeprecations(): array
  220. {
  221. return self::$ignoredLinks;
  222. }
  223. /**
  224. * @return self::TYPE_*
  225. */
  226. private static function getTypeFromEnv(): int
  227. {
  228. switch ($_SERVER['DOCTRINE_DEPRECATIONS'] ?? $_ENV['DOCTRINE_DEPRECATIONS'] ?? null) {
  229. case 'trigger':
  230. self::$type = self::TYPE_TRIGGER_ERROR;
  231. break;
  232. case 'track':
  233. self::$type = self::TYPE_TRACK_DEPRECATIONS;
  234. break;
  235. default:
  236. self::$type = self::TYPE_NONE;
  237. break;
  238. }
  239. return self::$type;
  240. }
  241. }