1
0
mirror of https://github.com/nicoverbruggen/phpmon.git synced 2025-11-05 04:20:06 +01:00

🚀 Version 25.09

This commit is contained in:
2025-09-30 16:14:39 +02:00
112 changed files with 2395 additions and 2144 deletions

View File

@@ -3,13 +3,25 @@ disabled_rules:
- identifier_name
- force_try
- force_cast
- private_over_fileprivate
opt_in_rules:
- empty_count
included:
- phpmon
- phpmon-tests
- phpmon-updater
- tests
excluded:
- phpmon/Vendor
line_length:
ignores_function_declarations: true
ignores_comments: true
ignores_urls: true
warning: 120
error: 200
analyzer_rules:
- unused_import

File diff suppressed because it is too large Load Diff

View File

@@ -1,146 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1640"
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.Dev"
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.Dev"
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.Dev"
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.Dev">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release.Dev"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View File

@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1640"
LastUpgradeVersion = "2600"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
@@ -31,7 +31,8 @@
<Testables>
<TestableReference
skipped = "NO"
parallelizable = "YES">
parallelizable = "NO"
testExecutionOrdering = "random">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "C4F7807825D7F84B000DBC97"
@@ -41,7 +42,7 @@
</BuildableReference>
</TestableReference>
<TestableReference
skipped = "NO"
skipped = "YES"
parallelizable = "YES">
<BuildableReference
BuildableIdentifier = "primary"
@@ -52,7 +53,8 @@
</BuildableReference>
</TestableReference>
<TestableReference
skipped = "NO">
skipped = "NO"
parallelizable = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "C471E7AC28F9B4940021E251"

View File

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

View File

@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1640"
LastUpgradeVersion = "2600"
version = "1.7">
<BuildAction
parallelizeBuildables = "YES"
@@ -26,13 +26,8 @@
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<TestPlans>
<TestPlanReference
reference = "container:PHP Monitor.xcodeproj/PHP Monitor.xctestplan"
default = "YES">
</TestPlanReference>
</TestPlans>
shouldUseLaunchSchemeArgsEnv = "YES"
shouldAutocreateTestPlan = "YES">
<Testables>
<TestableReference
skipped = "NO">

View File

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

View File

