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

♻️ Refactor Valet structure, add #143

This commit is contained in:
2022-02-22 20:39:35 +01:00
parent e8ba24e48b
commit e398f089af
8 changed files with 244 additions and 180 deletions

View File

@ -153,6 +153,10 @@
C4DEB7D427A5D60B00834718 /* Stats.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4DEB7D327A5D60B00834718 /* Stats.swift */; }; C4DEB7D427A5D60B00834718 /* Stats.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4DEB7D327A5D60B00834718 /* Stats.swift */; };
C4E0F7ED27BEBDA9007475F2 /* NSWindowExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4E0F7EC27BEBDA9007475F2 /* NSWindowExtension.swift */; }; C4E0F7ED27BEBDA9007475F2 /* NSWindowExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4E0F7EC27BEBDA9007475F2 /* NSWindowExtension.swift */; };
C4E0F7EE27BEBDA9007475F2 /* NSWindowExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4E0F7EC27BEBDA9007475F2 /* NSWindowExtension.swift */; }; C4E0F7EE27BEBDA9007475F2 /* NSWindowExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4E0F7EC27BEBDA9007475F2 /* NSWindowExtension.swift */; };
C4E4404627C56F4700D225E1 /* Valet.Site.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4E4404527C56F4700D225E1 /* Valet.Site.swift */; };
C4E4404727C56F4700D225E1 /* Valet.Site.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4E4404527C56F4700D225E1 /* Valet.Site.swift */; };
C4E4404927C56F5F00D225E1 /* Valet.Configuration.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4E4404827C56F5F00D225E1 /* Valet.Configuration.swift */; };
C4E4404A27C56F5F00D225E1 /* Valet.Configuration.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4E4404827C56F5F00D225E1 /* Valet.Configuration.swift */; };
C4EC1E66279DE0380010F296 /* ServicesView.xib in Resources */ = {isa = PBXBuildFile; fileRef = C4EC1E65279DE0380010F296 /* ServicesView.xib */; }; C4EC1E66279DE0380010F296 /* ServicesView.xib in Resources */ = {isa = PBXBuildFile; fileRef = C4EC1E65279DE0380010F296 /* ServicesView.xib */; };
C4EC1E68279DE0540010F296 /* ServicesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4EC1E67279DE0540010F296 /* ServicesView.swift */; }; C4EC1E68279DE0540010F296 /* ServicesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4EC1E67279DE0540010F296 /* ServicesView.swift */; };
C4EC1E73279DFCF40010F296 /* Events.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4EC1E72279DFCF40010F296 /* Events.swift */; }; C4EC1E73279DFCF40010F296 /* Events.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4EC1E72279DFCF40010F296 /* Events.swift */; };
@ -303,6 +307,8 @@
C4D9ADC7277611A0007277F4 /* InternalSwitcher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InternalSwitcher.swift; sourceTree = "<group>"; }; C4D9ADC7277611A0007277F4 /* InternalSwitcher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InternalSwitcher.swift; sourceTree = "<group>"; };
C4DEB7D327A5D60B00834718 /* Stats.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Stats.swift; sourceTree = "<group>"; }; C4DEB7D327A5D60B00834718 /* Stats.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Stats.swift; sourceTree = "<group>"; };
C4E0F7EC27BEBDA9007475F2 /* NSWindowExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSWindowExtension.swift; sourceTree = "<group>"; }; C4E0F7EC27BEBDA9007475F2 /* NSWindowExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSWindowExtension.swift; sourceTree = "<group>"; };
C4E4404527C56F4700D225E1 /* Valet.Site.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Valet.Site.swift; sourceTree = "<group>"; };
C4E4404827C56F5F00D225E1 /* Valet.Configuration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Valet.Configuration.swift; sourceTree = "<group>"; };
C4E713562570150F00007428 /* SECURITY.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = SECURITY.md; sourceTree = "<group>"; }; C4E713562570150F00007428 /* SECURITY.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = SECURITY.md; sourceTree = "<group>"; };
C4E713572570151400007428 /* docs */ = {isa = PBXFileReference; lastKnownFileType = folder; path = docs; sourceTree = "<group>"; }; C4E713572570151400007428 /* docs */ = {isa = PBXFileReference; lastKnownFileType = folder; path = docs; sourceTree = "<group>"; };
C4EC1E65279DE0380010F296 /* ServicesView.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = ServicesView.xib; sourceTree = "<group>"; }; C4EC1E65279DE0380010F296 /* ServicesView.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = ServicesView.xib; sourceTree = "<group>"; };
@ -572,6 +578,8 @@
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
C4AF9F792754499000D44ED0 /* Valet.swift */, C4AF9F792754499000D44ED0 /* Valet.swift */,
C4E4404527C56F4700D225E1 /* Valet.Site.swift */,
C4E4404827C56F5F00D225E1 /* Valet.Configuration.swift */,
); );
path = Valet; path = Valet;
sourceTree = "<group>"; sourceTree = "<group>";
@ -848,6 +856,7 @@
C48D6C70279CD2AC00F26D7E /* PhpVersionNumber.swift in Sources */, C48D6C70279CD2AC00F26D7E /* PhpVersionNumber.swift in Sources */,
C4B585412770FE3900DA4FBE /* Shell.swift in Sources */, C4B585412770FE3900DA4FBE /* Shell.swift in Sources */,
C4998F0A2617633900B2526E /* PrefsWC.swift in Sources */, C4998F0A2617633900B2526E /* PrefsWC.swift in Sources */,
C4E4404927C56F5F00D225E1 /* Valet.Configuration.swift in Sources */,
C4F8C0A422D4F12C002EFE61 /* DateExtension.swift in Sources */, C4F8C0A422D4F12C002EFE61 /* DateExtension.swift in Sources */,
C4AF9F7A2754499000D44ED0 /* Valet.swift in Sources */, C4AF9F7A2754499000D44ED0 /* Valet.swift in Sources */,
5420395926135DC100FB00FA /* PrefsVC.swift in Sources */, 5420395926135DC100FB00FA /* PrefsVC.swift in Sources */,
@ -859,6 +868,7 @@
C4EE55AD27708B9E001DF387 /* PMStatsView.swift in Sources */, C4EE55AD27708B9E001DF387 /* PMStatsView.swift in Sources */,
C4C8E818276F54D8003AC782 /* App+ConfigWatch.swift in Sources */, C4C8E818276F54D8003AC782 /* App+ConfigWatch.swift in Sources */,
54FCFD30276C8DA4004CE748 /* HotkeyPreferenceView.swift in Sources */, 54FCFD30276C8DA4004CE748 /* HotkeyPreferenceView.swift in Sources */,
C4E4404627C56F4700D225E1 /* Valet.Site.swift in Sources */,
C4EC1E68279DE0540010F296 /* ServicesView.swift in Sources */, C4EC1E68279DE0540010F296 /* ServicesView.swift in Sources */,
C4F2E43A2752F7D00020E974 /* PhpInstallation.swift in Sources */, C4F2E43A2752F7D00020E974 /* PhpInstallation.swift in Sources */,
C41E871A2763D42300161EE0 /* SiteListVC+ContextMenu.swift in Sources */, C41E871A2763D42300161EE0 /* SiteListVC+ContextMenu.swift in Sources */,
@ -999,7 +1009,9 @@
C40B24F127A3106D0018C7D2 /* ServicesView.swift in Sources */, C40B24F127A3106D0018C7D2 /* ServicesView.swift in Sources */,
C4F780C525D80B75000DBC97 /* MenuBarImageGenerator.swift in Sources */, C4F780C525D80B75000DBC97 /* MenuBarImageGenerator.swift in Sources */,
C4F780B725D80B5D000DBC97 /* App.swift in Sources */, C4F780B725D80B5D000DBC97 /* App.swift in Sources */,
C4E4404A27C56F5F00D225E1 /* Valet.Configuration.swift in Sources */,
C4927F0C27B2DFC200C55AFD /* Errors.swift in Sources */, C4927F0C27B2DFC200C55AFD /* Errors.swift in Sources */,
C4E4404727C56F4700D225E1 /* Valet.Site.swift in Sources */,
C44CCD4A27AFF3BC00CE40E5 /* MainMenu+Async.swift in Sources */, C44CCD4A27AFF3BC00CE40E5 /* MainMenu+Async.swift in Sources */,
C48D6C71279CD2AC00F26D7E /* PhpVersionNumber.swift in Sources */, C48D6C71279CD2AC00F26D7E /* PhpVersionNumber.swift in Sources */,
C4F780C925D80B75000DBC97 /* StringExtension.swift in Sources */, C4F780C925D80B75000DBC97 /* StringExtension.swift in Sources */,

