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 LATEST: PHPVersion = PHPVersion::PHP84;
99
100 pub const NEXT: PHPVersion = PHPVersion::PHP85;
110
111 #[inline]
127 pub const fn new(major: u32, minor: u32, patch: u32) -> Self {
128 Self((major << 16) | (minor << 8) | patch)
129 }
130
131 #[inline]
146 pub const fn from_version_id(version_id: u32) -> Self {
147 Self(version_id)
148 }
149
150 #[inline]
161 pub const fn major(&self) -> u32 {
162 self.0 >> 16
163 }
164
165 #[inline]
176 pub const fn minor(&self) -> u32 {
177 (self.0 >> 8) & 0xff
178 }
179
180 #[inline]
191 pub const fn patch(&self) -> u32 {
192 self.0 & 0xff
193 }
194
195 pub const fn is_at_least(&self, major: u32, minor: u32, patch: u32) -> bool {
210 self.0 >= ((major << 16) | (minor << 8) | patch)
211 }
212
213 pub const fn is_supported(&self, feature: Feature) -> bool {
230 match feature {
231 Feature::NullableTypeHint
232 | Feature::IterableTypeHint
233 | Feature::VoidTypeHint
234 | Feature::ClassLikeConstantVisibilityModifiers
235 | Feature::CatchUnionType => self.0 >= 0x07_01_00,
236 Feature::TrailingCommaInListSyntax
237 | Feature::ParameterTypeWidening
238 | Feature::AllUnicodeScalarCodePointsInMbSubstituteCharacter => self.0 >= 0x07_02_00,
239 Feature::ListReferenceAssignment | Feature::TrailingCommaInFunctionCalls => self.0 >= 0x07_03_00,
240 Feature::NullCoalesceAssign
241 | Feature::ParameterContravariance
242 | Feature::ReturnCovariance
243 | Feature::PregUnmatchedAsNull
244 | Feature::ArrowFunctions
245 | Feature::NumericLiteralSeparator
246 | Feature::TypedProperties => self.0 >= 0x070400,
247 Feature::NonCapturingCatches
248 | Feature::NativeUnionTypes
249 | Feature::LessOverriddenParametersWithVariadic
250 | Feature::ThrowExpression
251 | Feature::ClassConstantOnExpression
252 | Feature::PromotedProperties
253 | Feature::NamedArguments
254 | Feature::ThrowsTypeErrorForInternalFunctions
255 | Feature::ThrowsValueErrorForInternalFunctions
256 | Feature::HHPrintfSpecifier
257 | Feature::StricterRoundFunctions
258 | Feature::ThrowsOnInvalidMbStringEncoding
259 | Feature::WarnsAboutFinalPrivateMethods
260 | Feature::CastsNumbersToStringsOnLooseComparison
261 | Feature::NonNumericStringAndIntegerIsFalseOnLooseComparison
262 | Feature::AbstractTraitMethods
263 | Feature::StaticReturnTypeHint
264 | Feature::AccessClassOnObject
265 | Feature::Attributes
266 | Feature::MixedTypeHint
267 | Feature::MatchExpression
268 | Feature::NullSafeOperator
269 | Feature::TrailingCommaInClosureUseList
270 | Feature::FalseCompoundTypeHint
271 | Feature::NullCompoundTypeHint
272 | Feature::CatchOptionalVariable => self.0 >= 0x08_00_00,
273 Feature::FinalConstants
274 | Feature::ReadonlyProperties
275 | Feature::Enums
276 | Feature::PureIntersectionTypes
277 | Feature::TentativeReturnTypes
278 | Feature::NeverTypeHint
279 | Feature::ClosureCreation
280 | Feature::ArrayUnpackingWithStringKeys
281 | Feature::SerializableRequiresMagicMethods => self.0 >= 0x08_01_00,
282 Feature::ConstantsInTraits
283 | Feature::StrSplitReturnsEmptyArray
284 | Feature::DisjunctiveNormalForm
285 | Feature::ReadonlyClasses
286 | Feature::NeverReturnTypeInArrowFunction
287 | Feature::PregCaptureOnlyNamedGroups
288 | Feature::TrueTypeHint
289 | Feature::FalseTypeHint
290 | Feature::NullTypeHint => self.0 >= 0x08_02_00,
291 Feature::JsonValidate
292 | Feature::TypedClassLikeConstants
293 | Feature::DateTimeExceptions
294 | Feature::OverrideAttribute
295 | Feature::DynamicClassConstantAccess
296 | Feature::ReadonlyAnonymousClasses => self.0 >= 0x08_03_00,
297 Feature::AsymmetricVisibility
298 | Feature::LazyObjects
299 | Feature::HighlightStringDoesNotReturnFalse
300 | Feature::PropertyHooks
301 | Feature::NewWithoutParentheses
302 | Feature::DeprecatedAttribute => self.0 >= 0x08_04_00,
303 Feature::ClosureInConstantExpressions
304 | Feature::ConstantAttributes
305 | Feature::NoDiscardAttribute
306 | Feature::VoidCast
307 | Feature::AsymmetricVisibilityForStaticProperties
308 | Feature::ClosureCreationInConstantExpressions
309 | Feature::PipeOperator => self.0 >= 0x08_05_00,
310 Feature::CallableInstanceMethods
311 | Feature::LegacyConstructor
312 | Feature::UnsetCast
313 | Feature::CaseInsensitiveConstantNames
314 | Feature::ArrayFunctionsReturnNullWithNonArray
315 | Feature::SubstrReturnFalseInsteadOfEmptyString
316 | Feature::CurlUrlOptionCheckingFileSchemeWithOpenBasedir
317 | Feature::EmptyStringValidAliasForNoneInMbSubstituteCharacter
318 | Feature::NumericStringValidArgInMbSubstituteCharacter => self.0 < 0x08_00_00,
319 Feature::InterfaceConstantImplicitlyFinal => self.0 < 0x08_01_00,
320 Feature::PassNoneEncodings => self.0 < 0x07_03_00,
321 _ => true,
322 }
323 }
324
325 pub const fn is_deprecated(&self, feature: Feature) -> bool {
341 match feature {
342 Feature::DynamicProperties | Feature::CallStaticMethodOnTrait => self.0 >= 0x08_02_00,
343 Feature::ImplicitlyNullableParameterTypes => self.0 >= 0x08_04_00,
344 Feature::RequiredParameterAfterOptionalUnionOrMixed => self.0 >= 0x08_03_00,
345 Feature::RequiredParameterAfterOptionalNullableAndDefaultNull => self.0 >= 0x08_01_00,
346 Feature::RequiredParameterAfterOptional => self.0 >= 0x08_00_00,
347 _ => false,
348 }
349 }
350
351 pub const fn to_version_id(&self) -> u32 {
364 self.0
365 }
366}
367
368impl PHPVersionRange {
369 pub const PHP7: PHPVersionRange = Self::between(PHPVersion::new(7, 0, 0), PHPVersion::new(7, 99, 99));
371
372 pub const PHP8: PHPVersionRange = Self::between(PHPVersion::new(8, 0, 0), PHPVersion::new(8, 99, 99));
374
375 pub const fn any() -> Self {
377 Self { min: None, max: None }
378 }
379
380 pub const fn until(version: PHPVersion) -> Self {
382 Self { min: None, max: Some(version) }
383 }
384
385 pub const fn from(version: PHPVersion) -> Self {
387 Self { min: Some(version), max: None }
388 }
389
390 pub const fn between(min: PHPVersion, max: PHPVersion) -> Self {
392 Self { min: Some(min), max: Some(max) }
393 }
394
395 #[inline]
397 pub const fn includes(&self, version: PHPVersion) -> bool {
398 if let Some(min) = self.min
399 && version.0 < min.0
400 {
401 return false;
402 }
403
404 if let Some(max) = self.max
405 && version.0 > max.0
406 {
407 return false;
408 }
409
410 true
411 }
412}
413
414impl std::default::Default for PHPVersion {
415 fn default() -> Self {
416 Self::LATEST
417 }
418}
419
420impl std::fmt::Display for PHPVersion {
421 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
422 write!(f, "{}.{}.{}", self.major(), self.minor(), self.patch())
423 }
424}
425
426impl Serialize for PHPVersion {
427 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
428 where
429 S: Serializer,
430 {
431 serializer.serialize_str(&self.to_string())
432 }
433}
434
435impl<'de> Deserialize<'de> for PHPVersion {
436 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
437 where
438 D: Deserializer<'de>,
439 {
440 let s = String::deserialize(deserializer)?;
441
442 s.parse().map_err(serde::de::Error::custom)
443 }
444}
445
446impl FromStr for PHPVersion {
447 type Err = ParsingError;
448
449 fn from_str(s: &str) -> Result<Self, Self::Err> {
450 if s.is_empty() {
451 return Err(ParsingError::InvalidFormat);
452 }
453
454 let parts = s.split('.').collect::<Vec<_>>();
455 match parts.len() {
456 1 => {
457 let major = parts[0].parse()?;
458
459 Ok(Self::new(major, 0, 0))
460 }
461 2 => {
462 let major = parts[0].parse()?;
463 let minor = parts[1].parse()?;
464
465 Ok(Self::new(major, minor, 0))
466 }
467 3 => {
468 let major = parts[0].parse()?;
469 let minor = parts[1].parse()?;
470 let patch = parts[2].parse()?;
471
472 Ok(Self::new(major, minor, patch))
473 }
474 _ => Err(ParsingError::InvalidFormat),
475 }
476 }
477}
478
479#[cfg(test)]
480mod tests {
481 use super::*;
482
483 #[test]
484 fn test_version() {
485 let version = PHPVersion::new(7, 4, 0);
486 assert_eq!(version.major(), 7);
487 assert_eq!(version.minor(), 4);
488 assert_eq!(version.patch(), 0);
489 }
490
491 #[test]
492 fn test_display() {
493 let version = PHPVersion::new(7, 4, 0);
494 assert_eq!(version.to_string(), "7.4.0");
495 }
496
497 #[test]
498 fn test_from_str_single_segment() {
499 let v: PHPVersion = "7".parse().unwrap();
500 assert_eq!(v.major(), 7);
501 assert_eq!(v.minor(), 0);
502 assert_eq!(v.patch(), 0);
503 assert_eq!(v.to_string(), "7.0.0");
504 }
505
506 #[test]
507 fn test_from_str_two_segments() {
508 let v: PHPVersion = "7.4".parse().unwrap();
509 assert_eq!(v.major(), 7);
510 assert_eq!(v.minor(), 4);
511 assert_eq!(v.patch(), 0);
512 assert_eq!(v.to_string(), "7.4.0");
513 }
514
515 #[test]
516 fn test_from_str_three_segments() {
517 let v: PHPVersion = "8.1.2".parse().unwrap();
518 assert_eq!(v.major(), 8);
519 assert_eq!(v.minor(), 1);
520 assert_eq!(v.patch(), 2);
521 assert_eq!(v.to_string(), "8.1.2");
522 }
523
524 #[test]
525 fn test_from_str_invalid() {
526 let err = "7.4.0.1".parse::<PHPVersion>().unwrap_err();
527 assert_eq!(format!("{err}"), "Invalid version format, expected 'major.minor.patch'.");
528
529 let err = "".parse::<PHPVersion>().unwrap_err();
530 assert_eq!(format!("{err}"), "Invalid version format, expected 'major.minor.patch'.");
531
532 let err = "foo.4.0".parse::<PHPVersion>().unwrap_err();
533 assert_eq!(format!("{err}"), "Failed to parse integer component of version: invalid digit found in string.");
534
535 let err = "7.foo.0".parse::<PHPVersion>().unwrap_err();
536 assert_eq!(format!("{err}"), "Failed to parse integer component of version: invalid digit found in string.");
537
538 let err = "7.4.foo".parse::<PHPVersion>().unwrap_err();
539 assert_eq!(format!("{err}"), "Failed to parse integer component of version: invalid digit found in string.");
540 }
541
542 #[test]
543 fn test_is_supported_features_before_8() {
544 let v_7_4_0 = PHPVersion::new(7, 4, 0);
545
546 assert!(v_7_4_0.is_supported(Feature::NullCoalesceAssign));
547 assert!(!v_7_4_0.is_supported(Feature::NamedArguments));
548
549 assert!(v_7_4_0.is_supported(Feature::CallableInstanceMethods));
550 assert!(v_7_4_0.is_supported(Feature::LegacyConstructor));
551 }
552
553 #[test]
554 fn test_is_supported_features_8_0_0() {
555 let v_8_0_0 = PHPVersion::new(8, 0, 0);
556
557 assert!(v_8_0_0.is_supported(Feature::NamedArguments));
558 assert!(!v_8_0_0.is_supported(Feature::CallableInstanceMethods));
559 }
560
561 #[test]
562 fn test_is_deprecated_features() {
563 let v_7_4_0 = PHPVersion::new(7, 4, 0);
564 assert!(!v_7_4_0.is_deprecated(Feature::DynamicProperties));
565 assert!(!v_7_4_0.is_deprecated(Feature::RequiredParameterAfterOptional));
566
567 let v_8_0_0 = PHPVersion::new(8, 0, 0);
568 assert!(v_8_0_0.is_deprecated(Feature::RequiredParameterAfterOptional));
569 assert!(!v_8_0_0.is_deprecated(Feature::DynamicProperties));
570
571 let v_8_2_0 = PHPVersion::new(8, 2, 0);
572 assert!(v_8_2_0.is_deprecated(Feature::DynamicProperties));
573 }
574
575 #[test]
576 fn test_serde_serialize() {
577 let v_7_4_0 = PHPVersion::new(7, 4, 0);
578 let json = serde_json::to_string(&v_7_4_0).unwrap();
579 assert_eq!(json, "\"7.4.0\"");
580 }
581
582 #[test]
583 fn test_serde_deserialize() {
584 let json = "\"7.4.0\"";
585 let v: PHPVersion = serde_json::from_str(json).unwrap();
586 assert_eq!(v.major(), 7);
587 assert_eq!(v.minor(), 4);
588 assert_eq!(v.patch(), 0);
589
590 let json = "\"7.4\"";
591 let v: PHPVersion = serde_json::from_str(json).unwrap();
592 assert_eq!(v.major(), 7);
593 assert_eq!(v.minor(), 4);
594 assert_eq!(v.patch(), 0);
595 }
596
597 #[test]
598 fn test_serde_round_trip() {
599 let original = PHPVersion::new(8, 1, 5);
600 let serialized = serde_json::to_string(&original).unwrap();
601 let deserialized: PHPVersion = serde_json::from_str(&serialized).unwrap();
602 assert_eq!(original, deserialized);
603 assert_eq!(serialized, "\"8.1.5\"");
604 }
605}