1use std::str::FromStr;
2
3use schemars::JsonSchema;
4use serde::Deserialize;
5use serde::Deserializer;
6use serde::Serialize;
7use serde::Serializer;
8
9use crate::error::ParsingError;
10use crate::feature::Feature;
11
12pub mod error;
13pub mod feature;
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, JsonSchema)]
31#[schemars(with = "String")]
32#[repr(transparent)]
33pub struct PHPVersion(u32);
34
35#[derive(Debug, PartialEq, Eq, Ord, Copy, Clone, PartialOrd, Deserialize, Serialize, Default, Hash, JsonSchema)]
51pub struct PHPVersionRange {
52 pub min: Option<PHPVersion>,
53 pub max: Option<PHPVersion>,
54}
55
56impl PHPVersion {
57 pub const PHP70: PHPVersion = PHPVersion::new(7, 0, 0);
59
60 pub const PHP71: PHPVersion = PHPVersion::new(7, 1, 0);
62
63 pub const PHP72: PHPVersion = PHPVersion::new(7, 2, 0);
65
66 pub const PHP73: PHPVersion = PHPVersion::new(7, 3, 0);
68
69 pub const PHP74: PHPVersion = PHPVersion::new(7, 4, 0);
71
72 pub const PHP80: PHPVersion = PHPVersion::new(8, 0, 0);
74
75 pub const PHP81: PHPVersion = PHPVersion::new(8, 1, 0);
77
78 pub const PHP82: PHPVersion = PHPVersion::new(8, 2, 0);
80
81 pub const PHP83: PHPVersion = PHPVersion::new(8, 3, 0);
83
84 pub const PHP84: PHPVersion = PHPVersion::new(8, 4, 0);
86
87 pub const PHP85: PHPVersion = PHPVersion::new(8, 5, 0);
89
90 pub const PHP86: PHPVersion = PHPVersion::new(8, 6, 0);
92
93 pub const LATEST: PHPVersion = PHPVersion::PHP85;
102
103 pub const NEXT: PHPVersion = PHPVersion::PHP86;
113
114 #[inline]
130 #[must_use]
131 pub const fn new(major: u32, minor: u32, patch: u32) -> Self {
132 Self((major << 16) | (minor << 8) | patch)
133 }
134
135 #[inline]
150 #[must_use]
151 pub const fn from_version_id(version_id: u32) -> Self {
152 Self(version_id)
153 }
154
155 #[inline]
166 #[must_use]
167 pub const fn major(&self) -> u32 {
168 self.0 >> 16
169 }
170
171 #[inline]
182 #[must_use]
183 pub const fn minor(&self) -> u32 {
184 (self.0 >> 8) & 0xff
185 }
186
187 #[inline]
198 #[must_use]
199 pub const fn patch(&self) -> u32 {
200 self.0 & 0xff
201 }
202
203 #[must_use]
218 pub const fn is_at_least(&self, major: u32, minor: u32, patch: u32) -> bool {
219 self.0 >= ((major << 16) | (minor << 8) | patch)
220 }
221
222 #[must_use]
239 pub const fn is_supported(&self, feature: Feature) -> bool {
240 match feature {
241 Feature::NullableTypeHint
242 | Feature::IterableTypeHint
243 | Feature::VoidTypeHint
244 | Feature::ClassLikeConstantVisibilityModifiers
245 | Feature::CatchUnionType => self.0 >= 0x07_01_00,
246 Feature::TrailingCommaInListSyntax
247 | Feature::ParameterTypeWidening
248 | Feature::AllUnicodeScalarCodePointsInMbSubstituteCharacter => self.0 >= 0x07_02_00,
249 Feature::ListReferenceAssignment | Feature::TrailingCommaInFunctionCalls => self.0 >= 0x07_03_00,
250 Feature::NullCoalesceAssign
251 | Feature::ParameterContravariance
252 | Feature::ReturnCovariance
253 | Feature::PregUnmatchedAsNull
254 | Feature::ArrowFunctions
255 | Feature::NumericLiteralSeparator
256 | Feature::TypedProperties => self.0 >= 0x07_04_00,
257 Feature::NonCapturingCatches
258 | Feature::NativeUnionTypes
259 | Feature::LessOverriddenParametersWithVariadic
260 | Feature::ThrowExpression
261 | Feature::ClassConstantOnExpression
262 | Feature::PromotedProperties
263 | Feature::NamedArguments
264 | Feature::ThrowsTypeErrorForInternalFunctions
265 | Feature::ThrowsValueErrorForInternalFunctions
266 | Feature::HHPrintfSpecifier
267 | Feature::StricterRoundFunctions
268 | Feature::ThrowsOnInvalidMbStringEncoding
269 | Feature::WarnsAboutFinalPrivateMethods
270 | Feature::CastsNumbersToStringsOnLooseComparison
271 | Feature::NonNumericStringAndIntegerIsFalseOnLooseComparison
272 | Feature::AbstractTraitMethods
273 | Feature::StaticReturnTypeHint
274 | Feature::AccessClassOnObject
275 | Feature::Attributes
276 | Feature::MixedTypeHint
277 | Feature::MatchExpression
278 | Feature::NullSafeOperator
279 | Feature::TrailingCommaInClosureUseList
280 | Feature::FalseCompoundTypeHint
281 | Feature::NullCompoundTypeHint
282 | Feature::CatchOptionalVariable => self.0 >= 0x08_00_00,
283 Feature::FinalConstants
284 | Feature::ReadonlyProperties
285 | Feature::Enums
286 | Feature::PureIntersectionTypes
287 | Feature::TentativeReturnTypes
288 | Feature::NeverTypeHint
289 | Feature::ClosureCreation
290 | Feature::ArrayUnpackingWithStringKeys
291 | Feature::SerializableRequiresMagicMethods => self.0 >= 0x08_01_00,
292 Feature::ConstantsInTraits
293 | Feature::StrSplitReturnsEmptyArray
294 | Feature::DisjunctiveNormalForm
295 | Feature::ReadonlyClasses
296 | Feature::NeverReturnTypeInArrowFunction
297 | Feature::PregCaptureOnlyNamedGroups
298 | Feature::TrueTypeHint
299 | Feature::FalseTypeHint
300 | Feature::NullTypeHint => self.0 >= 0x08_02_00,
301 Feature::JsonValidate
302 | Feature::TypedClassLikeConstants
303 | Feature::DateTimeExceptions
304 | Feature::OverrideAttribute
305 | Feature::DynamicClassConstantAccess
306 | Feature::ReadonlyAnonymousClasses => self.0 >= 0x08_03_00,
307 Feature::AsymmetricVisibility
308 | Feature::LazyObjects
309 | Feature::HighlightStringDoesNotReturnFalse
310 | Feature::PropertyHooks
311 | Feature::NewWithoutParentheses
312 | Feature::DeprecatedAttribute => self.0 >= 0x08_04_00,
313 Feature::ClosureInConstantExpressions
314 | Feature::ConstantAttributes
315 | Feature::NoDiscardAttribute
316 | Feature::VoidCast
317 | Feature::CloneWith
318 | Feature::AsymmetricVisibilityForStaticProperties
319 | Feature::ClosureCreationInConstantExpressions
320 | Feature::PipeOperator => self.0 >= 0x08_05_00,
321 Feature::CallableInstanceMethods
322 | Feature::LegacyConstructor
323 | Feature::UnsetCast
324 | Feature::CaseInsensitiveConstantNames
325 | Feature::ArrayFunctionsReturnNullWithNonArray
326 | Feature::SubstrReturnFalseInsteadOfEmptyString
327 | Feature::CurlUrlOptionCheckingFileSchemeWithOpenBasedir
328 | Feature::EmptyStringValidAliasForNoneInMbSubstituteCharacter
329 | Feature::NumericStringValidArgInMbSubstituteCharacter => self.0 < 0x08_00_00,
330 Feature::InterfaceConstantImplicitlyFinal => self.0 < 0x08_01_00,
331 Feature::PassNoneEncodings => self.0 < 0x07_03_00,
332 Feature::PartialFunctionApplication => self.0 >= 0x08_06_00,
333 _ => true,
334 }
335 }
336
337 #[must_use]
353 pub const fn is_deprecated(&self, feature: Feature) -> bool {
354 match feature {
355 Feature::DynamicProperties | Feature::CallStaticMethodOnTrait => self.0 >= 0x08_02_00,
356 Feature::ImplicitlyNullableParameterTypes => self.0 >= 0x08_04_00,
357 Feature::RequiredParameterAfterOptionalUnionOrMixed => self.0 >= 0x08_03_00,
358 Feature::RequiredParameterAfterOptionalNullableAndDefaultNull => self.0 >= 0x08_01_00,
359 Feature::RequiredParameterAfterOptional => self.0 >= 0x08_00_00,
360 Feature::SwitchSemicolonSeparators => self.0 >= 0x08_05_00,
361 _ => false,
362 }
363 }
364
365 #[must_use]
378 pub const fn to_version_id(&self) -> u32 {
379 self.0
380 }
381}
382
383impl PHPVersionRange {
384 pub const PHP7: PHPVersionRange = Self::between(PHPVersion::new(7, 0, 0), PHPVersion::new(7, 99, 99));
386
387 pub const PHP8: PHPVersionRange = Self::between(PHPVersion::new(8, 0, 0), PHPVersion::new(8, 99, 99));
389
390 #[must_use]
392 pub const fn any() -> Self {
393 Self { min: None, max: None }
394 }
395
396 #[must_use]
398 pub const fn until(version: PHPVersion) -> Self {
399 Self { min: None, max: Some(version) }
400 }
401
402 #[must_use]
404 pub const fn from(version: PHPVersion) -> Self {
405 Self { min: Some(version), max: None }
406 }
407
408 #[must_use]
410 pub const fn between(min: PHPVersion, max: PHPVersion) -> Self {
411 Self { min: Some(min), max: Some(max) }
412 }
413
414 #[inline]
416 #[must_use]
417 pub const fn includes(&self, version: PHPVersion) -> bool {
418 if let Some(min) = self.min
419 && version.0 < min.0
420 {
421 return false;
422 }
423
424 if let Some(max) = self.max
425 && version.0 > max.0
426 {
427 return false;
428 }
429
430 true
431 }
432}
433
434impl std::default::Default for PHPVersion {
435 fn default() -> Self {
436 Self::LATEST
437 }
438}
439
440impl std::fmt::Display for PHPVersion {
441 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
442 write!(f, "{}.{}.{}", self.major(), self.minor(), self.patch())
443 }
444}
445
446impl Serialize for PHPVersion {
447 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
448 where
449 S: Serializer,
450 {
451 serializer.serialize_str(&self.to_string())
452 }
453}
454
455impl<'de> Deserialize<'de> for PHPVersion {
456 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
457 where
458 D: Deserializer<'de>,
459 {
460 let s = String::deserialize(deserializer)?;
461
462 s.parse().map_err(serde::de::Error::custom)
463 }
464}
465
466impl FromStr for PHPVersion {
467 type Err = ParsingError;
468
469 fn from_str(s: &str) -> Result<Self, Self::Err> {
470 if s.is_empty() {
471 return Err(ParsingError::InvalidFormat);
472 }
473
474 let parts = s.split('.').collect::<Vec<_>>();
475 match parts.len() {
476 1 => {
477 let major = parts[0].parse()?;
478
479 Ok(Self::new(major, 0, 0))
480 }
481 2 => {
482 let major = parts[0].parse()?;
483 let minor = parts[1].parse()?;
484
485 Ok(Self::new(major, minor, 0))
486 }
487 3 => {
488 let major = parts[0].parse()?;
489 let minor = parts[1].parse()?;
490 let patch = parts[2].parse()?;
491
492 Ok(Self::new(major, minor, patch))
493 }
494 _ => Err(ParsingError::InvalidFormat),
495 }
496 }
497}
498
499#[cfg(test)]
500mod tests {
501 use super::*;
502
503 #[test]
504 fn test_version() {
505 let version = PHPVersion::new(7, 4, 0);
506 assert_eq!(version.major(), 7);
507 assert_eq!(version.minor(), 4);
508 assert_eq!(version.patch(), 0);
509 }
510
511 #[test]
512 fn test_display() {
513 let version = PHPVersion::new(7, 4, 0);
514 assert_eq!(version.to_string(), "7.4.0");
515 }
516
517 #[test]
518 fn test_from_str_single_segment() {
519 let v: PHPVersion = "7".parse().unwrap();
520 assert_eq!(v.major(), 7);
521 assert_eq!(v.minor(), 0);
522 assert_eq!(v.patch(), 0);
523 assert_eq!(v.to_string(), "7.0.0");
524 }
525
526 #[test]
527 fn test_from_str_two_segments() {
528 let v: PHPVersion = "7.4".parse().unwrap();
529 assert_eq!(v.major(), 7);
530 assert_eq!(v.minor(), 4);
531 assert_eq!(v.patch(), 0);
532 assert_eq!(v.to_string(), "7.4.0");
533 }
534
535 #[test]
536 fn test_from_str_three_segments() {
537 let v: PHPVersion = "8.1.2".parse().unwrap();
538 assert_eq!(v.major(), 8);
539 assert_eq!(v.minor(), 1);
540 assert_eq!(v.patch(), 2);
541 assert_eq!(v.to_string(), "8.1.2");
542 }
543
544 #[test]
545 fn test_from_str_invalid() {
546 let err = "7.4.0.1".parse::<PHPVersion>().unwrap_err();
547 assert_eq!(format!("{err}"), "Invalid version format, expected 'major.minor.patch'.");
548
549 let err = "".parse::<PHPVersion>().unwrap_err();
550 assert_eq!(format!("{err}"), "Invalid version format, expected 'major.minor.patch'.");
551
552 let err = "foo.4.0".parse::<PHPVersion>().unwrap_err();
553 assert_eq!(format!("{err}"), "Failed to parse integer component of version: invalid digit found in string.");
554
555 let err = "7.foo.0".parse::<PHPVersion>().unwrap_err();
556 assert_eq!(format!("{err}"), "Failed to parse integer component of version: invalid digit found in string.");
557
558 let err = "7.4.foo".parse::<PHPVersion>().unwrap_err();
559 assert_eq!(format!("{err}"), "Failed to parse integer component of version: invalid digit found in string.");
560 }
561
562 #[test]
563 fn test_is_supported_features_before_8() {
564 let v_7_4_0 = PHPVersion::new(7, 4, 0);
565
566 assert!(v_7_4_0.is_supported(Feature::NullCoalesceAssign));
567 assert!(!v_7_4_0.is_supported(Feature::NamedArguments));
568
569 assert!(v_7_4_0.is_supported(Feature::CallableInstanceMethods));
570 assert!(v_7_4_0.is_supported(Feature::LegacyConstructor));
571 }
572
573 #[test]
574 fn test_is_supported_features_8_0_0() {
575 let v_8_0_0 = PHPVersion::new(8, 0, 0);
576
577 assert!(v_8_0_0.is_supported(Feature::NamedArguments));
578 assert!(!v_8_0_0.is_supported(Feature::CallableInstanceMethods));
579 }
580
581 #[test]
582 fn test_is_deprecated_features() {
583 let v_7_4_0 = PHPVersion::new(7, 4, 0);
584 assert!(!v_7_4_0.is_deprecated(Feature::DynamicProperties));
585 assert!(!v_7_4_0.is_deprecated(Feature::RequiredParameterAfterOptional));
586
587 let v_8_0_0 = PHPVersion::new(8, 0, 0);
588 assert!(v_8_0_0.is_deprecated(Feature::RequiredParameterAfterOptional));
589 assert!(!v_8_0_0.is_deprecated(Feature::DynamicProperties));
590
591 let v_8_2_0 = PHPVersion::new(8, 2, 0);
592 assert!(v_8_2_0.is_deprecated(Feature::DynamicProperties));
593 }
594
595 #[test]
596 fn test_serde_serialize() {
597 let v_7_4_0 = PHPVersion::new(7, 4, 0);
598 let json = serde_json::to_string(&v_7_4_0).unwrap();
599 assert_eq!(json, "\"7.4.0\"");
600 }
601
602 #[test]
603 fn test_serde_deserialize() {
604 let json = "\"7.4.0\"";
605 let v: PHPVersion = serde_json::from_str(json).unwrap();
606 assert_eq!(v.major(), 7);
607 assert_eq!(v.minor(), 4);
608 assert_eq!(v.patch(), 0);
609
610 let json = "\"7.4\"";
611 let v: PHPVersion = serde_json::from_str(json).unwrap();
612 assert_eq!(v.major(), 7);
613 assert_eq!(v.minor(), 4);
614 assert_eq!(v.patch(), 0);
615 }
616
617 #[test]
618 fn test_serde_round_trip() {
619 let original = PHPVersion::new(8, 1, 5);
620 let serialized = serde_json::to_string(&original).unwrap();
621 let deserialized: PHPVersion = serde_json::from_str(&serialized).unwrap();
622 assert_eq!(original, deserialized);
623 assert_eq!(serialized, "\"8.1.5\"");
624 }
625}