コンテンツにスキップ

4.0へのアップグレード

このガイドでは、既存のVapor 3.xプロジェクトを4.xにアップグレードする方法を説明します。このガイドでは、Vaporの公式パッケージに加え、よく使用されるプロバイダーについても網羅します。不足している内容があれば、Vaporのチームチャットで質問するのがおすすめです。IssuesやPull Requestも歓迎です。

依存関係

Vapor 4を使用するには、Xcode 11.4およびmacOS 10.15以上が必要です。

ドキュメントのインストールセクションで依存関係のインストールについて説明しています。

Package.swift

Vapor 4へのアップグレードの最初のステップは、パッケージの依存関係を更新することです。以下は更新されたPackage.swiftファイルの例です。更新されたテンプレートPackage.swiftも確認できます。

-// swift-tools-version:4.0
+// swift-tools-version:5.2
 import PackageDescription

 let package = Package(
     name: "api",
+    platforms: [
+        .macOS(.v10_15),
+    ],
     dependencies: [
-        .package(url: "https://github.com/vapor/fluent-postgresql.git", from: "1.0.0"),
+        .package(url: "https://github.com/vapor/fluent.git", from: "4.0.0"),
+        .package(url: "https://github.com/vapor/fluent-postgres-driver.git", from: "2.0.0"),
-        .package(url: "https://github.com/vapor/jwt.git", from: "3.0.0"),
+        .package(url: "https://github.com/vapor/jwt.git", from: "4.0.0"),
-        .package(url: "https://github.com/vapor/vapor.git", from: "3.0.0"),
+        .package(url: "https://github.com/vapor/vapor.git", from: "4.3.0"),
     ],
     targets: [
         .target(name: "App", dependencies: [
-            "FluentPostgreSQL", 
+            .product(name: "Fluent", package: "fluent"),
+            .product(name: "FluentPostgresDriver", package: "fluent-postgres-driver"),
-            "Vapor", 
+            .product(name: "Vapor", package: "vapor"),
-            "JWT", 
+            .product(name: "JWT", package: "jwt"),
         ]),
-        .target(name: "Run", dependencies: ["App"]),
-        .testTarget(name: "AppTests", dependencies: ["App"])
+        .target(name: "Run", dependencies: [
+            .target(name: "App"),
+        ]),
+        .testTarget(name: "AppTests", dependencies: [
+            .target(name: "App"),
+        ])
     ]
 )

Vapor 4向けにアップグレードされたすべてのパッケージは、メジャーバージョン番号が1つ増加します。

Warning

Vapor 4の一部のパッケージはまだ正式にリリースされていないため、-rcプレリリース識別子が使用されています。

廃止されたパッケージ

いくつかのVapor 3パッケージは非推奨となりました:

  • vapor/auth: Vaporに含まれるようになりました。
  • vapor/core: いくつかのモジュールに吸収されました。
  • vapor/crypto: SwiftCryptoに置き換えられました(Vaporに含まれています)。
  • vapor/multipart: Vaporに含まれるようになりました。
  • vapor/url-encoded-form: Vaporに含まれるようになりました。
  • vapor-community/vapor-ext: Vaporに含まれるようになりました。
  • vapor-community/pagination: Fluentの一部になりました。
  • IBM-Swift/LoggerAPI: SwiftLogに置き換えられました。

Fluent依存関係

vapor/fluentは、依存関係リストとターゲットに個別の依存関係として追加する必要があります。すべてのデータベース固有のパッケージには、vapor/fluentへの依存関係を明確にするために-driverが付けられています。

- .package(url: "https://github.com/vapor/fluent-postgresql.git", from: "1.0.0"),
+ .package(url: "https://github.com/vapor/fluent.git", from: "4.0.0"),
+ .package(url: "https://github.com/vapor/fluent-postgres-driver.git", from: "2.0.0"),

プラットフォーム

Vaporのパッケージマニフェストは、macOS 10.15以上を明示的にサポートするようになりました。これにより、あなたのパッケージもプラットフォームサポートを指定する必要があります。

+ platforms: [
+     .macOS(.v10_15),
+ ],

