private/PhpVersion.ps1

class PhpVersion : System.IComparable
{
    <#
    The version of PHP, without the RC state
    #>

    [string]
    [ValidateNotNull()]
    [ValidateLength(1, [int]::MaxValue)]
    $Version
    <#
    The number of the release candidate, if any
    #>

    [Nullable[int]] $RC
    <#
    the version of PHP, possibly including the RC state
    #>

    [ValidateNotNull()]
    [ValidateLength(1, [int]::MaxValue)]
    [string] $FullVersion
    <#
    A string used to display the details of the version
    #>

    [ValidateNotNull()]
    [ValidateLength(1, [int]::MaxValue)]
    [string] $DisplayName
    <#
    A System.Version to be used to compare PHP versions
    #>

    [ValidateNotNull()]
    [System.Version] hidden $ComparableVersion
    <#
    The architecture, x86 or x64
    #>

    [ValidateNotNull()]
    [ValidateSet('x86', 'x64')]
    [string] $Architecture
    <#
    Is this version thread-safe?
    #>

    [ValidateNotNull()]
    [bool] $ThreadSafe
    <#
    The version of the Visual C++ Redistributables required by this PHP version
    #>

    [ValidateNotNull()]
    [int] $VCVersion
    <#
    Initialize the instance.
    Keys for $data:
    - Version: required
    - RC: optional
    - Architecture: required
    - ThreadSafe: required
    - VCVersion: required
    #>

    hidden PhpVersion([hashtable] $data)
    {
        $this.Version = $data.Version
        if ($data.ContainsKey('RC') -and $data.RC -ne '') {
            $this.RC = [int] $data.RC
        } else {
            $this.RC = $null
        }
        $dv = $this.Version
        $cv = $this.Version
        if ($null -eq $this.RC) {
            $cv += '.9999'
        } else {
            $dv += 'RC' + $this.RC
            $cv += '.' + $this.RC
        }
        $this.FullVersion = $dv
        $this.ComparableVersion = [System.Version] $cv
        $this.Architecture = $data.Architecture
        $this.ThreadSafe = $data.ThreadSafe
        $this.VCVersion = $data.VCVersion
        $dn = 'PHP ' + $this.FullVersion + ' ' + $this.Architecture
        if ($this.Architecture -eq $Script:ARCHITECTURE_32BITS) {
            $dn += ' (32-bit)'
        } elseif ($this.Architecture -eq $Script:ARCHITECTURE_64BITS) {
            $dn += ' (64-bit)'
        }
        if ($this.ThreadSafe) {
            $dn += ' Thread-Safe'
        } else {
            $dn += ' Non-Thread-Safe'
        }
        $this.DisplayName = $dn
    }

    [int] CompareTo($that)
    {
        if (-Not($that -is [PhpVersion])) {
            throw "A PhpVersion instance can be compared only to another PhpVersion instance"
        }
        if ($this.ComparableVersion -lt $that.ComparableVersion) {
            $cmp = -1
        } elseif ($this.ComparableVersion -gt $that.ComparableVersion) {
            $cmp = 1
        } else {
            if ($this.Architecture -gt $that.Architecture) {
                $cmp = -1
            } elseif ($this.Architecture -lt $that.Architecture) {
                $cmp = -1
            } else {
                if ($this.ThreadSafe -and -Not $that.ThreadSafe) {
                    $cmp = -1
                } elseif ($that.ThreadSafe -and -Not $this.ThreadSafe) {
                    $cmp = 1
                } else {
                    $cmp = 0
                }
            }
        }
        return $cmp
    }
}

class PhpVersionDownloadable : PhpVersion
{
    <#
    The state of the release
    #>

    [ValidateNotNull()]
    [ValidateLength(1, [int]::MaxValue)]
    [string] $ReleaseState
    <#
    The URL where the PHP version ZIP archive can be downloaded from
    #>

    [ValidateNotNull()]
    [ValidateLength(1, [int]::MaxValue)]
    [string] $DownloadUrl
    <#
    Initialize the instance.
    Keys for $data: the ones of PhpVersion plus:
    - ReleaseState: required
    - DownloadUrl: required
    #>