Binary file not shown.

View File

@ -39,19 +39,20 @@ struct ComposerJson: Decodable {
Checks what the PHP version constraint is. Checks what the PHP version constraint is.
Returns a tuple (constraint, location of constraint). Returns a tuple (constraint, location of constraint).
*/ */
public func getPhpVersion() -> (String, String) { public func getPhpVersion() -> (String, Valet.Site.VersionSource)
{
// Check if in platform // Check if in platform
if configuration?.platform?.php != nil { if configuration?.platform?.php != nil {
return (configuration!.platform!.php!, "platform") return (configuration!.platform!.php!, .platform)
} }
// Check if in dependencies // Check if in dependencies
if dependencies?["php"] != nil { if dependencies?["php"] != nil {
return (dependencies!["php"]!, "require") return (dependencies!["php"]!, .require)
} }
// Unknown! // Unknown!
return ("???", "unknown") return ("???", .unknown)
} }
/** /**

View File

@ -0,0 +1,32 @@
//
// Valet.Configuration.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 22/02/2022.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
//
import Foundation
extension Valet {
struct Configuration: Decodable {
/// Top level domain suffix. Usually "test" but can be set to something else.
/// - Important: Does not include the actual dot. ("test", not ".test"!)
let tld: String
/// The paths that need to be checked.
let paths: [String]
/// The loopback address. Optional.
let loopback: String?
/// The default site that is served if the domain is not found. Optional.
let defaultSite: String?
private enum CodingKeys: String, CodingKey {
case tld, paths, loopback, defaultSite = "default"
}
}
}

View File

@ -0,0 +1,188 @@
//
// Valet+Subclasses.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 22/02/2022.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
//
import Foundation
extension Valet {
class Site {
/// Name of the site. Does not include the TLD.
var name: String!
/// The absolute path to the directory that is served.
var absolutePath: String!
/// The absolute path to the directory that is served,
/// replacing the user's home folder with ~.
lazy var absolutePathRelative: String = {
return self.absolutePath
.replacingOccurrences(of: "/Users/\(Paths.whoami)", with: "~")
}()
/// Location of the alias. If set, this is a linked domain.
var aliasPath: String?
/// Whether the site has been secured.
var secured: Bool!
/// What driver is currently in use. If not detected, defaults to nil.
var driver: String? = nil
/// Whether the driver was determined by checking the Composer file.
var driverDeterminedByComposer: Bool = false
/// A list of notable Composer dependencies.
var notableComposerDependencies: [String: String] = [:]
/// The PHP version as discovered in `composer.json` or in .valetphprc.
var composerPhp: String = "???"
/// Check whether the PHP version is valid for the currently linked version.
var composerPhpCompatibleWithLinked: Bool = false
/// How the PHP version was determined.
var composerPhpSource: VersionSource = .unknown
enum VersionSource: String {
case unknown = "unknown"
case require = "require"
case platform = "platform"
case valetphprc = "valetphprc"
}
init() {}
convenience init(absolutePath: String, tld: String) {
self.init()
self.absolutePath = absolutePath
self.name = URL(fileURLWithPath: absolutePath).lastPathComponent
self.aliasPath = nil
determineSecured(tld)
determineComposerPhpVersion()
determineDriver()
}
convenience init(aliasPath: String, tld: String) {
self.init()
self.absolutePath = try! FileManager.default.destinationOfSymbolicLink(atPath: aliasPath)
self.name = URL(fileURLWithPath: aliasPath).lastPathComponent
self.aliasPath = aliasPath
determineSecured(tld)
determineComposerPhpVersion()
determineDriver()
}
/**
Checks if a certificate file can be found in the `valet/Certificates` directory.
- Note: The file is not validated, only its presence is checked.
*/
public func determineSecured(_ tld: String) {
secured = Filesystem.fileExists("~/.config/valet/Certificates/\(self.name!).\(tld).key")
}
/**
Checks if `composer.json` exists in the folder, and extracts notable information:
- The PHP version required (the constraint, so it could be `^8.0`, for example)
- Where the PHP version was found (`require` or `platform` or via .valetphprc)
- Notable PHP dependencies (determined via `PhpFrameworks.DependencyList`)
The method then also checks if the determined constraint (if found) is compatible
with the currently linked version of PHP (see `composerPhpMatchesSystem`).
*/
public func determineComposerPhpVersion() {
do {
let path = "\(absolutePath!)/composer.json"
if Filesystem.fileExists(path) {
let decoded = try JSONDecoder().decode(
ComposerJson.self,
from: String(contentsOf: URL(fileURLWithPath: path), encoding: .utf8).data(using: .utf8)!
)
(self.composerPhp, self.composerPhpSource) = decoded.getPhpVersion()
self.notableComposerDependencies = decoded.getNotableDependencies()
}
} catch {
Log.err("Something went wrong reading the Composer JSON file.")
}
do {
let path = "\(absolutePath!)/.valetphprc"
if Filesystem.fileExists(path) {
let contents = try String(contentsOf: URL(fileURLWithPath: path), encoding: .utf8)
if let version = VersionExtractor.from(contents) {
self.composerPhp = version
self.composerPhpSource = .valetphprc
}
}
} catch {
Log.err("Something went wrong parsing the .valetphprc file")
}
// TODO: Check if .valetphprc exists and if it contains a valid version number
if self.composerPhp == "???" {
return
}
// Split the composer list (on "|") to evaluate multiple constraints
// For example, for Laravel 8 projects the value is "^7.3|^8.0"
self.composerPhpCompatibleWithLinked =
self.composerPhp.split(separator: "|").map { string in
return PhpVersionNumberCollection.make(from: [PhpEnv.phpInstall.version.long])
.matching(constraint: string.trimmingCharacters(in: .whitespacesAndNewlines))
.count > 0
}.contains(true)
}
/**
Determine the driver to be displayed in the list of sites. In v5.0, this has been changed
to load the "framework" or "project type" instead.
*/
public func determineDriver() {
self.determineDriverViaComposer()
if self.driver == nil {
self.driver = PhpFrameworks.detectFallbackDependency(self.absolutePath)
}
}
/**
Check the dependency list and see if a particular dependency can't be found.
We'll revert the dependency list so that Laravel and Symfony are detected last.
(Some other frameworks might use Laravel, so if we found it first the detection would be incorrect:
this would happen with Statamic, for example.)
*/
private func determineDriverViaComposer() {
self.driverDeterminedByComposer = true
PhpFrameworks.DependencyList.reversed().forEach { (key: String, value: String) in
if self.notableComposerDependencies.keys.contains(key) {
self.driver = value
}
}
}
@available(*, deprecated, renamed: "determineDriver")
private func determineDriverViaValet() {
let driver = Shell.pipe("cd '\(absolutePath!)' && valet which", requiresPath: true)
if driver.contains("This site is served by") {
self.driver = driver
.replacingOccurrences(of: "This site is served by [", with: "")
.replacingOccurrences(of: "ValetDriver].\n", with: "")
} else {
self.driver = nil
}
}
}
}