将来的にVaporは追加のサポートプラットフォームを追加する可能性があります。あなたのパッケージは、バージョン番号がVaporの最小バージョン要件以上である限り、これらのプラットフォームの任意のサブセットをサポートできます。

Xcode

Vapor 4はXcode 11のネイティブSPMサポートを利用しています。これにより、.xcodeprojファイルを生成する必要がなくなりました。Xcodeでプロジェクトのフォルダーを開くと、自動的にSPMが認識され、依存関係が取得されます。

vapor xcodeまたはopen Package.swiftを使用して、Xcodeでプロジェクトをネイティブに開くことができます。

Package.swiftを更新したら、Xcodeを閉じてルートディレクトリから以下のフォルダーを削除する必要があるかもしれません:

  • Package.resolved
  • .build
  • .swiftpm
  • *.xcodeproj

更新されたパッケージが正常に解決されると、コンパイラエラーが表示されるはずです--おそらくかなりの数です。心配しないでください!修正方法をお見せします。

Run

最初に行うべきことは、Runモジュールのmain.swiftファイルを新しい形式に更新することです。

import App
import Vapor

var env = try Environment.detect()
try LoggingSystem.bootstrap(from: &env)
let app = Application(env)
defer { app.shutdown() }
try configure(app)
try app.run()

main.swiftファイルの内容はAppモジュールのapp.swiftを置き換えるため、そのファイルは削除できます。

App

基本的なAppモジュール構造の更新方法を見てみましょう。

configure.swift

configureメソッドはApplicationのインスタンスを受け入れるように変更する必要があります。

- public func configure(_ config: inout Config, _ env: inout Environment, _ services: inout Services) throws
+ public func configure(_ app: Application) throws

以下は更新されたconfigureメソッドの例です。

import Fluent
import FluentSQLiteDriver
import Vapor

// アプリケーションが初期化される前に呼び出されます。
public func configure(_ app: Application) throws {
    // `Public/`ディレクトリからファイルを提供
    // app.middleware.use(FileMiddleware(publicDirectory: app.directory.publicDirectory))
    // SQLiteデータベースを設定
    app.databases.use(.sqlite(.file("db.sqlite")), as: .sqlite)

    // マイグレーションを設定
    app.migrations.add(CreateTodo())

    try routes(app)
}

ルーティング、ミドルウェア、Fluentなどの設定に関する構文の変更は以下で説明します。

boot.swift

bootの内容は、アプリケーションインスタンスを受け入れるようになったため、configureメソッドに配置できます。

routes.swift

routesメソッドはApplicationのインスタンスを受け入れるように変更する必要があります。

- public func routes(_ router: Router, _ container: Container) throws
+ public func routes(_ app: Application) throws

ルーティング構文の変更に関する詳細は以下で説明します。

サービス

Vapor 4のサービスAPIは、サービスの発見と使用を容易にするために簡素化されました。サービスはApplicationRequestのメソッドとプロパティとして公開されるようになり、コンパイラがそれらの使用を支援できます。

これをよりよく理解するために、いくつかの例を見てみましょう。

// サーバーのデフォルトポートを8281に変更
- services.register { container -> NIOServerConfig in
-     return .default(port: 8281)
- }
+ app.http.server.configuration.port = 8281

NIOServerConfigをサービスに登録する代わりに、サーバー設定はApplicationの単純なプロパティとして公開され、オーバーライドできます。

// CORSミドルウェアを登録
let corsConfiguration = CORSMiddleware.Configuration(
    allowedOrigin: .all,
    allowedMethods: [.POST, .GET, .PATCH, .PUT, .DELETE, .OPTIONS]
)
let corsMiddleware = CORSMiddleware(configuration: corsConfiguration)
- var middlewares = MiddlewareConfig() // _空の_ミドルウェア設定を作成
- middlewares.use(corsMiddleware)
- services.register(middlewares)
+ app.middleware.use(corsMiddleware)

MiddlewareConfigを作成してサービスに登録する代わりに、ミドルウェアはApplicationのプロパティとして公開され、追加できます。

// ルートハンドラー内でリクエストを行う。
- try req.make(Client.self).get("https://vapor.codes")
+ req.client.get("https://vapor.codes")

