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

Compare commits

...

52 Commits
v2.5 ... v3.0

Author SHA1 Message Date
aa07a42d8f 🚀 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
  ...
2021-02-12 09:47:28 +01:00
b6b3a9a74c 🔧 Bump version for release 2021-02-12 09:38:09 +01:00
ec4371e6da 👌 All methods are internal by default 2021-02-10 16:35:23 +01:00
c7a92d502f 📝 Move all FAQ to README 2021-02-07 11:52:53 +01:00
172a38e459 📝 Reorganized document 2021-02-05 10:59:55 +01:00
d18e8298b6 📝 Updated quick troubleshooting 2021-02-05 10:54:09 +01:00
df325474c6 📝 New screenshot 2021-02-05 10:27:34 +01:00
60278d7817 🔧 Bump build version 2021-02-05 10:11:15 +01:00
9e8117d31f 🎨 Use Self to refer to current type 2021-02-05 10:07:48 +01:00
3d042b24ed Improved byte value parsing 2021-02-05 09:56:02 +01:00
fcf3483dd0 📝 Updated SECURITY 2021-02-04 22:35:37 +01:00
3db333dd77 📝 Updated README, new screenshot 2021-02-04 22:32:46 +01:00
b0f0fd8c6b 🔧 Bump build version 2021-02-04 22:09:36 +01:00
69755cd8cc 🚚 Move files around 2021-02-04 22:07:09 +01:00
6b2fa0c605 🔥 Crash if the view is unavailable 2021-02-04 22:04:29 +01:00
dd41b5a1e0 🎨 Fix threading issue 2021-02-04 22:00:58 +01:00
a3fe09ed8a Completed new design for v3.0 2021-02-04 21:41:36 +01:00
ad81c4a3db Tweak custom views 2021-02-04 21:27:52 +01:00
572b0f69dd Custom views 2021-02-04 21:23:41 +01:00
c1b80552a0 🎨 Updated localization, cleanup 2021-02-03 22:09:53 +01:00
0493f61dc8 🔥 Cleanup 2021-02-03 19:39:49 +01:00
9824ed2b83 Load certain info from ini files 2021-02-03 18:01:25 +01:00
3e0a16cc73 📝 Document extension detection & toggling 2021-01-31 23:06:57 +01:00
5a50e7fdab Allow toggling of PHP extensions in php.ini 2021-01-31 22:50:39 +01:00
1dc85a4d48 🔧 Bump version and build 2021-01-29 20:48:49 +01:00
85ef3ff067 ♻️ Paths now uses static variables instead of methods 2021-01-29 20:39:55 +01:00
2958378411 ♻️ Add static methods to Shell 2021-01-29 20:36:33 +01:00
3a2cd3bff6 ♻️ Rework file checks & replacement into separate methods 2021-01-29 20:32:12 +01:00
fbe791902c 🎨 Update copyright, minor bracket cleanup 2021-01-29 20:08:27 +01:00
77f4de8ca9 🎨 Cleanup code 2021-01-29 19:57:13 +01:00
0f6e47a594 📝 Added build instructions 2021-01-09 13:46:45 +01:00
7408da2a91 ♻️ Assets now available as SVG 2021-01-09 13:38:32 +01:00
12a66f3467 📝 Updated Homebrew instructions 2021-01-06 18:09:16 +01:00
d3615138dd 🚀 Version 2.6
* develop:
  📝 Updated quick troubleshooting
  📝 Updated documentation
  📝 Updated SECURITY
  📝 Updated README
   New build number, updated shortcut keys
  🌐 Add info about `valet install` when running force reload
   Fix my PHP should also restart dnsmasq
   Add menu item to restart dnsmasq
  🔧 Bump build number
  ♻️ Cleanup, updated README (#20)
  ♻️ Refactoring, version bump for M1 support (#20)
   Add support for Homebrew in /opt/homebrew (#20)
  👌 Clean up switchToPhpVersion() and fixMyPhp()
  👌 Avoid using sender.tag
  👌 Improved pipe() method
  🌐 Localized startup environment checks
  🐛 Perform the phpinfo.html on another thread and show busy
  📝 Updated instructions
2021-01-06 17:51:29 +01:00
dd27b91527 📝 Updated quick troubleshooting 2021-01-06 17:49:26 +01:00
bc093cc945 📝 Updated documentation 2021-01-06 17:42:09 +01:00
fd46ce6b35 📝 Updated SECURITY 2021-01-06 17:29:24 +01:00
e35acadeaf 📝 Updated README 2021-01-06 17:18:59 +01:00
7b8aab85d6 New build number, updated shortcut keys
- Cmd-F now [F]orce reloads
- Cmd-S now restarts all [S]ervices
- Cmd-P now restarts the [P]hp service
- Cmd-D now restarts the [D]nsmasq service
2021-01-01 23:55:37 +01:00
ec59715b3d 🌐 Add info about valet install when running force reload 2021-01-01 23:49:51 +01:00
6f21913ae2 Fix my PHP should also restart dnsmasq 2021-01-01 23:48:12 +01:00
b53fbe471b Add menu item to restart dnsmasq 2021-01-01 23:19:31 +01:00
209f3e889d 🔧 Bump build number 2021-01-01 23:06:24 +01:00
5825e8d0b0 ♻️ Cleanup, updated README (#20) 2021-01-01 23:05:16 +01:00
4ea11c5f59 ♻️ Refactoring, version bump for M1 support (#20) 2021-01-01 22:58:27 +01:00
94f086881a Add support for Homebrew in /opt/homebrew (#20) 2021-01-01 22:54:03 +01:00
e73474e30c 👌 Clean up switchToPhpVersion() and fixMyPhp() 2020-12-19 18:38:00 +01:00
c7a0e25336 👌 Avoid using sender.tag 2020-12-13 19:32:43 +01:00
e353fb7524 👌 Improved pipe() method 2020-12-13 19:16:51 +01:00
458868d051 🌐 Localized startup environment checks 2020-12-08 23:18:52 +01:00
932fafc728 🐛 Perform the phpinfo.html on another thread and show busy 2020-11-27 17:01:30 +01:00
b70c4f690a 📝 Updated instructions 2020-11-27 16:40:37 +01:00
42 changed files with 1517 additions and 751 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,6 +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 */; };
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 */; };
@ -42,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>"; };
@ -52,6 +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>"; };
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>"; };
@ -104,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 */,
@ -117,28 +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 */,
C48D0C9525CC80B100CC7490 /* HeaderView.swift */,
C48D0C9925CC888B00CC7490 /* HeaderView.xib */,
C48D0CA225CC992000CC7490 /* StatsView.swift */,
C48D0C8F25CC7FD000CC7490 /* StatsView.xib */,
);
path = Menu;
sourceTree = "<group>";
@ -146,6 +156,7 @@
C4811D2622D70CEF00B5F6B3 /* Singletons */ = {
isa = PBXGroup;
children = (
C49EAB45259FC305007F6C3B /* Paths.swift */,
C41C1B4622B009A400E7CF16 /* Shell.swift */,
C42295DC2358D02000E263B2 /* Command.swift */,
C4811D2322D70A4700B5F6B3 /* App.swift */,
@ -167,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>";
@ -244,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;
};
@ -257,19 +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 */,
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 */,
@ -415,7 +443,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 26;
CURRENT_PROJECT_VERSION = 45;
DEVELOPMENT_TEAM = 8M54J5J787;
ENABLE_HARDENED_RUNTIME = YES;
INFOPLIST_FILE = phpmon/Info.plist;
@ -423,7 +451,7 @@
"$(inherited)",
"@executable_path/../Frameworks",
);
MARKETING_VERSION = 2.5;
MARKETING_VERSION = 3.0;
PRODUCT_BUNDLE_IDENTIFIER = com.nicoverbruggen.phpmon;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
@ -439,7 +467,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 26;
CURRENT_PROJECT_VERSION = 45;
DEVELOPMENT_TEAM = 8M54J5J787;
ENABLE_HARDENED_RUNTIME = YES;
INFOPLIST_FILE = phpmon/Info.plist;
@ -447,7 +475,7 @@
"$(inherited)",
"@executable_path/../Frameworks",
);
MARKETING_VERSION = 2.5;
MARKETING_VERSION = 3.0;
PRODUCT_BUNDLE_IDENTIFIER = com.nicoverbruggen.phpmon;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";

226
README.md
View File

@ -4,46 +4,206 @@
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.
It also gives you quick access to various useful functionality (like switching PHP versions, restarting services, accessing configuration files, and more).
<img src="./docs/screenshot.png" width="389px" alt="phpmon screenshot (menu bar app)"/>
<img src="./docs/screenshot.png" width="370px" alt="phpmon screenshot"/>
<small><i>Screenshot: A menu showing all of the functionality of PHP Monitor.</i></small>
For me, it comes in handy when running multiple versions of PHP with Homebrew. If you wish to be able to see at a glance which version is currently linked & active with Laravel Valet, PHP Monitor is your new best friend.
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)!
It's also super convenient to switch between different versions of PHP, or to find your currently active .ini file!
<img src="./docs/notification.png" width="370px" alt="phpmon screenshot (notification)"/>
It also gives you quick access to various useful functionality (like accessing configuration files, restarting services, and more).
## 🖥 System requirements
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)
* The brew formula `php` has to be installed (which version it is, is detected)
* Laravel Valet 2.x
* 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:
brew tap nicoverbruggen/homebrew-cask
brew cask install phpmon
brew install --cask phpmon
To upgrade your existing installation, run:
brew upgrade phpmon
_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?
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.
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.
## 🤬 The app won't start?!
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.
**Follow instructions as specified in the alert in order to resolve any issues.**
## 🙋‍♂️ 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
@ -59,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?
@ -69,35 +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. 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`
- PHP is missing in `/usr/local/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.)
## 📝 Additional information & FAQ
Please consult the [additional information][2] file that contains more information. It has 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

