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

Compare commits

...

413 Commits

Author SHA1 Message Date
dba2ce5bf3 🚀 Version 7.0.5 2024-10-31 22:15:11 +01:00
4644c1ada4 👌 Move cut-off date to PHP 8.4 release day 2024-10-31 22:14:53 +01:00
f3e1b4de6f 🚀 Version 7.0.4 2024-06-28 11:47:25 +02:00
a3226b632f 🐛 Restart PHP-FPM after managing extensions
This is done to prevent issues like #292.
2024-06-28 11:37:01 +02:00
652878d97f 👌 Remove unused WIP class 2024-06-28 11:14:20 +02:00
032610ad5c 🌐 Add localization for "Actions" 2024-06-28 11:12:55 +02:00
2c2627dc9f Add AppMenu class for easy access to main menu 2024-06-27 21:23:59 +02:00
62587bdf65 WIP: Add context menu to main menu 2024-06-11 18:53:55 +02:00
5e9dae78f5 🐛 Fix Bedrock project detection (#288) 2024-06-06 21:30:06 +02:00
949ba5b559 🔧 Build self-updater as part of main target
The self-updater is now a requirement for the main app to be able to be
built. You no longer need the existing binary. This makes it easier for
anyone to just try out the app locally and makes reproducible builds
also possible.

(This is done because the self-updater code will soon be moved to a
separate package, and I want to make this entire updater process
as simple as possible.)

In order to avoid the self-updater app from appearing as a product when
archiving a build, SKIP_INSTALL is set to true. This avoids a variety
of annoying issues including the archive appearing under "Other Items".
2024-05-31 23:54:42 +02:00
ce88f897ef 🌐 Added Chinese translations, credits
- Chinese translations contributed by @guanguans (via #285).
- Updated the credits so that all translators are now also listed
  separately, since the GitHub issue has been closed.
2024-05-02 10:24:47 +02:00
fa9b51aaab 🚀 Version 7.0.3 2024-04-10 13:07:58 +02:00
b8affad5ee 📝 Updated SECURITY.md 2024-04-10 13:07:30 +02:00
41e5f5b4c3 🔧 Bump build for EAP release 2024-04-09 12:33:55 +02:00
79f6a60a16 👌 Cleanup dependencies for site isolation 2024-04-03 14:50:05 +02:00
06bc4ddb9a 👌 Improve removing indirect dependency
Mind you, the interaction with the domain list controller also needs to
be abstracted away, but this is fine, for now.
2024-04-03 14:11:45 +02:00
bf728a24f0 👌 Fix PHP version suggestions in popover
- If your Valet installation supports using site isolation, that will
  be displayed as the suggestion. If not, the traditional "Switch to
  PHP" options will be available.

- Overflowing buttons have been fixed. Three columns are now used with
  a LazyVGrid, to prevent text from being unreadable.
2024-04-02 15:25:06 +02:00
b7cad3af62 👌 Fix Swift 5.10 concurrency warnings 2024-03-30 17:40:09 +01:00
4a3dee3c50 🚀 Version 7.0.2 2024-03-30 17:14:04 +01:00
9d5a0ed745 🔧 Xcode upgrade check 2024-03-20 13:46:56 +01:00
b3b509409a Fix test 2024-03-20 10:47:52 +01:00
4934f35d0b 🍱 Bump width of no results view 2024-03-19 14:23:19 +01:00
92e7418158 👌 Cleanup 2024-03-19 14:22:10 +01:00
52ea64db40 👌 Use base translation for no domain results 2024-03-19 14:20:34 +01:00
f66e9b7340 Add UnavailableContentView to domains 2024-03-19 14:10:08 +01:00
2bf28fe247 👌 Allow resizable windows 2024-03-16 00:32:05 +01:00
c6e4f785bc 🍱 Use PHP icon for phpman 2024-03-15 23:38:12 +01:00
94fe7df3bd 🍱 Custom button for "upgrade all" 2024-03-15 23:18:09 +01:00
f373621a4a 👌 Polish PHP installation management
- Make spinner view opaque to hide incorrect info
  when refreshing PHP installations
- Resolve each PHP installation to a Homebrew version
- Fix an issue where certain PHP upgrades don't show up

In this build, resolving PHP upgrades happens based on the formula name.

This avoids issues where available PHP version upgrades would *not* be
detected correctly (based on the installed version).
2024-03-15 22:09:59 +01:00
5104a865fb 🚀 Version 7.0.1 2024-02-12 18:28:45 +01:00
7b10973330 🔧 Bump build 2024-02-12 16:33:01 +01:00
bc208bddf9 👌 Notify if list of extensions is empty (#275) 2024-02-12 16:30:18 +01:00
321b4aaf8b 🐛 Fix extension detection on Intel (#275) 2024-02-12 15:26:48 +01:00
b26fc3bc4b 🚀 Version 7.0 2024-02-11 21:35:40 +01:00
f758c5d63a 👌 Cut off bottom of marketing screenshot 2024-02-10 16:18:58 +01:00
c7510d778d 🍱 Update marketing screenshot 2024-02-10 16:17:24 +01:00
70c5aadb7f 🔧 Bump build for PHP Monitor EAP 2024-01-25 13:48:39 +01:00
a731f15cf7 🐛 Prevent PHP dropdown from resetting 2024-01-21 14:42:06 +01:00
ab4c436202 🔧 Bump build for PHP Monitor EAP 2024-01-21 14:23:52 +01:00
c0231690d4 🐛 Fix alternative installation check 2024-01-21 14:22:51 +01:00
988e9d3351 👌 Cleanup and add comments 2024-01-13 13:24:06 +01:00
2f119d4332 Fix even more tests 2024-01-13 13:22:21 +01:00
d83c629a7b Fix some tests 2024-01-13 12:46:33 +01:00
e7d98dbeae 🔧 Bump build for PHP Monitor EAP 2024-01-09 21:24:50 +01:00
f3d5946743 🌐 Localize extension management (for domains) 2024-01-09 21:20:43 +01:00
7728a1125c 📝 Update README to reflect php = PHP 8.3 2024-01-09 21:00:06 +01:00
3612351df7 📝 Update README to reflect php = PHP 8.3 2024-01-09 20:59:52 +01:00
8e912151fb Allow toggling extensions via site list 2024-01-09 20:55:16 +01:00
3a2209e604 🌐 Add translations for language choice 2024-01-09 20:06:40 +01:00
1f0b56cab6 Language choice, prompt user to restart app 2024-01-09 20:01:08 +01:00
e08d970edd 🏗️ WIP: Load preferred language strings 2024-01-09 19:44:29 +01:00
32c757e711 🏗️ WIP: Language override option 2024-01-09 19:28:39 +01:00
480cdb94ae 🏗️ WIP: Language picker, fix SelectPreferenceView 2024-01-09 18:58:02 +01:00
7fbcac5dc2 👌 Check symlinks after PHP version modification 2023-12-27 14:47:21 +01:00
4edb5f5015 👌 Use generated asset catalog symbol extensions 2023-12-27 13:03:24 +01:00
294f84ccb2 👌 Further cleanup 2023-12-27 12:57:08 +01:00
155b57eb9e 🐛 Add incorrect PHP symlink purge (#270)
This commit introduces a new diagnostics feature which is executed
when the app boots. When the app boots, the integrity of the PHP
symlinks are checked to ensure that all symlinks correctly link to
a valid PHP version. If any links to an incorrect PHP version,
then those outdated or incorrect symlinks will be removed.

For example, if `php@8.2` links to `../Cellar/php/8.3.0` then that is
an obvious reason for that symlink to be purged because it links to
the incorrect version. (This example behaviour has been noted in #270.)

This occurs prior to the rest of the startup process, so this way
no incorrect PHP versions can pop up in the version switcher, and
no incorrect PHP version is reported as being installed when
managing installed PHP versions.
2023-12-27 12:40:35 +01:00
a459f015e1 🌐 Added translations for extension manager
(These were generated using OpenAI's API.)
2023-11-30 17:31:35 +01:00
27676f13f4 🔧 Fix build number 2023-11-30 17:14:36 +01:00
b4b2d7052f 🌐 Replace untranslated text w/ keys 2023-11-29 18:59:50 +01:00
6d25cf585e 🌐 Localize extension manager (English only) 2023-11-29 18:55:36 +01:00
ba04c94c05 👌 Cleanup UI code 2023-11-29 18:42:14 +01:00
13447ba533 Disallow installation if dependent exists 2023-11-29 18:35:31 +01:00
6f2e8f4b20 Check cutoff date for PHP version management 2023-11-27 21:39:41 +01:00
dc860074ef Parse extension dependencies 2023-11-27 21:38:38 +01:00
f586b8fcbe Allow choosing which PHP version to manage 2023-11-27 00:33:45 +01:00
94714c3e7a Extension manager responds to PHP change 2023-11-27 00:07:47 +01:00
904d05bdce 👌 Cleanup, fix loading issue 2023-11-26 23:51:39 +01:00
ec30bee72b Ask for confirmation before removing extensions 2023-11-26 22:39:44 +01:00
2fe3a4b7eb Perform cleanup when removing extensions 2023-11-26 22:03:25 +01:00
a7d5950aa0 Test synchronous shell output 2023-11-26 21:48:40 +01:00
e8306289ce Load extension info for all PHP versions
In order to make this possible, I've added a new `sync()` method to the
Shellable protocol, which now should allow us to run shell commands
synchronously. Back to basics, as this was how *all* commands were
run in legacy versions of PHP Monitor.

The advantage here is that there is now a choice. Previously, you'd
have to use the `system()` helper that I added.

Usage of that helper is now discouraged, as is the synchronous
shell method, but it may be useful for some methods where waiting
for the outcome of the output is required.

(Important: the `system()` helper is still required when determining
what the preferred terminal is during the initialization of the
`Paths` class.)
2023-11-26 21:26:48 +01:00
ff61d8c52e 🚀 Version 6.2.2 2023-11-24 23:30:12 +01:00
da41673855 Fix broken tests 2023-11-24 23:29:25 +01:00
5bda727981 🔧 Bump version 2023-11-24 22:58:04 +01:00
23cf575026 ⬆️ Upstream PHP 8.3 upgrade changes to v7.0 2023-11-24 22:57:33 +01:00
d3053b8fe3 Allow for seamless upgrade to PHP 8.3
The older version (8.2 in most cases) that becomes obsolete will also
be reinstalled and the app will attempt to switch to the last active
version as well. It is likely that PHP Monitor will have to repair
your older PHP installations after upgrading the `php` formula, but
this too should be a seamless process.
2023-11-24 22:26:33 +01:00
7159ca8612 🐛 Correctly handle mismatches when upgrading PHP 2023-11-24 20:23:12 +01:00
141c06d14b 🐛 Update PHP alias 2023-11-24 18:35:36 +01:00
94c84aaab3 👌 Tweak text 2023-11-22 21:37:11 +01:00
9ca16e72d5 Show external extensions 2023-11-22 21:29:51 +01:00
67a00f979a 📝 TODO items as warnings 2023-11-21 22:37:28 +01:00
1e4c45dcbd 🍱 Tweak UI of extension list 2023-11-21 22:36:05 +01:00
87c44f3ae3 Allow installation and removal of extensions (#266) 2023-11-21 22:18:28 +01:00
f39732a0e6 🏗 WIP: UI for searchable extensions 2023-11-21 20:03:50 +01:00
3b78ac43d7 👌 Fixed various lint issues 2023-11-21 18:19:17 +01:00
1f19b81530 🔀 Merge branch 'dev/6.x' into dev/7.x 2023-11-21 18:12:10 +01:00
d714d7ad4c 👌 Install additional taps
The following taps are now automatically installed:

- `shivammathur/php`
- `shivammathur/extensions`
2023-11-21 18:11:18 +01:00
4dce6c033e 🌐 French localization fixes, onboarding size fix 2023-11-21 17:41:47 +01:00
72a8a1e382 🔧 Tweak which formulae to install for PHP 8.3 and PHP 8.0
- Since PHP 8.0 is now EOL, it will be installed via the tap.
- Since PHP 8.3 is now stable, it will be installed without the tap.
2023-11-21 17:41:41 +01:00
07b17f3f84 🌐 French localization fixes, onboarding size fix 2023-11-21 17:40:53 +01:00
7f0f7ff3e9 🔧 Tweak which formulae to install for PHP 8.3 and PHP 8.0
- Since PHP 8.0 is now EOL, it will be installed via the tap.
- Since PHP 8.3 is now stable, it will be installed without the tap.
2023-11-21 17:15:36 +01:00
c7c143c760 🌐 Added French translation
With contributions from @tplesnar and @nhedger
2023-11-21 17:12:44 +01:00
ee050af364 🌐 Added French translation
With contributions from @tplesnar and @nhedger
2023-11-21 17:12:29 +01:00
f7e2551587 🏗 WIP: Adjust extension manager view 2023-11-21 17:11:28 +01:00
cc0cc21e5f 🏗️ WIP: Cleanup 2023-11-13 17:44:11 +01:00
883ea05bd1 🏗️ WIP: Extension manager UI (rough) 2023-11-13 13:14:27 +01:00
641bddfce7 Add UI test for PHP config editor 2023-11-07 18:21:14 +01:00
2f7223fba5 🔥 Remove unused ProgressWindowView 2023-11-07 18:08:12 +01:00
3b23ce7805 👌 Even more cleanup 2023-11-07 18:04:13 +01:00
a634d083a6 ⬆️ Adopt #Preview, cleanup PHP Version Manager 2023-11-07 17:56:38 +01:00
9a3dd2fa22 🐛 Fix extensions toggle (#265) 2023-11-02 17:26:46 +01:00
8790b30706 🚀 Version 6.2.1 2023-11-02 17:17:40 +01:00
c42188b717 🔧 Bump build 2023-11-02 17:07:42 +01:00
cc251686f9 🐛 Fix extensions toggle (#265) 2023-11-02 13:33:24 +01:00
6fd6241567 🔧 Start of v7.x branch, updated version number 2023-11-01 12:35:24 +01:00
c8ab2e67f6 ♻️ Various refactoring 2023-11-01 12:33:34 +01:00
f82ab913c6 Detect which extensions are available 2023-10-31 20:40:11 +01:00
58943148fa 🏗️ WIP: Detect which extensions are available 2023-10-30 20:21:50 +01:00
8a46b9d374 Experimental versions can graduate to stable 2023-10-30 19:56:32 +01:00
a62ebcff92 🐛 Ensure isBusy is isolated to main thread
There was an issue where updating a configuration file (including .ini)
or having an action occur that marked PHP Monitor as busy would prevent
the icon from updating correctly. This happened because access to the
busy boolean state variable could happen from various threads.

Since adding main thread isolation to the variable, you must access
`PhpEnvironments.shared.isBusy` via the main thread, therefore ensuring
that the busy state that is read from the app is always synchronized
and accurate whenever it is checked, making it so that going from
busy to no longer busy can no longer fail.

(It was possible for work in another thread to complete and fail to set
the icon to "not busy" because the work was done faster than the icon
could be set to busy.)
2023-10-30 19:34:36 +01:00
541378f3f9 🔧 Mark PHP 8.3 as stable for official release 2023-10-29 12:36:19 +01:00
e6f1d7e834 📝 Update SECURITY.md 2023-10-29 12:35:09 +01:00
20d19f2f92 🚀 Version 6.2.0 2023-10-27 17:10:43 +02:00
91bc347e57 🐛 Fix tests, fix issue with window to front 2023-10-26 19:31:55 +02:00
e05300b25b 🔧 Bump build 2023-10-26 18:55:21 +02:00
1ae7a20870 👌 Adjust error message 2023-10-26 17:14:23 +02:00
5594130ccd 👌 Cleanup filesystem watcher code 2023-10-24 18:24:18 +02:00
b9c7cdb3cc Generate Fish-friendly scripts (#264) 2023-10-20 17:34:19 +02:00
00b4760b85 🔧 Bump build to 1330 2023-10-07 13:02:56 +02:00
9a35014d2a Warn about Laravel Herd running & conflicts
It's perfectly possible to run Laravel Valet (standalone) and Laravel
Herd side-by-side or mix usage, but it's not recommended and/or
supported (especially since Herd recommends migrating away from regular
Valet and may terminate Valet's services).

To avoid issues, PHP Monitor won't work if Herd is currently running.
It's totally fine to relaunch Herd after PHP Monitor's done starting
up, since the check only happens at launch.

Also, PHP Monitor warns about the PATH changes in `~/.zshrc` after 
installing Laravel Herd, because that may impact the usage of
PHP aliases/global PHP version in terminal provided by PHP Monitor.
2023-10-07 13:02:22 +02:00
7cba25b52e 👌 Cleanup 2023-10-03 20:40:59 +02:00
c6c3996c7b 🐛 Fixed detection order (#263)
Dictionary key order in Swift isn't a thing, so the process is
now a two-pronged approach: 1) check for specific apps, 2) check for
specific broad frameworks after no other matches were made.

Previously, detection would only work correctly some of the time.

Also cleaned up the `getNotableDependencies()` method.
2023-10-03 20:37:47 +02:00
03c96a1d16 👌 Fixes after upgrading to Xcode 15 2023-09-19 12:40:43 +02:00
a6fa4b240f Allow editing of limits 2023-09-13 18:13:51 +02:00
7e78026d06 🏗️ WIP: PHP Config Editor
- Has UI height rendering issues (w/ SwiftUI)
- Needs debounce on UI elements
- Cannot currently persist modified settings
- Cannot display On/Off settings
- Cannot display regular text settings
2023-09-12 19:26:10 +02:00
d5888c1c7a 🚀 Version 6.1.0 2023-09-10 11:15:14 +02:00
e40b9fe45a Add UI test for PHP version manager 2023-09-10 11:14:38 +02:00
f5d0ad20cd 🔧 New EAP build 2023-09-07 19:17:36 +02:00
0615927f2f 📝 Update README.md 2023-09-07 19:16:41 +02:00
3d1806c094 📝 Update SECURITY.md 2023-09-07 19:16:07 +02:00
8a57557074 🐛 Avoid duplicate site insertion (#261) 2023-09-07 18:55:54 +02:00
19f4819450 🌐 Formal Dutch, initial TL pass (je -> u) 2023-09-06 19:07:16 +02:00
aa8309dd9a 🌐 Localization fixes
- Adds a fallback for missing keys
- Make type column resizable on domains
- Localized "Open with" text in context menu
- Updated some Dutch translations
2023-09-06 18:53:26 +02:00
7977a4e177 🏗️ WIP: Reading of bitsize config entries 2023-09-04 18:39:32 +02:00
51c100f6fe 👌 Ensure all fields error out (#258) 2023-09-01 01:11:38 +02:00
aebfc9dd09 👌 Domain column now sorts based on name (#259) 2023-09-01 01:09:59 +02:00
f9acbd34d0 👌 Various fixes
- Fixed issue with Version Manager
- Separate behaviour for previews for Version Manager
- Remove verbose logging when previews are in use

(Note: The latter change may break various other previews. Will need to
investigate this particular issue.)
2023-09-01 01:01:06 +02:00
eb566bb523 Allow pre-release builds to be installed 2023-08-31 19:05:40 +02:00
528f213f17 Finalized initial config UI without bindings 2023-08-30 21:05:37 +02:00
f8e6aa988e 🏗️ Mapping more PHP configuration values 2023-08-30 20:20:08 +02:00
93e841735e 🏗️ SwiftUI and config views 2023-08-30 19:59:21 +02:00
cb28243181 🔧 Bump build, fix layout issue for prefs 2023-07-29 20:45:20 +02:00
fc68e37458 🌐 Updated translations
- Portuguese translations provided by @joseborges
2023-07-29 20:40:55 +02:00
ae6736102a 🏗️ Further experiments with SwiftUI 2023-07-25 19:53:37 +02:00
3ef1a6e60d 🏗️ Example of what a preference view might be 2023-07-25 19:24:04 +02:00
5e7c7bc903 🔧 Always use module name PHP_Monitor 2023-07-25 19:06:47 +02:00
94f3c1c7c5 📝 Fix FAQ ("PHP Version Manager") 2023-07-20 18:18:30 +02:00
20aad90ba9 📝 Update README 2023-07-20 18:17:37 +02:00
8bd85d8354 📝 Add info about Laravel Herd 2023-07-20 18:14:21 +02:00
90b068d200 ♻️ Modular approach 2023-07-18 19:56:09 +02:00
943b5aa6af ♻️ Code reorganization
It was necessary to do some summer cleaning. Here's what's changed:

* First, I'm taking a new modular approach to Swift-based components
  that are part of PHP Monitor.

* I've fixed the naming of various parts of the app. I plan on doing
  an even deeper check in the future. The following are affected:
  - "PHP Formulae Status" is now known as "PHP Version Manager".
  - "Warnings List" is now known as "PHP Doctor".
  - The associated window controllers have also been updated.

(I've also added a new module: "PHP Config Editor". We'll see what that
brings in the future... but the main purpose will be to edit key PHP
configuration values without needing to go to the .ini files.)
2023-07-18 19:52:15 +02:00
4bf475bae2 👌 Remove appcast since it is no longer expected 2023-07-09 15:22:47 +02:00
125b9bb198 Add message about failing to load info (#258) 2023-07-07 20:10:41 +02:00
72cbf6996d 🔧 Removed usage of Base Internationalization 2023-06-26 21:29:28 +02:00
e7cc940f65 🌐 Updated translations
- German translations provided by @dsturm
- Vietnamese update provided by @xuandung38
2023-06-26 21:23:00 +02:00
c8323a8c27 🔧 Bump build to 1300, remove launch language 2023-06-26 14:45:06 +02:00
6805855f03 🌐 Add translations for preferences tabs 2023-06-26 14:43:45 +02:00
db101f5a66 🔧 Bump to version 6.1 2023-06-26 14:33:37 +02:00
2302d5a5ee 🌐 Consistency in Dutch translations (WIP) 2023-06-26 14:33:29 +02:00
5cfb0f452c 🌐 Localized columns in domains list 2023-06-26 14:31:57 +02:00
7da20b4f20 🌐 Updated localization 2023-06-26 14:14:37 +02:00
f1b037ce26 🌐 Localization
- Dutch WIP by me (project now runs debug with Dutch localization)
- Vietnamese provided by @xuandung38
- Added `phpman.services` status to TL
2023-06-26 10:43:45 +02:00
e59347ed7f 📝 Update README 2023-06-02 21:31:51 +02:00
206dff289f 🚀 Version 6.0.1 2023-05-30 17:26:09 +02:00
02f579fe81 🐛 Don't load services info when standalone (#253) 2023-05-30 17:04:03 +02:00
2a74b11462 🐛 Ensure Valet check occurs (#252) 2023-05-29 20:45:05 +02:00
371f98b875 🚀 Version 6.0 2023-05-28 10:28:02 +02:00
7955c777e7 📝 Updated credits w/ sponsors
The list of sponsors includes a list of all public sponsors.

(Private sponsors have been omitted from the list.)
2023-05-27 12:46:39 +02:00
5c9b06d83b 👌 Missing localized strings 2023-05-27 12:43:31 +02:00
3c7bed0a9b 🔧 Bump build number for release version 2023-05-27 12:11:59 +02:00
54f83a0aed 🐛 Prevent operations when PHP Monitor is busy 2023-05-27 11:57:41 +02:00
b041ca37be 🐛 Disable Sites menu item when Standalone 2023-05-27 11:53:11 +02:00
2b2b027317 📝 Update README 2023-05-26 21:27:36 +02:00
cdbd959159 👌 Update strings 2023-05-26 20:49:18 +02:00
6fc613ac4c 🐛 Own /bin and /sbin folders specifically 2023-05-24 19:29:38 +02:00
8240b676c1 🐛 Own the entire Homebrew formula directory 2023-05-24 19:19:13 +02:00
cbebf75b48 🐛 Fix error message, check sbin folder ownership 2023-05-24 19:17:41 +02:00
40c24793f5 🐛 Show "Please wait" text when running command 2023-05-24 19:08:41 +02:00
6a921d8e3e 🐛 Use PHP Guard when removing a PHP version 2023-05-24 19:05:58 +02:00
a3368effec 🐛 Fix async issue when PHP Guard reset kicks in
Whenever PHP Guard is used to reset the PHP version when a different PHP
version is installed using the PHP Version Manager, it would previously
kick its version switching process off asynchronously as a separate task
which meant that the app would go into "ready" state too soon. Now this
is considered a blocking task that the app will wait for (async) before
turning the app back into its "ready" state again.
2023-05-24 18:59:49 +02:00
7f4c6878e4 📝 Update README 2023-05-22 20:12:49 +02:00
82626b7174 🔧 Bump build for new DEV build 2023-05-22 17:14:21 +02:00
326e5c58e2 🐛 Empty byte count now returns a warning symbol 2023-05-19 11:49:50 +02:00
2848b4dcd2 🔥 Cleanup and Valet checks after version switch 2023-05-18 12:45:13 +02:00
a8cf6daa94 👌 Check for /usr/local/homebrew directory (#251)
For this to work, Homebrew cannot be installed in /usr/local, which
means that the /usr/local/Cellar folder will be missing and the folder
/usr/local/homebrew will exist.
2023-05-17 20:22:22 +02:00
c4749673c9 🔧 Bump build number for EAP 2023-05-17 20:13:13 +02:00
20a0059f73 👌 Alter update notification for DEV & EAP 2023-05-17 20:10:41 +02:00
402e65f82d ⬆️ Apply recommendations about dead code stripping 2023-05-17 20:02:57 +02:00
29d17b3880 Ensure all UI tests pass 2023-05-15 19:44:50 +02:00
7f04dd5fcb Fix tests 2023-05-15 19:15:02 +02:00
0fe9281e3c 🔧 Fix deploy targets for tests 2023-05-15 19:06:58 +02:00
f88035b425 ♻️ Rename PhpEnv to PhpEnvironments 2023-05-15 19:04:05 +02:00
2b7bb3f352 👌 Show repair button in version manager 2023-05-14 12:04:25 +02:00
c7ee4b8838 Update README, check for broken formulae 2023-05-14 11:47:35 +02:00
ab993efbde 🐛 Ensure quit menu item is always available 2023-05-13 13:09:28 +02:00
c6f49de70c 🔧 New build 2023-05-13 13:06:01 +02:00
8b79dc44d0 🏗 Avoid dependents check for reinstall 2023-05-11 22:42:56 +02:00
458b952787 🏗 Keep track of Homebrew error log 2023-05-11 22:21:44 +02:00
67ec63212c 🏗 WIP: Cleanup 2023-05-11 22:08:42 +02:00
104f3a7d8c 🐛 Fix a bug related to installing PHP versions 2023-05-11 21:20:22 +02:00
d2304323fe ♻️ Use new InstallAndUpgradeCommand 2023-05-11 21:11:38 +02:00
76d078735c 🏗 Allow switching silently, add operation title 2023-05-11 20:16:07 +02:00
1eeba747cf 🏗 Copy logic from InstallPhpVersionCommand 2023-05-10 19:55:40 +02:00
64c259d804 🏗 HomebrewOperation now allows (attempted) repair of broken PHP 2023-05-10 19:24:11 +02:00
b307251f81 🏗 Health check in UI 2023-05-06 21:03:13 +02:00
bf610b6c4e 🏗 PHP installation health check 2023-05-06 12:52:10 +02:00
518fb16f23 🏗 Cleanup 2023-05-02 20:17:12 +02:00
24ef7eacfe 🏗 Show banner to update all versions 2023-04-30 10:30:00 +02:00
050c154894 🏗 WIP: Use BrewFormula struct for operations 2023-04-28 14:42:51 +02:00
18496de104 🏗 WIP: Change the way Homebrew operations run 2023-04-28 14:22:05 +02:00
a21418a608 🏗 WIP: Keep track of and restore linked PHP version 2023-04-26 19:49:22 +02:00
f9ee63ddf6 🏗 WIP: Separate code into PhpGuard class 2023-04-25 21:00:02 +02:00
b7de54dfa7 🏗 WIP: Rework folder ownership operations 2023-04-23 12:01:30 +02:00
2d0deed4fd Add "Standalone Mode" menu item 2023-04-22 10:13:16 +02:00
4b1fc1a5ce 👌 Improve visibility of Standalone Mode 2023-04-22 10:08:53 +02:00
227131bd7e 📝 Update info about using PHP Manager 2023-04-17 18:42:42 +02:00
f48a265bbf 🔧 New bug report format 2023-04-17 18:12:36 +02:00
0c3b68734c 🔧 New bug report format 2023-04-12 13:48:22 +02:00
7e5cbadc09 👌 Various improvements to PHP Version Manager
- A warning has been added if you are not running Homebrew 4.0, since
  running older (or newer) versions of Homebrew are not officially
  supported. This check is only displayed once per cold app boot.

- The PHP Version Manager now shows the full version number for
  up-to-date PHP installations (mostly important for patch version).

- You must now confirm the deletion of an installed version of PHP
  before PHP Monitor will uninstall that version.

- It is no longer possible to press the refresh button if the app
  is already busy checking for updated PHP versions
2023-04-04 20:50:51 +02:00
9f608439fc 🏗 WIP: Upgrade command dry-run check 2023-04-03 20:15:13 +02:00
8b0aeef2e6 🚀 Version 5.8.1 2023-03-29 18:05:03 +02:00
aa406434d0 🔧 Bump build 2023-03-29 18:04:27 +02:00
4bca47a6d9 Add early access release notes 2023-03-27 19:37:47 +02:00
c5e8c4c4a6 Add helpful dialog for What's This 2023-03-27 18:51:17 +02:00
85d7c8f9a3 🏗 Added #warning for disabled notifications 2023-03-25 12:42:45 +01:00
4c7361e635 🔧 Bump PHP Monitor Self-Updater 2023-03-24 01:05:15 +01:00
6413287606 👌 Fix updater bundle logic, skip release notes 2023-03-24 01:03:19 +01:00
ff8eb4fa04 👌 Use app colors for status 2023-03-24 00:47:27 +01:00
6b72d4da65 👌 Disallow installation of linked PHP 2023-03-24 00:40:53 +01:00
b456fdc65c 🐛 Valet specific file checks 2023-03-24 00:30:53 +01:00
0e3c9a5a68 Added new target, did some testing 2023-03-24 00:21:45 +01:00
7cf8d4697f 👌 Add icon for early access builds 2023-03-23 20:05:30 +01:00
34e5a97155 👌 Avoid sponsor messages on EAP 2023-03-23 19:57:24 +01:00
ff2c2c9b69 🐛 Fix unsafe usage of try for path handling 2023-03-23 19:09:32 +01:00
d320c49092 👌 Avoid force unwrapping try (may crash) 2023-03-23 19:08:39 +01:00
966033e052 🐛 Prevent crash upon parsing invalid Valet directories
This fixes #247, which can be caused when certain folders are not
accessible for some reason. This can occur due to network reasons,
but also because the linked folder in question is iCloud Drive.
2023-03-23 18:00:45 +01:00
4e095a5ae5 👌 Fixed build URL 2023-03-23 15:17:11 +01:00
6aff283d08 👌 Sync global & window busy indicator 2023-03-22 21:58:50 +01:00
9c8da2aa1c Mark as busy 2023-03-22 21:49:55 +01:00
5755d608f8 👌 Add updater for Early Access builds 2023-03-22 21:47:55 +01:00
29fcc66cba Added EAP target 2023-03-22 21:43:20 +01:00
3a826b7e51 🏗 WIP: Remove command 2023-03-21 22:15:05 +01:00
2d8ad9e9bc 🏗 WIP: Move commands to separate files 2023-03-21 21:13:35 +01:00
64b3c4e9bb ♻️ Refactor Brew commands 2023-03-21 21:09:20 +01:00
f3b3dcf449 👌 Various UI improvements 2023-03-20 16:37:27 +01:00
0bdbc0a056 👌 Make PHP version manager previewable 2023-03-19 23:48:12 +01:00
4f11f3d8d3 🏗 WIP: Async formulae 2023-03-19 21:54:49 +01:00
e1eb61859e Display window, load info for all PHP versions 2023-03-19 19:02:34 +01:00
2939a2ab28 🏗 WIP: Add overlay UI 2023-03-19 16:51:46 +01:00
08dcfb36f4 🏗 WIP: Formulae manager UI changes 2023-03-19 14:00:15 +01:00
f8b605f749 👌 Parse upgradable versions 2023-03-18 02:11:57 +01:00
8f1304308d 🏗 WIP: Parsing version updates 2023-03-17 20:38:21 +01:00
7e04f8b881 👌 More UI workshopping 2023-03-17 17:57:31 +01:00
9dae03a04e Homebrew version validation, UI workshopping 2023-03-17 17:40:30 +01:00
78c24555f7 👌 Bring window to front, avoid crashing 2023-03-16 21:09:21 +01:00
41af058661 👌 Improved logging, HOMEBREW_NO_INSTALL_UPGRADE 2023-03-16 19:26:24 +01:00
7c192730e1 📝 Update SECURITY 2023-03-15 20:09:32 +01:00
bb56e33ee8 📝 Update SECURITY 2023-03-15 20:09:10 +01:00
84902f3a42 🔀 Merge additional tests into MainMenuTest 2023-03-15 20:07:35 +01:00
8775e70178 📝 Update README 2023-03-15 20:06:43 +01:00
de4cefd1b9 📝 Valet 4 has been released 2023-03-15 20:03:36 +01:00
ebb04001d0 👌 Check if ownership needs to be taken 2023-03-15 19:37:32 +01:00
bd23f65668 👌 Ensure removal occurs after permissions fix 2023-03-15 19:16:49 +01:00
22295ed55a 📝 Wrote about problems with PHP version removal 2023-03-14 22:19:25 +01:00
2bd5b8f79e Ensure Valet configuration files exist 2023-03-14 22:10:48 +01:00
34e9e3f829 👌 Improve progress view 2023-03-13 23:23:22 +01:00
016f36a8fd 🏗 WIP 2023-03-13 21:24:36 +01:00
862add8512 🏗 WIP: Ensure unsupported PHP can be removed 2023-03-13 21:21:29 +01:00
6dabcd7668 🏗 WIP: Properly install formula 2023-03-13 21:11:56 +01:00
7b7a5e5236 👌 Did someone say "out of range"? 2023-03-13 21:08:24 +01:00
a6aecff557 🏗 WIP: Fix omission of php formula 2023-03-13 21:04:18 +01:00
81ed154db1 🏗 WIP: Discern installation status
Depending on the installation status, PHP Monitor should be able to
find out which versions of PHP can be installed and removed.
2023-03-13 21:03:13 +01:00
e0b574b33d 🏗 WIP: Experiments, progress view 2023-03-11 13:08:48 +01:00
f414c723e4 Open additional windows 2023-03-06 22:11:44 +01:00
f3ef1da2bf Click through the preferences panes 2023-03-06 22:01:58 +01:00
f7c716096c Fix issue with php-config binary being missing 2023-03-04 00:37:45 +01:00
54630c222b Fix issue with php-config binary being missing 2023-03-04 00:37:02 +01:00
1fc63e0471 🔀 Merge branch 'dev/5.8-tests' into dev/6.0 2023-03-04 00:29:57 +01:00
127d5f4494 Improve tests 2023-03-03 23:11:40 +01:00
13ee618d5c 🚀 Version 5.8.0 2023-03-03 16:49:38 +01:00
ed1d7f8aed Test improvements 2023-03-03 16:49:17 +01:00
063a729d67 🐛 Fix user initiated issue 2023-03-03 15:28:34 +01:00
d1498eb070 🐛 Fix issue with parsing CaskFile 2023-03-03 14:44:15 +01:00
7a02fb8a1a 🏗 WIP 2023-03-03 00:48:48 +01:00
b925262620 🏗 WIP 2023-03-02 20:53:39 +01:00
aaa814ac9c 🏗 WIP: Respond directly to PHP binary changes 2023-03-02 20:27:30 +01:00
870868bacf 🔀 Merge branch 'dev/5.8' into dev/6.0 2023-03-02 16:30:58 +01:00
e149a2500e 📝 Updated README 2023-03-02 16:27:26 +01:00
aef7d4021c 🔧 Bump build 2023-03-01 19:22:26 +01:00
a25f8c9748 👌 Use same cleanup code as used on 5.8 branch 2023-03-01 19:21:40 +01:00
050c489158 🐛 Prevent upgrade file removal from crashing the app 2023-03-01 19:20:13 +01:00
61af6e2dc0 🔀 Merge branch 'dev/5.8' into dev/6.0 2023-03-01 19:18:12 +01:00
130bf38dd9 👌 Clean up HOMEBREW_DIR/Caskroom directory
Only when using the self-updater, of course.

Also include a description that reflects this cleanup scenario.
2023-03-01 19:17:18 +01:00
a6c3f0ceba 🍱 Use classic DEV channel icon 2023-02-27 19:16:15 +01:00
defb6e357a 🔀 Merge branch 'dev/5.8' into dev/6.0 2023-02-27 19:13:12 +01:00
f274e9e004 👌 Updated IAP for accessing formulae.brew.sh 2023-02-27 19:08:04 +01:00
a07881a987 👌 Fix copy of missing config check 2023-02-27 19:04:50 +01:00
ae9863b962 👌 Use classic DEV channel icon again 2023-02-26 23:28:24 +01:00
d679c7e75c 👌 Tweak onboarding for Valet-less users 2023-02-26 22:56:23 +01:00
47d921547f 👌 Move Valet-specific check to Valet class 2023-02-26 22:28:40 +01:00
bc22129399 👌 Improve logic in InternalSwitcher 2023-02-26 15:25:06 +01:00
98fcb686bf 👌 Avoid certain things when Valet-less 2023-02-26 15:23:10 +01:00
715b674929 Fix tests 2023-02-26 15:02:43 +01:00
089ebe7b4e 👌 More strict Valet check, remove print 2023-02-26 15:02:29 +01:00
608525209b Remove unused test that caused failing suite 2023-02-26 14:50:31 +01:00
c7eb1d5ce5 Fix tests 2023-02-26 14:49:18 +01:00
81eb2fee90 👌 Tweak logging 2023-02-26 12:41:16 +01:00
2eea15aac2 👌 Improve WarningListView and modify body 2023-02-25 15:57:39 +01:00
eb664477f9 👌 Improve async PHP Doctor 2023-02-25 15:48:01 +01:00
e1adcbcde6 👌 Improve PHP Doctor w/ async code
- This fixes an issue with PHP Doctor's "Scan Again" button not working.
- This also adds a new check which verifies if "php.ini", "php-fpm.conf"
  and "php-fpm.d/valet-fpm.conf" exist (all required files).
2023-02-24 19:41:10 +01:00
cb504cc3f0 🔧 Bump build 2023-02-24 19:37:41 +01:00
7c08a2fdfe 👌 Improve PHP Doctor w/ async code
- This fixes an issue with PHP Doctor's "Scan Again" button not working.
- This also adds a new check which verifies if "php.ini", "php-fpm.conf"
  and "php-fpm.d/valet-fpm.conf" exist (all required files).
2023-02-24 19:36:12 +01:00
3f14754177 📝 Update README to reflect #239 2023-02-21 18:18:23 +01:00
2f47610c85 🔧 Use macOS 12.4 deployment target 2023-02-21 02:27:03 +01:00
b5758dfca7 🔧 Use macOS 12.4 deployment target 2023-02-21 02:26:45 +01:00
2d4112708e ♻️ PHP Guard changes 2023-02-20 18:16:39 +01:00
cc6324b692 ♻️ PHP Guard changes 2023-02-20 18:15:12 +01:00
15d75a7f98 🔧 Bump build 2023-02-17 17:21:13 +01:00
32a44524ef 🐛 Ensure checkbox shows correct initial state 2023-02-17 17:20:05 +01:00
c7c5311ff9 🐛 Ensure checkbox shows correct initial state 2023-02-17 17:19:56 +01:00
26a097ed07 On macOS 13 and newer, add "Start at login" (#210) 2023-02-15 20:37:24 +01:00
c93f047909 🔧 Bump build 2023-02-15 20:32:56 +01:00
f82a2120f7 🔧 Add display name 2023-02-15 20:32:37 +01:00
857cba9f45 On macOS 13 and newer, add "Start at login" (#210) 2023-02-15 20:24:01 +01:00
b0de0c04c6 🚀 Version 5.7.4 2023-02-14 18:53:32 +01:00
ec49257bcc 🔧 Bump build 2023-02-13 17:43:52 +01:00
5fc159ae5a 🐛 Adjusted for new Homebrew JSON output (#235) 2023-02-13 17:38:46 +01:00
9c6a21008a 🐛 Adjusted for new Homebrew JSON output (#235) 2023-02-13 17:38:09 +01:00
f27e07fc78 🔧 Bump build 2023-02-13 17:30:37 +01:00
9fceab3e2e 🐛 Adjusted for new Homebrew JSON output (#235) 2023-02-13 17:30:01 +01:00
67a91e1211 👌 Fix warnings and update schemes 2023-02-11 20:48:14 +01:00
babb734c25 👌 Do not load identity asynchronously 2023-02-11 20:47:17 +01:00
5dffbf57d1 👌 Do not load identity asynchronously 2023-02-11 20:46:05 +01:00
21a1d6576e 🔧 Bump build 2023-02-10 19:36:42 +01:00
6456741880 Add support for wildcard constraints (#224) 2023-02-10 19:31:20 +01:00
b08912ce11 Add support for wildcard constraints (#224) 2023-02-10 19:31:07 +01:00
f6d2f09b8d 👌 Improve first launch onboarding experience 2023-02-07 22:02:45 +01:00
7285d24ef3 👌 Improve first launch onboarding experience 2023-02-07 22:02:34 +01:00
ac60c66bb9 🐛 Add missing strings for update 2023-02-06 19:33:41 +01:00
03fdf23f0a 🚀 Version 5.7.3 2023-02-06 19:15:25 +01:00
412b7bad5c 🐛 Fix generated script (#231) 2023-02-06 19:13:49 +01:00
9a7575790a 🔧 Bump build 2023-02-06 19:12:34 +01:00
a36487f6e0 🐛 Fix generated script (#231) 2023-02-06 19:10:24 +01:00
cd5cbccb04 🐛 Fix generated script (#231) 2023-02-06 19:10:10 +01:00
e7e8658ea6 📝 Update README about new updater 2023-02-05 18:53:35 +01:00
20291bf034 📝 Update README about new updater 2023-02-05 18:53:24 +01:00
762527ece9 👌 Add quotes around paths 2023-02-05 18:43:13 +01:00
2c40f433d3 Add updater to project
If you want to see the source code to the updater, you can find it here:
https://github.com/nicoverbruggen/phpmon-updater

Starting with version 6.0, the code of the updater will be included
in this repository.
2023-02-05 18:37:18 +01:00
5ac4817048 👌 Add built app to .gitignore 2023-02-05 18:17:07 +01:00
300880f3e5 ♻️ Reworked updater 2023-02-05 18:00:37 +01:00
78e682688b 🏗 Improved version comparison 2023-02-05 17:18:09 +01:00
208a430066 🏗 Parse CaskFile using regex 2023-02-05 13:16:04 +01:00
5c92d47ff0 Parse CaskFile, WIP for new AppUpdater 2023-02-04 22:13:43 +01:00
121a227510 👌 Create the updater directory to prevent crash 2023-02-04 14:47:19 +01:00
f8642b21e6 🏗 Test build 2023-02-02 19:50:41 +01:00
b182218cad Notify about installed update 2023-02-02 19:46:07 +01:00
0bb3e5c173 📝 Update information about build 2023-02-02 19:34:18 +01:00
e541dced4a Add Self-Updater to gitignore 2023-02-02 19:32:11 +01:00
04c6cef277 🔥 Remove signed Self-Updater from repository 2023-02-02 19:31:12 +01:00
6d1ea4e93b Add timeout for updater 2023-02-02 19:27:58 +01:00
4fd48baf63 Use concurrency for updater 2023-02-02 19:22:52 +01:00
1260022d51 Working self-updater (needs refactor) 2023-02-02 12:20:12 +01:00
ca72b79924 🏗 Initial working version of the updater
This class is in dire need of a refactor, however.
2023-02-02 12:14:30 +01:00
1a17a275d4 🏗 Add instructions for next step 2023-02-02 02:02:21 +01:00
202f9ed9d1 🏗 Add 'Install Update' button to UI 2023-02-02 01:57:38 +01:00
881d863bb8 👌 Add notarized updater & include in target 2023-02-02 01:35:41 +01:00
ef37876508 🍱 Update icon for self-updater 2023-02-02 01:23:48 +01:00
0c52720e55 🏗 Cleanup zip and manifest 2023-02-02 01:18:30 +01:00
744ec95630 🏗 Do not make assumptions about zip contents 2023-02-02 01:14:53 +01:00
69ff907f4a 🏗 Read release manifest 2023-02-02 01:04:20 +01:00
57c90c216f 🏗 Download and validate checksum 2023-02-02 00:47:53 +01:00
8df126a7b0 🏗 Working proof-of-concept updater if zip exists 2023-02-01 20:12:04 +01:00
173206bed9 🏗 Let self-updater quit PHP Monitor first 2023-02-01 19:31:04 +01:00
8fa270fd54 🏗 Allow self-updater to launch PHP Monitor 2023-02-01 19:22:20 +01:00
92509b5a84 🏗 Rename Affinity asset 2023-02-01 19:06:59 +01:00
c609022c9b Add target and app for self-updater 2023-02-01 18:23:59 +01:00
bbbbe7555b 🔀 Merge branch main into dev/6.0 2023-01-31 18:24:08 +01:00
1216fe4974 🐛 Avoid duplicate verbose mode output 2023-01-31 18:20:44 +01:00
147407666d 🔥 main branch cleanup
After the last merge, there was one file I accidentally included that
doesn't need to be here: the legacy ServicesManager class! In order to
ensure that 5.7, 6.0 and `main` branches are somewhat in order and
easy to merge, I have now removed this file.
2023-01-31 18:15:00 +01:00
f9363dd35b 🔀 Merge branch 'dev/5.7' into dev/6.0
This merge brings all of the changes from 5.7 into the 6.0 branch,
while avoiding the need for cherry picking now that the conflicts
have been resolved.
2023-01-31 18:11:04 +01:00
7be6a335df 👌 Add system_quiet helper 2023-01-31 18:06:47 +01:00
b39b4dc58b 🐛 Avoid duplicate verbose mode output 2023-01-31 17:32:37 +01:00
3ceded3456 🐛 Avoid duplicate verbose mode output 2023-01-31 17:32:31 +01:00
6d89f94c92 🐛 Fix version parsing (#227) 2023-01-30 19:19:01 +01:00
ff2eff2a75 👌 Remove unneeded print() statement 2023-01-30 19:18:31 +01:00
339eafd34d Extra verbose logging
You can start extra verbose logging by running: `touch ~/.config/phpmon/verbose`

Once this file exists, you can find the latest log in: `~/.config/phpmon/last_session.log`.
2023-01-30 19:18:00 +01:00
6115ef02de Add test to validate Valet version number with deprecations 2023-01-30 19:16:45 +01:00
8fcaa34cbb 🔧 Extra CLI mode (--cli) 2023-01-30 13:08:28 +01:00
5fa1836693 🐛 Fix services not shutting down (#225) 2023-01-29 14:58:20 +01:00
24aecb3148 ♻️ Cleanup 2023-01-26 20:45:34 +01:00
25f824defd Correctly parse .valetrc files 2023-01-24 19:48:25 +01:00
7936d14440 🏗 WIP: Add test for feature to be implemented 2023-01-24 19:48:25 +01:00
c3261b8873 🏗 WIP: Parsing .valetrc file 2023-01-24 19:48:25 +01:00
5923be099f 👌 Update copyright message 2023-01-19 18:11:25 +01:00
f92a3f545a 🐛 Fix bug related to "?" not showing up 2023-01-19 18:11:08 +01:00
b8e7397233 👌 Adjust SECURITY and support matrix for Valet 4
This commit ensures that PHP Monitor knows about which versions of
PHP are supported by the upcoming Valet 4.0: PHP 7.1 and higher.

Ensures compatibility with https://github.com/laravel/valet/pull/1318
2023-01-19 17:31:54 +01:00
5c62f744ad 👌 Improve onboarding 2023-01-18 20:42:39 +01:00
70ebb2ef59 👌 Fixed onboarding for initial launch 2023-01-18 20:42:39 +01:00
d9f4a19b92 👌 More compact ServicesView 2023-01-18 20:42:39 +01:00
90c8bcc0df 📝 TL QC Pass 1 2023-01-18 20:42:39 +01:00
1ef4f0bb81 📝 Update information about supported PHP versions 2023-01-18 20:42:39 +01:00
0f62b1f1d0 📝 Update documentation
* Updated README
* Updated SECURITY
* Revert the minimum Valet version to v2.16
2023-01-18 20:42:39 +01:00
89642de12e Add testable configuration for Valet-free env 2023-01-17 19:16:36 +01:00
c6f2167c92 Added environment check groups 2023-01-17 18:57:21 +01:00
e8d705e228 👌 Use specific theme colors for services status 2023-01-17 18:10:04 +01:00
a46602d4b4 👌 Sort PHP versions, amend message 2023-01-16 21:11:16 +01:00
7c3f416789 Tell users about older unsupported PHP versions 2023-01-13 19:51:49 +01:00
8d42e27ef6 🔧 Keep track of unsupported (but installed) PHP versions 2023-01-13 19:50:37 +01:00
894365488a 🚛 Moved some files around 2023-01-13 00:22:55 +01:00
3c6d2d74ff 🐛 Fix concurrency crashes with @objc methods 2023-01-12 17:30:27 +01:00
17efb50872 👌 Allow PHP Monitor to run without linked PHP 2023-01-12 00:04:54 +01:00
44800a03a1 🏗 WIP: Allow unlinked PHP version 2023-01-11 22:22:52 +01:00
255 changed files with 15051 additions and 2579 deletions

View File

@ -1,38 +0,0 @@
---
name: Bug report
about: Something going wrong? File a bug report!
title: ''
labels: bug
assignees: nicoverbruggen
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Required information**
- Did you consult the FAQ in the README? [yes/no]
- Did you try "Fix My Valet"? [yes/no]
- OS: [e.g. macOS Monterey]
- PHP Monitor version [e.g. v5.0.1]
**Additional log**
You can help me figure out even more information by sending me your verbose log for your latest session of PHP Monitor. Logging is disabled by default.
You can start extra verbose logging by running: `touch ~/.config/phpmon/verbose` and restarting PHP Monitor. You can find the latest log in: `~/.config/phpmon/last_session.log`. Please attach it here!
**Additional context**
Add any other context about the problem here.

62
.github/ISSUE_TEMPLATE/bug_report.yaml vendored Normal file
View File

@ -0,0 +1,62 @@
name: 🐞 Bug report
description: Something going wrong? File a bug report!
title: "[Bug] <title>"
labels: [bug]
assignees: nicoverbruggen
body:
- type: checkboxes
attributes:
label: Is there an existing issue for this?
description: Please search to see if an issue already exists for the bug you encountered.
options:
- label: I have searched the existing issues
required: true
- type: textarea
attributes:
label: Current Behavior
description: A concise description of what you're experiencing.
validations:
required: false
- type: textarea
attributes:
label: Expected Behavior
description: A concise description of what you expected to happen.
validations:
required: false
- type: textarea
attributes:
label: Steps To Reproduce
description: Steps to reproduce the behavior.
placeholder: |
1. Open this menu...
2. Click here...
3. Scroll to...
4. See error...
validations:
required: false
- type: textarea
attributes:
label: Environment
description: |
examples:
- **macOS**: (e.g. Ventura 13.3)
- **Valet**: (e.g. 4.0)
- **PHP Monitor**: (e.g. 5.8)
value: |
- macOS:
- Valet:
- PHP Monitor:
render: markdown
validations:
required: false
- type: textarea
attributes:
label: Do you have a log file (or a screenshot) or any additional information?
description: |
You can start extra verbose logging by running: `touch ~/.config/phpmon/verbose` and restarting PHP Monitor.
You can find the latest log in: `~/.config/phpmon/last_session.log`. Please attach it here!
(You can attach images or log files by clicking this area to highlight it and then dragging files in.)
validations:
required: false

View File

@ -1,22 +0,0 @@
---
name: Feature request
about: Suggest an enhancement.
title: ''
labels: enhancement
assignees: nicoverbruggen
---
_Enhancement requests that are not immediately approved will be moved to a discussion instead._
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.

View File

@ -0,0 +1,31 @@
name: 😎 Feature request
description: Do you have a great idea for an enhancement that could improve PHP Monitor?
title: "[Feature] <title>"
labels: [enhancement]
assignees: nicoverbruggen
body:
- type: checkboxes
attributes:
label: Is there an existing issue for this?
description: Please search to see if an issue already exists for the bug you encountered. Please make sure you've checked the discussions tab as well. Enhancement requests that are not immediately approved will be moved to a discussion instead, so you will find some there.
options:
- label: I have searched the existing issues and discussions
required: true
- type: textarea
attributes:
label: Is this feature request related to a problem?
description: "A clear and concise description of what the problem is. For example: 'I am always frustrated when...'"
validations:
required: true
- type: textarea
attributes:
label: Describe the solution you'd like to see
description: What would be a user-friendly way of resolving this particular issue?
validations:
required: true
- type: textarea
attributes:
label: Additional information or context
description: Add any other context or screenshots about the feature request here.
validations:
required: false

3
.gitignore vendored
View File

@ -2,4 +2,5 @@ phpmon.xcodeproj/project.xcworkspace
phpmon.xcodeproj/xcuserdata
PHP Monitor.xcodeproj/project.xcworkspace
PHP Monitor.xcodeproj/xcuserdata
.DS_Store
phpmon-updater/PHP Monitor Self-Updater.app/
.DS_Store

View File

@ -28,12 +28,15 @@ defaults delete com.nicoverbruggen.phpmon && killall cfprefsd
<img src="./docs/build.png" width="404px" alt="build button in Xcode"/>
### PHP Monitor
If you'd like to build PHP Monitor yourself, you need:
* Xcode (usually the latest version)
* The contents of this repository
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.)
Once you have downloaded this repository, open `PHP Monitor.xcodeproj`, and you should be able to 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.)
**Important**: The updater now gets automatically built and included as part of the main target.
If you'd like to create a production build, choose "Any Mac" as the target and select Product > Archive.

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1320"
LastUpgradeVersion = "1540"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
@ -91,12 +91,16 @@
</CommandLineArgument>
<CommandLineArgument
argument = "--cli"
isEnabled = "YES">
isEnabled = "NO">
</CommandLineArgument>
<CommandLineArgument
argument = "--configuration:~/.phpmon_fconf_working.json"
isEnabled = "NO">
</CommandLineArgument>
<CommandLineArgument
argument = "--configuration:~/.phpmon_fconf_working_no_valet.json"
isEnabled = "NO">
</CommandLineArgument>
<CommandLineArgument
argument = "--configuration:~/.phpmon_fconf_broken.json"
isEnabled = "NO">

View File

@ -0,0 +1,146 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1540"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "C41C1B3222B0097F00E7CF16"
BuildableName = "PHP Monitor.app"
BlueprintName = "PHP Monitor"
ReferencedContainer = "container:PHP Monitor.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug.EA"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES"
codeCoverageEnabled = "YES">
<Testables>
<TestableReference
skipped = "NO"
parallelizable = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "C4F7807825D7F84B000DBC97"
BuildableName = "Unit Tests.xctest"
BlueprintName = "Unit Tests"
ReferencedContainer = "container:PHP Monitor.xcodeproj">
</BuildableReference>
</TestableReference>
<TestableReference
skipped = "NO"
parallelizable = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "C471E7BB28F9B90F0021E251"
BuildableName = "UI Tests.xctest"
BlueprintName = "UI Tests"
ReferencedContainer = "container:PHP Monitor.xcodeproj">
</BuildableReference>
</TestableReference>
<TestableReference
skipped = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "C471E7AC28F9B4940021E251"
BuildableName = "Feature Tests.xctest"
BlueprintName = "Feature Tests"
ReferencedContainer = "container:PHP Monitor.xcodeproj">
</BuildableReference>
</TestableReference>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug.EA"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
enableGPUValidationMode = "1"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "C41C1B3222B0097F00E7CF16"
BuildableName = "PHP Monitor.app"
BlueprintName = "PHP Monitor"
ReferencedContainer = "container:PHP Monitor.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
<CommandLineArguments>
<CommandLineArgument
argument = "--v"
isEnabled = "NO">
</CommandLineArgument>
<CommandLineArgument
argument = "--cli"
isEnabled = "NO">
</CommandLineArgument>
<CommandLineArgument
argument = "--configuration:~/.phpmon_fconf_working.json"
isEnabled = "NO">
</CommandLineArgument>
<CommandLineArgument
argument = "--configuration:~/.phpmon_fconf_working_no_valet.json"
isEnabled = "NO">
</CommandLineArgument>
<CommandLineArgument
argument = "--configuration:~/.phpmon_fconf_broken.json"
isEnabled = "NO">
</CommandLineArgument>
</CommandLineArguments>
<EnvironmentVariables>
<EnvironmentVariable
key = "EXTREME_DOCTOR_MODE"
value = ""
isEnabled = "NO">
</EnvironmentVariable>
<EnvironmentVariable
key = "PAINT_PHPMON_SWIFTUI_VIEWS"
value = ""
isEnabled = "NO">
</EnvironmentVariable>
</EnvironmentVariables>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release.EA"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "C41C1B3222B0097F00E7CF16"
BuildableName = "PHP Monitor.app"
BlueprintName = "PHP Monitor"
ReferencedContainer = "container:PHP Monitor.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug.EA">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release.EA"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View File

@ -0,0 +1,78 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1540"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "C406A5EF298AD2CE00B5B85A"
BuildableName = "PHP Monitor Self-Updater.app"
BlueprintName = "PHP Monitor Self-Updater"
ReferencedContainer = "container:PHP Monitor.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<Testables>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Release"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "C406A5EF298AD2CE00B5B85A"
BuildableName = "PHP Monitor Self-Updater.app"
BlueprintName = "PHP Monitor Self-Updater"
ReferencedContainer = "container:PHP Monitor.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "C406A5EF298AD2CE00B5B85A"
BuildableName = "PHP Monitor Self-Updater.app"
BlueprintName = "PHP Monitor Self-Updater"
ReferencedContainer = "container:PHP Monitor.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View File

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

View File

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

125
README.md
View File

@ -3,17 +3,15 @@
<p align="center"><img src="./docs/logo.png" alt="PHP Monitor Logo" width="500px" /></p>
**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's tightly integrated with [Laravel Valet](https://github.com/laravel/valet), so <u>you need to have it set up before you can use this app</u> (consult the FAQ below with info about how to set up your environment).
**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's tightly integrated with [Laravel Valet](https://github.com/laravel/valet), so <u>you need to have it set up if you want to use all of the functionality of the app</u> (consult the FAQ below with info about how to set up your environment).
<img src="./docs/screenshot.jpg#gh-light-mode-only" width="1280px" alt="phpmon screenshot (menu bar app)"/>
<img src="./docs/screenshot-dark.jpg#gh-dark-mode-only" width="1280px" alt="phpmon screenshot (menu bar app)"/>
<img src="./docs/screenshot.jpg" width="1280px" alt="phpmon screenshot (menu bar app)"/>
<small><i>Screenshot: Showing the key functionality of PHP Monitor.</i></small>
It's super convenient to switch between different versions of PHP. You'll even get notifications (only if you choose to opt-in, of course)!
<img src="./docs/notification.png#gh-light-mode-only" width="370px" alt="phpmon screenshot (notification)"/>
<img src="./docs/notification-dark.png#gh-dark-mode-only" width="370px" alt="phpmon screenshot (notification)"/>
<img src="./docs/notification.png" width="370px" alt="phpmon screenshot (notification)"/>
PHP Monitor also gives you quick access to various useful functionality (like accessing configuration files, restarting services, and more).
@ -24,18 +22,18 @@ You can also add new domains as links, isolate sites, manage various services, a
PHP Monitor is a universal application that runs natively on Apple Silicon **and** Intel-based Macs.
* Your user account can administer your computer (required for some functionality, e.g. certificate generation)
* macOS 11 Big Sur or later
* Homebrew is installed in `/usr/local/homebrew` or `/opt/homebrew`
* macOS 12.4 or later (Monterey, Ventura and Sonoma are supported)
* Homebrew is installed in the default location (`/usr/local/homebrew` or `/opt/homebrew`)
* Homebrew `php` formula is installed
* Laravel Valet (works with Valet v2, v3 and v4)
* Optional but recommended: Laravel Valet
_You may need to update your Valet installation to keep everything working if a major version update of PHP has been released. You can do this by running `composer global update && valet install`. Some features are not supported when running Valet 2._
_Starting with PHP Monitor 6.0, you do not need to have Laravel Valet installed for PHP Monitor to work. To get access to all features of PHP Monitor however, installing Valet is **recommended**._
For more information, please see [SECURITY.md](./SECURITY.md) to find out which version of the app is currently supported.
## 🚀 How to install
Again, make sure you have **[Laravel Valet](https://laravel.com/docs/master/valet)** installed first:
Again, if you want to have access to *all features* of PHP Monitor, I recommend installing **[Laravel Valet](https://laravel.com/docs/master/valet)** first:
```sh
composer global require laravel/valet
@ -43,22 +41,30 @@ valet install
valet trust
```
Once that's done, you can install PHP Monitor via Homebrew (recommended), or (alternatively) you may download the latest release on GitHub.
Currently, PHP Monitor is compatible with Laravel Valet v2, v3 and v4. Each of these versions of Valet support slightly different PHP versions, which is why legacy versions remain supported. Please note that some features are not available in older versions of Valet, like site isolation.
To install via Homebrew, run:
#### Manual installation (recommended, first time only)
Once that's done, you can [download the latest release](https://github.com/nicoverbruggen/phpmon/releases/latest), unzip it and place it in `/Applications`.
#### Installation via Homebrew
*Prior to version 5.8, this was the recommended way of installing PHP Monitor.*
If you prefer to install the app via Homebrew, you can also run the following:
```sh
brew tap nicoverbruggen/homebrew-cask
brew install --cask phpmon
```
To upgrade your existing installation, run:
## ⬆️ How to update
```sh
brew upgrade phpmon
```
The recommended method of updating the app to the latest version is to use **the built-in updater**.
(You may need to run `brew update` or `brew update-reset` first in order to update the cask file if you ran a Homebrew operation recently.)
If you have a very slow internet connection, the updater may report that the download has timed out. In that case, you may wish to manually update by [downloading the latest release](https://github.com/nicoverbruggen/phpmon/releases/latest) and placing the app in `/Applications`.
(You may also use Homebrew to update PHP Monitor, but this will require you to approve the app every time an update is installed. If you use the built-in updater, this won't be necessary.)
## ⚡️ Launchers (Alfred, Raycast)
@ -76,6 +82,12 @@ I wanted to be able to **see at a glance** which version of PHP was linked, and
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.
## 🐘 Why not use Laravel Herd?
If you don't need to customize your local PHP setup and just want an easy and ready-to-go environment to start coding, [Laravel Herd](https://herd.laravel.com) is probably more than sufficient for many use cases.
If you need more customization and flexibility I encourage you to consider PHP Monitor in combination with Laravel Valet or some other solution like Docker (with Laravel Sail, for example).
## 🤬 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.
@ -98,48 +110,47 @@ All stable and supported PHP versions are also supported by PHP Monitor. However
> **Note**
> If you have versions of PHP installed that can be detected by PHP Monitor but is *not* supported by the currently active version of Valet, you will be alerted by an item in the menu with an exclamation mark emoji. (⚠️)
Backports are available via [this tap](https://github.com/shivammathur/homebrew-php). For more information about those backports, please see the next FAQ entry.
Backports that are installable via PHP Monitor's **PHP Version Manager** functionality are subject to availability via [this tap](https://github.com/shivammathur/homebrew-php).
PHP extensions that are installable via PHP Monitor's **PHP Extension Manager** functionality are subject to availability via [this tap](https://github.com/shivammathur/homebrew-extensions).
For maximum compatibility with older PHP versions, you may wish to keep using Valet 2 or 3. For more information, please see [SECURITY.md](./SECURITY.md) to find out which versions of PHP are supported with different versions of Valet.
</details>
<details>
<summary><strong>How do I install additional versions of PHP, including legacy versions?</strong></summary>
Assuming you have installed the `php` formula, the latest stable version of PHP is installed. At the time of writing, this is PHP 8.2.
Assuming you have installed the `php` formula, the latest stable version of PHP is installed. At the time of writing, this is PHP 8.3.
You can install other supported versions of PHP out of the box, so `php@8.0` and `php@8.1` at the time of writing.
You can install other supported versions of PHP via PHP Monitor's **PHP Version Manager**. (You can manually install or upgrade PHP versions too, but this is not recommended.)
If you wish to install older (officially unsupported) versions of PHP for local use, you can do so by using [Shivam Mathur's tap](https://github.com/shivammathur/homebrew-php):
Please keep in mind that installing or updating PHP versions, even when done via PHP Monitor's **PHP Version Manager**, may cause other required formula dependencies (required software needed to keep those PHP versions functional) to be upgraded. It might not be very transparent when this happens, but this is likely the cause if installing a PHP version takes longer than expected: usually other dependencies are also being installed.
```sh
brew tap shivammathur/php
```
Additionally, upgrading one specific version of PHP may also cause other installed versions of PHP to *also* be updated in one go, if the dependencies for that one version also apply to the other (newer) version(s) of PHP. It's a bit tricky to manage PHP versions via Homebrew, and even PHP Monitor may encounter some difficulties.
You may find that this tap is already in use: if you've used Valet before, it automatically uses this tap for legacy versions of PHP.
If you encounter a strange scenario or a malfunction, please open an issue on the issue tracker and get in touch. I'd like to keep enhancing this process to make it as foolproof as possible.
```sh
brew install shivammathur/php/php@7.4
brew install shivammathur/php/php@7.3
brew install shivammathur/php/php@7.2
brew install shivammathur/php/php@7.1
brew install shivammathur/php/php@7.0
```
**Always make sure to restart PHP Monitor after installing or upgrading PHP versions!**
> *Note*: Using this tap may cause [temporary alias conflicts](https://github.com/nicoverbruggen/phpmon/issues/54#issuecomment-979789724) while the core tap alias and the tap's alias refer to a different version of PHP, but this is generally speaking a minor inconvenience, since this normally only applies when a new PHP version releases.
> *Note*: Using PHP Monitor when managing PHP versions may cause [temporary alias conflicts](https://github.com/nicoverbruggen/phpmon/issues/54#issuecomment-979789724) while the core tap alias and the tap's alias refer to a different version of PHP, but this is generally speaking a minor inconvenience, since this normally only applies when a new PHP version releases.
</details>
<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.
If you are running macOS Ventura or newer, there's an option in the Settings menu that you can select: "Start PHP Monitor at login".
If you are on an older version of macOS, 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>What features are unavailable in Standalone Mode?</strong></summary>
The services manager is disabled, and all other obvious Laravel Valet integrations (configuration finder, domains list, Fix My Valet) are also disabled.
(Most other features remain available.)
</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>
@ -166,7 +177,7 @@ If you're on an Apple Silicon-based Mac, you'll need to add:
and add the following to your `.zshrc` file, but add this BEFORE the homebrew PATH additions:
export PATH=$HOME/bin:~/.composer/vendor/bin:$PATH
If you're adding `composer` and Homebrew binaries, ensure that Homebrew binaries are preferred by adding these to the path last. On my system, that looks like this:
export PATH=$HOME/bin:/usr/local/bin:$PATH
@ -185,8 +196,12 @@ Make sure PHP is linked correctly:
should return: `/usr/local/bin/php` (or `/opt/homebrew/bin/php` if you are on Apple Silicon)
**If you don't need Laravel Valet, you can stop here. PHP Monitor will work like this in Standalone Mode.**
If you'd like to have Valet as well, continue and install Valet with Composer, like this.
composer global require laravel/valet
For optimal results, you should lock your PHP platform for global dependencies to the oldest version of PHP you intend to run. If that version is PHP 7.0, your `~/.composer/composer.json` file could look like this (please adjust the version accordingly!):
```
@ -205,18 +220,13 @@ For optimal results, you should lock your PHP platform for global dependencies t
Run `composer global update` again. This ensures that when you switch to a different global PHP version, [Valet won't break](https://github.com/nicoverbruggen/phpmon/issues/178). If it does, PHP Monitor will let you know what you can do about this.
Then, install Valet:
valet install
This should install `dnsmasq` and set up Valet. Great, almost there!
valet trust
You can now install PHP Monitor, if you haven't already:
brew tap nicoverbruggen/homebrew-cask
brew install --cask phpmon
Finally, run PHP Monitor. Since the app is notarized and signed with a developer ID, it should work. You will need to approve the initial launch of the app, but you should be ready to go now.
</details>
@ -225,13 +235,17 @@ Finally, run PHP Monitor. Since the app is notarized and signed with a developer
PHP Monitor will check if an update is available every time you start the app.
You can disable this behaviour by going to Preferences (via the PHP Monitor icon in the menu bar) and unchecking "Automatically check for updates". You can always check for updates manually.
You can disable this behaviour by going to Preferences (via the PHP Monitor icon in the menu bar) and unchecking "Automatically check for updates". (You can always check for updates manually.)
</details>
<details>
<summary><strong>I have PHP Monitor installed, and it works. I want to upgrade my PHP installations to the latest version, what's the best way to do this?</strong></summary>
The easiest way is to simply use the built-in **PHP Version Manager**, which will allow you to upgrade your PHP versions with one click.
If you want to do this manually, you can follow the instructions below.
It's easy to make a mistake here, and end up with an unlinked version of PHP or have versions missing from PHP Monitor.
Here's what I usually do:
@ -261,7 +275,7 @@ This should resolve the issue! If that does not fix the issue, run `brew link ph
brew install php
brew link php --force
</details>
<details>
@ -273,6 +287,8 @@ This problem is usually resolved by upgrading Valet and running `valet install`
composer global update
valet install
If you are seeing a 502 (Bad Gateway) error after about 30 seconds or so, your request is likely timing out. You may need to solve a performance issue with your own code.
</details>
@ -314,12 +330,14 @@ Make sure you have at least **Valet 3.0** installed, since support for isolation
<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!)
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.)
(If you are using Valet, you can adjust these limits in the `.conf.d/php-memory-limits.ini` file. Otherwise, you may need to adjust `php.ini`.)
</details>
<details>
@ -408,6 +426,9 @@ You can omit the `php` key in the preset if you do not wish for the preset to sw
<details>
<summary><strong>How do I ensure additional Homebrew services are shown in the app?</strong></summary>
> **Info**
> Homebrew services aren't displayed if you are using Valet in Standalone Mode.
You must set these services up in a JSON file, located in `~/.config/phpmon/config.json`.
You can specify custom services in the configuration file for Homebrew services that run as your own user (not root).
@ -588,9 +609,9 @@ Thank you very much for your contributions, kind words and support.
### Loading info about PHP in the background
This utility runs `php-config --version` in the background periodically. It also checks your `.ini` files for extensions and loads more information about your limits (memory limit, POST limit, upload limit).
This app runs `php-config --version` in the background periodically, usually whenever your Homebrew configuration is modified. A filesystem watcher is used to determine if anything changes in your Homebrew's `bin` directory.
In order to save power, this only happens once every 60 seconds.
PHP Monitor also checks your `.ini` files for extensions and loads more information about your limits (memory limit, POST limit, upload limit). See also the section on *Config change detection* below.
### Switching PHP versions
@ -598,7 +619,7 @@ This utility will detect which PHP versions you have installed via Homebrew, and
The switcher will disable all PHP-FPM services not belonging to the version you wish to use, and link the desired version of PHP. Then, it'll restart your desired PHP version's FPM process. This all happens in parallel, so this should be a bit faster than Valets switcher.
If you're using Valet 3, versions of PHP-FPM required to keep isolated sites up and running will also be started or stopped as needed.
If you're using Valet 3 or newer, versions of PHP-FPM required to keep isolated sites up and running will also be started or stopped as needed.
### Config change detection

View File

@ -4,19 +4,22 @@
Generally speaking, only the latest version of **PHP Monitor** is supported, except during transition periods (for example, when particular system requirements go up):
| Version | Apple Silicon | Supported | Supported macOS | Deployment Target | Detected PHP Versions | Recommended Valet Version |
| Version | Apple Silicon | Supported | Supported macOS | Minimum Deployment | Detected PHP Versions | Recommended Valet Version |
| ------- | ------------- | ------------------ | ----- | ----- | ----- | ----
| 5.7 | ✅ Universal binary | ✅ Yes | Big Sur (11.0)<br/>Monterey (12.0)<br/>Ventura (13.0) | macOS 11+ | PHP 5.6—PHP 8.2 (w/ Valet 2.x)<br/>PHP 7.0—PHP 8.2 (w/ Valet 3.x)<br/>PHP 7.1-PHP 8.2 (w/ Valet 4.x*) | 3.0 or higher recommended<br/> 2.16.2 minimum |
(*) Preliminary listing. Valet 4 hasn't been released yet and the versions of PHP Valet can work with might still change.
| 7.0 | ✅ Universal binary | ✅ Yes | Monterey (12.4+)<br/>Ventura (13.0+)<br/>Sonoma (14.0) | macOS 12.4+ | PHP 5.6—PHP 8.2 (w/ Valet 2.x)<br/>PHP 7.0—PHP 8.4 (w/ Valet 3.x)<br/>PHP 7.1-PHP 8.4 (w/ Valet 4.x)| 3.0 or higher recommended<br/> 2.16.2 minimum |
## Legacy versions
These versions of PHP Monitor are no longer supported, but if youre using an older computer with an older version of Homebrew, Valet or macOS, you might want to use one of these versions.
| Version | Apple Silicon | Supported | Supported macOS | Deployment Target | Detected PHP Versions | Minimum Required Valet Version |
| Version | Apple Silicon | Supported | Supported macOS | Minimum Deployment | Detected PHP Versions | Minimum Required Valet Version |
| ------- | ------------- | ------------------ | ----- | ----- | ----- | ----
| 5.6 | ✅ Universal binary | ❌ | Big Sur (11.0)<br/>Monterey (12.0)<br/>Ventura (13.0)* | macOS 11+ | PHP 5.6—PHP 8.2 (w/ Valet 2.x)<br/>PHP 7.0—PHP 8.2 (w/ Valet 3.x) | 3.0 recommended<br/> 2.16.2 minimum |
| 6.2 | ✅ Universal binary | ❌ | Monterey (12.4+)<br/>Ventura (13.0+)<br/>Sonoma (14.0) | macOS 12.4+ | PHP 5.6—PHP 8.2 (w/ Valet 2.x)<br/>PHP 7.0—PHP 8.4 (w/ Valet 3.x)<br/>PHP 7.1-PHP 8.4 (w/ Valet 4.x)| 3.0 or higher recommended<br/> 2.16.2 minimum |
| 6.1 | ✅ Universal binary | ❌ | Monterey (12.4+)<br/>Ventura (13.0+)<br/>Sonoma (14.0) | macOS 12.4+ | PHP 5.6—PHP 8.2 (w/ Valet 2.x)<br/>PHP 7.0—PHP 8.4 (w/ Valet 3.x)<br/>PHP 7.1-PHP 8.4 (w/ Valet 4.x)| 3.0 or higher recommended<br/> 2.16.2 minimum |
| 6.0 | ✅ Universal binary | ❌ | Monterey (12.4+)<br/>Ventura (13.0+) | macOS 12.4+ | PHP 5.6—PHP 8.2 (w/ Valet 2.x)<br/>PHP 7.0—PHP 8.2 (w/ Valet 3.x)<br/>PHP 7.1-PHP 8.2 (w/ Valet 4.x) | 3.0 or higher recommended<br/> 2.16.2 minimum |
| 5.8 | ✅ Universal binary | ❌ | Monterey (12.4+)<br/>Ventura (13.0+) | macOS 12.4+ | PHP 5.6—PHP 8.2 (w/ Valet 2.x)<br/>PHP 7.0—PHP 8.2 (w/ Valet 3.x)<br/>PHP 7.1-PHP 8.2 (w/ Valet 4.x) | 3.0 or higher recommended<br/> 2.16.2 minimum |
| 5.7 | ✅ Universal binary | ❌ | Big Sur (11.0)<br/>Monterey (12.0)<br/>Ventura (13.0) | macOS 11+ | PHP 5.6—PHP 8.2 (w/ Valet 2.x)<br/>PHP 7.0—PHP 8.2 (w/ Valet 3.x)<br/>PHP 7.1-PHP 8.2 (w/ Valet 4.x) | 3.0 or higher recommended<br/> 2.16.2 minimum |
| 5.6 | ✅ Universal binary | ❌ | Big Sur (11.0)<br/>Monterey (12.0)<br/>Ventura (13.0) | macOS 11+ | PHP 5.6—PHP 8.2 (w/ Valet 2.x)<br/>PHP 7.0—PHP 8.2 (w/ Valet 3.x) | 3.0 recommended<br/> 2.16.2 minimum |
| 4.1 | ✅ Universal binary | ❌ | Big Sur (11.0)<br/>Monterey (12.0) | macOS 11+ | PHP 5.6—PHP 8.2 | 2.16.2 |
| 4.0 | ✅ Universal binary | ❌ | Big Sur (11.0)<br/>Monterey (12.0) | macOS 10.14+ | PHP 5.6—PHP 8.2 | 2.13 |
| 3.5 | ✅ Universal binary | ❌ | Big Sur (11.0)<br/>Monterey (12.0) | macOS 10.14+ | PHP 5.6—PHP 8.2 | 2.13 |

Binary file not shown.

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 524 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 519 KiB

After

Width:  |  Height:  |  Size: 723 KiB

View File

@ -0,0 +1,11 @@
{
"colors" : [
{
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 811 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 301 KiB

View File

@ -0,0 +1,6 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,46 @@
//
// LaunchControl.swift
// PHP Monitor Self-Updater
//
// Created by Nico Verbruggen on 02/02/2023.
// Copyright © 2023 Nico Verbruggen. All rights reserved.
//
import Foundation
import Cocoa
class LaunchControl {
public static func smartRestart(priority: [String]) async {
for appPath in priority {
if FileManager.default.fileExists(atPath: appPath) {
let app = await LaunchControl.startApplication(at: appPath)
if app != nil {
return
}
}
}
}
public static func terminateApplications(bundleIds: [String]) async {
let runningApplications = NSWorkspace.shared.runningApplications
// Terminate all instances found
for id in bundleIds {
if let phpmon = runningApplications.first(where: {
(application) in return application.bundleIdentifier == id
}) {
phpmon.terminate()
}
}
}
public static func startApplication(at path: String) async -> NSRunningApplication? {
await withCheckedContinuation { continuation in
let url = NSURL(fileURLWithPath: path, isDirectory: true) as URL
let configuration = NSWorkspace.OpenConfiguration()
NSWorkspace.shared.openApplication(at: url, configuration: configuration) { phpmon, error in
continuation.resume(returning: phpmon)
}
}
}
}

View File

@ -0,0 +1,162 @@
//
// Updater.swift
// PHP Monitor Updater
//
// Created by Nico Verbruggen on 01/02/2023.
// Copyright © 2023 Nico Verbruggen. All rights reserved.
//
import Cocoa
class Updater: NSObject, NSApplicationDelegate {
var updaterDirectory: String = ""
var manifestPath: String = ""
var manifest: ReleaseManifest! = nil
func applicationDidFinishLaunching(_ aNotification: Notification) {
Task { await self.installUpdate() }
}
func installUpdate() async {
print("PHP MONITOR SELF-UPDATER by Nico Verbruggen")
print("===========================================")
self.updaterDirectory = "~/.config/phpmon/updater"
.replacingOccurrences(of: "~", with: NSHomeDirectory())
print("Updater directory set to: \(self.updaterDirectory)")
self.manifestPath = "\(updaterDirectory)/update.json"
// Fetch the manifest on the local filesystem
let manifest = await parseManifest()!
// Download the latest file
let zipPath = await download(manifest)
// Terminate all instances of PHP Monitor first
await LaunchControl.terminateApplications(bundleIds: [
"com.nicoverbruggen.phpmon.eap",
"com.nicoverbruggen.phpmon.dev",
"com.nicoverbruggen.phpmon"
])
// Install the app based on the zip
let appPath = await extractAndInstall(zipPath: zipPath)
// Restart PHP Monitor, this will also close the updater
_ = await LaunchControl.startApplication(at: appPath)
exit(1)
}
func applicationWillTerminate(_ aNotification: Notification) {
exit(1)
}
func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool {
return false
}
private func parseManifest() async -> ReleaseManifest? {
// Read out the correct information from the manifest JSON
print("Checking manifest file at \(manifestPath)...")
do {
let manifestText = try String(contentsOfFile: manifestPath)
manifest = try JSONDecoder().decode(ReleaseManifest.self, from: manifestText.data(using: .utf8)!)
return manifest
} catch {
print("Parsing the manifest failed (or the manifest file doesn't exist)!")
await Alert.show(description: "The manifest file for a potential update was not found. Please try searching for updates again in PHP Monitor.")
}
return nil
}
private func download(_ manifest: ReleaseManifest) async -> String {
// Remove all zips
system_quiet("rm -rf \(updaterDirectory)/*.zip")
// Download the file (and follow redirects + no output on failure)
system_quiet("cd \"\(updaterDirectory)\" && curl \(manifest.url) -fLO --max-time 20")
// Identify the downloaded file
let filename = system("cd \"\(updaterDirectory)\" && ls | grep .zip")
.trimmingCharacters(in: .whitespacesAndNewlines)
// Ensure the zip exists
if filename.isEmpty {
print("The update has not been downloaded. Sadly, that means that PHP Monitor cannot not updated!")
await Alert.show(description: "The update could not be downloaded, or the file was not correctly written to disk. \n\nPlease try again. \n\n(Note that the download will time-out after 20 seconds, so for slow connections it is recommended to manually download the update.)")
}
// Calculate the checksum for the downloaded file
let checksum = system("openssl dgst -sha256 \"\(updaterDirectory)/\(filename)\" | awk '{print $NF}'")
.trimmingCharacters(in: .whitespacesAndNewlines)
// Compare the checksums
print("""
Comparing checksums...
Expected SHA256: \(manifest.sha256)
Actual SHA256: \(checksum)
""")
// Make sure the checksum matches before we do anything with the file
if checksum != manifest.sha256 {
print("The checksums failed to match. Cancelling!")
await Alert.show(description: "The downloaded update failed checksum validation. Please try again. If this issue persists, there may be an issue with the server and I do not recommend upgrading.")
}
// Return the path to the zip
return "\(updaterDirectory)/\(filename)"
}
private func extractAndInstall(zipPath: String) async -> String {
// Remove the directory that will contain the extracted update
system_quiet("rm -rf \"\(updaterDirectory)/extracted\"")
// Recreate the directory where we will unzip the .app file
system_quiet("mkdir -p \"\(updaterDirectory)/extracted\"")
// Make sure the updater directory exists
var isDirectory: ObjCBool = true
if !FileManager.default.fileExists(atPath: "\(updaterDirectory)/extracted", isDirectory: &isDirectory) {
await Alert.show(description: "The updater directory is missing. The automatic updater will quit. Make sure that ` ~/.config/phpmon/updater` is writeable.")
}
// Unzip the file
system_quiet("unzip \"\(zipPath)\" -d \"\(updaterDirectory)/extracted\"")
// Find the .app file
let app = system("ls \"\(updaterDirectory)/extracted\" | grep .app")
.trimmingCharacters(in: .whitespacesAndNewlines)
print("Finished extracting: \(updaterDirectory)/extracted/\(app)")
// Make sure the file was extracted
if app.isEmpty {
await Alert.show(description: "The downloaded file could not be extracted. The automatic updater will quit. Make sure that ` ~/.config/phpmon/updater` is writeable.")
}
// Remove the original app
print("Removing \(app) before replacing...")
system_quiet("rm -rf \"/Applications/\(app)\"")
// Move the new app in place
system_quiet("mv \"\(updaterDirectory)/extracted/\(app)\" \"/Applications/\(app)\"")
// Remove the zip
system_quiet("rm \"\(zipPath)\"")
// Remove the manifest
system_quiet("rm \"\(manifestPath)\"")
// Write a file that is only written when we upgraded successfully
system_quiet("touch \"\(updaterDirectory)/upgrade.success\"")
// Return the new location of the app
return "/Applications/\(app)"
}
}

View File

@ -0,0 +1,34 @@
//
// Utility.swift
// PHP Monitor Self-Updater
//
// Created by Nico Verbruggen on 02/02/2023.
// Copyright © 2023 Nico Verbruggen. All rights reserved.
//
import Foundation
import Cocoa
class Alert {
public static func show(description: String, shouldExit: Bool = true) async {
await withUnsafeContinuation { continuation in
DispatchQueue.main.async {
let alert = NSAlert()
alert.messageText = "The app could not be updated."
alert.informativeText = description
alert.addButton(withTitle: "OK")
alert.alertStyle = .critical
alert.runModal()
if shouldExit {
exit(0)
}
continuation.resume()
}
}
}
}
public struct ReleaseManifest: Codable {
let url: String
let sha256: String
}

14
phpmon-updater/main.swift Normal file
View File

@ -0,0 +1,14 @@
//
// AppDelegate.swift
// PHP Monitor Self-Updater
//
// Created by Nico Verbruggen on 01/02/2023.
// Copyright © 2023 Nico Verbruggen. All rights reserved.
//
import Cocoa
let app = NSApplication.shared
let delegate = Updater()
app.delegate = delegate
_ = NSApplicationMain(CommandLine.argc, CommandLine.unsafeArgv)

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict/>
</plist>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 575 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

View File

@ -0,0 +1,68 @@
{
"images" : [
{
"filename" : "icon_16x16.png",
"idiom" : "mac",
"scale" : "1x",
"size" : "16x16"
},
{
"filename" : "icon_16x16@2x.png",
"idiom" : "mac",
"scale" : "2x",
"size" : "16x16"
},
{
"filename" : "icon_32x32.png",
"idiom" : "mac",
"scale" : "1x",
"size" : "32x32"
},
{
"filename" : "icon_32x32@2x.png",
"idiom" : "mac",
"scale" : "2x",
"size" : "32x32"
},
{
"filename" : "icon_128x128.png",
"idiom" : "mac",
"scale" : "1x",
"size" : "128x128"
},
{
"filename" : "icon_128x128@2x.png",
"idiom" : "mac",
"scale" : "2x",
"size" : "128x128"
},
{
"filename" : "icon_256x256.png",
"idiom" : "mac",
"scale" : "1x",
"size" : "256x256"
},
{
"filename" : "icon_256x256@2x.png",
"idiom" : "mac",
"scale" : "2x",
"size" : "256x256"
},
{
"filename" : "icon_512x512.png",
"idiom" : "mac",
"scale" : "1x",
"size" : "512x512"
},
{
"filename" : "icon_512x512@2x.png",
"idiom" : "mac",
"scale" : "2x",
"size" : "512x512"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 644 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 162 KiB

View File

@ -0,0 +1,38 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "1.000",
"green" : "1.000",
"red" : "1.000"
}
},
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0.172",
"green" : "0.182",
"red" : "0.182"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,38 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0.988",
"green" : "0.580",
"red" : "0.278"
}
},
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0.988",
"green" : "0.444",
"red" : "0.277"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,38 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "0.300",
"blue" : "0.180",
"green" : "0.841",
"red" : "1.000"
}
},
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "0.300",
"blue" : "0.426",
"green" : "0.809",
"red" : "1.000"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,25 @@
{
"images" : [
{
"filename" : "php.svg",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"preserves-vector-representation" : true,
"template-rendering-intent" : "template"
}
}

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="800" height="800" viewBox="0 0 24 24"><path d="M7.01 10.207h-.944l-.515 2.648h.838c.556 0 .97-.105 1.242-.314.272-.21.455-.559.55-1.049.092-.47.05-.802-.124-.995-.175-.193-.523-.29-1.047-.29zM12 5.688C5.373 5.688 0 8.514 0 12s5.373 6.313 12 6.313S24 15.486 24 12c0-3.486-5.373-6.312-12-6.312zm-3.26 7.451c-.261.25-.575.438-.917.551-.336.108-.765.164-1.285.164H5.357l-.327 1.681H3.652l1.23-6.326h2.65c.797 0 1.378.209 1.744.628.366.418.476 1.002.33 1.752a2.836 2.836 0 0 1-.305.847c-.143.255-.33.49-.561.703zm4.024.715.543-2.799c.063-.318.039-.536-.068-.651-.107-.116-.336-.174-.687-.174H11.46l-.704 3.625H9.388l1.23-6.327h1.367l-.327 1.682h1.218c.767 0 1.295.134 1.586.401s.378.7.263 1.299l-.572 2.944h-1.389zm7.597-2.265a2.782 2.782 0 0 1-.305.847c-.143.255-.33.49-.561.703a2.44 2.44 0 0 1-.917.551c-.336.108-.765.164-1.286.164h-1.18l-.327 1.682h-1.378l1.23-6.326h2.649c.797 0 1.378.209 1.744.628.366.417.477 1.001.331 1.751zm-2.595-1.382h-.943l-.516 2.648h.838c.557 0 .971-.105 1.242-.314.272-.21.455-.559.551-1.049.092-.47.049-.802-.125-.995s-.524-.29-1.047-.29z"/></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -16,7 +16,26 @@ protocol CommandProtocol {
- Parameter path: The path of the command or program to invoke.
- Parameter arguments: A list of arguments that are passed on.
- Parameter trimNewlines: Removes empty new line output.
- Parameter withStandardError: Outputs standard error output to the same string output as well.
*/
func execute(path: String, arguments: [String], trimNewlines: Bool) -> String
func execute(
path: String,
arguments: [String],
trimNewlines: Bool,
withStandardError: Bool
) -> String
/**
Immediately executes a command.
- Parameter path: The path of the command or program to invoke.
- Parameter arguments: A list of arguments that are passed on.
- Parameter trimNewlines: Removes empty new line output.
*/
func execute(
path: String,
arguments: [String],
trimNewlines: Bool
) -> String
}

View File

@ -9,13 +9,23 @@ import Cocoa
public class RealCommand: CommandProtocol {
public func execute(path: String, arguments: [String], trimNewlines: Bool = false) -> String {
public func execute(
path: String,
arguments: [String],
trimNewlines: Bool,
withStandardError: Bool
) -> String {
let task = Process()
task.launchPath = path
task.arguments = arguments
let pipe = Pipe()
task.standardOutput = pipe
if withStandardError {
task.standardError = pipe
}
task.launch()
let data = pipe.fileHandleForReading.readDataToEndOfFile()
@ -30,4 +40,17 @@ public class RealCommand: CommandProtocol {
return output
}
public func execute(
path: String,
arguments: [String],
trimNewlines: Bool = false
) -> String {
self.execute(
path: path,
arguments: arguments,
trimNewlines: trimNewlines,
withStandardError: false
)
}
}

View File

@ -12,37 +12,46 @@ class Actions {
// MARK: - Services
public static func linkPhp() async {
await brew("link php --overwrite --force")
}
public static func restartPhpFpm() async {
await brew("services restart \(Homebrew.Formulae.php.name)", sudo: Homebrew.Formulae.php.elevated)
await brew("services restart \(HomebrewFormulae.php)", sudo: HomebrewFormulae.php.elevated)
}
public static func restartPhpFpm(version: String) async {
let formula = (version == PhpEnvironments.brewPhpAlias) ? "php" : "php@\(version)"
await brew("services restart \(formula)", sudo: HomebrewFormulae.php.elevated)
}
public static func restartNginx() async {
await brew("services restart \(Homebrew.Formulae.nginx.name)", sudo: Homebrew.Formulae.nginx.elevated)
await brew("services restart \(HomebrewFormulae.nginx)", sudo: HomebrewFormulae.nginx.elevated)
}
public static func restartDnsMasq() async {
await brew("services restart \(Homebrew.Formulae.dnsmasq.name)", sudo: Homebrew.Formulae.dnsmasq.elevated)
await brew("services restart \(HomebrewFormulae.dnsmasq)", sudo: HomebrewFormulae.dnsmasq.elevated)
}
public static func stopValetServices() async {
await brew("services stop \(Homebrew.Formulae.php.name)", sudo: Homebrew.Formulae.php.elevated)
await brew("services stop \(Homebrew.Formulae.nginx.name)", sudo: Homebrew.Formulae.nginx.elevated)
await brew("services stop \(Homebrew.Formulae.dnsmasq.name)", sudo: Homebrew.Formulae.dnsmasq.elevated)
await brew("services stop \(HomebrewFormulae.php)", sudo: HomebrewFormulae.php.elevated)
await brew("services stop \(HomebrewFormulae.nginx)", sudo: HomebrewFormulae.nginx.elevated)
await brew("services stop \(HomebrewFormulae.dnsmasq)", sudo: HomebrewFormulae.dnsmasq.elevated)
}
public static func fixHomebrewPermissions() throws {
var servicesCommands = [
"\(Paths.brew) services stop \(Homebrew.Formulae.nginx)",
"\(Paths.brew) services stop \(Homebrew.Formulae.dnsmasq)"
"\(Paths.brew) services stop \(HomebrewFormulae.nginx)",
"\(Paths.brew) services stop \(HomebrewFormulae.dnsmasq)"
]
var cellarCommands = [
"chown -R \(Paths.whoami):admin \(Paths.cellarPath)/\(Homebrew.Formulae.nginx)",
"chown -R \(Paths.whoami):admin \(Paths.cellarPath)/\(Homebrew.Formulae.dnsmasq)"
"chown -R \(Paths.whoami):admin \(Paths.cellarPath)/\(HomebrewFormulae.nginx)",
"chown -R \(Paths.whoami):admin \(Paths.cellarPath)/\(HomebrewFormulae.dnsmasq)"
]
PhpEnv.shared.availablePhpVersions.forEach { version in
let formula = version == PhpEnv.brewPhpAlias
PhpEnvironments.shared.availablePhpVersions.forEach { version in
let formula = version == PhpEnvironments.brewPhpAlias
? "php"
: "php@\(version)"
servicesCommands.append("\(Paths.brew) services stop \(formula)")
@ -54,9 +63,10 @@ class Actions {
+ " && "
+ cellarCommands.joined(separator: " && ")
let appleScript = NSAppleScript(
source: "do shell script \"\(script)\" with administrator privileges"
)
let source = "do shell script \"\(script)\" with administrator privileges"
Log.perf(source)
let appleScript = NSAppleScript(source: source)
let eventResult: NSAppleEventDescriptor? = appleScript?.executeAndReturnError(nil)
@ -118,9 +128,9 @@ class Actions {
extensions and/or run `composer global update`.
*/
public static func fixMyValet() async {
await InternalSwitcher().performSwitch(to: PhpEnv.brewPhpAlias)
await brew("services restart \(Homebrew.Formulae.dnsmasq)", sudo: Homebrew.Formulae.dnsmasq.elevated)
await brew("services restart \(Homebrew.Formulae.php)", sudo: Homebrew.Formulae.php.elevated)
await brew("services restart \(Homebrew.Formulae.nginx)", sudo: Homebrew.Formulae.nginx.elevated)
await InternalSwitcher().performSwitch(to: PhpEnvironments.brewPhpAlias)
await brew("services restart \(HomebrewFormulae.dnsmasq)", sudo: HomebrewFormulae.dnsmasq.elevated)
await brew("services restart \(HomebrewFormulae.php)", sudo: HomebrewFormulae.php.elevated)
await brew("services restart \(HomebrewFormulae.nginx)", sudo: HomebrewFormulae.nginx.elevated)
}
}

View File

@ -18,6 +18,53 @@ struct Constants {
*/
static let MinimumRecommendedValetVersion = "2.16.2"
/**
PHP Monitor supplies a hardcoded list of PHP packages in its own
PHP Version Manager.
This hardcoded list will expire and will need to be modified when
the cutoff date occurs, which is when the `php` formula will
become PHP 8.4, and a new build will need to be made.
If users launch an older version of the app, then a warning
will be displayed to let them know that certain operations
will not work correctly and that they need to update their app.
*/
static let PhpFormulaeCutoffDate = "2024-11-31" // YYYY-MM-DD
/**
* The PHP versions that are considered pre-release versions.
* Past a certain date, an experimental version "graduates"
* to a release version and is no longer marked as experimental.
*/
static var ExperimentalPhpVersions: Set<String> {
let releaseDates = [
"8.4": Date.fromString("2024-11-22")
]
return Set(releaseDates
.filter { (_: String, date: Date?) in
guard let date else {
return false
}
return date > Date.now
}.map { (version: String, _: Date?) in
return version
})
}
/**
The Homebrew services that should be automatically
detected and show up in the list of managed services.
*/
static let DetectedHomebrewServices: Set = [
"mailhog",
"mysql@",
"postgresql@",
"redis"
]
/**
* The PHP versions supported by this application.
* Any other PHP versions are considered invalid.
@ -25,7 +72,8 @@ struct Constants {
static let DetectedPhpVersions: Set = [
"5.6",
"7.0", "7.1", "7.2", "7.3", "7.4",
"8.0", "8.1", "8.2", "8.3"
"8.0", "8.1", "8.2", "8.3",
"8.4"
]
/**
@ -41,14 +89,14 @@ struct Constants {
3: // Valet v3 dropped support for v5.6
[
"7.0", "7.1", "7.2", "7.3", "7.4",
"8.0", "8.1", "8.2",
"8.3" // dev
"8.0", "8.1", "8.2", "8.3",
"8.4" // dev
],
4: // Valet v4 dropped support for v7.0
[
"7.1", "7.2", "7.3", "7.4",
"8.0", "8.1", "8.2",
"8.3" // dev
"8.0", "8.1", "8.2", "8.3",
"8.4" // dev
]
]
@ -82,6 +130,16 @@ struct Constants {
string: "https://raw.githubusercontent.com/nicoverbruggen/homebrew-cask/master/Casks/phpmon-dev.rb"
)!
// EAP URLs
static let EarlyAccessCaskFile = URL(
string: "https://phpmon.app/builds/early-access/sponsors/phpmon-eap.rb"
)!
static let EarlyAccessChangelog = URL(
string: "https://phpmon.app/early-access/release-notes"
)!
}
}

View File

@ -8,13 +8,6 @@
// MARK: Common Shell Commands
/**
Runs a `valet` command. Defaults to running as superuser.
*/
func valet(_ command: String, sudo: Bool = true) async -> String {
return await Shell.pipe("\(sudo ? "sudo " : "")" + "\(Paths.valet) \(command)").out
}
/**
Runs a `brew` command. Can run as superuser.
*/

View File

@ -8,38 +8,38 @@
import Foundation
class Homebrew {
static var fake: Bool = false
struct Formulae {
static var php: HomebrewFormula {
if Homebrew.fake {
return HomebrewFormula("php", elevated: true)
}
if PhpEnv.shared.homebrewPackage == nil {
fatalError("You must either load the HomebrewPackage object or call `fake` on the Homebrew class.")
}
return HomebrewFormula(PhpEnv.phpInstall.formula, elevated: true)
struct HomebrewFormulae {
static var php: HomebrewFormula {
if PhpEnvironments.shared.homebrewPackage == nil {
return HomebrewFormula("php", elevated: true)
}
static var nginx: HomebrewFormula {
return HomebrewDiagnostics.usesNginxFullFormula
? HomebrewFormula("nginx-full", elevated: true)
: HomebrewFormula("nginx", elevated: true)
guard let install = PhpEnvironments.phpInstall else {
return HomebrewFormula("php", elevated: true)
}
static var dnsmasq: HomebrewFormula {
return HomebrewFormula("dnsmasq", elevated: true)
}
return HomebrewFormula(install.formula, elevated: true)
}
static var nginx: HomebrewFormula {
return BrewDiagnostics.usesNginxFullFormula
? HomebrewFormula("nginx-full", elevated: true)
: HomebrewFormula("nginx", elevated: true)
}
static var dnsmasq: HomebrewFormula {
return HomebrewFormula("dnsmasq", elevated: true)
}
}
class HomebrewFormula: Equatable, Hashable {
class HomebrewFormula: Equatable, Hashable, CustomStringConvertible {
let name: String
let elevated: Bool
var description: String {
return name
}
init(_ name: String, elevated: Bool = true) {
self.name = name
self.elevated = elevated

View File

@ -13,6 +13,7 @@ class Log {
static var shared = Log()
var logFilePath = "~/.config/phpmon/last_session.log"
var logExists = false
enum Verbosity: Int {
@ -29,9 +30,9 @@ class Log {
public func prepareLogFile() {
if !isRunningTests && Verbosity.cli.isApplicable() {
_ = system("mkdir -p ~/.config/phpmon 2> /dev/null")
_ = system("rm ~/.config/phpmon/last_session.log 2> /dev/null")
_ = system("touch ~/.config/phpmon/last_session.log 2> /dev/null")
system_quiet("mkdir -p ~/.config/phpmon 2> /dev/null")
system_quiet("rm ~/.config/phpmon/last_session.log 2> /dev/null")
system_quiet("touch ~/.config/phpmon/last_session.log 2> /dev/null")
self.logExists = FileSystem.fileExists(self.logFilePath)
}
}
@ -72,6 +73,12 @@ class Log {
}
}
static func line(as verbosity: Verbosity = .info) {
if verbosity.isApplicable() {
Log.shared.log("----------------------------------")
}
}
private func log(_ text: String) {
print(text)

View File

@ -16,16 +16,28 @@ public class Paths {
public static let shared = Paths()
internal var baseDir: Paths.HomebrewDir
private var userName: String! = nil
private var userName: String
private var preferredShell: String
init() {
// Assume the default directory is correct
baseDir = App.architecture != "x86_64" ? .opt : .usr
}
public func loadUser() async {
let output = await Shell.pipe("id -un").out
userName = String(output.split(separator: "\n")[0])
// Ensure that if a different location is used, it takes precendence
if baseDir == .usr
&& FileSystem.directoryExists("/usr/local/homebrew")
&& !FileSystem.directoryExists("/usr/local/Cellar") {
Log.warn("Using /usr/local/homebrew as base directory!")
baseDir = .usr_hb
}
userName = identity()
preferredShell = preferred_shell()
if !isRunningSwiftUIPreview {
Log.info("The current username is `\(userName)`.")
Log.info("The user's shell is `\(preferredShell)`.")
}
}
public func detectBinaryPaths() {
@ -90,6 +102,23 @@ public class Paths {
return "\(shared.baseDir.rawValue)/etc"
}
public static var tapPath: String {
if shared.baseDir == .usr {
return "\(shared.baseDir.rawValue)/homebrew/Library/Taps"
}
return "\(shared.baseDir.rawValue)/Library/Taps"
}
public static var caskroomPath: String {
return "\(shared.baseDir.rawValue)/Caskroom/"
+ (App.identifier.contains(".dev") ? "phpmon-dev" : "phpmon")
}
public static var shell: String {
return shared.preferredShell
}
// MARK: - Flexible Binaries
// (these can be in multiple locations, so we scan common places because)
// (PHP Monitor will not use the user's own PATH)
@ -99,6 +128,8 @@ public class Paths {
Paths.composer = "/usr/local/bin/composer"
} else if FileSystem.fileExists("/opt/homebrew/bin/composer") {
Paths.composer = "/opt/homebrew/bin/composer"
} else if FileSystem.fileExists("/usr/local/homebrew/bin/composer") {
Paths.composer = "/usr/local/homebrew/bin/composer"
} else {
Paths.composer = nil
Log.warn("Composer was not found.")
@ -110,6 +141,7 @@ public class Paths {
public enum HomebrewDir: String {
case opt = "/opt/homebrew"
case usr = "/usr/local"
case usr_hb = "/usr/local/homebrew"
}
}

View File

@ -15,4 +15,10 @@ extension Date {
return dateFormatter.string(from: self)
}
static func fromString(_ string: String) -> Date? {
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy-MM-dd"
return dateFormatter.date(from: string)
}
}

View File

@ -8,6 +8,18 @@ import Foundation
import SwiftUI
struct Localization {
static var preferredLanguage: String? {
guard let language = Preferences.preferences[.languageOverride] as? String else {
return nil
}
if language.isEmpty {
return nil
}
return language
}
static var bundle: Bundle = {
if !isRunningTests {
return Bundle.main
@ -32,13 +44,30 @@ struct Localization {
extension String {
var localized: String {
if #available(macOS 13, *) {
return NSLocalizedString(
self, tableName: nil, bundle: Localization.bundle, value: "", comment: ""
).replacingOccurrences(of: "Preferences", with: "Settings")
var preferredBundle: Bundle = Localization.bundle
if let preferred = Localization.preferredLanguage,
let path = Localization.bundle.path(forResource: preferred, ofType: "lproj"),
let bundle = Bundle(path: path) {
preferredBundle = bundle
}
return NSLocalizedString(self, tableName: nil, bundle: Localization.bundle, value: "", comment: "")
let string = NSLocalizedString(self, tableName: nil, bundle: preferredBundle, value: "", comment: "")
// Fallback to English translation if the localized value is equal to the key (should not happen)
if string == self {
guard let path = Localization.bundle.path(forResource: "en", ofType: "lproj"),
let bundle = Bundle(path: path)
else { return self }
return NSLocalizedString(self, bundle: bundle, comment: "")
}
// Ensure that on more recent versions of macOS, "Preferences" is replaced with "Settings"
if #available(macOS 13, *) {
return string.replacingOccurrences(of: "Preferences", with: "Settings")
}
return string
}
var localizedForSwiftUI: LocalizedStringKey {
@ -131,4 +160,10 @@ extension String {
return ""
}
}
var isNumber: Bool {
return self.range(
of: "^[0-9]*$", // 1
options: .regularExpression) != nil
}
}

View File

@ -41,27 +41,30 @@ class RealFileSystem: FileSystemProtocol {
}
func getShallowContentsOfDirectory(_ path: String) throws -> [String] {
return try FileManager.default.contentsOfDirectory(atPath: path)
return try FileManager.default.contentsOfDirectory(atPath: path.replacingTildeWithHomeDirectory)
}
func getDestinationOfSymlink(_ path: String) throws -> String {
return try FileManager.default.destinationOfSymbolicLink(atPath: path)
return try FileManager.default.destinationOfSymbolicLink(atPath: path.replacingTildeWithHomeDirectory)
}
// MARK: - Move & Delete Files
func move(from path: String, to newPath: String) throws {
try FileManager.default.moveItem(atPath: path, toPath: newPath)
try FileManager.default.moveItem(
atPath: path.replacingTildeWithHomeDirectory,
toPath: newPath.replacingTildeWithHomeDirectory
)
}
func remove(_ path: String) throws {
try FileManager.default.removeItem(atPath: path)
try FileManager.default.removeItem(atPath: path.replacingTildeWithHomeDirectory)
}
// MARK: FS Attributes
func makeExecutable(_ path: String) throws {
_ = system("chmod +x \(path.replacingTildeWithHomeDirectory)")
_ = ActiveShell.shared.sync("chmod +x \(path.replacingTildeWithHomeDirectory)")
}
// MARK: - Checks

View File

@ -14,6 +14,7 @@ class Alert {
messageText: String,
informativeText: String,
buttonTitle: String = "generic.ok".localized,
buttonIsDestructive: Bool = false,
secondButtonTitle: String = "generic.cancel".localized,
style: NSAlert.Style = .warning,
onFirstButtonPressed: @escaping (() -> Void)
@ -27,6 +28,7 @@ class Alert {
alert.messageText = messageText
alert.informativeText = informativeText
alert.addButton(withTitle: buttonTitle)
alert.buttons.first?.hasDestructiveAction = buttonIsDestructive
if !secondButtonTitle.isEmpty {
alert.addButton(withTitle: secondButtonTitle)
}

View File

@ -10,8 +10,8 @@ import UserNotifications
class LocalNotification {
@MainActor public static func send(title: String, subtitle: String, preference: PreferenceName) {
if !Preferences.isEnabled(preference) {
@MainActor public static func send(title: String, subtitle: String, preference: PreferenceName?) {
if preference != nil && !Preferences.isEnabled(preference!) {
return
}

View File

@ -0,0 +1,25 @@
//
// LoginItemManager.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 15/02/2023.
// Copyright © 2023 Nico Verbruggen. All rights reserved.
//
import AppKit
import ServiceManagement
@available(macOS 13.0, *)
class LoginItemManager {
func loginItemIsEnabled() -> Bool {
return SMAppService.mainApp.status == .enabled
}
func disableLoginItem() {
try? SMAppService.mainApp.unregister()
}
func enableLoginItem() {
try? SMAppService.mainApp.register()
}
}

View File

@ -0,0 +1,17 @@
//
// Measurements.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 02/03/2023.
// Copyright © 2023 Nico Verbruggen. All rights reserved.
//
import Foundation
public struct Measurement {
let started = Date()
var milliseconds: Double {
return round(Date().timeIntervalSince(started) * 1000 * 1000) / 1000
}
}

View File

@ -75,7 +75,7 @@ class MenuBarImageGenerator {
// Then we'll fetch the image we want on the left
var iconType = Preferences.preferences[.iconTypeToDisplay] as? String
if iconType == nil {
if iconType == nil || !MenuBarIcon.allCases.map({ $0.rawValue }).contains(iconType) {
Log.warn("Invalid icon type found, using the default")
iconType = MenuBarIcon.iconPhp.rawValue
}

View File

@ -29,6 +29,8 @@ class PMWindowController: NSWindowController, NSWindowDelegate {
App.shared.remove(window: windowName)
}
func windowDidResize(_ notification: Notification) {}
deinit {
Log.perf("deinit: \(String(describing: self)).\(#function)")
}
@ -37,13 +39,13 @@ class PMWindowController: NSWindowController, NSWindowDelegate {
extension NSWindowController {
public func positionWindowInTopLeftCorner() {
public func positionWindowInTopRightCorner(offsetY: CGFloat = 0, offsetX: CGFloat = 0) {
guard let frame = NSScreen.main?.frame else { return }
guard let window = self.window else { return }
window.setFrame(NSRect(
x: frame.size.width - window.frame.size.width - 20,
y: frame.size.height - window.frame.size.height - 40,
x: frame.size.width - window.frame.size.width - 20 + offsetX,
y: frame.size.height - window.frame.size.height - 40 + offsetY,
width: window.frame.width,
height: window.frame.height
), display: true)

View File

@ -10,7 +10,6 @@ import Foundation
/**
Run a simple blocking Shell command on the user's own system.
Avoid using this method in favor of the fakeable Shell class unless needed for express system operations.
*/
public func system(_ command: String) -> String {
let task = Process()
@ -26,3 +25,50 @@ public func system(_ command: String) -> String {
return output
}
/**
Same as the `system` command, but does not return the output.
*/
public func system_quiet(_ command: String) {
let task = Process()
task.launchPath = "/bin/sh"
task.arguments = ["-c", command]
let pipe = Pipe()
task.standardOutput = pipe
task.launch()
_ = pipe.fileHandleForReading.readDataToEndOfFile()
return
}
/**
Retrieves the username for the currently signed in user via `/usr/bin/id`.
This cannot fail or the application will crash.
*/
public func identity() -> String {
let task = Process()
task.launchPath = "/usr/bin/id"
task.arguments = ["-un"]
let pipe = Pipe()
task.standardOutput = pipe
task.launch()
guard let output = String(
data: pipe.fileHandleForReading.readDataToEndOfFile(),
encoding: String.Encoding.utf8
) else {
fatalError("Could not retrieve username via `id -un`!")
}
return output.trimmingCharacters(in: .whitespacesAndNewlines)
}
/**
Retrieves the user's preferred shell.
*/
public func preferred_shell() -> String {
return system("dscl . -read ~/ UserShell | sed 's/UserShell: //'")
.trimmingCharacters(in: .whitespacesAndNewlines)
}

View File

@ -1,17 +0,0 @@
//
// WIP.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 01/11/2022.
// Copyright © 2023 Nico Verbruggen. All rights reserved.
//
import Foundation
func todo(_ context: String = "") {
if !context.isEmpty {
fatalError("To be implemented: \(context)")
}
fatalError("To be implemented")
}

View File

@ -32,18 +32,25 @@ class ActivePhpInstallation {
// MARK: - Computed
var formula: String {
return (version.short == PhpEnv.brewPhpAlias) ? "php" : "php@\(version.short)"
return (version.short == PhpEnvironments.brewPhpAlias) ? "php" : "php@\(version.short)"
}
// MARK: - Initializer
public static func load() -> ActivePhpInstallation? {
if !FileSystem.fileExists(Paths.phpConfig) {
return nil
}
return ActivePhpInstallation()
}
init() {
// Show information about the current version
do {
try determineVersion()
} catch {
// TODO: In future versions of PHP Monitor, this should not crash
fatalError("Could not determine or parse PHP version; aborting")
fatalError("Could not determine or parse PHP version; aborting!")
}
// Initialize the list of ini files that are loaded
@ -55,13 +62,6 @@ class ActivePhpInstallation {
return
}
// Load extension information
let mainConfigurationFileUrl = URL(fileURLWithPath: "\(Paths.etcPath)/php/\(version.short)/php.ini")
if let file = PhpConfigurationFile.from(filePath: mainConfigurationFileUrl.path) {
iniFiles.append(file)
}
// Get configuration values
limits = Limits(
memory_limit: getByteCount(key: "memory_limit"),
@ -69,15 +69,10 @@ class ActivePhpInstallation {
post_max_size: getByteCount(key: "post_max_size")
)
// Return a list of .ini files parsed after php.ini
let paths = Command.execute(
path: Paths.php,
arguments: ["-r", "echo php_ini_scanned_files();"],
trimNewlines: false
)
.replacingOccurrences(of: "\n", with: "")
.split(separator: ",")
.map { String($0) }
let paths = ActiveShell.shared
.sync("\(Paths.php) --ini | grep -E -o '(/[^ ]+\\.ini)'").out
.split(separator: "\n")
.map { String($0) }
// See if any extensions are present in said .ini files
paths.forEach { (iniFilePath) in
@ -122,29 +117,19 @@ class ActivePhpInstallation {
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: NSRange(location: 0, length: value.count)).first
return (match == nil) ? "⚠️" : "\(value)B"
}
/**
Determine if PHP-FPM is configured correctly.
For PHP 5.6, we'll check if `valet.sock` is included in the main `php-fpm.conf` file, but for more recent
versions of PHP, we can just check for the existence of the `valet-fpm.conf` file. If the check here fails,
that means that Valet won't work properly.
*/
func checkPhpFpmStatus() async -> Bool {
if self.version.short == "5.6" {
// The main PHP config file should contain `valet.sock` and then we're probably fine?
let fileName = "\(Paths.etcPath)/php/5.6/php-fpm.conf"
return await Shell.pipe("cat \(fileName)").out
.contains("valet.sock")
if value.isEmpty {
return "⚠️"
}
// Make sure to check if valet-fpm.conf exists. If it does, we should be fine :)
return FileSystem.fileExists("\(Paths.etcPath)/php/\(self.version.short)/php-fpm.d/valet-fpm.conf")
// 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: NSRange(location: 0, length: value.count)
).first
return (match == nil) ? "⚠️" : "\(value)B"
}
// MARK: - Structs

View File

@ -12,11 +12,11 @@ import Cocoa
class Xdebug {
public static var enabled: Bool {
return PhpEnv.shared.getConfigFile(forKey: "xdebug.mode") != nil
return PhpEnvironments.shared.getConfigFile(forKey: "xdebug.mode") != nil
}
public static var activeModes: [String] {
guard let file = PhpEnv.shared.getConfigFile(forKey: "xdebug.mode") else {
guard let file = PhpEnvironments.shared.getConfigFile(forKey: "xdebug.mode") else {
return []
}

View File

@ -1,5 +1,5 @@
//
// HomebrewPackage.swift
// HomebrewDecodable.swift
// PHP Monitor
//
// Copyright © 2023 Nico Verbruggen. All rights reserved.
@ -8,8 +8,6 @@
import Foundation
struct HomebrewPackage: Decodable {
let name: String
let full_name: String
let aliases: [String]
let installed: [HomebrewInstalled]
@ -19,7 +17,6 @@ struct HomebrewPackage: Decodable {
return aliases.first!
.replacingOccurrences(of: "php@", with: "")
}
}
struct HomebrewInstalled: Decodable {
@ -28,3 +25,15 @@ struct HomebrewInstalled: Decodable {
let installed_as_dependency: Bool
let installed_on_request: Bool
}
struct OutdatedFormulae: Decodable {
let formulae: [OutdatedFormula]
}
struct OutdatedFormula: Decodable {
let name: String
let installed_versions: [String]
let current_version: String
let pinned: Bool
let pinned_version: String?
}

View File

@ -1,5 +1,5 @@
//
// PhpSwitcher.swift
// PhpEnvironments.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 21/12/2021.
@ -8,15 +8,28 @@
import Foundation
class PhpEnv {
class PhpEnvironments {
// MARK: - Initializer
/**
Loads the currently active PHP installation upon startup. May be empty.
*/
init() {
self.currentInstall = ActivePhpInstallation()
self.currentInstall = ActivePhpInstallation.load()
}
func determinePhpAlias() async {
/**
Creates the shared instance. Called when starting the app.
*/
static func prepare() {
_ = Self.shared
}
/**
Determine which PHP version the `php` formula is aliased to.
*/
@MainActor func determinePhpAlias() async {
let brewPhpAlias = await Shell.pipe("\(Paths.brew) info php --json").out
self.homebrewPackage = try! JSONDecoder().decode(
@ -24,7 +37,28 @@ class PhpEnv {
from: brewPhpAlias.data(using: .utf8)!
).first!
Log.info("[BREW] On your system, the `php` formula means version \(homebrewPackage.version)!")
PhpEnvironments.brewPhpAlias = self.homebrewPackage.version
Log.info("[BREW] On your system, the `php` formula means version \(homebrewPackage.version).")
// Check if that version actually corresponds to an older version
let phpConfigExecutablePath = "\(Paths.optPath)/php/bin/php-config"
if FileSystem.fileExists(phpConfigExecutablePath) {
let longVersionString = Command.execute(
path: phpConfigExecutablePath,
arguments: ["--version"],
trimNewlines: false
).trimmingCharacters(in: .whitespacesAndNewlines)
if let version = try? VersionNumber.parse(longVersionString) {
PhpEnvironments.brewPhpAlias = version.short
if version.short != homebrewPackage.version {
Log.info("[BREW] An older version of `php` is actually installed (\(version.short)).")
}
} else {
Log.warn("Could not determine the actual version of the php binary; assuming Homebrew is correct.")
PhpEnvironments.brewPhpAlias = homebrewPackage.version
}
}
}
// MARK: - Properties
@ -32,11 +66,16 @@ class PhpEnv {
/** The delegate that is informed of updates. */
weak var delegate: PhpSwitcherDelegate?
/** The static app instance. Accessible at any time. */
static let shared = PhpEnv()
/** The static instance. Accessible at any time. */
static let shared = PhpEnvironments()
/** Whether the switcher is busy performing any actions. */
var isBusy: Bool = false
@MainActor var isBusy: Bool = false {
didSet {
MainMenu.shared.refreshIcon()
MainMenu.shared.rebuild()
}
}
/** All versions of PHP that are currently supported. */
var availablePhpVersions: [String] = []
@ -48,7 +87,14 @@ class PhpEnv {
var cachedPhpInstallations: [String: PhpInstallation] = [:]
/** Information about the currently linked PHP installation. */
var currentInstall: ActivePhpInstallation!
var currentInstall: ActivePhpInstallation? {
didSet {
// Let the PHP extension manager, if it exists, know the version changed
if let version = currentInstall?.version.short {
App.shared.phpExtensionManagerWindowController?.view?.manager.phpVersion = version
}
}
}
/**
The version that the `php` formula via Brew is aliased to on the current system.
@ -59,16 +105,21 @@ class PhpEnv {
As such, we take that information from Homebrew.
*/
static var brewPhpAlias: String {
if Homebrew.fake { return "8.2" }
static var brewPhpAlias: String = ""
return Self.shared.homebrewPackage.version
/**
It's possible for the alias to be newer than the actual installed version of PHP.
*/
static var homebrewBrewPhpAlias: String {
if PhpEnvironments.shared.homebrewPackage == nil { return "8.2" }
return PhpEnvironments.shared.homebrewPackage.version
}
/**
The currently linked and active PHP installation.
*/
static var phpInstall: ActivePhpInstallation {
static var phpInstall: ActivePhpInstallation? {
return Self.shared.currentInstall
}
@ -79,25 +130,45 @@ class PhpEnv {
// MARK: - Methods
/**
The switcher that is currently in use.
This was originally added so the Internal and Valet switcher could be swapped out,
but currently this is no longer needed.
*/
public static var switcher: PhpSwitcher {
return InternalSwitcher()
}
/**
Alias that detects which versions of PHP are installed.
See also: `detectPhpVersions()`. Please note that this method
does *not* return the set of PHP versions that are supported.
*/
public static func detectPhpVersions() async {
_ = await Self.shared.detectPhpVersions()
}
/**
Detects which versions of PHP are installed.
This step also detects which versions of PHP are incompatible with the current version of Valet.
If a PHP installation is currently broken, that will also be reflected.
Returns a `Set<String>` of installations that are considered valid.
*/
public func detectPhpVersions() async -> Set<String> {
let files = await Shell.pipe("ls \(Paths.optPath) | grep php@").out
let versions = await extractPhpVersions(from: files.components(separatedBy: "\n"))
let supportedByValet = Constants.ValetSupportedPhpVersionMatrix[Valet.shared.version.major] ?? []
let supportedByValet: Set<String> = {
guard let version = Valet.shared.version else {
return Constants.DetectedPhpVersions
}
var supportedVersions = versions.intersection(supportedByValet)
return Constants.ValetSupportedPhpVersionMatrix[version.major] ?? []
}()
var supportedVersions = Valet.installed ? versions.intersection(supportedByValet) : versions
// Make sure the aliased version is detected
// The user may have `php` installed, but not e.g. `php@8.0`
@ -106,7 +177,12 @@ class PhpEnv {
// Avoid inserting a duplicate
if !supportedVersions.contains(phpAlias) && FileSystem.fileExists("\(Paths.optPath)/php/bin/php") {
supportedVersions.insert(phpAlias)
let phpAliasInstall = PhpInstallation(phpAlias)
// Before inserting, ensure that the actual output matches the alias
// if that isn't the case, our formula remains out-of-date
if !phpAliasInstall.isMissingBinary {
supportedVersions.insert(phpAlias)
}
}
availablePhpVersions = Array(supportedVersions)
@ -167,6 +243,10 @@ class PhpEnv {
return output
}
/**
Returns a list of `VersionNumber` instances based on the available PHP versions
that are valid to switch to for a given constraint.
*/
public func validVersions(for constraint: String) -> [VersionNumber] {
constraint.split(separator: "|").flatMap {
return PhpVersionNumberCollection
@ -179,7 +259,12 @@ class PhpEnv {
Validates whether the currently running version matches the provided version.
*/
public func validate(_ version: String) -> Bool {
if self.currentInstall.version.short == version {
guard let install = PhpEnvironments.phpInstall else {
Log.info("It appears as if no PHP installation is currently active.")
return false
}
if install.version.short == version {
Log.info("Switching to version \(version) seems to have succeeded. Validation passed.")
Log.info("Keeping track that this is the new version!")
Stats.persistCurrentGlobalPhpVersion(version: version)
@ -195,7 +280,11 @@ class PhpEnv {
You can then use the configuration file instance to change values.
*/
public func getConfigFile(forKey key: String) -> PhpConfigurationFile? {
return PhpEnv.phpInstall.iniFiles
guard let install = PhpEnvironments.phpInstall else {
return nil
}
return install.iniFiles
.reversed()
.first(where: { $0.has(key: key) })
}

View File

@ -28,10 +28,12 @@ class PhpHelper {
Task { // Create the appropriate folders and check if the files exist
do {
if !FileSystem.directoryExists("~/.config/phpmon/bin") {
try FileSystem.createDirectory(
"~/.config/phpmon/bin",
withIntermediateDirectories: true
)
Task { @MainActor in
try FileSystem.createDirectory(
"~/.config/phpmon/bin",
withIntermediateDirectories: true
)
}
}
if FileSystem.fileExists(destination) {
@ -47,22 +49,17 @@ class PhpHelper {
let path = URL(fileURLWithPath: "\(Paths.optPath)/php@\(version)/bin")
.resolvingSymlinksInPath().path
// The contents of the script!
let script = """
#!/bin/zsh
# \(keyPhrase)
# It reflects the location of PHP \(version)'s binaries on your system.
# Usage: . pm\(dotless)
[[ $ZSH_EVAL_CONTEXT =~ :file$ ]] \\
&& echo "PHP Monitor has enabled this terminal to use PHP \(version)." \\
|| echo "You must run '. pm\(dotless)' (or 'source pm\(dotless)') instead!";
export PATH=\(path):$PATH
"""
// Check if the user uses Fish
let script = Paths.shell.contains("/fish")
? fishScript(path, keyPhrase, version, dotless)
: zshScript(path, keyPhrase, version, dotless)
try FileSystem.writeAtomicallyToFile(destination, content: script)
Task { @MainActor in
try FileSystem.writeAtomicallyToFile(destination, content: script)
if !FileSystem.isExecutableFile(destination) {
try FileSystem.makeExecutable(destination)
if !FileSystem.isExecutableFile(destination) {
try FileSystem.makeExecutable(destination)
}
}
// Create a symlink if the folder is not in the PATH
@ -83,6 +80,40 @@ class PhpHelper {
}
}
private static func zshScript(
_ path: String,
_ keyPhrase: String,
_ version: String,
_ dotless: String
) -> String {
return """
#!/bin/zsh
# \(keyPhrase)
# It reflects the location of PHP \(version)'s binaries on your system.
# Usage: . pm\(dotless)
[[ $ZSH_EVAL_CONTEXT =~ :file$ ]] \\
&& echo "PHP Monitor has enabled this terminal to use PHP \(version)." \\
|| echo "You must run '. pm\(dotless)' (or 'source pm\(dotless)') instead!";
export PATH=\(path):$PATH
"""
}
private static func fishScript(
_ path: String,
_ keyPhrase: String,
_ version: String,
_ dotless: String
) -> String {
return """
#!\(Paths.binPath)/fish
# \(keyPhrase)
# It reflects the location of PHP \(version)'s binaries on your system.
# Usage: . pm\(dotless)
echo "PHP Monitor has enabled this terminal to use PHP \(version)."; \\
set -x PATH \(path) $PATH
"""
}
private static func createSymlink(_ dotless: String) async {
let source = "\(Paths.homePath)/.config/phpmon/bin/pm\(dotless)"
let destination = "/usr/local/bin/pm\(dotless)"

View File

@ -35,6 +35,7 @@ public struct PhpVersionNumberCollection: Equatable {
- Parameter strict: Whether the patch version check is strict. See more below.
The strict mode does not matter if a patch version is provided for all versions in the collection.
It also does not matter for certain comparisons (e.g. when dealing with wildcards).
Strict mode assumes that any PHP version lacking precise patch information, e.g. inferred
from Homebrew corresponds to the .0 patch version of that version. The default, which is imprecise,
@ -45,6 +46,7 @@ public struct PhpVersionNumberCollection: Equatable {
Given versions 8.0.? and 8.1.?, but the requirement is ^8.0.1, in strict mode only 8.1.? will
be considered valid (8.0 translates to 8.0.0 and as such is older than 8.0.1, 8.1.0 is OK).
When checking against actual PHP versions installed by the user (with patch precision), use
strict mode.
@ -52,11 +54,26 @@ public struct PhpVersionNumberCollection: Equatable {
Given versions 8.0.? and 8.1.?, but the requirement is ^8.0.1, in non-strict mode version 8.0
is assumed to be equal to version 8.0.999, which is actually fine if 8.0.1 is the required version.
In non-strict mode, the patch version is ignored for regular version checks (no caret / tilde).
If checking compatibility with general Homebrew versions of PHP, do NOT use strict mode, since
the patch version there is not used. (The formula php@8.0 suffices for ^8.0.1.)
*/
public func matching(constraint: String, strict: Bool = false) -> [VersionNumber] {
if constraint == "*" {
return self.versions
}
if let version = VersionNumber.make(from: constraint, type: .wildCardPatch) {
// Wildcard for patch (e.g. "7.4.*") must match major and minor (any patch)
return self.versions.filter { $0.hasSameMajorAndMinor(version) }
}
if let version = VersionNumber.make(from: constraint, type: .wildCardMinor) {
// Strict constraint (e.g. "7.*") -> must only match major (any patch, minor)
return self.versions.filter { $0.isSameMajorVersionAs(version) }
}
if let version = VersionNumber.make(from: constraint, type: .versionOnly) {
// Strict constraint (e.g. "7.0") -> returns specific version
return self.versions.filter { $0.isSameAs(version, strict) }

View File

@ -39,6 +39,8 @@ public struct VersionNumber: Equatable, Hashable {
public enum MatchType: String {
case versionOnly = #"^(?<major>\d+).(?<minor>\d+).?(?<patch>\d+)?\z"#
case wildCardPatch = #"^(?<major>\d+).(?<minor>\d+).?(?<patch>\*)?\z"#
case wildCardMinor = #"^(?<major>\d+).(?<minor>\*)?\z"#
case caretVersionRange = #"^\^(?<major>\d+).(?<minor>\d+).?(?<patch>\d+)?\z"#
case tildeVersionRange = #"^~(?<major>\d+).(?<minor>\d+).?(?<patch>\d+)?\z"#
case greaterThanOrEqual = #"^>=(?<major>\d+).(?<minor>\d+).?(?<patch>\d+)?\z"#
@ -64,21 +66,25 @@ public struct VersionNumber: Equatable, Hashable {
range: NSRange(location: 0, length: versionString.count)
).first
if match != nil {
let major = Int(
versionString[Range(match!.range(withName: "major"), in: versionString)!]
)!
let minor = Int(
versionString[Range(match!.range(withName: "minor"), in: versionString)!]
)!
var patch: Int?
if let minorRange = Range(match!.range(withName: "patch"), in: versionString) {
patch = Int(versionString[minorRange])
}
return Self(major: major, minor: minor, patch: patch)
guard let match else { return nil }
let major = Int(versionString[Range(match.range(withName: "major"), in: versionString)!])!
var minor: Int = 0
var patch: Int?
if let minorRange = Range(match.range(withName: "minor"), in: versionString) {
let value = versionString[minorRange] as String
// Zero is the fallback if a wildcard was used
minor = Int(value) ?? 0
}
return nil
if let patchRange = Range(match.range(withName: "patch"), in: versionString) {
let value = versionString[patchRange] as String
// nil is the fallback if a wildcard was used
patch = Int(value) ?? nil
}
return Self(major: major, minor: minor, patch: patch)
}
// MARK: Comparison Logic
@ -93,6 +99,10 @@ public struct VersionNumber: Equatable, Hashable {
&& (strict ? self.patch(strict, version) == version.patch(strict) : true)
}
internal func hasSameMajorAndMinor(_ version: VersionNumber) -> Bool {
return self.major == version.major && self.minor == version.minor
}
internal func isNewerThan(_ version: VersionNumber, _ strict: Bool) -> Bool {
return (
self.major > version.major ||

View File

@ -69,8 +69,9 @@ class PhpConfigurationFile: CreatedFromFile {
return nil
}
enum ReplacementErrors: Error {
public enum ReplacementErrors: Error {
case missingKey
case missingFile
}
/**
@ -95,10 +96,16 @@ class PhpConfigurationFile: CreatedFromFile {
// Replace the specific line
self.lines[item.lineIndex] = components.joined(separator: "=")
// Ensure the watchers aren't tripped up by config changes
ConfigWatchManager.ignoresModificationsToConfigValues = true
// Finally, join the string and save the file atomatically again
try self.lines.joined(separator: "\n")
.write(toFile: self.filePath, atomically: true, encoding: .utf8)
// Ensure watcher behaviour is reverted
ConfigWatchManager.ignoresModificationsToConfigValues = false
// Reload the original file
self.reload()
}

View File

@ -67,7 +67,7 @@ class PhpExtension {
self.name = String(fullPath.split(separator: "/").last!) // take last segment
self.enabled = !line.contains(";")
self.enabled = !line.starts(with: ";")
self.file = file
}
@ -76,7 +76,7 @@ class PhpExtension {
You may need to restart the other services in order for this change to apply.
*/
func toggle() async {
let newLine = enabled
let newLine = !line.starts(with: ";")
// DISABLED: Commented out line
? "; \(line)"
// ENABLED: Line where the comment delimiter (;) is removed
@ -84,14 +84,14 @@ class PhpExtension {
await sed(file: file, original: line, replacement: newLine)
enabled.toggle()
self.enabled = !newLine.starts(with: ";")
self.line = newLine
if !isRunningTests {
Task { @MainActor in
MainMenu.shared.rebuild()
}
}
}
// MARK: - Static Methods

View File

@ -12,15 +12,46 @@ class PhpInstallation {
var versionNumber: VersionNumber
var iniFiles: [PhpConfigurationFile] = []
var isMissingBinary: Bool = false
var isHealthy: Bool = true
var extensions: [PhpExtension] {
return self.iniFiles.flatMap({ $0.extensions })
}
var formulaName: String {
let version = self.versionNumber.short
if version == PhpEnvironments.brewPhpAlias {
return "php"
}
return "php@\(self.versionNumber.short)"
}
/**
In order to determine details about a PHP installation, well simply run `php-config --version`
in the relevant directory.
In order to determine details about a PHP installation,
well simply run `php-config --version` in the relevant directory.
*/
init(_ version: String) {
let phpConfigExecutablePath = "\(Paths.optPath)/php@\(version)/bin/php-config",
phpExecutablePath = "\(Paths.optPath)/php@\(version)/bin/php"
let phpConfigExecutablePath = "\(Paths.optPath)/php@\(version)/bin/php-config"
self.versionNumber = VersionNumber.make(from: version)!
versionNumber = VersionNumber.make(from: version)!
determineVersion(phpConfigExecutablePath, phpExecutablePath)
determineHealth(phpExecutablePath)
determineIniFiles(phpExecutablePath)
// Find all enabled extensions
let enabled = self.extensions.filter({ $0.enabled }).map({ $0.name })
Log.info("PHP \(versionNumber.short) has the following extensions enabled: \(enabled)")
}
private func determineVersion(_ phpConfigExecutablePath: String, _ phpExecutablePath: String) {
if FileSystem.fileExists(phpConfigExecutablePath) {
let longVersionString = Command.execute(
path: phpConfigExecutablePath,
@ -30,8 +61,43 @@ class PhpInstallation {
// The parser should always work, or the string has to be very unusual.
// If so, the app SHOULD crash, so that the users report what's up.
self.versionNumber = try! VersionNumber.parse(longVersionString)
versionNumber = try! VersionNumber.parse(longVersionString)
} else {
// Keep track that the `php-config` binary is missing; this often means there's a mismatch between
// the `php` version alias and the actual installed version (e.g. you haven't upgraded `php`)
isMissingBinary = true
}
}
private func determineHealth(_ phpExecutablePath: String) {
if FileSystem.fileExists(phpExecutablePath) {
let testCommand = Command.execute(
path: phpExecutablePath,
arguments: ["-v"],
trimNewlines: false,
withStandardError: true
).trimmingCharacters(in: .whitespacesAndNewlines)
// If the "dyld: Library not loaded" issue pops up, we have an unhealthy PHP installation
// and we will need to reinstall this version of PHP via Homebrew.
if testCommand.contains("Library not loaded") && testCommand.contains("dyld") {
self.isHealthy = false
Log.err("The PHP installation of \(self.versionNumber.short) is not healthy!")
}
}
}
private func determineIniFiles(_ phpExecutablePath: String) {
let paths = ActiveShell.shared
.sync("\(phpExecutablePath) --ini | grep -E -o '(/[^ ]+\\.ini)'").out
.split(separator: "\n")
.map { String($0) }
// See if any extensions are present in said .ini files
paths.forEach { (iniFilePath) in
if let file = PhpConfigurationFile.from(filePath: iniFilePath) {
iniFiles.append(file)
}
}
}
}

View File

@ -0,0 +1,136 @@
//
// InternalSwitcher+Valet.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 14/03/2023.
// Copyright © 2023 Nico Verbruggen. All rights reserved.
//
import Foundation
extension InternalSwitcher {
typealias FixApplied = Bool
public func ensureValetConfigurationIsValidForPhpVersion(_ version: String) async -> FixApplied {
// Early exit if Valet is not installed
if !Valet.installed {
assertionFailure("Cannot ensure that Valet configuration is valid if Valet is not installed.")
return false
}
let corrections = [
await self.disableDefaultPhpFpmPool(version),
await self.ensureConfigurationFilesExist(version)
]
return corrections.contains(true)
}
// MARK: - Corrections
public func disableDefaultPhpFpmPool(_ version: String) async -> FixApplied {
let pool = "\(Paths.etcPath)/php/\(version)/php-fpm.d/www.conf"
if FileSystem.fileExists(pool) {
Log.info("A default `www.conf` file was found in the php-fpm.d directory for PHP \(version).")
let existing = "\(Paths.etcPath)/php/\(version)/php-fpm.d/www.conf"
let new = "\(Paths.etcPath)/php/\(version)/php-fpm.d/www.conf.disabled-by-phpmon"
do {
if FileSystem.fileExists(new) {
Log.info("A moved `www.conf.disabled-by-phpmon` file was found for PHP \(version), "
+ "cleaning up so the newer `www.conf` can be moved again.")
try FileSystem.remove(new)
}
try FileSystem.move(from: existing, to: new)
Log.info("Success: A default `www.conf` file was disabled for PHP \(version).")
return true
} catch {
Log.err(error)
return false
}
}
return false
}
public func ensureConfigurationFilesExist(_ version: String) async -> FixApplied {
let files = self.getExpectedConfigurationFiles(for: version)
// For each of the files, attempt to fix anything that is wrong
let outcomes = files.map { file in
let configFileExists = FileSystem.fileExists("\(Paths.etcPath)/php/\(version)/" + file.destination)
if configFileExists {
return false
}
Log.info("Config file `\(file.destination)` does not exist, will attempt to automatically fix!")
if !file.applies() {
return false
}
do {
var contents = try FileSystem.getStringFromFile("~/.composer/vendor/laravel/valet" + file.source)
for (original, replacement) in file.replacements {
contents = contents.replacingOccurrences(of: original, with: replacement)
}
try FileSystem.writeAtomicallyToFile(
"\(Paths.etcPath)/php/\(version)" + file.destination,
content: contents
)
} catch {
Log.err("Automatically fixing \(file.destination) did not work.")
return false
}
return true
}
// If any fixes were applied, return true
return outcomes.contains(true)
}
// MARK: - Internals
private func getExpectedConfigurationFiles(for version: String) -> [ExpectedConfigurationFile] {
return [
ExpectedConfigurationFile(
destination: "/php-fpm.d/valet-fpm.conf",
source: "/cli/stubs/etc-phpfpm-valet.conf",
replacements: [
"VALET_USER": Paths.whoami,
"VALET_HOME_PATH": "~/.config/valet".replacingTildeWithHomeDirectory,
"valet.sock": "valet\(version.replacingOccurrences(of: ".", with: "")).sock"
],
applies: { Valet.shared.version!.major > 2 }
),
ExpectedConfigurationFile(
destination: "/conf.d/error_log.ini",
source: "/cli/stubs/etc-phpfpm-error_log.ini",
replacements: [
"VALET_USER": Paths.whoami,
"VALET_HOME_PATH": "~/.config/valet".replacingTildeWithHomeDirectory
],
applies: { return true }
),
ExpectedConfigurationFile(
destination: "/conf.d/php-memory-limits.ini",
source: "/cli/stubs/php-memory-limits.ini",
replacements: [:],
applies: { return true }
)
]
}
}
public struct ExpectedConfigurationFile {
let destination: String
let source: String
let replacements: [String: String]
let applies: () -> Bool
}

View File

@ -22,14 +22,12 @@ class InternalSwitcher: PhpSwitcher {
*/
func performSwitch(to version: String) async {
Log.info("Switching to \(version), unlinking all versions...")
let versions = getVersionsToBeHandled(version)
await withTaskGroup(of: String.self, body: { group in
for available in PhpEnv.shared.availablePhpVersions {
for available in PhpEnvironments.shared.availablePhpVersions {
group.addTask {
await self.disableDefaultPhpFpmPool(available)
await self.stopPhpVersion(available)
await self.unlinkAndStopPhpVersion(available)
return available
}
}
@ -43,12 +41,19 @@ class InternalSwitcher: PhpSwitcher {
Log.info("Linking the new version \(version)!")
for formula in versions {
if Valet.installed {
Log.info("Ensuring that the Valet configuration is valid...")
_ = await self.ensureValetConfigurationIsValidForPhpVersion(formula)
}
Log.info("Will start PHP \(version)... (primary: \(version == formula))")
await self.startPhpVersion(formula, primary: (version == formula))
await self.linkAndStartPhpVersion(formula, primary: (version == formula))
}
Log.info("Restarting nginx, just to be sure!")
await brew("services restart nginx", sudo: true)
if Valet.installed {
Log.info("Restarting nginx, just to be sure!")
await brew("services restart nginx", sudo: true)
}
Log.info("The new version(s) have been linked!")
})
@ -70,56 +75,36 @@ class InternalSwitcher: PhpSwitcher {
return versions
}
func requiresDisablingOfDefaultPhpFpmPool(_ version: String) -> Bool {
let pool = "\(Paths.etcPath)/php/\(version)/php-fpm.d/www.conf"
return FileSystem.fileExists(pool)
func unlinkAndStopPhpVersion(_ version: String) async {
let formula = (version == PhpEnvironments.brewPhpAlias) ? "php" : "php@\(version)"
await brew("unlink \(formula)")
if Valet.installed {
await brew("services stop \(formula)", sudo: true)
Log.info("Unlinked and stopped services for \(formula)")
} else {
Log.info("Unlinked \(formula)")
}
}
func disableDefaultPhpFpmPool(_ version: String) async {
let pool = "\(Paths.etcPath)/php/\(version)/php-fpm.d/www.conf"
if FileSystem.fileExists(pool) {
Log.info("A default `www.conf` file was found in the php-fpm.d directory for PHP \(version).")
let existing = "\(Paths.etcPath)/php/\(version)/php-fpm.d/www.conf"
let new = "\(Paths.etcPath)/php/\(version)/php-fpm.d/www.conf.disabled-by-phpmon"
do {
if FileSystem.fileExists(new) {
Log.info("A moved `www.conf.disabled-by-phpmon` file was found for PHP \(version), "
+ "cleaning up so the newer `www.conf` can be moved again.")
try FileSystem.remove(new)
}
try FileSystem.move(from: existing, to: new)
Log.info("Success: A default `www.conf` file was disabled for PHP \(version).")
} catch {
Log.err(error)
func linkAndStartPhpVersion(_ version: String, primary: Bool) async {
let formula = (version == PhpEnvironments.brewPhpAlias) ? "php" : "php@\(version)"
if primary {
Log.info("\(formula) is the primary formula, linking...")
await brew("link \(formula) --overwrite --force")
} else {
Log.info("\(formula) is an isolated PHP version, not linking!")
}
if Valet.installed {
await brew("services start \(formula)", sudo: true)
if Valet.enabled(feature: .isolatedSites) && primary {
let socketVersion = version.replacingOccurrences(of: ".", with: "")
await Shell.quiet("ln -sF ~/.config/valet/valet\(socketVersion).sock ~/.config/valet/valet.sock")
Log.info("Symlinked new socket version (valet\(socketVersion).sock → valet.sock).")
}
}
}
func stopPhpVersion(_ version: String) async {
let formula = (version == PhpEnv.brewPhpAlias) ? "php" : "php@\(version)"
await brew("unlink \(formula)")
await brew("services stop \(formula)", sudo: true)
Log.info("Unlinked and stopped services for \(formula)")
}
func startPhpVersion(_ version: String, primary: Bool) async {
let formula = (version == PhpEnv.brewPhpAlias) ? "php" : "php@\(version)"
if primary {
Log.info("\(formula) is the primary formula, linking and starting services...")
await brew("link \(formula) --overwrite --force")
} else {
Log.info("\(formula) is an isolated PHP version, starting services only...")
}
await brew("services start \(formula)", sudo: true)
if Valet.enabled(feature: .isolatedSites) && primary {
let socketVersion = version.replacingOccurrences(of: ".", with: "")
await Shell.quiet("ln -sF ~/.config/valet/valet\(socketVersion).sock ~/.config/valet/valet.sock")
Log.info("Symlinked new socket version (valet\(socketVersion).sock → valet.sock).")
}
}
}

View File

@ -86,14 +86,37 @@ class RealShell: ShellProtocol {
// MARK: - Shellable Protocol
func sync(_ command: String) -> ShellOutput {
let task = getShellProcess(for: command)
let outputPipe = Pipe()
let errorPipe = Pipe()
if ProcessInfo.processInfo.environment["SLOW_SHELL_MODE"] != nil {
sleep(3)
}
task.standardOutput = outputPipe
task.standardError = errorPipe
task.launch()
task.waitUntilExit()
let stdOut = String(data: outputPipe.fileHandleForReading.readDataToEndOfFile(), encoding: .utf8)!
let stdErr = String(data: errorPipe.fileHandleForReading.readDataToEndOfFile(), encoding: .utf8)!
if Log.shared.verbosity == .cli {
log(task: task, stdOut: stdOut, stdErr: stdErr)
}
return .out(stdOut, stdErr)
}
func pipe(_ command: String) async -> ShellOutput {
let task = getShellProcess(for: command)
let outputPipe = Pipe()
let errorPipe = Pipe()
// Seriously slow down how long it takes for the shell to return output
// (in order to debug or identify async issues)
if ProcessInfo.processInfo.environment["SLOW_SHELL_MODE"] != nil {
Log.info("[SLOW SHELL] \(command)")
await delay(seconds: 3.0)
@ -104,46 +127,41 @@ class RealShell: ShellProtocol {
task.launch()
task.waitUntilExit()
let stdOut = String(
data: outputPipe.fileHandleForReading.readDataToEndOfFile(),
encoding: .utf8
)!
let stdErr = String(
data: errorPipe.fileHandleForReading.readDataToEndOfFile(),
encoding: .utf8
)!
let stdOut = String(data: outputPipe.fileHandleForReading.readDataToEndOfFile(), encoding: .utf8)!
let stdErr = String(data: errorPipe.fileHandleForReading.readDataToEndOfFile(), encoding: .utf8)!
if Log.shared.verbosity == .cli {
var args = task.arguments
let last: String = "\"" + (args?.popLast() ?? "") + "\""
let concat = [self.launchPath] + task.arguments! + [last]
let command = concat.joined(separator: " ")
var log = """
log(task: task, stdOut: stdOut, stdErr: stdErr)
}
return .out(stdOut, stdErr)
}
private func log(task: Process, stdOut: String, stdErr: String) {
var args = task.arguments ?? []
let last = "\"" + (args.popLast() ?? "") + "\""
var log = """
<~~~~~~~~~~~~~~~~~~~~~~~
$ \(command)
$ \(([self.launchPath] + args + [last]).joined(separator: " "))
[OUT]:
\(stdOut)
"""
if !stdErr.isEmpty {
log.append("""
if !stdErr.isEmpty {
log.append("""
[ERR]:
\(stdErr)
""")
}
}
log.append("""
log.append("""
~~~~~~~~~~~~~~~~~~~~~~~~>
""")
Log.info(log)
}
return .out(stdOut, stdErr)
Log.info(log)
}
func quiet(_ command: String) async {

View File

@ -14,6 +14,16 @@ protocol ShellProtocol {
*/
var PATH: String { get }
/**
Run a command synchronously. Use with caution.
Common usage:
```
let output = Shell.sync("php -v")
```
*/
func sync(_ command: String) -> ShellOutput
/**
Run a command asynchronously.
Returns the most relevant output (prefers error output if it exists).

View File

@ -0,0 +1,29 @@
//
// BusyStatus.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 02/05/2023.
// Copyright © 2023 Nico Verbruggen. All rights reserved.
//
import Foundation
class BusyStatus: ObservableObject {
@Published var busy: Bool
@Published var title: String
@Published var description: String
init(busy: Bool, title: String, description: String) {
self.busy = busy
self.title = title
self.description = description
}
public static func notBusy() -> BusyStatus {
return BusyStatus(busy: false, title: "", description: "")
}
public static func busy() -> BusyStatus {
return BusyStatus(busy: false, title: "", description: "")
}
}

View File

@ -19,6 +19,10 @@ class TestableCommand: CommandProtocol {
self.execute(path: path, arguments: arguments, trimNewlines: false)
}
public func execute(path: String, arguments: [String], trimNewlines: Bool, withStandardError: Bool) -> String {
self.execute(path: path, arguments: arguments, trimNewlines: trimNewlines)
}
public func execute(path: String, arguments: [String], trimNewlines: Bool) -> String {
let concatenatedCommand = "\(path) \(arguments.joined(separator: " "))"
assert(commands.keys.contains(concatenatedCommand), "Command `\(concatenatedCommand)` not found")

View File

@ -13,11 +13,113 @@ public struct TestableConfiguration: Codable {
var filesystem: [String: FakeFile]
var shellOutput: [String: BatchFakeShellOutput]
var commandOutput: [String: String]
var preferenceOverrides: [PreferenceName: Bool]
init(
architecture: String,
filesystem: [String: FakeFile],
shellOutput: [String: BatchFakeShellOutput],
commandOutput: [String: String],
preferenceOverrides: [PreferenceName: Bool],
phpVersions: [VersionNumber]
) {
self.architecture = architecture
self.filesystem = filesystem
self.shellOutput = shellOutput
self.commandOutput = commandOutput
self.preferenceOverrides = preferenceOverrides
phpVersions.enumerated().forEach { (index, version) in
self.addPhpVersion(version, primary: index == 0)
}
}
private enum CodingKeys: String, CodingKey {
case architecture, filesystem, shellOutput, commandOutput, preferenceOverrides
}
// MARK: Add PHP versions
private var primaryPhpVersion: VersionNumber?
private var secondaryPhpVersions: [VersionNumber] = []
// swiftlint:disable function_body_length
mutating func addPhpVersion(_ version: VersionNumber, primary: Bool) {
if primary {
if primaryPhpVersion != nil {
fatalError("You cannot add multiple primary PHP versions to a testable configuration!")
}
primaryPhpVersion = version
} else {
self.secondaryPhpVersions.append(version)
}
self.filesystem = self.filesystem.merging([
"/opt/homebrew/opt/php@\(version.short)/bin/php"
: .fake(.symlink, "/opt/homebrew/Cellar/php/\(version.long)/bin/php"),
"/opt/homebrew/opt/php@\(version.short)/bin/php-config"
: .fake(.symlink, "/opt/homebrew/Cellar/php/\(version.long)/bin/php-config"),
"/opt/homebrew/Cellar/php/\(version.long)/bin/php"
: .fake(.binary),
"/opt/homebrew/Cellar/php/\(version.long)/bin/php-config"
: .fake(.binary),
"/opt/homebrew/etc/php/\(version.short)/php-fpm.d/www.conf"
: .fake(.text),
"/opt/homebrew/etc/php/\(version.short)/php-fpm.d/valet-fpm.conf"
: .fake(.text),
"/opt/homebrew/etc/php/\(version.short)/php.ini"
: .fake(.text),
"/opt/homebrew/etc/php/\(version.short)/conf.d/php-memory-limits.ini"
: .fake(.text)
]) { (_, new) in new }
// PHP configuration files
self.shellOutput["/opt/homebrew/opt/php@\(version.short)/bin/php --ini | grep -E -o '(/[^ ]+\\.ini)'"] =
.instant("/opt/homebrew/etc/php/\(version.short)/conf.d/php-memory-limits.ini")
// PHP Homebrew operations
self.shellOutput["/opt/homebrew/bin/brew unlink php@\(version.short)"] = .delayed(0.2, "OK")
self.shellOutput["sudo /opt/homebrew/bin/brew services stop php@\(version.short)"] = .delayed(0.2, "OK")
self.shellOutput["sudo /opt/homebrew/bin/brew services start php@\(version.short)"] = .delayed(0.2, "OK")
self.shellOutput["/opt/homebrew/bin/brew link php@\(version.short) --overwrite --force"] = .delayed(0.2, "OK")
// PHP version output
self.commandOutput["/opt/homebrew/opt/php@\(version.short)/bin/php-config --version"] = version.long
self.commandOutput["/opt/homebrew/opt/php@\(version.short)/bin/php -v"] = "OK"
if primary {
// Files expected to be present for currently linked PHP version
self.shellOutput["ls /opt/homebrew/opt | grep php"] =
.instant("php")
self.shellOutput["/opt/homebrew/bin/php --ini | grep -E -o '(/[^ ]+\\.ini)'"] =
.instant("/opt/homebrew/etc/php/\(version.short)/conf.d/php-memory-limits.ini")
self.filesystem["/opt/homebrew/opt/php"]
= .fake(.symlink, "/opt/homebrew/Cellar/php/\(version.long)")
self.filesystem["/opt/homebrew/opt/php/bin/php"]
= .fake(.symlink, "/opt/homebrew/Cellar/php/\(version.long)/bin/php")
self.filesystem["/opt/homebrew/bin/php"]
= .fake(.symlink, "/opt/homebrew/Cellar/php/\(version.long)/bin/php")
self.filesystem["/opt/homebrew/bin/php-config"]
= .fake(.symlink, "/opt/homebrew/Cellar/php/\(version.short)/bin/php-config")
self.commandOutput["/opt/homebrew/bin/php-config --version"]
= version.long
} else {
// Output expected to be present for non-linked PHP versions
self.shellOutput["ls /opt/homebrew/opt | grep php@"] =
BatchFakeShellOutput.instant(
self.secondaryPhpVersions
.map { "php@\($0.short)" }
.joined(separator: "\n")
)
}
}
// swiftlint:enable function_body_length
// MARK: Interactions
func apply() {
Log.separator()
Log.info("USING TESTABLE CONFIGURATION...")
Homebrew.fake = true
Log.separator()
Log.info("Applying fake shell...")
ActiveShell.useTestable(shellOutput)
@ -25,14 +127,23 @@ public struct TestableConfiguration: Codable {
ActiveFileSystem.useTestable(filesystem)
Log.info("Applying fake commands...")
ActiveCommand.useTestable(commandOutput)
Log.info("Applying fake scanner...")
ValetScanner.useFake()
Log.info("Applying fake services manager...")
ServicesManager.useFake()
Log.info("Applying fake Valet domain interactor...")
ValetInteractor.useFake()
Log.info("Applying temporary preference overrides...")
preferenceOverrides.forEach { (key: PreferenceName, value: Any?) in
Preferences.shared.cachedPreferences[key] = value
}
if Valet.shared.installed {
Log.info("Applying fake scanner...")
ValetScanner.useFake()
Log.info("Applying fake services manager...")
ServicesManager.useFake()
Log.info("Applying fake Valet domain interactor...")
ValetInteractor.useFake()
}
}
// MARK: Persist and load
func toJson(pretty: Bool = false) -> String {
let data = try! JSONEncoder().encode(self)

Some files were not shown because too many files have changed in this diff Show More