mirror of
https://github.com/nicoverbruggen/phpmon.git
synced 2025-08-08 04:20:07 +02:00
♻️ Rework various common classes
This commit is contained in:
202
phpmon-common/Core/Actions.swift
Normal file
202
phpmon-common/Core/Actions.swift
Normal file
@ -0,0 +1,202 @@
|
||||
//
|
||||
// Services.swift
|
||||
// PHP Monitor
|
||||
//
|
||||
// Copyright © 2021 Nico Verbruggen. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import AppKit
|
||||
|
||||
class Actions {
|
||||
|
||||
// MARK: - Services
|
||||
|
||||
public static func restartPhpFpm()
|
||||
{
|
||||
brew("services restart \(PhpSwitcher.phpInstall.formula)", sudo: true)
|
||||
}
|
||||
|
||||
public static func restartNginx()
|
||||
{
|
||||
brew("services restart nginx", sudo: true)
|
||||
}
|
||||
|
||||
public static func restartDnsMasq()
|
||||
{
|
||||
brew("services restart dnsmasq", sudo: true)
|
||||
}
|
||||
|
||||
public static func stopAllServices()
|
||||
{
|
||||
brew("services stop \(PhpSwitcher.phpInstall.formula)", sudo: true)
|
||||
brew("services stop nginx", sudo: true)
|
||||
brew("services stop dnsmasq", sudo: true)
|
||||
}
|
||||
|
||||
/**
|
||||
Kindly asks Valet to switch to a specific PHP version.
|
||||
*/
|
||||
public static func switchToPhpVersionUsingValet(
|
||||
version: String,
|
||||
availableVersions: [String],
|
||||
completed: @escaping () -> Void
|
||||
) {
|
||||
print("Switching to \(version) using Valet")
|
||||
print(valet("use php@\(version)"))
|
||||
completed()
|
||||
}
|
||||
|
||||
/**
|
||||
Switching to a new PHP version involves:
|
||||
- unlinking the current version
|
||||
- stopping the active services
|
||||
- linking the new desired version
|
||||
|
||||
Please note that depending on which version is installed,
|
||||
the version that is switched to may or may not be identical to `php` (without @version).
|
||||
*/
|
||||
public static func switchToPhpVersion(
|
||||
version: String,
|
||||
availableVersions: [String],
|
||||
completed: @escaping () -> Void
|
||||
) {
|
||||
print("Switching to \(version), unlinking all versions...")
|
||||
|
||||
let group = DispatchGroup()
|
||||
|
||||
availableVersions.forEach { (available) in
|
||||
group.enter()
|
||||
|
||||
DispatchQueue.global(qos: .userInitiated).async {
|
||||
let formula = (available == PhpSwitcher.brewPhpVersion)
|
||||
? "php" : "php@\(available)"
|
||||
|
||||
brew("unlink \(formula)")
|
||||
brew("services stop \(formula)", sudo: true)
|
||||
|
||||
group.leave()
|
||||
}
|
||||
}
|
||||
|
||||
group.notify(queue: .global(qos: .userInitiated)) {
|
||||
print("All versions have been unlinked!")
|
||||
print("Linking the new version!")
|
||||
|
||||
let formula = (version == PhpSwitcher.brewPhpVersion) ? "php" : "php@\(version)"
|
||||
brew("link \(formula) --overwrite --force")
|
||||
brew("services start \(formula)", sudo: true)
|
||||
|
||||
print("The new version has been linked!")
|
||||
completed()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Finding Config Files
|
||||
|
||||
public static func openGenericPhpConfigFolder()
|
||||
{
|
||||
let files = [NSURL(fileURLWithPath: "\(Paths.etcPath)/php")];
|
||||
NSWorkspace.shared.activateFileViewerSelecting(files as [URL])
|
||||
}
|
||||
|
||||
public static func openGlobalComposerFolder()
|
||||
{
|
||||
let file = FileManager.default.homeDirectoryForCurrentUser
|
||||
.appendingPathComponent(".composer/composer.json")
|
||||
NSWorkspace.shared.activateFileViewerSelecting([file] as [URL])
|
||||
}
|
||||
|
||||
public static func openPhpConfigFolder(version: String)
|
||||
{
|
||||
let files = [NSURL(fileURLWithPath: "\(Paths.etcPath)/php/\(version)/php.ini")];
|
||||
NSWorkspace.shared.activateFileViewerSelecting(files as [URL])
|
||||
}
|
||||
|
||||
public static func openValetConfigFolder()
|
||||
{
|
||||
let file = FileManager.default.homeDirectoryForCurrentUser
|
||||
.appendingPathComponent(".config/valet")
|
||||
NSWorkspace.shared.activateFileViewerSelecting([file] as [URL])
|
||||
}
|
||||
|
||||
// MARK: - Quick Fix
|
||||
|
||||
/**
|
||||
Detects all currently available PHP versions,
|
||||
and unlinks each and every one of them.
|
||||
|
||||
After this, the brew services are also stopped,
|
||||
the latest PHP version is linked, and php + nginx are restarted.
|
||||
|
||||
If this does not solve the issue, the user may need to install additional
|
||||
extensions and/or run `composer global update`.
|
||||
*/
|
||||
public static func fixMyPhp()
|
||||
{
|
||||
brew("services restart dnsmasq", sudo: true)
|
||||
|
||||
PhpSwitcher.shared.detectPhpVersions().forEach { (version) in
|
||||
let formula = (version == PhpSwitcher.brewPhpVersion) ? "php" : "php@\(version)"
|
||||
brew("unlink php@\(version)")
|
||||
brew("services stop \(formula)")
|
||||
brew("services stop \(formula)", sudo: true)
|
||||
}
|
||||
|
||||
brew("services stop php")
|
||||
brew("services stop nginx")
|
||||
brew("link php")
|
||||
brew("services restart dnsmasq", sudo: true)
|
||||
brew("services stop php", sudo: true)
|
||||
brew("services stop nginx", sudo: true)
|
||||
}
|
||||
|
||||
// MARK: Common Shell Commands
|
||||
|
||||
/**
|
||||
Runs a `valet` command.
|
||||
*/
|
||||
public static func valet(_ command: String) -> String
|
||||
{
|
||||
return Shell.pipe("sudo \(Paths.valet) \(command)", requiresPath: true)
|
||||
}
|
||||
|
||||
/**
|
||||
Runs a `brew` command. Can run as superuser.
|
||||
*/
|
||||
public static func brew(_ command: String, sudo: Bool = false)
|
||||
{
|
||||
Shell.run("\(sudo ? "sudo " : "")" + "\(Paths.brew) \(command)")
|
||||
}
|
||||
|
||||
/**
|
||||
Runs `sed` in order to replace all occurrences of a string in a specific file with another.
|
||||
*/
|
||||
public static func sed(file: String, original: String, replacement: String)
|
||||
{
|
||||
// Escape slashes (or `sed` won't work)
|
||||
let e_original = original.replacingOccurrences(of: "/", with: "\\/")
|
||||
let e_replacement = replacement.replacingOccurrences(of: "/", with: "\\/")
|
||||
|
||||
// Check if gsed exists; it is able to follow symlinks,
|
||||
// which we want to do to toggle the extension
|
||||
if Shell.fileExists("\(Paths.binPath)/gsed") {
|
||||
Shell.run("\(Paths.binPath)/gsed -i --follow-symlinks 's/\(e_original)/\(e_replacement)/g' \(file)")
|
||||
} else {
|
||||
Shell.run("sed -i '' 's/\(e_original)/\(e_replacement)/g' \(file)")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Uses `grep` to determine whether a particular query string can be found in a particular file.
|
||||
*/
|
||||
public static func grepContains(file: String, query: String) -> Bool
|
||||
{
|
||||
return Shell.pipe("""
|
||||
grep -q '\(query)' \(file); [ $? -eq 0 ] && echo "YES" || echo "NO"
|
||||
""")
|
||||
.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
.contains("YES")
|
||||
}
|
||||
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
//
|
||||
// Command.swift
|
||||
// phpmon-common
|
||||
// PHP Monitor
|
||||
//
|
||||
// Copyright © 2021 Nico Verbruggen. All rights reserved.
|
||||
//
|
56
phpmon-common/Core/Constants.swift
Normal file
56
phpmon-common/Core/Constants.swift
Normal file
@ -0,0 +1,56 @@
|
||||
//
|
||||
// Constants.swift
|
||||
// PHP Monitor
|
||||
//
|
||||
// Copyright © 2021 Nico Verbruggen. All rights reserved.
|
||||
//
|
||||
|
||||
import Cocoa
|
||||
|
||||
class Constants {
|
||||
|
||||
/**
|
||||
* The latest PHP version that is considered to be stable at the time of release.
|
||||
* This version number is currently not used (only as a default fallback).
|
||||
*/
|
||||
static let LatestStablePhpVersion = "8.1"
|
||||
|
||||
/**
|
||||
The minimum version of Valet that is recommended.
|
||||
If the installed version is older, a notification will be shown
|
||||
every time the app launches (with a recommendation to upgrade).
|
||||
|
||||
The minimum requirement is currently synced to PHP 8.1 compatibility.
|
||||
See also: https://github.com/laravel/valet/releases/tag/v2.16.2
|
||||
*/
|
||||
static let MinimumRecommendedValetVersion = "2.16.2"
|
||||
|
||||
/**
|
||||
* The PHP versions supported by this application.
|
||||
* Versions that do not appear in this array are omitted from the list.
|
||||
*/
|
||||
static let SupportedPhpVersions = [
|
||||
// ====================
|
||||
// STABLE RELEASES
|
||||
// ====================
|
||||
// Versions of PHP that are stable and are supported.
|
||||
"5.6",
|
||||
"7.0",
|
||||
"7.1",
|
||||
"7.2",
|
||||
"7.3",
|
||||
"7.4",
|
||||
"8.0",
|
||||
"8.1",
|
||||
|
||||
// ====================
|
||||
// EXPERIMENTAL SUPPORT
|
||||
// ====================
|
||||
// Every release that supports the next release will always support the next
|
||||
// dev release. In this case, that means that the version below is detected.
|
||||
"8.2"
|
||||
]
|
||||
|
||||
|
||||
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
//
|
||||
// Paths.swift
|
||||
// phpmon-common
|
||||
// PHP Monitor
|
||||
//
|
||||
// Copyright © 2021 Nico Verbruggen. All rights reserved.
|
||||
//
|
@ -1,6 +1,6 @@
|
||||
//
|
||||
// Shell.swift
|
||||
// phpmon-common
|
||||
// PHP Monitor
|
||||
//
|
||||
// Copyright © 2021 Nico Verbruggen. All rights reserved.
|
||||
//
|
167
phpmon-common/PHP/ActivePhpInstallation.swift
Normal file
167
phpmon-common/PHP/ActivePhpInstallation.swift
Normal file
@ -0,0 +1,167 @@
|
||||
//
|
||||
// ActivePhpInstallation.swift
|
||||
// PHP Monitor
|
||||
//
|
||||
// Copyright © 2021 Nico Verbruggen. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/**
|
||||
An installed version of PHP, that was detected by scanning the `/opt/php@version/bin` directory.
|
||||
|
||||
When initialized, that version's .ini files are also scanned (for active or inactive extensions).
|
||||
Integrity checks can be performed to determine whether PHP-FPM is configured correctly.
|
||||
|
||||
- Note: Each installation has a separate version number.
|
||||
Using `version.short` is advisable if you want to interact with Homebrew.
|
||||
*/
|
||||
class ActivePhpInstallation {
|
||||
|
||||
var version: Version!
|
||||
var limits: Limits!
|
||||
var extensions: [PhpExtension]!
|
||||
|
||||
// MARK: - Computed
|
||||
|
||||
var formula: String {
|
||||
return (version.short == PhpSwitcher.brewPhpVersion) ? "php" : "php@\(version.short)"
|
||||
}
|
||||
|
||||
// MARK: - Initializer
|
||||
|
||||
init() {
|
||||
// Show information about the current version
|
||||
getVersion()
|
||||
|
||||
// If an error occurred, exit early
|
||||
if (version.error) {
|
||||
limits = Limits()
|
||||
extensions = []
|
||||
return
|
||||
}
|
||||
|
||||
// Load extension information
|
||||
let path = URL(fileURLWithPath: "\(Paths.etcPath)/php/\(version.short)/php.ini")
|
||||
extensions = PhpExtension.load(from: path)
|
||||
|
||||
// Get configuration values
|
||||
limits = Limits(
|
||||
memory_limit: getByteCount(key: "memory_limit"),
|
||||
upload_max_filesize: getByteCount(key: "upload_max_filesize"),
|
||||
post_max_size: getByteCount(key: "post_max_size")
|
||||
)
|
||||
|
||||
// Return a list of .ini files parsed after php.ini
|
||||
let paths = Command.execute(path: Paths.php, arguments: ["-r", "echo php_ini_scanned_files();"])
|
||||
.replacingOccurrences(of: "\n", with: "")
|
||||
.split(separator: ",")
|
||||
.map { String($0) }
|
||||
|
||||
// See if any extensions are present in said .ini files
|
||||
paths.forEach { (iniFilePath) in
|
||||
let exts = PhpExtension.load(from: URL(fileURLWithPath: iniFilePath))
|
||||
if exts.count > 0 {
|
||||
extensions.append(contentsOf: exts)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
When the app tries to retrieve the version, the installation is considered broken if the output is nothing,
|
||||
_or_ if the output contains the word "Warning" or "Error". In normal situations this should not be the case.
|
||||
*/
|
||||
private func getVersion() -> Void {
|
||||
self.version = Version()
|
||||
|
||||
let version = Command.execute(path: Paths.phpConfig, arguments: ["--version"], trimNewlines: true)
|
||||
|
||||
if (version == "" || version.contains("Warning") || version.contains("Error")) {
|
||||
self.version.short = "💩 BROKEN"
|
||||
self.version.long = ""
|
||||
self.version.error = true
|
||||
return
|
||||
}
|
||||
|
||||
// That's the long version
|
||||
self.version.long = version
|
||||
|
||||
// Next up, let's strip away the minor version number
|
||||
let segments = self.version.long.components(separatedBy: ".")
|
||||
|
||||
// Get the first two elements
|
||||
self.version.short = segments[0...1].joined(separator: ".")
|
||||
}
|
||||
|
||||
/**
|
||||
Retrieves the display value for a specific key in the `.ini` file.
|
||||
|
||||
The following values are valid:
|
||||
* -1: unlimited (show the infinity icon)
|
||||
* 10000: an integer = amount of bytes
|
||||
* 1K, 1M, 1G = shorthand for kilobytes, megabytes and gigabytes
|
||||
|
||||
If none of these notations are used, the _fallback_ value is used.
|
||||
We'll show an emoji to indicate something has gone wrong here.
|
||||
To clarify, B gets appended to valid values.
|
||||
As a result, "5M" (valid) becomes "5MB", and "5MB" (invalid) becomes ⚠️.
|
||||
|
||||
- Parameter key: The key of the `ini` value that needs to be retrieved. For example, you can use `memory_limit`.
|
||||
*/
|
||||
private func getByteCount(key: String) -> String {
|
||||
let value = Command.execute(path: Paths.php, arguments: ["-r", "echo ini_get('\(key)');"])
|
||||
|
||||
// Check if the value is unlimited
|
||||
if (value == "-1") {
|
||||
return "∞"
|
||||
}
|
||||
|
||||
// Check if the syntax is valid otherwise
|
||||
let regex = try! NSRegularExpression(pattern: #"^([0-9]*)(K|M|G|)$"#, options: [])
|
||||
let match = regex.matches(in: value, options: [], range: NSMakeRange(0, value.count)).first
|
||||
return (match == nil) ? "⚠️" : "\(value)B"
|
||||
}
|
||||
|
||||
/**
|
||||
Determine if PHP-FPM is configured correctly.
|
||||
|
||||
For PHP 5.6, we'll check if `valet.sock` is included in the main `php-fpm.conf` file, but for more recent
|
||||
versions of PHP, we can just check for the existence of the `valet-fpm.conf` file. If the check here fails,
|
||||
that means that Valet won't work properly.
|
||||
*/
|
||||
func checkPhpFpmStatus() -> Bool {
|
||||
if self.version.short == "5.6" {
|
||||
// The main PHP config file should contain `valet.sock` and then we're probably fine?
|
||||
let fileName = "\(Paths.etcPath)/php/5.6/php-fpm.conf"
|
||||
return Shell.pipe("cat \(fileName)").contains("valet.sock")
|
||||
}
|
||||
|
||||
// Make sure to check if valet-fpm.conf exists. If it does, we should be fine :)
|
||||
return Shell.fileExists("\(Paths.etcPath)/php/\(self.version.short)/php-fpm.d/valet-fpm.conf")
|
||||
}
|
||||
|
||||
// MARK: - Structs
|
||||
|
||||
/**
|
||||
Struct containing information about the version number of the current PHP installation.
|
||||
Also includes information about whether the install is considered "broken" or not.
|
||||
If an error was found in the terminal output, `error` is set to `true` and the installation
|
||||
can be considered broken. (The app will display this as well.)
|
||||
*/
|
||||
struct Version {
|
||||
var short = "???"
|
||||
var long = "???"
|
||||
var error = false
|
||||
}
|
||||
|
||||
/**
|
||||
Struct containing information about the limits of the current PHP installation.
|
||||
Includes: memory limit, max upload size and max post size.
|
||||
*/
|
||||
struct Limits {
|
||||
var memory_limit = "???"
|
||||
var upload_max_filesize = "???"
|
||||
var post_max_size = "???"
|
||||
}
|
||||
|
||||
}
|
30
phpmon-common/PHP/HomebrewPackage.swift
Normal file
30
phpmon-common/PHP/HomebrewPackage.swift
Normal file
@ -0,0 +1,30 @@
|
||||
//
|
||||
// HomebrewPackage.swift
|
||||
// PHP Monitor
|
||||
//
|
||||
// Copyright © 2021 Nico Verbruggen. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
struct HomebrewPackage: Decodable {
|
||||
|
||||
let name: String
|
||||
let full_name: String
|
||||
let aliases: [String]
|
||||
let installed: [HomebrewInstalled]
|
||||
let linked_keg: String?
|
||||
|
||||
public var version: String {
|
||||
return aliases.first!
|
||||
.replacingOccurrences(of: "php@", with: "")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
struct HomebrewInstalled: Decodable {
|
||||
let version: String
|
||||
let built_as_bottle: Bool
|
||||
let installed_as_dependency: Bool
|
||||
let installed_on_request: Bool
|
||||
}
|
108
phpmon-common/PHP/PhpExtension.swift
Normal file
108
phpmon-common/PHP/PhpExtension.swift
Normal file
@ -0,0 +1,108 @@
|
||||
//
|
||||
// PhpExtension.swift
|
||||
// PHP Monitor
|
||||
//
|
||||
// Created by Nico Verbruggen on 31/01/2021.
|
||||
// Copyright © 2021 Nico Verbruggen. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/**
|
||||
A PHP extension that was detected in the php.ini file.
|
||||
Please note that the extension may be disabled.
|
||||
|
||||
- Note: You need to know more about regular expressions to be able to deal with these NSRegularExpression
|
||||
instances. You can find more information here: https://nshipster.com/swift-regular-expressions/
|
||||
*/
|
||||
class PhpExtension {
|
||||
|
||||
/// The file where this extension was located.
|
||||
var file: String
|
||||
|
||||
/// The original string that was used to determine this extension is active.
|
||||
var line: String
|
||||
|
||||
/// The name of the extension. This is always identical to the name found in the original string. If you want to display this name, capitalize this.
|
||||
var name: String
|
||||
|
||||
/// Whether the extension has been enabled.
|
||||
var enabled: Bool
|
||||
|
||||
/// The file where this extension was located, but only the filename, not the full path to the .ini file.
|
||||
var fileNameOnly: String {
|
||||
return String(file.split(separator: "/").last ?? "php.ini")
|
||||
}
|
||||
|
||||
/**
|
||||
This regular expression will allow us to identify lines which activate an extension.
|
||||
|
||||
It will match the following items:
|
||||
|
||||
* `extension="name.so"`
|
||||
* `zend_extension="name.so"`
|
||||
* `; extension="name.so"`
|
||||
* `; zend_extension="name.so"`
|
||||
|
||||
- Note: Extensions that are disabled in a different way will not be detected. This is intentional.
|
||||
*/
|
||||
static let extensionRegex = #"^(extension|zend_extension|;(\s?)extension|;(\s?)zend_extension)(\s?)(=)(\s?)(?<name>["]?(?:\/?.\/?)+(?:\.so)"?)$"#
|
||||
|
||||
/**
|
||||
When registering an extension, we do that based on the line found inside the .ini file.
|
||||
*/
|
||||
init(_ line: String, file: String) {
|
||||
let regex = try! NSRegularExpression(pattern: Self.extensionRegex, options: [])
|
||||
let match = regex.matches(in: line, options: [], range: NSMakeRange(0, line.count)).first
|
||||
let range = Range(match!.range(withName: "name"), in: line)!
|
||||
|
||||
self.line = line
|
||||
|
||||
let fullPath = String(line[range])
|
||||
.replacingOccurrences(of: "\"", with: "") // replace excess "
|
||||
.replacingOccurrences(of: ".so", with: "") // replace excess .so
|
||||
|
||||
self.name = String(fullPath.split(separator: "/").last!) // take last segment
|
||||
|
||||
self.enabled = !line.contains(";")
|
||||
self.file = file
|
||||
}
|
||||
|
||||
/**
|
||||
This simply toggles the extension in the .ini file. You may need to restart the other services in order for this change to apply.
|
||||
*/
|
||||
func toggle() {
|
||||
let newLine = enabled
|
||||
// DISABLED: Commented out line
|
||||
? "; \(line)"
|
||||
// ENABLED: Line where the comment delimiter (;) is removed
|
||||
: line.replacingOccurrences(of: "; ", with: "")
|
||||
|
||||
Actions.sed(file: file, original: line, replacement: newLine)
|
||||
|
||||
enabled.toggle()
|
||||
}
|
||||
|
||||
// MARK: - Static Methods
|
||||
|
||||
/**
|
||||
This method will attempt to identify all extensions in the .ini file at a certain URL.
|
||||
*/
|
||||
static func load(from path: URL) -> [PhpExtension] {
|
||||
let file = try? String(contentsOf: path, encoding: .utf8)
|
||||
|
||||
if (file == nil) {
|
||||
print("There was an issue reading the file. Assuming no extensions were found.")
|
||||
return []
|
||||
}
|
||||
|
||||
return file!.components(separatedBy: "\n")
|
||||
.filter {
|
||||
return $0.range(of: Self.extensionRegex, options: .regularExpression) != nil
|
||||
}
|
||||
.map {
|
||||
return PhpExtension($0, file: path.path)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
30
phpmon-common/PHP/PhpInstallation.swift
Normal file
30
phpmon-common/PHP/PhpInstallation.swift
Normal file
@ -0,0 +1,30 @@
|
||||
//
|
||||
// PhpInstallation.swift
|
||||
// PHP Monitor
|
||||
//
|
||||
// Created by Nico Verbruggen on 28/11/2021.
|
||||
// Copyright © 2021 Nico Verbruggen. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
class PhpInstallation {
|
||||
|
||||
var longVersion: String
|
||||
|
||||
/**
|
||||
In order to determine details about a PHP installation, we’ll simply run `php-config --version`
|
||||
in the relevant directory.
|
||||
*/
|
||||
init(_ version: String) {
|
||||
let phpConfigExecutablePath = "\(Paths.optPath)/php@\(version)/bin/php-config"
|
||||
self.longVersion = version
|
||||
if Shell.fileExists(phpConfigExecutablePath) {
|
||||
self.longVersion = Command.execute(
|
||||
path: phpConfigExecutablePath,
|
||||
arguments: ["--version"]
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
146
phpmon-common/PHP/PhpSwitcher.swift
Normal file
146
phpmon-common/PHP/PhpSwitcher.swift
Normal file
@ -0,0 +1,146 @@
|
||||
//
|
||||
// PhpSwitcher.swift
|
||||
// PHP Monitor
|
||||
//
|
||||
// Created by Nico Verbruggen on 21/12/2021.
|
||||
// Copyright © 2021 Nico Verbruggen. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
protocol PhpSwitcherDelegate: AnyObject {
|
||||
func switcherDidStartSwitching()
|
||||
func switcherDidCompleteSwitch()
|
||||
}
|
||||
|
||||
class PhpSwitcher {
|
||||
|
||||
// MARK: - Initializer
|
||||
|
||||
init() {
|
||||
self.currentInstall = ActivePhpInstallation()
|
||||
|
||||
let brewPhpAlias = Shell.pipe("\(Paths.brew) info php --json");
|
||||
|
||||
self.homebrewPackage = try! JSONDecoder().decode(
|
||||
[HomebrewPackage].self,
|
||||
from: brewPhpAlias.data(using: .utf8)!
|
||||
).first!
|
||||
|
||||
print("When on your system, the `php` formula means version \(homebrewPackage.version)!")
|
||||
}
|
||||
|
||||
// MARK: - Properties
|
||||
|
||||
/** The delegate that is informed of updates. */
|
||||
weak var delegate: PhpSwitcherDelegate?
|
||||
|
||||
/** The static app instance. Accessible at any time. */
|
||||
static let shared = PhpSwitcher()
|
||||
|
||||
/** Whether the switcher is busy performing any actions. */
|
||||
var isBusy: Bool = false
|
||||
|
||||
/** All available versions of PHP. */
|
||||
var availablePhpVersions: [String] = []
|
||||
|
||||
/** Cached information about the PHP installations. */
|
||||
var cachedPhpInstallations: [String: PhpInstallation] = [:]
|
||||
|
||||
/** Information about the currently linked PHP installation. */
|
||||
var currentInstall: ActivePhpInstallation
|
||||
|
||||
/**
|
||||
The version that the `php` formula via Brew is aliased to on the current system.
|
||||
|
||||
If you're up to date, `php` will be aliased to the latest version,
|
||||
but that might not be the case since not everyone keeps their
|
||||
software up-to-date.
|
||||
|
||||
As such, we take that information from Homebrew.
|
||||
*/
|
||||
static var brewPhpVersion: String {
|
||||
return Self.shared.homebrewPackage.version
|
||||
}
|
||||
|
||||
/**
|
||||
The currently linked and active PHP installation.
|
||||
*/
|
||||
static var phpInstall: ActivePhpInstallation {
|
||||
return Self.shared.currentInstall
|
||||
}
|
||||
|
||||
/**
|
||||
Information we were able to discern from the Homebrew info command.
|
||||
*/
|
||||
var homebrewPackage: HomebrewPackage! = nil
|
||||
|
||||
// MARK: - Methods
|
||||
|
||||
public static func detectPhpVersions() -> Void {
|
||||
_ = Self.shared.detectPhpVersions()
|
||||
}
|
||||
|
||||
/**
|
||||
Detects which versions of PHP are installed.
|
||||
*/
|
||||
public func detectPhpVersions() -> [String]
|
||||
{
|
||||
let files = Shell.pipe("ls \(Paths.optPath) | grep php@")
|
||||
|
||||
var versionsOnly = extractPhpVersions(from: files.components(separatedBy: "\n"))
|
||||
|
||||
// Make sure the aliased version is detected
|
||||
// The user may have `php` installed, but not e.g. `php@8.0`
|
||||
// We should also detect that as a version that is installed
|
||||
let phpAlias = homebrewPackage.version
|
||||
|
||||
// Avoid inserting a duplicate
|
||||
if (!versionsOnly.contains(phpAlias) && Shell.fileExists("\(Paths.optPath)/php/bin/php")) {
|
||||
versionsOnly.append(phpAlias)
|
||||
}
|
||||
|
||||
print("The PHP versions that were detected are: \(versionsOnly)")
|
||||
|
||||
availablePhpVersions = versionsOnly
|
||||
|
||||
var mappedVersions: [String: PhpInstallation] = [:]
|
||||
|
||||
availablePhpVersions.forEach { version in
|
||||
mappedVersions[version] = PhpInstallation(version)
|
||||
}
|
||||
|
||||
cachedPhpInstallations = mappedVersions
|
||||
|
||||
return versionsOnly
|
||||
}
|
||||
|
||||
/**
|
||||
Extracts valid PHP versions from an array of strings.
|
||||
This array of strings is usually retrieved from `grep`.
|
||||
*/
|
||||
public func extractPhpVersions(
|
||||
from versions: [String],
|
||||
checkBinaries: Bool = true
|
||||
) -> [String] {
|
||||
var output : [String] = []
|
||||
|
||||
versions.filter { (version) -> Bool in
|
||||
// Omit everything that doesn't start with php@
|
||||
// (e.g. something-php@8.0 won't be detected)
|
||||
return version.starts(with: "php@")
|
||||
}.forEach { (string) in
|
||||
let version = string.components(separatedBy: "php@")[1]
|
||||
// Only append the version if it doesn't already exist (avoid dupes),
|
||||
// is supported and where the binary exists (avoids broken installs)
|
||||
if !output.contains(version)
|
||||
&& Constants.SupportedPhpVersions.contains(version)
|
||||
&& (checkBinaries ? Shell.fileExists("\(Paths.optPath)/php@\(version)/bin/php") : true)
|
||||
{
|
||||
output.append(version)
|
||||
}
|
||||
}
|
||||
|
||||
return output
|
||||
}
|
||||
}
|
@ -1,60 +0,0 @@
|
||||
//
|
||||
// PhpSwitcher.swift
|
||||
// PHP Monitor
|
||||
//
|
||||
// Created by Nico Verbruggen on 21/12/2021.
|
||||
// Copyright © 2021 Nico Verbruggen. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
protocol PhpSwitcherDelegate: AnyObject {
|
||||
func switcherDidStartSwitching()
|
||||
func switcherDidCompleteSwitch()
|
||||
}
|
||||
|
||||
class PhpSwitcher {
|
||||
|
||||
init() {
|
||||
self.currentInstall = ActivePhpInstallation()
|
||||
}
|
||||
|
||||
/** The delegate that is informed of updates. */
|
||||
weak var delegate: PhpSwitcherDelegate?
|
||||
|
||||
/** The static app instance. Accessible at any time. */
|
||||
static let shared = PhpSwitcher()
|
||||
|
||||
/** Whether the switcher is busy performing any actions. */
|
||||
var isBusy: Bool = false
|
||||
|
||||
/** All available versions of PHP. */
|
||||
var availablePhpVersions: [String] = []
|
||||
|
||||
/** Cached information about the PHP installations. */
|
||||
var cachedPhpInstallations: [String: PhpInstallation] = [:]
|
||||
|
||||
/** Static accessor for `PhpSwitcher.shared.currentInstall`. */
|
||||
static var phpInstall: ActivePhpInstallation {
|
||||
return Self.shared.currentInstall
|
||||
}
|
||||
|
||||
/** Information about the currently linked PHP installation. */
|
||||
var currentInstall: ActivePhpInstallation
|
||||
|
||||
/**
|
||||
The version that the `php` formula via Brew is aliased to on the current system.
|
||||
|
||||
If you're up to date, `php` will be aliased to the latest version,
|
||||
but that might not be the case.
|
||||
*/
|
||||
var brewPhpVersion: String {
|
||||
return homebrewPackage.version
|
||||
}
|
||||
|
||||
/**
|
||||
Information we were able to discern from the Homebrew info command.
|
||||
*/
|
||||
var homebrewPackage: HomebrewPackage! = nil
|
||||
|
||||
}
|
Reference in New Issue
Block a user