View File

@ -180,176 +180,4 @@ class Valet {
} }
} }
// MARK: - Structs
class Site {
/// Name of the site. Does not include the TLD.
var name: String!
/// The absolute path to the directory that is served.
var absolutePath: String!
/// The absolute path to the directory that is served,
/// replacing the user's home folder with ~.
lazy var absolutePathRelative: String = {
return self.absolutePath
.replacingOccurrences(of: "/Users/\(Paths.whoami)", with: "~")
}()
/// Location of the alias. If set, this is a linked domain.
var aliasPath: String?
/// Whether the site has been secured.
var secured: Bool!
/// What driver is currently in use. If not detected, defaults to nil.
var driver: String? = nil
/// Whether the driver was determined by checking the Composer file.
var driverDeterminedByComposer: Bool = false
/// A list of notable Composer dependencies.
var notableComposerDependencies: [String: String] = [:]
/// The PHP version as discovered in `composer.json`.
var composerPhp: String = "???"
/// Check whether the PHP version is valid for the currently linked version.
var composerPhpCompatibleWithLinked: Bool = false
/// How the PHP version was determined.
var composerPhpSource: String = "unknown"
init() {}
convenience init(absolutePath: String, tld: String) {
self.init()
self.absolutePath = absolutePath
self.name = URL(fileURLWithPath: absolutePath).lastPathComponent
self.aliasPath = nil
determineSecured(tld)
determineComposerPhpVersion()
determineDriver()
}
convenience init(aliasPath: String, tld: String) {
self.init()
self.absolutePath = try! FileManager.default.destinationOfSymbolicLink(atPath: aliasPath)
self.name = URL(fileURLWithPath: aliasPath).lastPathComponent
self.aliasPath = aliasPath
determineSecured(tld)
determineComposerPhpVersion()
determineDriver()
}
/**
Checks if a certificate file can be found in the `valet/Certificates` directory.
- Note: The file is not validated, only its presence is checked.
*/
public func determineSecured(_ tld: String) {
secured = Filesystem.fileExists("~/.config/valet/Certificates/\(self.name!).\(tld).key")
}
/**
Checks if `composer.json` exists in the folder, and extracts notable information:
- The PHP version required (the constraint, so it could be `^8.0`, for example)
- Where the PHP version was found (`require` or `platform`)
- Notable PHP dependencies (determined via `PhpFrameworks.DependencyList`)
The method then also checks if the determined constraint (if found) is compatible
with the currently linked version of PHP (see `composerPhpMatchesSystem`).
*/
public func determineComposerPhpVersion() {
let path = "\(absolutePath!)/composer.json"
do {
if Filesystem.fileExists(path) {
let decoded = try JSONDecoder().decode(
ComposerJson.self,
from: String(contentsOf: URL(fileURLWithPath: path), encoding: .utf8).data(using: .utf8)!
)
(self.composerPhp, self.composerPhpSource) = decoded.getPhpVersion()
self.notableComposerDependencies = decoded.getNotableDependencies()
}
} catch {
Log.err("Something went wrong reading the composer JSON file.")
}
if self.composerPhp == "???" {
return
}
// Split the composer list (on "|") to evaluate multiple constraints
// For example, for Laravel 8 projects the value is "^7.3|^8.0"
self.composerPhpCompatibleWithLinked =
self.composerPhp.split(separator: "|").map { string in
return PhpVersionNumberCollection.make(from: [PhpEnv.phpInstall.version.long])
.matching(constraint: string.trimmingCharacters(in: .whitespacesAndNewlines))
.count > 0
}.contains(true)
}
/**
Determine the driver to be displayed in the list of sites. In v5.0, this has been changed
to load the "framework" or "project type" instead.
*/
public func determineDriver() {
self.determineDriverViaComposer()
if self.driver == nil {
self.driver = PhpFrameworks.detectFallbackDependency(self.absolutePath)
}
}
/**
Check the dependency list and see if a particular dependency can't be found.
We'll revert the dependency list so that Laravel and Symfony are detected last.
(Some other frameworks might use Laravel, so if we found it first the detection would be incorrect:
this would happen with Statamic, for example.)
*/
private func determineDriverViaComposer() {
self.driverDeterminedByComposer = true
PhpFrameworks.DependencyList.reversed().forEach { (key: String, value: String) in
if self.notableComposerDependencies.keys.contains(key) {
self.driver = value
}
}
}
@available(*, deprecated, renamed: "determineDriver")
private func determineDriverViaValet() {
let driver = Shell.pipe("cd '\(absolutePath!)' && valet which", requiresPath: true)
if driver.contains("This site is served by") {
self.driver = driver
.replacingOccurrences(of: "This site is served by [", with: "")
.replacingOccurrences(of: "ValetDriver].\n", with: "")
} else {
self.driver = nil
}
}
}
struct Configuration: Decodable {
/// Top level domain suffix. Usually "test" but can be set to something else.
/// - Important: Does not include the actual dot. ("test", not ".test"!)
let tld: String
/// The paths that need to be checked.
let paths: [String]
/// The loopback address. Optional.
let loopback: String?
/// The default site that is served if the domain is not found. Optional.
let defaultSite: String?
private enum CodingKeys: String, CodingKey {
case tld, paths, loopback, defaultSite = "default"
}
}
} }

