1
0
mirror of https://github.com/nicoverbruggen/phpmon.git synced 2026-03-27 14:30:08 +01:00

🚧 WIP: Add ContainerAccess macro

This commit is contained in:
2025-10-05 17:03:06 +02:00
parent 2e06b1a59e
commit 6227a6f2cc
9 changed files with 377 additions and 13 deletions

View File

@@ -3,10 +3,11 @@
archiveVersion = 1; archiveVersion = 1;
classes = { classes = {
}; };
objectVersion = 54; objectVersion = 60;
objects = { objects = {
/* Begin PBXBuildFile section */ /* Begin PBXBuildFile section */
030341272E92BD130031BE17 /* NVContainer in Frameworks */ = {isa = PBXBuildFile; productRef = 030341262E92BD130031BE17 /* NVContainer */; };
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 */; };
@@ -1295,6 +1296,7 @@
isa = PBXFrameworksBuildPhase; isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
030341272E92BD130031BE17 /* NVContainer in Frameworks */,
C47014FF2C46D57C0069AAE7 /* NVAlert in Frameworks */, C47014FF2C46D57C0069AAE7 /* NVAlert in Frameworks */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
@@ -2431,6 +2433,7 @@
name = "PHP Monitor"; name = "PHP Monitor";
packageProductDependencies = ( packageProductDependencies = (
C47014FE2C46D57C0069AAE7 /* NVAlert */, C47014FE2C46D57C0069AAE7 /* NVAlert */,
030341262E92BD130031BE17 /* NVContainer */,
); );
productName = phpmon; productName = phpmon;
productReference = C41C1B3322B0097F00E7CF16 /* PHP Monitor.app */; productReference = C41C1B3322B0097F00E7CF16 /* PHP Monitor.app */;
@@ -2548,6 +2551,7 @@
packageReferences = ( packageReferences = (
C47014FA2C46D31B0069AAE7 /* XCRemoteSwiftPackageReference "NVAppUpdater" */, C47014FA2C46D31B0069AAE7 /* XCRemoteSwiftPackageReference "NVAppUpdater" */,
C47014FD2C46D57C0069AAE7 /* XCRemoteSwiftPackageReference "NVAlert" */, C47014FD2C46D57C0069AAE7 /* XCRemoteSwiftPackageReference "NVAlert" */,
030341252E92BD130031BE17 /* XCLocalSwiftPackageReference "packages/container-macro" */,
); );
productRefGroup = C41C1B3422B0097F00E7CF16 /* Products */; productRefGroup = C41C1B3422B0097F00E7CF16 /* Products */;
projectDirPath = ""; projectDirPath = "";
@@ -4503,6 +4507,13 @@
}; };
/* End XCConfigurationList section */ /* End XCConfigurationList section */
/* Begin XCLocalSwiftPackageReference section */
030341252E92BD130031BE17 /* XCLocalSwiftPackageReference "packages/container-macro" */ = {
isa = XCLocalSwiftPackageReference;
relativePath = "packages/container-macro";
};
/* End XCLocalSwiftPackageReference section */
/* Begin XCRemoteSwiftPackageReference section */ /* Begin XCRemoteSwiftPackageReference section */
C47014FA2C46D31B0069AAE7 /* XCRemoteSwiftPackageReference "NVAppUpdater" */ = { C47014FA2C46D31B0069AAE7 /* XCRemoteSwiftPackageReference "NVAppUpdater" */ = {
isa = XCRemoteSwiftPackageReference; isa = XCRemoteSwiftPackageReference;
@@ -4523,6 +4534,10 @@
/* End XCRemoteSwiftPackageReference section */ /* End XCRemoteSwiftPackageReference section */
/* Begin XCSwiftPackageProductDependency section */ /* Begin XCSwiftPackageProductDependency section */
030341262E92BD130031BE17 /* NVContainer */ = {
isa = XCSwiftPackageProductDependency;
productName = NVContainer;
};
C47014FB2C46D31B0069AAE7 /* NVAppUpdater */ = { C47014FB2C46D31B0069AAE7 /* NVAppUpdater */ = {
isa = XCSwiftPackageProductDependency; isa = XCSwiftPackageProductDependency;
package = C47014FA2C46D31B0069AAE7 /* XCRemoteSwiftPackageReference "NVAppUpdater" */; package = C47014FA2C46D31B0069AAE7 /* XCRemoteSwiftPackageReference "NVAppUpdater" */;

View File

@@ -0,0 +1,14 @@
<?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>SchemeUserState</key>
<dict>
<key>NVContainer.xcscheme_^#shared#^_</key>
<dict>
<key>orderHint</key>
<integer>4</integer>
</dict>
</dict>
</dict>
</plist>

View File

@@ -0,0 +1,38 @@
// swift-tools-version: 5.9
import PackageDescription
import CompilerPluginSupport
let package = Package(
name: "NVContainer",
platforms: [.macOS(.v13)],
products: [
.library(
name: "NVContainer",
targets: ["NVContainer"]
),
],
dependencies: [
.package(url: "https://github.com/apple/swift-syntax.git", from: "509.0.0"),
],
targets: [
.macro(
name: "NVContainerMacros",
dependencies: [
.product(name: "SwiftSyntaxMacros", package: "swift-syntax"),
.product(name: "SwiftCompilerPlugin", package: "swift-syntax")
]
),
.target(
name: "NVContainer",
dependencies: ["NVContainerMacros"]
),
.testTarget(
name: "NVContainerTests",
dependencies: [
"NVContainerMacros",
.product(name: "SwiftSyntaxMacrosTestSupport", package: "swift-syntax"),
]
),
]
)

View File

@@ -0,0 +1,65 @@
# NVContainer Macro
A Swift macro for automatic container dependency injection in PHP Monitor.
## Usage
```swift
import NVContainer
// Expose all Container services
@ContainerAccess
class MyClass {
func doSomething() {
shell.run("command")
favorites.add(site)
warningManager.evaluateWarnings()
}
}
// Or expose only specific services
@ContainerAccess(["shell", "favorites"])
class AnotherClass {
func doSomething() {
shell.run("command")
favorites.add(site)
}
}
```
## What it generates
The `@ContainerAccess` macro automatically adds:
- A private `container: Container` property
- An `init(container:)` with default parameter `App.shared.container`
- Computed properties for each Container service you want to access
## Maintenance
When you add new services to `Container`, you must update the service list in:
**`Sources/NVContainerMacros/ContainerAccessMacro.swift`** (lines 14-18):
```swift
let allContainerServices: [(name: String, type: String)] = [
("shell", "ShellProtocol"),
("favorites", "Favorites"),
("warningManager", "WarningManager"),
// Add your new service here:
// ("myNewService", "MyServiceType"),
]
```
## Testing
Run tests with:
```bash
cd packages/container-macro
swift test
```
## Integration
The package is added as a local Swift Package in Xcode:
- File → Add Package Dependencies → Add Local...
- Select `packages/container-macro`

View File

@@ -0,0 +1,37 @@
/// 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

@@ -0,0 +1,92 @@
import SwiftCompilerPlugin
import SwiftSyntax
import SwiftSyntaxBuilder
import SwiftSyntaxMacros
public struct ContainerAccessMacro: MemberMacro {
public static func expansion(
of node: AttributeSyntax,
providingMembersOf declaration: some DeclGroupSyntax,
in context: some MacroExpansionContext
) throws -> [DeclSyntax] {
// Map of ALL Container properties to their types
// This should be kept in sync with the Container class
let allContainerServices: [(name: String, type: String)] = [
("shell", "ShellProtocol"),
("favorites", "Favorites"),
("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) {
return true
}
return false
}
var members: [DeclSyntax] = []
// Add the container property
members.append(
"""
private let container: Container
"""
)
// Only add the initializer if one doesn't already exist
if !hasExistingInit {
members.append(
"""
init(container: Container = App.shared.container) {
self.container = container
}
"""
)
}
// Add computed properties for each service
for service in servicesToExpose {
members.append(
"""
private var \(raw: service.name): \(raw: service.type) {
return container.\(raw: service.name)
}
"""
)
}
return members
}
}
@main
struct NVContainerMacrosPlugin: CompilerPlugin {
let providingMacros: [Macro.Type] = [
ContainerAccessMacro.self,
]
}

View File

@@ -0,0 +1,106 @@
import SwiftSyntaxMacros
import SwiftSyntaxMacrosTestSupport
import XCTest
#if canImport(NVContainerMacros)
import NVContainerMacros
final class ContainerAccessMacroTests: XCTestCase {
let testMacros: [String: Macro.Type] = [
"ContainerAccess": ContainerAccessMacro.self,
]
func testContainerAccessWithSpecificServices() throws {
assertMacroExpansion(
"""
@ContainerAccess(["shell"])
class InternalSwitcher {
func doSomething() {
print("Hello")
}
}
""",
expandedSource: """
class InternalSwitcher {
func doSomething() {
print("Hello")
}
private let container: Container
init(container: Container = App.shared.container) {
self.container = container
}
private var shell: ShellProtocol {
return container.shell
}
}
""",
macros: testMacros
)
}
func testContainerAccessWithMultipleServices() throws {
assertMacroExpansion(
"""
@ContainerAccess(["shell", "favorites"])
class MyClass {
}
""",
expandedSource: """
class MyClass {
private let container: Container
init(container: Container = App.shared.container) {
self.container = container
}
private var shell: ShellProtocol {
return container.shell
}
private var favorites: Favorites {
return container.favorites
}
}
""",
macros: testMacros
)
}
func testContainerAccessWithAllServices() throws {
assertMacroExpansion(
"""
@ContainerAccess
class MyClass {
}
""",
expandedSource: """
class MyClass {
private let container: Container
init(container: Container = App.shared.container) {
self.container = container
}
private var shell: ShellProtocol {
return container.shell
}
private var favorites: Favorites {
return container.favorites
}
private var warningManager: WarningManager {
return container.warningManager
}
}
""",
macros: testMacros
)
}
}
#endif

View File

@@ -6,6 +6,7 @@
// //
import Foundation import Foundation
import NVContainer
/** /**
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.
@@ -16,6 +17,8 @@ import Foundation
- Note: Each installation has a separate version number. - Note: Each installation has a separate version number.
Using `version.short` is advisable if you want to interact with Homebrew. Using `version.short` is advisable if you want to interact with Homebrew.
*/ */
@ContainerAccess
class ActivePhpInstallation { class ActivePhpInstallation {
var version: VersionNumber! var version: VersionNumber!
var limits: Limits! var limits: Limits!
@@ -45,7 +48,9 @@ class ActivePhpInstallation {
return ActivePhpInstallation() return ActivePhpInstallation()
} }
init() { init(container: Container = App.shared.container) {
self.container = container
// Show information about the current version // Show information about the current version
do { do {
try determineVersion() try determineVersion()
@@ -69,7 +74,7 @@ class ActivePhpInstallation {
post_max_size: getByteCount(key: "post_max_size") post_max_size: getByteCount(key: "post_max_size")
) )
let paths = Shell let paths = shell
.sync("\(Paths.php) --ini | grep -E -o '(/[^ ]+\\.ini)'").out .sync("\(Paths.php) --ini | grep -E -o '(/[^ ]+\\.ini)'").out
.split(separator: "\n") .split(separator: "\n")
.map { String($0) } .map { String($0) }

View File

@@ -7,18 +7,10 @@
// //
import Foundation import Foundation
import NVContainer
@ContainerAccess
class InternalSwitcher: PhpSwitcher { class InternalSwitcher: PhpSwitcher {
private var container: Container
init(container: Container = App.shared.container) {
self.container = container
}
var shell: ShellProtocol {
return container.shell
}
/** /**
Switching to a new PHP version involves: Switching to a new PHP version involves:
- unlinking the current version - unlinking the current version