@ -1,15 +1,17 @@
# Security Policy
## Supported Versions
## Supported versions
The following versions of PHP Monitor are supported:
Generally speaking, only the latest version of **PHP Monitor** is supported:
| Version | Universal | Supported | Runs on macOS |
| ------- | ------------- | ------------------ | ----- |
| 2.5 | ✅ | ✅ | Catalina (10.15), Big Sur (11.0) |
| 2.4 | ✅ | ❌ | Catalina (10.15), Big Sur (11.0) |
| < 2.4 | | | Catalina (10.15) |
| Version | Apple Silicon | Supported | Supported macOS | Deployment Target |
| ------- | ------------- | ------------------ | ----- | ----- |
| 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
## Reporting a vulnerability
Contact Nico Verbruggen at the email address used for the commits in the repository. Please include "PHP Monitor" in the subject.
Contact me (Nico Verbruggen) at the email address used for the commits in the repository. Please include "PHP Monitor" in the subject.

Binary file not shown.

1
assets/icon.svg Normal file
View File

@ -0,0 +1 @@
<svg width="512" height="512" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><filter x="-4.6%" y="-3.4%" width="109.2%" height="109.2%" filterUnits="objectBoundingBox" id="b"><feOffset dy="5" in="SourceAlpha" result="shadowOffsetOuter1"/><feGaussianBlur stdDeviation="5.5" in="shadowOffsetOuter1" result="shadowBlurOuter1"/><feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.26 0" in="shadowBlurOuter1"/></filter><filter x="-8.6%" y="-11%" width="117.1%" height="121.9%" filterUnits="objectBoundingBox" id="d"><feOffset dx="1" dy="2" in="SourceAlpha" result="shadowOffsetOuter1"/><feGaussianBlur stdDeviation="6.5" in="shadowOffsetOuter1" result="shadowBlurOuter1"/><feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.372049825 0" in="shadowBlurOuter1" result="shadowMatrixOuter1"/><feMerge><feMergeNode in="shadowMatrixOuter1"/><feMergeNode in="SourceGraphic"/></feMerge></filter><linearGradient x1="50.023%" y1="-15.482%" x2="50.023%" y2="104.493%" id="e"><stop stop-color="#51C2FB" offset=".053%"/><stop stop-color="#1C91FE" offset="100%"/></linearGradient><path d="M167.939 50H344.06c41.01 0 55.881 4.27 70.874 12.288 14.992 8.018 26.759 19.785 34.777 34.777C457.73 112.058 462 126.93 462 167.94V344.06c0 41.01-4.27 55.881-12.288 70.874-8.018 14.992-19.785 26.759-34.777 34.777C399.942 457.73 385.07 462 344.06 462H167.94c-41.01 0-55.881-4.27-70.874-12.288-14.992-8.018-26.759-19.785-34.777-34.777C54.27 399.942 50 385.07 50 344.06V167.94c0-41.01 4.27-55.881 12.288-70.874 8.018-14.992 19.785-26.759 34.777-34.777C112.058 54.27 126.93 50 167.94 50z" id="a"/></defs><g fill="none" fill-rule="evenodd"><mask id="c" fill="#fff"><use xlink:href="#a"/></mask><use filter="url(#b)" xlink:href="#a" fill-rule="nonzero" fill="#000"/><g mask="url(#c)"><path fill="#FFF" d="M50 50h412v412H50z"/></g><g filter="url(#d)" mask="url(#c)"><path fill="#FFF" d="M131.535 155.144l63.377-8.907 33.68 239.645-63.377 8.907zM315.79 263.497l63.377-8.907 15.17 107.94-63.377 8.906z"/><path fill="#FFF" d="M315.79 263.497l63.377-8.907 15.17 107.94-63.377 8.906z"/><path fill="#FFF" d="M316.01 173.224l84.001 68.319-53.919 66.92-84-68.318z"/><path fill="#FFF" d="M93.155 157.813l164.384-23.102 21.433 152.5-164.385 23.104z"/><path d="M133.3 83.75h-12.9V54.437c0-2.303-1.935-4.187-4.3-4.187h-8.6c-2.365 0-4.3 1.884-4.3 4.188v41.874c0 2.304 1.935 4.188 4.3 4.188h25.8c2.365 0 4.3-1.884 4.3-4.188v-8.374c0-2.304-1.935-4.188-4.3-4.188zm202.1 100.5h-8.6v-56.584c0-6.647-2.741-13.033-7.579-17.744L265.525 57.63c-4.837-4.71-11.395-7.38-18.221-7.38H223.6V25.125C223.6 11.254 212.044 0 197.8 0h-172C11.556 0 0 11.254 0 25.125v167.5c0 13.871 11.556 25.125 25.8 25.125h8.6C34.4 245.492 57.513 268 86 268c28.487 0 51.6-22.508 51.6-50.25h68.8c0 27.742 23.112 50.25 51.6 50.25 28.488 0 51.6-22.508 51.6-50.25h25.8c4.73 0 8.6-3.769 8.6-8.375v-16.75c0-4.606-3.87-8.375-8.6-8.375zM86 242.875c-14.244 0-25.8-11.254-25.8-25.125s11.556-25.125 25.8-25.125c14.244 0 25.8 11.254 25.8 25.125s-11.556 25.125-25.8 25.125zm25.8-92.125c-33.271 0-60.2-26.224-60.2-58.625 0-32.4 26.929-58.625 60.2-58.625S172 59.724 172 92.125c0 32.4-26.929 58.625-60.2 58.625zM258 242.875c-14.244 0-25.8-11.254-25.8-25.125s11.556-25.125 25.8-25.125c14.244 0 25.8 11.254 25.8 25.125s-11.556 25.125-25.8 25.125zM301 134h-77.4V75.375h23.704L301 127.666V134z" fill="url(#e)" fill-rule="nonzero" transform="rotate(-8 1129.292 -453.38)"/></g></g></svg>

After

Width:  |  Height:  |  Size: 3.4 KiB

1
assets/menubar.svg Normal file
View File

@ -0,0 +1 @@
<svg width="22" height="22" xmlns="http://www.w3.org/2000/svg"><path d="M8.975 8.375H8.3V6.844a.223.223 0 00-.225-.219h-.45a.223.223 0 00-.225.219V9.03c0 .12.101.219.225.219h1.35a.223.223 0 00.225-.219v-.437a.223.223 0 00-.225-.219zm10.575 5.25h-.45v-2.956c0-.347-.143-.68-.397-.927l-2.81-2.731a1.37 1.37 0 00-.953-.386H13.7V5.312C13.7 4.588 13.095 4 12.35 4h-9C2.605 4 2 4.588 2 5.313v8.75c0 .724.605 1.312 1.35 1.312h.45C3.8 16.825 5.01 18 6.5 18c1.49 0 2.7-1.176 2.7-2.625h3.6c0 1.45 1.21 2.625 2.7 2.625 1.49 0 2.7-1.176 2.7-2.625h1.35c.247 0 .45-.197.45-.438v-.874a.445.445 0 00-.45-.438zM6.5 16.688c-.745 0-1.35-.588-1.35-1.313s.605-1.313 1.35-1.313c.745 0 1.35.588 1.35 1.313s-.605 1.313-1.35 1.313zm1.35-4.813c-1.74 0-3.15-1.37-3.15-3.063C4.7 7.12 6.11 5.75 7.85 5.75S11 7.12 11 8.813c0 1.692-1.41 3.062-3.15 3.062zm7.65 4.813c-.745 0-1.35-.588-1.35-1.313s.605-1.313 1.35-1.313c.745 0 1.35.588 1.35 1.313s-.605 1.313-1.35 1.313zM17.75 11H13.7V7.937h1.24l2.81 2.732V11z" fill="#000" fill-rule="nonzero"/></svg>

After

Width:  |  Height:  |  Size: 1017 B

87
assets/source.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 44 KiB

View File

@ -1,111 +0,0 @@
### Q&A
#### Q: Does this support Apple Silicon?
Yes. This is a universal app.
#### Q: Is PHP 8.x 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: How can I set this up on a fresh Mac?
If you want to set up your computer for the very first time, 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:
export PATH=$HOME/bin:/usr/local/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`
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.
#### 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.
#### Q: PHP Monitor says the correct version is loaded, but my Valet sites don't work!
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,12 +2,12 @@
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
7. Create zipped version
8. Calculate SHA256: `openssl dgst -sha256 phpmon-2.x.zip`
9. Upload to GitHub
10. Update Cask
8. Calculate SHA256: `openssl dgst -sha256 phpmon.zip`
9. Upload to GitHub and add to tagged release
10. Update Cask with new version + hash
11. Check new version can be installed via Cask

BIN
docs/build.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

BIN
docs/notification.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 270 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
@ -33,6 +32,12 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserNotificationCenterDele
*/
let menu : MainMenu
/**
The paths singleton that determines where Homebrew is installed,
and where to look for binaries.
*/
let paths : Paths
// MARK: - Initializer
/**
@ -42,6 +47,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserNotificationCenterDele
self.sharedShell = Shell.user
self.state = App.shared
self.menu = MainMenu.shared
self.paths = Paths.shared
super.init()
}

View File

@ -1,143 +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 /usr/local/opt | 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 brew services restart php")
} else {
Shell.user.run("sudo brew services restart php@\(version)")
}
}
public static func restartNginx()
{
Shell.user.run("sudo brew services restart nginx")
}
public static func switchToPhpVersion(version: String, availableVersions: [String]) {
availableVersions.forEach { (version) in
// Unlink the current version
Shell.user.run("brew unlink php@\(version)")
// Stop the services
if (version == App.shared.brewPhpVersion) {
Shell.user.run("sudo brew services stop php")
} else {
Shell.user.run("sudo brew services stop php@\(version)")
}
}
if (availableVersions.contains(App.shared.brewPhpVersion)) {
// Use the latest version as a default
Shell.user.run("brew link php@\(App.shared.brewPhpVersion) --overwrite --force")
if (version == App.shared.brewPhpVersion) {
// If said version was also requested, all we need to do is start the service
Shell.user.run("sudo brew services start php")
} else {
// Otherwise, link the correct php version + start the correct service
Shell.user.run("brew link php@\(version) --overwrite --force")
Shell.user.run("sudo brew services start php@\(version)")
}
}
}
public static func openGenericPhpConfigFolder() {
let files = [NSURL(fileURLWithPath: "/usr/local/etc/php")];
NSWorkspace.shared.activateFileViewerSelecting(files as [URL])
}
public static func openPhpConfigFolder(version: String) {
let files = [NSURL(fileURLWithPath: "/usr/local/etc/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"' /usr/local/etc/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"' /usr/local/etc/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' /usr/local/etc/php/\(version!)/php.ini
"""
if (self.didEnableXdebug(version!)) {
command = """
sed -i '' 's/zend_extension="xdebug.so"/; zend_extension="xdebug.so"/g' /usr/local/etc/php/\(version!)/php.ini
"""
}
Shell.user.run(command)
}
// unlink all the crap and link the latest version
// this also restarts all services
public static func fixMyPhp() {
let versions = self.detectPhpVersions()
versions.forEach { (version) in
Shell.user.run("brew unlink php@\(version)")
if (version == App.shared.brewPhpVersion) {
Shell.user.run("brew services stop php")
Shell.user.run("sudo brew services stop php")
} else {
Shell.user.run("brew services stop php@\(version)")
Shell.user.run("sudo brew services stop php@\(version)")
}
}
Shell.user.run("brew services stop php")
Shell.user.run("brew services stop nginx")
Shell.user.run("brew link php")
Shell.user.run("sudo brew services restart php")
Shell.user.run("sudo brew services restart nginx")
}
}

View File

@ -1,132 +0,0 @@
//
// Environment.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 12/06/2019.
// Copyright © 2019 Nico Verbruggen. All rights reserved.
//
import Foundation
class Startup {
public var failed : Bool = false
public var failureCallback = {}
/**
Checks the user's environment and checks if PHP Monitor can be used properly.
This checks if PHP is installed, Valet is running, the appropriate permissions are set, and more.
- 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)
{
self.failureCallback = failure
self.performEnvironmentCheck(
!Shell.user.pipe("which php").contains("/usr/local/bin/php"),
messageText: "PHP is not correctly installed",
informativeText: "You must install PHP via brew. Try running `which php` in Terminal, it should return `/usr/local/bin/php`. The app will not work correctly until you resolve this issue. (Usually `brew link php` resolves this issue.)",
breaking: true
)
self.performEnvironmentCheck(
!Shell.user.pipe("ls /usr/local/opt | grep php").contains("php"),
messageText: "PHP is not correctly installed",
informativeText: "PHP alias was not found in `/usr/local/opt`. The app will not work correctly until you resolve this issue. If you already have the `php` formula installed, you may need to run `brew install php` in order for PHP Monitor to detect this installation.",
breaking: true
)
self.performEnvironmentCheck(
!Shell.user.pipe("which valet").contains("/usr/local/bin/valet"),
messageText: "Laravel Valet is not correctly installed",
informativeText: "You must install Valet with composer. Try running `which valet` in Terminal, it should return `/usr/local/bin/valet`. The app will not work correctly until you resolve this issue.",
breaking: true
)
self.performEnvironmentCheck(
!Shell.user.pipe("cat /private/etc/sudoers.d/brew").contains("/usr/local/bin/brew"),
messageText: "Brew has not been added to sudoers.d",
informativeText: "You must run `sudo valet trust` to ensure Valet can start and stop services without having to use sudo every time. The app will not work correctly until you resolve this issue.",
breaking: true
)
self.performEnvironmentCheck(
!Shell.user.pipe("cat /private/etc/sudoers.d/valet").contains("/usr/local/bin/valet"),
messageText: "Valet has not been added to sudoers.d",
informativeText: "You must run `sudo valet trust` to ensure Valet can start and stop services without having to use sudo every time. The app will not work correctly until you resolve this issue.",
breaking: true
)
let services = Shell.user.pipe("brew services list | grep php")
self.performEnvironmentCheck(
(services.countInstances(of: "started") > 1),
messageText: "Multiple PHP services are active",
informativeText: "This can cause php-fpm to serve a more recent version of PHP than the one you'd like to see active. Please terminate all extra PHP processes." +
"\n\nThe easiest solution is to choose the option 'Force load latest PHP version' in the menu bar." +
"\n\nAlternatively, you can fix this manually. You can do this by running `brew services list` and running `sudo brew services stop php@7.3` (and use the version that applies)." +
"\n\nPHP Monitor usually handles the starting and stopping of these services, so once the correct version is the only PHP version running you should not have any issues. It is recommended to restart PHP Monitor once you have resolved this issue." +
"\n\nFor more information about this issue, please see the README.md file in the repository on GitHub.",
breaking: false
)
if (!self.failed) {
self.determineBrewAliasVersion()
success()
}
}
/**
* In order to avoid having to hard-code which version of PHP is aliased to what specific subversion,
* PHP Monitor now determines the alias by checking the user's system.
*/
private func determineBrewAliasVersion()
{
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("brew info php --json");
App.shared.brewPhpPackage = try! JSONDecoder().decode(
[HomebrewPackage].self,
from: brewPhpAlias.data(using: .utf8)!
).first!
print("When on your system, the `php` formula means version \(App.shared.brewPhpVersion)!")
}
/**
* Perform an environment check. Will cause the application to terminate, if `breaking` is set to true.
*
* - Parameter condition: Condition to check for
* - Parameter messageText: Short description of what is wrong
* - Parameter informativeText: Expanded description of the environment check that failed
* - Parameter breaking: If the application should terminate afterwards
*/
private func performEnvironmentCheck(
_ condition: Bool,
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()
}
}
}
}
}

View File

@ -1,48 +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: "/usr/local/bin/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,91 +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 = NSMenuItem(title: "\("mi_php_switch".localized) \(version) (\(brew))", action: (version == App.shared.currentVersion?.short) ? nil : action, keyEquivalent: "\(shortcutKey)")
menuItem.tag = index
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_php_fpm".localized, action: #selector(MainMenu.restartPhpFpm), keyEquivalent: "f"))
self.addItem(NSMenuItem(title: "mi_restart_nginx".localized, action: #selector(MainMenu.restartNginx), keyEquivalent: "n"))
self.addItem(NSMenuItem(title: "mi_force_load_latest".localized, action: #selector(MainMenu.forceRestartLatestPhp), keyEquivalent: ""))
}
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

@ -0,0 +1,119 @@
//
// Environment.swift
// PHP Monitor
//
// Copyright © 2021 Nico Verbruggen. All rights reserved.
//
import Foundation
class Startup {
public var failed : Bool = false
public var failureCallback = {}
/**
Checks the user's environment and checks if PHP Monitor can be used properly.
This checks if PHP is installed, Valet is running, the appropriate permissions are set, and more.
- 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
*/
func checkEnvironment(success: () -> Void, failure: @escaping () -> Void)
{
self.failureCallback = failure
self.performEnvironmentCheck(
!Shell.fileExists("\(Paths.binPath)/php"),
messageText: "startup.errors.php_binary.title".localized,
informativeText: "startup.errors.php_binary_desc".localized,
breaking: true
)
self.performEnvironmentCheck(
!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.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.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.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.pipe("\(Paths.brew) services list | grep php")
self.performEnvironmentCheck(
(services.countInstances(of: "started") > 1),
messageText: "startup.errors.services.title".localized,
informativeText: "startup.errors.services.desc".localized,
breaking: false
)
if (!self.failed) {
self.determineBrewAliasVersion()
success()
}
}
/**
* In order to avoid having to hard-code which version of PHP is aliased to what specific subversion,
* PHP Monitor now determines the alias by checking the user's system.
*/
private func determineBrewAliasVersion()
{
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.pipe("\(Paths.brew) info php --json");
App.shared.brewPhpPackage = try! JSONDecoder().decode(
[HomebrewPackage].self,
from: brewPhpAlias.data(using: .utf8)!
).first!
print("When on your system, the `php` formula means version \(App.shared.brewPhpVersion)!")
}
/**
* Perform an environment check. Will cause the application to terminate, if `breaking` is set to true.
*
* - Parameter condition: Fail condition to check for; if this returns `true`, the alert will be shown
* - Parameter messageText: Short description of what is wrong
* - Parameter informativeText: Expanded description of the environment check that failed
* - Parameter breaking: If the application should terminate afterwards
*/
private func performEnvironmentCheck(
_ condition: Bool,
messageText: String,
informativeText: String,
breaking: Bool
) {
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,97 +176,118 @@ class MainMenu: NSObject, NSWindowDelegate {
// MARK: - Actions
@objc public func restartPhpFpm() {
@objc func restartPhpFpm() {
self.waitAndExecute({
Actions.restartPhpFpm()
})
}
@objc public func restartNginx() {
@objc func restartAllServices() {
self.waitAndExecute({
Actions.restartDnsMasq()
Actions.restartPhpFpm()
Actions.restartNginx()
})
}
@objc func restartNginx() {
self.waitAndExecute({
Actions.restartNginx()
})
}
@objc public func toggleXdebug() {
@objc func restartDnsMasq() {
self.waitAndExecute({
Actions.toggleXdebug()
Actions.restartDnsMasq()
})
}
@objc public func openPhpInfo() {
try! "<?php phpinfo();".write(toFile: "/tmp/phpmon_phpinfo.php", atomically: true, encoding: .utf8)
Shell.user.run("/usr/local/bin/php-cgi -q /tmp/phpmon_phpinfo.php > /tmp/phpmon_phpinfo.html")
NSWorkspace.shared.open(URL(string: "file:///private/tmp/phpmon_phpinfo.html")!)
@objc func toggleExtension(sender: ExtensionMenuItem) {
self.waitAndExecute({
// Toggle that extension
print("Toggling extension '\(sender.phpExtension!.name)'")
sender.phpExtension?.toggle()
})
}
@objc public func forceRestartLatestPhp() {
@objc func openPhpInfo() {
self.waitAndExecute({
try! "<?php phpinfo();".write(toFile: "/tmp/phpmon_phpinfo.php", atomically: true, encoding: .utf8)
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 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: AnyObject) {
@objc func switchToPhpVersion(sender: PhpMenuItem) {
print("Switching to: PHP \(sender.version)")
self.setBusyImage()
// TODO: A wise man once said: using tags is not good. Fix this.
let index = sender.tag!
let version = App.shared.availablePhpVersions[index]
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: version,
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()
self.update()
// Send a notification that the switch has been completed
LocalNotification.send(
title: String(format: "notification.version_changed_title".localized, version),
subtitle: String(format: "notification.version_changed_desc".localized, version)
title: String(format: "notification.version_changed_title".localized, sender.version),
subtitle: String(format: "notification.version_changed_desc".localized, sender.version)
)
}
}
}
@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

@ -0,0 +1,65 @@
//
// Paths.swift
// PHP Monitor
//
// Copyright © 2021 Nico Verbruggen. All rights reserved.
//
import Foundation
enum HomebrewDir: String {
case opt = "/opt/homebrew"
case usr = "/usr/local"
}
class Paths {
static let shared = Paths()
var baseDir : HomebrewDir
init() {
let optBrewFound = Shell.fileExists("\(HomebrewDir.opt.rawValue)/bin/brew")
let usrBrewFound = Shell.fileExists("\(HomebrewDir.usr.rawValue)/bin/brew")
if (optBrewFound) {
// This is usually the case with Homebrew installed on Apple Silicon
self.baseDir = .opt
} else if (usrBrewFound) {
// This is usually the case with Homebrew installed on Intel (or Rosetta 2)
self.baseDir = .usr
} else {
// Falling back to default "legacy" Homebrew location (for Intel)
print("Seems like we couldn't determine the Homebrew directory.")
print("This usually means we're in trouble... (no Homebrew?)")
self.baseDir = .usr
}
print("Homebrew directory: \(self.baseDir)")
}
// - MARK: Binaries
public static var brew: String {
return "\(self.binPath)/brew"
}
public static var php: String {
return "\(self.binPath)/php"
}
// - MARK: Paths
public static var binPath: String {
return "\(self.shared.baseDir.rawValue)/bin"
}
public static var optPath: String {
return "\(self.shared.baseDir.rawValue)/opt"
}
public static var etcPath: String {
return "\(self.shared.baseDir.rawValue)/etc"
}
}

View File

@ -0,0 +1,69 @@
//
// Shell.swift
// PHP Monitor
//
// 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)
*/
static let user = Shell()
/**
Runs a shell command without using the output.
Uses the default shell.
- Parameter command: The command to run
*/
func run(_ command: String) {
// Equivalent of piping to /dev/null; don't do anything with the string
_ = self.pipe(command)
}
/**
Runs a shell command and returns the output.
- Parameter command: The command to run
- Parameter shell: Path to the shell to invoke
*/
func pipe(_ command: String, shell: String = "/bin/sh") -> String {
let task = Process()
let pipe = Pipe()
task.launchPath = shell
task.arguments = ["--login", "-c", command]
task.standardOutput = pipe
task.launch()
return String(
data: pipe.fileHandleForReading.readDataToEndOfFile(),
encoding: .utf8
)!
}
/**
Checks if a file exists at the provided path.
*/
public static func fileExists(_ path: String) -> Bool {
return Shell.pipe(
"if [ -f \(path) ]; then echo \"PHP_Y_FE\"; fi"
).contains("PHP_Y_FE")
}
}

View File

@ -17,19 +17,26 @@
"mi_php_broken_3" = "You could also try switching to another version.";
"mi_php_broken_4" = "Running `brew reinstall php` (or for the equivalent version) might help.";
"mi_diagnostics" = "Diagnostics";
"mi_active_services" = "Active Services";
"mi_restart_php_fpm" = "Restart php-fpm service";
"mi_restart_nginx" = "Restart nginx service";
"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";
@ -47,10 +54,36 @@
// Force Reload Done
"alert.force_reload_done.title" = "PHP has been force reloaded";
"alert.force_reload_done.info" = "All appropriate services have been restarted, and the latest version of PHP is now active. You can now try switching to another version of PHP.";
"alert.force_reload_done.info" = "All appropriate services have been restarted, and the latest version of PHP is now active. You can now try switching to another version of PHP. If visiting sites still does not work, you may try running `valet install` again, this can fix a 502 issue (Bad Gateway).";
// PHP Monitor Cannot Start
"alert.cannot_start.title" = "PHP Monitor cannot start";
"alert.cannot_start.info" = "The issue you were just notified about is keeping PHP Monitor from functioning correctly. Please fix the issue and restart PHP Monitor. After clicking on OK, PHP Monitor will close.\n\nIf you have fixed the issue (or don't remember what the exact issue is) you can click on Retry, which will have PHP Monitor retry the startup checks.";
"alert.cannot_start.close" = "Close";
"alert.cannot_start.retry" = "Retry";
// STARTUP
/// 1. PHP binary not found
"startup.errors.php_binary.title" = "PHP is not correctly installed";
"startup.errors.php_binary_desc" = "You must install PHP via brew. Try running `which php` in Terminal, it should return `/usr/local/bin/php` (or `/opt/homebrew/bin/php`). The app will not work correctly until you resolve this issue. (Usually `brew link php` resolves this issue.)";
/// 2. PHP not found in /usr/local/opt or /opt/homebrew/opt
"startup.errors.php_opt.title" = "PHP is not correctly installed";
"startup.errors.php_opt.desc" = "PHP alias was not found in `/usr/local/opt` (or `/opt/homebrew/opt`). The app will not work correctly until you resolve this issue. If you already have the `php` formula installed, you may need to run `brew install php` in order for PHP Monitor to detect this installation.";
/// 3. Valet not installed
"startup.errors.valet_executable.title" = "Laravel Valet is not correctly installed";
"startup.errors.valet_executable.desc" = "You must install Valet with composer. Try running `which valet` in Terminal, it should return `/usr/local/bin/valet`. The app will not work correctly until you resolve this issue.";
/// 4. Brew & sudoers
"startup.errors.sudoers_brew.title" = "Brew has not been added to sudoers.d";
"startup.errors.sudoers_brew.desc" = "You must run `sudo valet trust` to ensure Valet can start and stop services without having to use sudo every time. The app will not work correctly until you resolve this issue.";
/// 5. Valet & sudoers
"startup.errors.sudoers_valet.title" = "Valet has not been added to sudoers.d";
"startup.errors.sudoers_valet.desc" = "You must run `sudo valet trust` to ensure Valet can start and stop services without having to use sudo every time. The app will not work correctly until you resolve this issue.";
/// 6. Multiple services active
"startup.errors.services.title" = "Multiple PHP services are active";
"startup.errors.services.desc" = "This can cause php-fpm to serve a more recent version of PHP than the one you'd like to see active. Please terminate all extra PHP processes.\n\nThe easiest solution is to choose the option 'Force load latest PHP version' in the menu bar.\n\nAlternatively, you can fix this manually. You can do this by running `brew services list` and running `sudo brew services stop php@7.3` (and use the version that applies).\n\nPHP Monitor usually handles the starting and stopping of these services, so once the correct version is the only PHP version running you should not have any issues. It is recommended to restart PHP Monitor once you have resolved this issue.\n\nFor more information about this issue, please see the README.md file in the repository on GitHub.";

View File

@ -1,53 +0,0 @@
//
// Shell.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 11/06/2019.
// Copyright © 2019 Nico Verbruggen. All rights reserved.
//
import Cocoa
class Shell {
/**
Singleton to access a user shell (with --login)
*/
static let user = Shell()
/**
Runs a shell command without using the output.
Uses the default shell.
- Parameter command: The command to run
*/
public func run(_ command: String) {
// Equivalent of piping to /dev/null; don't do anything with the string
_ = self.pipe(command)
}
/**
Runs a shell command and returns the output.
- Parameter command: The command to run
- Parameter shell: Path to the shell to invoke
*/
public func pipe(_ command: String, shell: String = "/bin/sh") -> String {
let task = Process()
task.launchPath = shell
task.arguments = ["--login", "-c", command]
let pipe = Pipe()
task.standardOutput = pipe
task.launch()
let data = pipe.fileHandleForReading.readDataToEndOfFile()
let output: String = NSString(
data: data,
encoding: String.Encoding.utf8.rawValue
)! as String
return output
}
}