1: <?php declare(strict_types = 1);
2:
3: namespace PHPStan\Php;
4:
5: use PHPStan\DependencyInjection\AutowiredService;
6: use PHPStan\Turbo\ReferencedByTurboExtension;
7: use function floor;
8:
9: /**
10: * Represents a specific PHP version for version-dependent analysis behavior.
11: *
12: * The version is stored as PHP_VERSION_ID format (e.g. 80100 for PHP 8.1.0).
13: * Extension developers can access it by injecting PhpVersion via constructor injection.
14: *
15: * @api
16: */
17: #[AutowiredService(factory: '@PHPStan\Php\PhpVersionFactory::create')]
18: #[ReferencedByTurboExtension(key: 'phpVersion')]
19: final class PhpVersion
20: {
21:
22: public const SOURCE_RUNTIME = 1;
23: public const SOURCE_CONFIG = 2;
24: public const SOURCE_COMPOSER_PLATFORM_PHP = 3;
25: public const SOURCE_UNKNOWN = 4;
26:
27: /**
28: * @api
29: * @param self::SOURCE_* $source
30: */
31: public function __construct(private int $versionId, private int $source = self::SOURCE_UNKNOWN)
32: {
33: }
34:
35: /**
36: * @return self::SOURCE_*
37: */
38: public function getSource(): int
39: {
40: return $this->source;
41: }
42:
43: public function getSourceLabel(): string
44: {
45: switch ($this->source) {
46: case self::SOURCE_RUNTIME:
47: return 'runtime';
48: case self::SOURCE_CONFIG:
49: return 'config';
50: case self::SOURCE_COMPOSER_PLATFORM_PHP:
51: return 'config.platform.php in composer.json';
52: }
53:
54: return 'unknown';
55: }
56:
57: public function getVersionId(): int
58: {
59: return $this->versionId;
60: }
61:
62: public function getMajorVersionId(): int
63: {
64: return (int) floor($this->versionId / 10000);
65: }
66:
67: public function getMinorVersionId(): int
68: {
69: return (int) floor(($this->versionId % 10000) / 100);
70: }
71:
72: public function getPatchVersionId(): int
73: {
74: return (int) floor($this->versionId % 100);
75: }
76:
77: public function getVersionString(): string
78: {
79: $first = $this->getMajorVersionId();
80: $second = $this->getMinorVersionId();
81: $third = $this->getPatchVersionId();
82:
83: return $first . '.' . $second . ($third !== 0 ? '.' . $third : '');
84: }
85:
86: public function supportsNullCoalesceAssign(): bool
87: {
88: return $this->versionId >= 70400;
89: }
90:
91: public function supportsParameterContravariance(): bool
92: {
93: return $this->versionId >= 70400;
94: }
95:
96: public function supportsReturnCovariance(): bool
97: {
98: return $this->versionId >= 70400;
99: }
100:
101: public function supportsNoncapturingCatches(): bool
102: {
103: return $this->versionId >= 80000;
104: }
105:
106: public function supportsNativeUnionTypes(): bool
107: {
108: return $this->versionId >= 80000;
109: }
110:
111: public function supportsNativeMixed(): bool
112: {
113: return $this->versionId >= 80000;
114: }
115:
116: public function supportsTrueFalseNullStandaloneType(): bool
117: {
118: return $this->versionId >= 80200;
119: }
120:
121: public function supportsPipeOperator(): bool
122: {
123: return $this->versionId >= 80500;
124: }
125:
126: public function deprecatesRequiredParameterAfterOptional(): bool
127: {
128: return $this->versionId >= 80000;
129: }
130:
131: public function deprecatesRequiredParameterAfterOptionalNullableAndDefaultNull(): bool
132: {
133: return $this->versionId >= 80100;
134: }
135:
136: public function deprecatesRequiredParameterAfterOptionalUnionOrMixed(): bool
137: {
138: return $this->versionId >= 80300;
139: }
140:
141: public function supportsLessOverridenParametersWithVariadic(): bool
142: {
143: return $this->versionId >= 80000;
144: }
145:
146: public function supportsThrowExpression(): bool
147: {
148: return $this->versionId >= 80000;
149: }
150:
151: public function supportsClassConstantOnExpression(): bool
152: {
153: return $this->versionId >= 80000;
154: }
155:
156: public function supportsLegacyConstructor(): bool
157: {
158: return $this->versionId < 80000;
159: }
160:
161: public function supportsPromotedProperties(): bool
162: {
163: return $this->versionId >= 80000;
164: }
165:
166: public function supportsParameterTypeWidening(): bool
167: {
168: return $this->versionId >= 70200;
169: }
170:
171: public function supportsUnsetCast(): bool
172: {
173: return $this->versionId < 80000;
174: }
175:
176: public function supportsNamedArguments(): bool
177: {
178: return $this->versionId >= 80000;
179: }
180:
181: public function throwsTypeErrorForInternalFunctions(): bool
182: {
183: return $this->versionId >= 80000;
184: }
185:
186: public function throwsValueErrorForInternalFunctions(): bool
187: {
188: return $this->versionId >= 80000;
189: }
190:
191: public function supportsHhPrintfSpecifier(): bool
192: {
193: return $this->versionId >= 80000;
194: }
195:
196: public function isEmptyStringValidAliasForNoneInMbSubstituteCharacter(): bool
197: {
198: return $this->versionId < 80000;
199: }
200:
201: public function supportsAllUnicodeScalarCodePointsInMbSubstituteCharacter(): bool
202: {
203: return $this->versionId >= 70200;
204: }
205:
206: public function isNumericStringValidArgInMbSubstituteCharacter(): bool
207: {
208: return $this->versionId < 80000;
209: }
210:
211: public function isNullValidArgInMbSubstituteCharacter(): bool
212: {
213: return $this->versionId >= 80000;
214: }
215:
216: public function isInterfaceConstantImplicitlyFinal(): bool
217: {
218: return $this->versionId < 80100;
219: }
220:
221: public function supportsFinalConstants(): bool
222: {
223: return $this->versionId >= 80100;
224: }
225:
226: public function supportsReadOnlyProperties(): bool
227: {
228: return $this->versionId >= 80100;
229: }
230:
231: public function supportsEnums(): bool
232: {
233: return $this->versionId >= 80100;
234: }
235:
236: public function supportsPureIntersectionTypes(): bool
237: {
238: return $this->versionId >= 80100;
239: }
240:
241: public function supportsCaseInsensitiveConstantNames(): bool
242: {
243: return $this->versionId < 80000;
244: }
245:
246: public function hasStricterRoundFunctions(): bool
247: {
248: return $this->versionId >= 80000;
249: }
250:
251: public function throwsValueErrorForInvalidRoundingMode(): bool
252: {
253: return $this->versionId >= 80400;
254: }
255:
256: public function hasTentativeReturnTypes(): bool
257: {
258: return $this->versionId >= 80100;
259: }
260:
261: public function supportsFirstClassCallables(): bool
262: {
263: return $this->versionId >= 80100;
264: }
265:
266: public function supportsArrayUnpackingWithStringKeys(): bool
267: {
268: return $this->versionId >= 80100;
269: }
270:
271: public function throwsOnInvalidMbStringEncoding(): bool
272: {
273: return $this->versionId >= 80000;
274: }
275:
276: public function supportsPassNoneEncodings(): bool
277: {
278: return $this->versionId < 70300;
279: }
280:
281: public function producesWarningForFinalPrivateMethods(): bool
282: {
283: return $this->versionId >= 80000;
284: }
285:
286: public function deprecatesDynamicProperties(): bool
287: {
288: return $this->versionId >= 80200;
289: }
290:
291: public function strSplitReturnsEmptyArray(): bool
292: {
293: return $this->versionId >= 80200;
294: }
295:
296: public function supportsDisjunctiveNormalForm(): bool
297: {
298: return $this->versionId >= 80200;
299: }
300:
301: public function serializableRequiresMagicMethods(): bool
302: {
303: return $this->versionId >= 80100;
304: }
305:
306: public function arrayFunctionsReturnNullWithNonArray(): bool
307: {
308: return $this->versionId < 80000;
309: }
310:
311: // see https://www.php.net/manual/en/migration80.incompatible.php#migration80.incompatible.core.string-number-comparision
312: public function castsNumbersToStringsOnLooseComparison(): bool
313: {
314: return $this->versionId >= 80000;
315: }
316:
317: public function nonNumericStringAndIntegerIsFalseOnLooseComparison(): bool
318: {
319: return $this->versionId >= 80000;
320: }
321:
322: public function supportsCallableInstanceMethods(): bool
323: {
324: return $this->versionId < 80000;
325: }
326:
327: public function supportsJsonValidate(): bool
328: {
329: return $this->versionId >= 80300;
330: }
331:
332: public function supportsConstantsInTraits(): bool
333: {
334: return $this->versionId >= 80200;
335: }
336:
337: public function supportsNativeTypesInClassConstants(): bool
338: {
339: return $this->versionId >= 80300;
340: }
341:
342: public function supportsAbstractTraitMethods(): bool
343: {
344: return $this->versionId >= 80000;
345: }
346:
347: public function supportsOverrideAttribute(): bool
348: {
349: return $this->versionId >= 80300;
350: }
351:
352: public function supportsDynamicClassConstantFetch(): bool
353: {
354: return $this->versionId >= 80300;
355: }
356:
357: public function supportsReadOnlyClasses(): bool
358: {
359: return $this->versionId >= 80200;
360: }
361:
362: public function supportsReadOnlyAnonymousClasses(): bool
363: {
364: return $this->versionId >= 80300;
365: }
366:
367: public function supportsReadonlyPropertyReinitializationOnClone(): bool
368: {
369: return $this->versionId >= 80300;
370: }
371:
372: public function supportsNeverReturnTypeInArrowFunction(): bool
373: {
374: return $this->versionId >= 80200;
375: }
376:
377: public function supportsPregUnmatchedAsNull(): bool
378: {
379: // while PREG_UNMATCHED_AS_NULL is defined in php-src since 7.2.x it starts working as expected with 7.4.x
380: // https://3v4l.org/v3HE4
381: return $this->versionId >= 70400;
382: }
383:
384: public function supportsPregCaptureOnlyNamedGroups(): bool
385: {
386: // https://php.watch/versions/8.2/preg-n-no-capture-modifier
387: return $this->versionId >= 80200;
388: }
389:
390: public function supportsPropertyHooks(): bool
391: {
392: return $this->versionId >= 80400;
393: }
394:
395: public function supportsFinalProperties(): bool
396: {
397: return $this->versionId >= 80400;
398: }
399:
400: public function supportsAsymmetricVisibility(): bool
401: {
402: return $this->versionId >= 80400;
403: }
404:
405: public function supportsAsymmetricVisibilityForStaticProperties(): bool
406: {
407: return $this->versionId >= 80500;
408: }
409:
410: public function supportsLazyObjects(): bool
411: {
412: return $this->versionId >= 80400;
413: }
414:
415: public function hasDateTimeExceptions(): bool
416: {
417: return $this->versionId >= 80300;
418: }
419:
420: public function isCurloptUrlCheckingFileSchemeWithOpenBasedir(): bool
421: {
422: // Before PHP 8.0, when setting CURLOPT_URL, an unparsable URL or a file:// scheme would fail if open_basedir is used
423: // https://github.com/php/php-src/blob/php-7.4.33/ext/curl/interface.c#L139-L158
424: // https://github.com/php/php-src/blob/php-8.0.0/ext/curl/interface.c#L128-L130
425: return $this->versionId < 80000;
426: }
427:
428: /**
429: * whether curl handles are represented as 'resource' or CurlShareHandle
430: */
431: public function supportsCurlShareHandle(): bool
432: {
433: return $this->versionId >= 80000;
434: }
435:
436: public function supportsCurlSharePersistentHandle(): bool
437: {
438: return $this->versionId >= 80500;
439: }
440:
441: public function highlightStringDoesNotReturnFalse(): bool
442: {
443: return $this->versionId >= 80400;
444: }
445:
446: public function deprecatesImplicitlyNullableParameterTypes(): bool
447: {
448: return $this->versionId >= 80400;
449: }
450:
451: public function substrReturnFalseInsteadOfEmptyString(): bool
452: {
453: return $this->versionId < 80000;
454: }
455:
456: public function supportsBcMathNumberOperatorOverloading(): bool
457: {
458: return $this->versionId >= 80400;
459: }
460:
461: public function hasPDOSubclasses(): bool
462: {
463: return $this->versionId >= 80400;
464: }
465:
466: public function deprecatesImplicitlyFloatConversionToInt(): bool
467: {
468: return $this->versionId >= 80100;
469: }
470:
471: public function deprecatesNullArrayOffset(): bool
472: {
473: return $this->versionId >= 80500;
474: }
475:
476: public function supportsFinalPromotedProperties(): bool
477: {
478: return $this->versionId >= 80500;
479: }
480:
481: public function supportsVoidCast(): bool
482: {
483: return $this->versionId >= 80500;
484: }
485:
486: public function supportsNoDiscardAttribute(): bool
487: {
488: return $this->versionId >= 80500;
489: }
490:
491: public function deprecatesNonStandardCasts(): bool
492: {
493: return $this->versionId >= 80500;
494: }
495:
496: public function deprecatesBacktickOperator(): bool
497: {
498: return $this->versionId >= 80500;
499: }
500:
501: public function supportsAttributesOnGlobalConstants(): bool
502: {
503: return $this->versionId >= 80500;
504: }
505:
506: public function supportsDeprecatedTraits(): bool
507: {
508: return $this->versionId >= 80500;
509: }
510:
511: public function supportsOverrideAttributeOnProperty(): bool
512: {
513: return $this->versionId >= 80500;
514: }
515:
516: public function deprecatesDecOnNonNumericString(): bool
517: {
518: return $this->versionId >= 80300;
519: }
520:
521: public function deprecatesIncOnNonNumericString(): bool
522: {
523: return $this->versionId >= 80500;
524: }
525:
526: public function supportsObjectsInArraySumProduct(): bool
527: {
528: return $this->versionId >= 80300;
529: }
530:
531: public function hasFilterThrowOnFailureConstant(): bool
532: {
533: return $this->versionId >= 80500;
534: }
535:
536: public function throwsOnStringCast(): bool
537: {
538: return $this->versionId >= 70400;
539: }
540:
541: }
542: