= {
+ guard let version = Valet.shared.version else {
+ return Constants.DetectedPhpVersions
+ }
- var supportedVersions = versions.intersection(supportedByValet)
+ return Constants.ValetSupportedPhpVersionMatrix[version.major] ?? []
+ }()
+
+ var supportedVersions = Valet.installed ? versions.intersection(supportedByValet) : versions
// Make sure the aliased version is detected
// The user may have `php` installed, but not e.g. `php@8.0`
@@ -167,6 +207,10 @@ class PhpEnv {
return output
}
+ /**
+ Returns a list of `VersionNumber` instances based on the available PHP versions
+ that are valid to switch to for a given constraint.
+ */
public func validVersions(for constraint: String) -> [VersionNumber] {
constraint.split(separator: "|").flatMap {
return PhpVersionNumberCollection
@@ -179,7 +223,12 @@ class PhpEnv {
Validates whether the currently running version matches the provided version.
*/
public func validate(_ version: String) -> Bool {
- if self.currentInstall.version.short == version {
+ guard let install = PhpEnvironments.phpInstall else {
+ Log.info("It appears as if no PHP installation is currently active.")
+ return false
+ }
+
+ if install.version.short == version {
Log.info("Switching to version \(version) seems to have succeeded. Validation passed.")
Log.info("Keeping track that this is the new version!")
Stats.persistCurrentGlobalPhpVersion(version: version)
@@ -195,7 +244,11 @@ class PhpEnv {
You can then use the configuration file instance to change values.
*/
public func getConfigFile(forKey key: String) -> PhpConfigurationFile? {
- return PhpEnv.phpInstall.iniFiles
+ guard let install = PhpEnvironments.phpInstall else {
+ return nil
+ }
+
+ return install.iniFiles
.reversed()
.first(where: { $0.has(key: key) })
}
diff --git a/phpmon/Common/PHP/PHP Version/PhpHelper.swift b/phpmon/Common/PHP/PHP Version/PhpHelper.swift
index 75aa912..be41b47 100644
--- a/phpmon/Common/PHP/PHP Version/PhpHelper.swift
+++ b/phpmon/Common/PHP/PHP Version/PhpHelper.swift
@@ -28,10 +28,12 @@ class PhpHelper {
Task { // Create the appropriate folders and check if the files exist
do {
if !FileSystem.directoryExists("~/.config/phpmon/bin") {
- try FileSystem.createDirectory(
- "~/.config/phpmon/bin",
- withIntermediateDirectories: true
- )
+ Task { @MainActor in
+ try FileSystem.createDirectory(
+ "~/.config/phpmon/bin",
+ withIntermediateDirectories: true
+ )
+ }
}
if FileSystem.fileExists(destination) {
@@ -48,21 +50,14 @@ class PhpHelper {
.resolvingSymlinksInPath().path
// The contents of the script!
- let script = """
- #!/bin/zsh
- # \(keyPhrase)
- # It reflects the location of PHP \(version)'s binaries on your system.
- # Usage: . pm\(dotless)
- [[ $ZSH_EVAL_CONTEXT =~ :file$ ]] \\
- && echo "PHP Monitor has enabled this terminal to use PHP \(version)." \\
- || echo "You must run '. pm\(dotless)' (or 'source pm\(dotless)') instead!";
- export PATH=\(path):$PATH
- """
+ let script = script(path, keyPhrase, version, dotless)
- try FileSystem.writeAtomicallyToFile(destination, content: script)
+ Task { @MainActor in
+ try FileSystem.writeAtomicallyToFile(destination, content: script)
- if !FileSystem.isExecutableFile(destination) {
- try FileSystem.makeExecutable(destination)
+ if !FileSystem.isExecutableFile(destination) {
+ try FileSystem.makeExecutable(destination)
+ }
}
// Create a symlink if the folder is not in the PATH
@@ -83,6 +78,24 @@ class PhpHelper {
}
}
+ private static func script(
+ _ path: String,
+ _ keyPhrase: String,
+ _ version: String,
+ _ dotless: String
+ ) -> String {
+ return """
+ #!/bin/zsh
+ # \(keyPhrase)
+ # It reflects the location of PHP \(version)'s binaries on your system.
+ # Usage: . pm\(dotless)
+ [[ $ZSH_EVAL_CONTEXT =~ :file$ ]] \\
+ && echo "PHP Monitor has enabled this terminal to use PHP \(version)." \\
+ || echo "You must run '. pm\(dotless)' (or 'source pm\(dotless)') instead!";
+ export PATH=\(path):$PATH
+ """
+ }
+
private static func createSymlink(_ dotless: String) async {
let source = "\(Paths.homePath)/.config/phpmon/bin/pm\(dotless)"
let destination = "/usr/local/bin/pm\(dotless)"
diff --git a/phpmon/Common/PHP/PhpInstallation.swift b/phpmon/Common/PHP/PhpInstallation.swift
index 4b19881..5658747 100644
--- a/phpmon/Common/PHP/PhpInstallation.swift
+++ b/phpmon/Common/PHP/PhpInstallation.swift
@@ -12,13 +12,17 @@ class PhpInstallation {
var versionNumber: VersionNumber
+ var isHealthy: Bool = true
+
/**
- In order to determine details about a PHP installation, we’ll simply run `php-config --version`
- in the relevant directory.
+ In order to determine details about a PHP installation,
+ we’ll simply run `php-config --version` in the relevant directory.
*/
init(_ version: String) {
-
let phpConfigExecutablePath = "\(Paths.optPath)/php@\(version)/bin/php-config"
+
+ let phpExecutablePath = "\(Paths.optPath)/php@\(version)/bin/php"
+
self.versionNumber = VersionNumber.make(from: version)!
if FileSystem.fileExists(phpConfigExecutablePath) {
@@ -32,6 +36,21 @@ class PhpInstallation {
// If so, the app SHOULD crash, so that the users report what's up.
self.versionNumber = try! VersionNumber.parse(longVersionString)
}
- }
+ if FileSystem.fileExists(phpExecutablePath) {
+ let testCommand = Command.execute(
+ path: phpExecutablePath,
+ arguments: ["-v"],
+ trimNewlines: false,
+ withStandardError: true
+ ).trimmingCharacters(in: .whitespacesAndNewlines)
+
+ // If the "dyld: Library not loaded" issue pops up, we have an unhealthy PHP installation
+ // and we will need to reinstall this version of PHP via Homebrew.
+ if testCommand.contains("Library not loaded") && testCommand.contains("dyld") {
+ self.isHealthy = false
+ Log.err("The PHP installation of \(self.versionNumber.short) is not healthy!")
+ }
+ }
+ }
}
diff --git a/phpmon/Common/PHP/Switcher/InternalSwitcher+Valet.swift b/phpmon/Common/PHP/Switcher/InternalSwitcher+Valet.swift
new file mode 100644
index 0000000..83de877
--- /dev/null
+++ b/phpmon/Common/PHP/Switcher/InternalSwitcher+Valet.swift
@@ -0,0 +1,134 @@
+//
+// InternalSwitcher+Valet.swift
+// PHP Monitor
+//
+// Created by Nico Verbruggen on 14/03/2023.
+// Copyright © 2023 Nico Verbruggen. All rights reserved.
+//
+
+import Foundation
+
+extension InternalSwitcher {
+
+ typealias FixApplied = Bool
+
+ public func ensureValetConfigurationIsValidForPhpVersion(_ version: String) async -> FixApplied {
+ // Early exit if Valet is not installed
+ if !Valet.installed {
+ assertionFailure("Cannot ensure that Valet configuration is valid if Valet is not installed.")
+ return false
+ }
+
+ let corrections = [
+ await self.disableDefaultPhpFpmPool(version),
+ await self.ensureConfigurationFilesExist(version)
+ ]
+
+ return corrections.contains(true)
+ }
+
+ // MARK: - PHP FPM pool
+
+ public func disableDefaultPhpFpmPool(_ version: String) async -> FixApplied {
+ let pool = "\(Paths.etcPath)/php/\(version)/php-fpm.d/www.conf"
+
+ if FileSystem.fileExists(pool) {
+ Log.info("A default `www.conf` file was found in the php-fpm.d directory for PHP \(version).")
+ let existing = "\(Paths.etcPath)/php/\(version)/php-fpm.d/www.conf"
+ let new = "\(Paths.etcPath)/php/\(version)/php-fpm.d/www.conf.disabled-by-phpmon"
+ do {
+ if FileSystem.fileExists(new) {
+ Log.info("A moved `www.conf.disabled-by-phpmon` file was found for PHP \(version), "
+ + "cleaning up so the newer `www.conf` can be moved again.")
+ try FileSystem.remove(new)
+ }
+ try FileSystem.move(from: existing, to: new)
+ Log.info("Success: A default `www.conf` file was disabled for PHP \(version).")
+ return true
+ } catch {
+ Log.err(error)
+ return false
+ }
+ }
+
+ return false
+ }
+
+ func getExpectedConfigurationFiles(for version: String) -> [ExpectedConfigurationFile] {
+ return [
+ ExpectedConfigurationFile(
+ destination: "/php-fpm.d/valet-fpm.conf",
+ source: "/cli/stubs/etc-phpfpm-valet.conf",
+ replacements: [
+ "VALET_USER": Paths.whoami,
+ "VALET_HOME_PATH": "~/.config/valet".replacingTildeWithHomeDirectory,
+ "valet.sock": "valet\(version.replacingOccurrences(of: ".", with: "")).sock"
+ ],
+ applies: { Valet.shared.version!.major > 2 }
+ ),
+ ExpectedConfigurationFile(
+ destination: "/conf.d/error_log.ini",
+ source: "/cli/stubs/etc-phpfpm-error_log.ini",
+ replacements: [
+ "VALET_USER": Paths.whoami,
+ "VALET_HOME_PATH": "~/.config/valet".replacingTildeWithHomeDirectory
+ ],
+ applies: { return true }
+ ),
+ ExpectedConfigurationFile(
+ destination: "/conf.d/php-memory-limits.ini",
+ source: "/cli/stubs/php-memory-limits.ini",
+ replacements: [:],
+ applies: { return true }
+ )
+ ]
+ }
+
+ func ensureConfigurationFilesExist(_ version: String) async -> FixApplied {
+ let files = self.getExpectedConfigurationFiles(for: version)
+
+ // For each of the files, attempt to fix anything that is wrong
+ let outcomes = files.map { file in
+ let configFileExists = FileSystem.fileExists("\(Paths.etcPath)/php/\(version)/" + file.destination)
+
+ if configFileExists {
+ return false
+ }
+
+ Log.info("Config file `\(file.destination)` does not exist, will attempt to automatically fix!")
+
+ if !file.applies() {
+ return false
+ }
+
+ do {
+ var contents = try FileSystem.getStringFromFile("~/.composer/vendor/laravel/valet" + file.source)
+
+ for (original, replacement) in file.replacements {
+ contents = contents.replacingOccurrences(of: original, with: replacement)
+ }
+
+ try FileSystem.writeAtomicallyToFile(
+ "\(Paths.etcPath)/php/\(version)" + file.destination,
+ content: contents
+ )
+ } catch {
+ Log.err("Automatically fixing \(file.destination) did not work.")
+ return false
+ }
+
+ return true
+ }
+
+ // If any fixes were applied, return true
+ return outcomes.contains(true)
+ }
+
+}
+
+public struct ExpectedConfigurationFile {
+ let destination: String
+ let source: String
+ let replacements: [String: String]
+ let applies: () -> Bool
+}
diff --git a/phpmon/Common/PHP/Switcher/InternalSwitcher.swift b/phpmon/Common/PHP/Switcher/InternalSwitcher.swift
index 5b465aa..ca5087d 100644
--- a/phpmon/Common/PHP/Switcher/InternalSwitcher.swift
+++ b/phpmon/Common/PHP/Switcher/InternalSwitcher.swift
@@ -25,10 +25,9 @@ class InternalSwitcher: PhpSwitcher {
let versions = getVersionsToBeHandled(version)
await withTaskGroup(of: String.self, body: { group in
- for available in PhpEnv.shared.availablePhpVersions {
+ for available in PhpEnvironments.shared.availablePhpVersions {
group.addTask {
- await self.disableDefaultPhpFpmPool(available)
- await self.stopPhpVersion(available)
+ await self.unlinkAndStopPhpVersion(available)
return available
}
}
@@ -42,12 +41,19 @@ class InternalSwitcher: PhpSwitcher {
Log.info("Linking the new version \(version)!")
for formula in versions {
+ if Valet.installed {
+ Log.info("Ensuring that the Valet configuration is valid...")
+ _ = await self.ensureValetConfigurationIsValidForPhpVersion(formula)
+ }
+
Log.info("Will start PHP \(version)... (primary: \(version == formula))")
- await self.startPhpVersion(formula, primary: (version == formula))
+ await self.linkAndStartPhpVersion(formula, primary: (version == formula))
}
- Log.info("Restarting nginx, just to be sure!")
- await brew("services restart nginx", sudo: true)
+ if Valet.installed {
+ Log.info("Restarting nginx, just to be sure!")
+ await brew("services restart nginx", sudo: true)
+ }
Log.info("The new version(s) have been linked!")
})
@@ -69,56 +75,36 @@ class InternalSwitcher: PhpSwitcher {
return versions
}
- func requiresDisablingOfDefaultPhpFpmPool(_ version: String) -> Bool {
- let pool = "\(Paths.etcPath)/php/\(version)/php-fpm.d/www.conf"
- return FileSystem.fileExists(pool)
+ func unlinkAndStopPhpVersion(_ version: String) async {
+ let formula = (version == PhpEnvironments.brewPhpAlias) ? "php" : "php@\(version)"
+ await brew("unlink \(formula)")
+
+ if Valet.installed {
+ await brew("services stop \(formula)", sudo: true)
+ Log.info("Unlinked and stopped services for \(formula)")
+ } else {
+ Log.info("Unlinked \(formula)")
+ }
}
- func disableDefaultPhpFpmPool(_ version: String) async {
- let pool = "\(Paths.etcPath)/php/\(version)/php-fpm.d/www.conf"
- if FileSystem.fileExists(pool) {
- Log.info("A default `www.conf` file was found in the php-fpm.d directory for PHP \(version).")
- let existing = "\(Paths.etcPath)/php/\(version)/php-fpm.d/www.conf"
- let new = "\(Paths.etcPath)/php/\(version)/php-fpm.d/www.conf.disabled-by-phpmon"
- do {
- if FileSystem.fileExists(new) {
- Log.info("A moved `www.conf.disabled-by-phpmon` file was found for PHP \(version), "
- + "cleaning up so the newer `www.conf` can be moved again.")
- try FileSystem.remove(new)
- }
- try FileSystem.move(from: existing, to: new)
- Log.info("Success: A default `www.conf` file was disabled for PHP \(version).")
- } catch {
- Log.err(error)
+ func linkAndStartPhpVersion(_ version: String, primary: Bool) async {
+ let formula = (version == PhpEnvironments.brewPhpAlias) ? "php" : "php@\(version)"
+
+ if primary {
+ Log.info("\(formula) is the primary formula, linking...")
+ await brew("link \(formula) --overwrite --force")
+ } else {
+ Log.info("\(formula) is an isolated PHP version, not linking!")
+ }
+
+ if Valet.installed {
+ await brew("services start \(formula)", sudo: true)
+
+ if Valet.enabled(feature: .isolatedSites) && primary {
+ let socketVersion = version.replacingOccurrences(of: ".", with: "")
+ await Shell.quiet("ln -sF ~/.config/valet/valet\(socketVersion).sock ~/.config/valet/valet.sock")
+ Log.info("Symlinked new socket version (valet\(socketVersion).sock → valet.sock).")
}
}
}
-
- func stopPhpVersion(_ version: String) async {
- let formula = (version == PhpEnv.brewPhpAlias) ? "php" : "php@\(version)"
- await brew("unlink \(formula)")
- await brew("services stop \(formula)", sudo: true)
- Log.info("Unlinked and stopped services for \(formula)")
- }
-
- func startPhpVersion(_ version: String, primary: Bool) async {
- let formula = (version == PhpEnv.brewPhpAlias) ? "php" : "php@\(version)"
-
- if primary {
- Log.info("\(formula) is the primary formula, linking and starting services...")
- await brew("link \(formula) --overwrite --force")
- } else {
- Log.info("\(formula) is an isolated PHP version, starting services only...")
- }
-
- await brew("services start \(formula)", sudo: true)
-
- if Valet.enabled(feature: .isolatedSites) && primary {
- let socketVersion = version.replacingOccurrences(of: ".", with: "")
- await Shell.quiet("ln -sF ~/.config/valet/valet\(socketVersion).sock ~/.config/valet/valet.sock")
- Log.info("Symlinked new socket version (valet\(socketVersion).sock → valet.sock).")
- }
-
- }
-
}
diff --git a/phpmon/Common/Testables/TestableCommand.swift b/phpmon/Common/Testables/TestableCommand.swift
index b7e8503..a3679db 100644
--- a/phpmon/Common/Testables/TestableCommand.swift
+++ b/phpmon/Common/Testables/TestableCommand.swift
@@ -19,6 +19,10 @@ class TestableCommand: CommandProtocol {
self.execute(path: path, arguments: arguments, trimNewlines: false)
}
+ public func execute(path: String, arguments: [String], trimNewlines: Bool, withStandardError: Bool) -> String {
+ self.execute(path: path, arguments: arguments, trimNewlines: trimNewlines)
+ }
+
public func execute(path: String, arguments: [String], trimNewlines: Bool) -> String {
let concatenatedCommand = "\(path) \(arguments.joined(separator: " "))"
assert(commands.keys.contains(concatenatedCommand), "Command `\(concatenatedCommand)` not found")
diff --git a/phpmon/Common/Testables/TestableConfiguration.swift b/phpmon/Common/Testables/TestableConfiguration.swift
index 2dbb33e..bdcb206 100644
--- a/phpmon/Common/Testables/TestableConfiguration.swift
+++ b/phpmon/Common/Testables/TestableConfiguration.swift
@@ -15,10 +15,93 @@ public struct TestableConfiguration: Codable {
var commandOutput: [String: String]
var preferenceOverrides: [PreferenceName: Bool]
+ init(
+ architecture: String,
+ filesystem: [String: FakeFile],
+ shellOutput: [String: BatchFakeShellOutput],
+ commandOutput: [String: String],
+ preferenceOverrides: [PreferenceName: Bool],
+ phpVersions: [VersionNumber]
+ ) {
+ self.architecture = architecture
+ self.filesystem = filesystem
+ self.shellOutput = shellOutput
+ self.commandOutput = commandOutput
+ self.preferenceOverrides = preferenceOverrides
+
+ phpVersions.enumerated().forEach { (index, version) in
+ self.addPhpVersion(version, primary: index == 0)
+ }
+ }
+
+ private enum CodingKeys: String, CodingKey {
+ case architecture, filesystem, shellOutput, commandOutput, preferenceOverrides
+ }
+
+ // MARK: Add PHP versions
+
+ private var primaryPhpVersion: VersionNumber?
+ private var secondaryPhpVersions: [VersionNumber] = []
+
+ mutating func addPhpVersion(_ version: VersionNumber, primary: Bool) {
+ if primary {
+ if primaryPhpVersion != nil {
+ fatalError("You cannot add multiple primary PHP versions to a testable configuration!")
+ }
+ primaryPhpVersion = version
+ } else {
+ self.secondaryPhpVersions.append(version)
+ }
+
+ self.filesystem = self.filesystem.merging([
+ "/opt/homebrew/opt/php@\(version.short)/bin/php"
+ : .fake(.symlink, "/opt/homebrew/Cellar/php/\(version.long)/bin/php"),
+ "/opt/homebrew/Cellar/php/\(version.long)/bin/php"
+ : .fake(.binary),
+ "/opt/homebrew/Cellar/php/\(version.long)/bin/php-config"
+ : .fake(.binary),
+ "/opt/homebrew/etc/php/\(version.short)/php-fpm.d/www.conf"
+ : .fake(.text),
+ "/opt/homebrew/etc/php/\(version.short)/php-fpm.d/valet-fpm.conf"
+ : .fake(.text),
+ "/opt/homebrew/etc/php/\(version.short)/php.ini"
+ : .fake(.text),
+ "/opt/homebrew/etc/php/\(version.short)/conf.d/php-memory-limits.ini"
+ : .fake(.text)
+ ]) { (_, new) in new }
+
+ if primary {
+ self.shellOutput["ls /opt/homebrew/opt | grep php"]
+ = .instant("php")
+ self.filesystem["/opt/homebrew/opt/php"]
+ = .fake(.symlink, "/opt/homebrew/Cellar/php/\(version.long)")
+ self.filesystem["/opt/homebrew/opt/php/bin/php"]
+ = .fake(.symlink, "/opt/homebrew/Cellar/php/\(version.long)/bin/php")
+ self.filesystem["/opt/homebrew/bin/php"]
+ = .fake(.symlink, "/opt/homebrew/Cellar/php/\(version.long)/bin/php")
+ self.filesystem["/opt/homebrew/bin/php-config"]
+ = .fake(.symlink, "/opt/homebrew/Cellar/php/\(version.long)/bin/php-config")
+ self.commandOutput["/opt/homebrew/bin/php-config --version"]
+ = version.long
+ self.commandOutput["/opt/homebrew/bin/php -r echo php_ini_scanned_files();"] =
+ """
+ /opt/homebrew/etc/php/\(version.short)/conf.d/php-memory-limits.ini,
+ """
+ } else {
+ self.shellOutput["ls /opt/homebrew/opt | grep php@"] =
+ BatchFakeShellOutput.instant(
+ self.secondaryPhpVersions
+ .map { "php@\($0.short)" }
+ .joined(separator: "\n")
+ )
+ }
+ }
+
+ // MARK: Interactions
+
func apply() {
Log.separator()
Log.info("USING TESTABLE CONFIGURATION...")
- Homebrew.fake = true
Log.separator()
Log.info("Applying fake shell...")
ActiveShell.useTestable(shellOutput)
@@ -26,18 +109,23 @@ public struct TestableConfiguration: Codable {
ActiveFileSystem.useTestable(filesystem)
Log.info("Applying fake commands...")
ActiveCommand.useTestable(commandOutput)
- Log.info("Applying fake scanner...")
- ValetScanner.useFake()
- Log.info("Applying fake services manager...")
- ServicesManager.useFake()
- Log.info("Applying fake Valet domain interactor...")
- ValetInteractor.useFake()
Log.info("Applying temporary preference overrides...")
preferenceOverrides.forEach { (key: PreferenceName, value: Any?) in
Preferences.shared.cachedPreferences[key] = value
}
+
+ if Valet.shared.installed {
+ Log.info("Applying fake scanner...")
+ ValetScanner.useFake()
+ Log.info("Applying fake services manager...")
+ ServicesManager.useFake()
+ Log.info("Applying fake Valet domain interactor...")
+ ValetInteractor.useFake()
+ }
}
+ // MARK: Persist and load
+
func toJson(pretty: Bool = false) -> String {
let data = try! JSONEncoder().encode(self)
diff --git a/phpmon/Credits.html b/phpmon/Credits.html
index 078adb3..4f45299 100644
--- a/phpmon/Credits.html
+++ b/phpmon/Credits.html
@@ -13,11 +13,13 @@
- Do you enjoy using the app? Leave a star on GitHub!
+ Do you enjoy using the app? Is it helping you save time? Leave a star on GitHub!
Having issues? Consult the FAQ section, I did my best to ensure everything is documented.
Want to support further development of PHP Monitor? You can financially support the continued development of this app.
- Get the latest on Mastodon. Give me a follow on Mastodon to learn about what's brewing and when new updates drop.
-
+ Get the latest on Twitter or Mastodon. Give me a follow on Twitter or Mastodon to learn about what's brewing and when new updates drop.
+ Special thanks to all current and past sponsors of PHP Monitor, who have helped to make further development of the app possible.
+ Made possible by these GitHub Sponsors: @abdusfauzi, @abicons, @adrolli, @andresayej, @andyunleashed, @anzacorp, @argirisp, @AshPowell, @aurawindsurfing, @awsmug, @barrycarton, @BertvanHoekelen, @calebporzio, @caseyalee, @cgreuling, @cjcox17, @Diewy, @drfraker, @driftingly, @duellsy, @edalzell, @EYOND, @faithfm, @frankmichel, @gwleuverink, @hopkins385, @intrepidws, @jacksleight, @JacobBennett, @jasonvarga, @jeromegamez, @jimmyaldape, @jimmysawczuk, @joetannenbaum, @jolora, @joshuablum, @jpeinelt, @jreviews, @JustSteveKing, @Kajvdh, @KFoobar, @Laravel-Backpack, @leganz, @martinleveille, @mathiasonea, @matthewmnewman, @mcastillo1030, @megabubbletea, @mennen-online, @mike-healy, @mostafakram, @mpociot, @MrMicky-FR, @MrMooky, @murdercode, @nckrtl, @nhedger, @ninjaparade, @ozanuzer, @pepatel, @philbraun, @pickuse2013, @pk-informatics, @Plytas, @rderimay, @rickyjohnston, @rico, @RobertBoes, @runofthemill, @SahinU88, @sdebacker, @sdevore, @shadracnicholas, @simonhamp, @SRWieZ, @stefanbauer, @StriveMedia, @swilla, @Tailcode-Studio, @theutz, @ThomasEnssner, @tillkruss, @timothyrowan, @ttnppedr, @vincent-tarrit, @WheresMarco, @xPand4B, @xuandung38, @yeslandi89, @zackkatz, @zacksmash, @zaherg.
(Some names have been omitted due to their sponsorships being private. Thank you all!)
+