Applicationと同様に、Requestもサービスを単純なプロパティとメソッドとして公開します。ルートクロージャ内では、常にRequest固有のサービスを使用する必要があります。

この新しいサービスパターンは、Vapor 3のContainerService、およびConfigタイプを置き換えます。

プロバイダー

サードパーティパッケージを設定するためにプロバイダーは必要なくなりました。各パッケージは代わりにApplicationとRequestを新しいプロパティとメソッドで拡張して設定します。

Vapor 4でLeafがどのように設定されるか見てみましょう。

// ビューレンダリングにLeafを使用。
- try services.register(LeafProvider())
- config.prefer(LeafRenderer.self, for: ViewRenderer.self)
+ app.views.use(.leaf)

Leafを設定するには、app.leafプロパティを使用します。

// Leafビューキャッシュを無効化。
- services.register { container -> LeafConfig in
-     return LeafConfig(tags: ..., viewsDir: ..., shouldCache: false)
- }
+ app.leaf.cache.isEnabled = false

環境

現在の環境(production、developmentなど)はapp.environmentでアクセスできます。

カスタムサービス

Vapor 3でServiceプロトコルに準拠し、コンテナに登録されていたカスタムサービスは、ApplicationまたはRequestの拡張として表現できるようになりました。

struct MyAPI {
    let client: Client
    func foo() { ... }
}
- extension MyAPI: Service { }
- services.register { container -> MyAPI in
-     return try MyAPI(client: container.make())
- }
+ extension Request {
+     var myAPI: MyAPI { 
+         .init(client: self.client)
+     }
+ }

このサービスはmakeの代わりに拡張を使用してアクセスできます。

- try req.make(MyAPI.self).foo()
+ req.myAPI.foo()

カスタムプロバイダー

ほとんどのカスタムサービスは、前のセクションで示したように拡張を使用して実装できます。ただし、一部の高度なプロバイダーは、アプリケーションのライフサイクルにフックしたり、保存されたプロパティを使用したりする必要があるかもしれません。

Applicationの新しいLifecycleヘルパーを使用してライフサイクルハンドラーを登録できます。

struct PrintHello: LifecycleHandler {
    func willBoot(_ app: Application) throws {
        print("Hello!")
    }
}

app.lifecycle.use(PrintHello())

Applicationに値を保存するには、新しいStorageヘルパーを使用できます。

struct MyNumber: StorageKey {
    typealias Value = Int
}
app.storage[MyNumber.self] = 5
print(app.storage[MyNumber.self]) // 5

app.storageへのアクセスは、簡潔なAPIを作成するために設定可能な計算プロパティでラップできます。

extension Application {
    var myNumber: Int? {
        get { self.storage[MyNumber.self] }
        set { self.storage[MyNumber.self] = newValue }
    }
}

app.myNumber = 42
print(app.myNumber) // 42

NIO

Vapor 4はSwiftNIOの非同期APIを直接公開するようになり、mapflatMapのようなメソッドをオーバーロードしたり、EventLoopFutureのようなタイプをエイリアスしたりしようとしなくなりました。Vapor 3は、SwiftNIOが存在する前にリリースされた初期ベータバージョンとの下位互換性のためにオーバーロードとエイリアスを提供していました。これらは、他のSwiftNIO互換パッケージとの混乱を減らし、SwiftNIOのベストプラクティスの推奨事項により良く従うために削除されました。

非同期の名前変更

最も明白な変更は、EventLoopFutureFutureタイプエイリアスが削除されたことです。これは検索と置換で簡単に修正できます。

さらに、NIOはVapor 3が追加したto:ラベルをサポートしていません。Swift 5.2の改善された型推論により、to:はそれほど必要ではなくなりました。

- futureA.map(to: String.self) { ... }
+ futureA.map { ... }

newPromiseのようにnewで始まるメソッドは、Swiftスタイルに合わせてmakeに変更されました。

- let promise = eventLoop.newPromise(String.self)
+ let promise = eventLoop.makePromise(of: String.self)

catchMapは利用できなくなりましたが、NIOのmapErrorflatMapErrorThrowingのようなメソッドが代わりに機能します。

