# Aggiornamento a 4.0 Questa guida ti mostrerà come aggiornare un progetto Vapor 3.x a Vapor 4.x. La guida cercherà di coprire tutti i cambiamenti riguardanti i pacchetti Vapor e anche alcuni dei più comuni pacchetti di terze parti. Se noti che manca qualcosa, non esitare a chiedere aiuto nella [chat del team Vapor](https://discord.gg/vapor). Anche issues e pull requests su GitHub sono ben accette. ## Dipendenze Per usare Vapor 4, avrai bisogno di almeno Xcode 11.4 e macOS 10.15. La sezione Installazione della documentazione contiene le istruzioni per installare le dipendenze. ## Package.swift Il primo passo per aggiornare a Vapor 4 è aggiornare il file delle dipendenze del pacchetto. Qui è riportato un esempio di un `Package.swift` aggiornato. Puoi anche visitare il [template Package.swift aggiornato](https://github.com/vapor/template/blob/main/Package.swift). ```diff -// 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"), + ]) ] ) ``` Tutti i pacchetti che sono stati aggiornati a Vapor 4 avranno la versione major incrementata di 1. !!! warning "Attenzione" L'identificatore di pre-rilascio `-rc` indica che alcuni pacchetti di Vapor 4 non sono ancora stati rilasciati ufficialmente. ### Vecchi Pacchetti Alcuni dei pacchetti di Vapor 3 sono stati deprecati: - `vapor/auth`: Ora incluso in Vapor. - `vapor/core`: Assorbito in diversi moduli. - `vapor/crypto`: Ora incluso in vapor tramite SwiftCrypto - `vapor/multipart`: Ora incluso in Vapor. - `vapor/url-encoded-form`: Ora incluso in Vapor. - `vapor-community/vapor-ext`: Ora incluso in Vapor. - `vapor-community/pagination`: Ora incluso in Fluent. - `IBM-Swift/LoggerAPI`: Sostituito da SwiftLog. ### Dipendenza Fluent Ora `vapor/fluent` dev'essere aggiunto come una dipendenza separata. Tutti i pacchetti specifici per un database hanno ottenuto il suffisso `-driver` per rendere chiara la dipendenza di `vapor/fluent`. ```diff - .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"), ``` ### Piattaforme Ora il manifesto del pacchetto supporta esplicitamente macOS 10.15 o successivi. Questo implica che anche i progetti dovranno specificare quali piattaforme supportano. ```diff + platforms: [ + .macOS(.v10_15), + ], ``` Vapor potrebbe eventualmente aggiungere il supporto per piattaforme aggiuntive in futuro. I pacchetti potrebbero supportare un qualsiasi sottoinsieme di tali piattaforme finché il numero di versione è uguale o maggiore rispetto al minimo richiesto da Vapor. ### Xcode Vapor 4 utilizza il supporto nativo di SPM di Xcode 11. Ciò significa che non ci sarà più bisogno di generare il file `.xcodeproj`. Per aprire un progetto Vapor 4 in Xcode, basterà aprire il file `Package.swift` tramite `vapor xcode` o `open Package.swift`, Xcode poi procederà a scaricare le dipendenze. Una volta aggiornato il Package.swift, potresti dover chiudere Xcode e rimuovere i seguenti file dalla directory del progetto: - `Package.resolved` - `.build` - `.swiftpm` - `*.xcodeproj` Una volta che le nuove dipendenze sono state scaricate, noterai errori di compilazione, probabilmente un bel po'. Non ti preoccupare! Ti mostreremo come risolverli. ## Run Prima di tutto bisogna aggiornare il file `main.swift` del modulo Run al nuovo formato: ```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() ``` Il file `main.swift` andrà a sostituire il file `app.swift`, quindi potete rimuovere quel file. ## App Diamo un'occhiata a come aggiornare la struttura di base di App. ### configure.swift Il metodo `configure` ora accetta un'istanza di `Application`. ```diff - public func configure(_ config: inout Config, _ env: inout Environment, _ services: inout Services) throws + public func configure(_ app: Application) throws ``` Riportiamo un esempio di un file `configure.swift` aggiornato: ```swift import Fluent import FluentSQLiteDriver import Vapor // Called before your application initializes. public func configure(_ app: Application) throws { // Serves files from `Public/` directory // app.middleware.use(FileMiddleware(publicDirectory: app.directory.publicDirectory)) // Configure SQLite database app.databases.use(.sqlite(.file("db.sqlite")), as: .sqlite) // Configure migrations app.migrations.add(CreateTodo()) try routes(app) } ``` Cambiamenti di sintassi per cose come routing, middleware, fluent, ecc. sono menzionati nelle sezioni seguenti. ### boot.swift Il contenuto di `boot` può essere inserito nel metodo `configure` dal momento che ora accetta un'istanza di `Application`. ### routes.swift Il metodo `routes` ora accetta un'istanza di `Application`. ```diff - public func routes(_ router: Router, _ container: Container) throws + public func routes(_ app: Application) throws ``` Più avanti ci saranno altre informazioni sui cambiamenti della sintassi di routing. ## Servizi La API dei servizi di Vapor 4 è stata semplificata in modo da rendere molto più facile l'aggiunta di nuovi servizi. Ora i servizi sono esposti come metodi e proprietà sulle istanze di `Application` e `Request`. Per capire meglio questo concetto, diamo un'occhiata a qualche esempio: ```diff // Cambiamento della porta di default del server a 8281 - services.register { container -> NIOServerConfig in - return .default(port: 8281) - } + app.http.server.configuration.port = 8281 ``` Invece che registrare un un `NIOServerConfig` ai servizi, la configurazione del server è esposta come una proprietà su `Application` e può essere modificata direttamente. ```diff // Registrazione del middleware CORS let corsConfiguration = CORSMiddleware.Configuration( allowedOrigin: .all, allowedMethods: [.POST, .GET, .PATCH, .PUT, .DELETE, .OPTIONS] ) let corsMiddleware = CORSMiddleware(configuration: corsConfiguration) - var middlewares = MiddlewareConfig() // Create _empty_ middleware config - middlewares.use(corsMiddleware) - services.register(middlewares) + app.middleware.use(corsMiddleware) ``` Invece che registrare un `MiddlewareConfig` ai servizi, i middleware possono essere aggiunti direttamente a `Application` tramite una proprietà ```diff // Fare una richiesta in un route handler - try req.make(Client.self).get("https://vapor.codes") + req.client.get("https://vapor.codes") ``` Come Application, anche Request espone servizi come proprietà e metodi. È fortemente consigliato l'uso di servizi specifici alla Request da dentro le closure dei route handler. Questo nuovo pattern va a sostituire il vecchio pattern di `Container` e `Service` e `Config` che era usato in Vapor 3. ### Provider I provider sono stati rimossi in Vapor 4. I provider erano usati per registrare servizi e configurazioni ai servizi. Ora i pacchetti possono estendere direttamente `Application` e `Request` per registrare servizi e configurazioni. Diamo un'occhiata a come è configurato Leaf in Vapor 4. ```diff // Usa Leaf per renderizzare le view - try services.register(LeafProvider()) - config.prefer(LeafRenderer.self, for: ViewRenderer.self) + app.views.use(.leaf) ``` Per usare Leaf, basta usare `app.leaf`. ```diff // Disabilita il caching delle view di Leaf - services.register { container -> LeafConfig in - return LeafConfig(tags: ..., viewsDir: ..., shouldCache: false) - } + app.leaf.cache.isEnabled = false ``` ### Ambiente Si può accedere all'ambiente attuale (produzione, sviluppo, ecc.) tramite la proprietà `app.environment`. ### Servizi Personalizzati I servizi personalizzati che implementano il protocollo `Service` ed erano registrati al container in Vapor 3 vengono ora espressi come estensioni di Application o Request. ```diff 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) + } + } ``` Per accedere a questo servizio non c'è più bisogno di usare `make`: ```diff - try req.make(MyAPI.self).foo() + req.myAPI.foo() ``` ### Provider Personalizzati La maggior parte dei servizi può essere implementata come indicato sopra, tuttavia se si deve poter accedere al `Lifecycle` dell'applicazione si può fare così: ```swift struct PrintHello: LifecycleHandler { func willBoot(_ app: Application) throws { print("Hello!") } } app.lifecycle.use(PrintHello()) ``` Per salvare dati su Application, si può utilizzare il nuovo `Application.storage`: ```swift struct MyNumber: StorageKey { typealias Value = Int } app.storage[MyNumber.self] = 5 print(app.storage[MyNumber.self]) // 5 ``` L'accesso a `Application.storage` può essere avvolto in una proprietà computata per rendere il codice più leggibile: ```swift 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 utilizza le API asincrone di SwiftNIO direttamente senza fare l'overload di metodi come `map` e `flatMap` o tipi alias come `EventLoopFuture`. Vapor 3 forniva overload ed alias per retrocompatiblità con versioni di Vapor che non usavano SwiftNIO. ### Cambi di nome Il cambiamento più ovvio è che il typealias `Future` di `EventLoopFuture` è stato rimosso. Si può risolvere questo problema semplicemente usando "trova e sostituisci". In più NIO non supporta il label `to:` che veniva usato da Vapor 3, che comunque dato il nuovo sistema di inferenza dei tipi di Swift 5.2 non è più necessario. ```diff - futureA.map(to: String.self) { ... } + futureA.map { ... } ``` Metodi con il prefisso `new`, come `newPromise`, sono stati rinominati in `make` per essere più coerenti con lo standard di Swift. ```diff - let promise = eventLoop.newPromise(String.self) + let promise = eventLoop.makePromise(of: String.self) ``` `catchMap` non è più disponibile. Si può usare `mapError` o `flatMapErrorThrowing` al suo posto. Il `flatMap` globale di Vapor 3 per combinare diversi futuri non è più disponibile. Si può utilizzare `and` al suo posto. ```diff - flatMap(futureA, futureB) { a, b in + futureA.and(futureB).flatMap { (a, b) in // Fai qualcosa con a e b. } ``` ### ByteBuffer Molti metodi e proprietà che utilizzavano `Data` ora usano `ByteBuffer`, un tipo di storage di byte più potente e performante. Potete leggere di più su `ByteBuffer` nella [documentazione di SwiftNIO](https://swiftpackageindex.com/apple/swift-nio/main/documentation/niocore/bytebuffer). Per convertire un `ByteBuffer` in `Data` si può usare: ```swift Data(buffer.readableBytesView) ``` ### Throwing map / flatMap La difficoltà maggiore è che `map` e `flatMap` non possono più lanciare errori. `map` ha una versione che può lanciare errori, `flatMapThrowing`. `flatMap` non ha una versione che può lanciare errori, questo potrebbe implicare il cambiamento della struttura del vostro codice asincrono. Map che _non_ lanciano errori dovrebbero continuare a funzionare senza problemi. ```swift // Non lancia errori futureA.map { a in return b } ``` Map che lanciano errori devono essere aggiornate a `flatMapThrowing`: ```diff - futureA.map { a in + futureA.flatMapThrowing { a in if ... { throw SomeError() } else { return b } } ``` FlatMap che _non_ lanciano errori dovrebbero continuare a funzionare senza problemi. ```swift // Non lancia errori futureA.flatMap { a in return futureB } ``` Invece di lanciare errori dentro una flatMap, si può ritornare un errore futuro. Se l'errore ha origine da un altro metodo, si può utilizzare il costrutto do / catch. ```swift // Ritorna un errore futuro: futureA.flatMap { a in do { try doSomething() return futureB } catch { return eventLoop.makeFailedFuture(error) } } ``` Se l'errore è generato direttamente dentro la flatMap, si può usare `flatMapThrowing`: ```swift // Metodo che lancia errori riscritto con flatMapThrowing futureA.flatMapThrowing { a in try (a, doSomeThing()) }.flatMap { (a, result) in // result è il valore ritornato da doSomething() return futureB } ``` ## Routing Ora le route sono registrate direttamente su Application. ```swift app.get("hello") { req in return "Hello, world!" } ``` Ciò significa che non c'è più bisogno di registrare un router ai servizi. Basta passare l'istanza di `Application` al metodo `routes` e si può cominciare ad aggiungere endpoint. Tutti i metodi disponibili sul `RoutesBuilder` sono disponibili su `Application`. ### Contenuto Sincrono La decodifica delle richieste è ora sincrona. ```swift let payload = try req.content.decode(MyPayload.self) print(payload) // MyPayload ``` Si può fare l'override di questo comportamento utilizzando la strategia di collezione del body `.stream`. ```swift app.on(.POST, "streaming", body: .stream) { req in // Il body della richiesta è ora asincrono. req.body.collect().map { buffer in HTTPStatus.ok } } ``` ### URL divisi da virgole In Vapor 4 gli URL sono divisi da virgole e non devono contenere `/`. ```diff - router.get("v1/users/", "posts", "/comments") { req in + app.get("v1", "users", "posts", "comments") { req in // Gestisci la richiesta } ``` ### Parametri di una route Il protocollo `Parameter` è stato rimosso per promuovere l'uso di parametri chiamati esplicitamente. In questo modo si evitano problemi di parametri duplicati e il fetching non ordinato dei parametri nei middleware e nei gestori delle route. ```diff - 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)" } ``` Si tratta dell'utilizzo dei parametri nelle routes nella sezione di Fluent. ## Middleware `MiddlewareConfig` è stato rinominato in `MiddlewareConfiguration` ed è ora una proprietà di `Application`. Si possono aggiungere dei middleware all'applicazione usando `app.middleware`. ```diff let corsMiddleware = CORSMiddleware(configuration: ...) - var middleware = MiddlewareConfig() - middleware.use(corsMiddleware) + app.middleware.use(corsMiddleware) - services.register(middlewares) ``` Ora è obbligatorio inizializzare i Middleware prima di registrarli. ```diff - middleware.use(ErrorMiddleware.self) + app.middleware.use(ErrorMiddleware.default(environment: app.environment)) ``` Per rimuovere tutti i middleware di default si può utilizzare: ```swift app.middleware = .init() ``` ## Fluent Ora l'API di Fluent è indipendente dal database su cui viene utilizzata. Basta importare `Fluent`. ```diff - import FluentMySQL + import Fluent ``` ### Modelli I modelli utilizzano il protocollo `Model` e devono essere delle classi: ```diff - struct Planet: MySQLModel { + final class Planet: Model { ``` Tutti i campi sono dichiarati utilizzando `@Field` o `@OptionalField`. ```diff + @Field(key: "name") var name: String + @OptionalField(key: "age") var age: Int? ``` L'ID di un modello dev'essere definito utilizzando `@ID`. ```diff + @ID(key: "id") var id: UUID? ``` I modelli che utilizzano un ID personalizzato devono utilizzare `@ID(custom:)`. Tutti i modelli devono avere il nome della loro tabella definito staticamente. ```diff final class Planet: Model { + static let schema = "Planet" ``` I metodi `save`, `update` e `create` non ritornano più l'istanza del modello. ```diff - model.save(on: ...) + model.save(on: ...).map { model } ``` I modelli non possono più venire utilizzati come parametri del route path. Si può utilizzare `find` e `req.parameters.get`. ```diff - try req.parameters.next(ServerSize.self) + ServerSize.find(req.parameters.get("size"), on: req.db) + .unwrap(or: Abort(.notFound)) ``` `Model.ID` è stato rinominato in `Model.IDValue`. I timestamp sono ora dichiarati usando `@Timestamp`. ```diff - static var createdAtKey: TimestampKey? = \.createdAt + @Timestamp(key: "createdAt", on: .create) var createdAt: Date? ``` ### Relazioni Le relazioni sono ora dichiarate usando `@Parent`, `@Children` e `@Siblings`. Le relazioni `@Parent` contengono il campo internamente. La chiave passata a `@Parent` è il nome del campo che contiene la chiave esterna. ```diff - var serverID: Int - var server: Parent { - parent(\.serverID) - } + @Parent(key: "serverID") + var server: Server ``` Le relazioni `@Children` hanno un key path al relativo `@Parent`. ```diff - var apps: Children { - children(\.serverID) - } + @Children(for: \.$server) + var apps: [App] ``` Le relazioni `@Siblings` hanno dei key path al relativo ad una tabella pivot. ```diff - var users: Siblings { - siblings() - } + @Siblings(through: Permission.self, from: \.$user, to: \.$company) + var companies: [Company] ``` Le tabelle pivot sono modelli normali che conformano a `Model` con due proprietà `@Parent` e zero o più campi aggiuntivi. ### Query Si può accedere al contesto del database utilizzando `req.db` nei route handler. ```diff - Planet.query(on: req) + Planet.query(on: req.db) ``` `DatabaseConnectable` è stato rinominato in `Database`. Ora i key path ai campi hanno il prefisso `$` per specificare che si tratta del wrapper e non del valore del campo. ```diff - filter(\.foo == ...) + filter(\.$foo == ...) ``` ### Migrazioni Le migrazioni devono essere scritte manualmente e non si basano più sul concetto di reflection: ```diff - extension Planet: Migration { } + struct CreatePlanet: Migration { + ... +} ``` Ora le migrazioni sono tipate tramite stringe e non sono direttamente collegate ai modelli, utilizzano infatti il protocollo `Migration`. ```diff - struct CreateGalaxy: <#Database#>Migration { + struct CreateGalaxy: Migration { ``` I metodi `prepare` e `revert` non sono più statici. ```diff - static func prepare(on conn: <#Database#>Connection) -> Future { + func prepare(on database: Database) -> EventLoopFuture ``` La creazione di un costruttore di schema è fatta tramite un metodo su `Database`. ```diff - <#Database#>Database.create(Galaxy.self, on: conn) { builder in - // Usa builder. - } + var builder = database.schema("Galaxy") + // Usa builder. ``` I metodi `create`, `update` e `delete` sono metodi del costruttore di schema e assomigliano al funzionamento di un costruttore di query. La definizione dei campi è tipata tramite stringhe e usa il seguente pattern: ```swift field(, , ) ``` ```diff - builder.field(for: \.name) + builder.field("name", .string, .required) ``` La costruzione degli schemi può essere concatenata come un costruttore di query: ```swift database.schema("Galaxy") .id() .field("name", .string, .required) .create() ``` ### Configurazione di Fluent `DatabasesConfig` è stato sostituito da `app.databases`. ```swift try app.databases.use(.postgres(url: "postgres://..."), as: .psql) ``` `MigrationsConfig` è stato sostituito da `app.migrations`. ```swift app.migrations.use(CreatePlanet(), on: .psql) ``` ### Repository Dal momento che è cambiato il modo in cui funzionano i servizi, anche la struttura delle Repository è cambiata. Servirà comunque un protocollo come `UserRepository`, ma invece di creare una `final class` che implementi il protocollo, basta creare una `struct`. ```diff - final class DatabaseUserRepository: UserRepository { + struct DatabaseUserRepository: UserRepository { let database: Database func all() -> EventLoopFuture<[User]> { return User.query(on: database).all() } } ``` Si può rimuovere l'utilizzo di `ServiceType`, dal momento che non esiste più. ```diff - extension DatabaseUserRepository { - static let serviceSupports: [Any.Type] = [Athlete.self] - static func makeService(for worker: Container) throws -> Self { - return .init() - } - } ``` Si può invece creare una `UserRepositoryFactory`: ```swift struct UserRepositoryFactory { var make: ((Request) -> UserRepository)? mutating func use(_ make: @escaping ((Request) -> UserRepository)) { self.make = make } } ``` Questa struct ha la responsabilità di ritornare una `UserRepository` per una `Request`. Il prossimo passo è quello di estendere l'`Application` con una proprietà computata che ritorna una `UserRepository`: ```swift extension Application { private struct UserRepositoryKey: StorageKey { typealias Value = UserRepositoryFactory } var users: UserRepositoryFactory { get { self.storage[UserRepositoryKey.self] ?? .init() } set { self.storage[UserRepositoryKey.self] = newValue } } } ``` Per utilizzare l'effettiva repository all'interno di un route handler: ```swift extension Request { var users: UserRepository { self.application.users.make!(self) } } ``` L'ultimo passo è quello di specificare la factory nel metodo `configure`: ```swift app.users.use { req in DatabaseUserRepository(database: req.db) } ``` Si può ora accedere alla repository nei route handler con `req.users.all()` e si può facilmente sostituire la repository con una simulata per i test. Basta creare un nuovo file `TestUserRepository`: ```swift 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) } } ``` Si può usare la repository in questo modo: ```swift final class MyTests: XCTestCase { func test() throws { let users: [User] = [] app.users.use { TestUserRepository(users: users, eventLoop: $0.eventLoop) } ... } } ```