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:
@@ -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" */;
|
||||||
|
|||||||
@@ -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>
|
||||||
38
packages/container-macro/Package.swift
Normal file
38
packages/container-macro/Package.swift
Normal 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"),
|
||||||
|
]
|
||||||
|
),
|
||||||
|
]
|
||||||
|
)
|
||||||
65
packages/container-macro/README.md
Normal file
65
packages/container-macro/README.md
Normal 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`
|
||||||
37
packages/container-macro/Sources/NVContainer/Macros.swift
Normal file
37
packages/container-macro/Sources/NVContainer/Macros.swift
Normal 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"
|
||||||
|
)
|
||||||
@@ -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,
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -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
|
||||||
@@ -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) }
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user