From b8b9643ca75d6941b05726ec01dbc84ce8c6ed53 Mon Sep 17 00:00:00 2001 From: "Gerasimos (Makis) Maropoulos" Date: Fri, 22 Dec 2017 10:25:34 +0200 Subject: [PATCH] Remove big README examples, will be replaced with small quick examples so new users can found easier to get started Former-commit-id: d5e804babf0edc0765a4195247071e410b27ea27 --- README.md | 851 -------------------------------------- README_ZH.md | 828 ------------------------------------- _examples/mvc/README.md | 875 +--------------------------------------- 3 files changed, 8 insertions(+), 2546 deletions(-) diff --git a/README.md b/README.md index 272f9204..552625e5 100644 --- a/README.md +++ b/README.md @@ -167,857 +167,6 @@ func main() { Guidelines for bootstrapping applications can be found at the [_examples/structuring](_examples/#structuring). -### Quick MVC Tutorial - -```go -package main - -import ( - "github.com/kataras/iris" - "github.com/kataras/iris/mvc" -) - -func main() { - app := iris.New() - - app.Controller("/helloworld", new(HelloWorldController)) - - app.Run(iris.Addr("localhost:8080")) -} - -type HelloWorldController struct { - mvc.C - - // [ Your fields here ] - // Request lifecycle data - // Models - // Database - // Global properties -} - -// -// GET: /helloworld - -func (c *HelloWorldController) Get() string { - return "This is my default action..." -} - -// -// GET: /helloworld/{name:string} - -func (c *HelloWorldController) GetBy(name string) string { - return "Hello " + name -} - -// -// GET: /helloworld/welcome - -func (c *HelloWorldController) GetWelcome() (string, int) { - return "This is the GetWelcome action func...", iris.StatusOK -} - -// -// GET: /helloworld/welcome/{name:string}/{numTimes:int} - -func (c *HelloWorldController) GetWelcomeBy(name string, numTimes int) { - // Access to the low-level Context, - // output arguments are optional of course so we don't have to use them here. - c.Ctx.Writef("Hello %s, NumTimes is: %d", name, numTimes) -} -``` - -> The [_examples/mvc](_examples/mvc) and [mvc/controller_test.go](https://github.com/kataras/iris/blob/master/mvc/controller_test.go) files explain each feature with simple paradigms, they show how you can take advantage of the Iris MVC Binder, Iris MVC Models and many more... - -Every `exported` func prefixed with an HTTP Method(`Get`, `Post`, `Put`, `Delete`...) in a controller is callable as an HTTP endpoint. In the sample above, all funcs writes a string to the response. Note the comments preceding each method. - -An HTTP endpoint is a targetable URL in the web application, such as `http://localhost:8080/helloworld`, and combines the protocol used: HTTP, the network location of the web server (including the TCP port): `localhost:8080` and the target URI `/helloworld`. - -The first comment states this is an [HTTP GET](https://www.w3schools.com/tags/ref_httpmethods.asp) method that is invoked by appending "/helloworld" to the base URL. The third comment specifies an [HTTP GET](https://www.w3schools.com/tags/ref_httpmethods.asp) method that is invoked by appending "/helloworld/welcome" to the URL. - -Controller knows how to handle the "name" on `GetBy` or the "name" and "numTimes" at `GetWelcomeBy`, because of the `By` keyword, and builds the dynamic route without boilerplate; the third comment specifies an [HTTP GET](https://www.w3schools.com/tags/ref_httpmethods.asp) dynamic method that is invoked by any URL that starts with "/helloworld/welcome" and followed by two more path parts, the first one can accept any value and the second can accept only numbers, i,e: "http://localhost:8080/helloworld/welcome/golang/32719", otherwise a [404 Not Found HTTP Error](https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4.5) will be sent to the client instead. - -### Quick MVC Tutorial #2 - -Iris has a very powerful and **blazing [fast](_benchmarks)** MVC support, you can return any value of any type from a method function -and it will be sent to the client as expected. - -* if `string` then it's the body. -* if `string` is the second output argument then it's the content type. -* if `int` then it's the status code. -* if `error` and not nil then (any type) response will be omitted and error's text with a 400 bad request will be rendered instead. -* if `(int, error)` and error is not nil then the response result will be the error's text with the status code as `int`. -* if `bool` is false then it throws 404 not found http error by skipping everything else. -* if `custom struct` or `interface{}` or `slice` or `map` then it will be rendered as json, unless a `string` content type is following. -* if `mvc.Result` then it executes its `Dispatch` function, so good design patters can be used to split the model's logic where needed. - -The example below is not intended to be used in production but it's a good showcase of some of the return types we saw before; - -```go -package main - -import ( - "github.com/kataras/iris" - "github.com/kataras/iris/middleware/basicauth" - "github.com/kataras/iris/mvc" -) - -// Movie is our sample data structure. -type Movie struct { - Name string `json:"name"` - Year int `json:"year"` - Genre string `json:"genre"` - Poster string `json:"poster"` -} - -// movies contains our imaginary data source. -var movies = []Movie{ - { - Name: "Casablanca", - Year: 1942, - Genre: "Romance", - Poster: "https://iris-go.com/images/examples/mvc-movies/1.jpg", - }, - { - Name: "Gone with the Wind", - Year: 1939, - Genre: "Romance", - Poster: "https://iris-go.com/images/examples/mvc-movies/2.jpg", - }, - { - Name: "Citizen Kane", - Year: 1941, - Genre: "Mystery", - Poster: "https://iris-go.com/images/examples/mvc-movies/3.jpg", - }, - { - Name: "The Wizard of Oz", - Year: 1939, - Genre: "Fantasy", - Poster: "https://iris-go.com/images/examples/mvc-movies/4.jpg", - }, -} - - -var basicAuth = basicauth.New(basicauth.Config{ - Users: map[string]string{ - "admin": "password", - }, -}) - - -func main() { - app := iris.New() - - app.Use(basicAuth) - - app.Controller("/movies", new(MoviesController)) - - app.Run(iris.Addr(":8080")) -} - -// MoviesController is our /movies controller. -type MoviesController struct { - mvc.C -} - -// Get returns list of the movies -// Demo: -// curl -i http://localhost:8080/movies -func (c *MoviesController) Get() []Movie { - return movies -} - -// GetBy returns a movie -// Demo: -// curl -i http://localhost:8080/movies/1 -func (c *MoviesController) GetBy(id int) Movie { - return movies[id] -} - -// PutBy updates a movie -// Demo: -// curl -i -X PUT -F "genre=Thriller" -F "poster=@/Users/kataras/Downloads/out.gif" http://localhost:8080/movies/1 -func (c *MoviesController) PutBy(id int) Movie { - // get the movie - m := movies[id] - - // get the request data for poster and genre - file, info, err := c.Ctx.FormFile("poster") - if err != nil { - c.Ctx.StatusCode(iris.StatusInternalServerError) - return Movie{} - } - file.Close() // we don't need the file - poster := info.Filename // imagine that as the url of the uploaded file... - genre := c.Ctx.FormValue("genre") - - // update the poster - m.Poster = poster - m.Genre = genre - movies[id] = m - - return m -} - -// DeleteBy deletes a movie -// Demo: -// curl -i -X DELETE -u admin:password http://localhost:8080/movies/1 -func (c *MoviesController) DeleteBy(id int) iris.Map { - // delete the entry from the movies slice - deleted := movies[id].Name - movies = append(movies[:id], movies[id+1:]...) - // and return the deleted movie's name - return iris.Map{"deleted": deleted} -} -``` - -### Quick MVC Tutorial #3 - -Nothing stops you from using your favorite **folder structure**. Iris is a low level web framework, it has got MVC first-class support but it doesn't limit your folder structure, this is your choice. - -Structuring depends on your own needs. We can't tell you how to design your own application for sure but you're free to take a closer look to one typical example below; - -[![folder structure example](_examples/mvc/overview/folder_structure.png)](_examples/mvc/overview) - -Shhh, let's spread the code itself. - -#### Data Model Layer - -```go -// file: datamodels/movie.go - -package datamodels - -// Movie is our sample data structure. -// Keep note that the tags for public-use (for our web app) -// should be kept in other file like "web/viewmodels/movie.go" -// which could wrap by embedding the datamodels.Movie or -// declare new fields instead butwe will use this datamodel -// as the only one Movie model in our application, -// for the shake of simplicty. -type Movie struct { - ID int64 `json:"id"` - Name string `json:"name"` - Year int `json:"year"` - Genre string `json:"genre"` - Poster string `json:"poster"` -} -``` - -#### Data Source / Data Store Layer - -```go -// file: datasource/movies.go - -package datasource - -import "github.com/kataras/iris/_examples/mvc/overview/datamodels" - -// Movies is our imaginary data source. -var Movies = map[int64]datamodels.Movie{ - 1: { - ID: 1, - Name: "Casablanca", - Year: 1942, - Genre: "Romance", - Poster: "https://iris-go.com/images/examples/mvc-movies/1.jpg", - }, - 2: { - ID: 2, - Name: "Gone with the Wind", - Year: 1939, - Genre: "Romance", - Poster: "https://iris-go.com/images/examples/mvc-movies/2.jpg", - }, - 3: { - ID: 3, - Name: "Citizen Kane", - Year: 1941, - Genre: "Mystery", - Poster: "https://iris-go.com/images/examples/mvc-movies/3.jpg", - }, - 4: { - ID: 4, - Name: "The Wizard of Oz", - Year: 1939, - Genre: "Fantasy", - Poster: "https://iris-go.com/images/examples/mvc-movies/4.jpg", - }, - 5: { - ID: 5, - Name: "North by Northwest", - Year: 1959, - Genre: "Thriller", - Poster: "https://iris-go.com/images/examples/mvc-movies/5.jpg", - }, -} -``` - -#### Repositories - -The layer which has direct access to the "datasource" and can manipulate data directly. - -```go -// file: repositories/movie_repository.go - -package repositories - -import ( - "errors" - "sync" - - "github.com/kataras/iris/_examples/mvc/overview/datamodels" -) - -// Query represents the visitor and action queries. -type Query func(datamodels.Movie) bool - -// MovieRepository handles the basic operations of a movie entity/model. -// It's an interface in order to be testable, i.e a memory movie repository or -// a connected to an sql database. -type MovieRepository interface { - Exec(query Query, action Query, limit int, mode int) (ok bool) - - Select(query Query) (movie datamodels.Movie, found bool) - SelectMany(query Query, limit int) (results []datamodels.Movie) - - InsertOrUpdate(movie datamodels.Movie) (updatedMovie datamodels.Movie, err error) - Delete(query Query, limit int) (deleted bool) -} - -// NewMovieRepository returns a new movie memory-based repository, -// the one and only repository type in our example. -func NewMovieRepository(source map[int64]datamodels.Movie) MovieRepository { - return &movieMemoryRepository{source: source} -} - -// movieMemoryRepository is a "MovieRepository" -// which manages the movies using the memory data source (map). -type movieMemoryRepository struct { - source map[int64]datamodels.Movie - mu sync.RWMutex -} - -const ( - // ReadOnlyMode will RLock(read) the data . - ReadOnlyMode = iota - // ReadWriteMode will Lock(read/write) the data. - ReadWriteMode -) - -func (r *movieMemoryRepository) Exec(query Query, action Query, actionLimit int, mode int) (ok bool) { - loops := 0 - - if mode == ReadOnlyMode { - r.mu.RLock() - defer r.mu.RUnlock() - } else { - r.mu.Lock() - defer r.mu.Unlock() - } - - for _, movie := range r.source { - ok = query(movie) - if ok { - if action(movie) { - loops++ - if actionLimit >= loops { - break // break - } - } - } - } - - return -} - -// Select receives a query function -// which is fired for every single movie model inside -// our imaginary data source. -// When that function returns true then it stops the iteration. -// -// It returns the query's return last known "found" value -// and the last known movie model -// to help callers to reduce the LOC. -// -// It's actually a simple but very clever prototype function -// I'm using everywhere since I firstly think of it, -// hope you'll find it very useful as well. -func (r *movieMemoryRepository) Select(query Query) (movie datamodels.Movie, found bool) { - found = r.Exec(query, func(m datamodels.Movie) bool { - movie = m - return true - }, 1, ReadOnlyMode) - - // set an empty datamodels.Movie if not found at all. - if !found { - movie = datamodels.Movie{} - } - - return -} - -// SelectMany same as Select but returns one or more datamodels.Movie as a slice. -// If limit <=0 then it returns everything. -func (r *movieMemoryRepository) SelectMany(query Query, limit int) (results []datamodels.Movie) { - r.Exec(query, func(m datamodels.Movie) bool { - results = append(results, m) - return true - }, limit, ReadOnlyMode) - - return -} - -// InsertOrUpdate adds or updates a movie to the (memory) storage. -// -// Returns the new movie and an error if any. -func (r *movieMemoryRepository) InsertOrUpdate(movie datamodels.Movie) (datamodels.Movie, error) { - id := movie.ID - - if id == 0 { // Create new action - var lastID int64 - // find the biggest ID in order to not have duplications - // in productions apps you can use a third-party - // library to generate a UUID as string. - r.mu.RLock() - for _, item := range r.source { - if item.ID > lastID { - lastID = item.ID - } - } - r.mu.RUnlock() - - id = lastID + 1 - movie.ID = id - - // map-specific thing - r.mu.Lock() - r.source[id] = movie - r.mu.Unlock() - - return movie, nil - } - - // Update action based on the movie.ID, - // here we will allow updating the poster and genre if not empty. - // Alternatively we could do pure replace instead: - // r.source[id] = movie - // and comment the code below; - current, exists := r.Select(func(m datamodels.Movie) bool { - return m.ID == id - }) - - if !exists { // ID is not a real one, return an error. - return datamodels.Movie{}, errors.New("failed to update a nonexistent movie") - } - - // or comment these and r.source[id] = m for pure replace - if movie.Poster != "" { - current.Poster = movie.Poster - } - - if movie.Genre != "" { - current.Genre = movie.Genre - } - - // map-specific thing - r.mu.Lock() - r.source[id] = current - r.mu.Unlock() - - return movie, nil -} - -func (r *movieMemoryRepository) Delete(query Query, limit int) bool { - return r.Exec(query, func(m datamodels.Movie) bool { - delete(r.source, m.ID) - return true - }, limit, ReadWriteMode) -} -``` - -#### Services - -The layer which has access to call functions from the "repositories" and "models" (or even "datamodels" if simple application). It should contain the most of the domain logic. - -```go -// file: services/movie_service.go - -package services - -import ( - "github.com/kataras/iris/_examples/mvc/overview/datamodels" - "github.com/kataras/iris/_examples/mvc/overview/repositories" -) - -// MovieService handles some of the CRUID operations of the movie datamodel. -// It depends on a movie repository for its actions. -// It's here to decouple the data source from the higher level compoments. -// As a result a different repository type can be used with the same logic without any aditional changes. -// It's an interface and it's used as interface everywhere -// because we may need to change or try an experimental different domain logic at the future. -type MovieService interface { - GetAll() []datamodels.Movie - GetByID(id int64) (datamodels.Movie, bool) - DeleteByID(id int64) bool - UpdatePosterAndGenreByID(id int64, poster string, genre string) (datamodels.Movie, error) -} - -// NewMovieService returns the default movie service. -func NewMovieService(repo repositories.MovieRepository) MovieService { - return &movieService{ - repo: repo, - } -} - -type movieService struct { - repo repositories.MovieRepository -} - -// GetAll returns all movies. -func (s *movieService) GetAll() []datamodels.Movie { - return s.repo.SelectMany(func(_ datamodels.Movie) bool { - return true - }, -1) -} - -// GetByID returns a movie based on its id. -func (s *movieService) GetByID(id int64) (datamodels.Movie, bool) { - return s.repo.Select(func(m datamodels.Movie) bool { - return m.ID == id - }) -} - -// UpdatePosterAndGenreByID updates a movie's poster and genre. -func (s *movieService) UpdatePosterAndGenreByID(id int64, poster string, genre string) (datamodels.Movie, error) { - // update the movie and return it. - return s.repo.InsertOrUpdate(datamodels.Movie{ - ID: id, - Poster: poster, - Genre: genre, - }) -} - -// DeleteByID deletes a movie by its id. -// -// Returns true if deleted otherwise false. -func (s *movieService) DeleteByID(id int64) bool { - return s.repo.Delete(func(m datamodels.Movie) bool { - return m.ID == id - }, 1) -} -``` - -#### View Models - -There should be the view models, the structure that the client will be able to see. - -Example: - -```go -import ( - "github.com/kataras/iris/_examples/mvc/overview/datamodels" - - "github.com/kataras/iris/context" -) - -type Movie struct { - datamodels.Movie -} - -func (m Movie) IsValid() bool { - /* do some checks and return true if it's valid... */ - return m.ID > 0 -} -``` - -Iris is able to convert any custom data Structure into an HTTP Response Dispatcher, -so theoretically, something like the following is permitted if it's really necessary; - -```go -// Dispatch completes the `kataras/iris/mvc#Result` interface. -// Sends a `Movie` as a controlled http response. -// If its ID is zero or less then it returns a 404 not found error -// else it returns its json representation, -// (just like the controller's functions do for custom types by default). -// -// Don't overdo it, the application's logic should not be here. -// It's just one more step of validation before the response, -// simple checks can be added here. -// -// It's just a showcase, -// imagine the potentials this feature gives when designing a bigger application. -// -// This is called where the return value from a controller's method functions -// is type of `Movie`. -// For example the `controllers/movie_controller.go#GetBy`. -func (m Movie) Dispatch(ctx context.Context) { - if !m.IsValid() { - ctx.NotFound() - return - } - ctx.JSON(m, context.JSON{Indent: " "}) -} -``` - -However, we will use the "datamodels" as the only one models package because -Movie structure doesn't contain any sensitive data, clients are able to see all of its fields -and we don't need any extra functionality or validation inside it. - -#### Controllers - -Handles web requests, bridge between the services and the client. - -```go -// file: web/controllers/movie_controller.go - -package controllers - -import ( - "errors" - - "github.com/kataras/iris/_examples/mvc/overview/datamodels" - "github.com/kataras/iris/_examples/mvc/overview/services" - - "github.com/kataras/iris" - "github.com/kataras/iris/mvc" -) - -// MovieController is our /movies controller. -type MovieController struct { - mvc.C - - // Our MovieService, it's an interface which - // is binded from the main application. - Service services.MovieService -} - -// Get returns list of the movies. -// Demo: -// curl -i http://localhost:8080/movies -// -// The correct way if you have sensitive data: -// func (c *MovieController) Get() (results []viewmodels.Movie) { -// data := c.Service.GetAll() -// -// for _, movie := range data { -// results = append(results, viewmodels.Movie{movie}) -// } -// return -// } -// otherwise just return the datamodels. -func (c *MovieController) Get() (results []datamodels.Movie) { - return c.Service.GetAll() -} - -// GetBy returns a movie. -// Demo: -// curl -i http://localhost:8080/movies/1 -func (c *MovieController) GetBy(id int64) (movie datamodels.Movie, found bool) { - return c.Service.GetByID(id) // it will throw 404 if not found. -} - -// PutBy updates a movie. -// Demo: -// curl -i -X PUT -F "genre=Thriller" -F "poster=@/Users/kataras/Downloads/out.gif" http://localhost:8080/movies/1 -func (c *MovieController) PutBy(id int64) (datamodels.Movie, error) { - // get the request data for poster and genre - file, info, err := c.Ctx.FormFile("poster") - if err != nil { - return datamodels.Movie{}, errors.New("failed due form file 'poster' missing") - } - // we don't need the file so close it now. - file.Close() - - // imagine that is the url of the uploaded file... - poster := info.Filename - genre := c.Ctx.FormValue("genre") - - return c.Service.UpdatePosterAndGenreByID(id, poster, genre) -} - -// DeleteBy deletes a movie. -// Demo: -// curl -i -X DELETE -u admin:password http://localhost:8080/movies/1 -func (c *MovieController) DeleteBy(id int64) interface{} { - wasDel := c.Service.DeleteByID(id) - if wasDel { - // return the deleted movie's ID - return iris.Map{"deleted": id} - } - // right here we can see that a method function can return any of those two types(map or int), - // we don't have to specify the return type to a specific type. - return iris.StatusBadRequest -} -``` - -```go -// file: web/controllers/hello_controller.go - -package controllers - -import ( - "errors" - - "github.com/kataras/iris/mvc" -) - -// HelloController is our sample controller -// it handles GET: /hello and GET: /hello/{name} -type HelloController struct { - mvc.C -} - -var helloView = mvc.View{ - Name: "hello/index.html", - Data: map[string]interface{}{ - "Title": "Hello Page", - "MyMessage": "Welcome to my awesome website", - }, -} - -// Get will return a predefined view with bind data. -// -// `mvc.Result` is just an interface with a `Dispatch` function. -// `mvc.Response` and `mvc.View` are the built'n result type dispatchers -// you can even create custom response dispatchers by -// implementing the `github.com/kataras/iris/mvc#Result` interface. -func (c *HelloController) Get() mvc.Result { - return helloView -} - -// you can define a standard error in order to be re-usable anywhere in your app. -var errBadName = errors.New("bad name") - -// you can just return it as error or even better -// wrap this error with an mvc.Response to make it an mvc.Result compatible type. -var badName = mvc.Response{Err: errBadName, Code: 400} - -// GetBy returns a "Hello {name}" response. -// Demos: -// curl -i http://localhost:8080/hello/iris -// curl -i http://localhost:8080/hello/anything -func (c *HelloController) GetBy(name string) mvc.Result { - if name != "iris" { - return badName - // or - // GetBy(name string) (mvc.Result, error) { - // return nil, errBadName - // } - } - - // return mvc.Response{Text: "Hello " + name} OR: - return mvc.View{ - Name: "hello/name.html", - Data: name, - } -} -``` - -```go -// file: web/middleware/basicauth.go - -package middleware - -import "github.com/kataras/iris/middleware/basicauth" - -// BasicAuth middleware sample. -var BasicAuth = basicauth.New(basicauth.Config{ - Users: map[string]string{ - "admin": "password", - }, -}) -``` - -```html - - - - - {{.Title}} - My App - - - -

{{.MyMessage}}

- - - -``` - -```html - - - - - {{.}}' Portfolio - My App - - - -

Hello {{.}}

- - - -``` - -> Navigate to the [_examples/view](_examples/#view) for more examples -like shared layouts, tmpl funcs, reverse routing and more! - -#### Main - -This file creates any necessary component and links them together. - -```go -// file: main.go - -package main - -import ( - "github.com/kataras/iris/_examples/mvc/overview/datasource" - "github.com/kataras/iris/_examples/mvc/overview/repositories" - "github.com/kataras/iris/_examples/mvc/overview/services" - "github.com/kataras/iris/_examples/mvc/overview/web/controllers" - "github.com/kataras/iris/_examples/mvc/overview/web/middleware" - - "github.com/kataras/iris" -) - -func main() { - app := iris.New() - - // Load the template files. - app.RegisterView(iris.HTML("./web/views", ".html")) - - // Register our controllers. - app.Controller("/hello", new(controllers.HelloController)) - - // Create our movie repository with some (memory) data from the datasource. - repo := repositories.NewMovieRepository(datasource.Movies) - // Create our movie service, we will bind it to the movie controller. - movieService := services.NewMovieService(repo) - - app.Controller("/movies", new(controllers.MovieController), - // Bind the "movieService" to the MovieController's Service (interface) field. - movieService, - // Add the basic authentication(admin:password) middleware - // for the /movies based requests. - middleware.BasicAuth) - - // Start the web server at localhost:8080 - // http://localhost:8080/hello - // http://localhost:8080/hello/iris - // http://localhost:8080/movies - // http://localhost:8080/movies/1 - app.Run( - iris.Addr("localhost:8080"), - iris.WithoutVersionChecker, - iris.WithoutServerError(iris.ErrServerClosed), - iris.WithOptimizations, // enables faster json serialization and more - ) -} -``` - -More folder structure guidelines can be found at the [_examples/#structuring](_examples/#structuring) section. - ## Now you are ready to move to the next step and get closer to becoming a pro gopher Congratulations, since you've made it so far, we've crafted just for you some next level content to turn you into a real pro gopher 😃 diff --git a/README_ZH.md b/README_ZH.md index a4718c1e..da42f19e 100644 --- a/README_ZH.md +++ b/README_ZH.md @@ -147,834 +147,6 @@ $ go run main.go Iris的一些开发约定可以看看这里[_examples/structuring](_examples/#structuring)。 -### MVC指南 - -```go -package main - -import ( - "github.com/kataras/iris" - "github.com/kataras/iris/mvc" -) - -func main() { - app := iris.New() - - app.Controller("/helloworld", new(HelloWorldController)) - - app.Run(iris.Addr("localhost:8080")) -} - -type HelloWorldController struct { - mvc.C - - // [ Your fields here ] - // Request lifecycle data - // Models - // Database - // Global properties -} - -// -// GET: /helloworld - -func (c *HelloWorldController) Get() string { - return "This is my default action..." -} - -// -// GET: /helloworld/{name:string} - -func (c *HelloWorldController) GetBy(name string) string { - return "Hello " + name -} - -// -// GET: /helloworld/welcome - -func (c *HelloWorldController) GetWelcome() (string, int) { - return "This is the GetWelcome action func...", iris.StatusOK -} - -// -// GET: /helloworld/welcome/{name:string}/{numTimes:int} - -func (c *HelloWorldController) GetWelcomeBy(name string, numTimes int) { - // Access to the low-level Context, - // output arguments are optional of course so we don't have to use them here. - c.Ctx.Writef("Hello %s, NumTimes is: %d", name, numTimes) -} - -``` -> [_examples/mvc](_examples/mvc) 和 [mvc/controller_test.go](https://github.com/kataras/iris/blob/master/mvc/controller_test.go) 两个简单的例子可以让你更好的了解 Iris MVC 的使用方式 - -每一个在controller中导出的Go方法名都和HTTP方法(`Get`, `Post`, `Put`, `Delete`...) 一一对应 - -在Web应用中一个HTTP访问的资源就是一个URL(统一资源定位符),比如`http://localhost:8080/helloworld`是由HTTP协议、Web服务网络位置(包括TCP端口):`localhost:8080`以及资源名称URI(统一资源标志符) `/helloworld`组成的。 - -上面例子第一个方法映射到[HTTP GET](https://www.w3schools.com/tags/ref_httpmethods.asp)方法,访问资源是"/helloworld",第三个方法映射到[HTTP GET](https://www.w3schools.com/tags/ref_httpmethods.asp)方法,访问资源是"/helloworld/welcome" - - -Controller在处理`GetBy`方法时可以识别路径‘name’参数,`GetWelcomeBy`方法可以识别路径‘name’和‘numTimes’参数,因为Controller在识别`By`关键字后可以动态灵活的处理路由;上面第四个方法指示使用 [HTTP GET](https://www.w3schools.com/tags/ref_httpmethods.asp)方法,而且只处理以"/helloworld/welcome"开头的资源位置路径,并且此路径还得包括两部分,第一部分类型没有限制,第二部分只能是数字类型,比如"http://localhost:8080/helloworld/welcome/golang/32719" 是合法的,其它的就会给客户端返回[404 找不到](https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4.5)的提示 - - -### MVC 快速指南 2 - -Iris对MVC的支持非常**棒[看看基准测试](_benchmarks)** ,Iris通过方法的返回值,可以给客户端返回任意类型的数据: - -* 如果返回的是 `string` 类型,就直接给客户端返回字符串 -* 如果第二个返回值是 `string` 类型,那么这个值就是ContentType(HTTP header)的值 -* 如果返回的是 `int` 类型,这个值就是HTTP状态码 -* 如果返回 `error` 值不是空,Iris 将会把这个值作为HTTP 400页面的返回值内容 -*  如果返回 `(int, error)` 类型,并且error不为空,那么Iris返回error的内容,同时把 `int` 值作为HTTP状态码 -* 如果返回 `bool` 类型,并且值是 false ,Iris直接返回404页面 -* 如果返回自定义` struct` 、 `interface{}` 、 `slice` 及 `map` ,Iris 将按照JSON的方式返回,注意如果第二个返回值是 `string`,那么Iris就按照这个 `string` 值的ContentType处理了(不一定是'application/json') -*  如果 `mvc.Result` 调用了 `Dispatch` 函数, 就会按照自己的逻辑重新处理 - -下面这些例子仅供参考,生产环境谨慎使用 - -```go -package main - -import ( - "github.com/kataras/iris" - "github.com/kataras/iris/middleware/basicauth" - "github.com/kataras/iris/mvc" -) - -// Movie 是自定义数据结构 -type Movie struct { - Name string `json:"name"` - Year int `json:"year"` - Genre string `json:"genre"` - Poster string `json:"poster"` -} - -// movies 对象模拟数据源 -var movies = []Movie{ - { - Name: "Casablanca", - Year: 1942, - Genre: "Romance", - Poster: "https://iris-go.com/images/examples/mvc-movies/1.jpg", - }, - { - Name: "Gone with the Wind", - Year: 1939, - Genre: "Romance", - Poster: "https://iris-go.com/images/examples/mvc-movies/2.jpg", - }, - { - Name: "Citizen Kane", - Year: 1941, - Genre: "Mystery", - Poster: "https://iris-go.com/images/examples/mvc-movies/3.jpg", - }, - { - Name: "The Wizard of Oz", - Year: 1939, - Genre: "Fantasy", - Poster: "https://iris-go.com/images/examples/mvc-movies/4.jpg", - }, -} - - -var basicAuth = basicauth.New(basicauth.Config{ - Users: map[string]string{ - "admin": "password", - }, -}) - - -func main() { - app := iris.New() - - app.Use(basicAuth) - - app.Controller("/movies", new(MoviesController)) - - app.Run(iris.Addr(":8080")) -} - -// MoviesController 是 /movies controller. -type MoviesController struct { - mvc.C -} - -// 返回 movies列表 -// 例子: -// curl -i http://localhost:8080/movies -func (c *MoviesController) Get() []Movie { - return movies -} - -// GetBy 返回一个 movie -// 例子: -// curl -i http://localhost:8080/movies/1 -func (c *MoviesController) GetBy(id int) Movie { - return movies[id] -} - -// PutBy 更新一个 movie -// 例子: -// curl -i -X PUT -F "genre=Thriller" -F "poster=@/Users/kataras/Downloads/out.gif" http://localhost:8080/movies/1 -func (c *MoviesController) PutBy(id int) Movie { -    // 获取一个 movie - m := movies[id] - -    // 获取一个poster文件 -    file, info, err := c.Ctx.FormFile("poster") - if err != nil { - c.Ctx.StatusCode(iris.StatusInternalServerError) - return Movie{} - } -    file.Close()           // 我们不需要这个文件 -    poster := info.Filename // 比如这就是上传的文件url - genre := c.Ctx.FormValue("genre") - -    // 更新poster - m.Poster = poster - m.Genre = genre - movies[id] = m - - return m -} - -// DeleteBy 删除一个 movie -// 例子: -// curl -i -X DELETE -u admin:password http://localhost:8080/movies/1 -func (c *MoviesController) DeleteBy(id int) iris.Map { -    //从movies slice中删除索引 -    deleted := movies[id].Name - movies = append(movies[:id], movies[id+1:]...) -    // 返回删除movie的名称 -    return iris.Map{"deleted": deleted} -} -``` - -### MVC 快速指南 3 - -Iris是一个底层的Web开发框架,如果你喜欢按 **目录结构** 的约定方式开发,那么Iris框架对此毫无影响。 - -你可以根据自己的需求来创建目录结构,但是我建议你还是最好看看如下的目录结构例子: - -[![目录结构例子](_examples/mvc/overview/folder_structure.png)](_examples/mvc/overview) - -好了,直接上代码。 - - -#### 数据模型层 - -```go -// file: datamodels/movie.go - -package datamodels - -// Movie是我们例子数据结构 -// 此Movie可能会定义在类似"web/viewmodels/movie.go"的文件 -// Movie的数据模型在应用中只有一个,这样使用就很简单了 -type Movie struct { - ID int64 `json:"id"` - Name string `json:"name"` - Year int `json:"year"` - Genre string `json:"genre"` - Poster string `json:"poster"` -} -``` - -#### 数据层 / 数据存储层 - -```go -// file: datasource/movies.go - -package datasource - -import "github.com/kataras/iris/_examples/mvc/overview/datamodels" - -// Movies是模拟的数据源 -var Movies = map[int64]datamodels.Movie{ - 1: { - ID: 1, - Name: "Casablanca", - Year: 1942, - Genre: "Romance", - Poster: "https://iris-go.com/images/examples/mvc-movies/1.jpg", - }, - 2: { - ID: 2, - Name: "Gone with the Wind", - Year: 1939, - Genre: "Romance", - Poster: "https://iris-go.com/images/examples/mvc-movies/2.jpg", - }, - 3: { - ID: 3, - Name: "Citizen Kane", - Year: 1941, - Genre: "Mystery", - Poster: "https://iris-go.com/images/examples/mvc-movies/3.jpg", - }, - 4: { - ID: 4, - Name: "The Wizard of Oz", - Year: 1939, - Genre: "Fantasy", - Poster: "https://iris-go.com/images/examples/mvc-movies/4.jpg", - }, - 5: { - ID: 5, - Name: "North by Northwest", - Year: 1959, - Genre: "Thriller", - Poster: "https://iris-go.com/images/examples/mvc-movies/5.jpg", - }, -} -``` - -#### 数据仓库 - -数据仓库层直接访问数据源 - -```go -// file: repositories/movie_repository.go - -package repositories - -import ( - "errors" - "sync" - - "github.com/kataras/iris/_examples/mvc/overview/datamodels" -) - -// Query 是数据访问的集合入口 -type Query func(datamodels.Movie) bool - -// MovieRepository 中会有对movie实体的基本操作 -type MovieRepository interface { - Exec(query Query, action Query, limit int, mode int) (ok bool) - - Select(query Query) (movie datamodels.Movie, found bool) - SelectMany(query Query, limit int) (results []datamodels.Movie) - - InsertOrUpdate(movie datamodels.Movie) (updatedMovie datamodels.Movie, err error) - Delete(query Query, limit int) (deleted bool) -} - -// NewMovieRepository 返回movie内存数据 -func NewMovieRepository(source map[int64]datamodels.Movie) MovieRepository { - return &movieMemoryRepository{source: source} -} - -// movieMemoryRepository 就是 "MovieRepository",它管理movie的内存数据 -type movieMemoryRepository struct { - source map[int64]datamodels.Movie - mu sync.RWMutex -} - -const ( -    // 只读模式 - ReadOnlyMode = iota -    // 读写模式 - ReadWriteMode -) - -func (r *movieMemoryRepository) Exec(query Query, action Query, actionLimit int, mode int) (ok bool) { - loops := 0 - - if mode == ReadOnlyMode { - r.mu.RLock() - defer r.mu.RUnlock() - } else { - r.mu.Lock() - defer r.mu.Unlock() - } - - for _, movie := range r.source { - ok = query(movie) - if ok { - if action(movie) { - loops++ - if actionLimit >= loops { - break // break - } - } - } - } - - return -} - -// Select方法返回从模拟数据源找出的一个movie数据。 -// 当找到时就返回true,并停止迭代 -// -// Select 将会返回查询到的最新找到的movie数据,这样可以减少代码量 -// -// 自从我第一次想到用这种简单的原型函数后,我就经常用它了,希望这也对你有用 -func (r *movieMemoryRepository) Select(query Query) (movie datamodels.Movie, found bool) { - found = r.Exec(query, func(m datamodels.Movie) bool { - movie = m - return true - }, 1, ReadOnlyMode) - -    // 如果没有找到就让datamodels.Movie为空 -    // set an empty datamodels.Movie if not found at all. - if !found { - movie = datamodels.Movie{} - } - - return -} - -// 如果要查找很多值,用法基本一致,不过会返回datamodels.Movie slice。 -// 如果limit<=0,将返回全部数据 -func (r *movieMemoryRepository) SelectMany(query Query, limit int) (results []datamodels.Movie) { - r.Exec(query, func(m datamodels.Movie) bool { - results = append(results, m) - return true - }, limit, ReadOnlyMode) - - return -} - -// 插入或更新数据 -// -// 返回一个新的movie对象和error对象 -func (r *movieMemoryRepository) InsertOrUpdate(movie datamodels.Movie) (datamodels.Movie, error) { - id := movie.ID - - if id == 0 { // Create new action - var lastID int64 -        // 为了数据不重复,找到最大的ID。 -        // 生产环境你可以用第三方库生成一个UUID字串 - r.mu.RLock() - for _, item := range r.source { - if item.ID > lastID { - lastID = item.ID - } - } - r.mu.RUnlock() - - id = lastID + 1 - movie.ID = id - - // map-specific thing - r.mu.Lock() - r.source[id] = movie - r.mu.Unlock() - - return movie, nil - } -    //通过movie.ID更新数据 -    //这里举个例子看如果更新非空的poster和genre -    //其实我们可以直接更新对象r.source[id] = movie -    //用Select的话如下所示 - current, exists := r.Select(func(m datamodels.Movie) bool { - return m.ID == id - }) - -    if !exists { // ID不存在,返回error ID - return datamodels.Movie{}, errors.New("failed to update a nonexistent movie") - } - -    // 或者直接对象操作替换 -    // or comment these and r.source[id] = m for pure replace - if movie.Poster != "" { - current.Poster = movie.Poster - } - - if movie.Genre != "" { - current.Genre = movie.Genre - } - -    // 类map结构的处理 -    r.mu.Lock() - r.source[id] = current - r.mu.Unlock() - - return movie, nil -} - -func (r *movieMemoryRepository) Delete(query Query, limit int) bool { - return r.Exec(query, func(m datamodels.Movie) bool { - delete(r.source, m.ID) - return true - }, limit, ReadWriteMode) -} -``` - -#### 服务层 - -服务层主要调用“数据仓库”和“数据模型”的方法(即使是数据模型很简单的应用)。这一层将包含主要的数据处理逻辑。 - - -```go -// file: services/movie_service.go - -package services - -import ( - "github.com/kataras/iris/_examples/mvc/overview/datamodels" - "github.com/kataras/iris/_examples/mvc/overview/repositories" -) - -// MovieService主要包括对movie的CRUID(增删改查)操作。 -// MovieService主要调用movie 数据仓库的方法。 -// 下面例子的数据源是更高级别的组件 -// 这样可以用同样的逻辑可以返回不同的数据仓库 -// MovieService是一个接口,任何实现的地方都能用,这样可以替换不同的业务逻辑用来测试 -type MovieService interface { - GetAll() []datamodels.Movie - GetByID(id int64) (datamodels.Movie, bool) - DeleteByID(id int64) bool - UpdatePosterAndGenreByID(id int64, poster string, genre string) (datamodels.Movie, error) -} - -// NewMovieService 返回一个 movie 服务. -func NewMovieService(repo repositories.MovieRepository) MovieService { - return &movieService{ - repo: repo, - } -} - -type movieService struct { - repo repositories.MovieRepository -} - -// GetAll 返回所有 movies. -func (s *movieService) GetAll() []datamodels.Movie { - return s.repo.SelectMany(func(_ datamodels.Movie) bool { - return true - }, -1) -} - -// GetByID 是通过id找到movie. -func (s *movieService) GetByID(id int64) (datamodels.Movie, bool) { - return s.repo.Select(func(m datamodels.Movie) bool { - return m.ID == id - }) -} - - -// UpdatePosterAndGenreByID 更新一个 movie的 poster 和 genre. -func (s *movieService) UpdatePosterAndGenreByID(id int64, poster string, genre string) (datamodels.Movie, error) { - // update the movie and return it. - return s.repo.InsertOrUpdate(datamodels.Movie{ - ID: id, - Poster: poster, - Genre: genre, - }) -} - -// DeleteByID 通过id删除一个movie -// -// 返回true表示成功,其它都是失败 -func (s *movieService) DeleteByID(id int64) bool { - return s.repo.Delete(func(m datamodels.Movie) bool { - return m.ID == id - }, 1) -} -``` - -#### 视图模型 - -视图模型将处理结果返回给客户端 - -例子: -Example: - -```go -import ( - "github.com/kataras/iris/_examples/mvc/overview/datamodels" - - "github.com/kataras/iris/context" -) - -type Movie struct { - datamodels.Movie -} - -func (m Movie) IsValid() bool { -    /* 做一些检测,如果ID合法就返回true */ - return m.ID > 0 -} -``` - -Iris允许在HTTP Response Dispatcher中使用任何自定义数据结构, -所以理论上来说,除非万不得已,下面的代码不建议使用 - -```go -// Dispatch实现了`kataras/iris/mvc#Result`接口。在函数最后发送了一个`Movie`对象作为http response对象。 -// 如果ID小于等于0就回返回404,或者就返回json数据。 -//(这样就像控制器的方法默认返回自定义类型一样) -// -// 不要在这里写过多的代码,应用的主要逻辑不在这里 -// 在方法返回之前可以做个简单验证处理等等; -// -// 这里只是一个小例子,想想这个优势在设计大型应用是很有作用的 -// -// 这个方法是在`Movie`类型的控制器调用的。 -// 例子在这里:`controllers/movie_controller.go#GetBy`。 -func (m Movie) Dispatch(ctx context.Context) { - if !m.IsValid() { - ctx.NotFound() - return - } - ctx.JSON(m, context.JSON{Indent: " "}) -} -``` -然而,我们仅仅用"datamodels"作为一个数据模型包,是因为Movie数据结构没有包含敏感数据,客户端可以访问到其所有字段,我们不需要再有额外的功能去做验证处理了 - - -#### 控制器 - -控制器处理Web请求,它是服务层和客户端之间的桥梁 - -```go -// file: web/controllers/movie_controller.go - -package controllers - -import ( - "errors" - - "github.com/kataras/iris/_examples/mvc/overview/datamodels" - "github.com/kataras/iris/_examples/mvc/overview/services" - - "github.com/kataras/iris" - "github.com/kataras/iris/mvc" -) - -// MovieController是/movies的控制器 -type MovieController struct { - mvc.C - -    // MovieService是一个接口,主app对象会持有它 - Service services.MovieService -} - -// 获取movies列表 -// 例子: -// curl -i http://localhost:8080/movies -// -// 如果你有一些敏感的数据要处理的话,可以按照如下所示的方式: -// func (c *MovieController) Get() (results []viewmodels.Movie) { -// data := c.Service.GetAll() -// -// for _, movie := range data { -// results = append(results, viewmodels.Movie{movie}) -// } -// return -// } -//否则直接返回数据模型 -func (c *MovieController) Get() (results []datamodels.Movie) { - return c.Service.GetAll() -} - -// GetBy返回一个movie对象 -// 例子: -// curl -i http://localhost:8080/movies/1 -func (c *MovieController) GetBy(id int64) (movie datamodels.Movie, found bool) { -    return c.Service.GetByID(id) // 404 没有找到 -} - -// PutBy更新一个movie. -// 例子: -// curl -i -X PUT -F "genre=Thriller" -F "poster=@/Users/kataras/Downloads/out.gif" http://localhost:8080/movies/1 -func (c *MovieController) PutBy(id int64) (datamodels.Movie, error) { -    // 从请求中获取poster和genre - file, info, err := c.Ctx.FormFile("poster") - if err != nil { - return datamodels.Movie{}, errors.New("failed due form file 'poster' missing") - } -    // 关闭文件 -    file.Close() - -    //想象这就是一个上传文件的url - poster := info.Filename - genre := c.Ctx.FormValue("genre") - - return c.Service.UpdatePosterAndGenreByID(id, poster, genre) -} - -// DeleteBy删除一个movie对象 -// 例子: -// curl -i -X DELETE -u admin:password http://localhost:8080/movies/1 -func (c *MovieController) DeleteBy(id int64) interface{} { - wasDel := c.Service.DeleteByID(id) - if wasDel { -        // 返回要删除的ID - return iris.Map{"deleted": id} - } -    //现在我们可以看到这里可以返回一个有2个返回值(map或int)的函数 -    //我们并没有指定一个返回的类型 - return iris.StatusBadRequest -} -``` - -```go -// file: web/controllers/hello_controller.go - -package controllers - -import ( - "errors" - - "github.com/kataras/iris/mvc" -) - -// HelloController是控制器的例子 -// 下面会处理GET: /hello and GET: /hello/{name} -type HelloController struct { - mvc.C -} - -var helloView = mvc.View{ - Name: "hello/index.html", - Data: map[string]interface{}{ - "Title": "Hello Page", - "MyMessage": "Welcome to my awesome website", - }, -} - -// Get会返回预定义绑定数据的视图 -// -// `mvc.Result`是一个含有`Dispatch`方法的接口 -// `mvc.Response` 和 `mvc.View` dispatchers 内置类型 -// 你也可以通过实现`github.com/kataras/iris/mvc#Result`接口来自定义dispatchers -func (c *HelloController) Get() mvc.Result { - return helloView -} - -// 你可以定义一个标准通用的error -var errBadName = errors.New("bad name") - -//你也可以将error包裹在mvc.Response中,这样就和mvc.Result类型兼容了 -var badName = mvc.Response{Err: errBadName, Code: 400} - -// GetBy 返回 "Hello {name}" response -// 例子: -// curl -i http://localhost:8080/hello/iris -// curl -i http://localhost:8080/hello/anything -func (c *HelloController) GetBy(name string) mvc.Result { - if name != "iris" { - return badName -        // 或者 -        // GetBy(name string) (mvc.Result, error) { - // return nil, errBadName - // } - } - -    // 返回 mvc.Response{Text: "Hello " + name} 或者: - return mvc.View{ - Name: "hello/name.html", - Data: name, - } -} -``` - -```go -// file: web/middleware/basicauth.go - -package middleware - -import "github.com/kataras/iris/middleware/basicauth" - -// BasicAuth 中间件例 -var BasicAuth = basicauth.New(basicauth.Config{ - Users: map[string]string{ - "admin": "password", - }, -}) -``` - -```html - - - - - {{.Title}} - My App - - - -

{{.MyMessage}}

- - - -``` - -```html - - - - - {{.}}' Portfolio - My App - - - -

Hello {{.}}

- - - -``` - -> 戳[_examples/view](_examples/#view) 可以找到更多关于layouts,tmpl,routing的例子 - - -#### 程序入口 - -程序入口可以将任何组件包含进来 - -```go -// file: main.go - -package main - -import ( - "github.com/kataras/iris/_examples/mvc/overview/datasource" - "github.com/kataras/iris/_examples/mvc/overview/repositories" - "github.com/kataras/iris/_examples/mvc/overview/services" - "github.com/kataras/iris/_examples/mvc/overview/web/controllers" - "github.com/kataras/iris/_examples/mvc/overview/web/middleware" - - "github.com/kataras/iris" -) - -func main() { - app := iris.New() - -    // 加载模板文件 -    app.RegisterView(iris.HTML("./web/views", ".html")) - -    // 注册控制器 -    app.Controller("/hello", new(controllers.HelloController)) - -    // 创建movie 数据仓库,次仓库包含的是内存级的数据源 -    repo := repositories.NewMovieRepository(datasource.Movies) -    // 创建movie服务, 然后将其与控制器绑定 -    movieService := services.NewMovieService(repo) - - app.Controller("/movies", new(controllers.MovieController), -        // 将"movieService"绑定在 MovieController的Service接口 -        movieService, -        // 为/movies请求添加basic authentication(admin:password)中间件 - middleware.BasicAuth) - -    // 启动应用localhost:8080 - // http://localhost:8080/hello - // http://localhost:8080/hello/iris - // http://localhost:8080/movies - // http://localhost:8080/movies/1 - app.Run( - iris.Addr("localhost:8080"), - iris.WithoutVersionChecker, - iris.WithoutServerError(iris.ErrServerClosed), -        iris.WithOptimizations, // 可以启用快速json序列化等优化配置 -    ) -} -``` - -更多指南戳 [_examples/#structuring](_examples/#structuring) ## 现在你已经准备好进入下一阶段,又向专家级gopher迈进一步了 diff --git a/_examples/mvc/README.md b/_examples/mvc/README.md index a47ef736..e898b22e 100644 --- a/_examples/mvc/README.md +++ b/_examples/mvc/README.md @@ -133,872 +133,13 @@ By creating components that are independent of one another, developers are able If you're new to back-end web development read about the MVC architectural pattern first, a good start is that [wikipedia article](https://en.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93controller). -## Quick MVC Tutorial Part 1 (without output result) +## Examples -```go -package main +- [Hello world](mvc/hello-world/main.go) **UPDATED** +- [Session Controller](mvc/session-controller/main.go) **UPDATED** +- [Overview - Plus Repository and Service layers](mvc/overview) **UPDATED** +- [Login showcase - Plus Repository and Service layers](mvc/login) **UPDATED** +- [Singleton](mvc/singleton) **NEW** +- [Websocket Controller](mvc/websocket) **NEW** -import ( - "github.com/kataras/iris" - "github.com/kataras/iris/mvc" -) - -func main() { - app := iris.New() - - app.Controller("/helloworld", new(HelloWorldController)) - - app.Run(iris.Addr("localhost:8080")) -} - -type HelloWorldController struct { - mvc.C - - // [ Your fields here ] - // Request lifecycle data - // Models - // Database - // Global properties -} - -// -// GET: /helloworld - -func (c *HelloWorldController) Get() string { - return "This is my default action..." -} - -// -// GET: /helloworld/{name:string} - -func (c *HelloWorldController) GetBy(name string) string { - return "Hello " + name -} - -// -// GET: /helloworld/welcome - -func (c *HelloWorldController) GetWelcome() (string, int) { - return "This is the GetWelcome action func...", iris.StatusOK -} - -// -// GET: /helloworld/welcome/{name:string}/{numTimes:int} - -func (c *HelloWorldController) GetWelcomeBy(name string, numTimes int) { - // Access to the low-level Context, - // output arguments are optional of course so we don't have to use them here. - c.Ctx.Writef("Hello %s, NumTimes is: %d", name, numTimes) -} - -/* -func (c *HelloWorldController) Post() {} handles HTTP POST method requests -func (c *HelloWorldController) Put() {} handles HTTP PUT method requests -func (c *HelloWorldController) Delete() {} handles HTTP DELETE method requests -func (c *HelloWorldController) Connect() {} handles HTTP CONNECT method requests -func (c *HelloWorldController) Head() {} handles HTTP HEAD method requests -func (c *HelloWorldController) Patch() {} handles HTTP PATCH method requests -func (c *HelloWorldController) Options() {} handles HTTP OPTIONS method requests -func (c *HelloWorldController) Trace() {} handles HTTP TRACE method requests -*/ - -/* -func (c *HelloWorldController) All() {} handles All method requests -// OR -func (c *HelloWorldController) Any() {} handles All method requests -*/ -``` - -> The [_examples/mvc](https://github.com/kataras/iris/tree/master/_examples/mvc) and [mvc/controller_test.go](https://github.com/kataras/iris/blob/master/mvc/controller_test.go) files explain each feature with simple paradigms, they show how you can take advantage of the Iris MVC Binder, Iris MVC Models and many more... - -Every `exported` func prefixed with an HTTP Method(`Get`, `Post`, `Put`, `Delete`...) in a controller is callable as an HTTP endpoint. In the sample above, all funcs writes a string to the response. Note the comments preceding each method. - -An HTTP endpoint is a targetable URL in the web application, such as `http://localhost:8080/helloworld`, and combines the protocol used: HTTP, the network location of the web server (including the TCP port): `localhost:8080` and the target URI `/helloworld`. - -The first comment states this is an [HTTP GET](https://www.w3schools.com/tags/ref_httpmethods.asp) method that is invoked by appending "/helloworld" to the base URL. The third comment specifies an [HTTP GET](https://www.w3schools.com/tags/ref_httpmethods.asp) method that is invoked by appending "/helloworld/welcome" to the URL. - -Controller knows how to handle the "name" on `GetBy` or the "name" and "numTimes" at `GetWelcomeBy`, because of the `By` keyword, and builds the dynamic route without boilerplate; the third comment specifies an [HTTP GET](https://www.w3schools.com/tags/ref_httpmethods.asp) dynamic method that is invoked by any URL that starts with "/helloworld/welcome" and followed by two more path parts, the first one can accept any value and the second can accept only numbers, i,e: "http://localhost:8080/helloworld/welcome/golang/32719", otherwise a [404 Not Found HTTP Error](https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4.5) will be sent to the client instead. - ----- - -### Quick MVC Tutorial #2 - -Iris has a very powerful and **blazing [fast](_benchmarks)** MVC support, you can return any value of any type from a method function -and it will be sent to the client as expected. - -* if `string` then it's the body. -* if `string` is the second output argument then it's the content type. -* if `int` then it's the status code. -* if `error` and not nil then (any type) response will be omitted and error's text with a 400 bad request will be rendered instead. -* if `(int, error)` and error is not nil then the response result will be the error's text with the status code as `int`. -* if `bool` is false then it throws 404 not found http error by skipping everything else. -* if `custom struct` or `interface{}` or `slice` or `map` then it will be rendered as json, unless a `string` content type is following. -* if `mvc.Result` then it executes its `Dispatch` function, so good design patters can be used to split the model's logic where needed. - -The example below is not intended to be used in production but it's a good showcase of some of the return types we saw before; - -```go -package main - -import ( - "github.com/kataras/iris" - "github.com/kataras/iris/middleware/basicauth" - "github.com/kataras/iris/mvc" -) - -// Movie is our sample data structure. -type Movie struct { - Name string `json:"name"` - Year int `json:"year"` - Genre string `json:"genre"` - Poster string `json:"poster"` -} - -// movies contains our imaginary data source. -var movies = []Movie{ - { - Name: "Casablanca", - Year: 1942, - Genre: "Romance", - Poster: "https://iris-go.com/images/examples/mvc-movies/1.jpg", - }, - { - Name: "Gone with the Wind", - Year: 1939, - Genre: "Romance", - Poster: "https://iris-go.com/images/examples/mvc-movies/2.jpg", - }, - { - Name: "Citizen Kane", - Year: 1941, - Genre: "Mystery", - Poster: "https://iris-go.com/images/examples/mvc-movies/3.jpg", - }, - { - Name: "The Wizard of Oz", - Year: 1939, - Genre: "Fantasy", - Poster: "https://iris-go.com/images/examples/mvc-movies/4.jpg", - }, -} - - -var basicAuth = basicauth.New(basicauth.Config{ - Users: map[string]string{ - "admin": "password", - }, -}) - - -func main() { - app := iris.New() - - app.Use(basicAuth) - - app.Controller("/movies", new(MoviesController)) - - app.Run(iris.Addr(":8080")) -} - -// MoviesController is our /movies controller. -type MoviesController struct { - mvc.C -} - -// Get returns list of the movies -// Demo: -// curl -i http://localhost:8080/movies -func (c *MoviesController) Get() []Movie { - return movies -} - -// GetBy returns a movie -// Demo: -// curl -i http://localhost:8080/movies/1 -func (c *MoviesController) GetBy(id int) Movie { - return movies[id] -} - -// PutBy updates a movie -// Demo: -// curl -i -X PUT -F "genre=Thriller" -F "poster=@/Users/kataras/Downloads/out.gif" http://localhost:8080/movies/1 -func (c *MoviesController) PutBy(id int) Movie { - // get the movie - m := movies[id] - - // get the request data for poster and genre - file, info, err := c.Ctx.FormFile("poster") - if err != nil { - c.Ctx.StatusCode(iris.StatusInternalServerError) - return Movie{} - } - file.Close() // we don't need the file - poster := info.Filename // imagine that as the url of the uploaded file... - genre := c.Ctx.FormValue("genre") - - // update the poster - m.Poster = poster - m.Genre = genre - movies[id] = m - - return m -} - -// DeleteBy deletes a movie -// Demo: -// curl -i -X DELETE -u admin:password http://localhost:8080/movies/1 -func (c *MoviesController) DeleteBy(id int) iris.Map { - // delete the entry from the movies slice - deleted := movies[id].Name - movies = append(movies[:id], movies[id+1:]...) - // and return the deleted movie's name - return iris.Map{"deleted": deleted} -} -``` - -### Quick MVC Tutorial #3 - -Nothing stops you from using your favorite **folder structure**. Iris is a low level web framework, it has got MVC first-class support but it doesn't limit your folder structure, this is your choice. - -Structuring depends on your own needs. We can't tell you how to design your own application for sure but you're free to take a closer look to one typical example below; - -[![folder structure example](_examples/mvc/overview/folder_structure.png)](_examples/mvc/overview) - -Shhh, let's spread the code itself. - -#### Data Model Layer - -```go -// file: datamodels/movie.go - -package datamodels - -// Movie is our sample data structure. -// Keep note that the tags for public-use (for our web app) -// should be kept in other file like "web/viewmodels/movie.go" -// which could wrap by embedding the datamodels.Movie or -// declare new fields instead butwe will use this datamodel -// as the only one Movie model in our application, -// for the shake of simplicty. -type Movie struct { - ID int64 `json:"id"` - Name string `json:"name"` - Year int `json:"year"` - Genre string `json:"genre"` - Poster string `json:"poster"` -} -``` - -#### Data Source / Data Store Layer - -```go -// file: datasource/movies.go - -package datasource - -import "github.com/kataras/iris/_examples/mvc/overview/datamodels" - -// Movies is our imaginary data source. -var Movies = map[int64]datamodels.Movie{ - 1: { - ID: 1, - Name: "Casablanca", - Year: 1942, - Genre: "Romance", - Poster: "https://iris-go.com/images/examples/mvc-movies/1.jpg", - }, - 2: { - ID: 2, - Name: "Gone with the Wind", - Year: 1939, - Genre: "Romance", - Poster: "https://iris-go.com/images/examples/mvc-movies/2.jpg", - }, - 3: { - ID: 3, - Name: "Citizen Kane", - Year: 1941, - Genre: "Mystery", - Poster: "https://iris-go.com/images/examples/mvc-movies/3.jpg", - }, - 4: { - ID: 4, - Name: "The Wizard of Oz", - Year: 1939, - Genre: "Fantasy", - Poster: "https://iris-go.com/images/examples/mvc-movies/4.jpg", - }, - 5: { - ID: 5, - Name: "North by Northwest", - Year: 1959, - Genre: "Thriller", - Poster: "https://iris-go.com/images/examples/mvc-movies/5.jpg", - }, -} -``` - -#### Repositories - -The layer which has direct access to the "datasource" and can manipulate data directly. - -```go -// file: repositories/movie_repository.go - -package repositories - -import ( - "errors" - "sync" - - "github.com/kataras/iris/_examples/mvc/overview/datamodels" -) - -// Query represents the visitor and action queries. -type Query func(datamodels.Movie) bool - -// MovieRepository handles the basic operations of a movie entity/model. -// It's an interface in order to be testable, i.e a memory movie repository or -// a connected to an sql database. -type MovieRepository interface { - Exec(query Query, action Query, limit int, mode int) (ok bool) - - Select(query Query) (movie datamodels.Movie, found bool) - SelectMany(query Query, limit int) (results []datamodels.Movie) - - InsertOrUpdate(movie datamodels.Movie) (updatedMovie datamodels.Movie, err error) - Delete(query Query, limit int) (deleted bool) -} - -// NewMovieRepository returns a new movie memory-based repository, -// the one and only repository type in our example. -func NewMovieRepository(source map[int64]datamodels.Movie) MovieRepository { - return &movieMemoryRepository{source: source} -} - -// movieMemoryRepository is a "MovieRepository" -// which manages the movies using the memory data source (map). -type movieMemoryRepository struct { - source map[int64]datamodels.Movie - mu sync.RWMutex -} - -const ( - // ReadOnlyMode will RLock(read) the data . - ReadOnlyMode = iota - // ReadWriteMode will Lock(read/write) the data. - ReadWriteMode -) - -func (r *movieMemoryRepository) Exec(query Query, action Query, actionLimit int, mode int) (ok bool) { - loops := 0 - - if mode == ReadOnlyMode { - r.mu.RLock() - defer r.mu.RUnlock() - } else { - r.mu.Lock() - defer r.mu.Unlock() - } - - for _, movie := range r.source { - ok = query(movie) - if ok { - if action(movie) { - loops++ - if actionLimit >= loops { - break // break - } - } - } - } - - return -} - -// Select receives a query function -// which is fired for every single movie model inside -// our imaginary data source. -// When that function returns true then it stops the iteration. -// -// It returns the query's return last known "found" value -// and the last known movie model -// to help callers to reduce the LOC. -// -// It's actually a simple but very clever prototype function -// I'm using everywhere since I firstly think of it, -// hope you'll find it very useful as well. -func (r *movieMemoryRepository) Select(query Query) (movie datamodels.Movie, found bool) { - found = r.Exec(query, func(m datamodels.Movie) bool { - movie = m - return true - }, 1, ReadOnlyMode) - - // set an empty datamodels.Movie if not found at all. - if !found { - movie = datamodels.Movie{} - } - - return -} - -// SelectMany same as Select but returns one or more datamodels.Movie as a slice. -// If limit <=0 then it returns everything. -func (r *movieMemoryRepository) SelectMany(query Query, limit int) (results []datamodels.Movie) { - r.Exec(query, func(m datamodels.Movie) bool { - results = append(results, m) - return true - }, limit, ReadOnlyMode) - - return -} - -// InsertOrUpdate adds or updates a movie to the (memory) storage. -// -// Returns the new movie and an error if any. -func (r *movieMemoryRepository) InsertOrUpdate(movie datamodels.Movie) (datamodels.Movie, error) { - id := movie.ID - - if id == 0 { // Create new action - var lastID int64 - // find the biggest ID in order to not have duplications - // in productions apps you can use a third-party - // library to generate a UUID as string. - r.mu.RLock() - for _, item := range r.source { - if item.ID > lastID { - lastID = item.ID - } - } - r.mu.RUnlock() - - id = lastID + 1 - movie.ID = id - - // map-specific thing - r.mu.Lock() - r.source[id] = movie - r.mu.Unlock() - - return movie, nil - } - - // Update action based on the movie.ID, - // here we will allow updating the poster and genre if not empty. - // Alternatively we could do pure replace instead: - // r.source[id] = movie - // and comment the code below; - current, exists := r.Select(func(m datamodels.Movie) bool { - return m.ID == id - }) - - if !exists { // ID is not a real one, return an error. - return datamodels.Movie{}, errors.New("failed to update a nonexistent movie") - } - - // or comment these and r.source[id] = m for pure replace - if movie.Poster != "" { - current.Poster = movie.Poster - } - - if movie.Genre != "" { - current.Genre = movie.Genre - } - - // map-specific thing - r.mu.Lock() - r.source[id] = current - r.mu.Unlock() - - return movie, nil -} - -func (r *movieMemoryRepository) Delete(query Query, limit int) bool { - return r.Exec(query, func(m datamodels.Movie) bool { - delete(r.source, m.ID) - return true - }, limit, ReadWriteMode) -} -``` - -#### Services - -The layer which has access to call functions from the "repositories" and "models" (or even "datamodels" if simple application). It should contain the most of the domain logic. - -```go -// file: services/movie_service.go - -package services - -import ( - "github.com/kataras/iris/_examples/mvc/overview/datamodels" - "github.com/kataras/iris/_examples/mvc/overview/repositories" -) - -// MovieService handles some of the CRUID operations of the movie datamodel. -// It depends on a movie repository for its actions. -// It's here to decouple the data source from the higher level compoments. -// As a result a different repository type can be used with the same logic without any aditional changes. -// It's an interface and it's used as interface everywhere -// because we may need to change or try an experimental different domain logic at the future. -type MovieService interface { - GetAll() []datamodels.Movie - GetByID(id int64) (datamodels.Movie, bool) - DeleteByID(id int64) bool - UpdatePosterAndGenreByID(id int64, poster string, genre string) (datamodels.Movie, error) -} - -// NewMovieService returns the default movie service. -func NewMovieService(repo repositories.MovieRepository) MovieService { - return &movieService{ - repo: repo, - } -} - -type movieService struct { - repo repositories.MovieRepository -} - -// GetAll returns all movies. -func (s *movieService) GetAll() []datamodels.Movie { - return s.repo.SelectMany(func(_ datamodels.Movie) bool { - return true - }, -1) -} - -// GetByID returns a movie based on its id. -func (s *movieService) GetByID(id int64) (datamodels.Movie, bool) { - return s.repo.Select(func(m datamodels.Movie) bool { - return m.ID == id - }) -} - -// UpdatePosterAndGenreByID updates a movie's poster and genre. -func (s *movieService) UpdatePosterAndGenreByID(id int64, poster string, genre string) (datamodels.Movie, error) { - // update the movie and return it. - return s.repo.InsertOrUpdate(datamodels.Movie{ - ID: id, - Poster: poster, - Genre: genre, - }) -} - -// DeleteByID deletes a movie by its id. -// -// Returns true if deleted otherwise false. -func (s *movieService) DeleteByID(id int64) bool { - return s.repo.Delete(func(m datamodels.Movie) bool { - return m.ID == id - }, 1) -} -``` - -#### View Models - -There should be the view models, the structure that the client will be able to see. - -Example: - -```go -import ( - "github.com/kataras/iris/_examples/mvc/overview/datamodels" - - "github.com/kataras/iris/context" -) - -type Movie struct { - datamodels.Movie -} - -func (m Movie) IsValid() bool { - /* do some checks and return true if it's valid... */ - return m.ID > 0 -} -``` - -Iris is able to convert any custom data Structure into an HTTP Response Dispatcher, -so theoretically, something like the following is permitted if it's really necessary; - -```go -// Dispatch completes the `kataras/iris/mvc#Result` interface. -// Sends a `Movie` as a controlled http response. -// If its ID is zero or less then it returns a 404 not found error -// else it returns its json representation, -// (just like the controller's functions do for custom types by default). -// -// Don't overdo it, the application's logic should not be here. -// It's just one more step of validation before the response, -// simple checks can be added here. -// -// It's just a showcase, -// imagine the potentials this feature gives when designing a bigger application. -// -// This is called where the return value from a controller's method functions -// is type of `Movie`. -// For example the `controllers/movie_controller.go#GetBy`. -func (m Movie) Dispatch(ctx context.Context) { - if !m.IsValid() { - ctx.NotFound() - return - } - ctx.JSON(m, context.JSON{Indent: " "}) -} -``` - -However, we will use the "datamodels" as the only one models package because -Movie structure doesn't contain any sensitive data, clients are able to see all of its fields -and we don't need any extra functionality or validation inside it. - -#### Controllers - -Handles web requests, bridge between the services and the client. - -```go -// file: web/controllers/movie_controller.go - -package controllers - -import ( - "errors" - - "github.com/kataras/iris/_examples/mvc/overview/datamodels" - "github.com/kataras/iris/_examples/mvc/overview/services" - - "github.com/kataras/iris" - "github.com/kataras/iris/mvc" -) - -// MovieController is our /movies controller. -type MovieController struct { - mvc.C - - // Our MovieService, it's an interface which - // is binded from the main application. - Service services.MovieService -} - -// Get returns list of the movies. -// Demo: -// curl -i http://localhost:8080/movies -// -// The correct way if you have sensitive data: -// func (c *MovieController) Get() (results []viewmodels.Movie) { -// data := c.Service.GetAll() -// -// for _, movie := range data { -// results = append(results, viewmodels.Movie{movie}) -// } -// return -// } -// otherwise just return the datamodels. -func (c *MovieController) Get() (results []datamodels.Movie) { - return c.Service.GetAll() -} - -// GetBy returns a movie. -// Demo: -// curl -i http://localhost:8080/movies/1 -func (c *MovieController) GetBy(id int64) (movie datamodels.Movie, found bool) { - return c.Service.GetByID(id) // it will throw 404 if not found. -} - -// PutBy updates a movie. -// Demo: -// curl -i -X PUT -F "genre=Thriller" -F "poster=@/Users/kataras/Downloads/out.gif" http://localhost:8080/movies/1 -func (c *MovieController) PutBy(id int64) (datamodels.Movie, error) { - // get the request data for poster and genre - file, info, err := c.Ctx.FormFile("poster") - if err != nil { - return datamodels.Movie{}, errors.New("failed due form file 'poster' missing") - } - // we don't need the file so close it now. - file.Close() - - // imagine that is the url of the uploaded file... - poster := info.Filename - genre := c.Ctx.FormValue("genre") - - return c.Service.UpdatePosterAndGenreByID(id, poster, genre) -} - -// DeleteBy deletes a movie. -// Demo: -// curl -i -X DELETE -u admin:password http://localhost:8080/movies/1 -func (c *MovieController) DeleteBy(id int64) interface{} { - wasDel := c.Service.DeleteByID(id) - if wasDel { - // return the deleted movie's ID - return iris.Map{"deleted": id} - } - // right here we can see that a method function can return any of those two types(map or int), - // we don't have to specify the return type to a specific type. - return iris.StatusBadRequest -} -``` - -```go -// file: web/controllers/hello_controller.go - -package controllers - -import ( - "errors" - - "github.com/kataras/iris/mvc" -) - -// HelloController is our sample controller -// it handles GET: /hello and GET: /hello/{name} -type HelloController struct { - mvc.C -} - -var helloView = mvc.View{ - Name: "hello/index.html", - Data: map[string]interface{}{ - "Title": "Hello Page", - "MyMessage": "Welcome to my awesome website", - }, -} - -// Get will return a predefined view with bind data. -// -// `mvc.Result` is just an interface with a `Dispatch` function. -// `mvc.Response` and `mvc.View` are the built'n result type dispatchers -// you can even create custom response dispatchers by -// implementing the `github.com/kataras/iris/mvc#Result` interface. -func (c *HelloController) Get() mvc.Result { - return helloView -} - -// you can define a standard error in order to be re-usable anywhere in your app. -var errBadName = errors.New("bad name") - -// you can just return it as error or even better -// wrap this error with an mvc.Response to make it an mvc.Result compatible type. -var badName = mvc.Response{Err: errBadName, Code: 400} - -// GetBy returns a "Hello {name}" response. -// Demos: -// curl -i http://localhost:8080/hello/iris -// curl -i http://localhost:8080/hello/anything -func (c *HelloController) GetBy(name string) mvc.Result { - if name != "iris" { - return badName - // or - // GetBy(name string) (mvc.Result, error) { - // return nil, errBadName - // } - } - - // return mvc.Response{Text: "Hello " + name} OR: - return mvc.View{ - Name: "hello/name.html", - Data: name, - } -} -``` - -```go -// file: web/middleware/basicauth.go - -package middleware - -import "github.com/kataras/iris/middleware/basicauth" - -// BasicAuth middleware sample. -var BasicAuth = basicauth.New(basicauth.Config{ - Users: map[string]string{ - "admin": "password", - }, -}) -``` - -```html - - - - - {{.Title}} - My App - - - -

{{.MyMessage}}

- - - -``` - -```html - - - - - {{.}}' Portfolio - My App - - - -

Hello {{.}}

- - - -``` - -> Navigate to the [_examples/view](https://github.com/kataras/iris/tree/master/_examples/#view) for more examples -like shared layouts, tmpl funcs, reverse routing and more! - -#### Main - -This file creates any necessary component and links them together. - -```go -// file: main.go - -package main - -import ( - "github.com/kataras/iris/_examples/mvc/overview/datasource" - "github.com/kataras/iris/_examples/mvc/overview/repositories" - "github.com/kataras/iris/_examples/mvc/overview/services" - "github.com/kataras/iris/_examples/mvc/overview/web/controllers" - "github.com/kataras/iris/_examples/mvc/overview/web/middleware" - - "github.com/kataras/iris" -) - -func main() { - app := iris.New() - - // Load the template files. - app.RegisterView(iris.HTML("./web/views", ".html")) - - // Register our controllers. - app.Controller("/hello", new(controllers.HelloController)) - - // Create our movie repository with some (memory) data from the datasource. - repo := repositories.NewMovieRepository(datasource.Movies) - // Create our movie service, we will bind it to the movie controller. - movieService := services.NewMovieService(repo) - - app.Controller("/movies", new(controllers.MovieController), - // Bind the "movieService" to the MovieController's Service (interface) field. - movieService, - // Add the basic authentication(admin:password) middleware - // for the /movies based requests. - middleware.BasicAuth) - - // Start the web server at localhost:8080 - // http://localhost:8080/hello - // http://localhost:8080/hello/iris - // http://localhost:8080/movies - // http://localhost:8080/movies/1 - app.Run( - iris.Addr("localhost:8080"), - iris.WithoutVersionChecker, - iris.WithoutServerError(iris.ErrServerClosed), - iris.WithOptimizations, // enables faster json serialization and more - ) -} -``` - -More folder structure guidelines can be found at the [_examples/#structuring](https://github.com/kataras/iris/tree/master/_examples/#structuring) section. \ No newline at end of file +Folder structure guidelines can be found at the [_examples/#structuring](https://github.com/kataras/iris/tree/master/_examples/#structuring) section. \ No newline at end of file