    hidden PhpVersionDownloadable([hashtable] $data) : base($data)
    {
        $this.ReleaseState = $data.ReleaseState
        $this.DownloadUrl = $data.DownloadUrl
    }
}

class PhpVersionInstalled : PhpVersion
{
    <#
    The folder where PHP is installed (always set)
    #>

    [ValidateNotNull()]
    [ValidateLength(1, [int]::MaxValue)]
    [string] $Folder
    <#
    The folder where PHP is installed (always set) - May be different from Folder if Folder is a junction
    #>

    [ValidateNotNull()]
    [ValidateLength(1, [int]::MaxValue)]
    [string] hidden $ActualFolder
    <#
    The full path of php.exe (always set)
    #>

    [ValidateNotNull()]
    [ValidateLength(1, [int]::MaxValue)]
    [string] $ExecutablePath
    <#
    The full path of php.ini (always set)
    #>

    [ValidateNotNull()]
    [ValidateLength(1, [int]::MaxValue)]
    [string] hidden $IniPath
    <#
    The default path where the PHP extensions are stored (empty string if not set)
    #>

    [ValidateNotNull()]
    [string] $ExtensionsPath
    <#
    Initialize the instance.
    Keys for $data: the ones of PhpVersion plus:
    - ActualFolder: required
    - ExecutablePath: required
    - IniPath: required
    - ExtensionsPath: optional
    #>

