From c62e3a9905d918f47f6cb23a47fbe49ecf977478 Mon Sep 17 00:00:00 2001 From: Nico Verbruggen Date: Sun, 5 Oct 2025 17:30:35 +0200 Subject: [PATCH] =?UTF-8?q?=F0=9F=9A=A7=20WIP:=20Prepare=20container?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- PHP Monitor.xcodeproj/project.pbxproj | 14 +++---- .../xcschemes/xcschememanagement.plist | 5 +++ packages/container-macro/Package.swift | 16 ++++---- packages/container-macro/README.md | 16 ++------ .../Sources/ContainerMacro/Macros.swift | 25 +++++++++++++ .../ContainerAccessMacro.swift | 30 ++------------- .../Sources/NVContainer/Macros.swift | 37 ------------------- .../ContainerizedMacroTests.swift | 4 +- .../Common/Filesystem/ActiveFileSystem.swift | 16 +------- phpmon/Common/Filesystem/RealFileSystem.swift | 4 +- phpmon/Common/PHP/ActivePhpInstallation.swift | 2 +- .../PHP/Switcher/InternalSwitcher.swift | 2 +- phpmon/Common/Shell/RealShell.swift | 7 ++-- .../Testables/TestableConfiguration.swift | 2 - phpmon/Container.swift | 11 +++--- phpmon/Domain/App/AppDelegate.swift | 11 +++--- .../Valet/Proxies/ValetProxy.swift | 2 +- .../Integrations/Valet/Sites/ValetSite.swift | 6 +-- phpmon/Domain/Watcher/App+ConfigWatch.swift | 2 +- .../PHP Doctor/Data/WarningManager.swift | 6 +-- 20 files changed, 81 insertions(+), 137 deletions(-) create mode 100644 packages/container-macro/Sources/ContainerMacro/Macros.swift rename packages/container-macro/Sources/{NVContainerMacros => ContainerMacroPlugin}/ContainerAccessMacro.swift (58%) delete mode 100644 packages/container-macro/Sources/NVContainer/Macros.swift rename packages/container-macro/Tests/{NVContainerTests => ContainerMacroTests}/ContainerizedMacroTests.swift (97%) diff --git a/PHP Monitor.xcodeproj/project.pbxproj b/PHP Monitor.xcodeproj/project.pbxproj index e51655ec..8eabc1e2 100644 --- a/PHP Monitor.xcodeproj/project.pbxproj +++ b/PHP Monitor.xcodeproj/project.pbxproj @@ -7,7 +7,7 @@ objects = { /* Begin PBXBuildFile section */ - 030341272E92BD130031BE17 /* NVContainer in Frameworks */ = {isa = PBXBuildFile; productRef = 030341262E92BD130031BE17 /* NVContainer */; }; + 0303412A2E92C3560031BE17 /* ContainerMacro in Frameworks */ = {isa = PBXBuildFile; productRef = 030341292E92C3560031BE17 /* ContainerMacro */; }; 0309E6672B0D4B2F002AC007 /* BrewExtensionsObservable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0309E6662B0D4B2F002AC007 /* BrewExtensionsObservable.swift */; }; 031E2B692B1525A7007C29E1 /* BrewPhpExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 031E2B682B1525A7007C29E1 /* BrewPhpExtension.swift */; }; 031E2B6A2B1525A7007C29E1 /* BrewPhpExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 031E2B682B1525A7007C29E1 /* BrewPhpExtension.swift */; }; @@ -1296,7 +1296,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 030341272E92BD130031BE17 /* NVContainer in Frameworks */, + 0303412A2E92C3560031BE17 /* ContainerMacro in Frameworks */, C47014FF2C46D57C0069AAE7 /* NVAlert in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -2433,7 +2433,7 @@ name = "PHP Monitor"; packageProductDependencies = ( C47014FE2C46D57C0069AAE7 /* NVAlert */, - 030341262E92BD130031BE17 /* NVContainer */, + 030341292E92C3560031BE17 /* ContainerMacro */, ); productName = phpmon; productReference = C41C1B3322B0097F00E7CF16 /* PHP Monitor.app */; @@ -2551,7 +2551,7 @@ packageReferences = ( C47014FA2C46D31B0069AAE7 /* XCRemoteSwiftPackageReference "NVAppUpdater" */, C47014FD2C46D57C0069AAE7 /* XCRemoteSwiftPackageReference "NVAlert" */, - 030341252E92BD130031BE17 /* XCLocalSwiftPackageReference "packages/container-macro" */, + 030341282E92C3560031BE17 /* XCLocalSwiftPackageReference "packages/container-macro" */, ); productRefGroup = C41C1B3422B0097F00E7CF16 /* Products */; projectDirPath = ""; @@ -4508,7 +4508,7 @@ /* End XCConfigurationList section */ /* Begin XCLocalSwiftPackageReference section */ - 030341252E92BD130031BE17 /* XCLocalSwiftPackageReference "packages/container-macro" */ = { + 030341282E92C3560031BE17 /* XCLocalSwiftPackageReference "packages/container-macro" */ = { isa = XCLocalSwiftPackageReference; relativePath = "packages/container-macro"; }; @@ -4534,9 +4534,9 @@ /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ - 030341262E92BD130031BE17 /* NVContainer */ = { + 030341292E92C3560031BE17 /* ContainerMacro */ = { isa = XCSwiftPackageProductDependency; - productName = NVContainer; + productName = ContainerMacro; }; C47014FB2C46D31B0069AAE7 /* NVAppUpdater */ = { isa = XCSwiftPackageProductDependency; diff --git a/packages/container-macro/.swiftpm/xcode/xcuserdata/nico.xcuserdatad/xcschemes/xcschememanagement.plist b/packages/container-macro/.swiftpm/xcode/xcuserdata/nico.xcuserdatad/xcschemes/xcschememanagement.plist index b4604eb8..537602e9 100644 --- a/packages/container-macro/.swiftpm/xcode/xcuserdata/nico.xcuserdatad/xcschemes/xcschememanagement.plist +++ b/packages/container-macro/.swiftpm/xcode/xcuserdata/nico.xcuserdatad/xcschemes/xcschememanagement.plist @@ -4,6 +4,11 @@ SchemeUserState + ContainerMacro.xcscheme_^#shared#^_ + + orderHint + 4 + NVContainer.xcscheme_^#shared#^_ orderHint diff --git a/packages/container-macro/Package.swift b/packages/container-macro/Package.swift index 02c9b2b2..f5c2cb02 100644 --- a/packages/container-macro/Package.swift +++ b/packages/container-macro/Package.swift @@ -4,12 +4,12 @@ import PackageDescription import CompilerPluginSupport let package = Package( - name: "NVContainer", + name: "ContainerMacro", platforms: [.macOS(.v13)], products: [ .library( - name: "NVContainer", - targets: ["NVContainer"] + name: "ContainerMacro", + targets: ["ContainerMacro"] ), ], dependencies: [ @@ -17,20 +17,20 @@ let package = Package( ], targets: [ .macro( - name: "NVContainerMacros", + name: "ContainerMacroPlugin", dependencies: [ .product(name: "SwiftSyntaxMacros", package: "swift-syntax"), .product(name: "SwiftCompilerPlugin", package: "swift-syntax") ] ), .target( - name: "NVContainer", - dependencies: ["NVContainerMacros"] + name: "ContainerMacro", + dependencies: ["ContainerMacroPlugin"] ), .testTarget( - name: "NVContainerTests", + name: "ContainerMacroTests", dependencies: [ - "NVContainerMacros", + "ContainerMacroPlugin", .product(name: "SwiftSyntaxMacrosTestSupport", package: "swift-syntax"), ] ), diff --git a/packages/container-macro/README.md b/packages/container-macro/README.md index a6eee65b..5e5db3c2 100644 --- a/packages/container-macro/README.md +++ b/packages/container-macro/README.md @@ -1,13 +1,12 @@ -# NVContainer Macro +# ContainerMacro A Swift macro for automatic container dependency injection in PHP Monitor. ## Usage ```swift -import NVContainer +import ContainerMacro -// Expose all Container services @ContainerAccess class MyClass { func doSomething() { @@ -16,15 +15,6 @@ class MyClass { warningManager.evaluateWarnings() } } - -// Or expose only specific services -@ContainerAccess(["shell", "favorites"]) -class AnotherClass { - func doSomething() { - shell.run("command") - favorites.add(site) - } -} ``` ## What it generates @@ -38,7 +28,7 @@ The `@ContainerAccess` macro automatically adds: When you add new services to `Container`, you must update the service list in: -**`Sources/NVContainerMacros/ContainerAccessMacro.swift`** (lines 14-18): +**`Sources/ContainerMacroPlugin/ContainerAccessMacro.swift`** (lines 14-18): ```swift let allContainerServices: [(name: String, type: String)] = [ diff --git a/packages/container-macro/Sources/ContainerMacro/Macros.swift b/packages/container-macro/Sources/ContainerMacro/Macros.swift new file mode 100644 index 00000000..adba7a46 --- /dev/null +++ b/packages/container-macro/Sources/ContainerMacro/Macros.swift @@ -0,0 +1,25 @@ +/// Automatically adds container dependency injection to a class. +/// +/// This macro generates: +/// - A public `container` property +/// - An `init(container:)` with a default parameter of `App.shared.container` (only if no init exists) +/// - Computed properties for all Container services +/// +/// Usage: +/// ```swift +/// import ContainerMacro +/// +/// @ContainerAccess +/// class MyClass { +/// func doSomething() { +/// shell.run("command") +/// favorites.add(site) +/// warningManager.evaluateWarnings() +/// } +/// } +/// ``` +@attached(member, names: named(container), named(init(container:)), arbitrary) +public macro ContainerAccess() = #externalMacro( + module: "ContainerMacroPlugin", + type: "ContainerAccessMacro" +) diff --git a/packages/container-macro/Sources/NVContainerMacros/ContainerAccessMacro.swift b/packages/container-macro/Sources/ContainerMacroPlugin/ContainerAccessMacro.swift similarity index 58% rename from packages/container-macro/Sources/NVContainerMacros/ContainerAccessMacro.swift rename to packages/container-macro/Sources/ContainerMacroPlugin/ContainerAccessMacro.swift index 1546a7b5..7286baad 100644 --- a/packages/container-macro/Sources/NVContainerMacros/ContainerAccessMacro.swift +++ b/packages/container-macro/Sources/ContainerMacroPlugin/ContainerAccessMacro.swift @@ -17,30 +17,6 @@ public struct ContainerAccessMacro: MemberMacro { ("warningManager", "WarningManager") ] - // Extract the service names from the macro arguments (if provided) - var requestedServices: [String]? = nil - if let argumentList = node.arguments?.as(LabeledExprListSyntax.self), - let firstArgument = argumentList.first, - let arrayExpr = firstArgument.expression.as(ArrayExprSyntax.self) { - requestedServices = arrayExpr.elements.compactMap { element -> String? in - guard let stringLiteral = element.expression.as(StringLiteralExprSyntax.self), - let segment = stringLiteral.segments.first?.as(StringSegmentSyntax.self) else { - return nil - } - return segment.content.text - } - } - - // Determine which services to expose - let servicesToExpose: [(name: String, type: String)] - if let requested = requestedServices, !requested.isEmpty { - // Only expose the requested services - servicesToExpose = allContainerServices.filter { requested.contains($0.name) } - } else { - // No arguments provided - expose ALL services - servicesToExpose = allContainerServices - } - // Check if the class already has an initializer let hasExistingInit = declaration.memberBlock.members.contains { member in if let initDecl = member.decl.as(InitializerDeclSyntax.self) { @@ -54,7 +30,7 @@ public struct ContainerAccessMacro: MemberMacro { // Add the container property members.append( """ - private let container: Container + public let container: Container """ ) @@ -70,7 +46,7 @@ public struct ContainerAccessMacro: MemberMacro { } // Add computed properties for each service - for service in servicesToExpose { + for service in allContainerServices { members.append( """ private var \(raw: service.name): \(raw: service.type) { @@ -85,7 +61,7 @@ public struct ContainerAccessMacro: MemberMacro { } @main -struct NVContainerMacrosPlugin: CompilerPlugin { +struct ContainerMacroPlugin: CompilerPlugin { let providingMacros: [Macro.Type] = [ ContainerAccessMacro.self, ] diff --git a/packages/container-macro/Sources/NVContainer/Macros.swift b/packages/container-macro/Sources/NVContainer/Macros.swift deleted file mode 100644 index 51a04f87..00000000 --- a/packages/container-macro/Sources/NVContainer/Macros.swift +++ /dev/null @@ -1,37 +0,0 @@ -/// Automatically adds container dependency injection to a class. -/// -/// This macro generates: -/// - A private `container` property -/// - An `init(container:)` with a default parameter of `App.shared.container` -/// - Computed properties for Container services -/// -/// Usage: -/// ```swift -/// import NVContainer -/// -/// // Expose specific services: -/// @ContainerAccess(["shell", "favorites"]) -/// class MyClass { -/// func doSomething() { -/// shell.run("command") -/// favorites.add(site) -/// } -/// } -/// -/// // Or expose ALL Container services by omitting the array: -/// @ContainerAccess -/// class AnotherClass { -/// func doSomething() { -/// shell.run("command") -/// favorites.add(site) -/// warningManager.evaluateWarnings() -/// } -/// } -/// ``` -/// -/// - Parameter services: Optional array of service names to expose. If omitted, all Container services are exposed. -@attached(member, names: named(container), named(init(container:)), arbitrary) -public macro ContainerAccess(_ services: [String] = []) = #externalMacro( - module: "NVContainerMacros", - type: "ContainerAccessMacro" -) diff --git a/packages/container-macro/Tests/NVContainerTests/ContainerizedMacroTests.swift b/packages/container-macro/Tests/ContainerMacroTests/ContainerizedMacroTests.swift similarity index 97% rename from packages/container-macro/Tests/NVContainerTests/ContainerizedMacroTests.swift rename to packages/container-macro/Tests/ContainerMacroTests/ContainerizedMacroTests.swift index 3a834f3c..822b5aee 100644 --- a/packages/container-macro/Tests/NVContainerTests/ContainerizedMacroTests.swift +++ b/packages/container-macro/Tests/ContainerMacroTests/ContainerizedMacroTests.swift @@ -2,8 +2,8 @@ import SwiftSyntaxMacros import SwiftSyntaxMacrosTestSupport import XCTest -#if canImport(NVContainerMacros) -import NVContainerMacros +#if canImport(ContainerMacroPlugin) +import ContainerMacroPlugin final class ContainerAccessMacroTests: XCTestCase { let testMacros: [String: Macro.Type] = [ diff --git a/phpmon/Common/Filesystem/ActiveFileSystem.swift b/phpmon/Common/Filesystem/ActiveFileSystem.swift index 1e163551..f1e55b82 100644 --- a/phpmon/Common/Filesystem/ActiveFileSystem.swift +++ b/phpmon/Common/Filesystem/ActiveFileSystem.swift @@ -8,19 +8,7 @@ import Foundation +@available(*, deprecated, message: "Use an injected `Container` instance to access this instead.") var FileSystem: FileSystemProtocol { - return ActiveFileSystem.shared -} - -class ActiveFileSystem { - static var shared: FileSystemProtocol = RealFileSystem() - - /** Note: Intermediate directories are not automatically inferred and have to be manually declared. */ - public static func useTestable(_ files: [String: FakeFile]) { - Self.shared = TestableFileSystem(files: files) - } - - public static func useSystem() { - Self.shared = RealFileSystem() - } + return App.shared.container.filesystem } diff --git a/phpmon/Common/Filesystem/RealFileSystem.swift b/phpmon/Common/Filesystem/RealFileSystem.swift index 20c5d5ea..4cfe0ac0 100644 --- a/phpmon/Common/Filesystem/RealFileSystem.swift +++ b/phpmon/Common/Filesystem/RealFileSystem.swift @@ -17,9 +17,7 @@ extension String { class RealFileSystem: FileSystemProtocol { var container: Container - init( - container: Container = App.shared.container, - ) { + init(container: Container) { self.container = container } diff --git a/phpmon/Common/PHP/ActivePhpInstallation.swift b/phpmon/Common/PHP/ActivePhpInstallation.swift index 240d0667..649c5a4d 100644 --- a/phpmon/Common/PHP/ActivePhpInstallation.swift +++ b/phpmon/Common/PHP/ActivePhpInstallation.swift @@ -6,7 +6,7 @@ // import Foundation -import NVContainer +import ContainerMacro /** An installed version of PHP, that was detected by scanning the `/opt/php@version/bin` directory. diff --git a/phpmon/Common/PHP/Switcher/InternalSwitcher.swift b/phpmon/Common/PHP/Switcher/InternalSwitcher.swift index 71a9714f..94989976 100644 --- a/phpmon/Common/PHP/Switcher/InternalSwitcher.swift +++ b/phpmon/Common/PHP/Switcher/InternalSwitcher.swift @@ -7,7 +7,7 @@ // import Foundation -import NVContainer +import ContainerMacro @ContainerAccess class InternalSwitcher: PhpSwitcher { diff --git a/phpmon/Common/Shell/RealShell.swift b/phpmon/Common/Shell/RealShell.swift index a5a77849..1befe4c3 100644 --- a/phpmon/Common/Shell/RealShell.swift +++ b/phpmon/Common/Shell/RealShell.swift @@ -8,9 +8,10 @@ import Foundation -class RealShell: ShellProtocol, ContainerAccess { +class RealShell: ShellProtocol { var container: Container - init(container: Container = App.shared.container) { + + init(container: Container) { self.container = container } @@ -214,7 +215,7 @@ class RealShell: ShellProtocol, ContainerAccess { } func reload() { - container.shell = RealShell() + container.shell = RealShell(container: container) } } diff --git a/phpmon/Common/Testables/TestableConfiguration.swift b/phpmon/Common/Testables/TestableConfiguration.swift index f4b2c454..f4772cd6 100644 --- a/phpmon/Common/Testables/TestableConfiguration.swift +++ b/phpmon/Common/Testables/TestableConfiguration.swift @@ -124,8 +124,6 @@ public struct TestableConfiguration: Codable { Log.separator() Log.info("Applying to container...") App.shared.container.overrideWith(config: self) - Log.info("Applying fake filesystem...") - ActiveFileSystem.useTestable(filesystem) Log.info("Applying fake commands...") ActiveCommand.useTestable(commandOutput) Log.info("Applying temporary preference overrides...") diff --git a/phpmon/Container.swift b/phpmon/Container.swift index e86c8e82..14c892bd 100644 --- a/phpmon/Container.swift +++ b/phpmon/Container.swift @@ -8,14 +8,16 @@ class Container { var shell: ShellProtocol! + var filesystem: FileSystemProtocol! + var favorites: Favorites! var warningManager: WarningManager! init() {} public func prepare() { - self.shell = RealShell() - // TODO: filesystem etc. + self.shell = RealShell(container: self) + self.filesystem = RealFileSystem(container: self) self.favorites = Favorites() self.warningManager = WarningManager(container: self) @@ -23,9 +25,6 @@ class Container { public func overrideWith(config: TestableConfiguration) { self.shell = TestableShell(expectations: config.shellOutput) + self.filesystem = TestableFileSystem(files: config.filesystem) } } - -protocol ContainerAccess { - var container: Container { get set } -} diff --git a/phpmon/Domain/App/AppDelegate.swift b/phpmon/Domain/App/AppDelegate.swift index 307cc722..89004c6b 100644 --- a/phpmon/Domain/App/AppDelegate.swift +++ b/phpmon/Domain/App/AppDelegate.swift @@ -60,10 +60,14 @@ class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDele When the application initializes, create all singletons. */ override init() { + // Prepare the container with the defaults + self.state = App.shared + self.state.container.prepare() + #if DEBUG logger.verbosity = .performance if let profile = CommandLine.arguments.first(where: { $0.matches(pattern: "--configuration:*") }) { - Self.initializeTestingProfile(profile.replacingOccurrences(of: "--configuration:", with: "")) + AppDelegate.initializeTestingProfile(profile.replacingOccurrences(of: "--configuration:", with: "")) } #endif @@ -77,7 +81,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDele Log.info("Extra CLI mode has been activated via --cli flag.") } - if FileSystem.fileExists("~/.config/phpmon/verbose") { + if state.container.filesystem.fileExists("~/.config/phpmon/verbose") { logger.verbosity = .cli Log.info("Extra CLI mode is on (`~/.config/phpmon/verbose` exists).") } @@ -89,9 +93,6 @@ class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDele Log.separator(as: .info) } - self.state = App.shared - self.state.container.prepare() - self.paths = Paths.shared self.valet = Valet.shared self.brew = Brew.shared diff --git a/phpmon/Domain/Integrations/Valet/Proxies/ValetProxy.swift b/phpmon/Domain/Integrations/Valet/Proxies/ValetProxy.swift index 75a73c2b..546e7466 100644 --- a/phpmon/Domain/Integrations/Valet/Proxies/ValetProxy.swift +++ b/phpmon/Domain/Integrations/Valet/Proxies/ValetProxy.swift @@ -8,7 +8,7 @@ import Foundation -class ValetProxy: ValetListable, ContainerAccess { +class ValetProxy: ValetListable { var domain: String var tld: String var target: String diff --git a/phpmon/Domain/Integrations/Valet/Sites/ValetSite.swift b/phpmon/Domain/Integrations/Valet/Sites/ValetSite.swift index 1a3948a9..6db96852 100644 --- a/phpmon/Domain/Integrations/Valet/Sites/ValetSite.swift +++ b/phpmon/Domain/Integrations/Valet/Sites/ValetSite.swift @@ -7,8 +7,10 @@ // import Foundation +import ContainerMacro -class ValetSite: ValetListable, ContainerAccess { +@ContainerAccess +class ValetSite: ValetListable { /// Name of the site. Does not include the TLD. var name: String @@ -65,8 +67,6 @@ class ValetSite: ValetListable, ContainerAccess { "site:domain:\(name).\(tld)|path:\(absolutePath)" } - var container: Container - init( container: Container = App.shared.container, name: String, diff --git a/phpmon/Domain/Watcher/App+ConfigWatch.swift b/phpmon/Domain/Watcher/App+ConfigWatch.swift index cc2098c7..4865d7e8 100644 --- a/phpmon/Domain/Watcher/App+ConfigWatch.swift +++ b/phpmon/Domain/Watcher/App+ConfigWatch.swift @@ -28,7 +28,7 @@ extension App { } func handlePhpConfigWatcher(forceReload: Bool = false) { - if ActiveFileSystem.shared is TestableFileSystem { + if container.filesystem is TestableFileSystem { Log.warn("Config watch manager is disabled when using testable filesystem.") return } diff --git a/phpmon/Modules/PHP Doctor/Data/WarningManager.swift b/phpmon/Modules/PHP Doctor/Data/WarningManager.swift index b48243aa..b48d4d42 100644 --- a/phpmon/Modules/PHP Doctor/Data/WarningManager.swift +++ b/phpmon/Modules/PHP Doctor/Data/WarningManager.swift @@ -8,10 +8,10 @@ import Foundation import Cocoa +import ContainerMacro -class WarningManager: ObservableObject, ContainerAccess { - var container: Container - +@ContainerAccess +class WarningManager: ObservableObject { init( container: Container = App.shared.container, fake: Bool = false