複数のフューチャーを組み合わせるためのVapor 3のグローバルflatMapメソッドは利用できなくなりました。これは、NIOのandメソッドを使用して多くのフューチャーを組み合わせることで置き換えることができます。

- flatMap(futureA, futureB) { a, b in 
+ futureA.and(futureB).flatMap { (a, b) in
    // aとbで何かを行う。
}

ByteBuffer

以前はDataを使用していた多くのメソッドとプロパティは、NIOのByteBufferを使用するようになりました。このタイプは、より強力で高性能なバイトストレージタイプです。APIの詳細については、SwiftNIOのByteBufferドキュメントを参照してください。

ByteBufferDataに戻すには:

Data(buffer.readableBytesView)

map / flatMapのスロー

最も難しい変更は、mapflatMapがもはやスローできないことです。mapには(やや紛らわしいことに)flatMapThrowingという名前のスローバージョンがあります。しかし、flatMapにはスローする対応物がありません。これにより、いくつかの非同期コードの再構築が必要になる場合があります。

スローしないmapは引き続き正常に動作するはずです。

// スローしないmap。
futureA.map { a in
    return b
}

スローするmapはflatMapThrowingに名前を変更する必要があります。

- futureA.map { a in
+ futureA.flatMapThrowing { a in
    if ... {
        throw SomeError()
    } else {
        return b
    }
}

スローしないflat-mapは引き続き正常に動作するはずです。

// スローしないflatMap。
futureA.flatMap { a in
    return futureB
}

flat-map内でエラーをスローする代わりに、フューチャーエラーを返します。エラーが他のスローメソッドから発生する場合、エラーはdo / catchでキャッチしてフューチャーとして返すことができます。

// キャッチしたエラーをフューチャーとして返す。
futureA.flatMap { a in
    do {
        try doSomething()
        return futureB
    } catch {
        return eventLoop.makeFailedFuture(error)
    }
}

スローメソッド呼び出しは、flatMapThrowingにリファクタリングし、タプルを使用してチェーンすることもできます。

// タプルチェーンを使用してflatMapThrowingにリファクタリングされたスローメソッド。
futureA.flatMapThrowing { a in
    try (a, doSomeThing())
}.flatMap { (a, result) in
    // resultはdoSomethingの値です。
    return futureB
}

ルーティング

ルートはApplicationに直接登録されるようになりました。

app.get("hello") { req in
    return "Hello, world"
}

これは、ルーターをサービスに登録する必要がなくなったことを意味します。routesメソッドにアプリケーションを渡してルートを追加し始めるだけです。RoutesBuilderで利用可能なすべてのメソッドはApplicationで利用可能です。

同期コンテンツ

リクエストコンテンツのデコードは同期的になりました。

let payload = try req.content.decode(MyPayload.self)
print(payload) // MyPayload

この動作は、.streamボディコレクション戦略を使用してルートを登録することでオーバーライドできます。

app.on(.POST, "streaming", body: .stream) { req in
    // リクエストボディは非同期になりました。
    req.body.collect().map { buffer in
        HTTPStatus.ok
    }
}

カンマ区切りのパス

一貫性のため、パスはカンマ区切りである必要があり、/を含んではいけません。

- router.get("v1/users/", "posts", "/comments") { req in 
+ app.get("v1", "users", "posts", "comments") { req in
    // リクエストを処理。
}

ルートパラメータ

Parameterプロトコルは、明示的に名前付きパラメータを支持して削除されました。これにより、重複するパラメータの問題と、ミドルウェアとルートハンドラーでのパラメータの順不同の取得が防止されます。

- router.get("planets", String.parameter) { req in 
-     let id = req.parameters.next(String.self)
+ app.get("planets", ":id") { req in
+     let id = req.parameters.get("id")
      return "Planet id: \(id)"
  }

モデルを使用したルートパラメータの使用については、Fluentセクションで説明します。

ミドルウェア

MiddlewareConfigMiddlewareConfigurationに名前が変更され、Applicationのプロパティになりました。app.middlewareを使用してアプリにミドルウェアを追加できます。

let corsMiddleware = CORSMiddleware(configuration: ...)
- var middleware = MiddlewareConfig()
- middleware.use(corsMiddleware)
+ app.middleware.use(corsMiddleware)
- services.register(middlewares)

ミドルウェアはタイプ名で登録できなくなりました。登録する前にミドルウェアを初期化してください。

- middleware.use(ErrorMiddleware.self)
+ app.middleware.use(ErrorMiddleware.default(environment: app.environment))

すべてのデフォルトミドルウェアを削除するには、app.middlewareを空の設定に設定します:

app.middleware = .init()

Fluent

FluentのAPIはデータベースに依存しなくなりました。Fluentだけをインポートできます。

- import FluentMySQL
+ import Fluent

モデル

すべてのモデルはModelプロトコルを使用し、クラスである必要があります。

- struct Planet: MySQLModel {
+ final class Planet: Model {

すべてのフィールドは@Fieldまたは@OptionalFieldプロパティラッパーを使用して宣言されます。

+ @Field(key: "name")
var name: String

+ @OptionalField(key: "age")
var age: Int?

モデルのIDは@IDプロパティラッパーを使用して定義する必要があります。

+ @ID(key: .id)
var id: UUID?

カスタムキーまたはタイプの識別子を使用するモデルは@ID(custom:)を使用する必要があります。

すべてのモデルは、テーブルまたはコレクション名を静的に定義する必要があります。

final class Planet: Model {
+   static let schema = "Planet"    
}

すべてのモデルには空のイニシャライザが必要です。すべてのプロパティがプロパティラッパーを使用するため、これは空にできます。

final class Planet: Model {
+   init() { }
}

モデルのsaveupdatecreateは、モデルインスタンスを返さなくなりました。

- model.save(on: ...)
+ model.save(on: ...).map { model }

モデルはルートパスコンポーネントとして使用できなくなりました。代わりにfindreq.parameters.getを使用してください。

- try req.parameters.next(ServerSize.self)
+ ServerSize.find(req.parameters.get("size"), on: req.db)
+     .unwrap(or: Abort(.notFound))

Model.IDModel.IDValueに名前が変更されました。

モデルのタイムスタンプは@Timestampプロパティラッパーを使用して宣言されるようになりました。

- static var createdAtKey: TimestampKey? = \.createdAt
+ @Timestamp(key: "createdAt", on: .create)
var createdAt: Date?

リレーション

リレーションはプロパティラッパーを使用して定義されるようになりました。

親リレーションは@Parentプロパティラッパーを使用し、フィールドプロパティを内部に含みます。@Parentに渡されるキーは、データベース内の識別子を格納するフィールドの名前である必要があります。

- var serverID: Int
- var server: Parent<App, Server> { 
-    parent(\.serverID) 
- }
+ @Parent(key: "serverID") 
+ var server: Server

子リレーションは、関連する@Parentへのキーパスを持つ@Childrenプロパティラッパーを使用します。

- var apps: Children<Server, App> { 
-     children(\.serverID) 
- }
+ @Children(for: \.$server) 
+ var apps: [App]

兄弟リレーションは、ピボットモデルへのキーパスを持つ@Siblingsプロパティラッパーを使用します。

- var users: Siblings<Company, User, Permission> {
-     siblings()
- }
+ @Siblings(through: Permission.self, from: \.$user, to: \.$company) 
+ var companies: [Company]

ピボットは、2つの@Parentリレーションと0個以上の追加フィールドを持つModelに準拠する通常のモデルになりました。

クエリ

データベースコンテキストは、ルートハンドラー内でreq.dbを介してアクセスされるようになりました。

- Planet.query(on: req)
+ Planet.query(on: req.db)

DatabaseConnectableDatabaseに名前が変更されました。

フィールドへのキーパスは、フィールド値の代わりにプロパティラッパーを指定するために$で始まるようになりました。

- filter(\.foo == ...) 
+ filter(\.$foo == ...)

マイグレーション

モデルはリフレクションベースの自動マイグレーションをサポートしなくなりました。すべてのマイグレーションは手動で記述する必要があります。

- extension Planet: Migration { }
+ struct CreatePlanet: Migration {
+     ...
+}

マイグレーションは文字列型になり、モデルから切り離されてMigrationプロトコルを使用するようになりました。

- struct CreateGalaxy: <#Database#>Migration {
+ struct CreateGalaxy: Migration {

prepareおよびrevertメソッドは静的ではなくなりました。

- static func prepare(on conn: <#Database#>Connection) -> Future<Void> {
+ func prepare(on database: Database) -> EventLoopFuture<Void> 

スキーマビルダーの作成は、Databaseのインスタンスメソッドを介して行われます。

- <#Database#>Database.create(Galaxy.self, on: conn) { builder in
-    // ビルダーを使用。
- }
+ var builder = database.schema("Galaxy")
+ // ビルダーを使用。

createupdate、およびdeleteメソッドは、クエリビルダーと同様にスキーマビルダーで呼び出されるようになりました。

フィールド定義は文字列型になり、次のパターンに従います:

field(<name>, <type>, <constraints>)

以下の例を参照してください。

- builder.field(for: \.name)
+ builder.field("name", .string, .required)

スキーマビルドはクエリビルダーのようにチェーンできるようになりました。

database.schema("Galaxy")
    .id()
    .field("name", .string, .required)
    .create()

Fluent設定

DatabasesConfigapp.databasesに置き換えられました。

try app.databases.use(.postgres(url: "postgres://..."), as: .psql)

MigrationsConfigapp.migrationsに置き換えられました。

app.migrations.use(CreatePlanet(), on: .psql)

リポジトリ

Vapor 4でのサービスの動作方法が変更されたため、データベースリポジトリの実装方法も変更されました。UserRepositoryのようなプロトコルは引き続き必要ですが、そのプロトコルに準拠するfinal classを作成する代わりに、structを作成する必要があります。

- final class DatabaseUserRepository: UserRepository {
+ struct DatabaseUserRepository: UserRepository {
      let database: Database
      func all() -> EventLoopFuture<[User]> {
          return User.query(on: database).all()
      }
  }

また、Vapor 4にはもはや存在しないため、ServiceTypeへの準拠も削除する必要があります。

- extension DatabaseUserRepository {
-     static let serviceSupports: [Any.Type] = [Athlete.self]
-     static func makeService(for worker: Container) throws -> Self {
-         return .init()
-     }
- }

代わりにUserRepositoryFactoryを作成する必要があります:

struct UserRepositoryFactory {
    var make: ((Request) -> UserRepository)?
    mutating func use(_ make: @escaping ((Request) -> UserRepository)) {
        self.make = make
    }
}

このファクトリーはRequestに対してUserRepositoryを返す責任があります。

次のステップは、ファクトリーを指定するためにApplicationに拡張を追加することです:

extension Application {
    private struct UserRepositoryKey: StorageKey { 
        typealias Value = UserRepositoryFactory 
    }

    var users: UserRepositoryFactory {
        get {
            self.storage[UserRepositoryKey.self] ?? .init()
        }
        set {
            self.storage[UserRepositoryKey.self] = newValue
        }
    }
}

Request内で実際のリポジトリを使用するには、Requestにこの拡張を追加します:

extension Request {
    var users: UserRepository {
        self.application.users.make!(self)
    }
}

最後のステップは、configure.swift内でファクトリーを指定することです

app.users.use { req in
    DatabaseUserRepository(database: req.db)
}

これで、ルートハンドラー内でreq.users.all()を使用してリポジトリにアクセスでき、テスト内でファクトリーを簡単に置き換えることができます。 テスト内でモックされたリポジトリを使用したい場合は、まずTestUserRepositoryを作成します

final class TestUserRepository: UserRepository {
    var users: [User]
    let eventLoop: EventLoop

    init(users: [User] = [], eventLoop: EventLoop) {
        self.users = users
        self.eventLoop = eventLoop
    }

    func all() -> EventLoopFuture<[User]> {
        eventLoop.makeSuccededFuture(self.users)
    }
}

このモックされたリポジトリをテスト内で次のように使用できます:

final class MyTests: XCTestCase {
    func test() throws {
        let users: [User] = []
        app.users.use { TestUserRepository(users: users, eventLoop: $0.eventLoop) }
        ...
    }
}