    hidden PhpVersionInstalled([hashtable] $data) : base($data)
    {
        $this.Folder = (Split-Path -LiteralPath $data.ExecutablePath).TrimEnd([System.IO.Path]::DirectorySeparatorChar)
        $this.ActualFolder = $data.ActualFolder.TrimEnd([System.IO.Path]::DirectorySeparatorChar)
        $this.ExecutablePath = $data.ExecutablePath
        $this.IniPath = $data.IniPath
        if ($data.ContainsKey('ExtensionsPath') -and $null -ne $data.ExtensionsPath) {
            $this.ExtensionsPath = $data.ExtensionsPath
        } else {
            $this.ExtensionsPath = ''
        }
    }
    [PhpVersionInstalled] static FromPath([string] $Path)
    {
        $directorySeparator = [System.IO.Path]::DirectorySeparatorChar
        $data = @{}
        $item = Get-Item -LiteralPath $Path
        if ($item -is [System.IO.FileInfo]) {
            if ($item.Extension -ne '.exe') {
                return [PhpVersionInstalled]::FromPath($item.DirectoryName)
            }
            $directory = $item.Directory
            $data.ExecutablePath = $item.FullName
        } elseif ($item -is [System.IO.DirectoryInfo]) {
            $directory = $item
            $data.ExecutablePath = Join-Path -Path $item.FullName -ChildPath 'php.exe'
            if (-Not(Test-Path -LiteralPath $data.ExecutablePath -PathType Leaf)) {
                throw "Unable to find php.exe in $Path"
            }
        } else {
            throw "Unrecognized PHP path: $Path"
        }
        $directoryPath = $directory.FullName.TrimEnd($directorySeparator) + $directorySeparator
        $actualDirectoryPath = $null
        if ($directory.Target -and $directory.Target.Count -gt 0 -and $directory.Target[0]) {
            try {
                $actualDirectoryPath = (Get-Item -LiteralPath $directory.Target[0]).FullName.TrimEnd($directorySeparator) + $directorySeparator
            } catch {
                Write-Debug $_
            }
        }
        if (-Not($actualDirectoryPath)) {
            $actualDirectoryPath = $directoryPath
        }
        $data.ActualFolder = $actualDirectoryPath.TrimEnd($directorySeparator)
        $executableResult = & $data.ExecutablePath @('-n', '-r', 'echo PHP_VERSION, ''@'', PHP_INT_SIZE * 8;')
        if (-Not($executableResult)) {
            throw "Failed to execute php.exe: $LASTEXITCODE"
        }
        $match = $executableResult | Select-String -Pattern '^(\d+\.\d+\.\d+)(?:RC(\d+))?@(\d+)$'
        $data.Version = $match.Matches.Groups[1].Value
        $data.RC = $match.Matches.Groups[2].Value
        $data.Architecture = Get-Variable -Scope Script -ValueOnly -Name $('ARCHITECTURE_' + $match.Matches.Groups[3].Value + 'BITS')
        $executableResult = & $data.ExecutablePath @('-i')
        $match = $executableResult | Select-String -CaseSensitive -Pattern '^[ \t]*Thread Safety\s*=>\s*(\w+)'
        $data.ThreadSafe = $match.Matches.Groups[1].Value -eq 'enabled'
        $match = $executableResult | Select-String -CaseSensitive -Pattern '^[ \t]*Compiler\s*=>\s*MSVC([\d]{1,2})'
        if ($null -eq $match) {
            if ([System.Version]$data.Version -le [System.Version]'5.2.9999') {
                $data.VCVersion = 6
            } else {
                throw 'Failed to recognize VCVersion'
            }
        } else {
            $data.VCVersion = $match.Matches.Groups[1].Value
        }
        $match = $executableResult | Select-String -CaseSensitive -Pattern '^[ \t]*Loaded Configuration File\s*=>\s*([\S].*[\S])\s*$'
        $data.IniPath = ''
        if ($match) {
            $data.IniPath = $match.Matches.Groups[1].Value
            if ($data.IniPath -eq '(none)') {
                $data.IniPath = ''
            } else {
                $data.IniPath = $data.IniPath -replace '/',$directorySeparator
                $data.IniPath = [System.IO.Path]::Combine($actualDirectoryPath, $data.IniPath)
                $data.IniPath = $data.IniPath -replace [regex]::Escape("$directorySeparator.$directorySeparator"),$directorySeparator
            }
        }
        if ($data.IniPath -eq '') {
            $data.IniPath = Join-Path -Path $actualDirectoryPath -ChildPath 'php.ini'
        } elseif ($directoryPath -ne $actualDirectoryPath -and $data.IniPath -imatch ('^' + [regex]::Escape($directoryPath) + '.+')) {
            $data.IniPath = $data.IniPath -ireplace ('^' + [regex]::Escape($directoryPath)),$actualDirectoryPath
        }
        $match = $executableResult | Select-String -CaseSensitive -Pattern '^[ \t]*extension_dir\s*=>\s*([\S].*[\S])\s*=>'
        $data.ExtensionsPath = ''
        if ($match) {
            $data.ExtensionsPath = $match.Matches.Groups[1].Value
            if ($data.ExtensionsPath -eq '(none)') {
                $data.ExtensionsPath = ''
            } else {
                $data.ExtensionsPath = $data.ExtensionsPath -replace '/',$directorySeparator
                $data.ExtensionsPath = [System.IO.Path]::Combine($actualDirectoryPath, $data.ExtensionsPath)
                $data.ExtensionsPath = $data.ExtensionsPath -replace [regex]::Escape("$directorySeparator.$directorySeparator"),$directorySeparator
            }
        }
        return [PhpVersionInstalled]::new($data)
    }
    [PhpVersionInstalled[]] static FromEnvironment()
    {
        $result = @()
        $envPath = $env:Path
        if ($null -ne $envPath -and $envPath -ne '') {
            $donePaths = @{}
            $envPaths = $envPath -split [System.IO.Path]::PathSeparator
            foreach ($path in $envPaths) {
                if ($path -ne '') {
                    $ep = Join-Path -Path $path -ChildPath 'php.exe'
                    if (Test-Path -Path $ep -PathType Leaf) {
                        $ep = (Get-Item -LiteralPath $ep).FullName
                        $key = $ep.ToLowerInvariant()
                        if (-Not($donePaths.ContainsKey($key))) {
                            $donePaths[$key] = $true
                            $result += [PhpVersionInstalled]::FromPath($ep)
                        }
                    }
                }
            }
       }
       return $result
    }
    [PhpVersionInstalled] static FromEnvironmentOne()
    {
        $found = [PhpVersionInstalled]::FromEnvironment()
        if ($found.Count -eq 0) {
            throw "No PHP versions found in the current PATHs: use the -Path argument to specify the location of installed PHP"
        }
        if ($found.Count -gt 1) {
            throw "Multiple PHP versions found in the current PATHs: use the -Path argument to specify the location of installed PHP"
        }
        return $found[0]
    }
}