1
0
mirror of https://github.com/nicoverbruggen/phpmon.git synced 2025-08-07 20:10:08 +02:00

Compare commits

..

19 Commits

Author SHA1 Message Date
3e319cd50f 🚀 Version 7.1 2024-11-26 15:24:18 +01:00
595dc8c028 Add info about test failures 2024-11-26 15:22:03 +01:00
f7b1679e97 🐛 Serial dispatch queue for test FS 2024-11-26 14:17:06 +01:00
9f1761d68e Checked and updated tests 2024-11-26 13:44:08 +01:00
871480d70c 📝 Updated README.md, SECURITY.md 2024-11-26 12:56:38 +01:00
2b1c1c12f8 Add info button for PHP upgrades 2024-11-25 16:43:19 +01:00
a22346ed35 🐛 Fix issue with formulae upgrades for tap 2024-11-25 14:04:17 +01:00
e3fa34d4f9 🔨 Adjust URL for unavailable PHP (wiki) 2024-11-25 13:40:02 +01:00
3d225ea79f ✍️ Clarify text about upgrading PHP (EN only) 2024-11-22 19:09:10 +01:00
d2cd387c18 Add button that redirects to wiki 2024-11-22 18:23:28 +01:00
48bb782e33 🚧 WIP: Changes related to unavailable formulae 2024-11-22 17:42:28 +01:00
9710ffa8da 🚧 WIP: Handle temporarily unavailable formulae 2024-11-22 13:26:09 +01:00
46408f5ee5 Indicate DEV builds in PHP Version Manager 2024-11-15 16:43:59 +01:00
2c39f1db8b 🔧 Upgrade checks for Xcode 16.1 2024-11-15 16:10:25 +01:00
f20286cbd9 Notify user if startup takes too long (> 30s) 2024-11-15 16:03:44 +01:00
f1fe42e563 Updated constants for PHP 8.4 & 8.5 support
Thankfully, these changes are simple. Before releasing, I will be
testing the new build, though.

Here's what constants I changed, and why:

- Homebrew PHP formulae are now consistently sourced from the
  `shivammathur/php` tap. This should make the transition to new PHP
  releases a little bit easier, but I need to verify this works without
  issues before publishing this update.

- Bumped the PHP formulae cutoff date to Nov 30, 2025.
  At this point, PHP 8.5 should be released.

- Added support for pre-release (daily) versions of PHP 8.5.
2024-11-15 15:22:53 +01:00
9778fd5c7b 🚀 Version 7.0.6 2024-10-31 22:43:03 +01:00
dba2ce5bf3 🚀 Version 7.0.5 2024-10-31 22:15:11 +01:00
4644c1ada4 👌 Move cut-off date to PHP 8.4 release day 2024-10-31 22:14:53 +01:00
24 changed files with 315 additions and 131 deletions

View File

@ -51,6 +51,12 @@ Once you have downloaded this repository, open `PHP Monitor.xcodeproj`, and you
If you'd like to create a production build, choose "Any Mac" as the target and select Product > Archive.
## ✅ Testing
In order to properly test everything, you will want to use the _PHP Monitor DEV_ target. There are unit and UI tests both.
You may sporadically see failures in UI tests due to the following error: `Invalid parameter not satisfying: point.x != INFINITY && point.y != INFINITY`. This seems to be an issue with Xcode that Apple may need to resolve?
## 🚀 Release procedure
1. Merge into `main`

View File

