diff --git a/PHP Monitor.xcodeproj/project.pbxproj b/PHP Monitor.xcodeproj/project.pbxproj index 857a7a0..c42713c 100644 --- a/PHP Monitor.xcodeproj/project.pbxproj +++ b/PHP Monitor.xcodeproj/project.pbxproj @@ -153,6 +153,10 @@ C4DEB7D427A5D60B00834718 /* Stats.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4DEB7D327A5D60B00834718 /* Stats.swift */; }; C4E0F7ED27BEBDA9007475F2 /* 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 */; }; C4EC1E68279DE0540010F296 /* ServicesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4EC1E67279DE0540010F296 /* ServicesView.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 = ""; }; C4DEB7D327A5D60B00834718 /* Stats.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Stats.swift; sourceTree = ""; }; C4E0F7EC27BEBDA9007475F2 /* NSWindowExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSWindowExtension.swift; sourceTree = ""; }; + C4E4404527C56F4700D225E1 /* Valet.Site.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Valet.Site.swift; sourceTree = ""; }; + C4E4404827C56F5F00D225E1 /* Valet.Configuration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Valet.Configuration.swift; sourceTree = ""; }; C4E713562570150F00007428 /* SECURITY.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = SECURITY.md; sourceTree = ""; }; C4E713572570151400007428 /* docs */ = {isa = PBXFileReference; lastKnownFileType = folder; path = docs; sourceTree = ""; }; C4EC1E65279DE0380010F296 /* ServicesView.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = ServicesView.xib; sourceTree = ""; }; @@ -572,6 +578,8 @@ isa = PBXGroup; children = ( C4AF9F792754499000D44ED0 /* Valet.swift */, + C4E4404527C56F4700D225E1 /* Valet.Site.swift */, + C4E4404827C56F5F00D225E1 /* Valet.Configuration.swift */, ); path = Valet; sourceTree = ""; @@ -848,6 +856,7 @@ C48D6C70279CD2AC00F26D7E /* PhpVersionNumber.swift in Sources */, C4B585412770FE3900DA4FBE /* Shell.swift in Sources */, C4998F0A2617633900B2526E /* PrefsWC.swift in Sources */, + C4E4404927C56F5F00D225E1 /* Valet.Configuration.swift in Sources */, C4F8C0A422D4F12C002EFE61 /* DateExtension.swift in Sources */, C4AF9F7A2754499000D44ED0 /* Valet.swift in Sources */, 5420395926135DC100FB00FA /* PrefsVC.swift in Sources */, @@ -859,6 +868,7 @@ C4EE55AD27708B9E001DF387 /* PMStatsView.swift in Sources */, C4C8E818276F54D8003AC782 /* App+ConfigWatch.swift in Sources */, 54FCFD30276C8DA4004CE748 /* HotkeyPreferenceView.swift in Sources */, + C4E4404627C56F4700D225E1 /* Valet.Site.swift in Sources */, C4EC1E68279DE0540010F296 /* ServicesView.swift in Sources */, C4F2E43A2752F7D00020E974 /* PhpInstallation.swift in Sources */, C41E871A2763D42300161EE0 /* SiteListVC+ContextMenu.swift in Sources */, @@ -999,7 +1009,9 @@ C40B24F127A3106D0018C7D2 /* ServicesView.swift in Sources */, C4F780C525D80B75000DBC97 /* MenuBarImageGenerator.swift in Sources */, C4F780B725D80B5D000DBC97 /* App.swift in Sources */, + C4E4404A27C56F5F00D225E1 /* Valet.Configuration.swift in Sources */, C4927F0C27B2DFC200C55AFD /* Errors.swift in Sources */, + C4E4404727C56F4700D225E1 /* Valet.Site.swift in Sources */, C44CCD4A27AFF3BC00CE40E5 /* MainMenu+Async.swift in Sources */, C48D6C71279CD2AC00F26D7E /* PhpVersionNumber.swift in Sources */, C4F780C925D80B75000DBC97 /* StringExtension.swift in Sources */, diff --git a/assets/affinity/icon.afdesign b/assets/affinity/icon.afdesign index caf2d82..e5b3710 100644 Binary files a/assets/affinity/icon.afdesign and b/assets/affinity/icon.afdesign differ diff --git a/phpmon/Domain/Integrations/Composer/ComposerJson.swift b/phpmon/Domain/Integrations/Composer/ComposerJson.swift index 30f0b10..d06697d 100644 --- a/phpmon/Domain/Integrations/Composer/ComposerJson.swift +++ b/phpmon/Domain/Integrations/Composer/ComposerJson.swift @@ -39,19 +39,20 @@ struct ComposerJson: Decodable { Checks what the PHP version constraint is. Returns a tuple (constraint, location of constraint). */ - public func getPhpVersion() -> (String, String) { + public func getPhpVersion() -> (String, Valet.Site.VersionSource) + { // Check if in platform if configuration?.platform?.php != nil { - return (configuration!.platform!.php!, "platform") + return (configuration!.platform!.php!, .platform) } // Check if in dependencies if dependencies?["php"] != nil { - return (dependencies!["php"]!, "require") + return (dependencies!["php"]!, .require) } // Unknown! - return ("???", "unknown") + return ("???", .unknown) } /** diff --git a/phpmon/Domain/Integrations/Valet/Valet.Configuration.swift b/phpmon/Domain/Integrations/Valet/Valet.Configuration.swift new file mode 100644 index 0000000..36329cf --- /dev/null +++ b/phpmon/Domain/Integrations/Valet/Valet.Configuration.swift @@ -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" + } + } + +} diff --git a/phpmon/Domain/Integrations/Valet/Valet.Site.swift b/phpmon/Domain/Integrations/Valet/Valet.Site.swift new file mode 100644 index 0000000..2e4e5fb --- /dev/null +++ b/phpmon/Domain/Integrations/Valet/Valet.Site.swift @@ -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 + } + } + } + +} diff --git a/phpmon/Domain/Integrations/Valet/Valet.swift b/phpmon/Domain/Integrations/Valet/Valet.swift index c67daed..d7b4b9f 100644 --- a/phpmon/Domain/Integrations/Valet/Valet.swift +++ b/phpmon/Domain/Integrations/Valet/Valet.swift @@ -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" - } - } - } diff --git a/phpmon/Domain/SiteList/SiteListCell.swift b/phpmon/Domain/SiteList/SiteListCell.swift index 8a24f50..f4593a4 100644 --- a/phpmon/Domain/SiteList/SiteListCell.swift +++ b/phpmon/Domain/SiteList/SiteListCell.swift @@ -80,8 +80,8 @@ class SiteListCell: NSTableCellView alert.messageText = "alert.composer_php_requirement.title" .localized("\(site.name!).\(Valet.shared.config.tld)", site.composerPhp) - alert.informativeText = "alert.composer_php_requirement.info" - .localized(site.composerPhpSource) + alert.informativeText = "alert.composer_php_requirement.type.\(site.composerPhpSource.rawValue)" + .localized alert.addButton(withTitle: "site_link.close".localized) diff --git a/phpmon/Localizable.strings b/phpmon/Localizable.strings index caf0769..c55a0d4 100644 --- a/phpmon/Localizable.strings +++ b/phpmon/Localizable.strings @@ -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."; // Composer Version -"alert.composer_php_requirement.title" = "`%@` has the following PHP requirement: \"php\":\n\"%@\"."; -"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.title" = "`%@` has the following PHP requirement: %@."; +"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 "alert.php_switch_failed.title" = "Switching to PHP %@ seems to have failed.";