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

Extension loading improvements (#31) and more

* The README has been updated with additional information
* The acknowledgements section has been added to the README
* The php@X.X/opt/bin/php-config binary is now used (#39)
* Extensions are now loaded from all possible .ini files
* PHP Monitor's preferences window can now be triggered via hotkey
* The first nine extensions can be triggered via hotkey
This commit is contained in:
2021-04-07 16:58:05 +02:00
parent faf49fbe1d
commit 0c0e7fc87d
11 changed files with 147 additions and 22 deletions

View File

@ -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 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:
<details>
<summary><strong>Which versions of PHP are supported?</strong></summary>
<ul>
<li>PHP 5.6</li>
<li>PHP 7.0</li>
<li>PHP 7.1</li>
<li>PHP 7.2</li>
<li>PHP 7.3</li>
<li>PHP 7.4</li>
<li>PHP 8.0</li>
<li>PHP 8.1</li>
</ul>
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.
</details>
<details> <details>
<summary><strong>I want PHP Monitor to start up when I boot my Mac!</strong></summary> <summary><strong>I want PHP Monitor to start up when I boot my Mac!</strong></summary>
@ -135,6 +153,35 @@ This problem is usually resolved by upgrading Valet and running `valet install`
valet install valet install
</details> </details>
<details>
<summary><strong>PHP Monitor tells me my installation is broken, but I don't see why!</strong></summary>
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).
</details>
<details> <details>
<summary><strong>One of the limits (memory limit, max POST size, max upload size) shows an exclamation mark!</strong></summary> <summary><strong>One of the limits (memory limit, max POST size, max upload size) shows an exclamation mark!</strong></summary>
@ -150,7 +197,7 @@ You must a provide a value like so: `1024K`, `256M`, `1G`. Alternatively, `-1` i
<details> <details>
<summary><strong>One of my commented out extensions is not being detected...</strong></summary> <summary><strong>One of my commented out extensions is not being detected...</strong></summary>
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"`
* `; 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. 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.
</details> </details>
<details> <details>
@ -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. 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).
</details> </details>
@ -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!). 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 ## 🚜 How it works
### Loading info about PHP in the background ### 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. In order to save power, this only happens once every 60 seconds.

View File

@ -23,24 +23,35 @@ class ExtensionParserTest: XCTestCase {
func testExtensionNameIsCorrect() throws { func testExtensionNameIsCorrect() throws {
let extensions = PhpExtension.load(from: Self.phpIniFileUrl) let extensions = PhpExtension.load(from: Self.phpIniFileUrl)
XCTAssertEqual(extensions.first!.name, "xdebug") let extensionNames = extensions.map { (ext) -> String in
XCTAssertEqual(extensions.last!.name, "imagick") 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 { func testExtensionStatusIsCorrect() throws {
let extensions = PhpExtension.load(from: Self.phpIniFileUrl) let extensions = PhpExtension.load(from: Self.phpIniFileUrl)
XCTAssertEqual(extensions.first!.enabled, true) // xdebug should be enabled
XCTAssertEqual(extensions.last!.enabled, false) XCTAssertEqual(extensions[0].enabled, true)
// imagick should be disabled
XCTAssertEqual(extensions[1].enabled, false)
} }
func testToggleWorksAsExpected() throws { func testToggleWorksAsExpected() throws {
let destination = Utility.copyToTemporaryFile(resourceName: "php", fileExtension: "ini")! let destination = Utility.copyToTemporaryFile(resourceName: "php", fileExtension: "ini")!
let extensions = PhpExtension.load(from: destination) 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! let xdebug = extensions.first!
XCTAssertTrue(xdebug.name == "xdebug")
XCTAssertEqual(xdebug.enabled, true) XCTAssertEqual(xdebug.enabled, true)
xdebug.toggle() xdebug.toggle()
XCTAssertEqual(xdebug.enabled, false) XCTAssertEqual(xdebug.enabled, false)

View File

@ -1,5 +1,8 @@
zend_extension="xdebug.so" zend_extension="xdebug.so"
; zend_extension="imagick.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] [PHP]

View File

@ -29,6 +29,11 @@ class PhpExtension {
/// Whether the extension has been enabled. /// Whether the extension has been enabled.
var enabled: Bool 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. 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. - 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=)"(?<name>[a-zA-Z]*).so"$"# static let extensionRegex = #"^(extension=|zend_extension=|; extension=|; zend_extension=)(?<name>["]?(?:\/?.\/?)+(?:\.so)"?)$"#
/** /**
When registering an extension, we do that based on the line found inside the .ini file. 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)! let range = Range(match!.range(withName: "name"), in: line)!
self.line = 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.enabled = !line.contains(";")
self.file = file self.file = file
} }

View File

@ -34,7 +34,6 @@ class PhpInstallation {
// Load extension information // Load extension information
let path = URL(fileURLWithPath: "\(Paths.etcPath)/php/\(version.short)/php.ini") let path = URL(fileURLWithPath: "\(Paths.etcPath)/php/\(version.short)/php.ini")
extensions = PhpExtension.load(from: path) extensions = PhpExtension.load(from: path)
// Get configuration values // Get configuration values
@ -43,6 +42,20 @@ class PhpInstallation {
upload_max_filesize: Self.getByteCount(key: "upload_max_filesize"), upload_max_filesize: Self.getByteCount(key: "upload_max_filesize"),
post_max_size: Self.getByteCount(key: "post_max_size") 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 { private static func getVersion() -> Version {
var versionStruct = 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")) { if (version == "" || version.contains("Warning") || version.contains("Error")) {
versionStruct.short = "💩 BROKEN" versionStruct.short = "💩 BROKEN"

View File

@ -98,7 +98,7 @@ class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate {
menu.addItem(NSMenuItem.separator()) menu.addItem(NSMenuItem.separator())
// Add about & quit menu items // 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_about".localized, action: #selector(openAbout), keyEquivalent: ""))
menu.addItem(NSMenuItem(title: "mi_quit".localized, action: #selector(terminateApp), keyEquivalent: "q")) menu.addItem(NSMenuItem(title: "mi_quit".localized, action: #selector(terminateApp), keyEquivalent: "q"))

View File

@ -106,8 +106,10 @@ class StatusMenu : NSMenu {
self.addItem(NSMenuItem(title: "mi_no_extensions_detected".localized, action: nil, keyEquivalent: "")) self.addItem(NSMenuItem(title: "mi_no_extensions_detected".localized, action: nil, keyEquivalent: ""))
} }
var shortcutKey = 1
for phpExtension in App.phpInstall!.extensions { for phpExtension in App.phpInstall!.extensions {
self.addExtensionItem(phpExtension) self.addExtensionItem(phpExtension, shortcutKey)
shortcutKey += 1
} }
self.addItem(NSMenuItem.separator()) 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")) 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( let menuItem = ExtensionMenuItem(
title: "\(phpExtension.name.capitalized) (php.ini)", title: "\(phpExtension.name) (\(phpExtension.fileNameOnly))",
action: #selector(MainMenu.toggleExtension), keyEquivalent: "" action: #selector(MainMenu.toggleExtension),
keyEquivalent: keyEquivalent
) )
if menuItem.keyEquivalent != "" {
menuItem.keyEquivalentModifierMask = [.option]
}
menuItem.state = phpExtension.enabled ? .on : .off menuItem.state = phpExtension.enabled ? .on : .off
menuItem.phpExtension = phpExtension menuItem.phpExtension = phpExtension

View File

@ -162,7 +162,11 @@ class Actions {
*/ */
public static func sed(file: String, original: String, replacement: String) 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)")
} }
/** /**

View File

@ -14,8 +14,9 @@ class Command {
- Parameter path: The path of the command or program to invoke. - Parameter path: The path of the command or program to invoke.
- Parameter arguments: A list of arguments that are passed on. - 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() let task = Process()
task.launchPath = path task.launchPath = path
task.arguments = arguments task.arguments = arguments
@ -26,6 +27,13 @@ class Command {
let data = pipe.fileHandleForReading.readDataToEndOfFile() let data = pipe.fileHandleForReading.readDataToEndOfFile()
let output: String = String.init(data: data, encoding: String.Encoding.utf8)! 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; return output;
} }

View File

@ -47,6 +47,10 @@ class Paths {
return "\(binPath)/php" return "\(binPath)/php"
} }
public static var phpConfig: String {
return "\(binPath)/php-config"
}
// - MARK: Paths // - MARK: Paths
public static var binPath: String { public static var binPath: String {

View File

@ -45,7 +45,7 @@
// PREFERENCES // PREFERENCES
"prefs.title" = "Preferences"; "prefs.title" = "PHP Monitor: Preferences";
"prefs.close" = "Close"; "prefs.close" = "Close";
"prefs.dynamic_icon_title" = "Show a dynamic icon in the menu bar"; "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."; "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.";