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

🚀 Version 3.0

* develop: (29 commits)
  🔧 Bump version for release
  👌 All methods are internal by default
  📝 Move all FAQ to README
  📝 Reorganized document
  📝 Updated quick troubleshooting
  📝 New screenshot
  🔧 Bump build version
  🎨 Use Self to refer to current type
   Improved byte value parsing
  📝 Updated SECURITY
  📝 Updated README, new screenshot
  🔧 Bump build version
  🚚 Move files around
  🔥 Crash if the view is unavailable
  🎨 Fix threading issue
   Completed new design for v3.0
   Tweak custom views
   Custom views
  🎨 Updated localization, cleanup
  🔥 Cleanup
  ...
This commit is contained in:
2021-02-12 09:47:03 +01:00
35 changed files with 1128 additions and 665 deletions

View File

@ -15,7 +15,7 @@
C41C1B3E22B0098000E7CF16 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = C41C1B3C22B0098000E7CF16 /* Main.storyboard */; };
C41C1B4722B009A400E7CF16 /* Shell.swift in Sources */ = {isa = PBXBuildFile; fileRef = C41C1B4622B009A400E7CF16 /* Shell.swift */; };
C41C1B4922B00A9800E7CF16 /* MenuBarImageGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = C41C1B4822B00A9800E7CF16 /* MenuBarImageGenerator.swift */; };
C41C1B4B22B019FF00E7CF16 /* PhpVersion.swift in Sources */ = {isa = PBXBuildFile; fileRef = C41C1B4A22B019FF00E7CF16 /* PhpVersion.swift */; };
C41C1B4B22B019FF00E7CF16 /* PhpInstall.swift in Sources */ = {isa = PBXBuildFile; fileRef = C41C1B4A22B019FF00E7CF16 /* PhpInstall.swift */; };
C41C1B4D22B0215A00E7CF16 /* Actions.swift in Sources */ = {isa = PBXBuildFile; fileRef = C41C1B4C22B0215A00E7CF16 /* Actions.swift */; };
C42295DD2358D02000E263B2 /* Command.swift in Sources */ = {isa = PBXBuildFile; fileRef = C42295DC2358D02000E263B2 /* Command.swift */; };
C46FA23F246C358E00944F05 /* StringExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = C46FA23E246C358E00944F05 /* StringExtension.swift */; };
@ -25,8 +25,13 @@
C476FF9822B0DD830098105B /* Alert.swift in Sources */ = {isa = PBXBuildFile; fileRef = C476FF9722B0DD830098105B /* Alert.swift */; };
C4811D2422D70A4700B5F6B3 /* App.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4811D2322D70A4700B5F6B3 /* App.swift */; };
C4811D2A22D70F9A00B5F6B3 /* MainMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4811D2922D70F9A00B5F6B3 /* MainMenu.swift */; };
C486EFFC2586931100A02B2C /* PhpMenuItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = C486EFFB2586931100A02B2C /* PhpMenuItem.swift */; };
C48D0C9025CC7FD000CC7490 /* StatsView.xib in Resources */ = {isa = PBXBuildFile; fileRef = C48D0C8F25CC7FD000CC7490 /* StatsView.xib */; };
C48D0C9325CC804200CC7490 /* XibLoadable.swift in Sources */ = {isa = PBXBuildFile; fileRef = C48D0C9225CC804200CC7490 /* XibLoadable.swift */; };
C48D0C9625CC80B100CC7490 /* HeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C48D0C9525CC80B100CC7490 /* HeaderView.swift */; };
C48D0C9A25CC888B00CC7490 /* HeaderView.xib in Resources */ = {isa = PBXBuildFile; fileRef = C48D0C9925CC888B00CC7490 /* HeaderView.xib */; };
C48D0CA325CC992000CC7490 /* StatsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C48D0CA225CC992000CC7490 /* StatsView.swift */; };
C49EAB46259FC305007F6C3B /* Paths.swift in Sources */ = {isa = PBXBuildFile; fileRef = C49EAB45259FC305007F6C3B /* Paths.swift */; };
C4ACA38F25C754C100060C66 /* PhpExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4ACA38E25C754C100060C66 /* PhpExtension.swift */; };
C4D8016622B1584700C6DA1B /* Startup.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4D8016522B1584700C6DA1B /* Startup.swift */; };
C4EE188422D3386B00E126E5 /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4EE188322D3386B00E126E5 /* Constants.swift */; };
C4F8C0A422D4F12C002EFE61 /* DateExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4F8C0A322D4F12C002EFE61 /* DateExtension.swift */; };
@ -44,7 +49,7 @@
C41C1B4022B0098000E7CF16 /* phpmon.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = phpmon.entitlements; sourceTree = "<group>"; };
C41C1B4622B009A400E7CF16 /* Shell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Shell.swift; sourceTree = "<group>"; };
C41C1B4822B00A9800E7CF16 /* MenuBarImageGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuBarImageGenerator.swift; sourceTree = "<group>"; };
C41C1B4A22B019FF00E7CF16 /* PhpVersion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhpVersion.swift; sourceTree = "<group>"; };
C41C1B4A22B019FF00E7CF16 /* PhpInstall.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhpInstall.swift; sourceTree = "<group>"; };
C41C1B4C22B0215A00E7CF16 /* Actions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Actions.swift; sourceTree = "<group>"; };
C42295DC2358D02000E263B2 /* Command.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Command.swift; sourceTree = "<group>"; };
C46FA23E246C358E00944F05 /* StringExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StringExtension.swift; sourceTree = "<group>"; };
@ -54,8 +59,13 @@
C476FF9722B0DD830098105B /* Alert.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Alert.swift; sourceTree = "<group>"; };
C4811D2322D70A4700B5F6B3 /* App.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = App.swift; sourceTree = "<group>"; };
C4811D2922D70F9A00B5F6B3 /* MainMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainMenu.swift; sourceTree = "<group>"; };
C486EFFB2586931100A02B2C /* PhpMenuItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhpMenuItem.swift; sourceTree = "<group>"; };
C48D0C8F25CC7FD000CC7490 /* StatsView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = StatsView.xib; sourceTree = "<group>"; };
C48D0C9225CC804200CC7490 /* XibLoadable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XibLoadable.swift; sourceTree = "<group>"; };
C48D0C9525CC80B100CC7490 /* HeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HeaderView.swift; sourceTree = "<group>"; };
C48D0C9925CC888B00CC7490 /* HeaderView.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = HeaderView.xib; sourceTree = "<group>"; };
C48D0CA225CC992000CC7490 /* StatsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatsView.swift; sourceTree = "<group>"; };
C49EAB45259FC305007F6C3B /* Paths.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Paths.swift; sourceTree = "<group>"; };
C4ACA38E25C754C100060C66 /* PhpExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhpExtension.swift; sourceTree = "<group>"; };
C4D8016522B1584700C6DA1B /* Startup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Startup.swift; sourceTree = "<group>"; };
C4E713562570150F00007428 /* SECURITY.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = SECURITY.md; sourceTree = "<group>"; };
C4E713572570151400007428 /* docs */ = {isa = PBXFileReference; lastKnownFileType = folder; path = docs; sourceTree = "<group>"; };
@ -108,10 +118,7 @@
children = (
C41C1B3622B0097F00E7CF16 /* AppDelegate.swift */,
C4EE188322D3386B00E126E5 /* Constants.swift */,
C4811D2622D70CEF00B5F6B3 /* Singletons */,
C41E181722CB61EB0072CF09 /* Classes */,
C41E181822CB62200072CF09 /* View Controllers */,
C4F8C0A222D4F100002EFE61 /* Extensions */,
C41E181722CB61EB0072CF09 /* Domain */,
C41C1B3F22B0098000E7CF16 /* Info.plist */,
C41C1B4022B0098000E7CF16 /* phpmon.entitlements */,
C41C1B3A22B0098000E7CF16 /* Assets.xcassets */,
@ -121,29 +128,27 @@
path = phpmon;
sourceTree = "<group>";
};
C41E181722CB61EB0072CF09 /* Classes */ = {
C41E181722CB61EB0072CF09 /* Domain */ = {
isa = PBXGroup;
children = (
C4811D2622D70CEF00B5F6B3 /* Singletons */,
C4B13B1D25C4915000548C3A /* Core */,
C47331A0247093AC009A0597 /* Menu */,
C4811D2722D70D8E00B5F6B3 /* Commands */,
C4811D2822D70D9C00B5F6B3 /* Helpers */,
C4F8C0A222D4F100002EFE61 /* Extensions */,
);
path = Classes;
sourceTree = "<group>";
};
C41E181822CB62200072CF09 /* View Controllers */ = {
isa = PBXGroup;
children = (
C41C1B3C22B0098000E7CF16 /* Main.storyboard */,
);
path = "View Controllers";
path = Domain;
sourceTree = "<group>";
};
C47331A0247093AC009A0597 /* Menu */ = {
isa = PBXGroup;
children = (
C47331A1247093B7009A0597 /* StatusMenu.swift */,
C486EFFB2586931100A02B2C /* PhpMenuItem.swift */,
C48D0C9525CC80B100CC7490 /* HeaderView.swift */,
C48D0C9925CC888B00CC7490 /* HeaderView.xib */,
C48D0CA225CC992000CC7490 /* StatsView.swift */,
C48D0C8F25CC7FD000CC7490 /* StatsView.xib */,
);
path = Menu;
sourceTree = "<group>";
@ -173,19 +178,29 @@
isa = PBXGroup;
children = (
C476FF9722B0DD830098105B /* Alert.swift */,
C41C1B4A22B019FF00E7CF16 /* PhpVersion.swift */,
C41C1B4822B00A9800E7CF16 /* MenuBarImageGenerator.swift */,
C474B00524C0E98C00066A22 /* LocalNotification.swift */,
C412E5FB25700D5300A1FB67 /* HomebrewPackage.swift */,
);
path = Helpers;
sourceTree = "<group>";
};
C4B13B1D25C4915000548C3A /* Core */ = {
isa = PBXGroup;
children = (
C41C1B3C22B0098000E7CF16 /* Main.storyboard */,
C412E5FB25700D5300A1FB67 /* HomebrewPackage.swift */,
C41C1B4A22B019FF00E7CF16 /* PhpInstall.swift */,
C4ACA38E25C754C100060C66 /* PhpExtension.swift */,
);
path = Core;
sourceTree = "<group>";
};
C4F8C0A222D4F100002EFE61 /* Extensions */ = {
isa = PBXGroup;
children = (
C4F8C0A322D4F12C002EFE61 /* DateExtension.swift */,
C46FA23E246C358E00944F05 /* StringExtension.swift */,
C48D0C9225CC804200CC7490 /* XibLoadable.swift */,
);
path = Extensions;
sourceTree = "<group>";
@ -250,9 +265,11 @@
files = (
C41C1B3B22B0098000E7CF16 /* Assets.xcassets in Resources */,
C41C1B3E22B0098000E7CF16 /* Main.storyboard in Resources */,
C48D0C9025CC7FD000CC7490 /* StatsView.xib in Resources */,
C405A4D124B9B9140062FAFA /* InternetAccessPolicy.plist in Resources */,
C473319F2470923A009A0597 /* Localizable.strings in Resources */,
C405A4D024B9B9140062FAFA /* InternetAccessPolicy.strings in Resources */,
C48D0C9A25CC888B00CC7490 /* HeaderView.xib in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@ -263,21 +280,24 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
C4ACA38F25C754C100060C66 /* PhpExtension.swift in Sources */,
C4D8016622B1584700C6DA1B /* Startup.swift in Sources */,
C4F8C0A422D4F12C002EFE61 /* DateExtension.swift in Sources */,
C41C1B4722B009A400E7CF16 /* Shell.swift in Sources */,
C41C1B4D22B0215A00E7CF16 /* Actions.swift in Sources */,
C48D0CA325CC992000CC7490 /* StatsView.swift in Sources */,
C42295DD2358D02000E263B2 /* Command.swift in Sources */,
C4811D2422D70A4700B5F6B3 /* App.swift in Sources */,
C41C1B4922B00A9800E7CF16 /* MenuBarImageGenerator.swift in Sources */,
C48D0C9325CC804200CC7490 /* XibLoadable.swift in Sources */,
C4811D2A22D70F9A00B5F6B3 /* MainMenu.swift in Sources */,
C412E5FC25700D5300A1FB67 /* HomebrewPackage.swift in Sources */,
C41C1B3722B0097F00E7CF16 /* AppDelegate.swift in Sources */,
C41C1B4B22B019FF00E7CF16 /* PhpVersion.swift in Sources */,
C486EFFC2586931100A02B2C /* PhpMenuItem.swift in Sources */,
C41C1B4B22B019FF00E7CF16 /* PhpInstall.swift in Sources */,
C49EAB46259FC305007F6C3B /* Paths.swift in Sources */,
C476FF9822B0DD830098105B /* Alert.swift in Sources */,
C474B00624C0E98C00066A22 /* LocalNotification.swift in Sources */,
C48D0C9625CC80B100CC7490 /* HeaderView.swift in Sources */,
C47331A2247093B7009A0597 /* StatusMenu.swift in Sources */,
C46FA23F246C358E00944F05 /* StringExtension.swift in Sources */,
C4EE188422D3386B00E126E5 /* Constants.swift in Sources */,
@ -423,7 +443,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 31;
CURRENT_PROJECT_VERSION = 45;
DEVELOPMENT_TEAM = 8M54J5J787;
ENABLE_HARDENED_RUNTIME = YES;
INFOPLIST_FILE = phpmon/Info.plist;
@ -431,7 +451,7 @@
"$(inherited)",
"@executable_path/../Frameworks",
);
MARKETING_VERSION = 2.6;
MARKETING_VERSION = 3.0;
PRODUCT_BUNDLE_IDENTIFIER = com.nicoverbruggen.phpmon;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
@ -447,7 +467,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 31;
CURRENT_PROJECT_VERSION = 45;
DEVELOPMENT_TEAM = 8M54J5J787;
ENABLE_HARDENED_RUNTIME = YES;
INFOPLIST_FILE = phpmon/Info.plist;
@ -455,7 +475,7 @@
"$(inherited)",
"@executable_path/../Frameworks",
);
MARKETING_VERSION = 2.6;
MARKETING_VERSION = 3.0;
PRODUCT_BUNDLE_IDENTIFIER = com.nicoverbruggen.phpmon;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";

232
README.md
View File

@ -4,7 +4,9 @@
PHP Monitor (or phpmon) is a lightweight macOS utility app that runs on your Mac and displays the active PHP version in your status bar.
<img src="./docs/screenshot.png" width="370px" alt="phpmon screenshot (menu bar app)"/>
<img src="./docs/screenshot.png" width="389px" alt="phpmon screenshot (menu bar app)"/>
<small><i>Screenshot: A menu showing all of the functionality of PHP Monitor.</i></small>
It's also super convenient to switch between different versions of PHP. You'll even get notifications (only if you choose to opt-in, of course)!
@ -17,15 +19,15 @@ It also gives you quick access to various useful functionality (like accessing c
PHP Monitor is a universal application that runs on Apple Silicon **and** Intel-based Macs.
* macOS 10.15 Catalina or higher (works on macOS 11 Big Sur)
* Homebrew is installed in `/usr/local/homebrew` or `/opt/homebrew` (the default)
* Homebrew is installed in `/usr/local/homebrew` or `/opt/homebrew`
* The brew formula `php` has to be installed (which version is detected)
* Laravel Valet 2.13 or higher
_Please note that future versions of PHP will not work automatically, minor changes are usually required to add support for newer versions of PHP. You may need to update your Valet installation to keep everything working if a major version update of PHP has been released._
_You may need to update your Valet installation to keep everything working if a major version update of PHP has been released._
## 🚀 How to install
You can install via Homebrew, or may download the latest [release][1].
You can install via Homebrew, or may download the latest [release](https://github.com/nicoverbruggen/phpmon/releases).
To install via Homebrew, run:
@ -38,30 +40,170 @@ To upgrade your existing installation, run:
_The app is signed and notarized, meaning all you have to do is approve its first launch._
## 👨‍💻 Why I built this
## ⭐️ Star me!
I wanted to be able to see at a glance which version of PHP was linked, and handle dealing with Laravel Valet in a simple app without having to deal with the terminal every time.
If this software has been useful to you, all I ask is that you **please star the repository**, so I know that the software is being used. You can also send me [feedback](https://twitter.com/nicoverbruggen) if the app came in handy. 😃
Initially, I had an Alfred workflow for this. But this does the job as well, while also showing me at all times which version of PHP is linked (which is the main benefit over e.g. an Alfred workflow).
## 👨‍💻 Why build this?
## 🔧 Build instructions
I wanted to be able to **see at a glance** which version of PHP was linked, and handle dealing with Laravel Valet in a simple app without having to deal with the terminal every time.
<img src="./docs/build.png" width="320px" alt="build button in Xcode"/>
Initially, I had an Alfred workflow for this — but it has now been replaced with this utility, which also does a good job at displaying additional information at a glance, like the current PHP version, memory limits, and more.
If you'd like to build PHP Monitor yourself, you need:
## 🤬 The app won't start?!
* Xcode (usually the latest version)
* The contents of this repository
PHP Monitor performs some integrity checks to ensure a good experience when using the app. You'll get a message telling you that PHP Monitor won't work correctly in a variety of scenarios.
Once you have downloaded this repository, open `PHP Monitor.xcodeproj`, and you should be able to immediately build the app for your system by pressing Cmd-R. This will create a debug build. (If Xcode complains about code signing, you can turn it off.)
**Follow instructions as specified in the alert in order to resolve any issues.**
If you'd like to create a production build, choose "Any Mac" as the target and select Product > Archive.
## 🙋‍♂️ FAQ & Troubleshooting
> 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:
<details>
<summary><strong>I want PHP Monitor to start up when I boot my Mac!</strong></summary>
You can do this by dragging *PHP Monitor.app* into the **Login Items** section in **System Preferences > Users & Groups** for your account.
Super convenient!
</details>
<details>
<summary><strong>I want to set up PHP Monitor from scratch! I don't have Homebrew installed either, where do I begin?</strong></summary>
If you want to set up your computer for the very first time with PHP Monitor, here's how I do it:
Install [Homebrew](https://brew.sh) first.
Install PHP, composer, add to path:
brew install php
brew install composer
nano .zshrc
Make sure the following line is not in the comments:
# on an Intel Mac
export PATH=$HOME/bin:/usr/local/bin:$PATH
If you're on an Apple Silicon-based Mac, you'll need to add:
# on an M1 Mac
export PATH=$HOME/bin:/opt/homebrew/bin:$PATH
and add the following to your .zshrc:
export PATH=$HOME/bin:~/.composer/vendor/bin:$PATH
Make sure PHP is linked correctly:
which php
should return: `/usr/local/bin/php` (or `/opt/homebrew/bin/php`)
composer global require laravel/valet
valet install
This should install `dnsmasq` and set up Valet. Great, almost there!
valet trust
Finally, run PHP Monitor. Since the app is notarized and signed with a developer ID, it should work.
</details>
<details>
<summary><strong>PHP Monitor tells me `php` is not installed...</strong></summary>
Try installing again using `brew install php`.
This should resolve the issue! If that does not fix the issue, run `brew link php --force`. (Afterwards, you may need to restart your terminal to make sure the new linked version is detected.)
brew install php
brew link php --force
</details>
<details>
<summary><strong>Valet sites won't load. I'm getting a 502 Bad Gateway error!</strong></summary>
If you're visiting your `.test` domain, and you're getting a 502 (Bad Gateway) after switching to a different PHP version, you're dealing with a common issue.
This problem is usually resolved by upgrading Valet and running `valet install` again.
composer global update
valet install
</details>
<details>
<summary><strong>One of the limits (memory limit, max POST size, max upload size) shows an exclamation mark!</strong></summary>
The value you provided in your INI file is invalid. If that is the case, PHP will attempt to parse your value as bytes, which is usually unintended. (`1GB` will resolve to merely a few bytes, and all of your applications will run out of memory!)
You must a provide a value like so: `1024K`, `256M`, `1G`. Alternatively, `-1` is also allowed, or just an integer (which will result in N amount of bytes being the limit).
**Example**: Trying to use `1GB` as the memory limit, for example, will result in this exclamation mark. The correct way to set a 1GB limit is by using `1G` as the value. (Note: The displayed value will append `B` for clarity, so if you set `1G`, the value reported by PHP Monitor will be 1 GB.)
</details>
<details>
<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:
* `extension="*.so"`
* `; extension="*.so"`
For Zend extensions:
* `zend_extension="*.so"`
* `; zend_extension="*.so"`
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.
</details>
<details>
<summary><strong>I've got two Homebrew installations on my Apple Silicon Mac, can I choose which installation to use with PHP Monitor?</strong></summary>
Not at this time, no. PHP Monitor will prefer the `/opt/homebrew` installation over the classic installation directory.
</details>
<details>
<summary><strong>Why is the app doing network requests?</strong></summary>
It's Homebrew. I can't prevent `brew` from doing things via the network when I invoke it.
PHP Monitor itself doesn't do any network requests. Feel free to check the source code or intercept the traffic, if you don't believe me.
</details>
<details>
<summary><strong>After running PHP Monitor, Homebrew sometimes has issues with `brew upgrade`!</strong></summary>
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.
</details>
## 📝 Another issue?
I did not include any tracking or analytics software, so if you encounter issues, let me know [via an issue](https://github.com/nicoverbruggen/phpmon/issues/new).
## 💵 Support me?
I usually develop this application in my spare time, after work. If you find the application useful and you have a bit of money to spare, feel free to send me [a tip via PayPal](https://paypal.me/nicoverbruggen).
## 🚜 How it works
### Version detection
### Loading info about PHP in the background
This utility runs `php -r 'print phpversion()'` in the background periodically (every 60 seconds).
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).
In order to save power, this only happens once every 60 seconds.
### Switching PHP versions
@ -77,9 +219,9 @@ The utility runs the following commands:
- Unlink all detected PHP versions
- Switch to whatever version of PHP `php` is at (this is done to ensure that Valet works, even when attempting to use PHP 5.6)
- Stop all php-fpm service instances
- Stop all relevant services (`php`, `nginx`)
- Link the desired version of PHP
- Start the correct php-fpm service for the desired PHP version
- Start the correct `php` service for the desired PHP version
### Want to know more?
@ -87,55 +229,15 @@ If you want to know more about how this works, I recommend you check out the sou
This app isn't very complicated after all. In the end, this just (conveniently) executes some shell commands.
## 🤬 Troubleshooting
## 🔧 Build instructions
**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.**
<img src="./docs/build.png" width="320px" alt="build button in Xcode"/>
PHP Monitor performs some integrity checks to ensure a good experience when using the app. You'll get a message telling you that PHP Monitor won't work correctly in the following scenarios:
If you'd like to build PHP Monitor yourself, you need:
- The PHP binary is not located in `/usr/local/bin/php` (or `/opt/homebrew/bin/php`)
- PHP is missing in `/usr/local/opt` (or `/opt/homebrew/opt`)
- Laravel Valet is missing in `/usr/local/bin/valet`
- Brew has not been added to sudoers in `/private/etc/sudoers.d/brew`
- Valet has not been added to sudoers in `/private/etc/sudoers.d/valet`
- Multiple PHP services are active (see more info below)
* Xcode (usually the latest version)
* The contents of this repository
Follow instructions as specified in the alert in order to resolve any issues.
Once you have downloaded this repository, open `PHP Monitor.xcodeproj`, and you should be able to immediately build the app for your system by pressing Cmd-R. This will create a debug build. (If Xcode complains about code signing, you can turn it off.)
## 🏎 Quick Troubleshooting
### PHP Monitor tells me `php` is not installed
Try installing again using `brew install php`.
This should resolve the issue! If that does not fix the issue, run `brew link php --force`. (Afterwards, you may need to restart your terminal to make sure the new linked version is detected.)
brew install php
brew link php --force
### Valet sites won't load (502 Bad Gateway)
If you're visiting your `.test` domain, and you're getting a 502 (Bad Gateway) after switching to a different PHP version, you're dealing with a common issue.
This problem is usually resolved by upgrading Valet and running `valet install` again.
composer global update
valet install
## 📝 Additional Info
Please consult the [additional file][2] that contains more information. It may have answers to additional questions and more information to troubleshoot your problem.
## ⭐️ Star me!
If this software has been useful to you, I ask that you **please star the repository**, so I know that the software is being used.
I did not include any tracking or analytics software, so if you encounter issues, let me know [via an issue](https://github.com/nicoverbruggen/phpmon/issues/new).
## 💵 Support me?
I develop this application in my spare time, after work. If you find the application useful and you have a bit of money to spare, feel free to send me [a tip via PayPal][3].
[1]: https://github.com/nicoverbruggen/phpmon/releases
[2]: docs/ADDITIONAL.md
[3]: https://paypal.me/nicoverbruggen
If you'd like to create a production build, choose "Any Mac" as the target and select Product > Archive.

View File

@ -6,15 +6,11 @@ Generally speaking, only the latest version of **PHP Monitor** is supported:
| Version | Apple Silicon | Supported | Supported macOS | Deployment Target |
| ------- | ------------- | ------------------ | ----- | ----- |
| 2.6 | ✅ Universal binary, full support | ✅ | Big Sur (11.0) | macOS 10.14+ |
The following versions are no longer supported:
| Version | Apple Silicon | Supported | Supported macOS | Deployment Target |
| ------- | ------------- | ------------------ | ----- | ----- |
| 2.5 | ✴️ Universal binary<br/>`/usr/local/homebrew` installations only) | ❌ | Big Sur (11.0)<br/>Catalina (10.15) | macOS 10.14+ |
| 2.4 | ✴️ Universal binary<br/>`/usr/local/homebrew` installations only) | ❌ | Big Sur (11.0)<br/>Catalina (10.15) | macOS 10.14+ |
| < 2.4 | (Intel binary<br/>`/usr/local/homebrew` installations only) | ❌ | Catalina (10.15) | macOS 10.14+ |
| 3.0 | ✅ Universal binary | ✅ | Big Sur (11.0) | macOS 10.14+ |
| 2.6 | ✅ Universal binary | ❌ | Big Sur (11.0) | macOS 10.14+ |
| 2.5 | ✴️ Universal binary<br/>`/usr/local/homebrew` installations only | ❌ | Big Sur (11.0)<br/>Catalina (10.15) | macOS 10.14+ |
| 2.4 | ✴️ Universal binary<br/>`/usr/local/homebrew` installations only | ❌ | Big Sur (11.0)<br/>Catalina (10.15) | macOS 10.14+ |
| < 2.4 | Intel binary<br/>`/usr/local/homebrew` installations only | ❌ | Catalina (10.15) | macOS 10.14+ |
## Reporting a vulnerability

View File

@ -1,124 +0,0 @@
### Quick Setup
If you want to set up your computer for the very first time with PHP Monitor, here's how I do it:
Install [Homebrew](https://brew.sh) first.
Install PHP, composer, add to path:
brew install php
brew install composer
nano .zshrc
Make sure the following line is not in the comments:
# on an Intel Mac
export PATH=$HOME/bin:/usr/local/bin:$PATH
If you're on an Apple Silicon-based Mac, you'll need to add:
# on an M1 Mac
export PATH=$HOME/bin:/opt/homebrew/bin:$PATH
and add the following to your .zshrc:
export PATH=$HOME/bin:~/.composer/vendor/bin:$PATH
Make sure PHP is linked correctly:
which php
should return: `/usr/local/bin/php` (or `/opt/homebrew/bin/php`)
composer global require laravel/valet
valet install
This should install `dnsmasq` and set up Valet. Great, almost there!
valet trust
Finally, run PHP Monitor. Since the app is notarized and signed with a developer ID, it should work.
### FAQ
#### Q: Does this support Apple Silicon?
Yes. This is a universal app.
The following installation paths are supported:
* `/usr/local/homebrew` (default on Intel Macs)
* `/opt/homebrew` (default on Apple Silicon Macs)
#### Q: Is PHP 8.0 supported?
Yes.
#### Q: This app is doing network requests? Why?
It's Homebrew. I can't prevent `brew` from doing things via the network when I invoke it.
PHP Monitor itself doesn't do any network requests. Feel free to check the source code or intercept the traffic, if you don't believe me.
#### Q: I want PHP Monitor to start up when I boot my Mac!
You can do this by dragging *PHP Monitor.app* into the **Login Items** section in **System Preferences > Users & Groups** for your account.
Super convenient!
### Q: PHP Monitor says that the latest version of PHP is not installed, but it is!
Try installing again using `brew install php`.
This should resolve the issue! If that does not fix the issue, run `brew link php --force`. (Afterwards, you may need to restart your terminal to make sure the new linked version is detected.)
### Q: PHP Monitor says the correct version is loaded, but my Valet sites don't work!
Your sites aren't showing up, or you are seeing a 502? It's a common issue.
You may need to run `valet install`, preferably after updating `valet` by running `composer global update`.
#### Q: PHP Monitor reports another version compared to phpinfo on my local website, what is going on?
_Beginning with version 2.0 you'll get alerts about this at startup._
If you're still seeing another version of PHP in your scripts running on your local webserver (nginx) — e.g. when running `phpinfo()` — I recommend you shut down all PHP services that are currently active. You can find out what services are active by running:
sudo brew services list | grep php
This will present to you a list of services, like so (depending on the installed versions of PHP):
```
php started root /Library/LaunchDaemons/homebrew.mxcl.php.plist
php@5.6 stopped
php@7.0 stopped
php@7.1 stopped
php@7.2 stopped
php@7.3 stopped
```
You'll want to make sure that **only one service is running** and that it is running **as `root`**. You can terminate a service by running:
sudo brew services stop {service_name}
So in order to disable PHP 7.3, you'd need to run:
sudo brew services stop php@7.3
If you notice that PHP FPM is running as your own user account, you can turn off the service by running:
brew services stop php@7.3
The easiest way to make sure that PHP Monitor works again is to run the following commands:
sudo brew services stop php
sudo brew services stop php@7.3
sudo brew services stop php@7.2
sudo brew services stop php@7.1
sudo brew services stop php@7.0
sudo brew services stop php@5.6
sudo brew services stop nginx
Then, in PHP Monitor, select "Restart php-fpm service", which should start the service.
Alternatively, you can run `sudo brew services start php@7.4` where `7.4` is your preferred version of PHP (for the latest version of PHP, you may omit `@7.4` like in the example above).

View File

@ -2,7 +2,7 @@
1. Merge into `main`
2. Create tag
3. Add changes to changelog
3. Add changes to changelog + update security document
4. Archive
5. Notarize and prepare for own distribution
6. After notarization, export .app

Binary file not shown.

Before

Width:  |  Height:  |  Size: 162 KiB

After

Width:  |  Height:  |  Size: 127 KiB

View File

@ -2,8 +2,7 @@
// AppDelegate.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 11/06/2019.
// Copyright © 2019 Nico Verbruggen. All rights reserved.
// Copyright © 2021 Nico Verbruggen. All rights reserved.
//
import Cocoa

View File

@ -1,150 +0,0 @@
//
// Services.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 11/06/2019.
// Copyright © 2019 Nico Verbruggen. All rights reserved.
//
import Foundation
import AppKit
class Actions {
public static func detectPhpVersions() -> [String] {
let files = Shell.user.pipe("ls \(Paths.optPath()) | grep php@")
var versions = files.components(separatedBy: "\n")
// Remove all empty strings
versions.removeAll { (string) -> Bool in
return (string == "")
}
// Get a list of versions only
var versionsOnly : [String] = []
versions.forEach { (string) in
versionsOnly.append(string.components(separatedBy: "php@")[1])
}
// 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 = App.shared.brewPhpVersion
if (!versionsOnly.contains(phpAlias)) {
versionsOnly.append(phpAlias);
}
return versionsOnly
}
public static func restartPhpFpm() {
let version = App.shared.currentVersion!.short
if (version == App.shared.brewPhpVersion) {
Shell.user.run("sudo \(Paths.brew()) services restart php")
} else {
Shell.user.run("sudo \(Paths.brew()) services restart php@\(version)")
}
}
public static func restartNginx()
{
Shell.user.run("sudo \(Paths.brew()) services restart nginx")
}
public static func restartDnsMasq()
{
Shell.user.run("sudo \(Paths.brew()) services restart dnsmasq")
}
/**
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]) {
availableVersions.forEach { (available) in
let formula = (available == App.shared.brewPhpVersion) ? "php" : "php@\(available)"
Shell.user.run("\(Paths.brew()) unlink \(formula)")
Shell.user.run("sudo \(Paths.brew()) services stop \(formula)")
}
let formula = (version == App.shared.brewPhpVersion) ? "php" : "php@\(version)"
Shell.user.run("\(Paths.brew()) link \(formula) --overwrite --force")
Shell.user.run("sudo \(Paths.brew()) services start \(formula)")
}
public static func openGenericPhpConfigFolder() {
let files = [NSURL(fileURLWithPath: "\(Paths.etcPath())/php")];
NSWorkspace.shared.activateFileViewerSelecting(files 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 files = [NSURL(fileURLWithPath: NSString(string: "~/.config/valet").expandingTildeInPath)];
NSWorkspace.shared.activateFileViewerSelecting(files as [URL])
}
public static func didFindXdebug(_ version: String) -> Bool {
let command = """
grep -q 'zend_extension="xdebug.so"' \(Paths.etcPath())/php/\(version)/php.ini; [ $? -eq 0 ] && echo "YES" || echo "NO"
"""
let output = Shell.user.pipe(command).trimmingCharacters(in: .whitespacesAndNewlines)
return (output == "YES")
}
public static func didEnableXdebug(_ version: String) -> Bool {
let command = """
grep -q '; zend_extension="xdebug.so"' \(Paths.etcPath())/php/\(version)/php.ini; [ $? -eq 0 ] && echo "YES" || echo "NO"
"""
let output = Shell.user.pipe(command).trimmingCharacters(in: .whitespacesAndNewlines)
return (output == "NO")
}
public static func toggleXdebug() {
let version = App.shared.currentVersion?.short
var command = """
sed -i '' 's/; zend_extension="xdebug.so"/zend_extension="xdebug.so"/g' \(Paths.etcPath())/php/\(version!)/php.ini
"""
if (self.didEnableXdebug(version!)) {
command = """
sed -i '' 's/zend_extension="xdebug.so"/; zend_extension="xdebug.so"/g' \(Paths.etcPath())/php/\(version!)/php.ini
"""
}
Shell.user.run(command)
}
/**
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() {
Shell.user.run("sudo \(Paths.brew()) services stop dnsmasq")
Shell.user.run("sudo \(Paths.brew()) services start dnsmasq")
let versions = self.detectPhpVersions()
versions.forEach { (version) in
Shell.user.run("\(Paths.brew()) unlink php@\(version)")
if (version == App.shared.brewPhpVersion) {
Shell.user.run("\(Paths.brew()) services stop php")
Shell.user.run("sudo \(Paths.brew()) services stop php")
} else {
Shell.user.run("\(Paths.brew()) services stop php@\(version)")
Shell.user.run("sudo \(Paths.brew()) services stop php@\(version)")
}
}
Shell.user.run("\(Paths.brew()) services stop php")
Shell.user.run("\(Paths.brew()) services stop nginx")
Shell.user.run("\(Paths.brew()) link php")
Shell.user.run("sudo \(Paths.brew()) services restart dnsmasq")
Shell.user.run("sudo \(Paths.brew()) services restart php")
Shell.user.run("sudo \(Paths.brew()) services restart nginx")
}
}

View File

@ -1,51 +0,0 @@
//
// PhpVersionExtractor.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 11/06/2019.
// Copyright © 2019 Nico Verbruggen. All rights reserved.
//
import Foundation
class PhpVersion {
var short : String = "???"
var long : String = "???"
var xdebugFound: Bool = false
var xdebugEnabled : Bool = false
var error : Bool = false
init() {
let version = Command.execute(
path: Paths.php(),
arguments: ["-r", "print phpversion();"]
)
if (version == "" || version.contains("Warning")) {
self.short = "💩 BROKEN"
self.long = "";
self.error = true
return;
}
// That's the long version
self.long = version
// Next up, let's strip away the minor version number
let segments = long.components(separatedBy: ".")
// Get the first two elements
self.short = segments[0...1].joined(separator: ".")
// Load xdebug support
self.xdebugFound = Actions.didFindXdebug(self.short)
if (self.xdebugFound) {
self.xdebugEnabled = Actions.didEnableXdebug(self.short)
}
self.error = false;
}
}

View File

@ -1,15 +0,0 @@
//
// PhpMenuItem.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 13/12/2020.
// Copyright © 2020 Nico Verbruggen. All rights reserved.
//
import Cocoa
class PhpMenuItem: NSMenuItem {
var version: String = ""
}

View File

@ -1,97 +0,0 @@
//
// MainMenuBuilder.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 16/05/2020.
// Copyright © 2020 Nico Verbruggen. All rights reserved.
//
import Cocoa
class StatusMenu : NSMenu {
public func addPhpVersionMenuItems()
{
var string = "mi_unsure".localized
if (App.shared.currentVersion != nil) {
if (!App.shared.currentVersion!.error) {
// in case the php version loaded without issue
string = "\("mi_php_version".localized) \(App.shared.currentVersion!.long)"
self.addItem(NSMenuItem(title: string, action: nil, keyEquivalent: ""))
} else {
// in case of an error show the error message
["mi_php_broken_1", "mi_php_broken_2",
"mi_php_broken_3", "mi_php_broken_4"].forEach { (message) in
self.addItem(NSMenuItem(title: message.localized, action: nil, keyEquivalent: ""))
}
}
}
}
public func addPhpActionMenuItems()
{
if (App.shared.availablePhpVersions.count > 0 && !App.shared.busy) {
var shortcutKey = 1
for index in (0..<App.shared.availablePhpVersions.count).reversed() {
let version = App.shared.availablePhpVersions[index]
let action = #selector(MainMenu.switchToPhpVersion(sender:))
let brew = (version == App.shared.brewPhpVersion) ? "php" : "php@\(version)"
let menuItem = PhpMenuItem(title: "\("mi_php_switch".localized) \(version) (\(brew))", action: (version == App.shared.currentVersion?.short) ? nil : action, keyEquivalent: "\(shortcutKey)")
menuItem.version = version
shortcutKey = shortcutKey + 1
self.addItem(menuItem)
}
self.addItem(NSMenuItem.separator())
self.addItem(NSMenuItem(title: "mi_active_services".localized, action: nil, keyEquivalent: ""))
self.addItem(NSMenuItem(title: "mi_restart_dnsmasq".localized, action: #selector(MainMenu.restartDnsMasq), keyEquivalent: "d"))
self.addItem(NSMenuItem(title: "mi_restart_php_fpm".localized, action: #selector(MainMenu.restartPhpFpm), keyEquivalent: "p"))
self.addItem(NSMenuItem(title: "mi_restart_nginx".localized, action: #selector(MainMenu.restartNginx), keyEquivalent: "n"))
self.addItem(NSMenuItem(title: "mi_restart_all_services".localized, action: #selector(MainMenu.restartAllServices), keyEquivalent: "s"))
self.addItem(NSMenuItem.separator())
self.addItem(NSMenuItem(title: "mi_diagnostics".localized, action: nil, keyEquivalent: ""))
self.addItem(NSMenuItem(title: "mi_force_load_latest".localized, action: #selector(MainMenu.forceRestartLatestPhp), keyEquivalent: "f"))
}
if (App.shared.busy) {
self.addItem(NSMenuItem(title: "mi_busy".localized, action: nil, keyEquivalent: ""))
}
}
public func addPhpConfigurationMenuItems()
{
if (App.shared.currentVersion != nil) {
self.addItem(NSMenuItem(title: "mi_configuration".localized, action: nil, keyEquivalent: ""))
self.addItem(NSMenuItem(title: "mi_valet_config".localized, action: #selector(MainMenu.openValetConfigFolder), keyEquivalent: "v"))
self.addItem(NSMenuItem(title: "mi_php_config".localized, action: #selector(MainMenu.openActiveConfigFolder), keyEquivalent: "c"))
self.addItem(NSMenuItem(title: "mi_phpinfo".localized, action: #selector(MainMenu.openPhpInfo), keyEquivalent: "i"))
self.addItem(NSMenuItem.separator())
self.addItem(NSMenuItem(title: "mi_enabled_extensions".localized, action: nil, keyEquivalent: ""))
self.addXdebugMenuItem()
}
}
private func addXdebugMenuItem()
{
let xdebugFound = App.shared.currentVersion!.xdebugFound
if (xdebugFound) {
let xdebugOn = App.shared.currentVersion!.xdebugEnabled
let xdebugToggleMenuItem = NSMenuItem(
title: "mi_xdebug".localized,
action: #selector(MainMenu.toggleXdebug), keyEquivalent: "x"
)
if (xdebugOn) {
xdebugToggleMenuItem.state = .on
}
self.addItem(xdebugToggleMenuItem)
} else {
let disabledItem = NSMenuItem(
title: "mi_xdebug_missing".localized,
action: nil, keyEquivalent: "x"
)
disabledItem.isEnabled = false
self.addItem(disabledItem)
}
}
}

View File

@ -2,8 +2,7 @@
// Constants.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 08/07/2019.
// Copyright © 2019 Nico Verbruggen. All rights reserved.
// Copyright © 2021 Nico Verbruggen. All rights reserved.
//
import Cocoa

View File

@ -0,0 +1,157 @@
//
// Services.swift
// PHP Monitor
//
// Copyright © 2021 Nico Verbruggen. All rights reserved.
//
import Foundation
import AppKit
class Actions {
// MARK: - Detect PHP Versions
public static func detectPhpVersions() -> [String]
{
let files = Shell.pipe("ls \(Paths.optPath) | grep php@")
var versions = files.components(separatedBy: "\n")
// Remove all empty strings
versions.removeAll { (string) -> Bool in
return (string == "")
}
// Get a list of versions only
var versionsOnly : [String] = []
versions.forEach { (string) in
versionsOnly.append(string.components(separatedBy: "php@")[1])
}
// 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 = App.shared.brewPhpVersion
if (!versionsOnly.contains(phpAlias)) {
versionsOnly.append(phpAlias);
}
return versionsOnly
}
// MARK: - Services
public static func restartPhpFpm()
{
brew("services restart \(App.phpInstall!.formula)", sudo: true)
}
public static func restartNginx()
{
brew("services restart nginx", sudo: true)
}
public static func restartDnsMasq()
{
brew("services restart dnsmasq", sudo: true)
}
/**
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])
{
availableVersions.forEach { (available) in
let formula = (available == App.shared.brewPhpVersion) ? "php" : "php@\(available)"
brew("unlink \(formula)")
brew("services stop \(formula)", sudo: true)
}
let formula = (version == App.shared.brewPhpVersion) ? "php" : "php@\(version)"
brew("link \(formula) --overwrite --force")
brew("services start \(formula)", sudo: true)
}
// MARK: - Finding Config Files
public static func openGenericPhpConfigFolder()
{
let files = [NSURL(fileURLWithPath: "\(Paths.etcPath)/php")];
NSWorkspace.shared.activateFileViewerSelecting(files 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 files = [NSURL(fileURLWithPath: NSString(string: "~/.config/valet").expandingTildeInPath)];
NSWorkspace.shared.activateFileViewerSelecting(files 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)
self.detectPhpVersions().forEach { (version) in
let formula = (version == App.shared.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 `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)
{
Shell.run("sed -i '' 's/\(original)/\(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")
}
}

View File

@ -2,8 +2,7 @@
// Environment.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 12/06/2019.
// Copyright © 2019 Nico Verbruggen. All rights reserved.
// Copyright © 2021 Nico Verbruggen. All rights reserved.
//
import Foundation
@ -20,46 +19,46 @@ class Startup {
- Parameter success: Callback that is fired if the application can proceed with launch
- Parameter failure: Callback that is fired if the application must retry launch
*/
public func checkEnvironment(success: () -> Void, failure: @escaping () -> Void)
func checkEnvironment(success: () -> Void, failure: @escaping () -> Void)
{
self.failureCallback = failure
self.performEnvironmentCheck(
!Shell.fileExists("\(Paths.binPath())/php"),
!Shell.fileExists("\(Paths.binPath)/php"),
messageText: "startup.errors.php_binary.title".localized,
informativeText: "startup.errors.php_binary_desc".localized,
breaking: true
)
self.performEnvironmentCheck(
!Shell.user.pipe("ls \(Paths.optPath()) | grep php").contains("php"),
!Shell.pipe("ls \(Paths.optPath) | grep php").contains("php"),
messageText: "startup.errors.php_opt.title".localized,
informativeText: "startup.errors.php_opt.desc".localized,
breaking: true
)
self.performEnvironmentCheck(
!Shell.user.pipe("which valet").contains("/usr/local/bin/valet"),
!Shell.pipe("which valet").contains("/usr/local/bin/valet"),
messageText: "startup.errors.valet_executable.title".localized,
informativeText: "startup.errors.valet_executable.desc".localized,
breaking: true
)
self.performEnvironmentCheck(
!Shell.user.pipe("cat /private/etc/sudoers.d/brew").contains("\(Paths.binPath())/brew"),
!Shell.pipe("cat /private/etc/sudoers.d/brew").contains("\(Paths.binPath)/brew"),
messageText: "startup.errors.sudoers_brew.title".localized,
informativeText: "startup.errors.sudoers_brew.desc".localized,
breaking: true
)
self.performEnvironmentCheck(
!Shell.user.pipe("cat /private/etc/sudoers.d/valet").contains("/usr/local/bin/valet"),
!Shell.pipe("cat /private/etc/sudoers.d/valet").contains("/usr/local/bin/valet"),
messageText: "startup.errors.sudoers_valet.title".localized,
informativeText: "startup.errors.sudoers_valet.desc".localized,
breaking: true
)
let services = Shell.user.pipe("\(Paths.brew()) services list | grep php")
let services = Shell.pipe("\(Paths.brew) services list | grep php")
self.performEnvironmentCheck(
(services.countInstances(of: "started") > 1),
messageText: "startup.errors.services.title".localized,
@ -82,7 +81,7 @@ class Startup {
print("PHP Monitor has determined the application has successfully passed all checks.")
print("Determining which version of PHP is aliased to `php` via Homebrew...")
let brewPhpAlias = Shell.user.pipe("\(Paths.brew()) info php --json");
let brewPhpAlias = Shell.pipe("\(Paths.brew) info php --json");
App.shared.brewPhpPackage = try! JSONDecoder().decode(
[HomebrewPackage].self,
@ -105,24 +104,16 @@ class Startup {
messageText: String,
informativeText: String,
breaking: Bool
)
{
if (condition) {
// Only breaking issues will cause the notification
if (breaking) {
self.failed = true
}
DispatchQueue.main.async {
// Present the information to the user
_ = Alert.present(
messageText: messageText,
informativeText: informativeText
)
// Only breaking issues will throw the extra retry modal
if (breaking) {
self.failureCallback()
}
}
) {
if (!condition) { return }
self.failed = breaking
DispatchQueue.main.async {
// Present the information to the user
Alert.notify(message: messageText, info: informativeText)
// Only breaking issues will throw the extra retry modal
breaking ? self.failureCallback() : ()
}
}
}

View File

@ -2,8 +2,7 @@
// HomebrewPackage.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 26/11/2020.
// Copyright © 2020 Nico Verbruggen. All rights reserved.
// Copyright © 2021 Nico Verbruggen. All rights reserved.
//
import Foundation
@ -13,7 +12,7 @@ struct HomebrewPackage : Decodable {
let full_name: String
let aliases: [String]
public func getVersion() -> String {
public var version: String {
return aliases.first!.replacingOccurrences(of: "php@", with: "")
}
}

View File

@ -0,0 +1,93 @@
//
// 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
/**
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=|; extension=|; zend_extension=)"(?<name>[a-zA-Z]*).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
self.name = line[range]
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() {
Actions.sed(
file: self.file,
original: self.line,
replacement: self.enabled ? "; \(self.line)" : self.line.replacingOccurrences(of: "; ", with: "")
)
self.enabled = !self.enabled
}
// 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({ (line) -> Bool in
return line.range(of: Self.extensionRegex, options: .regularExpression) != nil
})
.map { (line) -> PhpExtension in
return PhpExtension(line, file: path.path)
}
}
}

View File

@ -0,0 +1,114 @@
//
// PhpInstall.swift
// PHP Monitor
//
// Copyright © 2021 Nico Verbruggen. All rights reserved.
//
import Foundation
class PhpInstall {
var version: Version
var configuration: Configuration
var extensions: [PhpExtension]
// MARK: - Computed
var formula: String {
return (self.version.short == App.shared.brewPhpVersion) ? "php" : "php@\(self.version.short)"
}
// MARK: - Initializer
init() {
// Show information about the current version
self.version = Self.getVersion()
// If an error occurred, exit early
if (self.version.error) {
self.configuration = Configuration()
self.extensions = []
return
}
// Load extension information
let path = URL(fileURLWithPath: "\(Paths.etcPath)/php/\(self.version.short)/php.ini")
self.extensions = PhpExtension.load(from: path)
// Get configuration values
self.configuration = Configuration(
memory_limit: Self.getByteCount(key: "memory_limit"),
upload_max_filesize: Self.getByteCount(key: "upload_max_filesize"),
post_max_size: Self.getByteCount(key: "post_max_size")
)
}
/**
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 static func getVersion() -> Version {
var versionStruct = Version()
let version = Command.execute(path: Paths.php, arguments: ["-r", "print phpversion();"])
if (version == "" || version.contains("Warning") || version.contains("Error")) {
versionStruct.short = "💩 BROKEN"
versionStruct.long = "";
versionStruct.error = true
return versionStruct;
}
// That's the long version
versionStruct.long = version
// Next up, let's strip away the minor version number
let segments = versionStruct.long.components(separatedBy: ".")
// Get the first two elements
versionStruct.short = segments[0...1].joined(separator: ".")
return versionStruct
}
/**
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 static 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"
}
// MARK: - Structs
struct Version {
var short = "???"
var long = "???"
var error = false
}
struct Configuration {
var memory_limit = "???"
var upload_max_filesize = "???"
var post_max_size = "???"
}
}

View File

@ -2,8 +2,7 @@
// Date.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 09/07/2019.
// Copyright © 2019 Nico Verbruggen. All rights reserved.
// Copyright © 2021 Nico Verbruggen. All rights reserved.
//
import Cocoa

View File

@ -2,10 +2,8 @@
// StringExtension.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 13/05/2020.
// Copyright © 2020 Nico Verbruggen. All rights reserved.
// Copyright © 2021 Nico Verbruggen. All rights reserved.
//
import Foundation
extension String {
@ -29,4 +27,10 @@ extension String {
return count
}
subscript (r: Range<String.Index>) -> String {
let start = r.lowerBound
let end = r.upperBound
return String(self[start ..< end])
}
}

View File

@ -0,0 +1,33 @@
//
// NibLoadable.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 04/02/2021.
// Copyright © 2021 Nico Verbruggen. All rights reserved.
//
import Foundation
import Cocoa
// Adapted from: https://stackoverflow.com/a/46268778
protocol XibLoadable {
static var xibName: String? { get }
static func createFromXib(in bundle: Bundle) -> Self?
}
extension XibLoadable where Self: NSView {
static var xibName: String? {
return String(describing: Self.self)
}
static func createFromXib(in bundle: Bundle = Bundle.main) -> Self? {
guard let xibName = xibName else { return nil }
var topLevelArray: NSArray? = nil
bundle.loadNibNamed(NSNib.Name(xibName), owner: self, topLevelObjects: &topLevelArray)
guard let results = topLevelArray else { return nil }
let views = Array<Any>(results).filter { $0 is Self }
return views.last as? Self
}
}

View File

@ -2,13 +2,13 @@
// Alert.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 12/06/2019.
// Copyright © 2019 Nico Verbruggen. All rights reserved.
// Copyright © 2021 Nico Verbruggen. All rights reserved.
//
import Cocoa
class Alert {
public static func present(
messageText: String,
informativeText: String,
@ -24,4 +24,9 @@ class Alert {
}
return alert.runModal() == .alertFirstButtonReturn
}
public static func notify(message: String, info: String) {
_ = self.present(messageText: message, informativeText: info, buttonTitle: "OK", secondButtonTitle: "")
}
}

View File

@ -2,18 +2,18 @@
// LocalNotification.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 16/07/2020.
// Copyright © 2020 Nico Verbruggen. All rights reserved.
// Copyright © 2021 Nico Verbruggen. All rights reserved.
//
import Foundation
class LocalNotification {
public static func send(title: String, subtitle: String)
{
public static func send(title: String, subtitle: String) {
let notification = NSUserNotification()
notification.title = title
notification.subtitle = subtitle
NSUserNotificationCenter.default.deliver(notification)
}
}

View File

@ -2,8 +2,7 @@
// ImageGenerator.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 11/06/2019.
// Copyright © 2019 Nico Verbruggen. All rights reserved.
// Copyright © 2021 Nico Verbruggen. All rights reserved.
//
import Cocoa

View File

@ -0,0 +1,23 @@
//
// HeaderView.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 04/02/2021.
// Copyright © 2021 Nico Verbruggen. All rights reserved.
//
import Foundation
import Cocoa
class HeaderView: NSView, XibLoadable {
@IBOutlet weak var textField: NSTextField!
static func asMenuItem(text: String) -> NSMenuItem {
let view = Self.createFromXib()
view!.textField.stringValue = text.uppercased()
let item = NSMenuItem()
item.view = view
item.target = self
return item
}
}

View File

@ -0,0 +1,35 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="17701" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
<dependencies>
<deployment identifier="macosx"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="17701"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
<customObject id="-2" userLabel="File's Owner"/>
<customObject id="-1" userLabel="First Responder" customClass="FirstResponder"/>
<customObject id="-3" userLabel="Application" customClass="NSObject"/>
<customView id="c22-O7-iKe" customClass="HeaderView" customModule="PHP_Monitor" customModuleProvider="target">
<rect key="frame" x="0.0" y="0.0" width="350" height="24"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<subviews>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="ddg-VQ-cOT">
<rect key="frame" x="12" y="5" width="113" height="15"/>
<textFieldCell key="cell" lineBreakMode="clipping" alignment="left" title="ACTIVE SERVICES" id="NHz-MZ-8FK">
<font key="font" metaFont="systemBold" size="12"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
</subviews>
<constraints>
<constraint firstItem="ddg-VQ-cOT" firstAttribute="centerY" secondItem="c22-O7-iKe" secondAttribute="centerY" id="n4Z-WN-RIh"/>
<constraint firstItem="ddg-VQ-cOT" firstAttribute="leading" secondItem="c22-O7-iKe" secondAttribute="leading" constant="14" id="yuW-pb-GQJ"/>
</constraints>
<connections>
<outlet property="textField" destination="ddg-VQ-cOT" id="aaQ-Xb-o2X"/>
</connections>
<point key="canvasLocation" x="-75" y="38"/>
</customView>
</objects>
</document>

View File

@ -0,0 +1,34 @@
//
// StatsView.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 04/02/2021.
// Copyright © 2021 Nico Verbruggen. All rights reserved.
//
import Foundation
import Cocoa
class StatsView: NSView, XibLoadable {
@IBOutlet weak var titleMemLimit: NSTextField!
@IBOutlet weak var titleMaxPost: NSTextField!
@IBOutlet weak var titleMaxUpload: NSTextField!
@IBOutlet weak var labelMemLimit: NSTextField!
@IBOutlet weak var labelMaxPost: NSTextField!
@IBOutlet weak var labelMaxUpload: NSTextField!
static func asMenuItem(memory: String, post: String, upload: String) -> NSMenuItem {
let view = Self.createFromXib()
view!.titleMemLimit.stringValue = "mi_memory_limit".localized.uppercased()
view!.titleMaxPost.stringValue = "mi_post_max_size".localized.uppercased()
view!.titleMaxUpload.stringValue = "mi_upload_max_filesize".localized.uppercased()
view!.labelMemLimit.stringValue = memory
view!.labelMaxPost.stringValue = post
view!.labelMaxUpload.stringValue = upload
let item = NSMenuItem()
item.view = view
item.target = self
return item
}
}

View File

@ -0,0 +1,144 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="17701" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
<dependencies>
<deployment identifier="macosx"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="17701"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
<customObject id="-2" userLabel="File's Owner"/>
<customObject id="-1" userLabel="First Responder" customClass="FirstResponder"/>
<customObject id="-3" userLabel="Application" customClass="NSObject"/>
<customView id="c22-O7-iKe" customClass="StatsView" customModule="PHP_Monitor" customModuleProvider="target">
<rect key="frame" x="0.0" y="0.0" width="341" height="55"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<subviews>
<stackView distribution="fillEqually" orientation="horizontal" alignment="top" spacing="20" horizontalStackHuggingPriority="249.99998474121094" verticalStackHuggingPriority="249.99998474121094" detachesHiddenViews="YES" translatesAutoresizingMaskIntoConstraints="NO" id="TnH-dX-qaQ">
<rect key="frame" x="30" y="6" width="281" height="43"/>
<subviews>
<stackView distribution="fill" orientation="vertical" alignment="centerX" spacing="2" horizontalStackHuggingPriority="249.99998474121094" verticalStackHuggingPriority="249.99998474121094" detachesHiddenViews="YES" translatesAutoresizingMaskIntoConstraints="NO" id="doH-ww-BDw">
<rect key="frame" x="0.0" y="4" width="87" height="35"/>
<subviews>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="At1-ch-qv2">
<rect key="frame" x="-2" y="21" width="91" height="14"/>
<textFieldCell key="cell" lineBreakMode="clipping" alignment="center" title="MEMORY LIMIT" id="LKe-C4-jxo">
<font key="font" metaFont="systemMedium" size="11"/>
<color key="textColor" name="secondaryLabelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="Emt-m3-Dt6">
<rect key="frame" x="16" y="0.0" width="55" height="19"/>
<textFieldCell key="cell" lineBreakMode="clipping" title="1024M" id="H6T-wY-PIG">
<font key="font" metaFont="systemMedium" size="16"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
</subviews>
<visibilityPriorities>
<integer value="1000"/>
<integer value="1000"/>
</visibilityPriorities>
<customSpacing>
<real value="3.4028234663852886e+38"/>
<real value="3.4028234663852886e+38"/>
</customSpacing>
</stackView>
<stackView distribution="fillEqually" orientation="vertical" alignment="centerX" spacing="2" horizontalStackHuggingPriority="249.99998474121094" verticalStackHuggingPriority="249.99998474121094" detachesHiddenViews="YES" translatesAutoresizingMaskIntoConstraints="NO" id="g4d-4N-NkC">
<rect key="frame" x="107" y="4" width="77" height="35"/>
<subviews>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="7um-XA-djV">
<rect key="frame" x="7" y="21" width="63" height="14"/>
<textFieldCell key="cell" lineBreakMode="clipping" title="MAX POST" id="Qfq-Bl-yuh">
<font key="font" metaFont="systemMedium" size="11"/>
<color key="textColor" name="secondaryLabelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="Vyu-AO-8SH">
<rect key="frame" x="11" y="0.0" width="55" height="19"/>
<textFieldCell key="cell" lineBreakMode="clipping" title="1024M" id="uH4-Zy-43x">
<font key="font" metaFont="systemMedium" size="16"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
</subviews>
<visibilityPriorities>
<integer value="1000"/>
<integer value="1000"/>
</visibilityPriorities>
<customSpacing>
<real value="3.4028234663852886e+38"/>
<real value="3.4028234663852886e+38"/>
</customSpacing>
</stackView>
<stackView distribution="fill" orientation="vertical" alignment="centerX" spacing="2" horizontalStackHuggingPriority="249.99998474121094" verticalStackHuggingPriority="249.99998474121094" detachesHiddenViews="YES" translatesAutoresizingMaskIntoConstraints="NO" id="nWj-33-m8Q">
<rect key="frame" x="204" y="4" width="77" height="35"/>
<subviews>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="Oef-6n-9QI">
<rect key="frame" x="-1" y="21" width="79" height="14"/>
<textFieldCell key="cell" lineBreakMode="clipping" title="MAX UPLOAD" id="lGh-MT-TgI">
<font key="font" metaFont="systemMedium" size="11"/>
<color key="textColor" name="secondaryLabelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="eHT-tr-Kwx">
<rect key="frame" x="11" y="0.0" width="55" height="19"/>
<textFieldCell key="cell" lineBreakMode="clipping" title="1024M" id="1iA-Ri-zYY">
<font key="font" metaFont="systemMedium" size="16"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
</subviews>
<visibilityPriorities>
<integer value="1000"/>
<integer value="1000"/>
</visibilityPriorities>
<customSpacing>
<real value="3.4028234663852886e+38"/>
<real value="3.4028234663852886e+38"/>
</customSpacing>
</stackView>
</subviews>
<constraints>
<constraint firstItem="nWj-33-m8Q" firstAttribute="top" secondItem="TnH-dX-qaQ" secondAttribute="top" constant="4" id="CAY-Pw-B8n"/>
<constraint firstAttribute="bottom" secondItem="doH-ww-BDw" secondAttribute="bottom" constant="4" id="Dq4-M6-1Wf"/>
<constraint firstItem="g4d-4N-NkC" firstAttribute="top" secondItem="TnH-dX-qaQ" secondAttribute="top" constant="4" id="bls-fM-H4b"/>
<constraint firstAttribute="bottom" secondItem="nWj-33-m8Q" secondAttribute="bottom" constant="4" id="f6j-eI-wiH"/>
<constraint firstAttribute="bottom" secondItem="g4d-4N-NkC" secondAttribute="bottom" constant="4" id="faS-Mo-Qa2"/>
<constraint firstItem="doH-ww-BDw" firstAttribute="top" secondItem="TnH-dX-qaQ" secondAttribute="top" constant="4" id="gL3-5S-OKo"/>
</constraints>
<visibilityPriorities>
<integer value="1000"/>
<integer value="1000"/>
<integer value="1000"/>
</visibilityPriorities>
<customSpacing>
<real value="3.4028234663852886e+38"/>
<real value="3.4028234663852886e+38"/>
<real value="3.4028234663852886e+38"/>
</customSpacing>
</stackView>
</subviews>
<constraints>
<constraint firstItem="TnH-dX-qaQ" firstAttribute="top" secondItem="c22-O7-iKe" secondAttribute="top" constant="6" id="1mo-iG-Z0D"/>
<constraint firstAttribute="trailing" secondItem="TnH-dX-qaQ" secondAttribute="trailing" constant="30" id="3dD-wf-5pS"/>
<constraint firstItem="TnH-dX-qaQ" firstAttribute="leading" secondItem="c22-O7-iKe" secondAttribute="leading" constant="30" id="S8i-CD-j3h"/>
<constraint firstAttribute="bottom" secondItem="TnH-dX-qaQ" secondAttribute="bottom" constant="6" id="eve-qD-gUH"/>
</constraints>
<connections>
<outlet property="labelMaxPost" destination="Vyu-AO-8SH" id="5Cm-QO-hJQ"/>
<outlet property="labelMaxUpload" destination="eHT-tr-Kwx" id="5pK-FD-c4h"/>
<outlet property="labelMemLimit" destination="Emt-m3-Dt6" id="6nD-Su-XZ6"/>
<outlet property="titleMaxPost" destination="7um-XA-djV" id="5MN-Xb-XwL"/>
<outlet property="titleMaxUpload" destination="Oef-6n-9QI" id="Q61-JI-RJq"/>
<outlet property="titleMemLimit" destination="At1-ch-qv2" id="SQT-B9-sWS"/>
</connections>
<point key="canvasLocation" x="-84.5" y="44"/>
</customView>
</objects>
</document>

View File

@ -0,0 +1,133 @@
//
// MainMenuBuilder.swift
// PHP Monitor
//
// Copyright © 2021 Nico Verbruggen. All rights reserved.
//
import Cocoa
class StatusMenu : NSMenu {
func addPhpVersionMenuItems() {
if App.shared.currentInstall == nil {
return
}
if App.phpInstall!.version.error {
for message in ["mi_php_broken_1", "mi_php_broken_2", "mi_php_broken_3", "mi_php_broken_4"] {
self.addItem(NSMenuItem(title: message.localized, action: nil, keyEquivalent: ""))
}
return
}
let phpVersionText = "\("mi_php_version".localized) \(App.phpInstall!.version.long)"
self.addItem(HeaderView.asMenuItem(text: phpVersionText))
}
func addPhpActionMenuItems() {
if App.busy {
self.addItem(NSMenuItem(title: "mi_busy".localized, action: nil, keyEquivalent: ""))
return
}
if App.shared.availablePhpVersions.count == 0 {
return
}
self.addSwitchToPhpMenuItems()
self.addItem(NSMenuItem.separator())
self.addServicesMenuItems()
}
private func addSwitchToPhpMenuItems() {
var shortcutKey = 1
for index in (0..<App.shared.availablePhpVersions.count).reversed() {
let version = App.shared.availablePhpVersions[index]
let action = #selector(MainMenu.switchToPhpVersion(sender:))
let brew = (version == App.shared.brewPhpVersion) ? "php" : "php@\(version)"
let menuItem = PhpMenuItem(
title: "\("mi_php_switch".localized) \(version) (\(brew))",
action: (version == App.phpInstall?.version.short) ? nil : action, keyEquivalent: "\(shortcutKey)"
)
menuItem.version = version
shortcutKey = shortcutKey + 1
self.addItem(menuItem)
}
}
private func addServicesMenuItems() {
self.addItem(HeaderView.asMenuItem(text: "mi_active_services".localized))
let services = NSMenuItem(title: "mi_restart_specific".localized, action: nil, keyEquivalent: "")
let servicesMenu = NSMenu()
servicesMenu.addItem(NSMenuItem(title: "mi_restart_dnsmasq".localized, action: #selector(MainMenu.restartDnsMasq), keyEquivalent: "d"))
servicesMenu.addItem(NSMenuItem(title: "mi_restart_php_fpm".localized, action: #selector(MainMenu.restartPhpFpm), keyEquivalent: "p"))
servicesMenu.addItem(NSMenuItem(title: "mi_restart_nginx".localized, action: #selector(MainMenu.restartNginx), keyEquivalent: "n"))
for item in servicesMenu.items {
item.target = MainMenu.shared
}
self.setSubmenu(servicesMenu, for: services)
self.addItem(NSMenuItem(title: "mi_force_load_latest".localized, action: #selector(MainMenu.forceRestartLatestPhp), keyEquivalent: "f"))
self.addItem(services)
self.addItem(NSMenuItem(title: "mi_restart_all_services".localized, action: #selector(MainMenu.restartAllServices), keyEquivalent: "s"))
}
func addPhpConfigurationMenuItems() {
if App.shared.currentInstall == nil {
return
}
// Configuration
self.addItem(HeaderView.asMenuItem(text: "mi_configuration".localized))
self.addItem(NSMenuItem(title: "mi_valet_config".localized, action: #selector(MainMenu.openValetConfigFolder), keyEquivalent: "v"))
self.addItem(NSMenuItem(title: "mi_php_config".localized, action: #selector(MainMenu.openActiveConfigFolder), keyEquivalent: "c"))
self.addItem(NSMenuItem(title: "mi_phpinfo".localized, action: #selector(MainMenu.openPhpInfo), keyEquivalent: "i"))
if (App.shared.busy) {
return
}
let stats = App.phpInstall!.configuration
// Stats
self.addItem(NSMenuItem.separator())
self.addItem(StatsView.asMenuItem(
memory: stats.memory_limit,
post: stats.post_max_size,
upload: stats.upload_max_filesize)
)
// Extensions
self.addItem(NSMenuItem.separator())
self.addItem(HeaderView.asMenuItem(text: "mi_detected_extensions".localized))
if (App.phpInstall!.extensions.count == 0) {
self.addItem(NSMenuItem(title: "mi_no_extensions_detected".localized, action: nil, keyEquivalent: ""))
}
for phpExtension in App.phpInstall!.extensions {
self.addExtensionItem(phpExtension)
}
}
private func addExtensionItem(_ phpExtension: PhpExtension) {
let menuItem = ExtensionMenuItem(
title: "\(phpExtension.name.capitalized) (php.ini)",
action: #selector(MainMenu.toggleExtension), keyEquivalent: ""
)
menuItem.state = phpExtension.enabled ? .on : .off
menuItem.phpExtension = phpExtension
self.addItem(menuItem)
}
}
// MARK: - In order to store extra data in each item, NSMenuItem is subclassed
class PhpMenuItem: NSMenuItem {
var version: String = ""
}
class ExtensionMenuItem: NSMenuItem {
var phpExtension: PhpExtension? = nil
}

View File

@ -2,25 +2,31 @@
// StateManager.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 11/07/2019.
// Copyright © 2019 Nico Verbruggen. All rights reserved.
// Copyright © 2021 Nico Verbruggen. All rights reserved.
//
import Cocoa
class App {
static let shared = App()
static var phpInstall: PhpInstall? {
return App.shared.currentInstall
}
static var busy: Bool {
return App.shared.busy
}
/**
Whether the application is busy switching versions.
*/
var busy: Bool = false
/**
The currently active version of PHP.
The currently active installation of PHP.
*/
var currentVersion: PhpVersion? = nil
var currentInstall: PhpInstall? = nil
/**
All available versions of PHP.
@ -37,7 +43,7 @@ class App {
*/
var brewPhpPackage: HomebrewPackage? = nil {
didSet {
self.brewPhpVersion = self.brewPhpPackage!.getVersion()
self.brewPhpVersion = self.brewPhpPackage!.version
}
}

View File

@ -2,8 +2,7 @@
// Command.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 17/10/2019.
// Copyright © 2019 Nico Verbruggen. All rights reserved.
// Copyright © 2021 Nico Verbruggen. All rights reserved.
//
import Cocoa

View File

@ -2,8 +2,7 @@
// MainMenu.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 11/07/2019.
// Copyright © 2019 Nico Verbruggen. All rights reserved.
// Copyright © 2021 Nico Verbruggen. All rights reserved.
//
import Cocoa
@ -24,16 +23,14 @@ class MainMenu: NSObject, NSWindowDelegate {
/**
Kick off the startup of the rendering of the main menu.
*/
public func startup() {
func startup() {
// Start with the icon
self.setStatusBar(image: NSImage(named: NSImage.Name("StatusBarIcon"))!)
// Perform environment boot checks
DispatchQueue.global(qos: .userInitiated).async { [unowned self] in
Startup().checkEnvironment(success: {
self.onEnvironmentPass()
}, failure: {
self.onEnvironmentFail()
})
Startup().checkEnvironment(success: { self.onEnvironmentPass() },
failure: { self.onEnvironmentFail() }
)
}
}
@ -66,19 +63,21 @@ class MainMenu: NSObject, NSWindowDelegate {
buttonTitle: "alert.cannot_start.close".localized,
secondButtonTitle: "alert.cannot_start.retry".localized
)
if (!close) {
self.startup()
} else {
if (close) {
exit(1)
}
self.startup()
}
}
/**
Update the menu's contents, based on what's going on.
*/
public func update() {
DispatchQueue.global(qos: .userInitiated).async { [unowned self] in
func update() {
// Update the menu item on the main thread
DispatchQueue.main.async {
// Create a new menu
let menu = StatusMenu()
@ -103,10 +102,7 @@ class MainMenu: NSObject, NSWindowDelegate {
item.target = self
})
// Update the menu item on the main thread
DispatchQueue.main.async {
self.statusItem.menu = menu
}
self.statusItem.menu = menu
}
}
@ -137,7 +133,7 @@ class MainMenu: NSObject, NSWindowDelegate {
while updating the UI as required. As long as the completion callback
does not fire, the app is presumed to be busy and the UI reflects this.
- Parameter execute: Escaping callback of the work that needs to happen.
- Parameter execute: Callback of the work that needs to happen.
- Parameter completion: Callback that is fired when the work is done.
*/
private func waitAndExecute(_ execute: @escaping () -> Void, _ completion: @escaping () -> Void = {})
@ -159,16 +155,16 @@ class MainMenu: NSObject, NSWindowDelegate {
// MARK: - User Interface
@objc func updatePhpVersionInStatusBar() {
App.shared.currentVersion = PhpVersion()
if (App.shared.busy) {
DispatchQueue.main.async {
App.shared.currentInstall = PhpInstall()
DispatchQueue.main.async {
if (App.shared.busy) {
self.setStatusBar(image: NSImage(named: NSImage.Name("StatusBarIcon"))!)
}
} else {
DispatchQueue.main.async {
self.setStatusBarImage(version: App.shared.currentVersion!.short)
} else {
self.setStatusBarImage(version: App.phpInstall!.version.short)
}
}
self.update()
}
@ -180,13 +176,13 @@ class MainMenu: NSObject, NSWindowDelegate {
// MARK: - Actions
@objc public func restartPhpFpm() {
@objc func restartPhpFpm() {
self.waitAndExecute({
Actions.restartPhpFpm()
})
}
@objc public func restartAllServices() {
@objc func restartAllServices() {
self.waitAndExecute({
Actions.restartDnsMasq()
Actions.restartPhpFpm()
@ -194,79 +190,85 @@ class MainMenu: NSObject, NSWindowDelegate {
})
}
@objc public func restartNginx() {
@objc func restartNginx() {
self.waitAndExecute({
Actions.restartNginx()
})
}
@objc public func restartDnsMasq() {
@objc func restartDnsMasq() {
self.waitAndExecute({
Actions.restartDnsMasq()
})
}
@objc public func toggleXdebug() {
@objc func toggleExtension(sender: ExtensionMenuItem) {
self.waitAndExecute({
Actions.toggleXdebug()
// Toggle that extension
print("Toggling extension '\(sender.phpExtension!.name)'")
sender.phpExtension?.toggle()
})
}
@objc public func openPhpInfo() {
@objc func openPhpInfo() {
self.waitAndExecute({
try! "<?php phpinfo();".write(toFile: "/tmp/phpmon_phpinfo.php", atomically: true, encoding: .utf8)
Shell.user.run("\(Paths.binPath())/php-cgi -q /tmp/phpmon_phpinfo.php > /tmp/phpmon_phpinfo.html")
Shell.run("\(Paths.binPath)/php-cgi -q /tmp/phpmon_phpinfo.php > /tmp/phpmon_phpinfo.html")
}, {
NSWorkspace.shared.open(URL(string: "file:///private/tmp/phpmon_phpinfo.html")!)
})
}
@objc public func forceRestartLatestPhp() {
@objc func forceRestartLatestPhp() {
// Tell the user the switch is about to occur
_ = Alert.present(
messageText: "alert.force_reload.title".localized,
informativeText: "alert.force_reload.info".localized
)
Alert.notify(message: "alert.force_reload.title".localized, info: "alert.force_reload.info".localized)
// Start switching
self.waitAndExecute({ Actions.fixMyPhp() }, {
_ = Alert.present(
messageText: "alert.force_reload_done.title".localized,
informativeText: "alert.force_reload_done.info".localized
)
})
self.waitAndExecute(
{ Actions.fixMyPhp() },
{ Alert.notify(
message: "alert.force_reload_done.title".localized,
info: "alert.force_reload_done.info".localized
) }
)
}
@objc public func openActiveConfigFolder() {
if (App.shared.currentVersion!.error) {
@objc func openActiveConfigFolder() {
if (App.phpInstall!.version.error) {
// php version was not identified
Actions.openGenericPhpConfigFolder()
} else {
// php version was identified
Actions.openPhpConfigFolder(version: App.shared.currentVersion!.short)
return
}
// php version was identified
Actions.openPhpConfigFolder(version: App.phpInstall!.version.short)
}
@objc public func openValetConfigFolder() {
@objc func openValetConfigFolder() {
Actions.openValetConfigFolder()
}
@objc public func switchToPhpVersion(sender: PhpMenuItem) {
@objc func switchToPhpVersion(sender: PhpMenuItem) {
print("Switching to: PHP \(sender.version)")
self.setBusyImage()
App.shared.busy = true
DispatchQueue.global(qos: .userInitiated).async { [unowned self] in
// Update the PHP version in the status bar
self.updatePhpVersionInStatusBar()
// Update the menu
self.update()
// Switch the PHP version
Actions.switchToPhpVersion(
version: sender.version,
availableVersions: App.shared.availablePhpVersions
)
// Mark as no longer busy
App.shared.busy = false
// Perform UI updates on main thread
DispatchQueue.main.async {
self.updatePhpVersionInStatusBar()
@ -280,12 +282,12 @@ class MainMenu: NSObject, NSWindowDelegate {
}
}
@objc public func openAbout() {
@objc func openAbout() {
NSApplication.shared.activate(ignoringOtherApps: true)
NSApplication.shared.orderFrontStandardAboutPanel()
}
@objc public func terminateApp() {
@objc func terminateApp() {
NSApplication.shared.terminate(nil)
}
}

View File

@ -2,7 +2,6 @@
// Paths.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 01/01/2021.
// Copyright © 2021 Nico Verbruggen. All rights reserved.
//
@ -40,25 +39,25 @@ class Paths {
// - MARK: Binaries
public static func brew() -> String {
return "\(self.binPath())/brew"
public static var brew: String {
return "\(self.binPath)/brew"
}
public static func php() -> String {
return "\(self.binPath())/php"
public static var php: String {
return "\(self.binPath)/php"
}
// - MARK: Paths
public static func binPath() -> String {
public static var binPath: String {
return "\(self.shared.baseDir.rawValue)/bin"
}
public static func optPath() -> String {
public static var optPath: String {
return "\(self.shared.baseDir.rawValue)/opt"
}
public static func etcPath() -> String {
public static var etcPath: String {
return "\(self.shared.baseDir.rawValue)/etc"
}
}

View File

@ -2,14 +2,25 @@
// Shell.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 11/06/2019.
// Copyright © 2019 Nico Verbruggen. All rights reserved.
// Copyright © 2021 Nico Verbruggen. All rights reserved.
//
import Cocoa
class Shell {
// MARK: - Invoke static functions
public static func run(_ command: String) {
Shell.user.run(command)
}
public static func pipe(_ command: String, shell: String = "/bin/sh") -> String {
Shell.user.pipe(command, shell: shell)
}
// MARK: - Singleton
/**
Singleton to access a user shell (with --login)
*/
@ -21,7 +32,7 @@ class Shell {
- Parameter command: The command to run
*/
public func run(_ command: String) {
func run(_ command: String) {
// Equivalent of piping to /dev/null; don't do anything with the string
_ = self.pipe(command)
}
@ -32,7 +43,7 @@ class Shell {
- Parameter command: The command to run
- Parameter shell: Path to the shell to invoke
*/
public func pipe(_ command: String, shell: String = "/bin/sh") -> String {
func pipe(_ command: String, shell: String = "/bin/sh") -> String {
let task = Process()
let pipe = Pipe()
@ -51,7 +62,7 @@ class Shell {
Checks if a file exists at the provided path.
*/
public static func fileExists(_ path: String) -> Bool {
return Shell.user.pipe(
return Shell.pipe(
"if [ -f \(path) ]; then echo \"PHP_Y_FE\"; fi"
).contains("PHP_Y_FE")
}

View File

@ -22,17 +22,21 @@
"mi_restart_php_fpm" = "Restart service: php";
"mi_restart_nginx" = "Restart service: nginx";
"mi_restart_dnsmasq" = "Restart service: dnsmasq";
"mi_restart_specific" = "Restart specific service";
"mi_restart_all_services" = "Restart all services";
"mi_force_load_latest" = "Force load latest PHP version";
"mi_configuration" = "Configuration";
"mi_limits" = "Limits Configuration";
"mi_memory_limit" = "Memory Limit";
"mi_post_max_size" = "Max POST";
"mi_upload_max_filesize" = "Max Upload";
"mi_valet_config" = "Locate Valet folder (.config/valet)";
"mi_php_config" = "Locate PHP configuration file (php.ini)";
"mi_phpinfo" = "Show current configuration (phpinfo)";
"mi_enabled_extensions" = "Enabled Extensions";
"mi_xdebug" = "Xdebug";
"mi_xdebug_missing" = "xdebug.so missing";
"mi_detected_extensions" = "Detected Extensions";
"mi_no_extensions_detected" = "No additional extensions detected.";
"mi_quit" = "Quit PHP Monitor";
"mi_about" = "About PHP Monitor";