1
0
mirror of https://github.com/nicoverbruggen/phpmon.git synced 2025-12-21 11:10:08 +01:00

🚧 WIP: Prepare container

This commit is contained in:
2025-10-05 17:30:35 +02:00
parent 6227a6f2cc
commit c62e3a9905
20 changed files with 81 additions and 137 deletions

View File

@@ -7,7 +7,7 @@
objects = { objects = {
/* Begin PBXBuildFile section */ /* 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 */; }; 0309E6672B0D4B2F002AC007 /* BrewExtensionsObservable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0309E6662B0D4B2F002AC007 /* BrewExtensionsObservable.swift */; };
031E2B692B1525A7007C29E1 /* BrewPhpExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 031E2B682B1525A7007C29E1 /* BrewPhpExtension.swift */; }; 031E2B692B1525A7007C29E1 /* BrewPhpExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 031E2B682B1525A7007C29E1 /* BrewPhpExtension.swift */; };
031E2B6A2B1525A7007C29E1 /* 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; isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
030341272E92BD130031BE17 /* NVContainer in Frameworks */, 0303412A2E92C3560031BE17 /* ContainerMacro in Frameworks */,
C47014FF2C46D57C0069AAE7 /* NVAlert in Frameworks */, C47014FF2C46D57C0069AAE7 /* NVAlert in Frameworks */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
@@ -2433,7 +2433,7 @@
name = "PHP Monitor"; name = "PHP Monitor";
packageProductDependencies = ( packageProductDependencies = (
C47014FE2C46D57C0069AAE7 /* NVAlert */, C47014FE2C46D57C0069AAE7 /* NVAlert */,
030341262E92BD130031BE17 /* NVContainer */, 030341292E92C3560031BE17 /* ContainerMacro */,
); );
productName = phpmon; productName = phpmon;
productReference = C41C1B3322B0097F00E7CF16 /* PHP Monitor.app */; productReference = C41C1B3322B0097F00E7CF16 /* PHP Monitor.app */;
@@ -2551,7 +2551,7 @@
packageReferences = ( packageReferences = (
C47014FA2C46D31B0069AAE7 /* XCRemoteSwiftPackageReference "NVAppUpdater" */, C47014FA2C46D31B0069AAE7 /* XCRemoteSwiftPackageReference "NVAppUpdater" */,
C47014FD2C46D57C0069AAE7 /* XCRemoteSwiftPackageReference "NVAlert" */, C47014FD2C46D57C0069AAE7 /* XCRemoteSwiftPackageReference "NVAlert" */,
030341252E92BD130031BE17 /* XCLocalSwiftPackageReference "packages/container-macro" */, 030341282E92C3560031BE17 /* XCLocalSwiftPackageReference "packages/container-macro" */,
); );
productRefGroup = C41C1B3422B0097F00E7CF16 /* Products */; productRefGroup = C41C1B3422B0097F00E7CF16 /* Products */;
projectDirPath = ""; projectDirPath = "";
@@ -4508,7 +4508,7 @@
/* End XCConfigurationList section */ /* End XCConfigurationList section */
/* Begin XCLocalSwiftPackageReference section */ /* Begin XCLocalSwiftPackageReference section */
030341252E92BD130031BE17 /* XCLocalSwiftPackageReference "packages/container-macro" */ = { 030341282E92C3560031BE17 /* XCLocalSwiftPackageReference "packages/container-macro" */ = {
isa = XCLocalSwiftPackageReference; isa = XCLocalSwiftPackageReference;
relativePath = "packages/container-macro"; relativePath = "packages/container-macro";
}; };
@@ -4534,9 +4534,9 @@
/* End XCRemoteSwiftPackageReference section */ /* End XCRemoteSwiftPackageReference section */
/* Begin XCSwiftPackageProductDependency section */ /* Begin XCSwiftPackageProductDependency section */
030341262E92BD130031BE17 /* NVContainer */ = { 030341292E92C3560031BE17 /* ContainerMacro */ = {
isa = XCSwiftPackageProductDependency; isa = XCSwiftPackageProductDependency;
productName = NVContainer; productName = ContainerMacro;
}; };
C47014FB2C46D31B0069AAE7 /* NVAppUpdater */ = { C47014FB2C46D31B0069AAE7 /* NVAppUpdater */ = {
isa = XCSwiftPackageProductDependency; isa = XCSwiftPackageProductDependency;

View File

@@ -4,6 +4,11 @@
<dict> <dict>
<key>SchemeUserState</key> <key>SchemeUserState</key>
<dict> <dict>
<key>ContainerMacro.xcscheme_^#shared#^_</key>
<dict>
<key>orderHint</key>
<integer>4</integer>
</dict>
<key>NVContainer.xcscheme_^#shared#^_</key> <key>NVContainer.xcscheme_^#shared#^_</key>
<dict> <dict>
<key>orderHint</key> <key>orderHint</key>

View File

@@ -4,12 +4,12 @@ import PackageDescription
import CompilerPluginSupport import CompilerPluginSupport
let package = Package( let package = Package(
name: "NVContainer", name: "ContainerMacro",
platforms: [.macOS(.v13)], platforms: [.macOS(.v13)],
products: [ products: [
.library( .library(
name: "NVContainer", name: "ContainerMacro",
targets: ["NVContainer"] targets: ["ContainerMacro"]
), ),
], ],
dependencies: [ dependencies: [
@@ -17,20 +17,20 @@ let package = Package(
], ],
targets: [ targets: [
.macro( .macro(
name: "NVContainerMacros", name: "ContainerMacroPlugin",
dependencies: [ dependencies: [
.product(name: "SwiftSyntaxMacros", package: "swift-syntax"), .product(name: "SwiftSyntaxMacros", package: "swift-syntax"),
.product(name: "SwiftCompilerPlugin", package: "swift-syntax") .product(name: "SwiftCompilerPlugin", package: "swift-syntax")
] ]
), ),
.target( .target(
name: "NVContainer", name: "ContainerMacro",
dependencies: ["NVContainerMacros"] dependencies: ["ContainerMacroPlugin"]
), ),
.testTarget( .testTarget(
name: "NVContainerTests", name: "ContainerMacroTests",
dependencies: [ dependencies: [
"NVContainerMacros", "ContainerMacroPlugin",
.product(name: "SwiftSyntaxMacrosTestSupport", package: "swift-syntax"), .product(name: "SwiftSyntaxMacrosTestSupport", package: "swift-syntax"),
] ]
), ),

View File

@@ -1,13 +1,12 @@
# NVContainer Macro # ContainerMacro
A Swift macro for automatic container dependency injection in PHP Monitor. A Swift macro for automatic container dependency injection in PHP Monitor.
## Usage ## Usage
```swift ```swift
import NVContainer import ContainerMacro
// Expose all Container services
@ContainerAccess @ContainerAccess
class MyClass { class MyClass {
func doSomething() { func doSomething() {
@@ -16,15 +15,6 @@ class MyClass {
warningManager.evaluateWarnings() warningManager.evaluateWarnings()
} }
} }
// Or expose only specific services
@ContainerAccess(["shell", "favorites"])
class AnotherClass {
func doSomething() {
shell.run("command")
favorites.add(site)
}
}
``` ```
## What it generates ## 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: 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 ```swift
let allContainerServices: [(name: String, type: String)] = [ let allContainerServices: [(name: String, type: String)] = [

View File

@@ -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"
)

View File

@@ -17,30 +17,6 @@ public struct ContainerAccessMacro: MemberMacro {
("warningManager", "WarningManager") ("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 // Check if the class already has an initializer
let hasExistingInit = declaration.memberBlock.members.contains { member in let hasExistingInit = declaration.memberBlock.members.contains { member in
if let initDecl = member.decl.as(InitializerDeclSyntax.self) { if let initDecl = member.decl.as(InitializerDeclSyntax.self) {
@@ -54,7 +30,7 @@ public struct ContainerAccessMacro: MemberMacro {
// Add the container property // Add the container property
members.append( members.append(
""" """
private let container: Container public let container: Container
""" """
) )
@@ -70,7 +46,7 @@ public struct ContainerAccessMacro: MemberMacro {
} }
// Add computed properties for each service // Add computed properties for each service
for service in servicesToExpose { for service in allContainerServices {
members.append( members.append(
""" """
private var \(raw: service.name): \(raw: service.type) { private var \(raw: service.name): \(raw: service.type) {
@@ -85,7 +61,7 @@ public struct ContainerAccessMacro: MemberMacro {
} }
@main @main
struct NVContainerMacrosPlugin: CompilerPlugin { struct ContainerMacroPlugin: CompilerPlugin {
let providingMacros: [Macro.Type] = [ let providingMacros: [Macro.Type] = [
ContainerAccessMacro.self, ContainerAccessMacro.self,
] ]

View File

@@ -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"
)

View File

@@ -2,8 +2,8 @@ import SwiftSyntaxMacros
import SwiftSyntaxMacrosTestSupport import SwiftSyntaxMacrosTestSupport
import XCTest import XCTest
#if canImport(NVContainerMacros) #if canImport(ContainerMacroPlugin)
import NVContainerMacros import ContainerMacroPlugin
final class ContainerAccessMacroTests: XCTestCase { final class ContainerAccessMacroTests: XCTestCase {
let testMacros: [String: Macro.Type] = [ let testMacros: [String: Macro.Type] = [

View File

@@ -8,19 +8,7 @@
import Foundation import Foundation
@available(*, deprecated, message: "Use an injected `Container` instance to access this instead.")
var FileSystem: FileSystemProtocol { var FileSystem: FileSystemProtocol {
return ActiveFileSystem.shared return App.shared.container.filesystem
}
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()
}
} }

View File

@@ -17,9 +17,7 @@ extension String {
class RealFileSystem: FileSystemProtocol { class RealFileSystem: FileSystemProtocol {
var container: Container var container: Container
init( init(container: Container) {
container: Container = App.shared.container,
) {
self.container = container self.container = container
} }

View File

@@ -6,7 +6,7 @@
// //
import Foundation import Foundation
import NVContainer import ContainerMacro
/** /**
An installed version of PHP, that was detected by scanning the `/opt/php@version/bin` directory. An installed version of PHP, that was detected by scanning the `/opt/php@version/bin` directory.

View File

@@ -7,7 +7,7 @@
// //
import Foundation import Foundation
import NVContainer import ContainerMacro
@ContainerAccess @ContainerAccess
class InternalSwitcher: PhpSwitcher { class InternalSwitcher: PhpSwitcher {

View File

@@ -8,9 +8,10 @@
import Foundation import Foundation
class RealShell: ShellProtocol, ContainerAccess { class RealShell: ShellProtocol {
var container: Container var container: Container
init(container: Container = App.shared.container) {
init(container: Container) {
self.container = container self.container = container
} }
@@ -214,7 +215,7 @@ class RealShell: ShellProtocol, ContainerAccess {
} }
func reload() { func reload() {
container.shell = RealShell() container.shell = RealShell(container: container)
} }
} }

View File

@@ -124,8 +124,6 @@ public struct TestableConfiguration: Codable {
Log.separator() Log.separator()
Log.info("Applying to container...") Log.info("Applying to container...")
App.shared.container.overrideWith(config: self) App.shared.container.overrideWith(config: self)
Log.info("Applying fake filesystem...")
ActiveFileSystem.useTestable(filesystem)
Log.info("Applying fake commands...") Log.info("Applying fake commands...")
ActiveCommand.useTestable(commandOutput) ActiveCommand.useTestable(commandOutput)
Log.info("Applying temporary preference overrides...") Log.info("Applying temporary preference overrides...")

View File

@@ -8,14 +8,16 @@
class Container { class Container {
var shell: ShellProtocol! var shell: ShellProtocol!
var filesystem: FileSystemProtocol!
var favorites: Favorites! var favorites: Favorites!
var warningManager: WarningManager! var warningManager: WarningManager!
init() {} init() {}
public func prepare() { public func prepare() {
self.shell = RealShell() self.shell = RealShell(container: self)
// TODO: filesystem etc. self.filesystem = RealFileSystem(container: self)
self.favorites = Favorites() self.favorites = Favorites()
self.warningManager = WarningManager(container: self) self.warningManager = WarningManager(container: self)
@@ -23,9 +25,6 @@ class Container {
public func overrideWith(config: TestableConfiguration) { public func overrideWith(config: TestableConfiguration) {
self.shell = TestableShell(expectations: config.shellOutput) self.shell = TestableShell(expectations: config.shellOutput)
self.filesystem = TestableFileSystem(files: config.filesystem)
} }
} }
protocol ContainerAccess {
var container: Container { get set }
}

View File

@@ -60,10 +60,14 @@ class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDele
When the application initializes, create all singletons. When the application initializes, create all singletons.
*/ */
override init() { override init() {
// Prepare the container with the defaults
self.state = App.shared
self.state.container.prepare()
#if DEBUG #if DEBUG
logger.verbosity = .performance logger.verbosity = .performance
if let profile = CommandLine.arguments.first(where: { $0.matches(pattern: "--configuration:*") }) { 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 #endif
@@ -77,7 +81,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDele
Log.info("Extra CLI mode has been activated via --cli flag.") 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 logger.verbosity = .cli
Log.info("Extra CLI mode is on (`~/.config/phpmon/verbose` exists).") Log.info("Extra CLI mode is on (`~/.config/phpmon/verbose` exists).")
} }
@@ -89,9 +93,6 @@ class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDele
Log.separator(as: .info) Log.separator(as: .info)
} }
self.state = App.shared
self.state.container.prepare()
self.paths = Paths.shared self.paths = Paths.shared
self.valet = Valet.shared self.valet = Valet.shared
self.brew = Brew.shared self.brew = Brew.shared

View File

@@ -8,7 +8,7 @@
import Foundation import Foundation
class ValetProxy: ValetListable, ContainerAccess { class ValetProxy: ValetListable {
var domain: String var domain: String
var tld: String var tld: String
var target: String var target: String

View File

@@ -7,8 +7,10 @@
// //
import Foundation import Foundation
import ContainerMacro
class ValetSite: ValetListable, ContainerAccess { @ContainerAccess
class ValetSite: ValetListable {
/// Name of the site. Does not include the TLD. /// Name of the site. Does not include the TLD.
var name: String var name: String
@@ -65,8 +67,6 @@ class ValetSite: ValetListable, ContainerAccess {
"site:domain:\(name).\(tld)|path:\(absolutePath)" "site:domain:\(name).\(tld)|path:\(absolutePath)"
} }
var container: Container
init( init(
container: Container = App.shared.container, container: Container = App.shared.container,
name: String, name: String,

View File

@@ -28,7 +28,7 @@ extension App {
} }
func handlePhpConfigWatcher(forceReload: Bool = false) { 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.") Log.warn("Config watch manager is disabled when using testable filesystem.")
return return
} }

View File

@@ -8,10 +8,10 @@
import Foundation import Foundation
import Cocoa import Cocoa
import ContainerMacro
class WarningManager: ObservableObject, ContainerAccess { @ContainerAccess
var container: Container class WarningManager: ObservableObject {
init( init(
container: Container = App.shared.container, container: Container = App.shared.container,
fake: Bool = false fake: Bool = false