diff --git a/README.md b/README.md
index 6e6066c..7fae4ff 100644
--- a/README.md
+++ b/README.md
@@ -59,7 +59,25 @@ PHP Monitor performs some integrity checks to ensure a good experience when usin
> If you are having issues, the first thing you should be doing is installing the latest version of PHP Monitor _and_ Laravel Valet. This can resolve a variety of issues. To upgrade Valet, run `composer global update`. Don't forget to run `valet install` after upgrading.
-If you're still having issues, here's a few common issues and solutions:
+If you're still having issues, here's a few common questions & answers, as well as issues and solutions:
+
+
+Which versions of PHP are supported?
+
+
+- PHP 5.6
+- PHP 7.0
+- PHP 7.1
+- PHP 7.2
+- PHP 7.3
+- PHP 7.4
+- PHP 8.0
+- PHP 8.1
+
+
+For more details, consult the [constants file](https://github.com/nicoverbruggen/phpmon/blob/main/phpmon/Constants.swift#L16) file to see which versions are supported.
+
+
I want PHP Monitor to start up when I boot my Mac!
@@ -135,6 +153,35 @@ This problem is usually resolved by upgrading Valet and running `valet install`
valet install
+
+
+PHP Monitor tells me my installation is broken, but I don't see why!
+
+PHP Monitor tells you that a PHP installation is broken, if the configuration is causing warnings or errors when determining the version number.
+
+Since PHP Monitor changes the linked version via Homebrew, both Valet *and* your terminal (CLI) should use the new PHP version.
+
+However, this might not be the case on your system. You _might_ have a specific version of PHP linked if that is not the case. In that case, you may need to change your `.bashrc` or `.zshrc` file where the PATH is set (depending on the terminal you use).
+
+You can find out which version of PHP is being used by running `which php`.
+
+You can find out what exactly is causing the issue by running a command. On Intel, you can run (replace `7.4` with the version that is broken):
+
+```
+/usr/local/opt/php@7.4/bin/php -r "print phpversion();"
+```
+
+On Apple Silicon, you can run (replace `7.4` with the version that is broken):
+
+```
+/opt/homebrew/opt/php@7.4/bin/php -r "print phpversion();"
+```
+
+You should see an error or a warning here in the output.
+
+Usually this is a duplicate extension declaration causing issues, or an extension that couldn't be loaded. You'll have to solve that issue yourself (usually by removing the offending extension or reinstalling).
+
+
One of the limits (memory limit, max POST size, max upload size) shows an exclamation mark!
@@ -150,7 +197,7 @@ You must a provide a value like so: `1024K`, `256M`, `1G`. Alternatively, `-1` i
One of my commented out extensions is not being detected...
-The app searches in the relevant `php.ini` file for a specific pattern. For regular extensions:
+The app searches in the relevant `.ini` files for a specific pattern. For regular extensions:
* `extension="*.so"`
* `; extension="*.so"`
@@ -162,6 +209,8 @@ For Zend extensions:
The `*` is a wildcard and the name of the extension. If you've commented out the extension, make sure you've commented it out with a semicolon (;) and a single space after the semicolon for PHP Monitor to detect it.
+Since v3.4 all of the loaded .ini files are sourced to determine which extensions are enabled.
+
@@ -185,7 +234,7 @@ PHP Monitor itself doesn't do any network requests. Feel free to check the sourc
This is a security feature of Brew. When you start a service as an administrator, the root user becomes the owner of relevant binaries.
-You will need to manually clean up those folders yourself using `rm -rf` or by manually removing those folders via Finder.
+You will need to manually clean up those folders yourself using `rm -rf` (or by manually removing those folders via Finder).
@@ -201,11 +250,24 @@ You can find a [sponsor](https://nicoverbruggen.be/sponsor) link at the top of t
Donations really help with the Apple Developer Program cost, and keep me motivated to keep working on PHP Monitor outside of work hours (I do have a day job!).
+## 😎 Acknowledgements
+
+While I did make this application during my own free time, I have been lucky enough to do various experiments during work hours at [DIVE](https://dive.be). I'd also like to shout out the following folks:
+
+* My colleagues at [DIVE](https://dive.be)
+* The [Homebrew](https://brew.sh/) team who maintain
+* The [developers & maintainers of Valet](https://github.com/laravel/valet/graphs/contributors)
+* Everyone in the Laravel community who shared the app (thanks!)
+* Various folks who [reached](https://twitter.com/stauffermatt) [out](https://twitter.com/marcelpociot)
+* Everyone who left feedback via issues
+
+Thank you very much for your contributions, kind words and support.
+
## 🚜 How it works
### Loading info about PHP in the background
-This utility runs `php -r 'print phpversion()'` in the background periodically. It also checks your `.ini` files for extensions and loads more information about your limits (memory limit, POST limit, upload limit).
+This utility runs `php-config --version'` in the background periodically. It also checks your `.ini` files for extensions and loads more information about your limits (memory limit, POST limit, upload limit).
In order to save power, this only happens once every 60 seconds.
diff --git a/phpmon-tests/ExtensionParserTest.swift b/phpmon-tests/ExtensionParserTest.swift
index 5dc8db9..72565b7 100644
--- a/phpmon-tests/ExtensionParserTest.swift
+++ b/phpmon-tests/ExtensionParserTest.swift
@@ -23,24 +23,35 @@ class ExtensionParserTest: XCTestCase {
func testExtensionNameIsCorrect() throws {
let extensions = PhpExtension.load(from: Self.phpIniFileUrl)
- XCTAssertEqual(extensions.first!.name, "xdebug")
- XCTAssertEqual(extensions.last!.name, "imagick")
+ let extensionNames = extensions.map { (ext) -> String in
+ return ext.name
+ }
+
+ XCTAssertTrue(extensionNames.contains("xdebug"))
+ XCTAssertTrue(extensionNames.contains("imagick"))
+ XCTAssertTrue(extensionNames.contains("opcache"))
+ XCTAssertTrue(extensionNames.contains("yaml"))
+ XCTAssertFalse(extensionNames.contains("fake"))
}
func testExtensionStatusIsCorrect() throws {
let extensions = PhpExtension.load(from: Self.phpIniFileUrl)
- XCTAssertEqual(extensions.first!.enabled, true)
- XCTAssertEqual(extensions.last!.enabled, false)
+ // xdebug should be enabled
+ XCTAssertEqual(extensions[0].enabled, true)
+
+ // imagick should be disabled
+ XCTAssertEqual(extensions[1].enabled, false)
}
func testToggleWorksAsExpected() throws {
let destination = Utility.copyToTemporaryFile(resourceName: "php", fileExtension: "ini")!
let extensions = PhpExtension.load(from: destination)
- XCTAssertEqual(extensions.count, 2)
+ XCTAssertEqual(extensions.count, 4)
- // Try to disable it!
+ // Try to disable xdebug (should be detected first)!
let xdebug = extensions.first!
+ XCTAssertTrue(xdebug.name == "xdebug")
XCTAssertEqual(xdebug.enabled, true)
xdebug.toggle()
XCTAssertEqual(xdebug.enabled, false)
diff --git a/phpmon-tests/php.ini b/phpmon-tests/php.ini
index 5afb2fe..e0128ba 100644
--- a/phpmon-tests/php.ini
+++ b/phpmon-tests/php.ini
@@ -1,5 +1,8 @@
zend_extension="xdebug.so"
; zend_extension="imagick.so"
+zend_extension=/opt/homebrew/opt/php/lib/php/20200930/opcache.so
+zend_extension="/opt/homebrew/opt/php/lib/php/20200930/yaml.so"
+#zend_extension="/opt/homebrew/opt/php/lib/php/20200930/fake.so"
[PHP]
diff --git a/phpmon/Domain/Core/PhpExtension.swift b/phpmon/Domain/Core/PhpExtension.swift
index 1925bae..0e38414 100644
--- a/phpmon/Domain/Core/PhpExtension.swift
+++ b/phpmon/Domain/Core/PhpExtension.swift
@@ -29,6 +29,11 @@ class PhpExtension {
/// 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.
@@ -41,7 +46,7 @@ class PhpExtension {
- Note: Extensions that are disabled in a different way will not be detected. This is intentional.
*/
- static let extensionRegex = #"^(extension=|zend_extension=|; extension=|; zend_extension=)"(?[a-zA-Z]*).so"$"#
+ static let extensionRegex = #"^(extension=|zend_extension=|; extension=|; zend_extension=)(?["]?(?:\/?.\/?)+(?:\.so)"?)$"#
/**
When registering an extension, we do that based on the line found inside the .ini file.
@@ -52,7 +57,12 @@ class PhpExtension {
let range = Range(match!.range(withName: "name"), in: line)!
self.line = line
- self.name = line[range]
+
+ 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
}
diff --git a/phpmon/Domain/Core/PhpInstallation.swift b/phpmon/Domain/Core/PhpInstallation.swift
index 3c52a1f..cbc924a 100644
--- a/phpmon/Domain/Core/PhpInstallation.swift
+++ b/phpmon/Domain/Core/PhpInstallation.swift
@@ -34,7 +34,6 @@ class PhpInstallation {
// Load extension information
let path = URL(fileURLWithPath: "\(Paths.etcPath)/php/\(version.short)/php.ini")
-
extensions = PhpExtension.load(from: path)
// Get configuration values
@@ -43,6 +42,20 @@ class PhpInstallation {
upload_max_filesize: Self.getByteCount(key: "upload_max_filesize"),
post_max_size: Self.getByteCount(key: "post_max_size")
)
+
+ // Determine which folder(s) to scan for additional files
+ let iniFolder = Command.execute(path: Paths.phpConfig, arguments: ["--ini-dir"], trimNewlines: true)
+
+ // Check the contents of the ini dir
+ let enumerator = FileManager.default.enumerator(atPath: URL(fileURLWithPath: iniFolder).path)
+ let filePaths = enumerator?.allObjects as! [String]
+
+ filePaths.filter { $0.contains(".ini") }.forEach { (iniFileName) in
+ let extensions = PhpExtension.load(from: URL(fileURLWithPath: "\(iniFolder)/\(iniFileName)"))
+ if extensions.count > 0 {
+ self.extensions.append(contentsOf: extensions)
+ }
+ }
}
/**
@@ -51,7 +64,7 @@ class PhpInstallation {
*/
private static func getVersion() -> Version {
var versionStruct = Version()
- let version = Command.execute(path: Paths.php, arguments: ["-r", "print phpversion();"])
+ let version = Command.execute(path: Paths.phpConfig, arguments: ["--version"], trimNewlines: true)
if (version == "" || version.contains("Warning") || version.contains("Error")) {
versionStruct.short = "💩 BROKEN"
diff --git a/phpmon/Domain/Menu/MainMenu.swift b/phpmon/Domain/Menu/MainMenu.swift
index b269323..9e03c8c 100644
--- a/phpmon/Domain/Menu/MainMenu.swift
+++ b/phpmon/Domain/Menu/MainMenu.swift
@@ -98,7 +98,7 @@ class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate {
menu.addItem(NSMenuItem.separator())
// Add about & quit menu items
- menu.addItem(NSMenuItem(title: "mi_preferences".localized, action: #selector(openPrefs), keyEquivalent: ""))
+ menu.addItem(NSMenuItem(title: "mi_preferences".localized, action: #selector(openPrefs), keyEquivalent: ","))
menu.addItem(NSMenuItem(title: "mi_about".localized, action: #selector(openAbout), keyEquivalent: ""))
menu.addItem(NSMenuItem(title: "mi_quit".localized, action: #selector(terminateApp), keyEquivalent: "q"))
diff --git a/phpmon/Domain/Menu/StatusMenu.swift b/phpmon/Domain/Menu/StatusMenu.swift
index b4466f7..5d2d7a9 100644
--- a/phpmon/Domain/Menu/StatusMenu.swift
+++ b/phpmon/Domain/Menu/StatusMenu.swift
@@ -106,8 +106,10 @@ class StatusMenu : NSMenu {
self.addItem(NSMenuItem(title: "mi_no_extensions_detected".localized, action: nil, keyEquivalent: ""))
}
+ var shortcutKey = 1
for phpExtension in App.phpInstall!.extensions {
- self.addExtensionItem(phpExtension)
+ self.addExtensionItem(phpExtension, shortcutKey)
+ shortcutKey += 1
}
self.addItem(NSMenuItem.separator())
@@ -115,11 +117,19 @@ class StatusMenu : NSMenu {
self.addItem(NSMenuItem(title: "mi_php_refresh".localized, action: #selector(MainMenu.reloadPhpMonitorMenu), keyEquivalent: "r"))
}
- private func addExtensionItem(_ phpExtension: PhpExtension) {
+ private func addExtensionItem(_ phpExtension: PhpExtension, _ shortcutKey: Int) {
+ let keyEquivalent = shortcutKey < 9 ? "\(shortcutKey)" : ""
+
let menuItem = ExtensionMenuItem(
- title: "\(phpExtension.name.capitalized) (php.ini)",
- action: #selector(MainMenu.toggleExtension), keyEquivalent: ""
+ title: "\(phpExtension.name) (\(phpExtension.fileNameOnly))",
+ action: #selector(MainMenu.toggleExtension),
+ keyEquivalent: keyEquivalent
)
+
+ if menuItem.keyEquivalent != "" {
+ menuItem.keyEquivalentModifierMask = [.option]
+ }
+
menuItem.state = phpExtension.enabled ? .on : .off
menuItem.phpExtension = phpExtension
diff --git a/phpmon/Domain/Terminal/Actions.swift b/phpmon/Domain/Terminal/Actions.swift
index c917478..8fb4a6c 100644
--- a/phpmon/Domain/Terminal/Actions.swift
+++ b/phpmon/Domain/Terminal/Actions.swift
@@ -162,7 +162,11 @@ class Actions {
*/
public static func sed(file: String, original: String, replacement: String)
{
- Shell.run("sed -i '' 's/\(original)/\(replacement)/g' \(file)")
+ // Escape slashes (or `sed` won't work)
+ let e_original = original.replacingOccurrences(of: "/", with: "\\/")
+ let e_replacment = replacement.replacingOccurrences(of: "/", with: "\\/")
+
+ Shell.run("sed -i '' 's/\(e_original)/\(e_replacment)/g' \(file)")
}
/**
diff --git a/phpmon/Domain/Terminal/Command.swift b/phpmon/Domain/Terminal/Command.swift
index a09cb09..28014a2 100644
--- a/phpmon/Domain/Terminal/Command.swift
+++ b/phpmon/Domain/Terminal/Command.swift
@@ -14,8 +14,9 @@ class Command {
- Parameter path: The path of the command or program to invoke.
- Parameter arguments: A list of arguments that are passed on.
+ - Parameter trimNewlines: Removes empty new line output.
*/
- public static func execute(path: String, arguments: [String]) -> String {
+ public static func execute(path: String, arguments: [String], trimNewlines: Bool = false) -> String {
let task = Process()
task.launchPath = path
task.arguments = arguments
@@ -26,6 +27,13 @@ class Command {
let data = pipe.fileHandleForReading.readDataToEndOfFile()
let output: String = String.init(data: data, encoding: String.Encoding.utf8)!
+
+ if (trimNewlines) {
+ return output.components(separatedBy: .newlines)
+ .filter({ !$0.isEmpty })
+ .joined(separator: "\n")
+ }
+
return output;
}
diff --git a/phpmon/Domain/Terminal/Paths.swift b/phpmon/Domain/Terminal/Paths.swift
index 2b78567..77d4031 100644
--- a/phpmon/Domain/Terminal/Paths.swift
+++ b/phpmon/Domain/Terminal/Paths.swift
@@ -47,6 +47,10 @@ class Paths {
return "\(binPath)/php"
}
+ public static var phpConfig: String {
+ return "\(binPath)/php-config"
+ }
+
// - MARK: Paths
public static var binPath: String {
diff --git a/phpmon/Localizable.strings b/phpmon/Localizable.strings
index 26061a4..c26d952 100644
--- a/phpmon/Localizable.strings
+++ b/phpmon/Localizable.strings
@@ -45,7 +45,7 @@
// PREFERENCES
-"prefs.title" = "Preferences";
+"prefs.title" = "PHP Monitor: Preferences";
"prefs.close" = "Close";
"prefs.dynamic_icon_title" = "Show a dynamic icon in the menu bar";
"prefs.dynamic_icon_desc" = "If you uncheck this box, the truck icon will always be visible.\nIf checked, it will display the major version number of the currently linked PHP version.";