@ -2348,7 +2348,7 @@
attributes = {
BuildIndependentTargetsInParallel = YES;
LastSwiftUpdateCheck = 1420;
LastUpgradeCheck = 1530;
LastUpgradeCheck = 1610;
ORGANIZATIONNAME = "Nico Verbruggen";
TargetAttributes = {
C406A5EF298AD2CE00B5B85A = {
@ -3688,7 +3688,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 1516;
CURRENT_PROJECT_VERSION = 1525;
DEAD_CODE_STRIPPING = YES;
DEBUG = YES;
DEVELOPMENT_TEAM = 8M54J5J787;
@ -3719,7 +3719,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 1516;
CURRENT_PROJECT_VERSION = 1525;
DEAD_CODE_STRIPPING = YES;
DEBUG = NO;
DEVELOPMENT_TEAM = 8M54J5J787;
@ -3960,7 +3960,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 1516;
CURRENT_PROJECT_VERSION = 1525;
DEAD_CODE_STRIPPING = YES;
DEBUG = NO;
DEVELOPMENT_TEAM = 8M54J5J787;
@ -4077,7 +4077,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 1516;
CURRENT_PROJECT_VERSION = 1525;
DEAD_CODE_STRIPPING = YES;
DEBUG = YES;
DEVELOPMENT_TEAM = 8M54J5J787;
@ -4194,7 +4194,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 1516;
CURRENT_PROJECT_VERSION = 1525;
DEAD_CODE_STRIPPING = YES;
DEBUG = YES;
DEVELOPMENT_TEAM = 8M54J5J787;
@ -4377,7 +4377,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 1516;
CURRENT_PROJECT_VERSION = 1525;
DEAD_CODE_STRIPPING = YES;
DEBUG = NO;
DEVELOPMENT_TEAM = 8M54J5J787;

View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1540"
LastUpgradeVersion = "1610"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"

View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1540"
LastUpgradeVersion = "1610"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"

View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1540"
LastUpgradeVersion = "1610"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"

View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1540"
LastUpgradeVersion = "1610"
version = "1.7">
<BuildAction
parallelizeBuildables = "YES"

View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1540"
LastUpgradeVersion = "1610"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"

View File

@ -22,7 +22,7 @@ You can also add new domains as links, isolate sites, manage various services, a
PHP Monitor is a universal application that runs natively on Apple Silicon **and** Intel-based Macs.
* Your user account can administer your computer (required for some functionality, e.g. certificate generation)
* macOS 12.4 or later (Monterey, Ventura and Sonoma are supported)
* macOS 12.4 or later
* Homebrew is installed in the default location (`/usr/local/homebrew` or `/opt/homebrew`)
* Homebrew `php` formula is installed
* Optional but recommended: Laravel Valet

View File

@ -6,7 +6,7 @@ Generally speaking, only the latest version of **PHP Monitor** is supported, exc
| Version | Apple Silicon | Supported | Supported macOS | Minimum Deployment | Detected PHP Versions | Recommended Valet Version |
| ------- | ------------- | ------------------ | ----- | ----- | ----- | ----
| 7.0 | ✅ Universal binary | ✅ Yes | Monterey (12.4+)<br/>Ventura (13.0+)<br/>Sonoma (14.0) | macOS 12.4+ | PHP 5.6—PHP 8.2 (w/ Valet 2.x)<br/>PHP 7.0—PHP 8.4 (w/ Valet 3.x)<br/>PHP 7.1-PHP 8.4 (w/ Valet 4.x)| 3.0 or higher recommended<br/> 2.16.2 minimum |
| 7.1 | ✅ Universal binary | ✅ Yes | Monterey (12.4+)<br/>Ventura (13.0+)<br/>Sonoma (14.0+)<br/>Sequoia (15.0+) | macOS 12.4+ | PHP 5.6—PHP 8.2 (w/ Valet 2.x)<br/>PHP 7.0—PHP 8.4 (w/ Valet 3.x)<br/>PHP 7.1-PHP 8.5 (w/ Valet 4.x)| 3.0 or higher recommended<br/> 2.16.2 minimum |
## Legacy versions
@ -14,6 +14,7 @@ These versions of PHP Monitor are no longer supported, but if youre using an
| Version | Apple Silicon | Supported | Supported macOS | Minimum Deployment | Detected PHP Versions | Minimum Required Valet Version |
| ------- | ------------- | ------------------ | ----- | ----- | ----- | ----
| 7.0 | ✅ Universal binary | ❌ | Monterey (12.4+)<br/>Ventura (13.0+)<br/>Sonoma (14.0) | macOS 12.4+ | PHP 5.6—PHP 8.2 (w/ Valet 2.x)<br/>PHP 7.0—PHP 8.4 (w/ Valet 3.x)<br/>PHP 7.1-PHP 8.4 (w/ Valet 4.x)| 3.0 or higher recommended<br/> 2.16.2 minimum |
| 6.2 | ✅ Universal binary | ❌ | Monterey (12.4+)<br/>Ventura (13.0+)<br/>Sonoma (14.0) | macOS 12.4+ | PHP 5.6—PHP 8.2 (w/ Valet 2.x)<br/>PHP 7.0—PHP 8.4 (w/ Valet 3.x)<br/>PHP 7.1-PHP 8.4 (w/ Valet 4.x)| 3.0 or higher recommended<br/> 2.16.2 minimum |
| 6.1 | ✅ Universal binary | ❌ | Monterey (12.4+)<br/>Ventura (13.0+)<br/>Sonoma (14.0) | macOS 12.4+ | PHP 5.6—PHP 8.2 (w/ Valet 2.x)<br/>PHP 7.0—PHP 8.4 (w/ Valet 3.x)<br/>PHP 7.1-PHP 8.4 (w/ Valet 4.x)| 3.0 or higher recommended<br/> 2.16.2 minimum |
| 6.0 | ✅ Universal binary | ❌ | Monterey (12.4+)<br/>Ventura (13.0+) | macOS 12.4+ | PHP 5.6—PHP 8.2 (w/ Valet 2.x)<br/>PHP 7.0—PHP 8.2 (w/ Valet 3.x)<br/>PHP 7.1-PHP 8.2 (w/ Valet 4.x) | 3.0 or higher recommended<br/> 2.16.2 minimum |

View File

@ -24,13 +24,13 @@ struct Constants {
This hardcoded list will expire and will need to be modified when
the cutoff date occurs, which is when the `php` formula will
become PHP 8.4, and a new build will need to be made.
become PHP 8.5, and a new build will need to be made.
If users launch an older version of the app, then a warning
will be displayed to let them know that certain operations
will not work correctly and that they need to update their app.
*/
static let PhpFormulaeCutoffDate = "2024-11-30" // YYYY-MM-DD
static let PhpFormulaeCutoffDate = "2025-11-30" // YYYY-MM-DD
/**
* The PHP versions that are considered pre-release versions.
@ -39,8 +39,8 @@ struct Constants {
*/
static var ExperimentalPhpVersions: Set<String> {
let releaseDates = [
// "8.5": Date.fromString("2025-11-01"), // PLACEHOLDER DATE
"8.4": Date.fromString("2024-11-22"),
"8.5": Date.fromString(Self.PhpFormulaeCutoffDate),
"8.4": Date.fromString("2024-11-22")
]
return Set(releaseDates
@ -73,7 +73,8 @@ struct Constants {
static let DetectedPhpVersions: Set = [
"5.6",
"7.0", "7.1", "7.2", "7.3", "7.4",
"8.0", "8.1", "8.2", "8.3", "8.4"
"8.0", "8.1", "8.2", "8.3", "8.4",
"8.5" // DEV
]
/**
@ -94,7 +95,8 @@ struct Constants {
4: // Valet v4 dropped support for v7.0
[
"7.1", "7.2", "7.3", "7.4",
"8.0", "8.1", "8.2", "8.3", "8.4"
"8.0", "8.1", "8.2", "8.3", "8.4",
"8.5" // DEV
]
]
@ -110,6 +112,14 @@ struct Constants {
string: "https://phpmon.app/faq"
)!
static let WikiPhpUnavailable = URL(
string: "https://phpmon.app/php-unavailable"
)!
static let WikiPhpUpgrade = URL(
string: "https://phpmon.app/php-upgrade"
)!
static let DonationPayment = URL(
string: "https://phpmon.app/sponsor/now"
)!

View File

@ -14,6 +14,8 @@ class PhpInstallation {
var iniFiles: [PhpConfigurationFile] = []
var isPreRelease: Bool = false
var isMissingBinary: Bool = false
var isHealthy: Bool = true
@ -59,6 +61,10 @@ class PhpInstallation {
trimNewlines: false
).trimmingCharacters(in: .whitespacesAndNewlines)
if longVersionString.contains("-dev") {
isPreRelease = true
}
// The parser should always work, or the string has to be very unusual.
// If so, the app SHOULD crash, so that the users report what's up.
versionNumber = try! VersionNumber.parse(longVersionString)

View File

@ -18,11 +18,11 @@ class TestableFileSystem: FileSystemProtocol {
self.files = files
// Ensure that each of the ~ characters are replaced with the home directory path
for key in self.files.keys where key.contains("~") {
self.files.renameKey(
fromKey: key,
toKey: key.replacingOccurrences(of: "~", with: self.homeDirectory)
)
accessQueue.sync {
for (key, value) in files {
let adjustedKey = key.contains("~") ? key.replacingOccurrences(of: "~", with: self.homeDirectory) : key
self.files[adjustedKey] = value
}
}
// Ensure that intermediate directories are created
@ -46,38 +46,49 @@ class TestableFileSystem: FileSystemProtocol {
*/
private(set) var homeDirectory = "/Users/fake"
/**
Serial dispatch queue for ensuring thread-safe access to the `files` dictionary.
*/
private let accessQueue = DispatchQueue(label: "com.testablefilesystem.accessQueue")
// MARK: - Basics
func createDirectory(_ path: String, withIntermediateDirectories: Bool) throws {
let path = path.replacingTildeWithHomeDirectory
if files[path] != nil {
throw TestableFileSystemError.alreadyExists
try accessQueue.sync {
if files[path] != nil {
throw TestableFileSystemError.alreadyExists
}
self.createIntermediateDirectories(path)
self.files[path] = .fake(.directory)
}
self.createIntermediateDirectories(path)
self.files[path] = .fake(.directory)
}
func writeAtomicallyToFile(_ path: String, content: String) throws {
let path = path.replacingTildeWithHomeDirectory
if files[path] != nil {
throw TestableFileSystemError.alreadyExists
}
try accessQueue.sync {
if files[path] != nil {
throw TestableFileSystemError.alreadyExists
}
self.files[path] = .fake(.text, content)
self.files[path] = .fake(.text, content)
}
}
func getStringFromFile(_ path: String) throws -> String {
let path = path.replacingTildeWithHomeDirectory
guard let file = files[path] else {
throw TestableFileSystemError.fileMissing
}
return try accessQueue.sync {
guard let file = files[path] else {
throw TestableFileSystemError.fileMissing
}
return file.content ?? ""
return file.content ?? ""
}
}
func getShallowContentsOfDirectory(_ path: String) throws -> [String] {
@ -88,32 +99,36 @@ class TestableFileSystem: FileSystemProtocol {
seek = "\(seek)/"
}
return self.files.keys
.filter { $0.hasPrefix(seek) }
.map { $0.replacingOccurrences(of: seek, with: "") }
.filter { !$0.contains("/") }
return accessQueue.sync {
self.files.keys
.filter { $0.hasPrefix(seek) }
.map { $0.replacingOccurrences(of: seek, with: "") }
.filter { !$0.contains("/") }
}
}
func getDestinationOfSymlink(_ path: String) throws -> String {
let path = path.replacingTildeWithHomeDirectory
guard let file = files[path] else {
throw TestableFileSystemError.fileMissing
}
return try accessQueue.sync {
guard let file = files[path] else {
throw TestableFileSystemError.fileMissing
}
if file.type != .symlink {
throw TestableFileSystemError.notSymlink
}
if file.type != .symlink {
throw TestableFileSystemError.notSymlink
}
guard let pathToSymlink = file.content else {
throw TestableFileSystemError.invalidSymlink
}
guard let pathToSymlink = file.content else {
throw TestableFileSystemError.invalidSymlink
}
if !files.keys.contains(pathToSymlink) {
throw TestableFileSystemError.invalidSymlink
}
if !files.keys.contains(pathToSymlink) {
throw TestableFileSystemError.invalidSymlink
}
return pathToSymlink
return pathToSymlink
}
}
// MARK: - Move & Delete Files
@ -122,27 +137,31 @@ class TestableFileSystem: FileSystemProtocol {
let path = path.replacingTildeWithHomeDirectory
let newPath = newPath.replacingTildeWithHomeDirectory
self.files.keys.forEach { key in
if key.hasPrefix(path) {
self.files.renameKey(
fromKey: key,
toKey: key.replacingOccurrences(of: path, with: newPath)
)
accessQueue.sync {
self.files.keys.forEach { key in
if key.hasPrefix(path) {
self.files.renameKey(
fromKey: key,
toKey: key.replacingOccurrences(of: path, with: newPath)
)
}
}
}
self.files.renameKey(fromKey: path, toKey: newPath)
self.files.renameKey(fromKey: path, toKey: newPath)
}
}
func remove(_ path: String) throws {
// Remove recursively
self.files.keys.forEach { key in
if key.hasPrefix(path) {
self.files.removeValue(forKey: key)
accessQueue.sync {
// Remove recursively
self.files.keys.forEach { key in
if key.hasPrefix(path) {
self.files.removeValue(forKey: key)
}
}
}
self.files.removeValue(forKey: path)
self.files.removeValue(forKey: path)
}
}
// MARK: Attributes
@ -150,11 +169,13 @@ class TestableFileSystem: FileSystemProtocol {
func makeExecutable(_ path: String) throws {
let path = path.replacingTildeWithHomeDirectory
guard let file = files[path] else {
throw TestableFileSystemError.fileMissing
}
try accessQueue.sync {
guard let file = files[path] else {
throw TestableFileSystemError.fileMissing
}
file.type = .binary
file.type = .binary
}
}
// MARK: - Checks
@ -162,93 +183,107 @@ class TestableFileSystem: FileSystemProtocol {
func isExecutableFile(_ path: String) -> Bool {
let path = path.replacingTildeWithHomeDirectory
guard let file = files[path.replacingTildeWithHomeDirectory] else {
return false
}
return accessQueue.sync {
guard let file = files[path.replacingTildeWithHomeDirectory] else {
return false
}
return file.type == .binary
return file.type == .binary
}
}
func isWriteableFile(_ path: String) -> Bool {
let path = path.replacingTildeWithHomeDirectory
guard let file = files[path.replacingTildeWithHomeDirectory] else {
return false
}
return accessQueue.sync {
guard let file = files[path.replacingTildeWithHomeDirectory] else {
return false
}
return !file.readOnly
return !file.readOnly
}
}
func anyExists(_ path: String) -> Bool {
let path = path.replacingTildeWithHomeDirectory
return files.keys.contains(path)
return accessQueue.sync {
files.keys.contains(path)
}
}
func fileExists(_ path: String) -> Bool {
let path = path.replacingTildeWithHomeDirectory
guard let file = files[path] else {
return false
}
return accessQueue.sync {
guard let file = files[path] else {
return false
}
return [.binary, .symlink, .text].contains(file.type)
return [.binary, .symlink, .text].contains(file.type)
}
}
func directoryExists(_ path: String) -> Bool {
let path = path.replacingTildeWithHomeDirectory
guard let file = files[path] else {
return false
}
return accessQueue.sync {
guard let file = files[path] else {
return false
}
return [.directory].contains(file.type)
return [.directory].contains(file.type)
}
}
func isSymlink(_ path: String) -> Bool {
let path = path.replacingTildeWithHomeDirectory
guard let file = files[path] else {
return false
}
return accessQueue.sync {
guard let file = files[path] else {
return false
}
return file.type == .symlink
return file.type == .symlink
}
}
func isDirectory(_ path: String) -> Bool {
let path = path.replacingTildeWithHomeDirectory
guard let file = files[path] else {
return false
}
return accessQueue.sync {
guard let file = files[path] else {
return false
}
return file.type == .directory
return file.type == .directory
}
}
public func printContents() {
for key in self.files.keys.sorted() {
print("\(key) -> \(self.files[key]!.type)")
accessQueue.sync {
for key in self.files.keys.sorted() {
print("\(key) -> \(self.files[key]!.type)")
}
}
}
private func createIntermediateDirectories(_ path: String) {
let path = path.replacingTildeWithHomeDirectory
let items = path.components(separatedBy: "/")
var preceding = ""
var directoriesToCreate: [String] = []
for item in items {
let key = preceding == "/"
? "/\(item)"
: "\(preceding)/\(item)"
if !self.files.keys.contains(key) {
self.files[key] = .fake(.directory)
}
let key = preceding == "/" ? "/\(item)" : "\(preceding)/\(item)"
directoriesToCreate.append(key)
preceding = key
}
for key in directoriesToCreate where !self.files.keys.contains(key) {
self.files[key] = .fake(.directory)
}
}
}

View File

@ -11,6 +11,20 @@ import NVAlert
class Startup {
@MainActor static var startupTimer: Timer?
@MainActor func startTimeoutTimer() {
Self.startupTimer = Timer.scheduledTimer(
timeInterval: 30.0, target: self, selector: #selector(startupTimeout),
userInfo: nil, repeats: false
)
}
@MainActor static func invalidateTimeoutTimer() {
Self.startupTimer?.invalidate()
Self.startupTimer = nil
}
/**
Checks the user's environment and checks if PHP Monitor can be used properly.
This checks if PHP is installed, Valet is running, the appropriate permissions are set, and more.
@ -22,6 +36,11 @@ class Startup {
// Do the important system setup checks
Log.info("The user is running PHP Monitor with the architecture: \(App.architecture)")
// Set up a "background" timer on the main thread
Task { @MainActor in
startTimeoutTimer()
}
for group in self.groups {
if group.condition() {
Log.info("Now running \(group.checks.count) \(group.name) checks!")
@ -45,10 +64,34 @@ class Startup {
// If we get here, nothing has gone wrong. That's what we want!
initializeSwitcher()
Log.info("PHP Monitor has determined the application has successfully passed all checks.")
Log.separator(as: .info)
return true
}
/**
Displays an alert for when the application startup process takes too long.
*/
@MainActor @objc func startupTimeout() {
NVAlert()
.withInformation(
title: "startup.timeout.title".localized,
subtitle: "startup.timeout.subtitle".localized,
description: "startup.timeout.description".localized
)
.withPrimary(text: "alert.cannot_start.close".localized, action: { vc in
vc.close(with: .alertFirstButtonReturn)
exit(1)
})
.withSecondary(text: "startup.timeout.ignore".localized, action: { vc in
vc.close(with: .alertSecondButtonReturn)
})
.withTertiary(text: "", action: { _ in
NSWorkspace.shared.open(URL(string: "https://github.com/nicoverbruggen/phpmon/issues/294")!)
})
.show()
}
/**
Displays an alert for a particular check. There are two types of alerts:
- ones that require an app restart, which prompt the user to exit the app

View File

@ -35,10 +35,11 @@ class Brew {
/// Each formula for each PHP version that can be installed.
public static let phpVersionFormulae = [
"8.5": "shivammathur/php/php@8.5",
"8.4": "shivammathur/php/php@8.4",
"8.3": "php@8.3",
"8.2": "php@8.2",
"8.1": "php@8.1",
"8.3": "shivammathur/php/php@8.3",
"8.2": "shivammathur/php/php@8.2",
"8.1": "shivammathur/php/php@8.1",
"8.0": "shivammathur/php/php@8.0",
"7.4": "shivammathur/php/php@7.4",
"7.3": "shivammathur/php/php@7.3",

View File

@ -21,11 +21,12 @@ struct BrewPhpFormula: Equatable {
/// The upgrade that is currently available, if it exists.
let upgradeVersion: String?
// TODO: A rebuild attribute could be checked, to check if a Tap update exists for a pre-release version
/// Whether this formula is a stable version of PHP.
let prerelease: Bool
/// Whether this formula's associated Homebrew file exists.
var hasFormulaFile: Bool = false
/// Whether the formula is currently installed.
var isInstalled: Bool {
return installedVersion != nil
@ -43,6 +44,7 @@ struct BrewPhpFormula: Equatable {
self.installedVersion = installedVersion
self.upgradeVersion = upgradeVersion
self.prerelease = prerelease
self.hasFormulaFile = checkFormulaFile()
}
/// Whether the formula can be upgraded.
@ -93,6 +95,21 @@ struct BrewPhpFormula: Equatable {
return isHealthy() ?? true
}
/**
Verify whether the formula file exists (sourced via `shivammathur/homebrew-php`).
If it does not exist, the formula cannot be installed.
*/
private func checkFormulaFile() -> Bool {
guard let version = shortVersion else {
return false
}
return FileSystem.fileExists(
"\(Paths.tapPath)/shivammathur/homebrew-php/Formula/php@\(version).rb"
.replacingOccurrences(of: "php@" + PhpEnvironments.brewPhpAlias, with: "php")
)
}
/**
* Determines if this PHP installation is healthy.
* Uses the cached installation health check as basis.

View File

@ -39,19 +39,25 @@ class BrewPhpFormulaeHandler: HandlesBrewPhpFormulae {
OutdatedFormulae.self,
from: rawJsonText
).formulae.filter({ formula in
formula.name.starts(with: "php")
formula.name.starts(with: "shivammathur/php/php") || formula.name.starts(with: "php")
})
}
return Brew.phpVersionFormulae.map { (version, formula) in
var fullVersion: String?
var upgradeVersion: String?
var isPrerelease: Bool = Constants.ExperimentalPhpVersions.contains(version)
if let install = PhpEnvironments.shared.cachedPhpInstallations[version] {
fullVersion = install.versionNumber.text
fullVersion = install.isPreRelease ? "\(fullVersion!)-dev" : fullVersion
upgradeVersion = outdated?.first(where: { formula in
return formula.name == install.formulaName
return formula.name.replacingOccurrences(of: "shivammathur/php/", with: "")
== install.formulaName.replacingOccurrences(of: "shivammathur/php/", with: "")
})?.current_version
isPrerelease = install.isPreRelease
}
return BrewPhpFormula(
@ -59,7 +65,7 @@ class BrewPhpFormulaeHandler: HandlesBrewPhpFormulae {
displayName: "PHP \(version)",
installedVersion: fullVersion,
upgradeVersion: upgradeVersion,
prerelease: Constants.ExperimentalPhpVersions.contains(version)
prerelease: isPrerelease
)
}.sorted { $0.displayName > $1.displayName }
}

View File

@ -115,6 +115,9 @@ extension MainMenu {
// Finally!
Log.info("PHP Monitor is ready to serve!")
// Avoid showing the "startup timeout" alert
Startup.invalidateTimeoutTimer()
// Check if we upgraded from a previous version
AppUpdater.checkIfUpdateWasPerformed()
}

View File

@ -10,6 +10,11 @@ import Foundation
import SwiftUI
struct HelpButton: View {
@State var frameSize: CGFloat = 14
@State var textSize: CGFloat = 12
@State var shadowOpacity: CGFloat = 0.3
@State var shadowRadius: CGFloat = 1
var action: () -> Void
var body: some View {
@ -18,9 +23,10 @@ struct HelpButton: View {
Circle()
.strokeBorder(Color(NSColor.separatorColor), lineWidth: 0.5)
.background(Circle().foregroundColor(Color(NSColor.controlColor)).opacity(0.7))
.shadow(color: Color(NSColor.separatorColor).opacity(0.3), radius: 1)
.frame(width: 14, height: 14)
Text("?").font(.system(size: 12, weight: .medium))
.shadow(color: Color(NSColor.separatorColor)
.opacity(shadowOpacity), radius: shadowRadius)
.frame(width: frameSize, height: frameSize)
Text("?").font(.system(size: textSize, weight: .medium))
.foregroundColor(Color(NSColor.labelColor))
}
})

View File

@ -112,5 +112,4 @@ struct ByteLimitView: View {
#Preview("Config Manager") {
ConfigManagerView()
.frame(width: 600, height: .infinity)
.previewDisplayName("Config Manager")
}

View File

@ -9,6 +9,7 @@
import Foundation
import SwiftUI
// swiftlint:disable type_body_length
struct PhpVersionManagerView: View {
@ObservedObject var formulae: BrewFormulaeObservable
@ObservedObject var status: BusyStatus
@ -192,6 +193,7 @@ struct PhpVersionManagerView: View {
.buttonStyle(.automatic)
.controlSize(.large)
}
.accessibilityIdentifier("RefreshButton")
.focusable(false)
.disabled(self.status.busy)
@ -231,14 +233,18 @@ struct PhpVersionManagerView: View {
}
}
if formula.isInstalled {
if formula.hasUpgradedFormulaAlias {
HelpButton(frameSize: 18, textSize: 14, shadowOpacity: 1, shadowRadius: 2, action: {
NSWorkspace.shared.open(Constants.Urls.WikiPhpUpgrade)
})
} else if formula.isInstalled {
Button("phpman.buttons.uninstall".localizedForSwiftUI, role: .destructive) {
Task { await self.confirmUninstall(formula) }
}
} else {
Button("phpman.buttons.install".localizedForSwiftUI) {
Task { await self.install(formula) }
}.disabled(formula.hasUpgradedFormulaAlias)
}.disabled(formula.hasUpgradedFormulaAlias || !formula.hasFormulaFile)
}
}
}
@ -268,6 +274,8 @@ struct PhpVersionManagerView: View {
Text("phpman.version.installed".localized(formula.installedVersion!))
.font(.system(size: 11))
.foregroundColor(.gray)
} else if !formula.hasFormulaFile {
unavailableFormula()
} else {
Text("phpman.version.available_for_installation".localizedForSwiftUI)
.font(.system(size: 11))
@ -291,7 +299,19 @@ struct PhpVersionManagerView: View {
.foregroundColor(formula.iconColor)
.padding(.horizontal, 5)
}
private func unavailableFormula() -> some View {
HStack(spacing: 5) {
Text("phpman.version.unavailable".localizedForSwiftUI)
.font(.system(size: 11))
.foregroundColor(.gray)
HelpButton(action: {
NSWorkspace.shared.open(Constants.Urls.WikiPhpUnavailable)
})
}
}
}
// swiftlint:enable type_body_length
#Preview {
PhpVersionManagerView(

View File

@ -45,7 +45,7 @@ extension NSEvent.ModifierFlags {
}
}
extension NSEvent.ModifierFlags: CustomStringConvertible {
extension NSEvent.ModifierFlags: @retroactive CustomStringConvertible {
public var description: String {
var output = ""

View File

@ -138,7 +138,9 @@ and restart PHP Monitor for extensions to become visible. If the problem persist
"phpman.version.has_update" = "Version %@ installed, %@ available.";
"phpman.version.installed" = "Version %@ is currently installed.";
"phpman.version.available_for_installation" = "This version can be installed.";
"phpman.version.automatic_upgrade" = "This version will be automatically installed by upgrading an older version.";
"phpman.version.unavailable" = "This version is temporarily unavailable.";
"phpman.version.automatic_upgrade" = "This version will be automatically installed by upgrading an older version.
(Both this new version and the old one will be available after upgrading.)";
"phpman.buttons.uninstall" = "Uninstall";
"phpman.buttons.install" = "Install";
"phpman.buttons.update" = "Update";
@ -864,3 +866,14 @@ Please note that some features (greyed out below) are currently unavailable beca
"alert.language_changed.title" = "You must restart PHP Monitor!";
"alert.language_changed.subtitle" = "You just changed the display language of PHP Monitor. The menu will immediately use the correct language, but you may need to restart the app for all text throughout the app to reflect your new language choice.";
// STARTUP TIMEOUT
"startup.timeout.ignore" = "Ignore";
"startup.timeout.title" = "PHP Monitor is taking too long to initialize!";
"startup.timeout.subtitle" = "If PHP Monitor remains busy for longer than 30 seconds, there may be something wrong with your Homebrew setup.";
"startup.timeout.description" = "Sometimes, due to various file permission issues, things may break. You can try using `brew doctor` and `brew cleanup` to fix this.
It is recommended to restart PHP Monitor afterwards. Learn more about this issue at: https://github.com/nicoverbruggen/phpmon/issues/294.
If PHP Monitor has finished initializing anyway or you want to wait a bit longer, feel free to click 'Ignore' and use PHP Monitor as usual. Either way, you may want to investigate, because this isn't supposed to take this long.";

View File

@ -35,6 +35,19 @@ class TestableConfigurations {
"loopback": "127.0.0.1"
}
"""),
"/opt/homebrew/Library/Taps/shivammathur/homebrew-php/Formula/php.rb" : .fake(.text),
// "/opt/homebrew/Library/Taps/shivammathur/homebrew-php/Formula/php@8.5.rb" : .fake(.text),
"/opt/homebrew/Library/Taps/shivammathur/homebrew-php/Formula/php@8.4.rb" : .fake(.text),
"/opt/homebrew/Library/Taps/shivammathur/homebrew-php/Formula/php@8.3.rb" : .fake(.text),
"/opt/homebrew/Library/Taps/shivammathur/homebrew-php/Formula/php@8.2.rb" : .fake(.text),
"/opt/homebrew/Library/Taps/shivammathur/homebrew-php/Formula/php@8.1.rb" : .fake(.text),
"/opt/homebrew/Library/Taps/shivammathur/homebrew-php/Formula/php@8.0.rb" : .fake(.text),
"/opt/homebrew/Library/Taps/shivammathur/homebrew-php/Formula/php@7.4.rb" : .fake(.text),
"/opt/homebrew/Library/Taps/shivammathur/homebrew-php/Formula/php@7.3.rb" : .fake(.text),
"/opt/homebrew/Library/Taps/shivammathur/homebrew-php/Formula/php@7.2.rb" : .fake(.text),
"/opt/homebrew/Library/Taps/shivammathur/homebrew-php/Formula/php@7.1.rb" : .fake(.text),
"/opt/homebrew/Library/Taps/shivammathur/homebrew-php/Formula/php@7.0.rb" : .fake(.text),
"/opt/homebrew/Library/Taps/shivammathur/homebrew-php/Formula/php@5.6.rb" : .fake(.text)
],
shellOutput: [
"/opt/homebrew/bin/brew --version"

View File

@ -85,22 +85,27 @@ final class MainMenuTest: UITestCase {
final func test_can_open_php_version_manager() throws {
let app = launch(openMenu: true)
app.mainMenuItem(withText: "mi_php_version_manager".localized).click()
// Should display loader
assertExists(app.staticTexts["phpman.busy.title".localized], 1)
// After loading, should display PHP 8.2 and PHP 8.3
// After loading, should display PHP 8.2, PHP 8.3, PHP 8.4
assertExists(app.staticTexts["PHP 8.2"], 5)
assertExists(app.staticTexts["PHP 8.3"])
assertExists(app.staticTexts["PHP 8.4"])
// Should also display pre-release version
assertExists(app.staticTexts["PHP 8.4"])
assertExists(app.staticTexts["PHP 8.5"])
assertExists(app.staticTexts["phpman.version.prerelease".localized.uppercased()])
assertExists(app.staticTexts["phpman.version.available_for_installation".localized])
// But not PHP 8.5 (yet)
assertNotExists(app.staticTexts["PHP 8.5"])
// The pre-release version should be unavailable
assertExists(app.staticTexts["phpman.version.unavailable".localized])
// But not PHP 8.6 (yet)
assertNotExists(app.staticTexts["PHP 8.6"])
// Also, PHP 8.2 should have an update available
assertExists(app.staticTexts["phpman.version.has_update".localized(