@@ -399,13 +399,15 @@ PHP Monitor is a universal app and supports both architectures, so [find out her
<details>
<summary><strong>Why is the app doing network requests?</strong></summary>
The app will automatically check for updates, which is the most likely culprit.
This happens for various reasons.
This happens at launch (unless disabled), and the app directly checks the Caskfile hosted on GitHub. This data is not, and will not be used for analytics (and, as far as I can tell, cannot).
PHP Monitor will connect to the `api.phpmon.app` domain to check for updates. To provide a good update experience, some information about which version of PHP Monitor and macOS you are using is transmitted to determine which updates are available for your system configuration.
I also can't prevent `brew` from doing things via the network when PHP Monitor uses the binary.
The app includes an Internet Access Policy file, so if you're using something like [Little Snitch](https://www.obdev.at/products/littlesnitch/index.html) there should be a description why these connections occur.
The app includes an Internet Access Policy file, so if you're using something like Little Snitch there should be a description why these calls occur.
Certain connections may not be documented if Homebrew functionality is being invoked via the GUI. I also can't prevent `brew` from doing things via the network when PHP Monitor invokes `brew`, obviously.
For example: Homebrew automatically sends analytics to an `influxdata.com` endpoint (more info [here](https://docs.brew.sh/Analytics)). You can disable this by running `brew analytics off`.
</details>

View File

@@ -6,9 +6,7 @@ Generally speaking, only the latest version of **PHP Monitor** is supported, exc
| Version | Apple Silicon | Supported | Supported macOS | Minimum Deployment | Detected PHP Versions | Recommended Valet Version |
| ------- | ------------- | ------------------ | ----- | ----- | ----- | ----
| 25 | ✅ Universal binary | ✅ Yes | Ventura (13.5+)<br/>Sonoma (14.0+)<br/>Sequoia (15.0+)<br/>Tahoe (26.0+)* | macOS 13.5+ | 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.5 (w/ Valet 4.x)| 3.0 or higher recommended<br/> 2.16.2 minimum |
(*) Denotes preliminary supported based on the app being built with the latest version of the SDK prior to the release of the latest release of macOS. Please check out the pinned issue for more information.
| 25 | ✅ Universal binary | ✅ Yes | Ventura (13.5+)<br/>Sonoma (14.0+)<br/>Sequoia (15.0+)<br/>Tahoe (26.0+) | macOS 13.5+ | 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.5 (w/ Valet 4.x)| 3.0 or higher recommended<br/> 2.16.2 minimum |
## Legacy versions

View File

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 171 175" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<g>
<g id="Updater-Badge" serif:id="Updater Badge" transform="matrix(1,0,0,1,-319.58,-314.735)">
<g>
<g transform="matrix(1,0,0,1,20.6989,12.9824)">
<ellipse cx="383.215" cy="389.494" rx="69.228" ry="71.667" style="fill:rgb(189,237,171);"/>
</g>
<g transform="matrix(0.302874,0,0,0.311711,327.157,322.241)">
<path d="M256,504C393,504 504,393 504,256C504,119 393,8 256,8C119,8 8,119 8,256C8,393 119,504 256,504ZM256,56C366.5,56 456,145.5 456,256C456,366.5 366.5,456 256,456C145.5,456 56,366.5 56,256C56,145.5 145.5,56 256,56ZM276,384L236,384C229.4,384 224,378.6 224,372L224,256L157,256C146.3,256 141,243.1 148.5,235.5L247.5,136.5C252.2,131.8 259.8,131.8 264.5,136.5L363.5,235.5C371.1,243.1 365.7,256 355,256L288,256L288,372C288,378.6 282.6,384 276,384Z" style="fill:url(#_Linear1);fill-rule:nonzero;"/>
</g>
</g>
<g>
<g transform="matrix(1,0,0,1,20.6989,12.9824)">
<ellipse cx="383.215" cy="389.494" rx="69.228" ry="71.667" style="fill:rgb(189,237,171);"/>
</g>
<g transform="matrix(0.302874,0,0,0.311711,327.157,322.241)">
<path d="M256,504C393,504 504,393 504,256C504,119 393,8 256,8C119,8 8,119 8,256C8,393 119,504 256,504ZM256,56C366.5,56 456,145.5 456,256C456,366.5 366.5,456 256,456C145.5,456 56,366.5 56,256C56,145.5 145.5,56 256,56ZM276,384L236,384C229.4,384 224,378.6 224,372L224,256L157,256C146.3,256 141,243.1 148.5,235.5L247.5,136.5C252.2,131.8 259.8,131.8 264.5,136.5L363.5,235.5C371.1,243.1 365.7,256 355,256L288,256L288,372C288,378.6 282.6,384 276,384Z" style="fill:url(#_Linear2);fill-rule:nonzero;"/>
</g>
</g>
</g>
</g>
<defs>
<linearGradient id="_Linear1" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(2.82822e-14,448.789,-461.884,2.74804e-14,256.903,32.5209)"><stop offset="0" style="stop-color:rgb(0,179,48);stop-opacity:1"/><stop offset="0.5" style="stop-color:rgb(0,156,42);stop-opacity:1"/><stop offset="1" style="stop-color:rgb(0,133,37);stop-opacity:1"/></linearGradient>
<linearGradient id="_Linear2" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(2.82822e-14,448.789,-461.884,2.74804e-14,256.903,32.5209)"><stop offset="0" style="stop-color:rgb(0,179,48);stop-opacity:1"/><stop offset="0.5" style="stop-color:rgb(0,156,42);stop-opacity:1"/><stop offset="1" style="stop-color:rgb(0,133,37);stop-opacity:1"/></linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 3.0 KiB

View File

@@ -0,0 +1,84 @@
{
"fill" : {
"linear-gradient" : [
"srgb:0.27800,0.58000,0.98800,1.00000",
"srgb:0.27800,0.58000,0.98800,1.00000"
]
},
"groups" : [
{
"blur-material" : null,
"layers" : [
{
"blend-mode" : "normal",
"fill" : "none",
"glass" : true,
"image-name" : "upgrade.svg",
"name" : "upgrade",
"position" : {
"scale" : 2.3,
"translation-in-points" : [
251.390625,
248.5546875
]
}
}
],
"lighting" : "individual",
"position" : {
"scale" : 1,
"translation-in-points" : [
0.609375,
9.453125
]
},
"shadow" : {
"kind" : "neutral",
"opacity" : 0.5
},
"specular" : true,
"translucency" : {
"enabled" : false,
"value" : 0.5
}
},
{
"blend-mode" : "screen",
"blur-material" : null,
"layers" : [
{
"blend-mode" : "normal",
"fill" : {
"solid" : "srgb:1.00000,0.99038,0.96423,1.00000"
},
"glass" : true,
"image-name" : "phpmon.svg",
"name" : "phpmon",
"position" : {
"scale" : 1.85,
"translation-in-points" : [
3.890625,
2.3828125
]
}
}
],
"lighting" : "individual",
"shadow" : {
"kind" : "neutral",
"opacity" : 0.5
},
"specular" : true,
"translucency" : {
"enabled" : true,
"value" : 0.5
}
}
],
"supported-platforms" : {
"circles" : [
"watchOS"
],
"squares" : "shared"
}
}

View File

@@ -1,68 +0,0 @@
{
"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.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 811 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 131 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 131 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 450 KiB

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 500 500" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<g transform="matrix(1.25033,-0.175723,0.175723,1.25033,14.4412,107.226)">
<path d="M133.3,83.75L120.4,83.75L120.4,54.437C120.4,52.134 118.465,50.25 116.1,50.25L107.5,50.25C105.135,50.25 103.2,52.134 103.2,54.438L103.2,96.312C103.2,98.616 105.135,100.5 107.5,100.5L133.3,100.5C135.665,100.5 137.6,98.616 137.6,96.312L137.6,87.938C137.6,85.634 135.665,83.75 133.3,83.75ZM335.4,184.25L326.8,184.25L326.8,127.666C326.8,121.019 324.059,114.633 319.221,109.922L265.525,57.63C260.688,52.92 254.13,50.25 247.304,50.25L223.6,50.25L223.6,25.125C223.6,11.254 212.044,0 197.8,0L25.8,0C11.556,0 0,11.254 0,25.125L0,192.625C0,206.496 11.556,217.75 25.8,217.75L34.4,217.75C34.4,245.492 57.513,268 86,268C114.487,268 137.6,245.492 137.6,217.75L206.4,217.75C206.4,245.492 229.512,268 258,268C286.488,268 309.6,245.492 309.6,217.75L335.4,217.75C340.13,217.75 344,213.981 344,209.375L344,192.625C344,188.019 340.13,184.25 335.4,184.25ZM86,242.875C71.756,242.875 60.2,231.621 60.2,217.75C60.2,203.879 71.756,192.625 86,192.625C100.244,192.625 111.8,203.879 111.8,217.75C111.8,231.621 100.244,242.875 86,242.875ZM111.8,150.75C78.529,150.75 51.6,124.526 51.6,92.125C51.6,59.725 78.529,33.5 111.8,33.5C145.071,33.5 172,59.724 172,92.125C172,124.525 145.071,150.75 111.8,150.75ZM258,242.875C243.756,242.875 232.2,231.621 232.2,217.75C232.2,203.879 243.756,192.625 258,192.625C272.244,192.625 283.8,203.879 283.8,217.75C283.8,231.621 272.244,242.875 258,242.875ZM301,134L223.6,134L223.6,75.375L247.304,75.375L301,127.666L301,134Z" style="fill:url(#_Linear1);fill-rule:nonzero;"/>
</g>
<defs>
<linearGradient id="_Linear1" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(1.96882e-14,321.533,-321.533,1.96882e-14,172.079,-41.4918)"><stop offset="0" style="stop-color:rgb(81,194,251);stop-opacity:1"/><stop offset="0" style="stop-color:rgb(81,194,251);stop-opacity:1"/><stop offset="1" style="stop-color:rgb(28,145,254);stop-opacity:1"/></linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 343 138" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<g id="EAP-Pill" serif:id="EAP Pill" transform="matrix(0.989959,-5.55112e-17,5.55112e-17,1,-226.512,-352.307)">
<g transform="matrix(1.49702,2.46519e-32,-4.1157e-17,1,-175.953,-20.6096)">
<path d="M485.35,426.302L485.35,456.577C485.35,472.795 476.465,485.962 465.522,485.962L306.401,485.962C295.458,485.962 286.573,472.795 286.573,456.577L286.573,426.302C286.573,410.084 295.458,396.917 306.401,396.917L465.522,396.917C476.465,396.917 485.35,410.084 485.35,426.302Z" style="fill:url(#_Linear1);"/>
</g>
<g transform="matrix(1.49702,2.46519e-32,-4.1157e-17,1,-175.953,-20.6096)">
<path d="M485.35,426.302L485.35,456.577C485.35,472.795 476.465,485.962 465.522,485.962L306.401,485.962C295.458,485.962 286.573,472.795 286.573,456.577L286.573,426.302C286.573,410.084 295.458,396.917 306.401,396.917L465.522,396.917C476.465,396.917 485.35,410.084 485.35,426.302Z" style="fill:url(#_Linear2);"/>
</g>
<g transform="matrix(0.811641,-6.24693e-17,-3.25779e-17,1,143.651,1.2882)">
<g>
<path d="M184.151,434.829L184.151,422.301L202.79,422.301L202.79,412.221L184.151,412.221L184.151,404.445L207.27,404.445L207.27,394.365L169.366,394.365L169.366,444.909L207.987,444.909L207.987,434.829L184.151,434.829Z" style="fill:white;fill-rule:nonzero;"/>
<path d="M184.151,434.829L184.151,422.301L202.79,422.301L202.79,412.221L184.151,412.221L184.151,404.445L207.27,404.445L207.27,394.365L169.366,394.365L169.366,444.909L207.987,444.909L207.987,434.829L184.151,434.829Z" style="fill:white;fill-rule:nonzero;"/>
<path d="M230.389,444.909L233.974,438.789L257.541,438.789L261.125,444.909L277.524,444.909L248.222,393.933L243.383,393.933L213.812,444.909L230.389,444.909ZM245.713,417.117L252.254,428.997L239.171,428.997L245.713,417.117Z" style="fill:white;fill-rule:nonzero;"/>
<path d="M230.389,444.909L233.974,438.789L257.541,438.789L261.125,444.909L277.524,444.909L248.222,393.933L243.383,393.933L213.812,444.909L230.389,444.909ZM245.713,417.117L252.254,428.997L239.171,428.997L245.713,417.117Z" style="fill:white;fill-rule:nonzero;"/>
<path d="M327.346,412.581C327.346,399.333 310.679,390.333 283.348,394.869L283.348,444.909L298.134,444.909L298.134,432.093C314.263,432.021 327.346,424.461 327.346,412.581ZM298.134,404.733C308.17,403.725 312.113,408.621 312.113,412.581C312.113,417.189 308.349,421.941 298.134,421.581L298.134,404.733Z" style="fill:white;fill-rule:nonzero;"/>
</g>
<path d="M327.346,412.581C327.346,399.333 310.679,390.333 283.348,394.869L283.348,444.909L298.134,444.909L298.134,432.093C314.263,432.021 327.346,424.461 327.346,412.581ZM298.134,404.733C308.17,403.725 312.113,408.621 312.113,412.581C312.113,417.189 308.349,421.941 298.134,421.581L298.134,404.733Z" style="fill:white;fill-rule:nonzero;"/>
</g>
</g>
<defs>
<linearGradient id="_Linear1" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(4.99972e-15,-81.6517,36.5774,2.23972e-15,387.828,484.137)"><stop offset="0" style="stop-color:rgb(240,21,150);stop-opacity:1"/><stop offset="1" style="stop-color:rgb(246,86,109);stop-opacity:1"/></linearGradient>
<linearGradient id="_Linear2" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(4.99972e-15,-81.6517,36.5774,2.23972e-15,387.828,484.137)"><stop offset="0" style="stop-color:rgb(240,21,150);stop-opacity:1"/><stop offset="1" style="stop-color:rgb(246,86,109);stop-opacity:1"/></linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 4.0 KiB

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 500 500" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<g transform="matrix(1.25033,-0.175723,0.175723,1.25033,14.4412,107.226)">
<path d="M133.3,83.75L120.4,83.75L120.4,54.437C120.4,52.134 118.465,50.25 116.1,50.25L107.5,50.25C105.135,50.25 103.2,52.134 103.2,54.438L103.2,96.312C103.2,98.616 105.135,100.5 107.5,100.5L133.3,100.5C135.665,100.5 137.6,98.616 137.6,96.312L137.6,87.938C137.6,85.634 135.665,83.75 133.3,83.75ZM335.4,184.25L326.8,184.25L326.8,127.666C326.8,121.019 324.059,114.633 319.221,109.922L265.525,57.63C260.688,52.92 254.13,50.25 247.304,50.25L223.6,50.25L223.6,25.125C223.6,11.254 212.044,0 197.8,0L25.8,0C11.556,0 0,11.254 0,25.125L0,192.625C0,206.496 11.556,217.75 25.8,217.75L34.4,217.75C34.4,245.492 57.513,268 86,268C114.487,268 137.6,245.492 137.6,217.75L206.4,217.75C206.4,245.492 229.512,268 258,268C286.488,268 309.6,245.492 309.6,217.75L335.4,217.75C340.13,217.75 344,213.981 344,209.375L344,192.625C344,188.019 340.13,184.25 335.4,184.25ZM86,242.875C71.756,242.875 60.2,231.621 60.2,217.75C60.2,203.879 71.756,192.625 86,192.625C100.244,192.625 111.8,203.879 111.8,217.75C111.8,231.621 100.244,242.875 86,242.875ZM111.8,150.75C78.529,150.75 51.6,124.526 51.6,92.125C51.6,59.725 78.529,33.5 111.8,33.5C145.071,33.5 172,59.724 172,92.125C172,124.525 145.071,150.75 111.8,150.75ZM258,242.875C243.756,242.875 232.2,231.621 232.2,217.75C232.2,203.879 243.756,192.625 258,192.625C272.244,192.625 283.8,203.879 283.8,217.75C283.8,231.621 272.244,242.875 258,242.875ZM301,134L223.6,134L223.6,75.375L247.304,75.375L301,127.666L301,134Z" style="fill:url(#_Linear1);fill-rule:nonzero;"/>
</g>
<defs>
<linearGradient id="_Linear1" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(1.96882e-14,321.533,-321.533,1.96882e-14,172.079,-41.4918)"><stop offset="0" style="stop-color:rgb(81,194,251);stop-opacity:1"/><stop offset="0" style="stop-color:rgb(81,194,251);stop-opacity:1"/><stop offset="1" style="stop-color:rgb(28,145,254);stop-opacity:1"/></linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

@@ -0,0 +1,80 @@
{
"fill" : {
"linear-gradient" : [
"srgb:0.27800,0.58000,0.98800,1.00000",
"srgb:0.27800,0.58000,0.98800,1.00000"
]
},
"groups" : [
{
"layers" : [
{
"glass" : true,
"image-name" : "eap.svg",
"name" : "eap",
"position" : {
"scale" : 2.5,
"translation-in-points" : [
345.96875,
354.0390625
]
}
}
],
"position" : {
"scale" : 1,
"translation-in-points" : [
1.3203125,
6.5390625
]
},
"shadow" : {
"kind" : "neutral",
"opacity" : 0.5
},
"specular" : true,
"translucency" : {
"enabled" : false,
"value" : 0.5
}
},
{
"blend-mode" : "screen",
"blur-material" : null,
"layers" : [
{
"blend-mode" : "normal",
"fill" : {
"solid" : "srgb:1.00000,0.99038,0.96423,1.00000"
},
"glass" : true,
"image-name" : "phpmon.svg",
"name" : "phpmon",
"position" : {
"scale" : 1.85,
"translation-in-points" : [
21.8515625,
-6.34375
]
}
}
],
"lighting" : "individual",
"shadow" : {
"kind" : "neutral",
"opacity" : 0.5
},
"specular" : true,
"translucency" : {
"enabled" : true,
"value" : 0.5
}
}
],
"supported-platforms" : {
"circles" : [
"watchOS"
],
"squares" : "shared"
}
}

View File

@@ -1,68 +0,0 @@
{
"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.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 783 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 126 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 126 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 457 KiB

View File

@@ -1,68 +0,0 @@
{
"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.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 820 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 136 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 136 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 485 KiB

View File

@@ -26,7 +26,30 @@ struct Constants {
be displayed. This is based on an appropriate launch time on a
basic M1 Apple chip, with some margin for slower Intel chips.
*/
static let SlowBootThresholdInterval: TimeInterval = 30.0
static let SlowBootThresholdInterval: TimeInterval = .seconds(30)
/**
The interval between automatic background update checks.
*/
static let AutomaticUpdateCheckInterval: TimeInterval = .hours(24)
/**
The minimum interval that must pass before allowing another
automatic update check. This prevents excessive checking
on frequent app restarts (due to crashes or bad config).
*/
static let MinimumUpdateCheckInterval: TimeInterval = .minutes(60)
/**
Retry intervals for failed automatic update checks.
Uses exponential backoff before falling back to normal schedule.
*/
static let UpdateCheckRetryIntervals: [TimeInterval] = [
.minutes(5),
.minutes(15),
.hours(1),
.hours(3)
]
/**
PHP Monitor supplies a hardcoded list of PHP packages in its own
@@ -39,8 +62,12 @@ struct Constants {
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.
The cutoff date is always a few days after GA of the latest
release, as it often takes a while for Homebrew to make the
new release available and not everyone uses a separate tap.
*/
static let PhpFormulaeCutoffDate = "2025-11-30" // YYYY-MM-DD
static let PhpFormulaeCutoffDate = "2025-11-20" // YYYY-MM-DD
/**
* The PHP versions that are considered pre-release versions.
@@ -49,7 +76,8 @@ struct Constants {
*/
static var ExperimentalPhpVersions: Set<String> {
let releaseDates = [
"8.5": Date.fromString(Self.PhpFormulaeCutoffDate),
// "8.6": Date.fromString("2026-11-30"), // TBD
"8.5": Date.fromString(PhpFormulaeCutoffDate),
"8.4": Date.fromString("2024-11-22")
]
@@ -85,6 +113,7 @@ struct Constants {
"7.0", "7.1", "7.2", "7.3", "7.4",
"8.0", "8.1", "8.2", "8.3", "8.4",
"8.5" // DEV
// "8.6" // TBD
]
/**
@@ -107,57 +136,28 @@ struct Constants {
"7.1", "7.2", "7.3", "7.4",
"8.0", "8.1", "8.2", "8.3", "8.4",
"8.5" // DEV
// "8.6" // TBD
]
]
struct Urls {
// phpmon.app URLs (these are aliased to redirect correctly)
static let DonationPage = url("https://phpmon.app/sponsor")
static let DonationPage = URL(
string: "https://phpmon.app/sponsor"
)!
static let FrequentlyAskedQuestions = url("https://phpmon.app/faq")
static let FrequentlyAskedQuestions = URL(
string: "https://phpmon.app/faq"
)!
static let WikiPhpUnavailable = url("https://phpmon.app/php-unavailable")
static let WikiPhpUnavailable = URL(
string: "https://phpmon.app/php-unavailable"
)!
static let WikiPhpUpgrade = url("https://phpmon.app/php-upgrade")
static let WikiPhpUpgrade = URL(
string: "https://phpmon.app/php-upgrade"
)!
static let DonationPayment = url("https://phpmon.app/sponsor/now")
static let DonationPayment = URL(
string: "https://phpmon.app/sponsor/now"
)!
static let EarlyAccessChangelog = url("https://phpmon.app/early-access/release-notes")
// API endpoints (via api.phpmon.app)
static let UpdateCheckEndpoint = url("https://api.phpmon.app/api/v1/update-check")
// GitHub URLs (do not alias these)
static let GitHubReleases = URL(
string: "https://github.com/nicoverbruggen/phpmon/releases"
)!
static let StableBuildCaskFile = URL(
string: "https://raw.githubusercontent.com/nicoverbruggen/homebrew-cask/master/Casks/phpmon.rb"
)!
static let DevBuildCaskFile = URL(
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"
)!
static let GitHubReleases = url("https://github.com/nicoverbruggen/phpmon/releases")
}
}

View File

@@ -8,6 +8,8 @@
// MARK: Common Shell Commands
import Foundation
/**
Runs a `brew` command. Can run as superuser.
*/
@@ -49,3 +51,10 @@ func grepContains(file: String, query: String) async -> Bool {
func delay(seconds: Double) async {
try! await Task.sleep(nanoseconds: UInt64(seconds * 1_000_000_000))
}
/**
A simpler way to initialize a fixed, valid URL.
*/
func url(_ string: String) -> URL {
return URL(string: string)!
}

View File

@@ -111,8 +111,7 @@ public class Paths {
}
public static var caskroomPath: String {
return "\(shared.baseDir.rawValue)/Caskroom/"
+ (App.identifier.contains(".dev") ? "phpmon-dev" : "phpmon")
return "\(shared.baseDir.rawValue)/Caskroom/phpmon"
}
public static var shell: String {

View File

@@ -25,8 +25,7 @@ struct Localization {
return Bundle.main
}
let foundBundle = Bundle(identifier: "com.nicoverbruggen.phpmon.dev")
?? Bundle(identifier: "com.nicoverbruggen.phpmon")
let foundBundle = Bundle(identifier: "com.nicoverbruggen.phpmon")
?? Bundle(identifier: "com.nicoverbruggen.phpmon.ui-tests")
if foundBundle == nil {

View File

@@ -9,7 +9,14 @@
import Foundation
extension TimeInterval {
public static func minutes(_ amount: Int) -> TimeInterval {
return Double(amount * 60)
static func seconds(_ value: Double) -> TimeInterval { value }
static func minutes(_ value: Double) -> TimeInterval { value * 60 }
static func hours(_ value: Double) -> TimeInterval { value * 3600 }
static func days(_ value: Double) -> TimeInterval { value * 86400 }
}
extension Date {
func adding(_ interval: TimeInterval) -> Date {
return self.addingTimeInterval(interval)
}
}

View File

@@ -0,0 +1,25 @@
//
// ActiveApi.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 29/09/2025.
// Copyright © 2025 Nico Verbruggen. All rights reserved.
//
import Foundation
var Api: ApiProtocol {
return ActiveApi.shared
}
class ActiveApi {
static var shared: ApiProtocol = RealApi()
public static func useTestable(_ responses: [URL: FakeApiResponse]) {
Self.shared = TestableApi(responses: responses)
}
public static func useReal() {
Self.shared = RealApi()
}
}

View File

@@ -0,0 +1,11 @@
//
// ApiProtocol.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 30/09/2025.
// Copyright © 2025 Nico Verbruggen. All rights reserved.
//
protocol ApiProtocol {
}

View File

@@ -0,0 +1,9 @@
//
// RealApi.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 30/09/2025.
// Copyright © 2025 Nico Verbruggen. All rights reserved.
//
class RealApi: ApiProtocol {}

View File

@@ -111,7 +111,13 @@ class PhpEnvironments {
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" }
if PhpEnvironments.shared.homebrewPackage == nil {
// For UI testing and as a fallback, determine this version by using (fake) php-config
let version = Command.execute(path: "/opt/homebrew/bin/php-config",
arguments: ["--version"],
trimNewlines: true)
return try! VersionNumber.parse(version).short
}
return PhpEnvironments.shared.homebrewPackage.version
}

View File

@@ -0,0 +1,41 @@
//
// TestableApi.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 29/09/2025.
// Copyright © 2025 Nico Verbruggen. All rights reserved.
//
import Foundation
class TestableApi: ApiProtocol {
private var fakeResponses: [URL: FakeApiResponse] = [:]
init(responses: [URL: FakeApiResponse]) {
self.fakeResponses = responses
}
public func hasResponse(for url: URL) -> Bool {
return fakeResponses.keys.contains(url)
}
public func getResponse(for url: URL) -> FakeApiResponse {
return fakeResponses[url]!
}
}
struct FakeApiResponse {
let statusCode: Int
let headers: [String: String]
let data: Data?
init(statusCode: Int, headers: [String: String], text: String) {
self.statusCode = statusCode
self.headers = headers
self.data = text.data(using: .utf8)
}
var text: String {
return String(data: self.data!, encoding: .utf8) ?? ""
}
}

View File

@@ -140,6 +140,12 @@ public struct TestableConfiguration: Codable {
Log.info("Applying fake Valet domain interactor...")
ValetInteractor.useFake()
}
// Clear volatile app state for tests
UserDefaults.standard.removeObject(forKey: PersistentAppState.lastAutomaticUpdateCheck.rawValue)
// Set variable to tell app we're testin'
App.hasLoadedTestableConfiguration = true
}
// MARK: Persist and load

View File

@@ -23,11 +23,11 @@ class TestableFileSystem: FileSystemProtocol {
let adjustedKey = key.contains("~") ? key.replacingOccurrences(of: "~", with: self.homeDirectory) : key
self.files[adjustedKey] = value
}
}
// Ensure that intermediate directories are created
for file in self.files {
self.createIntermediateDirectories(file.key)
// Ensure that intermediate directories are created
for file in self.files {
self.createIntermediateDirectories(file.key)
}
}
}

View File

@@ -14,6 +14,9 @@ class App {
/** The static app instance. Accessible at any time. */
static let shared = App()
/** Use to determine whether a loaded testable configuration is being used. */
static var hasLoadedTestableConfiguration: Bool = false
/** Retrieve the version number from the main info dictionary, Info.plist. */
static var version: String {
let version = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as! String
@@ -53,6 +56,11 @@ class App {
return machine
}
static var macVersion: String {
let version = ProcessInfo.processInfo.operatingSystemVersion
return "\(version.majorVersion).\(version.minorVersion).\(version.patchVersion)"
}
/**
A fake architecture.
When set, the real machine's system architecture is not used,

View File

@@ -10,32 +10,28 @@ import Foundation
import Cocoa
import NVAlert
enum UpdateCheckResult {
case success
case networkError
case parseError
}
class AppUpdater {
var caskFile: CaskFile!
var latestVersionOnline: AppVersion!
var interactive: Bool = false
public func checkForUpdates(userInitiated: Bool) async {
public func checkForUpdates(userInitiated: Bool) async -> UpdateCheckResult {
self.interactive = userInitiated
if !interactive && !Preferences.isEnabled(.automaticBackgroundUpdateCheck) {
Log.info("Skipping automatic update check due to user preference.")
return
}
Log.info("The app will search for updates...")
var caskUrl = Constants.Urls.StableBuildCaskFile
if App.identifier.contains(".phpmon.eap") {
caskUrl = Constants.Urls.EarlyAccessCaskFile
} else if App.identifier.contains(".phpmon.dev") {
caskUrl = Constants.Urls.DevBuildCaskFile
}
let caskUrl = Constants.Urls.UpdateCheckEndpoint
guard let caskFile = await CaskFile.from(url: caskUrl) else {
Log.err("The contents of the CaskFile at '\(caskUrl.absoluteString)' could not be retrieved.")
return presentCouldNotRetrieveUpdateIfInteractive()
presentCouldNotRetrieveUpdateIfInteractive()
return .networkError
}
self.caskFile = caskFile
@@ -44,7 +40,8 @@ class AppUpdater {
guard let onlineVersion = AppVersion.from(caskFile.version) else {
Log.err("The version string from the CaskFile could not be read.")
return presentCouldNotRetrieveUpdateIfInteractive()
presentCouldNotRetrieveUpdateIfInteractive()
return .parseError
}
latestVersionOnline = onlineVersion
@@ -55,6 +52,8 @@ class AppUpdater {
} else if interactive {
presentNoNewerVersionAvailableAlert()
}
return .success
}
private func presentCouldNotRetrieveUpdateIfInteractive() {
@@ -68,9 +67,7 @@ class AppUpdater {
// MARK: - Alerts
public func presentNewerVersionAvailableAlert() {
let command = App.identifier.contains(".dev")
? "brew upgrade phpmon-dev"
: "brew upgrade phpmon"
let command = "brew upgrade phpmon"
Task { @MainActor in
NVAlert().withInformation(
@@ -188,7 +185,7 @@ class AppUpdater {
// Cleanup the upgrade.success file
if FileSystem.fileExists("~/.config/phpmon/updater/upgrade.success") {
Task { @MainActor in
if App.identifier.contains(".phpmon.eap") || App.identifier.contains(".phpmon.dev") {
if App.identifier.contains(".phpmon.eap") {
LocalNotification.send(
title: "notification.phpmon_updated.title".localized,
subtitle: "notification.phpmon_updated_dev.desc".localized(App.shortVersion, App.bundleVersion),

View File

@@ -1,8 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.Cocoa.Storyboard.XIB" version="3.0" toolsVersion="23727" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES">
<document type="com.apple.InterfaceBuilder3.Cocoa.Storyboard.XIB" version="3.0" toolsVersion="24127" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES">
<dependencies>
<deployment identifier="macosx"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="23727"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="24127"/>
<capability name="Image references" minToolsVersion="12.0"/>
<capability name="Named colors" minToolsVersion="9.0"/>
<capability name="Search Toolbar Item" minToolsVersion="12.0" minSystemVersion="11.0"/>
@@ -436,7 +436,7 @@
</toolbarItem>
<searchToolbarItem implicitItemIdentifier="7C834FBE-7118-4082-A09F-7CBECEC1356A" label="Search" paletteLabel="Search" visibilityPriority="1001" id="G2g-jS-RVc">
<nil key="toolTip"/>
<searchField key="view" focusRingType="none" verticalHuggingPriority="750" textCompletion="NO" id="0gE-Yr-MLy">
<searchField key="view" verticalHuggingPriority="750" textCompletion="NO" id="0gE-Yr-MLy">
<rect key="frame" x="0.0" y="0.0" width="100" height="21"/>
<autoresizingMask key="autoresizingMask"/>
<searchFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" selectable="YES" editable="YES" sendsActionOnEndEditing="YES" borderStyle="bezel" usesSingleLineMode="YES" bezelStyle="round" sendsSearchStringImmediately="YES" id="vp9-vH-goQ">
@@ -582,7 +582,7 @@ Gw
<constraint firstAttribute="bottom" secondItem="8zu-cF-KCX" secondAttribute="bottom" constant="20" symbolic="YES" id="wIl-uw-y3p"/>
</constraints>
</visualEffectView>
<textField focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="U1c-qS-cIm">
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="U1c-qS-cIm">
<rect key="frame" x="98" y="153" width="384" height="19"/>
<constraints>
<constraint firstAttribute="width" relation="lessThanOrEqual" constant="380" id="WgB-hj-d4P"/>
@@ -593,7 +593,7 @@ Gw
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<textField focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="yI6-qf-htf">
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="yI6-qf-htf">
<rect key="frame" x="98" y="127" width="384" height="16"/>
<textFieldCell key="cell" selectable="YES" title="This is a slightly more expanded explanation." id="rY3-Nd-Iit">
<font key="font" metaFont="system"/>
@@ -617,7 +617,7 @@ Gw
</constraints>
<imageCell key="cell" refusesFirstResponder="YES" alignment="left" imageScaling="proportionallyDown" id="7eT-Hw-EL9"/>
</imageView>
<textField focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="hml-dl-Cah">
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="hml-dl-Cah">
<rect key="frame" x="98" y="70" width="384" height="42"/>
<textFieldCell key="cell" selectable="YES" id="7iW-Lc-DqO">
<font key="font" metaFont="smallSystem"/>
@@ -706,7 +706,7 @@ Gw
<action selector="pressedCancel:" target="glS-wF-sEU" id="q0L-YZ-F3J"/>
</connections>
</button>
<textField focusRingType="none" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="ZX9-s1-23i">
<textField verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="ZX9-s1-23i">
<rect key="frame" x="20" y="150" width="440" height="21"/>
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" selectable="YES" editable="YES" sendsActionOnEndEditing="YES" borderStyle="bezel" placeholderString="Enter a domain name here." drawsBackground="YES" id="NFa-1D-Bi4">
<font key="font" metaFont="system"/>
@@ -717,7 +717,7 @@ Gw
<outlet property="delegate" destination="glS-wF-sEU" id="Dyf-0M-Gwj"/>
</connections>
</textField>
<textField focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="VzR-5a-cmT">
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="VzR-5a-cmT">
<rect key="frame" x="18" y="128" width="444" height="14"/>
<textFieldCell key="cell" title="[i18n] Preview text here" id="bJr-s6-tdP">
<font key="font" metaFont="smallSystem"/>
@@ -735,7 +735,7 @@ Gw
<action selector="pressedSecure:" target="glS-wF-sEU" id="OIj-Pz-5Ea"/>
</connections>
</button>
<textField focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="mmQ-7e-dlb">
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="mmQ-7e-dlb">
<rect key="frame" x="18" y="60" width="444" height="28"/>
<textFieldCell key="cell" title="[i18n] Securing a domain requires administrative privileges. You may be prompted for your password or Touch ID." id="4gd-KM-5Fu">
<font key="font" metaFont="smallSystem"/>
@@ -750,7 +750,7 @@ Gw
<url key="url" string="file:///Users/"/>
</pathCell>
</pathControl>
<textField focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="P0B-Ht-R8n">
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="P0B-Ht-R8n">
<rect key="frame" x="18" y="209" width="128" height="16"/>
<textFieldCell key="cell" lineBreakMode="clipping" title="[i18n] Link a Folder" id="S4j-ZC-ddT">
<font key="font" textStyle="headline" name=".SFNS-Bold"/>
@@ -758,7 +758,7 @@ Gw
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<textField hidden="YES" focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="900-Z2-tID">
<textField hidden="YES" horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="900-Z2-tID">
<rect key="frame" x="140" y="23" width="180" height="14"/>
<textFieldCell key="cell" lineBreakMode="clipping" title="That domain name already exists." id="jOt-n6-TQf">
<font key="font" metaFont="smallSystem"/>
@@ -893,7 +893,7 @@ Gw
<rect key="frame" x="69" y="0.0" width="200" height="54"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<textField focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" horizontalCompressionResistancePriority="250" translatesAutoresizingMaskIntoConstraints="NO" id="XJL-Uw-frD">
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" horizontalCompressionResistancePriority="250" translatesAutoresizingMaskIntoConstraints="NO" id="XJL-Uw-frD">
<rect key="frame" x="3" y="26" width="145" height="16"/>
<textFieldCell key="cell" lineBreakMode="truncatingTail" sendsActionOnEndEditing="YES" title="my-domain-name.test" id="SGC-Gm-Mxd">
<font key="font" metaFont="systemSemibold" size="13"/>
@@ -901,7 +901,7 @@ Gw
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<textField focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" horizontalCompressionResistancePriority="250" translatesAutoresizingMaskIntoConstraints="NO" id="CXK-Q9-CpO">
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" horizontalCompressionResistancePriority="250" translatesAutoresizingMaskIntoConstraints="NO" id="CXK-Q9-CpO">
<rect key="frame" x="3" y="12" width="75" height="14"/>
<textFieldCell key="cell" lineBreakMode="truncatingTail" sendsActionOnEndEditing="YES" title="~/path/to/site" id="fe7-Ha-mR9">
<font key="font" metaFont="smallSystem"/>
@@ -927,7 +927,7 @@ Gw
<rect key="frame" x="69" y="54" width="200" height="54"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<textField focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" horizontalCompressionResistancePriority="250" translatesAutoresizingMaskIntoConstraints="NO" id="aot-FJ-HIk">
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" horizontalCompressionResistancePriority="250" translatesAutoresizingMaskIntoConstraints="NO" id="aot-FJ-HIk">
<rect key="frame" x="33" y="26" width="145" height="16"/>
<textFieldCell key="cell" lineBreakMode="truncatingTail" sendsActionOnEndEditing="YES" title="my-domain-name.test" id="LHu-UF-QlC">
<font key="font" metaFont="systemSemibold" size="13"/>
@@ -935,7 +935,7 @@ Gw
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<textField focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" horizontalCompressionResistancePriority="250" translatesAutoresizingMaskIntoConstraints="NO" id="GNH-l8-oki">
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" horizontalCompressionResistancePriority="250" translatesAutoresizingMaskIntoConstraints="NO" id="GNH-l8-oki">
<rect key="frame" x="33" y="12" width="75" height="14"/>
<textFieldCell key="cell" lineBreakMode="truncatingTail" sendsActionOnEndEditing="YES" title="~/path/to/site" id="LNw-Ju-0Ot">
<font key="font" metaFont="smallSystem"/>
@@ -1078,7 +1078,7 @@ Gw
<rect key="frame" x="470" y="0.0" width="97" height="54"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<textField focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="Ljl-8B-key">
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="Ljl-8B-key">
<rect key="frame" x="6" y="26" width="93" height="14"/>
<textFieldCell key="cell" alignment="left" title="Laravel" id="0lu-L6-oKr">
<font key="font" metaFont="smallSystem"/>
@@ -1086,7 +1086,7 @@ Gw
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<textField focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="aPK-Xc-J4B">
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="aPK-Xc-J4B">
<rect key="frame" x="6" y="15" width="93" height="11"/>
<textFieldCell key="cell" alignment="left" title="PHP 8.0" id="puf-Jh-ham">
<font key="font" metaFont="miniSystem"/>
@@ -1153,7 +1153,7 @@ Gw
<constraint firstAttribute="height" constant="30" id="lfW-dB-Eu3"/>
</constraints>
</progressIndicator>
<textField wantsLayer="YES" focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="xoy-5Y-WDT">
<textField wantsLayer="YES" horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="xoy-5Y-WDT">
<rect key="frame" x="15" y="14" width="71" height="13"/>
<textFieldCell key="cell" lineBreakMode="clipping" title="PLEASE WAIT" id="tMX-Ky-caT">
<font key="font" metaFont="system" size="10"/>
@@ -1209,7 +1209,7 @@ Gw
<rect key="frame" x="0.0" y="0.0" width="540" height="286"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<textField focusRingType="none" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="QCK-Z9-w7g">
<textField verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="QCK-Z9-w7g">
<rect key="frame" x="20" y="196" width="500" height="21"/>
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" selectable="YES" editable="YES" sendsActionOnEndEditing="YES" borderStyle="bezel" title="http://127.0.0.1:80" placeholderString="http://127.0.0.1:80" drawsBackground="YES" id="muS-8M-KSy">
<font key="font" metaFont="system"/>
@@ -1220,7 +1220,7 @@ Gw
<outlet property="delegate" destination="dwh-CF-6iv" id="lNE-OI-G93"/>
</connections>
</textField>
<textField focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="Uib-vA-HRc">
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="Uib-vA-HRc">
<rect key="frame" x="18" y="221" width="325" height="14"/>
<textFieldCell key="cell" lineBreakMode="clipping" title="[i18n] Proxy subject (usually: protocol, IP address and port)" id="G1Z-3f-BhL">
<font key="font" metaFont="systemMedium" size="11"/>
@@ -1228,7 +1228,7 @@ Gw
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<textField focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="mlA-Zt-Hu8">
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="mlA-Zt-Hu8">
<rect key="frame" x="18" y="172" width="112" height="14"/>
<textFieldCell key="cell" lineBreakMode="clipping" title="[i18n] Domain name" id="dQs-oZ-80e">
<font key="font" metaFont="systemMedium" size="11"/>
@@ -1236,7 +1236,7 @@ Gw
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<textField focusRingType="none" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="SNw-oQ-bnb">
<textField verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="SNw-oQ-bnb">
<rect key="frame" x="20" y="147" width="500" height="21"/>
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" selectable="YES" editable="YES" sendsActionOnEndEditing="YES" borderStyle="bezel" placeholderString="Enter a domain name here." drawsBackground="YES" id="gTQ-Y2-Y9w">
<font key="font" metaFont="system"/>
@@ -1292,7 +1292,7 @@ Gw
<action selector="pressedCancel:" target="dwh-CF-6iv" id="J2T-Zj-A0j"/>
</connections>
</button>
<textField focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="JSZ-x8-Pqi">
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="JSZ-x8-Pqi">
<rect key="frame" x="18" y="128" width="504" height="14"/>
<constraints>
<constraint firstAttribute="width" relation="lessThanOrEqual" constant="500" id="sF1-RG-URI"/>
@@ -1313,7 +1313,7 @@ Gw
<action selector="pressedSecure:" target="dwh-CF-6iv" id="b74-8T-AzO"/>
</connections>
</button>
<textField focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="5x7-ll-2f7">
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="5x7-ll-2f7">
<rect key="frame" x="18" y="60" width="504" height="28"/>
<textFieldCell key="cell" title="[i18n] Securing a domain requires administrative privileges. You may be prompted for your password or Touch ID." id="IMB-O5-ZOy">
<font key="font" metaFont="smallSystem"/>
@@ -1321,7 +1321,7 @@ Gw
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<textField focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="DAh-br-Dfx">
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="DAh-br-Dfx">
<rect key="frame" x="18" y="250" width="123" height="16"/>
<textFieldCell key="cell" lineBreakMode="clipping" title="[i18n] Add a Proxy" id="AZ1-04-kUl">
<font key="font" textStyle="headline" name=".SFNS-Bold"/>
@@ -1329,7 +1329,7 @@ Gw
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<textField hidden="YES" focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="w0k-CK-0u4">
<textField hidden="YES" horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="w0k-CK-0u4">
<rect key="frame" x="191" y="23" width="180" height="14"/>
<textFieldCell key="cell" lineBreakMode="clipping" title="That domain name already exists." id="4sH-94-UJl">
<font key="font" metaFont="smallSystem"/>
@@ -1443,7 +1443,7 @@ Gw
<subviews>
<button wantsLayer="YES" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="L5n-Gw-J27">
<rect key="frame" x="-7" y="-7" width="172" height="32"/>
<buttonCell key="cell" type="push" title="[i18n] Create a Link" bezelStyle="rounded" image="IconLinked" imagePosition="left" alignment="center" borderStyle="border" imageScaling="proportionallyUpOrDown" inset="2" id="8UP-Sw-TP6">
<buttonCell key="cell" type="push" title="[i18n] Create a Link" bezelStyle="rounded" image="IconLinked" imagePosition="leading" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="8UP-Sw-TP6">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="system"/>
<string key="keyEquivalent">l</string>
@@ -1454,7 +1454,7 @@ Gw
</button>
<button wantsLayer="YES" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="01Z-IV-hv1">
<rect key="frame" x="159" y="-7" width="181" height="32"/>
<buttonCell key="cell" type="push" title="[i18n] Create a Proxy" bezelStyle="rounded" image="IconProxy" imagePosition="left" alignment="center" borderStyle="border" imageScaling="proportionallyUpOrDown" inset="2" id="bJ4-q8-1Ej">
<buttonCell key="cell" type="push" title="[i18n] Create a Proxy" bezelStyle="rounded" image="IconProxy" imagePosition="leading" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="bJ4-q8-1Ej">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="system"/>
<string key="keyEquivalent">p</string>
@@ -1473,7 +1473,7 @@ Gw
<real value="3.4028234663852886e+38"/>
</customSpacing>
</stackView>
<textField focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="fJK-Ke-IK3">
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="fJK-Ke-IK3">
<rect key="frame" x="18" y="138" width="504" height="19"/>
<textFieldCell key="cell" selectable="YES" alignment="left" title="[i18n] What kind of domain would you like to set up?" id="agk-Nj-FLd">
<font key="font" metaFont="systemBold" size="15"/>
@@ -1481,7 +1481,7 @@ Gw
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<textField wantsLayer="YES" focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="urj-Xq-TrJ">
<textField wantsLayer="YES" horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="urj-Xq-TrJ">
<rect key="frame" x="18" y="60" width="504" height="70"/>
<constraints>
<constraint firstAttribute="width" relation="lessThanOrEqual" constant="500" id="tbl-AV-4qB"/>

View File

@@ -0,0 +1,126 @@
//
// UpdateScheduler.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 26/09/2025.
// Copyright © 2025 Nico Verbruggen. All rights reserved.
//
import Foundation
actor UpdateScheduler {
static let shared = UpdateScheduler()
private var currentTimer: Timer?
private init() {}
/**
Start the automatic update checking process.
This should be called once during app startup.
*/
func startAutomaticUpdateChecking() async {
await performUpdateCheck()
}
/**
Perform an automatic update check and schedule the next one.
*/
private func performUpdateCheck() async {
guard Preferences.isEnabled(.automaticBackgroundUpdateCheck) else {
Log.info("Automatic update checks disabled. Skipping check but maintaining schedule.")
scheduleTimer()
return
}
guard isNotThrottled() else {
Log.info("Last check was too recent. Skipping check but maintaining schedule.")
scheduleTimer()
return
}
let result = await AppUpdater().checkForUpdates(userInitiated: false)
switch result {
case .success:
// Reset failure count and record successful check
UserDefaults.standard.removeObject(forKey: PersistentAppState.updateCheckFailureCount.rawValue)
UserDefaults.standard.set(Date(), forKey: PersistentAppState.lastAutomaticUpdateCheck.rawValue)
scheduleTimer()
case .networkError, .parseError:
// Handle failures with exponential backoff
handleFailure(result: result)
}
}
/**
Handle update check failures with exponential backoff retry logic.
*/
private func handleFailure(result: UpdateCheckResult) {
let currentFailureCount = UserDefaults.standard.integer(
forKey: PersistentAppState.updateCheckFailureCount.rawValue
)
let newFailureCount = currentFailureCount + 1
UserDefaults.standard.set(newFailureCount, forKey: PersistentAppState.updateCheckFailureCount.rawValue)
let retryInterval: TimeInterval
if newFailureCount <= Constants.UpdateCheckRetryIntervals.count {
// Use exponential backoff
retryInterval = Constants.UpdateCheckRetryIntervals[newFailureCount - 1]
Log.info("Update check failed (\(result)). Retry \(newFailureCount) in \(retryInterval)s.")
} else {
// Exceeded max retries, fall back to normal schedule and reset counter
retryInterval = Constants.AutomaticUpdateCheckInterval
UserDefaults.standard.removeObject(forKey: PersistentAppState.updateCheckFailureCount.rawValue)
Log.info("Update check failed (\(result)). Max retries exceeded. Normal schedule in \(retryInterval)s.")
}
scheduleTimer(after: retryInterval)
}
/**
Determine whether another automatic update check should occur based on the last check timestamp.
Returns true if a check should happen, false otherwise.
*/
private func isNotThrottled() -> Bool {
let minimumTimeAgo = Date().addingTimeInterval(-Constants.MinimumUpdateCheckInterval)
let lastCheckTime = UserDefaults.standard.object(
forKey: PersistentAppState.lastAutomaticUpdateCheck.rawValue
) as? Date
// If no previous check or last check was > minimum time frame, should check now
return lastCheckTime == nil || lastCheckTime! < minimumTimeAgo
}
/**
Schedule a timer to perform an update check after the specified interval.
*/
private func scheduleTimer(after interval: TimeInterval = Constants.AutomaticUpdateCheckInterval) {
// Invalidate any existing timer
currentTimer?.invalidate()
// Ensure timer is scheduled on main run loop since actors run on background threads
Task { @MainActor in
let timer = Timer.scheduledTimer(withTimeInterval: interval, repeats: false) { _ in
Task {
Log.info("Performing scheduled update check after \(interval)s.")
await self.performUpdateCheck()
}
}
// Store timer reference back in actor
await self.setCurrentTimer(timer)
}
Log.info("Next update check scheduled in \(interval)s.")
}
/**
Set the current timer reference. Used to store timer from main thread back to actor.
*/
private func setCurrentTimer(_ timer: Timer) {
currentTimer = timer
}
}

View File

@@ -0,0 +1,27 @@
//
// LoggableEvent.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 12/09/2025.
// Copyright © 2025 Nico Verbruggen. All rights reserved.
//
// TODO: Add anonymous analytics system
// Batch events and dispatch them every hour.
// Reset the counts when send successfully.
// That's the plan. Currently not implemented!
// Also, there should be an opt-out.
enum LoggableEvent: String {
case menuOpened = "menu_opened"
case phpVersionSwitched = "php_version_switched"
case openedDomainManagement = "opened_domain_management"
case openedPhpInstallations = "opened_php_installations"
case openedPhpExtensions = "opened_php_extensions"
case openedSettings = "opened_settings"
// TODO: Add more tracked things.
}

View File

@@ -24,13 +24,28 @@ struct CaskFile {
return self.properties["version"]!
}
private static func loadFromApi(_ url: URL) async -> String {
if App.hasLoadedTestableConfiguration || url.absoluteString.contains("https://raw.githubusercontent.com") {
return await Shell.pipe("curl -s --max-time 10 '\(url.absoluteString)'").out
} else {
return await Shell.pipe("""
curl -s --max-time 10 \
-H "User-Agent: phpmon-curl/1.0" \
-H "X-phpmon-version: \(App.shortVersion) (\(App.bundleVersion))" \
-H "X-phpmon-os-version: \(App.macVersion)" \
-H "X-phpmon-bundle-id: \(App.identifier)" \
'\(url.absoluteString)'
""").out
}
}
public static func from(url: URL) async -> CaskFile? {
var string: String?
if url.scheme == "file" {
string = try? String(contentsOf: url)
} else {
string = await Shell.pipe("curl -s --max-time 10 '\(url.absoluteString)'").out
string = await CaskFile.loadFromApi(url)
}
guard let string else {

View File

@@ -142,7 +142,7 @@ extension MainMenu {
}
} else {
// Check for updates
await AppUpdater().checkForUpdates(userInitiated: false)
await UpdateScheduler.shared.startAutomaticUpdateChecking()
// Check if the linked version has changed between launches of phpmon
await PhpGuard().compareToLastGlobalVersion()

View File

@@ -10,9 +10,6 @@
These are the keys used for every preference in the app.
*/
enum PreferenceName: String, Codable {
// FIRST-TIME LAUNCH
case wasLaunchedBefore = "launched_before"
// GENERAL
case autoServiceRestartAfterExtensionToggle = "auto_restart_after_extension_toggle"
case autoComposerGlobalUpdateAfterSwitch = "auto_composer_global_update_after_switch"
@@ -104,6 +101,18 @@ enum RetiredPreferenceName: String {
case shouldDisplayPhpHintInIcon = "add_php_to_icon"
}
/**
Persistent internal application state keys for UserDefaults.
These track internal app state and behavior that persists across launches,
but are not user preferences or statistics.
*/
enum PersistentAppState: String {
case wasLaunchedBefore = "launched_before"
case lastAutomaticUpdateCheck = "last_automatic_update_check"
case userFavorites = "user_favorites"
case updateCheckFailureCount = "update_check_failure_count"
}
/**
These are internal stats. They NEVER get shared.
*/

View File

@@ -83,6 +83,10 @@ class Preferences {
PreferenceName.displayPresets.rawValue: true,
PreferenceName.displayMisc.rawValue: true,
/// Persistent App State
PersistentAppState.lastAutomaticUpdateCheck.rawValue: 0,
PersistentAppState.updateCheckFailureCount.rawValue: 0,
/// Stats
InternalStats.switchCount.rawValue: 0,
InternalStats.launchCount.rawValue: 0,
@@ -90,13 +94,13 @@ class Preferences {
InternalStats.lastGlobalPhpVersion.rawValue: ""
])
if UserDefaults.standard.bool(forKey: PreferenceName.wasLaunchedBefore.rawValue) {
if UserDefaults.standard.bool(forKey: PersistentAppState.wasLaunchedBefore.rawValue) {
handleMigration()
return
}
Log.info("Saving first-time preferences!")
UserDefaults.standard.setValue(true, forKey: PreferenceName.wasLaunchedBefore.rawValue)
UserDefaults.standard.setValue(true, forKey: PersistentAppState.wasLaunchedBefore.rawValue)
UserDefaults.standard.synchronize()
}

View File

@@ -50,7 +50,6 @@ class AppearancePreferencesVC: GenericPreferenceVC {
class MenuStructurePreferencesVC: GenericPreferenceVC {
// swiftlint:disable line_length
public static func fromStoryboard() -> GenericPreferenceVC {
let vc = NSStoryboard(name: "Main", bundle: nil)
.instantiateController(withIdentifier: "preferencesTemplateVC") as! GenericPreferenceVC
@@ -67,7 +66,6 @@ class MenuStructurePreferencesVC: GenericPreferenceVC {
.addView(when: true, vc.displayFeature("prefs.display_misc", .displayMisc))
.addView(when: true, vc.displayFeature("prefs.display_driver", .displayDriver))
}
// swiftlint:enable line_length
}
class NotificationPreferencesVC: GenericPreferenceVC {

View File

@@ -110,7 +110,7 @@ class Stats {
return Log.info("A fake shell is in use, skipping sponsor alert.")
}
if App.identifier.contains(".dev") || App.identifier.contains(".eap") {
if App.identifier.contains(".eap") {
return Log.info("Sponsor messages never apply to beta builds.")
}

View File

@@ -172,7 +172,6 @@ struct Preset: Codable, Equatable {
// MARK: - Menu Items
// swiftlint:disable void_function_in_ternary
public func getMenuItemText() -> String {
var info = extensions.count == 1
? "preset.extension".localized(extensions.count)
@@ -197,7 +196,6 @@ struct Preset: Codable, Equatable {
+ info + "</i>"
+ "</span>"
}
// swiftlint:enable void_function_in_ternary
// MARK: - Reverting

View File

@@ -10,6 +10,22 @@
<string>https://github.com/nicoverbruggen/phpmon</string>
<key>Connections</key>
<array>
<dict>
<key>IsIncoming</key>
<false/>
<key>Host</key>
<string>phpmon.app, api.phpmon.app</string>
<key>NetworkProtocol</key>
<string>TCP</string>
<key>Port</key>
<string>80, 443</string>
<key>Relevance</key>
<string>Essential</string>
<key>Purpose</key>
<string>PHP Monitor contacts the official phpmon domain to check for updates, and visit aliased links that point to documentation.</string>
<key>DenyConsequences</key>
<string>If you deny these connections, PHP Monitor will not be able to determine if a newer version is available, and certain documentation links in the app may not function as desired.</string>
</dict>
<dict>
<key>IsIncoming</key>
<false/>

View File

@@ -14,7 +14,7 @@ class Favorites {
var items: [String]
init() {
if let items = UserDefaults.standard.array(forKey: "user_favorites") as? [String] {
if let items = UserDefaults.standard.array(forKey: PersistentAppState.userFavorites.rawValue) as? [String] {
self.items = items
} else {
self.items = []
@@ -32,7 +32,7 @@ class Favorites {
items.append(domain)
}
UserDefaults.standard.setValue(items, forKey: "user_favorites")
UserDefaults.standard.setValue(items, forKey: PersistentAppState.userFavorites.rawValue)
UserDefaults.standard.synchronize()
}
}

View File

@@ -439,7 +439,7 @@ This has no effect on other terminals, only for the particular terminal session
"prefs.open_protocol_desc" = "When checked, this will allow the interaction with third party utilities to work (e.g. Alfred, Raycast). If you disable this, PHP Monitor will still receive the commands, but will not act upon them.";
"prefs.automatic_update_check_title" = "Automatically check for updates";
"prefs.automatic_update_check_desc" = "When checked, PHP Monitor will automatically check if there is a newer version available, and notify you if that is the case.";
"prefs.automatic_update_check_desc" = "When checked, PHP Monitor will automatically check daily if there is a newer version available, and notify you if that is the case.";
"prefs.php_doctor_suggestions_title" = "Always show suggestions";
"prefs.php_doctor_suggestions_desc" = "If you uncheck this item, no PHP Doctor suggestions will appear in PHP Monitor's menu. Keep in mind that PHP Doctor will not appear if there are no recommendations.";

View File

@@ -1,10 +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>
<key>com.apple.security.app-sandbox</key>
<false/>
<key>com.apple.security.files.user-selected.read-only</key>
<false/>
</dict>
<dict/>
</plist>

View File

@@ -1,67 +0,0 @@
{
"configurations" : [
{
"id" : "98F42C11-E6D2-4AD9-A5CA-40EFE44F384A",
"name" : "Configuration 1",
"options" : {
}
}
],
"defaultOptions" : {
"codeCoverage" : false,
"commandLineArgumentEntries" : [
{
"argument" : "--v",
"enabled" : false
}
],
"environmentVariableEntries" : [
{
"enabled" : false,
"key" : "EXTREME_DOCTOR_MODE",
"value" : ""
},
{
"enabled" : false,
"key" : "SLOW_SHELL_MODE",
"value" : ""
},
{
"enabled" : false,
"key" : "PAINT_PHPMON_SWIFTUI_VIEWS",
"value" : ""
}
],
"targetForVariableExpansion" : {
"containerPath" : "container:PHP Monitor.xcodeproj",
"identifier" : "C41C1B3222B0097F00E7CF16",
"name" : "PHP Monitor"
}
},
"testTargets" : [
{
"parallelizable" : true,
"target" : {
"containerPath" : "container:PHP Monitor.xcodeproj",
"identifier" : "C4F7807825D7F84B000DBC97",
"name" : "Unit Tests"
}
},
{
"target" : {
"containerPath" : "container:PHP Monitor.xcodeproj",
"identifier" : "C471E7AC28F9B4940021E251",
"name" : "Feature Tests"
}
},
{
"target" : {
"containerPath" : "container:PHP Monitor.xcodeproj",
"identifier" : "C471E7BB28F9B90F0021E251",
"name" : "UI Tests"
}
}
],
"version" : 1
}

View File

@@ -8,6 +8,7 @@
import Foundation
// swiftlint:disable colon
class TestableConfigurations {
/** A functional, working system setup that is compatible with PHP Monitor. */
static var working: TestableConfiguration {
@@ -52,17 +53,16 @@ class TestableConfigurations {
shellOutput: [
"/opt/homebrew/bin/brew --version"
: .instant("""
Homebrew 4.0.17-93-gb0dc84b
Homebrew/homebrew-core (git revision 4113c35d80d; last commit 2023-04-06)
Homebrew/homebrew-cask (git revision bcd8ecb74c; last commit 2023-04-06)
Homebrew 4.6.11
"""),
"/opt/homebrew/bin/php -v"
: .instant("""
PHP 8.2.6 (cli) (built: May 11 2023 12:51:38) (NTS)
PHP 8.4.5 (cli) (built: Aug 26 2025 13:36:28) (NTS)
Copyright (c) The PHP Group
Zend Engine v4.2.6, Copyright (c) Zend Technologies
with Zend OPcache v8.2.6, Copyright (c), by Zend Technologies
with Xdebug v3.2.0, Copyright (c) 2002-2022, by Derick Rethans
Built by Homebrew
Zend Engine v4.4.12, Copyright (c) Zend Technologies
with Xdebug v3.4.5, Copyright (c) 2002-2025, by Derick Rethans
with Zend OPcache v8.4.12, Copyright (c), by Zend Technologies
"""),
"sysctl -n sysctl.proc_translated"
: .instant("0"),
@@ -105,7 +105,7 @@ class TestableConfigurations {
%admin ALL=(root) NOPASSWD:SETENV: VALET
"""),
"valet --version"
: .instant("Laravel Valet 3.1.11"),
: .instant("Laravel Valet 4.9.0"),
"/opt/homebrew/bin/brew tap"
: .instant("""
homebrew/cask
@@ -138,12 +138,27 @@ class TestableConfigurations {
: .instant(ShellStrings.shared.brewServicesAsRoot),
"/opt/homebrew/bin/brew services info --all --json"
: .instant(ShellStrings.shared.brewServicesAsUser),
"curl -s --max-time 10 '\(Constants.Urls.DevBuildCaskFile.absoluteString)'"
"curl -s --max-time 10 '\(Constants.Urls.UpdateCheckEndpoint.absoluteString)'"
: .delayed(0.5, """
cask 'phpmon-dev' do
depends_on formula: 'gnu-sed'
version '6.0.0_1000'
version '25.08.0_1000'
sha256 '1cb147bd1b1fbd52971d90dff577465b644aee7c878f15ede57f46e8f217067a'
url 'https://github.com/nicoverbruggen/phpmon/releases/download/v6.0/phpmon-dev.zip'
name 'PHP Monitor DEV'
homepage 'https://phpmon.app'
app 'PHP Monitor DEV.app', target: "PHP Monitor DEV.app"
end
"""),
"curl -s --max-time 10 'https://raw.githubusercontent.com/nicoverbruggen/homebrew-cask/master/Casks/phpmon.rb''" :
.delayed(0.5, """
cask 'phpmon-dev' do
depends_on formula: 'gnu-sed'
version '25.08.0_1000'
sha256 '1cb147bd1b1fbd52971d90dff577465b644aee7c878f15ede57f46e8f217067a'
url 'https://github.com/nicoverbruggen/phpmon/releases/download/v6.0/phpmon-dev.zip'
@@ -171,17 +186,19 @@ class TestableConfigurations {
: .delayed(0.2, "OK"),
"sudo /opt/homebrew/bin/brew services start dnsmasq"
: .delayed(0.2, "OK"),
"ln -sF ~/.config/valet/valet82.sock ~/.config/valet/valet.sock"
"ln -sF ~/.config/valet/valet84.sock ~/.config/valet/valet.sock"
: .instant("OK"),
"/opt/homebrew/bin/brew update >/dev/null && /opt/homebrew/bin/brew outdated --json --formulae": .delayed(2.0, """
"/opt/homebrew/bin/brew update >/dev/null && /opt/homebrew/bin/brew outdated --json --formulae"
: .delayed(2.0,
"""
{
"formulae": [
{
"name": "php",
"installed_versions": [
"8.2.6"
"8.4.5"
],
"current_version": "8.2.11",
"current_version": "8.4.11",
"pinned": false,
"pinned_version": null
}
@@ -193,12 +210,14 @@ class TestableConfigurations {
commandOutput: [
"/opt/homebrew/bin/php -r echo ini_get('memory_limit');": "512M",
"/opt/homebrew/bin/php -r echo ini_get('upload_max_filesize');": "512M",
"/opt/homebrew/bin/php -r echo ini_get('post_max_size');": "512M",
"/opt/homebrew/bin/php -r echo ini_get('post_max_size');": "512M"
],
preferenceOverrides: [
.automaticBackgroundUpdateCheck: false
],
phpVersions: [
VersionNumber(major: 8, minor: 4, patch: 5),
VersionNumber(major: 8, minor: 3, patch: 5),
VersionNumber(major: 8, minor: 2, patch: 6),
VersionNumber(major: 8, minor: 1, patch: 0),
VersionNumber(major: 8, minor: 0, patch: 0),
@@ -215,6 +234,7 @@ class TestableConfigurations {
return configuration
}
}
// swiftlint:enable colon
class ShellStrings {
static var shared = ShellStrings()

View File

@@ -17,7 +17,7 @@ class FeatureTestCase: XCTestCase {
return fs as! TestableFileSystem
}
fatalError("The active filesystem is not a TestableFileSystem. Please use `ActiveFileSystem` to use the fake filesystem.")
fatalError("The active filesystem is not a TestableFileSystem. Please use `ActiveFileSystem`.")
}
public func assertFileSystemHas(
@@ -44,6 +44,4 @@ class FeatureTestCase: XCTestCase {
) {
XCTAssertEqual(contents, fakeFileSystem.files[path]?.content, file: file, line: line)
}
}

View File

@@ -35,7 +35,7 @@ final class DomainsListTest: UITestCase {
searchField.click()
searchField.typeText("non-existent thing")
Thread.sleep(forTimeInterval: 0.2)
XCTAssertTrue(window.tables.tableRows.count == 0)
XCTAssertTrue(window.tables.tableRows.count == 0) // swiftlint:disable:this empty_count
searchField.clearText()
searchField.click()

View File

@@ -18,11 +18,12 @@ final class MainMenuTest: UITestCase {
let app = launch(openMenu: true)
assertAllExist([
// "Switch to PHP 8.2 (php)" should be visible since it is aliased to `php`
app.menuItems["\("mi_php_switch".localized) 8.2 (php)"],
// "Switch to PHP 8.4 (php)" should be visible since it is aliased to `php`
app.menuItems["\("mi_php_switch".localized) 8.4 (php)"],
// "Switch to PHP 8.1" should be the non-disabled option
app.menuItems["\("mi_php_switch".localized) 8.3 (php@8.3)"],
app.menuItems["\("mi_php_switch".localized) 8.2 (php@8.2)"],
app.menuItems["\("mi_php_switch".localized) 8.1 (php@8.1)"],
// "Switch to PHP 8.0" should be the non-disabled option
app.menuItems["\("mi_php_switch".localized) 8.0 (php@8.0)"],
// We should see the about and quit items
app.menuItems["mi_about".localized],
@@ -107,10 +108,10 @@ final class MainMenuTest: UITestCase {
// But not PHP 8.6 (yet)
assertNotExists(app.staticTexts["PHP 8.6"])
// Also, PHP 8.2 should have an update available
// Also, PHP 8.4 should have an update available
assertExists(app.staticTexts["phpman.version.has_update".localized(
"8.2.6",
"8.2.11"
"8.4.5",
"8.4.11"
)], 5)
}

View File

@@ -26,7 +26,7 @@ final class StartupTest: UITestCase {
assertAllExist([
app.dialogs["generic.notice".localized],
app.staticTexts["startup.errors.php_binary.title".localized],
app.buttons["generic.ok".localized],
app.buttons["generic.ok".localized]
])
click(app.buttons["generic.ok".localized])
@@ -48,7 +48,7 @@ final class StartupTest: UITestCase {
final func test_get_warning_about_missing_fpm_symlink() throws {
var configuration = TestableConfigurations.working
configuration.filesystem["/opt/homebrew/etc/php/8.2/php-fpm.d/valet-fpm.conf"] = nil
configuration.filesystem["/opt/homebrew/etc/php/8.4/php-fpm.d/valet-fpm.conf"] = nil
let app = launch(with: configuration)

View File

@@ -32,7 +32,7 @@ final class UpdateCheckTest: UITestCase {
// Ensure an update is available
configuration.shellOutput[
"curl -s --max-time 10 '\(Constants.Urls.DevBuildCaskFile.absoluteString)'"
"curl -s --max-time 10 '\(Constants.Urls.UpdateCheckEndpoint.absoluteString)'"
] = .delayed(0.5, """
cask 'phpmon-dev' do
depends_on formula: 'gnu-sed'
@@ -50,22 +50,21 @@ final class UpdateCheckTest: UITestCase {
let app = launch(openMenu: false, with: configuration)
// Expect to see the content of the appropriate alert box
assertExists(app.staticTexts["updater.alerts.newer_version_available.title".localized("99.0.0 (9999)")], 3.0)
// Expect to see the content of the appropriate alert box, but this may take a while; if this test fails try increasing the timeout
let timeout: TimeInterval = 10.0
assertExists(app.staticTexts["updater.alerts.newer_version_available.title".localized("99.0.0 (9999)")], timeout)
assertExists(app.buttons["updater.alerts.buttons.install".localized])
assertExists(app.buttons["updater.alerts.buttons.dismiss".localized])
}
final func test_will_require_manual_search_for_update() throws {
final func test_does_not_do_automatic_background_check() throws {
var configuration = TestableConfigurations.working
// Ensure automatic check is disabled
configuration.preferenceOverrides[.automaticBackgroundUpdateCheck] = false
// Ensure an update is available
configuration.shellOutput[
"curl -s --max-time 10 '\(Constants.Urls.DevBuildCaskFile.absoluteString)'"
] = .delayed(0.5, """
configuration.shellOutput["curl -s --max-time 10 '\(Constants.Urls.UpdateCheckEndpoint.absoluteString)'"] = .delayed(0.5, """
cask 'phpmon-dev' do
depends_on formula: 'gnu-sed'
@@ -85,9 +84,32 @@ final class UpdateCheckTest: UITestCase {
// The check should not happen if the preference is disabled
assertNotExists(app.staticTexts["updater.alerts.newer_version_available.title".localized("99.0.0 (9999)")], 2)
}
// Open the menu and check manually
app.statusItems.firstMatch.click()
final func test_will_require_manual_search_for_update() throws {
var configuration = TestableConfigurations.working
// Ensure automatic check is disabled
configuration.preferenceOverrides[.automaticBackgroundUpdateCheck] = false
// Ensure an update is available
configuration.shellOutput["curl -s --max-time 10 '\(Constants.Urls.UpdateCheckEndpoint.absoluteString)'"] = .delayed(0.5, """
cask 'phpmon-dev' do
depends_on formula: 'gnu-sed'
version '99.0.0_9999'
sha256 '1cb147bd1b1fbd52971d90dff577465b644aee7c878f15ede57f46e8f217067a'
url 'https://github.com/nicoverbruggen/phpmon/releases/download/v99.0/phpmon-dev.zip'
name 'PHP Monitor DEV'
homepage 'https://phpmon.app'
app 'PHP Monitor DEV.app', target: "PHP Monitor DEV.app"
end
""")
// Wait for the menu to open and search for updates
let app = launch(openMenu: true, with: configuration)
app.menuItems["mi_check_for_updates".localized].click()
// Expect to see the content of the appropriate alert box
@@ -103,9 +125,7 @@ final class UpdateCheckTest: UITestCase {
configuration.preferenceOverrides[.automaticBackgroundUpdateCheck] = false
// Ensure an update is available
configuration.shellOutput[
"curl -s --max-time 10 '\(Constants.Urls.DevBuildCaskFile.absoluteString)'"
] = .delayed(0.5, "404 PAGE NOT FOUND")
configuration.shellOutput["curl -s --max-time 10 '\(Constants.Urls.UpdateCheckEndpoint.absoluteString)'"] = .delayed(0.5, "404 PAGE NOT FOUND")
// Wait for the menu to open and search for updates
let app = launch(openMenu: true, with: configuration)

View File

@@ -1,51 +0,0 @@
//
// CaskFileParserTest.swift
// Unit Tests
//
// Created by Nico Verbruggen on 04/02/2023.
// Copyright © 2023 Nico Verbruggen. All rights reserved.
//
import XCTest
class CaskFileParserTest: XCTestCase {
// MARK: - Test Files
static var exampleFilePath: URL {
return Bundle(for: Self.self)
.url(forResource: "phpmon-dev", withExtension: "rb")!
}
func test_can_extract_fields_from_cask_file() async throws {
guard let caskFile = await CaskFile.from(url: CaskFileParserTest.exampleFilePath) else {
return XCTFail("The CaskFile could not be parsed, check the log for more info")
}
XCTAssertEqual(
caskFile.version,
"5.7.2_1035"
)
XCTAssertEqual(
caskFile.sha256,
"1cb147bd1b1fbd52971d90dff577465b644aee7c878f15ede57f46e8f217067a"
)
XCTAssertEqual(
caskFile.name,
"PHP Monitor DEV"
)
XCTAssertEqual(
caskFile.url,
"https://github.com/nicoverbruggen/phpmon/releases/download/v5.7.2/phpmon-dev.zip"
)
}
func test_can_extract_fields_from_remote_cask_file() async throws {
guard let caskFile = await CaskFile.from(url: Constants.Urls.StableBuildCaskFile) else {
return XCTFail("The remote CaskFile could not be parsed, check the log for more info")
}
XCTAssertTrue(caskFile.properties.keys.contains("version"))
XCTAssertTrue(caskFile.properties.keys.contains("homepage"))
XCTAssertTrue(caskFile.properties.keys.contains("url"))
}
}

View File

@@ -1,42 +0,0 @@
//
// BytePhpPreferenceTest.swift
// Unit Tests
//
// Created by Nico Verbruggen on 04/09/2023.
// Copyright © 2023 Nico Verbruggen. All rights reserved.
//
import XCTest
class BytePhpPreferenceTest: XCTestCase {
func test_can_extract_memory_value() throws {
let pref = BytePhpPreference(key: "memory_limit")
XCTAssertEqual(pref.internalValue, "512M")
XCTAssertEqual(pref.unit, .megabyte)
XCTAssertEqual(pref.value, 512)
}
func test_can_parse_all_kinds_of_values() throws {
var (unit, value) = BytePhpPreference.readFrom(internalValue: "1G")!
XCTAssertEqual(unit, .gigabyte)
XCTAssertEqual(value, 1)
(unit, value) = BytePhpPreference.readFrom(internalValue: "256M")!
XCTAssertEqual(unit, .megabyte)
XCTAssertEqual(value, 256)
(unit, value) = BytePhpPreference.readFrom(internalValue: "512K")!
XCTAssertEqual(unit, .kilobyte)
XCTAssertEqual(value, 512)
(unit, value) = BytePhpPreference.readFrom(internalValue: "1024")!
XCTAssertEqual(unit, .kilobyte)
XCTAssertEqual(value, 1024)
(unit, value) = BytePhpPreference.readFrom(internalValue: "-1")!
XCTAssertEqual(unit, .kilobyte)
XCTAssertEqual(value, -1)
}
}

View File

@@ -1,81 +0,0 @@
//
// NginxConfigurationTest.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 29/11/2021.
// Copyright © 2023 Nico Verbruggen. All rights reserved.
//
import XCTest
class NginxConfigurationTest: XCTestCase {
// MARK: - Test Files
static var regularUrl: URL {
return Bundle(for: Self.self).url(forResource: "nginx-site", withExtension: "test")!
}
static var isolatedUrl: URL {
return Bundle(for: Self.self).url(forResource: "nginx-site-isolated", withExtension: "test")!
}
static var proxyUrl: URL {
return Bundle(for: Self.self).url(forResource: "nginx-proxy", withExtension: "test")!
}
static var secureProxyUrl: URL {
return Bundle(for: Self.self).url(forResource: "nginx-secure-proxy", withExtension: "test")!
}
static var customTldProxyUrl: URL {
return Bundle(for: Self.self).url(forResource: "nginx-secure-proxy-custom-tld", withExtension: "test")!
}
// MARK: - Tests
func test_can_determine_site_name_and_tld() throws {
XCTAssertEqual(
"nginx-site",
NginxConfigurationFile.from(filePath: NginxConfigurationTest.regularUrl.path)?.domain
)
XCTAssertEqual(
"test",
NginxConfigurationFile.from(filePath: NginxConfigurationTest.regularUrl.path)?.tld
)
}
func test_can_determine_isolation() throws {
XCTAssertNil(
NginxConfigurationFile.from(filePath: NginxConfigurationTest.regularUrl.path)?.isolatedVersion
)
XCTAssertEqual(
"8.1",
NginxConfigurationFile.from(filePath: NginxConfigurationTest.isolatedUrl.path)?.isolatedVersion
)
}
func test_can_determine_proxy() throws {
let proxied = NginxConfigurationFile.from(filePath: NginxConfigurationTest.proxyUrl.path)!
XCTAssertTrue(proxied.contents.contains("# valet stub: proxy.valet.conf"))
XCTAssertEqual("http://127.0.0.1:90", proxied.proxy)
let normal = NginxConfigurationFile.from(filePath: NginxConfigurationTest.regularUrl.path)!
XCTAssertFalse(normal.contents.contains("# valet stub: proxy.valet.conf"))
XCTAssertEqual(nil, normal.proxy)
}
func test_can_determine_secured_proxy() throws {
let proxied = NginxConfigurationFile.from(filePath: NginxConfigurationTest.secureProxyUrl.path)!
XCTAssertTrue(proxied.contents.contains("# valet stub: secure.proxy.valet.conf"))
XCTAssertEqual("http://127.0.0.1:90", proxied.proxy)
}
func test_can_determine_proxy_with_custom_tld() throws {
let proxied = NginxConfigurationFile.from(filePath: NginxConfigurationTest.customTldProxyUrl.path)!
XCTAssertTrue(proxied.contents.contains("# valet stub: secure.proxy.valet.conf"))
XCTAssertEqual("http://localhost:8080", proxied.proxy)
}
}

View File

@@ -1,72 +0,0 @@
//
// ExtensionParserTest.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 13/02/2021.
// Copyright © 2023 Nico Verbruggen. All rights reserved.
//
import XCTest
class PhpExtensionTest: XCTestCase {
static var phpIniFileUrl: URL {
return Bundle(for: Self.self).url(forResource: "php", withExtension: "ini")!
}
func test_can_load_extension() throws {
let extensions = PhpExtension.from(filePath: Self.phpIniFileUrl.path)
XCTAssertGreaterThan(extensions.count, 0)
}
func test_extension_name_is_correct() throws {
let extensions = PhpExtension.from(filePath: Self.phpIniFileUrl.path)
let extensionNames = extensions.map { (ext) -> String in
return ext.name
}
// These 6 should be found
XCTAssertTrue(extensionNames.contains("xdebug"))
XCTAssertTrue(extensionNames.contains("imagick"))
XCTAssertTrue(extensionNames.contains("sodium-next"))
XCTAssertTrue(extensionNames.contains("opcache"))
XCTAssertTrue(extensionNames.contains("yaml"))
XCTAssertTrue(extensionNames.contains("custom"))
XCTAssertFalse(extensionNames.contains("fake"))
XCTAssertFalse(extensionNames.contains("nice"))
}
func test_extension_status_is_correct() throws {
let extensions = PhpExtension.from(filePath: Self.phpIniFileUrl.path)
// xdebug should be enabled
XCTAssertEqual(extensions[0].enabled, true)
// imagick should be disabled
XCTAssertEqual(extensions[1].enabled, false)
}
func test_toggle_works_as_expected() async throws {
let destination = Utility.copyToTemporaryFile(resourceName: "php", fileExtension: "ini")!
let extensions = PhpExtension.from(filePath: destination.path)
XCTAssertEqual(extensions.count, 6)
// Try to disable xdebug (should be detected first)!
let xdebug = extensions.first!
XCTAssertTrue(xdebug.name == "xdebug")
XCTAssertEqual(xdebug.enabled, true)
await xdebug.toggle()
XCTAssertEqual(xdebug.enabled, false)
// Check if the file contains the appropriate data
let file = try! String(contentsOf: destination, encoding: .utf8)
XCTAssertTrue(file.contains("; zend_extension=\"xdebug.so\""))
// Make sure if we load the data again, it's disabled
XCTAssertEqual(PhpExtension.from(filePath: destination.path).first!.enabled, false)
}
}

View File

@@ -1,50 +0,0 @@
//
// ValetRcTest.swift
// Unit Tests
//
// Created by Nico Verbruggen on 20/01/2023.
// Copyright © 2023 Nico Verbruggen. All rights reserved.
//
import XCTest
class ValetRcTest: XCTestCase {
// MARK: - Test Files
static var validPath: URL {
return Bundle(for: Self.self)
.url(forResource: "valetrc", withExtension: "valid")!
}
static var brokenPath: URL {
return Bundle(for: Self.self)
.url(forResource: "valetrc", withExtension: "broken")!
}
// MARK: - Tests
func test_can_extract_fields_from_valetrc_file() throws {
let fakeFile = RCFile.fromPath("/Users/fake/file.rc")
XCTAssertNil(fakeFile)
// Can parse the file
let validFile = RCFile.fromPath(ValetRcTest.validPath.path)
XCTAssertNotNil(validFile)
let fields = validFile!.fields
// Correctly parses and trims (and omits double quotes) per line
XCTAssertEqual(fields["PHP"], "php@8.2")
XCTAssertEqual(fields["OTHER"], "thing")
XCTAssertEqual(fields["PHPMON_WATCH"], "true")
XCTAssertEqual(fields["SYNTAX"], "variable")
// Ignores entries prefixed with #
XCTAssertTrue(!fields.keys.contains("#PHP"))
// Ignores invalid lines
XCTAssertTrue(!fields.keys.contains("OOF"))
}
}

View File

@@ -0,0 +1,29 @@
//
// Untitled.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 29/09/2025.
// Copyright © 2025 Nico Verbruggen. All rights reserved.
//
import Testing
import Foundation
struct TestableApiTest {
@Test func createFakeApi() {
let api = TestableApi(responses: [
url("https://api.phpmon.test"): FakeApiResponse(
statusCode: 200,
headers: [:],
text: "{\"success\": true}"
)
])
#expect(api.hasResponse(for: url("https://api.phpmon.test")) == true)
let response = api.getResponse(for: url("https://api.phpmon.test"))
#expect(response.statusCode == 200)
#expect(response.text.contains("success"))
}
}

View File

@@ -8,11 +8,8 @@
import Testing
@Suite("Commands")
struct CommandTest {
@Test
func determinePhpVersion() {
@Test func determinePhpVersion() {
let version = Command.execute(
path: Paths.php,
arguments: ["-v"],
@@ -24,5 +21,4 @@ struct CommandTest {
#expect(version.contains("built"))
#expect(version.contains("Zend"))
}
}

View File

@@ -8,7 +8,6 @@
import Testing
@Suite("Integration")
struct PackagistTest {
@Test func canRetrieveLaravelValetVersion() async {
let packageToCheck = "laravel/valet"

View File

@@ -0,0 +1,41 @@
//
// BytePhpPreferenceTest.swift
// Unit Tests
//
// Created by Nico Verbruggen on 04/09/2023.
// Copyright © 2023 Nico Verbruggen. All rights reserved.
//
import Testing
struct BytePhpPreferenceTest {
@Test func can_extract_memory_value() throws {
let pref = BytePhpPreference(key: "memory_limit")
#expect(pref.internalValue == "512M")
#expect(pref.unit == .megabyte)
#expect(pref.value == 512)
}
@Test func can_parse_all_kinds_of_values() throws {
var (unit, value) = BytePhpPreference.readFrom(internalValue: "1G")!
#expect(unit == .gigabyte)
#expect(value == 1)
(unit, value) = BytePhpPreference.readFrom(internalValue: "256M")!
#expect(unit == .megabyte)
#expect(value == 256)
(unit, value) = BytePhpPreference.readFrom(internalValue: "512K")!
#expect(unit == .kilobyte)
#expect(value == 512)
(unit, value) = BytePhpPreference.readFrom(internalValue: "1024")!
#expect(unit == .kilobyte)
#expect(value == 1024)
(unit, value) = BytePhpPreference.readFrom(internalValue: "-1")!
#expect(unit == .kilobyte)
#expect(value == -1)
}
}

View File

@@ -0,0 +1,60 @@
//
// CaskFileParserTest.swift
// Unit Tests
//
// Created by Nico Verbruggen on 04/02/2023.
// Copyright © 2023 Nico Verbruggen. All rights reserved.
//
import Testing
import Foundation
@Suite(.serialized)
struct CaskFileParserTest {
init() async throws {
ActiveShell.useSystem()
}
// MARK: - Test Files
static var exampleFilePath: URL {
TestBundle.url(forResource: "phpmon-dev", withExtension: "rb")!
}
@Test func can_extract_fields_from_cask_file() async throws {
guard let caskFile = await CaskFile.from(url: CaskFileParserTest.exampleFilePath) else {
Issue.record("The CaskFile could not be parsed, check the log for more info")
return
}
#expect(
caskFile.version ==
"5.7.2_1035"
)
#expect(
caskFile.sha256 ==
"1cb147bd1b1fbd52971d90dff577465b644aee7c878f15ede57f46e8f217067a"
)
#expect(
caskFile.name ==
"PHP Monitor DEV"
)
#expect(
caskFile.url ==
"https://github.com/nicoverbruggen/phpmon/releases/download/v5.7.2/phpmon-dev.zip"
)
}
@Test func can_extract_fields_from_remote_cask_file() async throws {
let url = URL(string: "https://raw.githubusercontent.com/nicoverbruggen/homebrew-cask/master/Casks/phpmon.rb")!
guard let caskFile = await CaskFile.from(url: url) else {
Issue.record("The remote CaskFile could not be parsed, check the log for more info")
return
}
#expect(caskFile.properties.keys.contains("version"))
#expect(caskFile.properties.keys.contains("homepage"))
#expect(caskFile.properties.keys.contains("url"))
}
}

View File

@@ -6,36 +6,32 @@
// Copyright © 2023 Nico Verbruggen. All rights reserved.
//
import XCTest
import Testing
import Foundation
final class ExtensionEnumeratorTest: XCTestCase {
override func setUp() async throws {
struct ExtensionEnumeratorTest {
init() async throws {
ActiveFileSystem.useTestable([
"\(Paths.tapPath)/shivammathur/homebrew-extensions/Formula/xdebug@8.1.rb": .fake(.text, "<test>"),
"\(Paths.tapPath)/shivammathur/homebrew-extensions/Formula/xdebug@8.2.rb": .fake(.text, "<test>"),
"\(Paths.tapPath)/shivammathur/homebrew-extensions/Formula/xdebug@8.3.rb": .fake(.text, "<test>"),
"\(Paths.tapPath)/shivammathur/homebrew-extensions/Formula/xdebug@8.4.rb": .fake(.text, "<test>"),
"\(Paths.tapPath)/shivammathur/homebrew-extensions/Formula/xdebug@8.4.rb": .fake(.text, "<test>")
])
}
func testCanReadFormulae() throws {
@Test func can_read_formulae() throws {
let directory = "\(Paths.tapPath)/shivammathur/homebrew-extensions/Formula"
let files = try FileSystem.getShallowContentsOfDirectory(directory)
XCTAssertEqual(
Set(["xdebug@8.1.rb", "xdebug@8.2.rb", "xdebug@8.3.rb", "xdebug@8.4.rb"]),
Set(files)
)
#expect(Set(files) == Set(["xdebug@8.1.rb", "xdebug@8.2.rb", "xdebug@8.3.rb", "xdebug@8.4.rb"]))
}
func testCanParseFormulaeBasedOnSyntax() throws {
@Test func can_parse_formulae_based_on_syntax() throws {
let formulae = BrewTapFormulae.from(tap: "shivammathur/homebrew-extensions")
XCTAssertEqual(formulae["8.1"], [BrewPhpExtension(path: "/", name: "xdebug", phpVersion: "8.1")])
XCTAssertEqual(formulae["8.2"], [BrewPhpExtension(path: "/", name: "xdebug", phpVersion: "8.2")])
XCTAssertEqual(formulae["8.3"], [BrewPhpExtension(path: "/", name: "xdebug", phpVersion: "8.3")])
XCTAssertEqual(formulae["8.4"], [BrewPhpExtension(path: "/", name: "xdebug", phpVersion: "8.4")])
#expect(formulae["8.1"] == [BrewPhpExtension(path: "/", name: "xdebug", phpVersion: "8.1")])
#expect(formulae["8.2"] == [BrewPhpExtension(path: "/", name: "xdebug", phpVersion: "8.2")])
#expect(formulae["8.3"] == [BrewPhpExtension(path: "/", name: "xdebug", phpVersion: "8.3")])
#expect(formulae["8.4"] == [BrewPhpExtension(path: "/", name: "xdebug", phpVersion: "8.4")])
}
}

View File

@@ -6,54 +6,53 @@
// Copyright © 2023 Nico Verbruggen. All rights reserved.
//
import XCTest
import Testing
import Foundation
class HomebrewPackageTest: XCTestCase {
struct HomebrewPackageTest {
// - MARK: SYNTHETIC TESTS
static var jsonBrewFile: URL {
return Bundle(for: Self.self)
.url(forResource: "brew-formula", withExtension: "json")!
TestBundle.url(forResource: "brew-formula", withExtension: "json")!
}
func test_can_load_extension_json() throws {
static var jsonBrewServicesFile: URL {
TestBundle.url(forResource: "brew-services", withExtension: "json")!
}
@Test func can_load_extension_json() throws {
let json = try! String(contentsOf: Self.jsonBrewFile, encoding: .utf8)
let package = try! JSONDecoder().decode(
[HomebrewPackage].self, from: json.data(using: .utf8)!
).first!
XCTAssertEqual(package.full_name, "php")
XCTAssertEqual(package.aliases.first!, "php@8.2")
XCTAssertEqual(package.installed.contains(where: { installed in
installed.version.starts(with: "8.2")
}), true)
#expect(package.full_name == "php")
#expect(package.aliases.first! == "php@8.4")
#expect(package.installed.contains(where: { installed in
installed.version.starts(with: "8.4")
}) == true)
}
static var jsonBrewServicesFile: URL {
return Bundle(for: Self.self)
.url(forResource: "brew-services", withExtension: "json")!
}
func test_can_parse_services_json() throws {
@Test func can_parse_services_json() throws {
let json = try! String(contentsOf: Self.jsonBrewServicesFile, encoding: .utf8)
let services = try! JSONDecoder().decode(
[HomebrewService].self, from: json.data(using: .utf8)!
)
XCTAssertGreaterThan(services.count, 0)
XCTAssertEqual(services.first?.name, "dnsmasq")
XCTAssertEqual(services.first?.service_name, "homebrew.mxcl.dnsmasq")
#expect(!services.isEmpty)
#expect(services.first?.name == "dnsmasq")
#expect(services.first?.service_name == "homebrew.mxcl.dnsmasq")
}
/*
// - MARK: LIVE TESTS
/// This test requires that you have a valid Homebrew installation set up,
/// and requires the Valet services to be installed: php, nginx and dnsmasq.
/// If this test fails, there is an issue with your Homebrew installation
/// or the JSON API of the Homebrew output may have changed.
func test_can_parse_services_json_from_cli_output() async throws {
@Test(.disabled("Uses system command; enable at your own risk"))
func can_parse_services_json_from_cli_output() async throws {
ActiveShell.useSystem()
let services = try! JSONDecoder().decode(
@@ -65,17 +64,19 @@ class HomebrewPackageTest: XCTestCase {
return ["php", "nginx", "dnsmasq"].contains(service.name)
})
XCTAssertTrue(services.contains(where: {$0.name == "php"}))
XCTAssertTrue(services.contains(where: {$0.name == "nginx"}))
XCTAssertTrue(services.contains(where: {$0.name == "dnsmasq"}))
XCTAssertEqual(services.count, 3)
#expect(services.contains(where: {$0.name == "php"}))
#expect(services.contains(where: {$0.name == "nginx"}))
#expect(services.contains(where: {$0.name == "dnsmasq"}))
#expect(services.count == 3)
}
/// This test requires that you have a valid Homebrew installation set up,
/// and requires the `php` formula to be installed.
/// If this test fails, there is an issue with your Homebrew installation
/// or the JSON API of the Homebrew output may have changed.
func test_can_load_extension_json_from_cli_output() async throws {
@Test(.disabled("Uses system command; enable at your own risk"))
func can_load_extension_json_from_cli_output() async throws {
ActiveShell.useSystem()
let package = try! JSONDecoder().decode(
@@ -83,7 +84,6 @@ class HomebrewPackageTest: XCTestCase {
from: await Shell.pipe("\(Paths.brew) info php --json").out.data(using: .utf8)!
).first!
XCTAssertTrue(package.name == "php")
#expect(package.full_name == "php")
}
*/
}

View File

@@ -6,26 +6,34 @@
// Copyright © 2023 Nico Verbruggen. All rights reserved.
//
import XCTest
import Testing
import Foundation
class HomebrewUpgradableTest: XCTestCase {
struct HomebrewUpgradableTest {
static var outdatedFileUrl: URL {
return Bundle(for: Self.self)
.url(forResource: "brew-outdated", withExtension: "json")!
return TestBundle.url(forResource: "brew-outdated", withExtension: "json")!
}
func test_upgradable_php_versions_can_be_parsed() async throws {
@Test func upgradable_php_versions_can_be_determined() async throws {
// Do not execute production cli commands
ActiveShell.useTestable([
"/opt/homebrew/bin/brew update >/dev/null && /opt/homebrew/bin/brew outdated --json --formulae"
: .instant(try! String(contentsOf: Self.outdatedFileUrl)),
: .instant(try! String(contentsOf: Self.outdatedFileUrl)),
"/opt/homebrew/bin/php --ini | grep -E -o '(/[^ ]+\\.ini)'"
: .instant("/opt/homebrew/etc/php/8.2/conf.d/php-memory-limits.ini"),
: .instant("/opt/homebrew/etc/php/8.2/conf.d/php-memory-limits.ini"),
"/opt/homebrew/opt/php@8.1.16/bin/php --ini | grep -E -o '(/[^ ]+\\.ini)'"
: .instant("/opt/homebrew/etc/php/8.1/conf.d/php-memory-limits.ini"),
: .instant("/opt/homebrew/etc/php/8.1/conf.d/php-memory-limits.ini"),
"/opt/homebrew/opt/php@8.2.3/bin/php --ini | grep -E -o '(/[^ ]+\\.ini)'"
: .instant("/opt/homebrew/etc/php/8.2/conf.d/php-memory-limits.ini"),
: .instant("/opt/homebrew/etc/php/8.2/conf.d/php-memory-limits.ini"),
"/opt/homebrew/opt/php@7.4.11/bin/php --ini | grep -E -o '(/[^ ]+\\.ini)'"
: .instant("/opt/homebrew/etc/php/7.4/conf.d/php-memory-limits.ini")
: .instant("/opt/homebrew/etc/php/7.4/conf.d/php-memory-limits.ini")
])
// Do not read our production files
ActiveFileSystem.useTestable([
"/opt/homebrew/etc/php/8.2/conf.d/php-memory-limits.ini": .fake(.text),
"/opt/homebrew/etc/php/8.1/conf.d/php-memory-limits.ini": .fake(.text),
"/opt/homebrew/etc/php/7.4/conf.d/php-memory-limits.ini": .fake(.text)
])
// This config file assumes our PHP alias (`php`) is v8.2
@@ -39,11 +47,11 @@ class HomebrewUpgradableTest: XCTestCase {
let data = await BrewPhpFormulaeHandler().loadPhpVersions(loadOutdated: true)
XCTAssertTrue(data.contains(where: { formula in
#expect(true == data.contains(where: { formula in
formula.installedVersion == "8.1.16" && formula.upgradeVersion == "8.1.17"
}))
XCTAssertTrue(data.contains(where: { formula in
#expect(true == data.contains(where: { formula in
formula.installedVersion == "8.2.3" && formula.upgradeVersion == "8.2.4"
}))
}

View File

@@ -0,0 +1,70 @@
//
// NginxConfigurationTest.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 29/11/2021.
// Copyright © 2023 Nico Verbruggen. All rights reserved.
//
import Testing
import Foundation
struct NginxConfigurationTest {
// MARK: - Test Files
static var regularUrl: URL {
TestBundle.url(forResource: "nginx-site", withExtension: "test")!
}
static var isolatedUrl: URL {
TestBundle.url(forResource: "nginx-site-isolated", withExtension: "test")!
}
static var proxyUrl: URL {
TestBundle.url(forResource: "nginx-proxy", withExtension: "test")!
}
static var secureProxyUrl: URL {
TestBundle.url(forResource: "nginx-secure-proxy", withExtension: "test")!
}
static var customTldProxyUrl: URL {
TestBundle.url(forResource: "nginx-secure-proxy-custom-tld", withExtension: "test")!
}
// MARK: - Tests
@Test func can_determine_site_name_and_tld() throws {
#expect("nginx-site" == NginxConfigurationFile.from(filePath: Self.regularUrl.path)?.domain)
#expect("test" == NginxConfigurationFile.from(filePath: Self.regularUrl.path)?.tld)
}
@Test func can_determine_isolation() throws {
#expect(nil == NginxConfigurationFile.from(filePath: Self.regularUrl.path)?.isolatedVersion)
#expect("8.1" == NginxConfigurationFile.from(filePath: Self.isolatedUrl.path)?.isolatedVersion)
}
@Test func can_determine_proxy() throws {
let proxied = NginxConfigurationFile.from(filePath: Self.proxyUrl.path)!
#expect(proxied.contents.contains("# valet stub: proxy.valet.conf"))
#expect("http://127.0.0.1:90" == proxied.proxy)
let normal = NginxConfigurationFile.from(filePath: Self.regularUrl.path)!
#expect(false == normal.contents.contains("# valet stub: proxy.valet.conf"))
#expect(nil == normal.proxy)
}
@Test func can_determine_secured_proxy() throws {
let proxied = NginxConfigurationFile.from(filePath: Self.secureProxyUrl.path)!
#expect(proxied.contents.contains("# valet stub: secure.proxy.valet.conf"))
#expect("http://127.0.0.1:90" == proxied.proxy)
}
@Test func can_determine_proxy_with_custom_tld() throws {
let proxied = NginxConfigurationFile.from(filePath: Self.customTldProxyUrl.path)!
#expect(proxied.contents.contains("# valet stub: secure.proxy.valet.conf"))
#expect("http://localhost:8080" == proxied.proxy)
}
}

View File

@@ -6,41 +6,43 @@
// Copyright © 2023 Nico Verbruggen. All rights reserved.
//
import XCTest
class PhpConfigurationFileTest: XCTestCase {
import Testing
import Foundation
@Suite(.serialized)
class PhpConfigurationFileTest {
static var phpIniFileUrl: URL {
return Bundle(for: Self.self).url(forResource: "php", withExtension: "ini")!
return TestBundle.url(forResource: "php", withExtension: "ini")!
}
func test_can_load_extension() throws {
@Test func can_load_extension() throws {
ActiveFileSystem.useSystem()
let iniFile = PhpConfigurationFile.from(filePath: Self.phpIniFileUrl.path)
#expect(iniFile != nil)
#expect(!iniFile!.extensions.isEmpty)
}
@Test func can_check_key_existence() throws {
print(Self.phpIniFileUrl.path)
let iniFile = PhpConfigurationFile.from(filePath: Self.phpIniFileUrl.path)!
XCTAssertNotNil(iniFile)
XCTAssertGreaterThan(iniFile.extensions.count, 0)
#expect(iniFile.has(key: "error_reporting"))
#expect(iniFile.has(key: "display_errors"))
#expect(false == iniFile.has(key: "my_unknown_key"))
}
func test_can_check_key_existence() throws {
@Test func can_check_key_value() throws {
let iniFile = PhpConfigurationFile.from(filePath: Self.phpIniFileUrl.path)!
XCTAssertTrue(iniFile.has(key: "error_reporting"))
XCTAssertTrue(iniFile.has(key: "display_errors"))
XCTAssertFalse(iniFile.has(key: "my_unknown_key"))
#expect(iniFile.get(for: "error_reporting") != nil)
#expect(iniFile.get(for: "error_reporting") == "E_ALL")
#expect(iniFile.get(for: "display_errors") != nil)
#expect(iniFile.get(for: "display_errors") == "On")
}
func test_can_check_key_value() throws {
let iniFile = PhpConfigurationFile.from(filePath: Self.phpIniFileUrl.path)!
XCTAssertNotNil(iniFile.get(for: "error_reporting"))
XCTAssert(iniFile.get(for: "error_reporting") == "E_ALL")
XCTAssertNotNil(iniFile.get(for: "display_errors"))
XCTAssert(iniFile.get(for: "display_errors") == "On")
}
func test_can_customize_configuration_value() throws {
@Test func can_customize_configuration_value() throws {
let destination = Utility
.copyToTemporaryFile(resourceName: "php", fileExtension: "ini")!
@@ -48,15 +50,15 @@ class PhpConfigurationFileTest: XCTestCase {
.from(filePath: destination.path)!
// 0. Verify the original value
XCTAssertEqual(configurationFile.get(for: "error_reporting"), "E_ALL")
#expect(configurationFile.get(for: "error_reporting") == "E_ALL")
// 1. Change the value
try! configurationFile.replace(
key: "error_reporting",
value: "E_ALL & ~E_DEPRECATED & ~E_STRICT"
)
XCTAssertEqual(
configurationFile.get(for: "error_reporting"),
#expect(
configurationFile.get(for: "error_reporting") ==
"E_ALL & ~E_DEPRECATED & ~E_STRICT"
)
@@ -65,20 +67,14 @@ class PhpConfigurationFileTest: XCTestCase {
key: "error_reporting",
value: "error_reporting"
)
XCTAssertEqual(
configurationFile.get(for: "error_reporting"),
"error_reporting"
)
#expect(configurationFile.get(for: "error_reporting") == "error_reporting")
// 3. Verify subsequent saves weren't broken
try! configurationFile.replace(
key: "error_reporting",
value: "E_ALL"
)
XCTAssertEqual(
configurationFile.get(for: "error_reporting"),
"E_ALL"
)
#expect(configurationFile.get(for: "error_reporting") == "E_ALL")
}
}

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