View File

@ -80,8 +80,8 @@ class SiteListCell: NSTableCellView
alert.messageText = "alert.composer_php_requirement.title" alert.messageText = "alert.composer_php_requirement.title"
.localized("\(site.name!).\(Valet.shared.config.tld)", site.composerPhp) .localized("\(site.name!).\(Valet.shared.config.tld)", site.composerPhp)
alert.informativeText = "alert.composer_php_requirement.info" alert.informativeText = "alert.composer_php_requirement.type.\(site.composerPhpSource.rawValue)"
.localized(site.composerPhpSource) .localized
alert.addButton(withTitle: "site_link.close".localized) alert.addButton(withTitle: "site_link.close".localized)

View File

@ -196,8 +196,11 @@ problem manually, using your own Terminal app (this just shows you the output)."
"alert.composer_success.info" = "Your global Composer dependencies have been successfully updated."; "alert.composer_success.info" = "Your global Composer dependencies have been successfully updated.";
// Composer Version // Composer Version
"alert.composer_php_requirement.title" = "`%@` has the following PHP requirement: \"php\":\n\"%@\"."; "alert.composer_php_requirement.title" = "`%@` has the following PHP requirement: %@.";
"alert.composer_php_requirement.info" = "This required PHP version was determined by checking the `%@` field in the `composer.json` file when the site list was last refreshed."; "alert.composer_php_requirement.type.unknown" = "The required PHP version was determined by an unknown factor.";
"alert.composer_php_requirement.type.require" = "This required PHP version was determined by checking the `require` field in the `composer.json` file when the site list was last refreshed.";
"alert.composer_php_requirement.type.platform" = "This required PHP version was determined by checking the `platform` field in the `composer.json` file when the site list was last refreshed.";
"alert.composer_php_requirement.type.valetphprc" = "This required PHP version was determined by checking the .valetphprc file in your project's directory.";
// Suggest Fix My Valet // Suggest Fix My Valet
"alert.php_switch_failed.title" = "Switching to PHP %@ seems to have failed."; "alert.php_switch_failed.title" = "Switching to PHP %@ seems to have failed.";