Les flux RSS de mes articles https://etienner.fr/ Etienne ROUZEAUD | Développeur Web Full Stack fr-fr Copyright 2017 Connexion / déconnexion avec Gin https://etienner.fr/connexion-deconnexion-avec-gin <p>Lorsqu'un utilisateur s'authentifie sur une page de connexion, cela créé un cookie de session. De ce fait, si l'utilisateur tente de se rendre sur une page protégée sans s'être authentifié, ne possédant pas le cookie de session sur son navigateur Web, il ne pourra pas y accéder. Quant à la déconnexion, cela aura pour effet de supprimer le cookie de session.</p> <p>Dans un premier temps on va mettre en place un système simpliste, puis on introduira la notion de hachage de mot de passe avec Bcrypt, l'apport d'un token CSRF et on finira en créant une liste d'utilisateurs sur une base de donnée de type SQL via SQLite.</p> <p><img src="https://etienner.fr/assets/img/news/gin_session.gif" alt="" /></p> <h2>Préparation</h2> <h3>Packages externes</h3> <pre><code class="language-bash">go get github.com/gin-gonic/gin &amp;&amp; go get github.com/gorilla/securecookie &amp;&amp; go get github.com/gorilla/csrf &amp;&amp; github.com/mattn/go-sqlite3 &amp;&amp; github.com/jinzhu/gorm</code></pre> <ul> <li><strong>Gin</strong> : le micro framework ;</li> <li><strong>securecookie</strong> : cookie de session ;</li> <li><strong>csrf</strong> : gestion du token du même nom ;</li> <li><strong>go-sqlite3</strong> : le driver pour SQlite ;</li> <li><strong>gorm</strong> : l'ORM pour les requêtes SQL.</li> </ul> <h3>Vue d'ensemble des fichiers</h3> <p>Ci-dessous l'architecture de notre application de connexion / déconnexion.</p> <pre><code class="language-bash">│ main.go │ ├───controllers │ back.go │ front.go │ session.go │ ├───db │ db.go | ├───helpers │ cookies.go │ password.go │ └───views view_admin.html view_footer.html view_form.html view_header.html view_index.html</code></pre> <h3>Structure de données</h3> <p>Dans le dossier &quot;db/db.go&quot;, on crée la structure de données &quot;Users&quot;.</p> <pre><code class="language-go">package db // La structure de données type Users struct { Id int Name, Password string }</code></pre> <p>Un utilisateur possède un nom (&quot;Name&quot;) et un mot de passe (&quot;Password&quot;).</p> <h3>Template</h3> <p>Il serait dommage de se priver des templates avec Go.</p> <h4>Le header</h4> <p>On définit un template &quot;header&quot; contenant la majorité de notre code HTML dans le fichier &quot;views/view_header.html&quot;.</p> <pre><code class="language-markup">{{ define "header" }} &lt;!DOCTYPE html&gt; &lt;html&gt; &lt;head&gt; &lt;meta charset="utf-8"&gt; &lt;title&gt;Bienvenue sur mon site&lt;/title&gt; &lt;link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css"&gt; &lt;/head&gt; &lt;body class="container"&gt; &lt;nav class="navbar navbar-default"&gt; &lt;div class="container-fluid"&gt; &lt;div class="navbar-header"&gt; &lt;button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#bs-example-navbar-collapse-1" aria-expanded="false"&gt; &lt;span class="sr-only"&gt;Toggle navigation&lt;/span&gt; &lt;span class="icon-bar"&gt;&lt;/span&gt; &lt;span class="icon-bar"&gt;&lt;/span&gt; &lt;span class="icon-bar"&gt;&lt;/span&gt; &lt;/button&gt; &lt;a class="navbar-brand" href="/"&gt;Golang Website&lt;/a&gt; &lt;/div&gt; &lt;div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1"&gt; &lt;ul class="nav navbar-nav navbar-right"&gt; &lt;li {{ if eq .currentPage "login" }} class="active" {{ end }}&gt;&lt;a href="/login"&gt;Connexion&lt;/a&gt;&lt;/li&gt; &lt;li {{ if eq .currentPage "signUp" }} class="active" {{ end }}&gt;&lt;a href="/signup"&gt;Inscription&lt;/a&gt;&lt;/li&gt; &lt;li {{ if eq .currentPage "admin" }} class="active" {{ end }}&gt;&lt;a href="/admin"&gt;Admin&lt;/a&gt;&lt;/li&gt; {{ if .userName }} &lt;li class="dropdown"&gt; &lt;a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false"&gt;{{ .userName }}&lt;span class="caret"&gt;&lt;/span&gt;&lt;/a&gt; &lt;ul class="dropdown-menu"&gt; &lt;li&gt;&lt;a href="/logout"&gt;Logout&lt;/a&gt;&lt;/li&gt; &lt;/ul&gt; &lt;/li&gt; {{ end }} &lt;/ul&gt; &lt;/div&gt; &lt;/div&gt; &lt;/nav&gt; {{ if .success }} &lt;div class="alert alert-success" role="alert"&gt; {{ .success }} &lt;button type="button" class="close" data-dismiss="alert" aria-label="Close"&gt; &lt;span aria-hidden="true"&gt;&amp;times;&lt;/span&gt; &lt;/button&gt; &lt;/div&gt; {{ end }} {{ if .warning }} &lt;div class="alert alert-warning" role="alert"&gt; {{ .warning }} &lt;button type="button" class="close" data-dismiss="alert" aria-label="Close"&gt; &lt;span aria-hidden="true"&gt;&amp;times;&lt;/span&gt; &lt;/button&gt; &lt;/div&gt; {{ end }} {{ end }}</code></pre> <h4>Le footer</h4> <p>Puis un second template nommé &quot;footer&quot; dans le fichier &quot;views/view_footer.html&quot;.</p> <pre><code class="language-html">{{ define "footer" }} &lt;script src="https://ajax.googleapis.com/ajax/libs/jquery/3.2.1/jquery.min.js"&gt;&lt;/script&gt; &lt;script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js"&gt;&lt;/script&gt; &lt;/body&gt; &lt;/html&gt; {{ end }}</code></pre> <h2>Routeur</h2> <p>Dans le fichier &quot;main.go&quot;, on configure notre routeur dans la fonction principale.</p> <pre><code class="language-go">package main import ( "[chemin_du_projet]/controllers" "github.com/gin-gonic/gin" ) func main() { // Initialisation du routeur r := gin.Default() // Dossier des vues HTML r.LoadHTMLGlob("views/*") // Page d'accueil r.GET("/", controllers.IndexHandler) // Page admin (privée) r.GET("/admin", controllers.AdminHandler) // Page de connexion r.GET("/login", controllers.LoginHandlerForm) r.POST("/login", controllers.LoginHandler) // Page de déconnexion r.GET("/logout", controllers.LogoutHandler) // Port du serveur r.Run(":3000") }</code></pre> <p>On a donc 5 routes distinctes dont une pour notre futur formulaire de connexion. Le port utilisé est le &quot;3000&quot;.</p> <h2>Middleware</h2> <p>On va avoir besoin de récupérer la valeur du cookie contenu dans le fichier &quot;helpers/cookies.go&quot;</p> <pre><code class="language-go">import ( "[chemin_du_projet]/controllers" "[chemin_du_projet]/helpers" "github.com/gin-gonic/gin" )</code></pre> <p>La route &quot;admin&quot; n'étant pas si privée que ça... on va donc créer un middleware qui va vérifier si le cookie de session n'existe pas alors on redirige l'utilisateur vers la page de connexion.</p> <pre><code class="language-go">func Private() gin.HandlerFunc { return func(c *gin.Context) { _, err := c.Request.Cookie(helpers.CookieSessionName) if err != nil { c.Redirect(302, "/login") c.Abort() // Très important (sinon pas protégée avec curl -i http://localhost:3000/admin) } } }</code></pre> <p>Remarque : la variable <code>CookieSessionName</code> sera déclarée prochainement dans le helper &quot;session.go&quot; (vous pouvez commenter l'intérieur de la fonction <code>Private()</code> pour le moment...).</p> <p>Puis remplacez la route concernée en ajoutant le middleware.</p> <pre><code class="language-go">// Page admin (privée) r.GET("/admin", Private(), controllers.AdminHandler)</code></pre> <p>Il est également possible de créer un groupe de routes et d'attribuer ce middleware à tout ce groupe (vive le DRY :D).</p> <pre><code class="language-go">admin := r.Group("/admin") // Nom du groupe "admin" dont l'URI est "/admin" admin.Use(Private()) // Utilisation du middleware Private() { // Page admin (privée) admin.GET("", controllers.AdminHandler) // Autres routes dans "/admin" // ... }</code></pre> <h2>Helper</h2> <h3>Les cookies</h3> <p>Ce helper va nous servir à créer un cookie de session, récupérer les infos de ce cookie, supprimer ce cookie mais également créer un cookie &quot;flash&quot; (sans durée), le supprimer sans oublier l'encodage et le décodage de sa valeur (pour les caractères spéciaux).</p> <h4>Initialisation de la session</h4> <p>Dans le fichier &quot;helpers/cookies.go&quot;.</p> <pre><code class="language-go">package helpers import ( "net/http" "net/url" "github.com/gin-gonic/gin" "github.com/gorilla/securecookie" ) // Création du cookie sécurisé var cookieHandler = securecookie.New(securecookie.GenerateRandomKey(64), securecookie.GenerateRandomKey(32)) // Nom du cookies de session var CookieSessionName = "session"</code></pre> <p>On importe le package &quot;net/http&quot; pour travailler sur les cookies ainsi que &quot;gorilla/securecookie&quot; pour créer un cookie sécurisé que l'on déclare dans une variable nommée &quot;cookieHandler&quot; via la fonction <code>securecookie.New()</code>. Le premier argument sert à définir une clef de hachage et le second à définir une clef de blocage. Quant aux packages &quot;net/http&quot; et &quot;net/url&quot;, ils nous serviront pour les cookies flash.</p> <h4>Création de la session</h4> <p>Puis on crée la fonction <code>SetSession</code> pour définir un cookie à partir du nom de l'utilisateur.</p> <pre><code class="language-go">func SetSession(userName string, c *gin.Context) { value := map[string]string{ "name": userName, } if encoded, err := cookieHandler.Encode(CookieSessionName, value); err == nil { cookie := &amp;http.Cookie{ Name: CookieSessionName, Value: encoded, Path: "/", HttpOnly: true, //Secure: true, SEULEMENT DISPONIBLE AVEC HTTPS ACTIVE } http.SetCookie(c.Writer, cookie) } }</code></pre> <ol> <li>On stocke la valeur de l'utilisateur dans une variable (<code>value</code>) ;</li> <li>On encode notre cookie à partir du super cookie <code>cookieHandler</code> dans la variable <code>encoded</code> ;</li> <li>Le cookie possède également un nom (&quot;session&quot; dans notre cas via la variable <code>cookieSessionName</code>) ainsi qu'un chemin.</li> </ol> <p>Je vous conseille d'activer le flag &quot;HttpOnly&quot; pour des raisons de sécurité (cela empèche notamment l'exécution de la fonction JavaScript <code>document.cookie</code>). Vous pouvez ajouter plus de sécurité si votre application est en HTTPS en définissant le cookie de session comme étant de type &quot;secure&quot; (<code>Secure: true</code>).</p> <h4>Récupérer la valeur de l'utilisateur</h4> <p>Puis on crée une autre fonction <code>getUserName</code> pour récupérer le nom d'utilisateur stocké dans le cookie.</p> <pre><code class="language-go">func GetUserName(c *gin.Context) (userName string) { if cookie, err := c.Request.Cookie(CookieSessionName); err == nil { cookieValue := make(map[string]string) if err = cookieHandler.Decode(CookieSessionName, cookie.Value, &amp;cookieValue); err == nil { userName = cookieValue["name"] } } return userName }</code></pre> <p>Si le cookie existe, alors on récupère la valeur &quot;name&quot; du cookie que l'on retourne comme chaine de caractères.</p> <h4>Supprimer la session</h4> <p>Et une autre fonction <code>ClearSession</code> pour détruire le cookie lors de la déconnexion.</p> <pre><code class="language-go">func ClearSession(c *gin.Context) { cookie := &amp;http.Cookie{ Name: CookieSessionName, Value: "", Path: "/", MaxAge: -1, } http.SetCookie(c.Writer, cookie) }</code></pre> <h4>Encodage / décodage de valeurs</h4> <p>On a besoin d'encoder et de décoder les cookies pour accepter les caractères spéciaux.</p> <pre><code class="language-go">// Encodage de la valeur du cookie func encode(value string) string { encode := &amp;url.URL{Path: value} return encode.String() } // Décodage de la valeur du cookie func decode(value string) string { decode, _ := url.QueryUnescape(value) return decode }</code></pre> <h4>Création du cookie &quot;flash&quot;</h4> <p>On aura besoin de stocker des données dans un cookie provisoire.</p> <pre><code class="language-go">func SetFlashCookie(c *gin.Context, name string, value string) { cookie := &amp;http.Cookie{ Name: name, Value: encode(value), Path: "/", MaxAge: 1, } http.SetCookie(c.Writer, cookie) }</code></pre> <p>On prend soin d'encoder la valeur du cookie. Avec un age très limité puisqu'il s'agit de données provisoires (message de succès, d'avertissement, d'erreur...).</p> <h4>Récupération de la valeur du cookie &quot;flash&quot;</h4> <p>On aura besoin de récupérer un cookie provisoire pour informer l'utilisateur.</p> <pre><code class="language-go">func GetFlashCookie(c *gin.Context, name string) (value string) { cookie, err := c.Request.Cookie(name) var cookieValue string if err == nil { cookieValue = cookie.Value } else { cookieValue = cookieValue } return decode(cookieValue) }</code></pre> <p>On retourne la valeur... décodée.</p> <h2>Contôleurs</h2> <p>On va traiter nos 3 contrôleurs en commencant par le front puis la session et pour finir, le back.</p> <h3>Front</h3> <p>Dans le fichier &quot;controllers/front.go&quot;.</p> <pre><code class="language-go">package controllers import ( "[chemin_du_projet]/helpers" "github.com/gin-gonic/gin" ) func IndexHandler(c *gin.Context) { userName := helpers.GetUserName(c) c.HTML(200, "view_index.html", gin.H{ "userName": userName, "currentPage": "index", "success": helpers.GetFlashCookie(c, "success"), }) }</code></pre> <p>On récupère (si elle existe), la valeur du cookie de session via la fonction <code>GetUserName(c)</code>.</p> <p>On affiche la page web dans &quot;views/view_index.html&quot;.</p> <pre><code class="language-html">{{ template "header" . }} &lt;h1&gt;Bienvenue :)&lt;/h1&gt; &lt;p&gt;Vous êtes sur la partie front de ce site Web.&lt;/p&gt; {{ template "footer" }}</code></pre> <h3>Session</h3> <p>On s'occupe de créer le formulaire &quot;views/view_form.html&quot; qui servira à la fois pour se connecter et pour s'inscrire.</p> <pre><code class="language-html">{{ template "header" . }} &lt;div class="col-sm-6 col-sm-offset-3 form-box"&gt; &lt;div class="form-top"&gt; &lt;div class="form-top-left"&gt; &lt;h3&gt;{{ if eq .currentPage "login" }}Connexion{{ else }}Inscription{{ end }}&lt;/h3&gt; &lt;/div&gt; &lt;/div&gt; &lt;div class="form-bottom"&gt; &lt;form role="form" action="" method="post" class="login-form"&gt; &lt;div class="form-group"&gt; &lt;label for="form-username"&gt;Nom d'utilisateur&lt;/label&gt; &lt;input name="username" placeholder="Username..." class="form-username form-control" id="form-username" type="text" required&gt; &lt;/div&gt; &lt;div class="form-group"&gt; &lt;label for="form-password"&gt;Mot de passe&lt;/label&gt; &lt;input name="password" placeholder="Password..." class="form-password form-control" id="form-password" type="password" required&gt; &lt;/div&gt; &lt;button type="submit" class="btn btn-primary"&gt;{{ if eq .currentPage "login" }}Se connecter{{ else }}Valider mon inscription{{ end }}&lt;/button&gt; &lt;/form&gt; &lt;/div&gt; &lt;/div&gt; {{ template "footer" }}</code></pre> <p>Le plus important étant la présence des 2 champs obligatoire.</p> <p>Dans le fichier &quot;controllers/session.go&quot;.</p> <pre><code class="language-go">package controllers import ( "[chemin_du_projet]/db" "[chemin_du_projet]/helpers" "github.com/gin-gonic/gin" )</code></pre> <p>Puis dans notre contrôleur, on affiche notre formulaire dans la route <code>LoginHandlerForm</code>.</p> <pre><code class="language-go">func LoginHandlerForm(c *gin.Context) { // Récupération du nom d'utilisateur pour le templating userName := helpers.GetUserName(c) c.HTML(200, "view_form.html", gin.H{ "userName": userName, "currentPage": "login", "warning": helpers.GetFlashCookie(c, "warning"), }) }</code></pre> <p>On récupère les données rentrées dans le formulaire (nom d'utilisateur et mot de passe) dans la route de type POST <code>LoginHandlerForm</code>.</p> <pre><code class="language-go">func LoginHandler(c *gin.Context) { // Utilisateur concerné user := db.Users{ "toto", "password", } // Récupération des champs name := c.PostForm("username") pass := c.PostForm("password") if name != "" &amp;&amp; pass != "" { if name == user.Name &amp;&amp; pass == user.Password { helpers.SetSession(name, c) helpers.SetFlashCookie(c, "success", "Bienvenue "+user.Name) // Redirection vers la page protégée c.Redirect(302, "/admin") } else { // Pas bon :( helpers.SetFlashCookie(c, "warning", "Identifiants incorrects") c.Redirect(302, "/login") } } }</code></pre> <p>Si la correspondance est correcte alors on instancie le cookie de session et on redirige vers la page privée. Sinon on stocke un message d'avertissement dans un cookie de type &quot;warning&quot;.</p> <p>Dans ce contrôleur, il ne nous manque plus que la déconnexion avec la route <code>LogoutHandler</code>.</p> <pre><code class="language-go">func LogoutHandler(c *gin.Context) { helpers.ClearSession(c) helpers.SetFlashCookie(c, "success", "Vous êtes désormais déconnecté(e)") c.Redirect(302, "/") } </code></pre> <p>On supprime le cookie de session avec <code>ClearSession</code> et on redirige vers la page d'accueil avec le message contenu dans le cookie flash.</p> <h3>Back</h3> <p>On crée notre magnifique page admin dans &quot;views/views_admin.html&quot;.</p> <pre><code class="language-html">{{ template "header" . }} &lt;h1&gt;Admin (page protégée)&lt;/h1&gt; {{ template "footer"}}</code></pre> <p>Dans la fonction de la route <code>AdminHandler</code> on appel la variable <code>userName</code> à partir de la fonction <code>getUserName</code>.</p> <pre><code class="language-go">package controllers import ( "[chemin_du_projet]/helpers" "github.com/gin-gonic/gin" ) // Admin func AdminHandler(c *gin.Context) { // Récupération du nom d'utilisateur pour le templating userName := helpers.GetUserName(c) c.HTML(200, "view_admin.html", gin.H{ "userName": userName, "currentPage": "admin", "success": helpers.GetFlashCookie(c, "success") }) }</code></pre> <h2>Bcrypt</h2> <p>Présent dans de nombreux langage de programmation, Bcrypt est une fonction de hash. Contrairement à MD5, SHA-1, SHA-256, etc..., le mot de passe est haché différement et de ce fait est bien plus résistant aux attaques par force brute. Bcrypt utilise 3 critères :</p> <ul> <li><strong>cost</strong> : le coût souhaité de l'algorithme (suivant la puissance de la machine hébergeant le serveur) ;</li> <li><strong>salt</strong> : sel de l'algorithme ;</li> <li><strong>key</strong> : le mot de passe que l'on souhaite encoder.</li> </ul> <p>On aura donc besoin de hacher et de comparer.</p> <h3>Préparation du fichier</h3> <p>Dans le fichier &quot;helpers/password.go&quot;, on importe la librairie <code>golang.org/x/crypto/bcrypt</code>.</p> <pre><code class="language-go">package helpers import ( "golang.org/x/crypto/bcrypt" )</code></pre> <h3>Hachage</h3> <pre><code class="language-go">func HashPassword(password string) (string, error) { bytes, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) return string(bytes), err }</code></pre> <p>Dans la fonction <code>hashPassword</code>, on passe en paramètre le mot de passe en clair. On retourne une chaine de caractères via la fonction <code>GenerateFromPassword</code>. Le coût (&quot;cost&quot;) est definit à 10 (le minimum étant 4 <code>bcrypt.MinCost</code>, le maximum 31 <code>bcrypt.MaxCost</code> et par défaut 10 <code>bcrypt.DefaultCost</code>).</p> <h3>Vérification par comparaison</h3> <pre><code class="language-go">func CheckPasswordHash(password, hash string) bool { err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)) return err == nil }</code></pre> <p>On passe en paramètre le mot de passe et le mot de passe haché. On retourne un booléen via la fonction <code>CompareHashAndPassword</code>.</p> <h3>Testons</h3> <p>Dans la fonction <code>LoginHandler</code>, on modifie les lignes concernées.</p> <pre><code class="language-go">func LoginHandler(c *gin.Context) { // Utilisateur concerné user := db.Users{ 1, "toto", "password", } // Mot de passe haché ("password") hash, _ := helpers.HashPassword(user.Password) // Récupération des champs name := c.PostForm("username") pass := c.PostForm("password") if name != "" &amp;&amp; pass != "" { if name == user.Name &amp;&amp; helpers.CheckPasswordHash(pass, hash) { helpers.SetSession(name, c) helpers.SetFlashCookie(c, "success", "Bienvenue "+user.Name) // Redirection vers la page protégée c.Redirect(302, "/admin") } else { // Pas bon :( helpers.SetFlashCookie(c, "warning", "Identifiants incorrects") c.Redirect(302, "/login") } } }</code></pre> <p>On passe en paramètre le mot de passe et le mot de passe haché. On retourne un booléen via la fonction <code>CompareHashAndPassword</code>.</p> <h2>Token CSRF</h2> <p>Le token CSRF (Cross-Site Request Forgery) permet de se protéger contre une faille bien connue, l'attaque XSS. Avec la génération de ce token aussi bien coté utilisateur que coté serveur, cela permet de renforcer la sécurité à moindres frais.</p> <h3>Coté serveur</h3> <p>Dans le fichier &quot;main.go&quot;, importez le package officiel &quot;net/http&quot; et le package pour notre token &quot;github.com/gorilla/csrf&quot;. Puis remplacez la ligne <code>r.Run(":3000")</code> par la ligne ci-dessous.</p> <pre><code class="language-go">http.ListenAndServe(":3000", csrf.Protect([]byte("32-byte-long-auth-key"), csrf.Secure(false))(r))</code></pre> <p>Il est important de spécifier le cookie de type Secure à false si HTTPS n'est pas activé (sinon le cookie ne fonctionnera pas).</p> <p>Remarque : vous pouvez remplacez <code>[]byte("32-byte-long-auth-key")</code> par <code>securecookie.GenerateRandomKey(32)</code> (après avoir importé le package &quot;github.com/gorilla/securecookie&quot;).</p> <p>Dans le fichier &quot;controller/session.go&quot;, dans la fonction <code>LoginHandlerForm</code>, ajoutez la propriété <code>csrf.TemplateTag</code> avec pour valeur <code>csrf.TemplateField(c.Request)</code>.</p> <pre><code class="language-go">c.HTML(200, "view_form.html", gin.H{ "userName": userName, "currentPage": "login", "warning": helpers.GetFlashCookie(c, "warning"), csrf.TemplateTag: csrf.TemplateField(c.Request), })</code></pre> <p>N'oubliez pas d'importer le package &quot;github.com/gorilla/csrf&quot; dans ce fichier.</p> <h3>Coté client</h3> <p>Dans le formulaire (&quot;views/view_form&quot;), ajoutez la valeur <code>{{ .csrfField }}</code> avant la fermeture de la balise <code>&lt;/form&gt;</code>. Cela aura pour effet d'ajouter un champ de type &quot;hidden&quot; dans le formulaire contenant le token comme valeur. En effet, la valeur <code>csrf.TemplateField(c.Request)</code> produit le code ci-dessous.</p> <pre><code class="language-go">fmt.Sprintf(`&lt;input type="hidden" name="%s" value="%s"&gt;`, "myFieldName", Token(r))</code></pre> <p>Un exemple de rendu en HTML.</p> <pre><code class="language-html">&lt;input name="gorilla.csrf.Token" value="JZ6ZacxKVbWWReZh90RgOtYgjzptXRL8NnpbCtI7a7P/R/oAtIWrgcUY5DgEDIiUmnMX3HfupUm3r4C3xci/IQ==" type="hidden"&gt;</code></pre> <p>Si vous supprimez ou modifiez la valeur du champ, vous aurez le message suivant &quot;Forbidden - CSRF token invalid&quot; avec une erreur HTTP 403.</p> <h2>Base de données</h2> <p>Jusqu'à présent on a testé avec un seul compte utilisateur. On va s'assurer que les futurs utilisateurs puissent s'inscrire tout en ayant leur mot de passe haché dans la base de données et en prenant en compte l'unicité des comptes.</p> <h3>Connexion à SQLite</h3> <p>Dans le fichier &quot;db.go&quot; présent dans le dossier &quot;db&quot;, on importe l'ORM et le driver pour SQLite.</p> <pre><code class="language-go">package db // Les imports de librairies import ( "github.com/jinzhu/gorm" _ "github.com/mattn/go-sqlite3" ) // La structure de données type Users struct { Id int `gorm:"AUTO_INCREMENT" form:"id"` Name string `gorm:"not null;unique" form:"username"` // Utilisateur unique! Password string `gorm:"not null" form:"password"` } // Connexion à la BDD SQLite func InitDb() *gorm.DB { // Ouverture de la connexion vers la BDD SQLite db, err := gorm.Open("sqlite3", "./data.db") // Afficher les requêtes SQL (facultatif) db.LogMode(true) // Création de la table "users" if !db.HasTable(&amp;Users{}) { db.CreateTable(&amp;Users{}) db.Set("gorm:table_options", "ENGINE=InnoDB").CreateTable(&amp;Users{}) } if err != nil { panic(err) } return db }</code></pre> <p>On modifie la structure de données pour l'accorder avec l'ORM Gorm. Et on initialise la connexion au fichier SQLite &quot;data.db&quot;.</p> <h3>Formulaire d'inscription</h3> <p>Dans le fichier &quot;main.go&quot;, ajoutez les 2 routes ci-dessous.</p> <pre><code class="language-go">// Page d'inscription r.GET("/signup", controllers.SignUpHandlerForm) r.POST("/signup", controllers.SignUpHandler)</code></pre> <p>Puis dans le contrôleur &quot;session.go&quot;.</p> <pre><code class="language-go">func SignUpHandlerForm(c *gin.Context) { // Récupération du nom d'utilisateur pour le templating userName := helpers.GetUserName(c) c.HTML(200, "view_form.html", gin.H{ "userName": userName, "currentPage": "signUp", "warning": helpers.GetFlashCookie(c, "warning"), csrf.TemplateTag: csrf.TemplateField(c.Request), }) }</code></pre> <p>On utilise le même formulaire que pour la connexion sans oublier de générer le token CSRF.</p> <p>Et la route de vérification de type &quot;POST&quot; <code>SignUpHandler</code>.</p> <pre><code class="language-go">func SignUpHandler(c *gin.Context) { var user db.Users if c.Bind(&amp;user) == nil { // Mot de passe haché hash, _ := helpers.HashPassword(user.Password) user.Password = hash // Connexion à SQLite dbmap := db.InitDb() defer dbmap.Close() if err := dbmap.Where("name = ?", user.Name).First(&amp;user).Error; err == nil { helpers.SetFlashCookie(c, "warning", "Inscription refusée, le nom d'utitlisateur "+user.Name+" existe déjà") c.Redirect(302, "/signup") } else { // Création de l'utilisateur dbmap.Create(&amp;user) // Création du cookie de session helpers.SetSession(user.Name, c) c.Redirect(302, "/admin") } } else { helpers.SetFlashCookie(c, "warning", "Champs non remplis") c.Redirect(302, "/signup") } }</code></pre> <ol> <li>On récupère les données postées dans le formulaire d'inscription avec la fonction <code>c.Bind</code> ;</li> <li>On hash le mot de passe via la fonction <code>hashPassword</code> ;</li> <li>On s'assure que le nom d'utilisateur n'existe pas dans la BDD.</li> <li>On se connecte à SQLite afin d'insérer les données avec la fonction <code>Create</code> ;</li> </ol> <h3>Connexion</h3> <p>Une fois le ou les utilisateurs créés, il faut modifier la route <code>LoginHandler</code>.</p> <pre><code class="language-go">func LoginHandler(c *gin.Context) { var user db.Users // Binding du formulaire if c.Bind(&amp;user) == nil { // Récupération du mdp en clair clearPassword := user.Password // Connexion au fichier SQLite dbmap := db.InitDb() defer dbmap.Close() if err := dbmap.Where("name = ?", user.Name).First(&amp;user).Error; err == nil { // Vérification du mdp if helpers.CheckPasswordHash(clearPassword, user.Password) { // Création du cookie de session helpers.SetSession(user.Name, c) helpers.SetFlashCookie(c, "success", "Bienvenue "+user.Name) c.Redirect(302, "/admin") } else { // MDP incorrect helpers.SetFlashCookie(c, "warning", "Mot de passe incorrect") c.Redirect(302, "/login") } } else { // Nom d'utilisateur incorrect helpers.SetFlashCookie(c, "warning", "Nom d'utilisateur incorrect") c.Redirect(302, "/login") } } else { // Champs non remplis helpers.SetFlashCookie(c, "warning", "Champs non remplis") c.Redirect(302, "/login") } }</code></pre> <ol> <li>On récupère les données postées dans le formulaire de connexion avec la fonction <code>c.Bind</code> ;</li> <li>On récupère le mot de passe en clair saisi par l'utilisateur ;</li> <li>On se connecte à SQLite afin de trouver le nom d'utilisateur saisie ;</li> <li>Avec la fonction <code>checkPasswordHash</code> qui renvoie un booléen, on s'assure que le mot de passe en clair et le mot de passe haché corresponde bien ;</li> <li>Bingo! On instancie le cookie de session via la fonction <code>SetSession</code> et on redirige l'utilisateur vers la page &quot;admin&quot;.</li> </ol> <h3>Afficher tous les utilisateur</h3> <p>Dans la page admin, on va afficher la liste des utilisateurs dans un tableau.</p> <pre><code class="language-go">func AdminHandler(c *gin.Context) { var users []db.Users dbmap := db.InitDb() defer dbmap.Close() dbmap.Find(&amp;users) // Récupération du nom d'utilisateur pour le templating userName := helpers.GetUserName(c) c.HTML(200, "view_admin.html", gin.H{ "userName": userName, "currentPage": "admin", "users": users, "success": helpers.GetFlashCookie(c, "success"), }) }</code></pre> <p>Pour que ce code fonctionne, importez le package interne &quot;[chemin_du_projet]/db&quot;.</p> <p>Puis dans la vue associée (&quot;view_admin.html&quot;), on boucle le tableau de données contenu dans la variable <code>users</code> avec <code>range</code>.</p> <pre><code class="language-html">{{ template "header" . }} &lt;h1 class="text-center"&gt;Liste des utilisateurs&lt;/h1&gt; &lt;div class="table-responsive"&gt; &lt;table class="table table-bordered"&gt; &lt;tr&gt; &lt;th&gt;Id&lt;/th&gt; &lt;th&gt;Name&lt;/th&gt; &lt;th&gt;Password (Bcrypt)&lt;/th&gt; &lt;/tr&gt; {{ range $user := .users }} &lt;tr&gt; &lt;td&gt;{{ $user.Id }}&lt;/td&gt; &lt;td&gt;{{ $user.Name }}&lt;/td&gt; &lt;td&gt;{{ $user.Password }}&lt;/td&gt; &lt;/tr&gt; {{ end }} &lt;/table&gt; &lt;/div&gt; {{ template "footer" }}</code></pre> <h2>Conclusion</h2> <p>La connexion et la déconnexion fonctionnent. Maintenant il faudrait offrir la possibilité à l'utilisateur de modifier son mot de passe et mettre en place un système de récupération du mot de passe par email.</p> <h2>Sources</h2> <ul> <li><a href="https://github.com/gin-gonic/gin"><a href="https://github.com/gin-gonic/gin">https://github.com/gin-gonic/gin</a></a></li> <li><a href="https://github.com/gorilla/securecookie"><a href="https://github.com/gorilla/securecookie">https://github.com/gorilla/securecookie</a></a></li> <li><a href="https://github.com/gorilla/csrf"><a href="https://github.com/gorilla/csrf">https://github.com/gorilla/csrf</a></a></li> <li><a href="https://github.com/jinzhu/gorm"><a href="https://github.com/jinzhu/gorm">https://github.com/jinzhu/gorm</a></a></li> <li><a href="https://gowebexamples.github.io/sessions"><a href="https://gowebexamples.github.io/sessions">https://gowebexamples.github.io/sessions</a></a></li> <li><a href="https://gowebexamples.github.io/password-hashing"><a href="https://gowebexamples.github.io/password-hashing">https://gowebexamples.github.io/password-hashing</a></a></li> <li><a href="http://www.alexedwards.net/blog/simple-flash-messages-in-golang"><a href="http://www.alexedwards.net/blog/simple-flash-messages-in-golang">http://www.alexedwards.net/blog/simple-flash-messages-in-golang</a></a></li> <li><a href="https://golang.org/pkg/html/template"><a href="https://golang.org/pkg/html/template">https://golang.org/pkg/html/template</a></a></li> <li><a href="https://fr.wikipedia.org/wiki/Bcrypt"><a href="https://fr.wikipedia.org/wiki/Bcrypt">https://fr.wikipedia.org/wiki/Bcrypt</a></a></li> </ul>; 2017-08-14 15:30:00 Les filtres en JavaScript https://etienner.fr/les-filtres-en-javascript <p>La méthode <code>filter</code> permet de créer un tableau contenant les éléments filtrés d'un autre tableau. Cette méthode est utilisée dans les frameworks JS comme Angular, Vue, etc... mais elle vient avant tout de la programmation fonctionnelle.</p> <p><img src="https://etienner.fr/assets/img/news/javascript_filters.gif" alt="Apercu" /></p> <h2>Anatomie d'un filtre</h2> <p>On met en place une fonction qui va nous servir de filtre.</p> <pre><code class="language-javascript">function Monfiltre(element, index, self) { console.log(element); console.log(index); console.log(self); }</code></pre> <p>On peut mettre 3 paramètres dans le callback d'un filtre :</p> <ul> <li>la valeur de l'élément courant (obligatoire) (string);</li> <li>l'index de l'élément courant (number);</li> <li>l'objet Array traversé (array).</li> </ul> <p><code>filter()</code> ne modifie pas le tableau filtré.</p> <h2>Premiers filtres</h2> <p>On commence avec un tableau simple constitué de plusieurs éléments.</p> <pre><code class="language-javascript">let agrumes = ['orange', 'citron', 'clémentine', 'mandarine'];</code></pre> <p>Et avec notre premier filtre en trouvant une correspondance dans le tableau &quot;agrumes&quot; créé précédement.</p> <pre><code class="language-javascript">let result = agrumes.filter(function(element) { return element == 'orange'; }); console.log(result); // [ "orange" ] console.log(agrumes); // [ "orange", "citron", "clémentine", "mandarine" ] résultat inchangé :)</code></pre> <p>On peut également déclarer ce filtre avec une expression de fonction fléchée afin d'écrire le code (de façon lisible) sur une seule ligne.</p> <pre><code class="language-javascript">let result = agrumes.filter( (element) =&gt; element == 'orange' );</code></pre> <p>C'est bien sympa un tableau simple mais traitons avec un tableau multidimensionnel.</p> <pre><code class="language-javascript">let recipes = [ { 'title': 'Jus d\'orange', 'ingredients': ['orange'] }, { 'title': 'Tarte aux agrumes', 'ingredients': ['farine', 'beurre', 'sucre', 'oeuf', 'citron', 'orange', 'clémentine'] }, { 'title': 'Citronnade', 'ingredients': ['eau', 'citron'] } ];</code></pre> <p>On crée un filtre, pour aller chercher dans l'attribut &quot;ingredients&quot;.</p> <pre><code class="language-javascript">let match = recipes.filter(function(element) { for (let i = 0; i &lt; element.ingredients.length; i++) { if (element.ingredients[i] === 'citron') { return element; } } }); console.table(match); // Retourne les 2 dernières lignes</code></pre> <p>Dans notre cas, on cherche les recettes où l'ingrédient &quot;citron&quot; est cité.</p> <h2>Filtres avancés</h2> <p>Dans un nouveau tableau, on a une liste de films dont chacun contient un titre, un ou plusieurs type(s) et une année.</p> <pre><code class="language-javascript">let movies = [ { 'title': 'Interstellar', 'type': ['Adventure', 'Drama', 'Sci-Fi'], 'year': 2014 }, { 'title': 'Independence Day', 'type': ['Action', 'Adventure', 'Sci-Fi'], 'year': 1996 }, { 'title': 'Deadpool', 'type': ['Action', 'Adventure', 'Comedy'], 'year': 2016 }, { 'title': 'Batman Begins', 'type': ['Action, Adventure'], 'year': 2005 }, { 'title': 'Bad Boys', 'type': ['Action', 'Comedy', 'Crime'], 'year': 1995 } ];</code></pre> <p>On veut trouver un ou plusieurs film(s) de type &quot;Action&quot; ou &quot;Sci-Fi&quot; et dont la date de sortie est avant 2010. On commence par les 2 types.</p> <pre><code class="language-javascript">function OldMovies(element) { for (let i = 0; i &lt; element.type.length; i++) { // film de type "Action" OU "Sci-Fi" if (element.type[i] == 'Action' || element.type[i] == 'Sci-Fi') { return element; } } } console.table(movies.filter(OldMovies));</code></pre> <p>Le resultat nous retourne un tableau de 5 lignes de résultats : &quot;Interstellar&quot;, &quot;Independance Day&quot;, &quot;Deadpool&quot;, &quot;Batman Begins&quot; et &quot;Bad Boys&quot;. Dans la boucle de notre filtre, on ajoute la condition pour la date.</p> <pre><code class="language-javascript">function OldMovies(element) { for (let i = 0; i &lt; element.type.length; i++) { // film de type "Action" OU "Sci-Fi" et datant d'avant 2010 if ( (element.type[i] == 'Action' || element.type[i] == 'Sci-Fi') &amp;&amp; element.year &lt; 2010) { return element; } } }</code></pre> <p>On n'oublie pas de mettre les 2 premières conditions entre parenthèses sinon le résultat est biaisé et on a bien le résultat désiré avec les 3 films : &quot;Independance Day&quot;, &quot;Batman Begins&quot; et &quot;Bad Boys&quot;.</p> <p>On obtient le même résultat mais avec les paramètres &quot;index&quot; et &quot;self&quot; sur une seule ligne et sans boucle.</p> <pre><code class="language-javascript">function OldMovies(element, index, self) { return self[index].type == "Adventure", "Action" &amp;&amp; self[index].year &lt; 2010; }</code></pre> <p>Remarque : cette méthode d'écriture ne fonctionne que s'il y a plusieurs valeurs à rechercher (dans notre exemple &quot;Adventure&quot; et &quot;Action&quot;).</p> <p>Jamais 2 sans 3, on peut également filtrer en utilisant <code>indexOf</code>.</p> <pre><code class="language-javascript">function OldMovies(element) { return (element.type.indexOf("Adventure") !== -1 || element.type.indexOf("Action") !== -1) &amp;&amp; element.year &lt; 2010; }</code></pre> <p>Un second filtre pour la route : les films de type &quot;Action&quot; et &quot;Adventure&quot; sortis après ou en 2005. Pour cela, on préféra utiliser <code>indexOf</code>.</p> <pre><code class="language-javascript">function RecentsMovies(element) { return (element.type.indexOf("Action") !== -1 &amp;&amp; element.type.indexOf("Adventure") !== -1) &amp;&amp; element.year &gt;= 2005; }</code></pre> <h2>Quelques exemples</h2> <h3>Première ou dernière lettre</h3> <pre><code class="language-javascript">let fruits = ['apple', 'avocado', 'banana', 'cherry'];</code></pre> <p>On veut récupérer le ou le(s) élément(s) dont la première lettre est &quot;a&quot;.</p> <pre><code class="language-javascript">function FirstLetter(item) { if (item.substring(0,1) == 'a') { return fruits; } } console.log(fruits.filter(FirstLetter)); // [ "apple", "avocado" ]</code></pre> <p>On veut récupérer le ou les élément(s) dont la dernière lettre est &quot;a&quot;.</p> <pre><code class="language-javascript">function LastLetter(item) { if (item.slice(-1) == 'a') { return arr; } } console.log(fruits.filter(LastLetter)); // Array [ "banana" ]</code></pre> <h3>Nombres paires ou impaires</h3> <p>Si vous êtes familier avec ce genre d'exercice dans d'autres langage de programmation, vous vous doutez que le plus simple est d'utiliser le modulo.</p> <pre><code class="language-javascript">let numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];</code></pre> <p>On veut récupérer les numéros paires d'une chaine d'entiers.</p> <pre><code class="language-javascript">function Even(item) { return item % 2 == 0; } console.log(numbers.filter(Even)); // [ 2, 4, 6, 8, 10 ]</code></pre> <p>On veut récupérer les numéros impaires d'une chaine d'entiers.</p> <pre><code class="language-javascript">function Odd(item) { return item %2 !== 0; } console.log(numbers.filter(Odd)); // [ 1, 3, 5, 7, 9 ]</code></pre> <h3>Supprimer un item</h3> <p>Au lieu d'utiliser la fonction native &quot;splice&quot; pour supprimer un élément du tableau, on peut utiliser un filtre.</p> <pre><code class="language-javascript">let item = 'banane'; // item à supprimer let fruits = ['pomme', 'banane', 'abricot']; // tableau existant fruits = fruits.filter(function(element) { return element !== item; }); console.log(fruits); //Array [ "pomme", "abricot" ]</code></pre> <h3>Supprimer les doublons</h3> <p>Dans un tableau de données, il se peut qu'il y ait des doublons. Pour les supprimer, un simple filtre suffit.</p> <pre><code class="language-javascript">let doublons = ['citron', 1, 'citron', 2, '1']; function Unique(element, index, self) { return self.indexOf(element) === index; } console.log(doublons.filter(Unique)); // [ "citron", 1, 2, "1" ]</code></pre> <p>Ce filtre marche avec n'importe quel type de valeurs (chaine de caractère, entiers, etc...) car on demande de retourner les valeurs. On peut également l'adapter pour afficher tous les types de nos films en commencant par récupérer tous les types.</p> <pre><code class="language-javascript">let types = []; // Chaque ligne (film) for (let i = 0; i &lt; movies.length; i++) { // Chaque "type" dans chaque ligne (film) for (let j = 0; j &lt; movies[i].type.length; j++) { types.push(movies[i].type[j]); } } console.log(types.filter(Unique)); // [ "Adventure", "Drama", "Sci-Fi", "Action", "Comedy", "Crime" ]</code></pre> <h3>Une valeur unique d'un tableau</h3> <p>Soit un tableau contenant plusieurs lignes.</p> <pre><code class="language-javascript">let characters = [ { id: 1, lastname: 'Doe', firstname: 'Jane' }, { id: 2, lastname: 'Doe', firstname: 'John' }, { id: 3, lastname: 'Connor', firstname: 'Sarah' }, { id: 4, lastname: 'Connor', firstname: 'John' } ];</code></pre> <p>On souhaite récupérer la troisième ligne dont l'id est 3.</p> <pre><code class="language-javascript">let id = 3; let getIdCharacter = characters.filter(function(element) { return element.id === id; })[0]; console.log(getIdCharacter); // { id: 3, lastname: "Connor", firstname: "Sarah" }</code></pre> <p>Remarque : si vous cherchez une valeur en double (exemple : &quot;firstname&quot; = &quot;John&quot;), le résultat affiché sera la 1ère ligne du tableau contenant cette valeur.</p> <h2>TP : Filtrer le contenu d'un tableau</h2> <p>On veut afficher la liste des types de films dans des checkbox ainsi que la liste complète des films. Lorsque l'utilisateur coche une ou plusieurs checkbox, alors on filtre suivant le type coché.</p> <h3>Préparation HTML</h3> <p>Une petite interface Booststrap 4 (version alpha) pour simplifier l'habillage graphique de notre future application.</p> <pre><code class="language-markup">&lt;!DOCTYPE html&gt; &lt;html&gt; &lt;head&gt; &lt;meta charset="UTF-8"&gt; &lt;meta name="viewport" content="width=device-width, initial-scale=1"&gt; &lt;title&gt;Movies - Filter&lt;/title&gt; &lt;link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-alpha.6/css/bootstrap.min.css"&gt; &lt;link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css"&gt; &lt;/head&gt; &lt;body class="container" style="margin-top: 1rem;"&gt; &lt;p id="count"&gt;&lt;/p&gt; &lt;div class="row"&gt; &lt;section id="movies" class="col-md-8"&gt; &lt;!-- Liste des films --&gt; &lt;/section&gt; &lt;ul id="types" class="col-4 list-unstyled"&gt; &lt;!-- Liste des types de films sous forme de checkboxes--&gt; &lt;/ul&gt; &lt;/div&gt; &lt;script&gt; // Ici le code JavaSript &lt;/script&gt; &lt;/body&gt; &lt;/html&gt; </code></pre> <h3>Initialisation</h3> <p>Il existe plusieurs moyen d'arriver à ses fins sur JavaScript. Je vous propose d'instancier l'application dans une function.</p> <pre><code class="language-javascript">let movies = [...] // contenu de la variable inchangé (remplacer [...] par le vrai tableau) let app = new function() { // Récupération des données this.movies = movies; // La suite }</code></pre> <h3>Listage des films</h3> <p>Dans une nouvelle fonction on sélectionne la div &quot;movies&quot;. On créé une variable vide qui va recevoir l'ensemble du code HTML dans la boucle. Cette dernière boucle dans le tableau des films contenus dans la variable &quot;data&quot; appelée en paramètre dans la fonction. On affiche ces données via la fonction native &quot;innerHTML&quot; Puis on fait appel à une fonction &quot;Count&quot; que l'on va créé à la suite.</p> <pre><code class="language-javascript">// Affiche les films (tous par défaut) this.FetchAll = function(data) { // Selection de l'élément let elMovies = document.getElementById('movies'); let htmlMovies = ''; for (let i in data) { htmlMovies += '&lt;article class="card mb-3"&gt;&lt;div class="card-header"&gt;'; for (let j in data[i].type) { htmlMovies += '&lt;i class="fa fa-tag" aria-hidden="true"&gt;&lt;/i&gt; ' + data[i].type[j] + ' '; } htmlMovies += '&lt;/div&gt;'; htmlMovies += '&lt;div class="card-block"&gt;&lt;h2&gt;' + data[i].title + '&lt;/h2&gt;&lt;/div&gt;'; htmlMovies += '&lt;div class="card-footer text-muted text-center"&gt; &lt;i class="fa fa-calendar" aria-hidden="true"&gt;&lt;/i&gt; ' + data[i].year + '&lt;/div&gt;'; htmlMovies += '&lt;/article&gt;'; } // Affichage de l'ensemble des lignes en HTML elMovies.innerHTML = htmlMovies; // Affiche le nombre de films this.Count(data); };</code></pre> <p>Déclarée précédement, on créé la fonction &quot;this.Count&quot; pour afficher le nombre de films dans la div &quot;count&quot;.</p> <pre><code class="language-javascript">// Retourne le nombre de films this.Count = (data) =&gt; document.getElementById('count').innerHTML = data.length + ' movies';</code></pre> <p>Pour afficher le résultat de cette fonction, il faut appeler la fonction &quot;FetchAll&quot; en dehors de la fonction &quot;app&quot;.</p> <pre><code class="language-javascript">let app = new function() { // Contenu inchangé } // Affichage de tous les films app.FetchAll(movies);</code></pre> <h3>Listage des types</h3> <p>On a besoin de récupérer tous les types des films. Dans un premier temps on récupère tous les types dans un tableau. Puis dans un second temps, on supprime l'ensemble des doublons présent dans le tableau à l'aide d'un filtre. Enfin dans un troisième temps d'afficher la liste filtrée des types.</p> <pre><code class="language-javascript">// Retourne la liste des checkboxes this.DisplayFilters = function() { // Selection de l'élément let elTypes = document.getElementById('types'); let types = []; // Chaque ligne (film) for (let i in movies) { // Chaque "type" dans chaque ligne (film) for (let j in movies[i].type) { types.push(movies[i].type[j]); } } let uniqueTypes = types.filter( (value, index, self) =&gt; self.indexOf(value) === index ); let htmlTypes = ''; for (let i in uniqueTypes) { htmlTypes += '&lt;li&gt;&lt;input type="checkbox" id="' + uniqueTypes[i] + '" name="types[]" value="' + uniqueTypes[i] + '"&gt; &lt;label for="' + uniqueTypes[i] + '"&gt;' + uniqueTypes[i] + '&lt;/label&gt;&lt;/li&gt;'; } elTypes.innerHTML = htmlTypes; };</code></pre> <p>On va utiliser ces fonctions dans la prochaine fonction, celle du triage.</p> <h3>Trie</h3> <p>Pour chaque checkbox cochée, on doit créer un évenement. Si on coche une valeur, alors on stocke la valeur du choix coché. Il faut gérer l'évènement inverse, c'est-à-dire si l'utitlisateur décoche la case alors il faut supprimer cette valeur du tableau.</p> <pre><code class="language-javascript">// Retourne les films filtrés this.FilterByType = function() { // Afiche les checkboxes this.DisplayFilters(); let checkboxes = document.querySelectorAll('input'); let arrType = []; let self = this; for (let checkbox of checkboxes) { checkbox.addEventListener('click', function() { if (checkbox.checked) { // Ajout dans le tableau de la valeur cochée arrType.push(checkbox.value); } else { // Suppression dans le tableau let removeItem = arrType.filter( (e) =&gt; e !== checkbox.value ); arrType = removeItem; } // Ici la suite du code }); } };</code></pre> <p>A partir de ce tableau, on peut gérer le filtrage des films. En se basant sur la longueur du tableau des choix, on peut effectuer les tris et retourner un nouveau tableau en paramètre de la fonction <code>FetchAll</code></p> <pre><code class="language-javascript">if (arrType.length &gt; 0) { let i = arrType.length - 1; // 1er choix if (arrType.length == 1) { filteredMovie = self.movies.filter( (e) =&gt; e.type.indexOf(arrType[0]) !== -1 ); // Autre(s) choix } else { filteredMovie = filteredMovie.filter(function(e) { for (let j = 0; j &lt; i; j++) { return e.type.indexOf(arrType[i]) !== -1; } }); } self.FetchAll(filteredMovie); } else { // Reset (aucune case cochée) app.FetchAll(movies); }</code></pre> <p>Pour afficher le résultat de cette fonction, il faut appeler la fonction &quot;FilterByType&quot; en dehors de la fonction &quot;app&quot;, à la suite de <code>FetchAll</code>.</p> <pre><code class="language-javascript">let app = new function() { // Contenu inchangé } // Affichage de tous les films app.FetchAll(movies); // Filtrage app.FilterByType();</code></pre> <h2>Conclusion</h2> <p>On a fait le tour des filtres en JavaScript. Concernant le TP, libre à vous de le modifier à votre gout en appelant par exemple la liste des films à partir d'un fichier JSON (voir d'une API), ajouter un champ de recherche pour rechercher par titre, trier avec un champ de type range pour les années, etc...</p> <h2>Sources</h2> <ul> <li><a href="https://github.com/airbnb/javascript">https://github.com/airbnb/javascript</a></li> <li><a href="https://developer.mozilla.org/fr/docs/Web/JavaScript/Reference/Fonctions/Fonctions_fl%C3%A9ch%C3%A9es">https://developer.mozilla.org/fr/docs/Web/JavaScript/Reference/Fonctions/Fonctions_fl%C3%A9ch%C3%A9es</a></li> </ul>; 2017-04-12 15:07:12 Créer une API RESTful sur Go https://etienner.fr/creer-une-api-restful-sur-go <p>Une API (Application Programming Interface) permet de fournir des données brutes accessibles depuis une URL. En général, cela permet de faire le pont entre une application cliente et une base de données, dans notre cas depuis SQLite. Pourquoi le choix de cette base ? Car SQLite est un système de base de données en SQL qui a pour principal atout de fonctionner sans serveur car les données sont contenues dans un fichier. Avec certes moins d'options mais cela est suffisant dans notre cas.</p> <p>Dans l'API que nous allons mettre en place, les données seront fournies à l'utilisateur final au format JSON (JavaScript Object Notation). Nous allons aussi réaliser des tests unitaires et configurer le serveur afin qu'il soit accessible pour les navigateurs Internet.</p> <p>Prérequis nécessaires :</p> <ul> <li>Go et Git installés ;</li> <li>Avoir des bases en Go (pas le jeu chinois ou danois...);</li> <li>Notions de requêtes CRUD en SQL et en HTTP;</li> </ul> <p>Prérequis optionnels :</p> <ul> <li>Avoir lu ce tutoriel : <a href="http://zestedesavoir.com/tutoriels/299/la-theorie-rest-restful-et-hateoas">http://zestedesavoir.com/tutoriels/299/la-theorie-rest-restful-et-hateoas</a> ;</li> <li>L'application Postman (<a href="https://chrome.google.com/webstore/detail/postman/fhbjgbiflinjbdggehcddcbncdddomop">https://chrome.google.com/webstore/detail/postman/fhbjgbiflinjbdggehcddcbncdddomop</a>) installée (application Chrome).</li> </ul> <p>Objectifs :</p> <ul> <li>Créer une API RESTful sur Go avec des requêtes en SQL via un ORM ;</li> <li>Réaliser des tests fonctionnels et unitaires ;</li> <li>Configurer CORS, OPTIONS, ajouter un token d'authentification.</li> </ul> <h2>Préparation du dossier de travail</h2> <p>Dans votre dossier &quot;gopath&quot; (<code>%gopath%</code> sur Windows, <code>$GOPATH</code> sur Linux et MacOS), dans le dossier &quot;src&quot; puis &quot;github.com&quot;, votre nom d'utilisateur (dans ce tutoriel, ce sera &quot;EtienneR&quot;) et créez un nouveau dossier (&quot;go_sqlite_api&quot; dans ce tutoriel). Le dossier de notre projet va comporter un fichier &quot;main.go&quot; contenant notre serveur et un dossier &quot;api&quot; avec les fichiers &quot;api.go&quot;, &quot;users.go&quot; et le fichier de tests unitaires &quot;users_test.go&quot;. Par la suite, le fichier SQLite &quot;data.db&quot; sera créé automatiquement.</p> <pre><code>gopath/ src/ github.com/ EtienneR/ go_sqlite_api/ api/ api.go users.go users_test.go main.go data.db</code></pre> <h3>Les librairies</h3> <p>Pour mettre en place cette API, on a besoin des 3 librairies ci-dessous.</p> <ul> <li><strong>Gin</strong> : le micro framework basé sur HttpRouter <code>go get github.com/gin-gonic/gin</code> ;</li> <li><strong>go-sqlite3</strong> : le &quot;driver&quot; (pilote en français) SQLite3 <code>go get github.com/mattn/go-sqlite3</code> ;</li> <li><strong>Gorm</strong> : l'ORM (Object-Relational Mapping) <code>go get github.com/jinzhu/gorm</code>.</li> </ul> <p>Dans le fichier &quot;api.go&quot;, on appel ces librairies dans &quot;import&quot;.</p> <pre><code class="language-go">package api import ( "github.com/gin-gonic/gin" "github.com/jinzhu/gorm" _ "github.com/mattn/go-sqlite3" )</code></pre> <p>Remarque : le pilote SQLite3 est indispensable pour faire fonctionner l'ORM. Gorm accepte également <strong>MySQL</strong> (et <strong>MariaDB</strong>), <strong>Postgres</strong> et <strong>FoundationDB</strong> à condition d'avoir à disposition le pilote correspondant.</p> <h3>Préparation de la base de données</h3> <p>Pour créer notre futur fichier de base de donnée SQLite &quot;data.db&quot;, on va avoir recours à l'ORM, Gorm.</p> <h4>Structure de données</h4> <p>Pour la structure dans notre fichier &quot;users.go&quot;, on reprend le nom de la table concernée &quot;users&quot; ainsi que les 2 champs &quot;id&quot; et &quot;name&quot;.</p> <pre><code class="language-go">package api import ( "github.com/gin-gonic/gin" ) type Users struct { Id int `gorm:"AUTO_INCREMENT" form:"id" json:"id"` Name string `gorm:"not null" form:"name" json:"name"` }</code></pre> <p>On met en place le &quot;databinding&quot; pour les données rentrées (POST et PUT) avec <code>json:"id"</code> et <code>json:"name"</code>. Si cette notion vous parait abstraite, vous comprendrez son principe lors de l'utilisation des routes concernées. Concernant <code>form:"id"</code> et <code>form:"name"</code>, ils permettent de récupérer les données depuis &quot;form-data&quot; et &quot;x-www-form-urlencoded&quot; disponibles dans Postman. Quant à &quot;gorm&quot;, ce sont des paramêtres de configuration dédiés à la création des champs concernés.</p> <h4>Initialisation de la base de données</h4> <p>Pour se connecter à la base de données, dans le fichier &quot;api.go&quot;, on indique le pilote utilisé &quot;sqlite3&quot; et le chemin du fichier &quot;data.db&quot;.</p> <pre><code class="language-go">func InitDb() *gorm.DB { // Ouverture du fichier db, err := gorm.Open("sqlite3", "./data.db") db.LogMode(true) // Création de la table if !db.HasTable(&amp;Users{}) { db.CreateTable(&amp;Users{}) db.Set("gorm:table_options", "ENGINE=InnoDB").CreateTable(&amp;Users{}) } // Erreur de chargement if err != nil { panic(err) } return db }</code></pre> <p>Dans la première condition, si la table &quot;users&quot; n'existe pas, alors on l'a créé avec les options déclarées dans la structure &quot;Users&quot; ainsi que le moteur SQL &quot;InnoDB&quot;.<br /> La fonction facultative mais utile en phase de développement <code>db.LogMode(true)</code> permet d'afficher la ou les requête(s) effectuée(s) dans le terminal.</p> <h3>Création du serveur</h3> <p>Dans le fichier &quot;main.go&quot;, on déploit un serveur HTTP fonctionnant sur le port 3000 et dont les routes seront déclarées dans le package &quot;api&quot;, dans le fichier &quot;api.go&quot;.</p> <pre><code class="language-go">package main import ( "log" "net/http" "github.com/EtienneR/go_sqlite_api/api" ) func main() { err := http.ListenAndServe(":3000", api.Handlers()) if err != nil { log.Fatal("ListenAndServe: ", err) } }</code></pre> <p>Remarque : en production, il faudra remplacer le port 3000 par 80.</p> <p>Vous l'avez compris, à ce stade, le serveur ne fonctionne pas car on n'a pas encore travaillé dans le fichier &quot;api.go&quot;. On ne touchera plus au fichier &quot;main.go&quot;.</p> <h2>Le routage</h2> <p>Dans cette partie, on va faire le plus gros, c'est-à-dire déclarer nos routes avec les requêtes SQL correspondantes en prenant soin de prendre en compte les erreurs éventuelles qui surviennent lors de l'appelation et l'envoi des données. Nous testerons nos routes avec Postman sauf si vous préférez CURL et sa syntaxe...</p> <h3>Objectifs</h3> <p>On va utiliser 5 routes basiques de CRUD (Create, Read, Update, Delete) listées ci-dessous.</p> <table> <thead> <tr> <th>Verbe</th> <th>URL</th> <th>Action</th> </tr> </thead> <tbody> <tr> <td>GET</td> <td>/api/v1/users</td> <td>Lister tous les utilisateurs</td> </tr> <tr> <td>GET</td> <td>/api/v1/users/1</td> <td>Lister l'utilisateur #1</td> </tr> <tr> <td>POST</td> <td>/api/v1/users</td> <td>Poster un nouvel utilisateur</td> </tr> <tr> <td>PUT</td> <td>/api/v1/users/1</td> <td>Modifier l'utilisateur #1</td> </tr> <tr> <td>DELETE</td> <td>/api/v1/users/1</td> <td>Supprimer l'utilisateur #1</td> </tr> </tbody> </table> <h3>Préparation</h3> <p>A la suite (dans le fichier &quot;api.go&quot;), dans une nouvelle fonction nommée <code>Handlers()</code>, on fait appel au micro-framework Gin pour déclarer nos routes.</p> <pre><code class="language-go">func Handlers() *gin.Engine { r := gin.Default() v1Users := r.Group("api/v1/users") { v1Users.POST("", PostUser) v1Users.GET("", GetUsers) v1Users.GET(":id", GetUser) v1Users.PUT(":id", EditUser) v1Users.DELETE(":id", DeleteUser) } return r }</code></pre> <p>Dans un premier temps, on instancie le serveur MUX dans une variable (<code>r</code>). Sachant que les URL de notre API commencent par le même chemin, le &quot;endpoint&quot; <code>api/v1/users</code>, on déclare un groupe pour nos routes dans une variable (<code>v1Users</code>). C'est dans cette fonction que l'on placera nos routes. Et on retourne les données de notre routeur MUX car on en a besoin dans notre fichier &quot;main.go&quot;. Pour mieux organiser notre code, nous allons créer les fonctions de nos routes dans le fichier &quot;users.go&quot;.</p> <h3>Ajouter un nouvel utilisateur</h3> <p>Pour insérer des données, on veut effectuer la requête SQL semblable à celle ci-dessous.</p> <pre><code class="language-sql">INSERT INTO "users" (name) VALUES ("toto");</code></pre> <p>On met en place une route de type POST dans la fonction <code>PostUser</code>.</p> <pre><code class="language-go">// Ajouter un utilisteur func PostUser(c *gin.Context) { db := InitDb() defer db.Close() var json Users c.Bind(&amp;json) // Si le champ est bien saisi if json.Name != "" { // INSERT INTO "users" (name) VALUES (json.Name); db.Create(&amp;json) // Affichage des données saisies c.JSON(201, gin.H{"success": json}) } else { // Affichage de l'erreur c.JSON(422, gin.H{"error": "Fields are empty"}) } }</code></pre> <p>Dans un premier temps, on récupère les données rentrées en JSON via la fonction <code>c.Bind()</code>. Puis on vérifie si le champ &quot;name&quot; n'est pas vide alors on envoie un message de succès avec le code HTTP &quot;201&quot;. Sinon on renvoie le code &quot;422&quot; avec un message d'erreur.</p> <p>Dans Postman, sélectionnez &quot;POST&quot; puis l'URL &quot;<a href="http://localhost:3000/api/v1/users">http://localhost:3000/api/v1/users</a>&quot;, cochez &quot;Body&quot; puis &quot;raw&quot;, sélectionnez &quot;JSON (application/json)&quot; et copiez les données à rentrer <code>{ "name": "John Doe" }</code> et cliquez sur &quot;Send&quot;.</p> <p>Attention : pour que &quot;form-data&quot; et &quot;x-www-form-urlencoded&quot; fonctionnent correctement, il ne faut pas qu'il y'ait d'en-têtes HTTP dans &quot;Headers&quot;.</p> <h3>Lister tous les utilisateurs</h3> <p>On veut afficher dans un tableau JSON tous les utilisateurs présents dans la table &quot;users&quot; ce qui revient à faire en SQL.</p> <pre><code class="language-sql">SELECT * FROM users;</code></pre> <p>On met en place une route de type GET dans la fonction <code>GetUsers</code>.</p> <pre><code class="language-go">// Obtenir la liste de tous les utilisateurs func GetUsers(c *gin.Context) { db := InitDb() defer db.Close() var users []Users // SELECT * FROM users db.Find(&amp;users) // Affichage des données c.JSON(200, users) }</code></pre> <p>On créé une variable <code>users</code> héritée de la structure du même nom en précisant que l'on souhaite un tableau (crochets ouvrant et fermant). Puis on effectue la requête SQL et on appel le résultat dans un appel au format JSON via la fonction <code>c.JSON()</code>.</p> <h3>Lister un utilisateur</h3> <p>On veut afficher les données d'un utilisateur ce qui revient à faire en SQL.</p> <pre><code class="language-sql">SELECT * FROM users WHERE id = 1;</code></pre> <p>On met en place une route de type GET avec l'id en paramètre dans la fonction <code>GetUser</code>.</p> <pre><code class="language-go">// Obtenir un utilisateur par son id func GetUser(c *gin.Context) { db := InitDb() defer db.Close() id := c.Params.ByName("id") var user Users // SELECT * FROM users WHERE id = 1; db.First(&amp;user, id) if user.Id != 0 { // Affichage des données c.JSON(200, user) } else { // Affichage de l'erreur c.JSON(404, gin.H{"error": "User not found"}) } }</code></pre> <p>Dans un premier temps, on stocke l'id concerné dans la variable <code>id</code> via la fonction <code>c.Params.ByName("id")</code>. Puis on vérifie que la requête SQL renvoie un résultat dans une ligne sinon on affiche une erreur 404 avec un message d'erreur personnalisé.</p> <h3>Modifier un utilisateur</h3> <p>Pour modifier des données, on veut effectuer la requête SQL.</p> <pre><code class="language-sql">UPDATE users SET name='toto2' WHERE id = 1;</code></pre> <p>On met en place une route de type PUT avec l'id en paramètre dans la fonction <code>EditUser</code>.</p> <pre><code class="language-go">// Modifier un utilisateur func EditUser(c *gin.Context) { db := InitDb() defer db.Close() id := c.Params.ByName("id") var user Users // SELECT * FROM users WHERE id = 1; db.First(&amp;user, id) if user.Name != "" { if user.Id != 0 { var json Users c.Bind(&amp;json) result := Users{ Id: user.Id, Name: json.Name, } // UPDATE users SET name='json.Name' WHERE id = user.Id; db.Model(&amp;user).Update("name", result.Name) // Affichage des données modifiées c.JSON(200, gin.H{"success": result}) } else { // Affichage de l'erreur c.JSON(404, gin.H{"error": "User not found"}) } } else { // Affichage de l'erreur c.JSON(422, gin.H{"error": "Fields are empty"}) } }</code></pre> <p>Dans un premier temps, on stocke l'id concerné dans la variable <code>id</code> via la fonction <code>c.Params.ByName("id")</code>. Comme dans la fonction précédente, on vérifie si le champ &quot;name&quot; n'est pas vide alors on envoie les données avec un message de succès de code HTTP &quot;201&quot;. Sinon on renvoie une erreur &quot;422&quot; avec un message d'erreur. Puis on vérifie que la requête SQL renvoie un résultat, sinon on affiche une erreur 404 avec un message d'erreur personnalisé. Et pour finir, on insère les données via <code>db.Model().Update()</code>.</p> <p>Dans Postman, sélectionnez &quot;PUT&quot; puis l'URL &quot;<a href="http://localhost:3000/api/v1/users/1">http://localhost:3000/api/v1/users/1</a>&quot;, cochez &quot;Body&quot; puis &quot;raw&quot; et copiez les données à rentrer <code>{ "name": "John la Frite" }</code> et cliquez sur &quot;Send&quot;.</p> <h3>Supprimer un utilisateur</h3> <p>Pour supprimer un utilisateur, on veut effectuer la requête SQL ci-dessous.</p> <pre><code class="language-sql">DELETE FROM users WHERE id = 1</code></pre> <p>On met en place une route de type DELETE avec l'id en paramètre dans la fonction <code>DeleteUser</code>.</p> <pre><code class="language-go">// Supprimer un utilisateur func DeleteUser(c *gin.Context) { db := InitDb() defer db.Close() // Récupération de l'id dans une variable id := c.Params.ByName("id") var user Users db.First(&amp;user, id) if user.Id != 0 { // DELETE FROM users WHERE id = user.Id db.Delete(&amp;user) // Affichage des données c.JSON(200, gin.H{"success": "User #" + id + " deleted"}) } else { // Affichage de l'erreur c.JSON(404, gin.H{"error": "User not found"}) } }</code></pre> <p>Dans un premier temps, on stocke l'id concerné dans la variable <code>id</code> via la fonction <code>c.Params.ByName("id")</code>. Puis, comme pour la route précédente, on vérifie que l'utilisateur existe sinon on affiche une erreur 404 avec un message d'erreur personnalisé. Si l'utilisateur existe alors on le supprime avec <code>db.Delete()</code> et on affiche un message de succès.</p> <p>Dans Postman, sélectionnez &quot;DELETE&quot; puis l'URL &quot;<a href="http://localhost:3000/api/v1/users/1">http://localhost:3000/api/v1/users/1</a>&quot; et cliquez sur &quot;Send&quot;.</p> <h2>Tests unitaires</h2> <p><img src="http://i.giphy.com/56LhCE2j6Uy2Y.gif" alt="" /></p> <p>Jusqu'ici, on a exécuté des tests fonctionnels avec Postman. Finalement, il est possible de s'en passer en effectuant une batterie de tests. Concretement, dans un fichier on va effectuer les mêmes taches que l'on a exécuté sur Postman mais de manière automatisées. Pour ce faire, on va donc travailler dans le fichier dédié, &quot;users_test.go&quot;.</p> <h3>Librairies et variables globales</h3> <p>On importe un certain nombre de librairies dont l'indispensable &quot;testing&quot; pour n'importe quel test sur Go ainsi que &quot;net/http&quot; et &quot;net/http/httptest&quot; pour des applications orientées Web. On déclare aussi des variables globales qui vont nous servir dans nos différentes fonctions.</p> <pre><code class="language-go">package api_test import ( "io" "net/http" "net/http/httptest" "strings" "testing" "github.com/EtienneR/go_sqlite_api/api" ) var ( server *httptest.Server reader io.Reader usersUrl, usersUrlId string userId int )</code></pre> <h3>Initialisation</h3> <p>Lorsqu'on lance une batterie de tests, on se base sur une base de données vide afin d'éviter les problèmes avec l'auto-increment des id. Pour cela on supprime la table et on l'a créé avec des utilisateurs. Dans notre cas, ce sera un fichier &quot;data.db&quot; dans le dossier &quot;api&quot;. Ensuite, on démarre un serveur HTTP de test basé sur nos routes. Dans 2 variables, on stocke les URL (la première sans le paramêtre &quot;id&quot; et la seconde avec).</p> <pre><code class="language-go">func init() { // Ouverture de la connexion vers la BDD SQLite db := api.InitDb() // Fermeture de la connexion vers la BDD SQLite defer db.Close() var user api.Users // Suppression de la table db.DropTable(user) // Création de la table db.CreateTable(user) // Création d'utilisateurs db.Create(&amp;api.Users{Name: "Pierre"}) db.Create(&amp;api.Users{Name: "Paul"}) db.Create(&amp;api.Users{Name: "Jacques"}) db.Create(&amp;api.Users{Name: "Marie Thérèse"}) // Démarrage du serveur HTTP server = httptest.NewServer(api.Handlers()) // URL sans paramêtre et avec usersUrl = server.URL + "/api/v1/users" usersUrlId = usersUrl + "/5" }</code></pre> <h3>Fonctions de test</h3> <p>Comme dans notre test fonctionnel, on va tester chacune des routes de notre API.</p> <h4>Tester l'ajout d'une ligne</h4> <pre><code class="language-go">func TestPostUser(t *testing.T) { // Contenu à soumettre userJson := `{"name": "Donovan"}` // Contenu à soumettre au bon format reader = strings.NewReader(userJson) // Déclaration de la requête : type, URL, contenu request, err := http.NewRequest("POST", usersUrl, reader) // Requête de type JSON request.Header.Set("Content-Type", "application/json") // Exécution de la requête response, err := http.DefaultClient.Do(req) // Erreur si route inacessible if err != nil { t.Error(err) } // Erreur si code HTTP différent de 201 if response.StatusCode != 201 { t.Errorf("Success expected: %d", response.StatusCode) } }</code></pre> <ol> <li>On stocke dans une variable le contenu de la ligne que l'on souhaite ajouter ;</li> <li>On modifie ce contenu pour le rendre lisible au format &quot;NewReader&quot; ;</li> <li>On déclare la requête avec 3 paramètres : <ul> <li>le type de la route : &quot;POST&quot; ;</li> <li>l'URL de la route ;</li> <li>le contenu ;</li> </ul></li> <li>On spécifie ce contenu au format JSON (afin d'éviter une erreur HTTP 422) ;</li> <li>S'il y a une erreur pour contacter la route, alors le test affichera une erreur ;</li> <li>Si le code HTTP n'est pas 201 alors le test affichera une erreur.</li> </ol> <h4>Tester la lecture des lignes</h4> <pre><code class="language-go">func TestGetUsers(t *testing.T) { // Contenu à soumettre vide reader = strings.NewReader("") // Déclaration de la reqûête : type, URL, contenu request, err := http.NewRequest("GET", usersUrl, reader) // Exécution de la requête response, err := http.DefaultClient.Do(request) // Erreur si route inacessible if err != nil { t.Error(err) } // Erreur si code HTTP différent de 200 if response.StatusCode != 200 { t.Errorf("Success expected: %d", response.StatusCode) } }</code></pre> <ol> <li>On stocke dans une variable aucun contenu;</li> <li>On déclare la requête avec 3 paramètres : <ul> <li>le type de la route : &quot;GET&quot; ;</li> <li>l'URL de la route ;</li> <li>le contenu (aucun) ;</li> </ul></li> <li>S'il y a une erreur pour contacter la route, alors le test affichera une erreur ;</li> <li>Si le code HTTP n'est pas 200 alors le test affichera une erreur.</li> </ol> <h4>Tester la lecture d'une ligne</h4> <pre><code class="language-go">func TestGetUser(t *testing.T) { // Contenu à soumettre vide reader = strings.NewReader("") // Déclaration de la requête : type, URL, contenu request, err := http.NewRequest("GET", usersUrlId, reader) // Exécution de la requête response, err := http.DefaultClient.Do(request) // Erreur si route inacessible if err != nil { t.Error(err) } // Erreur si code HTTP différent de 200 if response.StatusCode != 200 { t.Errorf("Success expected: %d", response.StatusCode) } }</code></pre> <ol> <li>On stocke dans une variable aucun contenu;</li> <li>On déclare la requête avec 3 paramètres : <ul> <li>le type de la route : &quot;GET&quot; ;</li> <li>l'URL de la route avec l'id en paramètre ;</li> <li>le contenu (aucun) ;</li> </ul></li> <li>S'il y a une erreur pour contacter la route, alors le test affichera une erreur ;</li> <li>Si le code HTTP n'est pas 200 alors le test affichera une erreur.</li> </ol> <h4>Tester la modification d'une ligne</h4> <pre><code class="language-go">func TestEditUser(t *testing.T) { // Contenu à soumettre userJson := `{"name": "Mark"}` // Contenu à soumettre au bon format reader = strings.NewReader(userJson) // Déclaration de la requête : type, URL, contenu request, err := http.NewRequest("PUT", usersUrlId, reader) // Requête de type JSON request.Header.Set("Content-Type", "application/json") // Exécution de la requête response, err := http.DefaultClient.Do(request) // Erreur si route inacessible if err != nil { t.Error(err) } // Erreur si code HTTP différent de 200 if response.StatusCode != 200 { t.Errorf("Success expected: %d", response.StatusCode) } }</code></pre> <ol> <li>On stocke dans une variable le contenu de la ligne que l'on souhaite ajouter ;</li> <li>On modifie ce contenu pour le rendre lisible au format &quot;NewReader&quot; ;</li> <li>On déclare la requête avec 3 paramètres : <ul> <li>le type de la route : &quot;PUT&quot; ;</li> <li>l'URL de la route avec l'id en paramètre ;</li> <li>le contenu ;</li> </ul></li> <li>On spécifie ce contenu au format JSON (afin d'éviter une erreur HTTP 422) ;</li> <li>S'il y a une erreur pour contacter la route, alors le test affichera une erreur ;</li> <li>Si le code HTTP n'est pas 200 alors le test affichera une erreur.</li> </ol> <h4>Tester la suppression d'une ligne</h4> <pre><code class="language-go">func TestDeleteUser(t *testing.T) { // Contenu à soumettre vide reader = strings.NewReader("") // Déclaration de la requête : type, URL, contenu request, err := http.NewRequest("DELETE", usersUrlId, reader) // Exécution de la requête response, err := http.DefaultClient.Do(request) // Erreur si route inacessible if err != nil { t.Error(err) } // Erreur si code HTTP différent de 200 if response.StatusCode != 200 { t.Errorf("Success expected: %d", response.StatusCode) } }</code></pre> <ol> <li>On stocke dans une variable aucun contenu;</li> <li>On déclare la requête avec 3 paramètres : <ul> <li>le type de la route : &quot;DELETE&quot; ;</li> <li>l'URL de la route avec l'id en paramètre ;</li> <li>le contenu (aucun) ;</li> </ul></li> <li>S'il y a une erreur pour contacter la route, alors le test affichera une erreur ;</li> <li>Si le code HTTP n'est pas 200 alors le test affichera une erreur.</li> </ol> <h3>Lancer la série des tests</h3> <p>Dans votre terminal, allez dans le dossier &quot;api&quot; et lancez le test avec la commande <code>go test api_test.go</code>. Si tout est ok, vous devriez avoir un message de ce genre : <code>ok command-line-arguments 0.210s</code>. </p> <p><img src="http://i.giphy.com/ZKf5OzdXdjtRu.gif" alt="" /></p> <p>Pour voir tout le processus des tests : <code>go test -bench=.</code>.</p> <p>Dans le dossier &quot;api&quot; vous avez remarqué qu'un nouveau fichier a fait son apparition, il s'agit du fichier &quot;data.db&quot; dédié aux tests.</p> <p>Remarque : si vous utilisez le protocole de versionning Git, n'oubliez pas d'ajouter le chemin du fichier de base de données de test dans le fichier &quot;.gitignore&quot;.</p> <h2>Options de configurations</h2> <p>Dans cette partie, nous allons utiliser la notion de &quot;middleware&quot;. C'est une fonction qui permet d'être appelée depuis une ou plusieurs fonctions.</p> <h3>CORS (Cross Origin Ressource Sharing)</h3> <p>Pour établir une communication interdomaine, il faut autoriser la connexion en activant le CORS sinon vous aurez un message explicite dans Firefox.</p> <p><code>Blocage d’une requête multi-origines (Cross-Origin Request) : la politique « Same Origin » ne permet pas de consulter la ressource distante située sur http://localhost:3000/api/v1/users. Raison : l’en-tête CORS « Access-Control-Allow-Origin » est manquant.</code></p> <p>Message d'erreur testé avec le code Javascript ci-dessous.</p> <pre><code class="language-javascript">var xhr = new XMLHttpRequest(); xhr.open('GET', 'http://localhost:3000/api/v1/users', true); xhr.onreadystatechange = function () { if (xhr.readyState == 4 &amp;&amp; xhr.status == '200') { console.table(JSON.parse(xhr.responseText)); } } xhr.send(null);</code></pre> <p>Au niveau local, dans la ou les route(s) concernée(s).</p> <pre><code class="language-go">c.Writer.Header().Add("Access-Control-Allow-Origin", "*") c.Next()</code></pre> <p>L'astérisque signifie que l'accès est autorisé pour n'importe quelle IP. Pour des raisons de sécurité, vous pouvez spécifier une adresse IP ou plusieurs, séparées par une virgule.</p> <p>Au niveau global, à partir d'un middleware, on créé une fonction nommée <code>Cors()</code>.</p> <pre><code class="language-go">func Cors() gin.HandlerFunc { return func(c *gin.Context) { c.Writer.Header().Add("Access-Control-Allow-Origin", "*") c.Next() } }</code></pre> <p>Puis on appel notre fonction <code>Cors()</code> dans la fonction <code>Handlers()</code> du fichier &quot;api.go&quot;.</p> <pre><code class="language-go">// Activation du CORS r.Use(Cors())</code></pre> <p>Coté test unitaire, ça donne la vérification du header &quot;Access-Control-Allow-Origin&quot;.</p> <pre><code class="language-go">if response.Header.Get("Access-Control-Allow-Origin") != "*" { t.Error("No CORS") }</code></pre> <h3>Activer OPTIONS</h3> <p>Par défaut, lorsque vous allez essayer de faire un requête vers une route de type POST, PUT ou DELETE, un exemple de message ci-dessous apparaitra sur Firefox.</p> <p><code>Blocage d’une requête multi-origines (Cross-Origin Request) : la politique « Same Origin » ne permet pas de consulter la ressource distante située sur http://localhost:3000/api/v1/users. (Raison : échec du canal de pré-vérification des requêtes CORS.</code></p> <p>Message d'erreur testé avec le code Javascript ci-dessous.</p> <pre><code class="language-javascript">var xhr = new XMLHttpRequest(); xhr.open('POST', 'http://localhost:3000/api/v1/users', true); xhr.setRequestHeader('Content-type', 'application/json;charset=UTF-8'); xhr.send(JSON.stringify({ name: "Jo" }));</code></pre> <p><img src="http://i.giphy.com/cAEm5rSuuBEGY.gif" alt="" /></p> <p>Alors oui ce message est ambigüe car on a activé le CORS pour toutes les routes. En fait, Firefox ou votre navigateur favori ne trouve pas la route de type &quot;OPTIONS&quot;. En regardant de plus près dans le terminal de Gin, cette route est effectivement déclarée comme 404.<br /> Pour remédier à ce problème, on ajoute 2 routes de type &quot;OPTIONS&quot;, la première pour POST et la seconde pour PUT et DELETE.</p> <pre><code class="language-go">v1Users.OPTIONS("", OptionsUser) // POST v1Users.OPTIONS(":id", OptionsUser) // PUT, DELETE</code></pre> <p>Ces dernières pointent toutes les deux sur la même fonction, &quot;OptionsUser&quot;.</p> <pre><code class="language-go">func OptionsUser(c *gin.Context) { c.Writer.Header().Set("Access-Control-Allow-Methods", "DELETE, POST, PUT") c.Writer.Header().Set("Access-Control-Allow-Headers", "Content-Type") c.Next() }</code></pre> <p>Coté tests unitaires, ça donne la vérification du header &quot;Access-Control-Allow-Methods&quot; ainsi que &quot;Access-Control-Allow-Headers&quot;</p> <pre><code class="language-go">if response.Header.Get("Access-Control-Allow-Methods") != "DELETE, POST, PUT" { t.Error("Access-Control-Allow-Methods is wrong :(") } if response.Header.Get("Access-Control-Allow-Headers") != "Content-Type" { t.Error("Access-Control-Allow-Headers is wrong :(") }</code></pre> <h3>Authentification avec un token</h3> <p>Le but du token c'est de donner un identifiant généré aléatoirement depuis un formulaire d'inscription. Le serveur vérifie ensuite si le token existe bien dans la base de données.<br /> On met en place un middleware nommé <code>TokenAuthMiddleware()</code>.</p> <pre><code class="language-go">func TokenAuthMiddleware() gin.HandlerFunc { return func(c *gin.Context) { // Récupération du paramètre "token" dans une variable token := c.Request.FormValue("token") // Token vide if token == "" { c.JSON(403, gin.H{"error": "Access denied, API token required"}) c.Abort() return } // Vérification de la valeur du token if token != "mon_super_token" { c.JSON(401, gin.H{"error": "Invalid API token"}) c.Abort() return } c.Next() } }</code></pre> <p>On récupère le champ token nommé &quot;token&quot; via la fonction <code>c.Request.FormValue()</code>. S'il est vide ou si le token n'est pas bon, on renvoit une erreur 403 ou une 401 en personnalisant le message d'erreur.</p> <p>On peut utiliser le middleware en local.</p> <pre><code class="language-go">v1Users.GET("", TokenAuthMiddleware(), GetUsers)</code></pre> <p>Ou en global dans la déclaration du groupe de routes.</p> <pre><code class="language-go">v1Users := r.Group("api/v1/users", TokenAuthMiddleware())</code></pre> <p>Pour communiquer avec l'API, on met le token en paramètre dans l'URL concernée <code>http://localhost:3000/api/v1/users?token=mon_super_token</code>. Bien entendu, il existe d'autre solutions comme HTTP authentification (Basic ou Digest), Oauth, Auth, OpenID et d'autres selon vos besoins.</p> <h2>Conclusion</h2> <p>Rapide à mettre en place une fois la structure définie en amont, les routes sont gérées en aval avec le micro framework accompagnées des requêtes SQL adéquates. Vous pouvez désormais vous concentrer sur vos applications SPA (Single Page Application) et mobiles (Android, IOS, Windows Phone, etc...). Pour aller plus loin, vous pouvez activer HTTPS ce qui activera HTTP 2 pour vous routes (seulement à partir de Go 1.6).</p> <h2>Sources</h2> <ul> <li>Espace de travail : <a href="https://golang.org/doc/code.html#Workspaces">https://golang.org/doc/code.html#Workspaces</a></li> <li>Gin : <a href="https://github.com/gin-gonic/gin">https://github.com/gin-gonic/gin</a></li> <li>Pilote SQLite : <a href="https://github.com/mattn/go-sqlite3">https://github.com/mattn/go-sqlite3</a></li> <li>Gorm : <a href="http://jinzhu.me/gorm">http://jinzhu.me/gorm</a></li> <li>S'en sortir avec SQL sur Go : <a href="http://go-database-sql.org">http://go-database-sql.org</a></li> </ul>; 2017-02-24 15:30:00 Un menu animé seulement en CSS https://etienner.fr/un-menu-anime-seulement-en-css <p>Depuis l'apparition du responsive design ainsi que l'évolution de CSS (version 3), les sites ont mué avec des menus cachés accessibles depuis un simple bouton. Les fameux menus surnomés &quot;burger&quot; mais qui devrait se nommer &quot;trigram&quot; et facilement intégrable avec jQuery ou simplement Javascript en jouant avec l'ajout et la suppression de classes. Hors il est possible de se passer de Javascript et de gérer les actions (afficher / masquer le menu) en CSS 3. On va voir avec 2 exemples que cela peut fonctionner avec 2 pseudos classes différentes : <code>:checked</code> et <code>:target</code>.</p> <p><img src="http://etienner.fr/assets/img/news/menu_left.gif" alt="" /></p> <h2>Code commun</h2> <p>Pour éviter de copier-coller du code, je vous met ci-dessous le code commun en HTML et CSS. Ce qui n'est pas identique est commenté (il s'agit du bouton de fermeture dans le menu et du menu &quot;burger&quot;).</p> <h3>HTML</h3> <pre><code class="language-markup">&lt;!-- Ici le menu caché par défaut --&gt; &lt;nav id="nav-left" role="navigation"&gt; &lt;ul&gt; &lt;li&gt; &lt;!-- Bouton de fermeture suivant l'exemple --&gt; &lt;/li&gt; &lt;li&gt; &lt;a href="#1"&gt;Lien #1&lt;/a&gt; &lt;/li&gt; &lt;li&gt; &lt;a href="#2"&gt;Lien #2&lt;/a&gt; &lt;/li&gt; &lt;li&gt; &lt;a href="#3"&gt;Lien #3&lt;/a&gt; &lt;/li&gt; &lt;/ul&gt; &lt;/nav&gt; &lt;div id="wrapper"&gt; &lt;!-- Menu "burger" suivant l'exemple --&gt; &lt;article&gt; &lt;h1&gt;Titre de la page&lt;/h1&gt; &lt;p&gt; Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. &lt;/p&gt; &lt;/article&gt; &lt;/div&gt;</code></pre> <p>Remarque : pour que l'exemple fonctionne sur un appareil mobile, n'oubliez pas d'ajouter la ligne ci-dessous entre les balises <code>&lt;head&gt;&lt;/head&gt;</code>.</p> <pre><code class="language-markup">&lt;meta name="viewport" content="width=device-width, maximum-scale=1" /&gt;</code></pre> <h3>CSS</h3> <p>On commence par styliser notre menu caché par défaut avec ses listes à puces.</p> <pre><code class="language-css">a { color: #000; text-decoration: none; } nav { background: #2980b9; left: 0; position: fixed; height: 100%; top: 0; left: -395px; width: 400px; z-index: 999; transition: left 0.75s; box-shadow: 3px 0 10px rgba(0,0,0,0.2); } #nav-left ul { list-style-type: none; padding: 0; } #nav-left ul li a, #nav-left ul li span { color: #ecf0f1; display: block; font-size: 2em; line-height: 1em; margin-bottom: 20px; outline: none; text-decoration: none; text-align: center; }</code></pre> <p>Puis l'apparence de notre wrapper.</p> <pre><code class="language-css">#wrapper { margin: 0 auto; padding: 0 5px; position: relative; width: 950px; }</code></pre> <p>On le met en position relative car on va mettre le lien du menu en position absolue afin de le positionner en haut à droite du wrapper.</p> <p>Et en bonus, la partie responsive.</p> <pre><code class="language-css">@media only screen and (max-width: 960px) { #wrapper { width: 96%; margin: 0 1%; padding: 0 1%; } } @media only screen and (max-width: 400px) { nav { width: 100%; } }</code></pre> <h2>Avec checked</h2> <p>La pseudo classe <code>:checked</code> gère les input tels que les <code>radio</code>, <code>checkbox</code> ou <code>option</code>. On peut modifier un ou plusieurs élements lorsque l'input est coché ou décoché. Pour se faire, on cache ce dernier et on affiche un <code>label</code> qui fera office de bouton cliquable.</p> <h3>Exemple</h3> <p>Un exemple avec une todolist.</p> <pre><code class="language-markup">&lt;div&gt; &lt;input type="checkbox" id="todolist-1"&gt; &lt;label for="todolist-1"&gt;Acheter du pain&lt;/label&gt; &lt;/div&gt; &lt;div&gt; &lt;input type="checkbox" id="todolist-2"&gt; &lt;label for="todolist-2"&gt;Nettoyer la tour du PC&lt;/label&gt; &lt;/div&gt; &lt;div&gt; &lt;input type="checkbox" id="todolist-3"&gt; &lt;label for="todolist-3"&gt;Réserver un billet pour la Lune&lt;/label&gt; &lt;/div&gt;</code></pre> <p>Puis on ajoute l'action sur les input de type checkbox dans le CSS.</p> <pre><code class="language-css">input[type=checkbox] + label { font-style: italic; } input[type=checkbox]:checked + label { color: #f00; font-style: normal; text-decoration:line-through; }</code></pre> <h3>HTML</h3> <p>On commence en douceur avec le code HTML en mettant un input avec un id que l'on tachera de masquer en CSS juste après la balise d'ouverture <code>&lt;body&gt;</code>.</p> <pre><code class="language-markup">&lt;!-- Ici l'input caché par défaut --&gt; &lt;input type="checkbox" id="menu-left"&gt;</code></pre> <p>Puis notre bouton de fermeture est un label.</p> <pre><code class="language-markup">&lt;!-- Ici le menu caché par défaut (#nav-left)--&gt; &lt;!-- Bouton de fermeture suivant l'exemple --&gt; &lt;li&gt; &lt;label for="menu-left" id="menu-left"&gt; &lt;span aria-label="Close Navigation"&gt;&amp;#10799;&lt;/span&gt; &lt;/label&gt; &lt;/li&gt;</code></pre> <p>Et notre menu &quot;burger&quot; est également contenu dans un label.</p> <pre><code class="language-markup">&lt;!-- Menu "burger" dans la balise #wrapper --&gt; &lt;label for="menu-left" id="menu-left"&gt; &lt;!-- Affiché par défaut --&gt; &lt;span class="open-menu" aria-label="Open Navigation"&gt;&amp;#9776;&lt;/span&gt; &lt;!-- Caché par défaut --&gt; &lt;span class="close-menu" aria-label="Close Navigation"&gt;&amp;#10799;&lt;/span&gt; &lt;/label&gt;</code></pre> <h3>CSS</h3> <pre><code class="language-css">#wrapper #menu-left { position: absolute; right: 1%; top: 0; font-size: 28px; line-height: 28px; } #menu-left { cursor: pointer; /* Evite la séléction du texte au double clic */ -webkit-user-select: none; -moz-user-select: none; -khtml-user-select: none; -ms-user-select: none; } #menu-left[type="checkbox"], #menu-left[type="checkbox"]:checked ~ * .open-menu, .close-menu { display: none; } #menu-left[type="checkbox"]:checked ~ nav { left: 0; transition: left 0.75s; } #menu-left[type="checkbox"]:checked ~ * .close-menu { display: block; }</code></pre> <p>On cache les élements :</p> <ol> <li>L'input checkbox ;</li> <li>L'icone de fermeture par défaut (menu fermé) ;</li> <li>Lorsque le menu est ouvert, l'icone d'ouverture du menu.</li> </ol> <p>On affiche la transition lorsque la checkbox est sélectionnée (le bloc du menu transite de la gauche vers la droite avec un délai de 0.75 secondes).</p> <p>Lorsque le menu est ouvert, on affiche l'icone de fermeture. Le code est disponible sur <a href="https://jsfiddle.net/c6ddarsf">JSFiddle</a>.</p> <p>Globalement, l'interaction fonctionne correctement sauf que... le lien n'est pas accessible avec la tabulation du clavier. :(</p> <p><img src="http://i.giphy.com/3zWquzsW3lo3u.gif" alt="" /></p> <h2>Avec target</h2> <p>La pseudo classe <code>:target</code> est mieux taillée pour cet exercice. En effet, elle permet de gérer l'action au clic sur les liens préfixés d'un dièse (ou &quot;hashtag&quot;) avec le nom de l'id associé.</p> <h3>Exemple</h3> <p>Ci-dessous un aperçu avec l'affichage d'un contenu caché par défaut et affichable depuis un lien.</p> <pre><code class="language-markup">&lt;a href="#result"&gt;Afficher le résultat&lt;/a&gt; &lt;div id="result"&gt; &lt;h1&gt;42 !&lt;/h1&gt; &lt;a href="#"&gt;Cacher le résultat&lt;/a&gt; &lt;/div&gt;</code></pre> <p>L'objectif est d'afficher le résultat la balise &quot;#result&quot; depuis le lien &quot;Afficher le résultat&quot;.</p> <pre><code class="language-css">#result:not(:target) { display: none; } #result:target { display: block; }</code></pre> <p>A la première ligne, le combo des 2 pseudos classes <code>:not(:target)</code> annule l'action lorsqu'il n'y a pas de clic sur le lien.</p> <h3>HTML</h3> <p>Le bouton de fermeture devient un simple lien.</p> <pre><code class="language-markup">&lt;!-- Ici le menu caché par défaut (#nav-left)--&gt; &lt;!-- Bouton de fermeture suivant l'exemple --&gt; &lt;li&gt; &lt;a href="#" aria-label="Close Navigation"&gt;&amp;#10799;&lt;/a&gt; &lt;/li&gt;</code></pre> <p>Quant au menu &quot;burger&quot;, on met en place 2 liens dont le premier lien pointe vers l'anchor &quot;#nav-left&quot; et le second est masqué par défaut.</p> <pre><code class="language-markup">&lt;div id="menu-left"&gt; &lt;!-- Affiché par défaut --&gt; &lt;a href="#nav-left" class="open-menu" aria-label="Open Navigation"&gt;&amp;#9776;&lt;/a&gt; &lt;!-- Masqué par défaut --&gt; &lt;a href="#" class="close-menu" aria-label="Close Navigation"&gt;&amp;#10799;&lt;/a&gt; &lt;/div&gt;</code></pre> <h3>CSS</h3> <p>Le code CSS est plus léger.</p> <pre><code class="language-css">#menu-left:not(target) { position: absolute; cursor: pointer; top: 0; right: 1%; /* Evite la séléction du texte au double clic */ -webkit-user-select: none; -moz-user-select: none; -khtml-user-select: none; -ms-user-select: none; font-size: 28px; line-height: 28px; } #nav-left:target { left: 0; transition: left 0.75s; } .open-menu, #nav-left:target ~ * .close-menu { display: block; } .close-menu, #nav-left:target ~ * .open-menu { display: none; }</code></pre> <p>Contairement à la méthode <code>:checked</code>, le lien du menu est bien accessible avec la tabulation du clavier.</p> <p>Le code est disponible sur <a href="https://jsfiddle.net/wfgndLa5">JSFiddle</a>.</p> <p><img src="http://i.giphy.com/3o6ZtbVUnhZoKtdgaI.gif" alt="" /></p> <h2>Conclusion</h2> <p>Ces 2 exemples affichent le même résultat excepté la navigation par tabulation qui n'est possible qu'avec <code>:target</code>. Quant à <code>:checked</code>, il sera plus utilisé pour la gestion des formulaires. Bien entendu le menu n'est pas 100% accessible notamment pour fermer le menu avec la touche Echap du clavier.</p> <h2>Sources</h2> <ul> <li>Compatiblité navigateurs <a href="http://caniuse.com/#search=%3Achecked">http://caniuse.com/#search=%3Achecked</a> et <a href="http://caniuse.com/#search=%3Atarget">http://caniuse.com/#search=%3Atarget</a></li> <li>Caractères au format Unicode : <a href="http://graphemica.com">http://graphemica.com</a></li> </ul>; 2016-09-13 12:36:47 Auto reload sur Go avec Gulp https://etienner.fr/auto-reload-sur-go-avec-gulp <p>Lors de la réalisation d'une application web sur Go, il faut compiler l'ensemble de l'application afin de faire tourner le serveur. Cette méthode permet de voir si l'application fonctionne correctement hors c'est une tache redondante dont on se passerait bien. Avec Gulp il est possible d'automatiser cette tache. Dès qu'un fichier go est modifié, une tache compile l'application dans le répertoire &quot;bin&quot; du dossier &quot;gopath&quot; (via <code>go install</code>) puis une autre lance l'exécutable, autrement dit, le serveur.</p> <p>On a donc 3 tâches distinctes :</p> <ul> <li>Compilation de l'application ;</li> <li>Lancement du serveur ;</li> <li>Surveillance des fichiers modifiés.</li> </ul> <p>Avant de commencez, vérifier que NodeJS et NPM sont installés sur votre machine <code>node -v &amp;&amp; npm -v</code> ainsi que la variable d'environnement &quot;GOPATH&quot; avec <code>go env</code>.</p> <h2>Préparation du fichier Gulp</h2> <p>A la racine de votre dossier, créer un fichier &quot;package.json&quot;.</p> <pre><code class="language-javascript">{ "devDependencies": { "gulp": "^3.8.11", "gulp-util": "*", "gulp-sync": "*", "gulp-livereload": "*", "node-notifier": "*" } } </code></pre> <p>Puis installez ces modules avec <code>npm install</code>.</p> <p>Et ensuite le fichier Gulpfile.js avec l'appel des dépendances installées.</p> <pre><code class="language-javascript">const gulp = require('gulp'), util = require('gulp-util'), notifier = require('node-notifier'), sync = require('gulp-sync')(gulp).sync, reload = require('gulp-livereload'), child = require('child_process'), os = require('os'); var server = null;</code></pre> <h2>Compilation de l'application</h2> <pre><code class="language-javascript">gulp.task('server:build', function() { var build = child.spawnSync('go', ['install']); return build; });</code></pre> <p>Sauf que ce n'est pas fini. Les erreurs de compilation n'ont pas été pris en compte.</p> <pre><code class="language-javascript">gulp.task('server:build', function() { var build = child.spawnSync('go', ['install']); if (build.stderr.length) { util.log(util.colors.red('Something wrong with this version :')); var lines = build.stderr.toString() .split('\n').filter(function(line) { return line.length }); for (var l in lines) util.log(util.colors.red( 'Error (go install): ' + lines[l] )); notifier.notify({ title: 'Error (go install)', message: lines }); } return build; });</code></pre> <p>Les erreurs sont ainsi affichées à la fois dans le terminal et dans une info bulle.</p> <h2>Lancement du serveur</h2> <pre><code class="language-javascript">gulp.task('server:spawn', function() { // Arrêt du serveur if (server &amp;&amp; server !== 'null') { server.kill(); } // Nom de l'application if (os.platform() == 'win32') { // Windows var path_folder = __dirname.split('\\'); } else { // Linux / MacOS var path_folder = __dirname.split('/'); } var length = path_folder.length; var app = path_folder[length - parseInt(1)]; // Si on est sur Windows if (os.platform() == 'win32') { server = child.spawn(app + '.exe'); } else { server = child.spawn(app); } // Affiche les données de votre application server.stderr.on('data', function(data) { //(console.log(data.toString(); plus avancé) process.stdout.write(data.toString()); }); });</code></pre> <p>Cette tâche est la plus longue :</p> <ol> <li>Si le serveur est en route, on l'arrète ;</li> <li>On récupère le nom de l'application ;</li> <li>On éxécute l'exécutable qui est le nom de l'application ;</li> <li>On affiche le contenu de l'application dans le terminal.</li> </ol> <h2>Surveillance</h2> <pre><code class="language-javascript">gulp.task('server:watch', function() { gulp.watch([ '*.go', '**/*.go', ], sync([ 'server:build', 'server:spawn' ], 'server')); });</code></pre> <p>Cette tâche surveille les fichiers &quot;.go&quot; présents à la racine et dans les dossiers puis éxecute les 2 tâches précédentes.</p> <h2>C'est parti !</h2> <p>Ajoutez la ligne ci-dessous qui va exécuter nos 3 tâches.</p> <pre><code class="language-javascript">gulp.task('default', ['server:build', 'server:spawn', 'server:watch']);</code></pre> <p>Tapez simplement <code>gulp</code>.</p>; 2016-03-19 19:51:28 Une API RESTful sans serveur de base de données https://etienner.fr/une-api-restful-sans-serveur-de-base-de-donnees <p>Créer une API RESTful en PHP c'est tout à fait possible. Pour cela on utilise traditionnellement une base de donnée en SQL comme MySQL ou MariaDB ou en NoSQL avec MongoDB, Redis... Mais utiliser les données directement dans un fichier JSON est également possible. En partant du principe que les données sont stockées dans un tableau, il est tout à fait possible de manipuler ce dernier par la suite.</p> <p><img src="https://media.giphy.com/media/8FNlmNPDTo2wE/giphy.gif" alt="" /></p> <p>Ci-dessous, le contenu du fichier &quot;db.json&quot;.</p> <pre><code class="language-javascript">[ { "id": 1, "title": "Interstellar", "releaseYear" : 2014 }, { "id": 2, "title": "The Revenant", "releaseYear": 2016 }, { "id": 3, "title": "Snowpiercer", "releaseYear": 2013 }, { "id": 4, "title": "The Host", "releaseYear": 2006 }, { "id": 5, "title": "Sicario", "releaseYear": 2015 } ]</code></pre> <p>Coté serveur, on va mettre en place un design pattern (&quot;patron de conception&quot;) de type singleton. Concrètement, on instancie l'ouverture du fichier JSON dans le constructeur de notre classe. On évite ainsi de l'appeler dans les 4 fonctions destinées au CRUD (Create Read, Update, Delete) :</p> <ul> <li><code>FetchAll</code> : récupère toutes les lignes sous forme d'un tableau ;</li> <li><code>FetchOne</code> : récupère une ligne en particulier ;</li> <li><code>Create</code> : créé une nouvelle ligne ;</li> <li><code>Update</code> : modifie une ligne en fonction de l'id ;</li> <li><code>Delete</code> : supprime une ligne en fonction de l'id.</li> </ul> <h2>Préparation de la classe</h2> <pre><code class="language-php">&lt;?php class Api { private $file_json = 'db.json'; function __construct() { // Ouverture du fichier $this-&gt;json = file_get_contents($this-&gt;file_json); // Tableau des données en PHP $this-&gt;get = json_decode($this-&gt;json, true); } // Affichage des messages / données JSON private function Display($data) { echo json_encode($data, JSON_UNESCAPED_UNICODE); } // Modification du fichier JSON private function Set($data) { file_put_contents($this-&gt;file_json, json_encode($data)); } // Id du tableau PHP private function CurrentRow($id) { return $id - 1; } // Nos futures fonctions destinées au CRUD }</code></pre> <p>Dans le constructeur, on instancie 2 variables :</p> <ul> <li><code>$this-&gt;json</code> : ouverture du fichier JSON avec <code>file_get_contents()</code> ;</li> <li><code>$this-&gt;get</code> : décode le JSON avec <code>json_decode()</code> sous la forme d'un tableau.</li> </ul> <p>Puis on prépare 2 fonctions privées :</p> <ul> <li><code>Display()</code> afficher des messages / données au format JSON avec <code>json_encode()</code> ;</li> <li><code>Set()</code> modifier le fichier JSON avec <code>file_put_contents()</code> ;</li> <li><code>CurrentRow()</code> l'id du tableau PHP (et non JSON) qui commence à partir de 0 (et non 1).</li> </ul> <h3>Toutes les données</h3> <pre><code class="language-php">// Lecture de tous le tableau JSON public function FetchAll() { foreach ($this-&gt;get as $row) { // Récupération du tableau de données $rows[] = $row; } $this-&gt;Display($rows); }</code></pre> <p>Dans la première fonction publique <code>FetchAll()</code>, on récupère toutes les données présentes dans le fichier JSON que l'on affiche sous la forme d'un tableau.</p> <h3>Une seule ligne</h3> <pre><code class="language-php">// Lecture d'une ligne à partir de son "id" public function FetchOne($id) { // Récupération du tableau de données $data = $this-&gt;get; // Vérification de l'existence de la ligne if (isset($data[$this-&gt;CurrentRow($id)])) { $this-&gt;Display($data[$this-&gt;CurrentRow($id)]); } else { $this-&gt;Display(array("error" =&gt; "the row #$id doesn't exists")); header("HTTP/1.0 404 Not Found"); } }</code></pre> <p>Avec comme paramètre dans la fonction <code>FetchOne()</code>, l'id d'une ligne, on récupère la ligne correspondante. Si elle n'existe pas, on affiche un message d'erreur.</p> <h3>Création d'une ligne</h3> <pre><code class="language-php">// Création d'une nouvelle ligne public function Create($data_input = array()) { if (!empty($data_input)) { // Récupération du tableau de données $data = $this-&gt;get; // Ajout d'un nouveau champ id $id = array('id' =&gt; count($data) + 1); // Fusion des données dans cette nouvelle ligne $row = array_merge($id, $datas_input); // Ajout de la nouvelle ligne dans le tableau général $all_data = array_merge($data, array($row)); // Modification du fichier JSON $this-&gt;Set($all_data); // Message de succès $this-&gt;Display(array("success" =&gt;"posted new row ", "data" =&gt; $row)); header("HTTP/1.0 201 Created"); } }</code></pre> <p>Dans la fonction <code>Create()</code>, on passe en paramètre les données à ajouter sous forme d'un tableau. Dans un premier temps, on récupère toutes les données présentes dans le fichier JSON. On adjoint dans ce tableau une nouvelle ligne dans laquelle on fusionne les données ajoutées en paramètre avec un nouvel id. Puis on sauvegarde ce nouveau tableau avec <code>$this-&gt;Set()</code>.</p> <h3>Modification d'une ligne</h3> <pre><code class="language-php">// Modification d'une ligne public function Update($id, $data_input = array()) { // Récupération du tableau de données $data = $this-&gt;get; // Vérification de l'existence de la ligne if (isset($data[$this-&gt;CurrentRow($id)])) { // MAJ de la ligne concernée $update = array_merge(array('id' =&gt; $id), $data_input); // Vérification de modification $diff = array_diff($update, $data[$this-&gt;CurrentRow($id)]); if (empty($diff)) { $this-&gt;Display(array("warning" =&gt; "no change")); } else { // Remplacement de la ligne $data[$this-&gt;CurrentRow($id)] = $update; // Modification du fichier JSON $this-&gt;Set($data); $this-&gt;Display(array("success" =&gt; "row updated", "data" =&gt; $update)); } } else { $this-&gt;Display(array("error" =&gt; "the row #$id doesn't exists")); header("HTTP/1.0 404 Not Found"); } }</code></pre> <p>Pour modifier une ligne, dans la fonction <code>Update()</code> on récupère toutes les données du fichier JSON. On vérifie que la ligne concernée existe. Puis comme pour créer une nouvelle ligne, on fusionne avec l'id, les données à modifier spécifiées en paramètre. On en profite également pour vérifier que ces dernières sont bien différentes que celles déja existantes. Si c'est le cas, alors on met à jour le fichier JSON avec la ligne modifiée.</p> <h3>Suppression d'une ligne</h3> <pre><code class="language-php">// Suppression d'une ligne public function Delete($id) { // Récupération du tableau de données $data = $this-&gt;get; // Vérification de l'existence de la ligne if (isset($data[$this-&gt;CurrentRow($id)])) { // Suppression de la ligne concernée dans le tableau unset($data[$this-&gt;CurrentRow($id)]); // Modification du fichier JSON $this-&gt;Set($data); $this-&gt;Display(array("success" =&gt; "row #$id deleted")); } else { $this-&gt;Display(array("error" =&gt; "the row #$id doesn't exists")); header("HTTP/1.0 404 Not Found"); } }</code></pre> <p>Pour supprimer une ligne, dans la fonction <code>Delete()</code> on récupère toutes les données du tableau. On vérifie que la ligne existe bien pour la supprimer et modifier le fichier JSON avec le tableau modifié.</p> <h2>Test</h2> <p>Dans le même fichier, après la fermeture de la classe, on appel cette dernière dans une variable.</p> <pre><code class="language-php">header('Content-Type: application/json; charset=utf-8'); // Appel de la classe "Api" $api = new Api();</code></pre> <p>Puis, on peut afficher toutes les données.</p> <pre><code class="language-php">$api-&gt;FetchAll();</code></pre> <p>Afficher uniquement la ligne ayant l'id 4.</p> <pre><code class="language-php">$api-&gt;FetchOne(4);</code></pre> <p>Créer une nouvelle ligne.</p> <pre><code class="language-php">$api-&gt;Create(array('title' =&gt; 'Prisonners', 'releaseYear' =&gt; 2012));</code></pre> <p>Modifier la ligne ayant l'id 6.</p> <pre><code class="language-php">$api-&gt;Update(6, array('title' =&gt; 'Prisoners', 'releaseYear' =&gt; 2013));</code></pre> <p>Supprimer la ligne ayant l'id 6.</p> <pre><code class="language-php">$api-&gt;Delete(6);</code></pre> <h2>Refactoring</h2> <p>Dans notre classe, on met en place 2 fonctions privées pour alléger nos 3 dernières fonctions publiques :</p> <ul> <li><code>CheckFound()</code> : vérifier que la ligne existe ;</li> <li><code>CheckData()</code> : verifier qu'il y a bien des données fournies en paramètre.</li> </ul> <h3>Gestion des erreurs 404</h3> <pre><code class="language-php">// Vérification de l'existence de la ligne private function CheckFound($id) { // Récupération du tableau de données $data = $this-&gt;get; if (!isset($data[$this-&gt;CurrentRow($id)])) { $this-&gt;Display(array("error" =&gt; "the row #$id doesn't exists")); header("HTTP/1.0 404 Not Found"); } } </code></pre> <p>Si la ligne n'existe pas, on affiche une erreur de type 404.</p> <h3>Gestion des données manquantes</h3> <pre><code class="language-php">private function CheckData($data_input = array()) { if (empty($data_input)) { $this-&gt;Display(array("error" =&gt; "missing data")); header("HTTP/1.0 400 Bad Request"); } }</code></pre> <p>Si le paramètre <code>$data_input</code> est vide, on affiche une erreur de type 400.</p> <h3>Réécritures des fonctions CRUD concernées</h3> <p>On peut ainsi réécrire les fonctions <code>FetchOne()</code>, <code>Create()</code>, <code>Update()</code> et <code>Delete()</code>.</p> <pre><code class="language-php">// Lecture d'une ligne à partir de son "id" public function FetchOne($id) { if ($this-&gt;CheckFound($id)) { // Récupération du tableau de données $data = $this-&gt;get; $this-&gt;Display($data[$this-&gt;CurrentRow($id)]); } } // Création d'une nouvelle ligne public function Create($data_input = array()) { if ($this-&gt;CheckData($data_input)) { // Récupération du tableau de données $data = $this-&gt;get; // Ajout d'un nouveau champ id $id = array('id' =&gt; count($data) + 1); // Fusion des données dans cette nouvelle ligne $row = array_merge($id, $data_input); // Ajout de la nouvelle ligne dans le tableau général $all_data = array_merge($data, array($row)); // Modification du fichier JSON $this-&gt;Set($all_data); // Message de succès $this-&gt;Display(array("success" =&gt; "posted new row ", "data" =&gt; $row)); header("HTTP/1.0 201 Created"); } } // Modification d'une ligne public function Update($id, $data_input = array()) { // Vérification de l'existence de la ligne if ($this-&gt;CheckFound($id) &amp;&amp; $this-&gt;CheckData($data_input)) { // Récupération du tableau de données $data = $this-&gt;get; // MAJ de la ligne concernée $update = array_merge(array('id' =&gt; $id), $data_input); // Vérification de modification $diff = array_diff($update, $data[$this-&gt;CurrentRow($id)]); if (empty($diff)) { $this-&gt;Display(array("warning" =&gt; "no change")); } else { // Remplacement de la ligne $data[$this-&gt;CurrentRow($id)] = $update; // Modification du fichier JSON $this-&gt;Set($data); $this-&gt;Display(array("success" =&gt; "row updated", "data" =&gt; $update)); } } } // Suppression d'une ligne public function Delete($id) { // Vérification de l'existence de la ligne if ($this-&gt;CheckFound($id)) { // Récupération du tableau de données $data = $this-&gt;get; // Suppression de la ligne concernée dans le tableau unset($data[$this-&gt;CurrentRow($id)]); // Modification du fichier JSON $this-&gt;Set($data); $this-&gt;Display(array("success" =&gt; "row #$id deleted")); } }</code></pre> <h2>Conclusion</h2> <p>Avec des conditions, des boucles et des fonctions natives de manipulation de tableau, il est finalement simple de mettre en place une classe pour manipuler une API en JSON sans serveur de base de données derière. D'autant plus qu'il est possible par la suite, d'imaginer la mise en place dans cette même classe d'un contrôle sur la structure de l'API (type de champ, champ obligatoire, unicité d'un champ, etc...).</p>; 2016-03-14 19:30:43 Serveur HTTP 2 avec Go 1.6 https://etienner.fr/serveur-http-2-avec-go-16 <p>Bonne nouvelle, depuis la version 1.6 de Go, il est possible d'implémenter le HTTP/2, autrement dit, le futur de HTTP/1.1 existant depuis plus d'une décennie (1994 pour la version 1 et de 1997 à 2014 pour la version 1.1). Cette nouvelle version siglée RFC 7540 propulse le chargement des fichiers en multiplexage avec des entêtes compressées. Autant vous dire que le temps de chargement est au rendez-vous pour la moyenne des sites actuels dont les pages pèsent environ 2 Mo avec 60 / 80 requêtes. Pas de panique, les methodes (GET, POST, PUT, DELETE, etc...) ne changent pas ainsi que les codes de statuts, les entêtes et la négociation.</p> <p><img src="../assets/img/news/golang-http2/http1.1_vs_http2.jpg" alt="" /></p> <p>Pour pouvoir mettre en place HTTP/2, il faut exécuter le serveur avec le chiffrement TLS, autrement dit HTTPS.</p> <h2>Préparation de la clef et du certificat</h2> <p>Pour que TLS fonctionne correctement, il faut générer un fichier qui va contenir la clef publique et un certificat. On utilise l'utilitaire &quot;openssl&quot; à la racine du projet pour générer nos 2 fichiers.<br /> On commence par générer la clef privée : <code>openssl genrsa -out localhost.key 2048</code> afin de générer le certificat : <code>openssl req -new -x509 -key localhost.key -out localhost.pem -days 730</code></p> <ul> <li>&quot;-x509&quot; : le cryptage utilisé ;</li> <li>&quot;-key localhost.key&quot; : le fichier de la clef publique ;</li> <li>&quot;out localhost.pem&quot; : le fichier du certificat ;</li> <li>&quot;-days 365&quot; : correspond au nombre de jour (ici 1 an) de validité du certificat.</li> </ul> <h2>Configurer HTTP/2</h2> <p>Tout d'abord si votre version de Go (<code>go version</code>) est inférieur à la 1.6, il faut télécharger la librairie mise à disposition pour HTTP/2 : <code>go get golang.org/x/net/http2</code> pour l'importer avec les autres librairies dont nous aurons besoin par la suite.</p> <pre><code class="language-go">package main import ( "fmt" "log" "net/http" // Go &lt; 1.6 "golang.org/x/net/http2" )</code></pre> <p>Si vous travaillez sur la version 1.6 ou +, vous n'avez pas besoin de suivre cette partie.<br /> Dans la fonction principale &quot;main()&quot;, on déclare une variable &quot;s&quot; de type &quot;http.Server&quot;. On active les logs dans le terminal du serveur en passant la valeur de &quot;http2.VerboseLogs&quot; à &quot;true&quot; sans oublier &quot;http2.ConfigureServer&quot; dans laquelle on met en premier paramètre l'expression &quot;&amp;s&quot; et en second &quot;nil&quot;.</p> <pre><code class="language-go">func main() { // Configuration de HTTP2 pour Go &lt; 1.6 var s http.Server http2.VerboseLogs = true http2.ConfigureServer(&amp;s, nil) // Suite du code )</code></pre> <h2>Création et appel d'une route</h2> <p>Pour afficher un résultat dans la route d'accueil, on créé une nouvelle fonction &quot;indexHandler&quot; avec les paramètres de la librairie http (&quot;w http.ResponseWriter, r *http.Request&quot;).</p> <pre><code class="language-go">// Route d'accueil func indexHandler(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "text/plain; charset=UTF-8") fmt.Fprintln(w, "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.") }</code></pre> <p>Rien d'extraordinaire, on affiche seulement du texte au format UTF-8 via &quot;fmt.Fprintln()&quot;. Puis dans notre fonction &quot;main()&quot;, on appelle cette route dans la fonction &quot;http.HandleFunc()&quot;.</p> <pre><code class="language-go">func main() { // Configuration de HTTP2 pour Go &lt; 1.6 var s http.Server http2.VerboseLogs = true http2.ConfigureServer(&amp;s, nil) // Appel de la route d'accueil http.HandleFunc("/", indexHandler) // Suite du code }</code></pre> <h2>ListenAndServeTLS</h2> <p>Maintenant que l'on a activé HTTP 2 et appelé notre unique route, il ne manque plus que le &quot;démarreur&quot; de notre serveur. Pour cela on utilise, la fonction &quot;http.ListenAndServeTLS()&quot; dans laquelle on indique le port (443 par défaut), le nom de notre certificat (&quot;localhost.pem&quot;), la clef publique (&quot;localhost.key&quot;) et &quot;nil&quot;.</p> <pre><code class="language-go">func main() { // Configuration de HTTP2 pour Go &lt; 1.6 var s http.Server http2.VerboseLogs = true http2.ConfigureServer(&amp;s, nil) // Appel de la route d'accueil http.HandleFunc("/", indexHandler) // Lancement du serveur HTTPS err := http.ListenAndServeTLS(":443", "localhost.pem", "localhost.key", nil) if err != nil { log.Fatal("ListenAndServe: ", err) } }</code></pre> <p>S'il y a une erreur au lancement du serveur (mauvais paramètre, fichier manquant, etc..) elle s'affichera grâce à &quot;log.Fatal()&quot;.</p> <h2>Lancement du serveur</h2> <p>Lancez votre serveur avec <code>go run main.go</code>.</p> <p>Dans votre navigateur Internet, accédez à votre serveur via <a href="https://localhost">https://localhost</a> (et non <a href="http://localhost">http://localhost</a> !!!). A la première connexion, vous devez accepter le certificat demandé par votre navigateur.</p> <p>Remarque : votre navigateur vous informe que le certificat est dangereux. C'est tout à fait juste car ce dernier n'est pas signé par une autorité compétente.</p> <h3>Linux</h3> <p>Sur Linux lorsque vous tentez de lancez le serveur avec le port 443 vous avez le droit à l'erreur suivante : <code>ListenAndServe: listen tcp :443: bind: permission denied</code>. En effet, il faut lancer la commande avec les privilèges de <code>sudo</code>.<br /> Pour résoudre ce problème d'autorisation, ouvrez le fichier de configuration de l'utilitaire sudo : <code>sudo vim /etc/sudoers</code> et ajoutez les 2 lignes ci-dessous :</p> <pre><code>Defaults env_keep +="GOPATH" Defaults env_keep +="GOROOT"</code></pre> <p>Puis enregistrez cette modification avec &quot;wq!&quot; et lancez le serveur avec <code>sudo go run main.go</code>.</p> <h2>Sources</h2> <ul> <li>Fonction <code>ConfigureServer</code> de golang.org/x/net/http2 : <a href="https://godoc.org/golang.org/x/net/http2#ConfigureServer">https://godoc.org/golang.org/x/net/http2#ConfigureServer</a></li> <li>Fonction <code>ListenAndServeTLS</code> : <a href="https://golang.org/pkg/net/http/#ListenAndServeTLS">https://golang.org/pkg/net/http/#ListenAndServeTLS</a></li> <li>A propos du fichier sudoers <a href="https://doc.ubuntu-fr.org/sudoers">https://doc.ubuntu-fr.org/sudoers</a></li> <li>Des outils pour tester HTTP 2https://blog.cloudflare.com/tools-for-debugging-testing-and-using-http-2</li> <li>&quot;HTTP/2 : quels sont les nouveautés et les gains ?&quot; <a href="https://devcentral.f5.com/articles/http2-est-l-quels-sont-les-gains-14945">https://devcentral.f5.com/articles/http2-est-l-quels-sont-les-gains-14945</a></li> </ul>; 2016-03-02 15:45:19 Serveur PHP avec Gulp https://etienner.fr/serveur-php-avec-gulp <p>Gulp est un &quot;task manager&quot;, littéralement un gestionnaire de tâches. Il nous offre la possibilité de faire le lien avec PHP et Gulp pour ainsi se passer d'Apache ou de Nginx avec moins d'options mais suffisament pour un projet de petite envergure. Par la suite, on utilisera &quot;Livereload&quot; via &quot;gulp-livereload&quot; et le SASS via &quot;gulp-sass&quot;.</p> <p>Avant de commencer, il faut PHP installé sur votre PC (<code>php -v</code>). Ainsi que le &quot;package manager&quot; (gestionnaire de paquets) NPM sous NodeJS (<code>npm -v</code>). Pour le serveur PHP, on va utiliser la dépendance &quot;gulp-connect-php&quot;. </p> <h2>Préparation du serveur</h2> <p>Dans un répertoire quelconque, créez un fichier Gulpfile.js.</p> <pre><code class="language-javascript">var gulp = require('gulp'), php = require('gulp-connect-php'); gulp.task('serve', function() { php.server(); });</code></pre> <p>Puis, téléchargez les 2 dépendances suivantes dans un terminal (placé dans le même répertoire que le Gulpfile) :</p> <ul> <li><code>npm install gulp</code></li> <li><code>npm install gulp-connect-php</code></li> </ul> <p>Créez un fichier &quot;index.php&quot; :</p> <pre><code class="language-php">&lt;?php echo phpinfo(); ?&gt;</code></pre> <p>Toujours dans le terminal, lancez la commande pour lancer le serveur <code>gulp serve</code>.</p> <p>Sur votre navigateur Web, lancez <a href="http://127.0.0.1:8000">http://127.0.0.1:8000</a>. Le contenu du fichier &quot;index.php&quot; s'affiche car le serveur PHP fonctionne. Par la suite, on va voir qu'il est possible de changer le port par défaut 8000 par un autre.</p> <p>Plus intéressant, on installe le framework PHP Codeigniter à partir de Github (de la branche &quot;stable&quot;) <code>git clone https://github.com/bcit-ci/CodeIgniter.git</code>. Codeigniter est téléchargé dans un dossier &quot;CodeIgniter&quot;. Il faut donc changer la base du serveur et optionnellement son port.</p> <pre><code class="language-javascript">var gulp = require('gulp'), php = require('gulp-connect-php'); gulp.task('serve', function() { php.server({ port: 80, // Port (8000 par défaut) base: './CodeIgniter' // Base du projet }); });</code></pre> <p>L'application Codeigniter est désormais disponible sur <a href="http://127.0.0.1">http://127.0.0.1</a>.</p> <h2>Livereload</h2> <p>Dans notre fichier de configuration, on va ajouter une nouvelle fonctionnalité dans Gulp pour recharger automatiquement le navigateur lorsqu'un fichier PHP est modifié. Pour cela, on va utiliser la dépendance &quot;gulp-livereload&quot;.</p> <p>Avant de modifier le Gulpfile, installez l'extension pour Chrome ou Safari : <a href="http://livereload.com/extensions">http://livereload.com/extensions</a>.</p> <pre><code class="language-javascript">var gulp = require('gulp'), php = require('gulp-connect-php'), livereload = require('gulp-livereload'); gulp.task('serve', function() { php.server({ port: 80, // Port (8000 par défaut) base: './CodeIgniter' // Base du projet }); livereload({start: true}); var livereloadPage = function () { livereload.reload(); }; gulp.watch('**/*.php', livereloadPage); });</code></pre> <p>On demande à Livereload de surveiller les fichiers PHP. Si un fichier est modifié, alors on reload la page. N'oubliez pas d'installer la dernière dépendance appelée <code>npm install gulp-livereload</code>. Puis tapez <code>gulp serve</code>.</p> <p>Sur Chrome, connectez-vous sur <a href="http://localhost">http://localhost</a> et activez le Livereload (un clic sur l'icone de l'extension).</p> <h2>SASS</h2> <p>On part du principe que dans le dossier &quot;assets&quot; présent à la racine de l'application CodeIgniter, sont présents 2 dossiers : &quot;css&quot; et &quot;sass&quot;.</p> <p><code>cd Codeigniter &amp;&amp; mkdir assets &amp;&amp; cd assets &amp;&amp; mkdir css &amp;&amp; mkdir sass</code></p> <p>Ensuite, on demande à Gulp de convertir les fichiers SCSS en CSS dans une tâche spécifique.</p> <pre><code class="language-javascript">var gulp = require('gulp'), php = require('gulp-connect-php'), livereload = require('gulp-livereload'), sass = require('gulp-sass'); gulp.task('serve', ['sass'], function() { php.server({ port: 80, // Port (8000 par défaut) base: './CodeIgniter' // Base du projet }); var livereloadPage = function () { livereload({start: true}); livereload.reload(); }; gulp.watch("./CodeIgniter/assets/sass/*.scss", ['sass']); gulp.watch('**/*.php', livereloadPage); }); gulp.task('sass', function () { gulp.src('./CodeIgniter/assets/sass/*.scss') .pipe(sass.sync().on('error', sass.logError)) .pipe(gulp.dest('./CodeIgniter/assets/css')) .pipe(livereload({start: true})); }); gulp.task('sass:watch', function () { gulp.watch('./CodeIgniter/assets/sass/*.scss', ['sass']); }); gulp.task('default', ['serve', 'sass:watch']);</code></pre> <p>Sans oublier d'installer la dépendance <code>npm install gulp-sass</code> et de relancer le serveur avec la commande <code>gulp</code>.</p> <p>Pour avoir un résultat flagrant, ouvrez le fichier de vue &quot;application/views/welcome_message.php&quot;, supprimez toutes les lignes de CSS et ajoutez la ligne d'appel dans le header du fichier.</p> <pre><code class="language-markup">&lt;link rel="stylesheet" href="../assets/css/style.css"&gt;</code></pre> <p>Puis dans le dossier &quot;assets/css/sass&quot;, créez un nouveau fichier &quot;style.scss&quot; contenant les propriétés ci-dessous :</p> <pre><code class="language-css">$color1: black; $color2: cyan; body { background: $color1; } body, a{ color: $color2; }</code></pre> <p><img src="../assets/img/news/gulp-livereload.gif" alt="" /></p> <p>Si vous faites une erreur en SASS, Gulp vous informe de l'erreur sans pour autant couper le serveur. Par exemple, avec un oubli du point virgule à la fin de la 1ère ligne, il renverra le message d'avertissement suivant :</p> <p><code>&gt; 2:1 top-level variable binding must be terminated by ';'</code></p> <h2>Astuces</h2> <h3>Manifeste des dépendances</h3> <p>On peut regrouper toutes nos dépendances dans un fichier Json afin d'éviter d'avoir à taper manuellement les commandes d'installation au cas par cas. A la racine du projet, créez un fichier &quot;package.json&quot;.</p> <pre><code class="language-javascript">{ "devDependencies": { "gulp": "^3.8.11", "gulp-connect-php": "*", "gulp-livereload": "*", "gulp-sass": "*" } }</code></pre> <p>Ainsi, vous pouvez installer toutes ces dépendances en une seule commande <code>npm install</code>.</p> <h3>Tâche par défaut</h3> <p>Ajoutez à la fin du Gulpfile la ligne de tâche par défaut ci-dessous :</p> <pre><code class="language-javascript">gulp.task('default', ['serve', 'sass:watch']);</code></pre> <p>Cela permet de taper &quot;gulp&quot; au lieux de &quot;gulp serve&quot; mais également de générer du CSS après modification du fichier SCSS sans lancer le serveur via la commande <code>gulp sass</code>.</p> <h3>gitignore</h3> <p>Une fois les dépendances téléchargées, elle prennent de la place dans le dossier &quot;node_modules&quot;. Pour ne pas les envoyer sur votre dépot Git, créez un fichier &quot;.gitignore&quot; afin d'ignorer ce dossier.</p> <pre><code class="language-markup"># Node node_modules npm-debug.log</code></pre> <p>&quot;npm-debug.log&quot; est un fichier de log d'erreur propre à NPM.</p> <h3>Bonus : code complet avec notification</h3> <p>Il est possible d'afficher une notification d'erreur en dehors de la console avec Gulp.</p> <pre><code class="language-javascript">var gulp = require('gulp'), php = require('gulp-connect-php'), livereload = require('gulp-livereload'), sass = require('gulp-sass'), errorNotifier = require('gulp-error-notifier'); gulp.task('serve', ['sass'], function() { php.server({ port: 80, base: './CodeIgniter' }); var livereloadPage = function () { livereload({start: true}); livereload.reload(); }; gulp.watch("./CodeIgniter/assets/sass/*.scss", ['sass']); gulp.watch('**/*.php', livereloadPage); }); gulp.task('sass', function () { gulp.src('./CodeIgniter/assets/sass/*.scss') .pipe(errorNotifier.handleError(sass())) .pipe(gulp.dest('./CodeIgniter/assets/css')) .pipe(livereload({start: true})); }); gulp.task('sass:watch', function () { gulp.watch('./CodeIgniter/assets/sass/*.scss', ['sass']); }); gulp.task('default', ['serve', 'sass:watch']);</code></pre> <p>Et package.json</p> <pre><code class="language-javascript">{ "devDependencies": { "gulp": "^3.8.11", "gulp-connect-php": "*", "gulp-livereload": "*", "gulp-sass": "*", "gulp-util": "*", "gulp-sync": "*", "gulp-error-notifier": "*", "node-notifier": "*" } }</code></pre> <p>En effet, &quot;gulp-error-notifier&quot; fonctionne avec &quot;node-notifier&quot;.</p> <h2>Sources</h2> <ul> <li>Gulp Connect : <a href="https://www.npmjs.com/package/gulp-connect-php">https://www.npmjs.com/package/gulp-connect-php</a></li> <li>Gulp Livereload : <a href="https://www.npmjs.com/package/gulp-livereload">https://www.npmjs.com/package/gulp-livereload</a></li> <li>Gulp SASS : <a href="https://www.npmjs.com/package/gulp-sass">https://www.npmjs.com/package/gulp-sass</a></li> <li>Gulp-error-notifier : <a href="https://www.npmjs.com/package/gulp-error-notifier">https://www.npmjs.com/package/gulp-error-notifier</a></li> <li>Node-notifier : <a href="https://www.npmjs.com/package/node-notifier">https://www.npmjs.com/package/node-notifier</a></li> </ul>; 2016-01-05 11:31:45 Un vrai mot de passe https://etienner.fr/un-vrai-mot-de-passe <p>Lorque vous développez un site web avec un dashboard (tableau de bord), vous devez mettre en place un système d'authentification. L'utilisateur devra rentrer un mot de passe que lui seul connait. Cette information est alors stockée dans une base de donnée. Hors si cette dernière se fait attaquée et les informations sont récupérées par une tiers personne, il faut que les mots de passe ne soient pas identifiables, autrement dit hachés. En PHP et dans de nombreux langages de programmations, il y a plusieurs façons de rendre ces mots de passe illisibles pour le commun des mortels.</p> <p><img src="http://i.giphy.com/X68QCGb5qx596.gif" alt="" /></p> <h2>MD5 et SHA1 au rebut</h2> <p>Encoder un mot de passe avec MD5 ou SHA-1 est une chose insignifiante pour un hacker. En effet, avec une liste de Rainbow Table (énorme table de base de données de MD5 ou SHA-1 et de leur équivalent en clair), attaque par force brute (test de toutes les combinaisons) ou attaque par dictionnaire, cela ne lui prendra pas beaucoup de temps pour déchiffrer les mots de passe de votre BDD.</p> <pre><code class="language-php">&lt;?php $password = 'password'; var_dump(md5($password)); var_dump(sha1($password)); ?&gt;</code></pre> <p><img src="http://i.giphy.com/uwm78X7lrvpdu.gif" alt="" /></p> <p>Cette méthode est décommendée de vive voix !</p> <h2>Du salt avec SHA-512 : &quot;cum grano salis&quot;</h2> <p>L'intéret du salt (ou clef secrète), c'est de &quot;casser&quot; la Rainbow Table en y mettant son grain de sel (&quot;cum grano salis&quot;). En salant avant de hacher, cette méthode est loin d'être infaillible car si le hacker trouve la clef secrète alors il pourra modifier son attaque en prenant en compte cette dernière.</p> <p><img src="http://i.giphy.com/11HOmFD2Fk1gaY.gif" alt="" /></p> <p>Depuis la version 5.1.2 de PHP, il existe une fonction <code>hash_hmac()</code> qui permet de générer ce genre de mot de passe rapidement :</p> <pre><code class="language-php">&lt;?php function hashPassword($password) { $hash = 'sha512'; $salt = 'clef secrete'; return hash_hmac($hash, $password, $salt); } print_r(hashPassword('password')); // mot de passe haché // MDP stocké en BDD $passwordCrypted = 'f4368737ffe88088e26b19099b408b6d9e0af4f103807541ad472d7fbd644f3da41903aa55d5a29155649a84ec2b52e12957754c196b415901e1bb45d7533a10'; function checkPassword($password, $passwordCrypted) { // MDP saisie par l'utilisateur == MDP en BDD if (hashPassword($password) == $passwordCrypted) { echo 'Le mot de passe est valide :)'; } else { echo 'Mauvais mot de passe :('; } } print_r(checkPassword('password', $passwordCrypted)); ?&gt;</code></pre> <p>Pour le hachage, il existe d'autres méthodes que &quot;sha512&quot; comme &quot;md5&quot;, &quot;sha256&quot;, &quot;haval160,4&quot;, etc... vous pouvez obtenir la liste complète avec la fonction <code>hash_algos()</code> :</p> <pre><code class="language-php">&lt;?php var_dump(hash_algos()); ?&gt;</code></pre> <h2>Addition salée avec Bcrypt</h2> <p>Avec Bcrypt, le salt n'est plus statique comme vue précédement mais généré aléatoirement ce qui a pour effet de générer un hash aléatoire du mot de passe.</p> <p><img src="http://i.giphy.com/ph7prW5qPhrZC.gif" alt="" /></p> <p>L'algorithme utilisé par Bcrypt pour créer la clef de hachage est &quot;CRYPT_BLOWFISH&quot; via &quot;PASSWORD_BCRYPT&quot; à partir de la version 5.5.0 de PHP.</p> <pre><code class="language-php">&lt;?php $options = [ 'cost' =&gt; 11, // Cout algorithmique 'salt' =&gt; mcrypt_create_iv(22, MCRYPT_DEV_URANDOM), // Salt automatique ]; // Génération du MDP $password = password_hash('password', PASSWORD_BCRYPT, $options); // Valeur aléatoire générée du MDP stocké en BDD $passwordCrypted = '$2y$11$pzXo0hIts06Tfcshew8HQeVmP8eY2bxcsChtslGLbzxrHbXDs0L9i'; function checkPassword($password, $passwordCrypted) { // Récupération du MDP saisie par l'utilisateur if (password_verify('password', $passwordCrypted)) { echo 'Le mot de passe est valide :)'; } else { echo 'Le mot de passe est invalide :('; } } print_r(checkPassword('password', $passwordCrypted)); ?&gt;</code></pre> <p>Quelques informations sur le mot de passe haché :</p> <ul> <li>&quot;$2y&quot; : identifiant de la clef de hachage standard de crypt()</li> <li>&quot;$11&quot; : niveau de difficulté (paramètre &quot;cost&quot; dans les options)</li> <li>&quot;$pzXo0hIts06Tfcshew8HQeVmP8eY2bxcsChtslGLbzxrHbXDs0L9i&quot; : le sel et le mot de passe en base64</li> </ul> <p>Quant à la longueur totale du MDP haché, elle sera toujours de 60 caractères.</p> <p>Remarque : pour utiliser Bcrypt, il faut que l'extension PHP &quot;mcrypt&quot; soit installée et activée sur votre serveur PHP.</p> <h2>Conclusion</h2> <p>Avoir des données protégées est un gage de confiance vis à vis des internautes inscris. Pour plus de sécurité, vous pouvez appliquer des règles lors de la création du compte en demandant à l'utilisateur au moins une majuscule et un caractère spécial lors de la création de son mot de passe.</p> <h2>Sources</h2> <ul> <li>Inspiration pour cet article : <a href="https://linuxfr.org/users/elyotna/journaux/l-art-de-stocker-des-mots-de-passe">https://linuxfr.org/users/elyotna/journaux/l-art-de-stocker-des-mots-de-passe</a></li> <li>Fonction <code>hash_hmac</code> : <a href="http://php.net/manual/fr/function.hash-hmac.php">http://php.net/manual/fr/function.hash-hmac.php</a></li> <li>Fonction <code>password_hash</code> : <a href="http://php.net/manual/fr/function.password-hash.php">http://php.net/manual/fr/function.password-hash.php</a></li> <li>Fonction <code>password_verify</code> :<a href="http://php.net/manual/fr/function.password-verify.php">http://php.net/manual/fr/function.password-verify.php</a></li> <li>Rainbox table : <a href="https://fr.wikipedia.org/wiki/Rainbow_table">https://fr.wikipedia.org/wiki/Rainbow_table</a></li> <li>Attaque par dictionnaire : <a href="https://fr.wikipedia.org/wiki/Attaque_par_dictionnaire">https://fr.wikipedia.org/wiki/Attaque_par_dictionnaire</a></li> <li>Attaque par force brute : <a href="https://fr.wikipedia.org/wiki/Attaque_par_force_brute">https://fr.wikipedia.org/wiki/Attaque_par_force_brute</a></li> <li>Le salage en cryptographie : <a href="https://fr.wikipedia.org/wiki/Salage_%28cryptographie%29">https://fr.wikipedia.org/wiki/Salage_%28cryptographie%29</a></li> <li>Algorithme Blowfish : <a href="https://fr.wikipedia.org/wiki/Blowfish">https://fr.wikipedia.org/wiki/Blowfish</a></li> <li>La fin de SHA-1 : <a href="http://www.silicon.fr/sha-1-algorithme-clef-chiffrement-https-plus-securise-129087.html">http://www.silicon.fr/sha-1-algorithme-clef-chiffrement-https-plus-securise-129087.html</a></li> <li>Sites qui vont vous faire oublier le MD5 et le SHA1 : <a href="https://md5hashing.net">https://md5hashing.net</a> - <a href="http://hashtoolkit.com">http://hashtoolkit.com</a> </li> <li>Mots de passe les plus utilisés en 2014 : <a href="http://www.sudouest.fr/2015/01/21/internet-le-top-25-des-mots-de-passe-que-vous-devriez-eviter-1804735-5166.php">http://www.sudouest.fr/2015/01/21/internet-le-top-25-des-mots-de-passe-que-vous-devriez-eviter-1804735-5166.php</a></li> </ul>; 2015-11-30 17:45:27 Créer une librairie "Database" https://etienner.fr/creer-une-librairie-database <p>On va concevoir une librairie en POO (Programmation Orienté Objet) pour pouvoir se connecter à la base de données avec PDO (PHP Data Objects). En plus de concevoir cette librairie, on va également créer un modèle contenant des requêtes SQL de type CRUD (Create Read Update Delete). Vous pourrez par la suite, réutiliser cette librairie dans un &quot;microframework&quot; PHP tel que Slim, Silex ou Lumen et ainsi profiter du modèle MVC.</p> <h2>Préparation</h2> <p>Pour faire ce tutoriel, il nous faut pour commencer, une base de donnée MySQL / MariaDB :</p> <pre><code class="language-sql">CREATE DATABASE IF NOT EXISTS test; CREATE TABLE IF NOT EXISTS `users` ( `id` int(11) NOT NULL AUTO_INCREMENT, `name` varchar(255) COLLATE utf8_unicode_ci NOT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci; INSERT INTO `users` (`id`, `name`) VALUES (1, 'Macon'), (2, 'Kalia'), (3, 'Damian'), (4, 'Nash'), (5, 'Keefe'), (6, 'Octavius'), (7, 'Wendy'), (8, 'Maggy'), (9, 'Walter'), (10, 'Cherokee');</code></pre> <h2>Classe &quot;Database&quot;</h2> <p>Créez un fichier &quot;Database.php&quot; dans le dossier &quot;config&quot; :</p> <pre><code class="language-php">&lt;?php // config/Database.php class Database { // Code à venir }</code></pre> <p>Cette classe va contenir la connexion à PDO puis différents types de requêtes SQL possibles en PDO.</p> <h3>Connexion à PDO</h3> <p>Pour se connecter à notre serveur via PDO, on a besoin des 5 informations obligatoires ci-dessous :</p> <ul> <li>le nom de la base</li> <li>le nom utilisateur (&quot;root&quot; par défaut)</li> <li>le mot de passe</li> <li>l'adresse du serveur</li> <li>le port du serveur (&quot;3306&quot; par défaut)</li> </ul> <p>On créé donc ces 5 variables instanciées dans le constructeur de notre classe :</p> <pre><code class="language-php">&lt;?php // config/Database.php class Database { private $pdo = null; public function __construct() { $this-&gt;db_name = "test"; $this-&gt;db_user = "root"; $this-&gt;db_pass = ""; $this-&gt;db_host = "localhost"; $this-&gt;db_port = 3306; } // Autres fonctions à venir }</code></pre> <p>Maintenant que l'on a ces données, on va pouvoir créer notre fonction de connexion PDO, que l'on nomme <code>getPDO()</code> :</p> <pre><code class="language-php">&lt;?php // config/Database.php class Database { private $pdo = null; public function __construct() { $this-&gt;db_name = "test"; $this-&gt;db_user = "root"; $this-&gt;db_pass = ""; $this-&gt;db_host = "localhost"; $this-&gt;db_port = 3306; } // Connexion à la BDD private function getPDO() { if ($this-&gt;pdo === null) { try { // DSN $pdo = new PDO("mysql:dbname=" . $this-&gt;db_name . ";host=" . $this-&gt;db_host . ";port=". $this-&gt;db_port, $this-&gt;db_user, $this-&gt;db_pass); $pdo-&gt;setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); $pdo-&gt;exec("SET CHARACTER SET utf8"); $this-&gt;pdo = $pdo; } catch (PDOException $e) { echo 'Pas de connexion avec la BDD : ' . $e-&gt;getMessage(); die(); } } return $this-&gt;pdo; } }</code></pre> <p>Avec <code>PDO::setAttribute</code>, on configure 2 attributs PDO :</p> <ul> <li><code>PDO::ATTR_ERRMODE</code> : affiche les rapports d'erreur</li> <li><code>PDO::ERRMODE_EXCEPTION</code> : émet une exception pour les erreurs</li> </ul> <p>Désormais, la connexion établie, on va pouvoir gérer 3 types de requêtes SQL dans 3 fonctions distinctes :</p> <ul> <li>Simple (&quot;query&quot;)</li> <li>Préparéé (&quot;prepare&quot;)</li> <li>Une seule ligne (&quot;row&quot;)</li> </ul> <h3>Requête simple</h3> <pre><code class="language-php">// Requête simple public function query($statement) { $req = $this-&gt;getPDO()-&gt;query($statement); $data = $req-&gt;fetchAll(PDO::FETCH_OBJ); return $data; }</code></pre> <p>Avec <code>PDOStatement::fetchAll</code>, on renvoi tous les résultat sous la forme d'un tableau d'objet de type &quot;stdClass&quot; avec les noms de propriétés qui correspondent aux noms des colonnes retournés dans le jeu de résultats via <code>PDO::FETCH_OBJ</code>. Exemple avec notre table :</p> <pre><code class="language-php">Array ( [0] =&gt; stdClass Object ( [id] =&gt; 1 [name] =&gt; Macon ) [1] =&gt; stdClass Object ( [id] =&gt; 2 [name] =&gt; Kalia ) [2] =&gt; stdClass Object ( [id] =&gt; 3 [name] =&gt; Damian ) [3] =&gt; stdClass Object ( [id] =&gt; 4 [name] =&gt; Nash ) [4] =&gt; stdClass Object ( [id] =&gt; 5 [name] =&gt; Keefe ) [5] =&gt; stdClass Object ( [id] =&gt; 6 [name] =&gt; Octavius ) [6] =&gt; stdClass Object ( [id] =&gt; 7 [name] =&gt; Wendy ) [7] =&gt; stdClass Object ( [id] =&gt; 8 [name] =&gt; Maggy ) [8] =&gt; stdClass Object ( [id] =&gt; 9 [name] =&gt; Walter ) [9] =&gt; stdClass Object ( [id] =&gt; 10 [name] =&gt; Cherokee ) ) </code></pre> <p>Si vous voulez un tableau de type &quot;array&quot;, remplacez <code>PDO::FETCH_OBJ</code> par <code>PDO::FETCH_ASSOC</code>. Ce qui donne avec nos données :</p> <pre><code class="language-php">Array ( [0] =&gt; Array ( [id] =&gt; 1 [name] =&gt; Macon ) [1] =&gt; Array ( [id] =&gt; 2 [name] =&gt; Kalia ) [2] =&gt; Array ( [id] =&gt; 3 [name] =&gt; Damian ) [3] =&gt; Array ( [id] =&gt; 4 [name] =&gt; Nash ) [4] =&gt; Array ( [id] =&gt; 5 [name] =&gt; Keefe ) [5] =&gt; Array ( [id] =&gt; 6 [name] =&gt; Octavius ) [6] =&gt; Array ( [id] =&gt; 7 [name] =&gt; Wendy ) [7] =&gt; Array ( [id] =&gt; 8 [name] =&gt; Maggy ) [8] =&gt; Array ( [id] =&gt; 9 [name] =&gt; Walter ) [9] =&gt; Array ( [id] =&gt; 10 [name] =&gt; Cherokee ) ) </code></pre> <h3>Requête préparée</h3> <pre><code class="language-php">// Requête préparée public function prepare($statement, $attributes = array()) { $query = explode(" ", $statement); // Récupération du 1èr mot $option = strtolower(array_shift($query)); $req = $this-&gt;getPDO()-&gt;prepare($statement); $req-&gt;execute($attributes); if ($option == "select" || $option == "show") { if ($req-&gt;rowCount() &gt; 0) { $data = $req-&gt;fetchAll(PDO::FETCH_CLASS); return $data; } } elseif ($option == "insert" || $option == "update" || $option == "delete") { if ($option == "insert") { // Valeur id inséré return $this-&gt;getPDO()-&gt;lastInsertId(); } else { return $req-&gt;rowCount(); } } }</code></pre> <p>Si c'est une requête de type <code>SELECT</code> ou <code>SHOW</code>, on affiche tous les objets. Si elle est du type <code>INSERT</code>, on retourne l'id retourné via <code>lastInsertId()</code>. Sinon pour celles du type <code>UPDATE</code> ou <code>DELETE</code>, on affiche le résultat sous formes d'un entier via <code>PDOStatement::rowCount</code> (0 = contenu non modifié, X = nombre de contenu modifié(s)).</p> <h3>Une seule ligne</h3> <pre><code class="language-php">// Une seule ligne public function row($statement, $attributes = array()) { $req = $this-&gt;getPDO()-&gt;prepare($statement); $req-&gt;execute($attributes); $data = $req-&gt;fetch(PDO::FETCH_ASSOC); return $data; }</code></pre> <p>Avec <code>fetch</code>, on retourne une seule ligne sous la forme d'un tableau indexé par le nom de la colonne comme retourné dans le jeu de résultats. Dans notre cas pour la 1ère ligne de notre table :</p> <pre><code class="language-php">Array ( [id] =&gt; 1 [name] =&gt; Macon )</code></pre> <h2>Le modèle</h2> <p>Créez une nouveau fichier &quot;Users.php&quot; dans le dossier &quot;models&quot; dont la classe &quot;Users&quot; est héritée de la classe &quot;Database&quot; :</p> <pre><code class="language-php">&lt;?php // models/Users.php require_once "./config/Database.php"; class Users extends Database { private $table; private $db; public function __construct($table = "users") { $this-&gt;table = $table; $this-&gt;db = new Database(); } // Ici la suite du code }</code></pre> <p>On ajoute à la suite, les 4 requètes CRUD (Create Read Update Delete) génériques.</p> <h3>Sélectionner tous les éléments</h3> <pre><code class="language-php">// Sélectionner tous les éléments public function findAll() { return $this-&gt;db-&gt;query("SELECT id, name FROM $this-&gt;table"); }</code></pre> <p>On utilise &quot;query&quot; car ce n'est pas une requète spécifique.</p> <h3>Sélectionner un élément par son id</h3> <pre><code class="language-php">// Sélectionner un élément par son id public function find($id = "") { if ($id) { return $this-&gt;db-&gt;row("SELECT id, name FROM $this-&gt;table WHERE id = :id LIMIT 1", array("id" =&gt; $id)); } }</code></pre> <p>On utilise &quot;row&quot; car on attend une seule ligne.</p> <h3>Ajouter un élément</h3> <pre><code class="language-php">// Ajouter un élément public function add($name = "") { if ($name) { return $this-&gt;db-&gt;prepare("INSERT INTO $this-&gt;table (name) VALUES (:name)", array("name" =&gt; $name)); } }</code></pre> <p>On utilise &quot;prepare&quot; car on spécifie la valeur du champ &quot;name&quot;.</p> <h3>Modifier un élément</h3> <pre><code class="language-php">// Modifier un élément public function edit($name = "", $id = "") { if ($name &amp;&amp; $id) { return $this-&gt;db-&gt;prepare("UPDATE $this-&gt;table SET name = :name WHERE id = :id", array("name" =&gt; $name, "id" =&gt; $id)); } }</code></pre> <p>On utilise &quot;prepare&quot; car on spécifie la valeur du champ &quot;name&quot; et de l'id.</p> <h3>Supprimer un élément</h3> <pre><code class="language-php">// Supprimer un élément public function delete($id = "") { if ($id) { return $this-&gt;db-&gt;prepare("DELETE FROM $this-&gt;table WHERE id=:id", array("id" =&gt; $id)); } }</code></pre> <p>On utilise &quot;prepare&quot; car on spécifie la valeur du champ &quot;id&quot;.</p> <h2>Tester</h2> <p>Depuis un fichier &quot;index.php&quot;, on charge le fichier du modèle &quot;Users&quot; avec <code>require_once</code> pour pouvoir l'instancier dans une variable avec <code>new Nom_de_ma_classe()</code>.</p> <pre><code class="language-php">&lt;?php header('Content-Type: text/html; charset=UTF-8'); require_once "models/Users.php"; $users = new Users();</code></pre> <p>De cette façon, on peut exécuter nos requêtes écrites dans notre modèle &quot;Users&quot; :</p> <pre><code class="language-php">&lt;?php header('Content-Type: text/html; charset=UTF-8'); require_once "models/Users.php"; $users = new Users(); print_r($users-&gt;findAll()); // Tous les utilisateurs print_r($users-&gt;find(1)); // Un seul utilisateur, celui avec l'id 1 //print_r($users-&gt;add("toto")); // Ajoute l'utilisateur "toto" et affiche l'id correspondant à cette nouvelle entrée //print_r($users-&gt;edit("aa", 1)); // Modifie l'utilisateur avec l'id 1 (et affiche 1 pour signaler une vraie modification) //print_r($users-&gt;delete(1)); // Supprime l'utilsateur avec l'id 1 (et affiche 1)</code></pre> <p>Pour afficher les &quot;stdClass Object&quot; proprement :</p> <pre><code class="language-php">&lt;?php header('Content-Type: text/html; charset=UTF-8'); require_once "models/Users.php"; $users = new Users(); $rows = $users-&gt;findAll(); if (!empty($rows)): ?&gt; &lt;table&gt; &lt;tr&gt; &lt;th&gt;Id&lt;/th&gt; &lt;th&gt;Name&lt;/th&gt; &lt;/tr&gt; &lt;?php foreach ($rows as $user): ?&gt; &lt;tr&gt; &lt;td&gt;&lt;?= $user-&gt;id; ?&gt;&lt;/td&gt; &lt;td&gt;&lt;?= $user-&gt;name; ?&gt;&lt;/td&gt; &lt;/tr&gt; &lt;?php endforeach; ?&gt; &lt;/table&gt; &lt;?php else: ?&gt; &lt;p&gt;Pas d'utilisateurs dans la BDD :(&lt;/p&gt; &lt;?php endif; ?&gt;</code></pre> <h2>Conclusion</h2> <p>En mettant en place une classe Database, on gagne du temps dans le développement et sur la lisibilité du code. En effet, on instancie seulement une fois la connexion à PDO puis on écrit les requêtes dans le modèle concerné.</p>; 2015-11-23 13:31:22 Uploader et lister des fichiers images avec Go https://etienner.fr/uploader-et-lister-des-fichiers-images-avec-go <p>Avec seulement des librairies natives de Go, il est possible de mettre en place un gestionnaire d'upload et une galerie d'images. À cela s'ajoute la lecture des images présentes dans le dossier. On va par la suite mettre en place une limitation de taille pour les fichiers mais aussi restreindre les fichiers qui ne sont pas au format image. En bonus, on mettra en place un système d'informations succès / erreurs ainsi que les dimensions des images.</p> <h2>Structure du projet</h2> <p>On créé un dossier &quot;tmp&quot; (pour les futurs fichiers uploadés) et un autre dossier &quot;views&quot; comportant la vue du formulaire &quot;form_upload&quot; sans oublier le fichier &quot;main.go&quot; à la racine du projet:</p> <pre><code>/tmp /views - form_upload.html - main.go</code></pre> <h2>Un simple serveur web</h2> <p>On met en place un formulaire &quot;multipart&quot; dans l'unique vue de notre projet &quot;form_upload.html&quot;:</p> <pre><code class="language-markup">&lt;!DOCTYPE html&gt; &lt;html&gt; &lt;meta charset="UTF-8" /&gt; &lt;title&gt;Go upload&lt;/title&gt; &lt;body&gt; &lt;form action="upload" method="post" enctype="multipart/form-data"&gt; &lt;input type="file" name="file" /&gt; &lt;input type="submit" value="Upload this file" /&gt; &lt;/form&gt; &lt;/body&gt; &lt;/html&gt;</code></pre> <p>Puis, dans notre fichier &quot;main.go&quot;, on met en place 2 routes:</p> <pre><code class="language-go">package main import ( "html/template" "log" "net/http" ) func main() { port := ":3000" log.Println("Starting Web Server 127.0.0.1" + port) // Nos 2 routes http.HandleFunc("/", indexHandler) http.HandleFunc("/upload", uploadHandler) // Afficher le contenu du dossier "tmp" http.Handle("/tmp/", http.StripPrefix("/tmp/", http.FileServer(http.Dir("tmp")))) err := http.ListenAndServe(port, nil) if err != nil { log.Fatal("ListenAndServe: ", err) } } func indexHandler(w http.ResponseWriter, r *http.Request) { tmpl, err := template.ParseFiles("views/form_upload.html") if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } tmpl.Execute(w, nil) // Affiche le contenu de "views/form_upload.html" } func uploadHandler(w http.ResponseWriter, r *http.Request) { // Récupère les informations de l'input file (name="file") file, fileheader, err := r.FormFile("file") if err != nil { log.Println(err) // Affiche "http: no such file" si pas de fichier } else { log.Println(file) // Le contenu du fichier (en langage "robot") log.Println(fileheader.Filename) // Le nom du fichier log.Println(fileheader.Header) // Données du header } defer file.Close() }</code></pre> <p>Dans la première fonction &quot;indexHandler&quot;, on affiche simplement notre vue.<br /> Ensuite, dans la seconde nommée &quot;uploadHandler&quot;, on récupère les informations du fichier rentrées dans le formulaire d'upload via &quot;r.Formfile&quot;.<br /> La variable &quot;file&quot; contient le contenu du fichier et &quot;filheader&quot; contient les informations du fichier comme son nom (&quot;fileheader.Filename&quot;) et le type de son contenu (&quot;fileheader.Header&quot;).</p> <h2>Upload</h2> <p>Maintenant que l'on a récupéré les informations et le contenu du fichier, dans un second temps, on créé un fichier comportant le nom du fichier vide avec &quot;os.Create&quot; puis dans un troisième temps, on copie le contenu du fichier dans ce fichier avec &quot;io.Copy&quot;, dans la fonction &quot;uploadHandler&quot;:</p> <pre><code class="language-go">package main import ( "html/template" "io" "log" "net/http" "os" ) func uploadHandler(w http.ResponseWriter, r *http.Request) { // Récupération des infos du fichier file, fileheader, err := r.FormFile("file") if err != nil { log.Println(err) } defer file.Close() // Création du fichier vide out, err := os.Create("tmp/" + fileheader.Filename) if err != nil { log.Println(err) } defer out.Close() // Copie du contenu dans le fichier précédement vide _, err = io.Copy(out, file) if err != nil { log.Println(err) } defer out.Close() // Redirection vers la page d'accueil http.Redirect(w, r, "/", http.StatusFound) }</code></pre> <p>Faites le test et regardez dans votre dossier &quot;tmp&quot;.</p> <h3>Limiter la taille d'envoi</h3> <p>Par défaut, la taille n'est pas limitée. Au début de la fonction &quot;uploadHandler&quot;, on ajoute une condition de taille maximale avec &quot;r.ContentLength&quot;. Si le fichier est trop lourd, la fonction &quot;http.MaxBytesReader&quot; bloquera alors le transfert du fichier (d'oû l'intéret de mettre cette condition au début de notre fonction...).</p> <p>La taille de limite est en octet... petit rappel:</p> <ul> <li>1 Kilo-octet (Ko) = 10^3 = 1000 octets</li> <li>1 Méga-octet (Mo) = 10^6 octets = 1 000 000 octets.</li> </ul> <pre><code class="language-go">var maxsize int64 = 2000000 // 2 Mo (2 * 10^6 octets) // Contrôle si taille est supérieur à 2 Mo if r.ContentLength &gt; maxsize { r.Body = http.MaxBytesReader(w, r.Body, maxsize) // Retourne une erreur 413 http.Error(w, "File too large", http.StatusRequestEntityTooLarge) return }</code></pre> <p>Si le fichier est trop lourd, alors le serveur retournera une erreur HTTP 413 (&quot;Request Entity Too Large&quot;) avec l'information: &quot;File too large&quot;.</p> <h3>Fichiers autorisés</h3> <p>Tout comme la limitation de taille, il n'y a pas de restriction par défaut, au niveau du format du fichier envoyé au serveur.<br /> Pour filter, on récupère l'extension du fichier avec la fonction &quot;filepath.Ext&quot; pour ensuite mettre en place une condition dans la fonction &quot;uploadHandler&quot;:</p> <pre><code class="language-go">package main import ( "html/template" "io" "log" "net/http" "os" "path/filepath" // Pour l'extension de fichier ) func uploadHandler(w http.ResponseWriter, r *http.Request) { file, fileheader, err := r.FormFile("file") if err != nil { log.Println(err) http.Redirect(w, r, "/", http.StatusFound) return } defer file.Close() var maxsize int64 = 2000000 if r.ContentLength &gt; maxsize { r.Body = http.MaxBytesReader(w, r.Body, maxsize) http.Error(w, "File too large", http.StatusRequestEntityTooLarge) return } // Récupération de l'extension du fichier extension := filepath.Ext(fileheader.Filename) // Si l'extension correspond à l'un de ces critères if extension == ".gif" || extension == ".jpg" || extension == ".png" { out, err := os.Create("tmp/" + fileheader.Filename) if err != nil { log.Println(err) } defer out.Close() _, err = io.Copy(out, file) if err != nil { log.Println(err) } defer out.Close() } else { http.Error(w, "Bad file format", http.StatusRequestEntityTooLarge) return } http.Redirect(w, r, "/", http.StatusFound) }</code></pre> <p>Si le fichier n'est pas au bon format, alors le serveur retournera une erreur HTTP 413 (&quot;Request Entity Too Large&quot;) avec l'information: &quot;Bad file format&quot;.</p> <h2>Lister les fichiers</h2> <p>Pour lister les fichiers uploadés, on va utiliser la fonction &quot;ioutil.ReadDir&quot;.<br /> Avant de continuer, on va se focaliser quelques instant sur ce que retourne cette fonction. En effet, dans le documentation officielle, elle est construire de la façon suivante:</p> <p><code>func ReadDir(dirname string) ([]os.FileInfo, error)</code></p> <p>Ce qui donne lieu à ce genre de code:</p> <pre><code class="language-go">entries, err := ioutil.ReadDir("tmp/") if err != nil { log.Println(err) } for _, entry := range entries { log.Println(entry.Name()) // Nom du fichier ("myphoto.jpg") log.Println(entry.Size()) // Taille en octet (/1024 = Ko) log.Println(entry.Mode()) // Droits d'écritures "-rw-rw-rw-" log.Println(entry.ModTime()) // Date de dernière modification log.Println(entry.IsDir()) // "false" par défaut (car on ne liste pas des "directories" / répertoires) }</code></pre> <p>Les informations sont lisibles sur une structure particulière de type &quot;FileInfo&quot; (<a href="http://golang.org/pkg/os/#FileInfo">http://golang.org/pkg/os/#FileInfo</a>).<br /> Pour exploiter ces informations, on va donc mettre en place une structure &quot;FileInfo&quot; et éditer notre route concernée, &quot;indexHandler&quot;:</p> <pre><code class="language-go">package main import ( "html/template" "io" "io/ioutil" "log" "net/http" "os" "path/filepath" "time" ) type FileInfo struct { Name string Size int64 Updated time.Time } func indexHandler(w http.ResponseWriter, r *http.Request) { tmpl, err := template.ParseFiles("views/form_upload.html") if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } // Récupération des fichiers présents dans le dossier entries, err := ioutil.ReadDir("tmp/") if err == nil { // Création d'un tableau basé sur la structure "FileInfo" files := []FileInfo{} // Boucle sur chaque fichier for _, entry := range entries { // Récupération des données f := FileInfo{ Name: entry.Name(), Size: entry.Size(), Updated: entry.ModTime(), } // Insertion des données dans le tableau files = append(files, f) } // Données pour le front data := map[string]interface{}{ "Files": files, } tmpl.Execute(w, data) } else { // Le dossier "tmp/" n'existe pas http.Error(w, err.Error(), http.StatusInternalServerError) } }</code></pre> <p>Et, on affiche ces nouvelles données dans notre unique fichier HTML dans une boucle &quot;range&quot;:</p> <pre><code class="language-markup">&lt;!DOCTYPE html&gt; &lt;html&gt; &lt;meta charset="UTF-8" /&gt; &lt;title&gt;Go upload&lt;/title&gt; &lt;link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css"&gt; &lt;body class="container"&gt; &lt;h1&gt;Upload file with Go&lt;/h1&gt; &lt;form action="upload" method="post" enctype="multipart/form-data"&gt; &lt;div class="form-group"&gt; &lt;input type="file" name="file" /&gt; &lt;button type="submit" class="btn btn-default"&gt;Submit&lt;/button&gt; &lt;/div&gt; &lt;/form&gt; {{ if .Files }} &lt;table class="table"&gt; &lt;tr&gt; &lt;th&gt;Name&lt;/th&gt; &lt;th&gt;Size (octets)&lt;/th&gt; &lt;th&gt;Last update&lt;/th&gt; &lt;/tr&gt; {{ range $FileInfo := .Files }} &lt;tr&gt; &lt;td&gt;&lt;a href="tmp/{{ $FileInfo.Name }}" target="_blank"&gt;{{ $FileInfo.Name }}&lt;/a&gt;&lt;/td&gt; &lt;td&gt;{{ $FileInfo.Size }}&lt;/td&gt; &lt;td&gt;{{ $FileInfo.Updated }}&lt;/td&gt; &lt;/tr&gt; {{ end }} &lt;/table&gt; {{ else }} &lt;p&gt;No files&lt;/p&gt; {{ end }} &lt;/body&gt; &lt;/html&gt;</code></pre> <p>Remarque: l'affichage se fait automatiquement par ordre alphabétique (nom du fichier).</p> <h3>Supprimer un fichier</h3> <p>On récupère le nom du fichier dans l'URL de type GET avec la fonction &quot;r.URL.Query().Get()&quot; puis on le supprime avec la fonction &quot;os.Remove&quot;:</p> <pre><code class="language-go">package main import ( "html/template" "io" "io/ioutil" "log" "net/http" "os" "path/filepath" "time" ) func indexHandler(w http.ResponseWriter, r *http.Request) { file := r.URL.Query().Get("file") // Si le fichier est bien renseigné dans l'URL if file != "" { // Suppression du fichier err := os.Remove("tmp/" + file) if err != nil { log.Println(err) return } // Redirection vers la page d'accueil http.Redirect(w, r, "/", http.StatusFound) } tmpl, err := template.ParseFiles("views/form_upload.html") if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } entries, err := ioutil.ReadDir("tmp/") if err == nil { files := []FileInfo{} for _, entry := range entries { f := FileInfo{ Name: entry.Name(), Size: entry.Size(), Updated: entry.ModTime(), } files = append(files, f) } data := map[string]interface{}{ "Files": files, } tmpl.Execute(w, data) } else { http.Error(w, err.Error(), http.StatusInternalServerError) } }</code></pre> <p>On édite le tableau de notre fichier HTML afin de récupérer le nom du fichier:</p> <pre><code class="language-markup">&lt;!DOCTYPE html&gt; &lt;html&gt; &lt;meta charset="UTF-8" /&gt; &lt;title&gt;Go upload&lt;/title&gt; &lt;link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css"&gt; &lt;body class="container"&gt; &lt;h1&gt;Upload file with Go&lt;/h1&gt; &lt;form action="upload" method="post" enctype="multipart/form-data"&gt; &lt;div class="form-group"&gt; &lt;input type="file" name="file" /&gt; &lt;button type="submit" class="btn btn-default"&gt;Submit&lt;/button&gt; &lt;/div&gt; &lt;/form&gt; {{ if .Files }} &lt;table class="table"&gt; &lt;tr&gt; &lt;th&gt;Name&lt;/th&gt; &lt;th&gt;Size (octets)&lt;/th&gt; &lt;th&gt;Last update&lt;/th&gt; &lt;th&gt;Delete&lt;/th&gt; &lt;/tr&gt; {{ range $FileInfo := .Files }} &lt;tr&gt; &lt;td&gt; &lt;a href="tmp/{{ $FileInfo.Name }}" target="_blank"&gt; &lt;img src="tmp/{{ $FileInfo.Name }}" height="120" alt="" /&gt; &lt;/a&gt; &lt;/td&gt; &lt;td&gt;{{ $FileInfo.Size }}&lt;/td&gt; &lt;td&gt;{{ $FileInfo.Updated }}&lt;/td&gt; &lt;td&gt;&lt;a href="/delete?file={{ $FileInfo.Name }}"&gt;Delete&lt;/a&gt;&lt;/td&gt; &lt;/tr&gt; {{ end }} &lt;/table&gt; {{ else }} &lt;p&gt;No files&lt;/p&gt; {{ end }} &lt;/body&gt; &lt;/html&gt;</code></pre> <h2>Bonus : messages d'informations et dimensions des images</h2> <p>On crée une fonction &quot;setFlash&quot; qui contiendra le message de succès ou d'erreur d'une action dans un cookie temporaire. Quant aux dimensions des images présentes dans le dossier &quot;tmp&quot;, on utilise la fonction &quot;os.Open&quot; pour ouvrir le fichier concerné puis &quot;image.DecodeConfig&quot; pour récupérer la largeur et la hauteur de l'image.</p> <pre><code class="language-go">package main import ( "html/template" "image" "image/gif" "image/jpeg" "image/png" "io" "io/ioutil" "log" "math" "net/http" "os" "path/filepath" "time" ) type FileInfo struct { Name string Size float64 Updated time.Time Width, Height int } func init() { // Appel des dépendances "image/gif", "image/jpeg" and "image/png" image.RegisterFormat("jpeg", "jpeg", jpeg.Decode, jpeg.DecodeConfig) image.RegisterFormat("gif", "gif", gif.Decode, gif.DecodeConfig) image.RegisterFormat("png", "png", png.Decode, png.DecodeConfig) } func main() { port := ":3000" log.Println("Starting Web Server 127.0.0.1" + port) http.HandleFunc("/", indexHandler) http.HandleFunc("/upload", uploadHandler) http.Handle("/tmp/", http.StripPrefix("/tmp/", http.FileServer(http.Dir("tmp")))) err := http.ListenAndServe(port, nil) if err != nil { log.Fatal("ListenAndServe: ", err) } } func indexHandler(w http.ResponseWriter, r *http.Request) { fileFormUrlGet := r.URL.Query().Get("file") if fileFormUrlGet != "" { err := os.Remove("tmp/" + fileFormUrlGet) if err != nil { log.Println(err) setFlash(w, "warning", "The file '"+fileFormUrlGet+"' can't be remove") http.Redirect(w, r, "/", http.StatusFound) return } setFlash(w, "success", "The file '"+fileFormUrlGet+"' has been remove successul") http.Redirect(w, r, "/", http.StatusFound) return } tmpl, err := template.ParseFiles("views/form_upload.html") if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } entries, err := ioutil.ReadDir("tmp/") if err == nil { files := []FileInfo{} for _, entry := range entries { // On ouvre le fichier dimensions, err := os.Open("tmp/" + entry.Name()) if err != nil { log.Println(err) } // Afin d'obtenir les dimensions imgConf, _, err := image.DecodeConfig(dimensions) if err != nil { log.Println(err) } // On modifie le tableau des informations pour y ajouter taille et largeur de l'image f := FileInfo{ Name: entry.Name(), Size: math.Ceil(float64(entry.Size()) / 1024), Updated: entry.ModTime(), Width: imgConf.Width, Height: imgConf.Height, } // On pense à bien fermer dimensions.Close() // Sinon erreur avec "os.Remove" files = append(files, f) } // Récupération des données des cookies "success" et "warning" cookieSuccess, a := r.Cookie("success") cookieWarning, b := r.Cookie("warning") success := "" warning := "" if a == nil { success = cookieSuccess.Value } if b == nil { warning = cookieWarning.Value } data := map[string]interface{}{ "Files": files, "Success": success, "Warning": warning, } tmpl.Execute(w, data) } else { http.Error(w, err.Error(), http.StatusInternalServerError) } } func uploadHandler(w http.ResponseWriter, r *http.Request) { file, fileheader, err := r.FormFile("file") if err != nil { log.Println(err) setFlash(w, "warning", "No file to upload") http.Redirect(w, r, "/", http.StatusFound) return } defer file.Close() var maxsize int64 = 2000000 if r.ContentLength &gt; maxsize { setFlash(w, "warning", "The file '"+fileheader.Filename+"' can't be uploaded: too heavy size") http.Redirect(w, r, "/", http.StatusFound) return } extension := filepath.Ext(fileheader.Filename) if extension == ".gif" || extension == ".jpg" || extension == ".png" { out, err := os.Create("tmp/" + fileheader.Filename) if err != nil { log.Println(err) } defer out.Close() _, err = io.Copy(out, file) if err != nil { log.Println(err) } defer out.Close() setFlash(w, "success", "The file '"+fileheader.Filename+"' has been upload successul") } else { setFlash(w, "warning", "Extension "+extension+" not accepted") } http.Redirect(w, r, "/", http.StatusFound) } // Fonction pour créer un cookie à durée limitée func setFlash(w http.ResponseWriter, name string, value string) { cookie := &amp;http.Cookie{Name: name, Value: value, Path: "/", MaxAge: 1} http.SetCookie(w, cookie) } </code></pre> <p>On affiche le message retourné par les cookies.</p> <pre><code class="language-markup">&lt;!DOCTYPE html&gt; &lt;html&gt; &lt;meta charset="UTF-8" /&gt; &lt;title&gt;Go upload&lt;/title&gt; &lt;link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css"&gt; &lt;body class="container"&gt; &lt;h1&gt;Upload file with Go&lt;/h1&gt; &lt;form action="upload" method="post" enctype="multipart/form-data"&gt; &lt;div class="form-group"&gt; &lt;input type="file" name="file" /&gt; &lt;button type="submit" class="btn btn-default"&gt;Submit&lt;/button&gt; &lt;/div&gt; &lt;/form&gt; {{ if .Success }} &lt;div class="alert alert-success" role="alert"&gt; {{ .Success }} &lt;button type="button" class="close" data-dismiss="alert" aria-label="Close"&gt; &lt;span aria-hidden="true"&gt;&amp;times;&lt;/span&gt; &lt;/button&gt; &lt;/div&gt; {{ end }} {{ if .Warning }} &lt;div class="alert alert-warning" role="alert"&gt; {{ .Warning }} &lt;button type="button" class="close" data-dismiss="alert" aria-label="Close"&gt; &lt;span aria-hidden="true"&gt;&amp;times;&lt;/span&gt; &lt;/button&gt; &lt;/div&gt; {{ end }} {{ if .Files }} &lt;table class="table"&gt; &lt;tr&gt; &lt;th&gt;Preview&lt;/th&gt; &lt;th&gt;Name&lt;/th&gt; &lt;th&gt;Size (ko)&lt;/th&gt; &lt;th&gt;Last update&lt;/th&gt; &lt;th&gt;Width&lt;/th&gt; &lt;th&gt;Height&lt;/th&gt; &lt;th&gt;Delete&lt;/th&gt; &lt;/tr&gt; {{ range $FileInfo := .Files }} &lt;tr&gt; &lt;td&gt; &lt;a href="tmp/{{ $FileInfo.Name }}" target="_blank"&gt; &lt;img src="tmp/{{ $FileInfo.Name }}" height="120" alt="" /&gt; &lt;/a&gt; &lt;/td&gt; &lt;td&gt;{{ $FileInfo.Name }}&lt;/td&gt; &lt;td&gt;{{ $FileInfo.Size }}&lt;/td&gt; &lt;td&gt;{{ $FileInfo.Updated }}&lt;/td&gt; &lt;td&gt;{{ $FileInfo.Width }}px&lt;/td&gt; &lt;td&gt;{{ $FileInfo.Height }}px&lt;/td&gt; &lt;td&gt;&lt;a href="?file={{ $FileInfo.Name }}"&gt;Delete&lt;/a&gt;&lt;/td&gt; &lt;/tr&gt; {{ end }} &lt;/table&gt; {{ else }} &lt;p&gt;No files&lt;/p&gt; {{ end }} &lt;script src="http://code.jquery.com/jquery-2.1.4.min.js"&gt;&lt;/script&gt; &lt;script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/js/bootstrap.min.js"&gt;&lt;/script&gt; &lt;/body&gt; &lt;/html&gt;</code></pre> <h2>Conclusion</h2> <p>On a désormais un système d'upload d'images fonctionnel sans librairie externe. Pour aller plus loin, on pourrait imaginer un système de pagination pour alléger le chargement de la page, générer une miniature de l'image, etc...</p>; 2015-09-10 16:02:52 Développer sur Firefox OS https://etienner.fr/developper-sur-firefox-os <p>Sur le marché du mobile un acteur du web à fait son apparition depuis quelques temps, la fondation Mozilla.Elle développe un système d'exploitation libre pour le mobile, Firefox OS. Développé par la fondation Mozilla est un système d'exploitation mobile libre. Destiné dans un premier temps au marché des mobiles low-cost, son moteur de rendu Gecko permet de faire fonctionner des applications web au format HTML5.</p> <p><img src="../assets/img/news/firefox_os/firefox_os_logo.gif" alt="" /></p> <h2>Installer Firefox Developper Edition</h2> <p>Pour travailler sur notre future application, il faut installer Firefox Developper Edition disponible à l'adresse suivante : <a href="https://www.mozilla.org/fr/firefox/developer/">https://www.mozilla.org/fr/firefox/developer/</a></p> <p><img src="../assets/img/news/firefox_os/firefox_os_firefox_developper_edition.png" alt="" /></p> <p>Remarque : cette article a été rédigé sur la version 40.0a2 de Firefox Developper Edition. </p> <h2>Premier aperçu</h2> <p>Cliquez sur l'onglet de Firefox WebIDE.<br /> Une fois l'éditeur ouvert, cliquez sur &quot;Project&quot;, &quot;New App&quot;. Sélectionnez la première &quot;HelloWorld&quot; puis saisissez un nom de projet dans &quot;Projet Name&quot; et validez. Indiquez un dossier sur votre disque dur où l'application sera générée.</p> <p><img src="../assets/img/news/firefox_os/firefox_os_nouveau_projet.jpg" alt="" /></p> <p>Une fois votre application générée, vous pouvez ouvrir les fichiers à partir de l'éditeur de Firefox ou bien depuis votre éditeur favoris. Commencons d'abord par le fichier &quot;manifest.webapp&quot; qui est écrit en Json :</p> <pre><code class="language-javascript">{ "name": "myApp", "description": "A Hello World app", "launch_path": "/index.html", "icons": { "16": "/icons/icon16x16.png", "48": "/icons/icon48x48.png", "60": "/icons/icon60x60.png", "128": "/icons/icon128x128.png" }, "type": "privileged", "permissions": {} }</code></pre> <p>Ce fichier n'est autre que le fichier de manifeste d'application. Il va servir à afficher les informations sur le marketplace de Firefox. C'est pour cela qu'il contient toutes les données relatives à notre application comme le nom (&quot;name&quot;), la description, le fichier de base (&quot;launch_path&quot;) et les icones de différentes tailles au format PNG présents dans le dossier &quot;icons&quot;. Quant aux deux dernières options, elle sont facultatives.</p> <p>Ensuite, on passe au fichier &quot;index.html&quot; :</p> <pre><code class="language-markup">&lt;!DOCTYPE html&gt; &lt;html&gt; &lt;head&gt; &lt;meta charset="utf-8"&gt; &lt;meta name="viewport" content="width=device-width,user-scalable=no,initial-scale=1"&gt; &lt;title&gt;Hello World&lt;/title&gt; &lt;style&gt; body { border: 1px solid black; } &lt;/style&gt; &lt;!-- Inline scripts are forbidden in Firefox OS apps (CSP restrictions), so we use a script file. --&gt; &lt;script src="app.js" defer&gt;&lt;/script&gt; &lt;/head&gt; &lt;body&gt; &lt;!-- This code is in the public domain. Enjoy! --&gt; &lt;h1&gt;Hello World&lt;/h1&gt; &lt;/body&gt; &lt;/html&gt;</code></pre> <p>C'est une page HTML qui nous affiche &quot;Hello World&quot;. Elle charge également un fichier Javascript &quot;app.js&quot; :</p> <pre><code class="language-javascript">window.addEventListener("load", function() { console.log("Hello World!"); });</code></pre> <p>Fini le tour du proprio, lancons le simulateur ! </p> <p>Toujours dans l'éditeur WebIDE, en haut à droite, cliquez sur &quot;Selectionnez l'environnement&quot; et sélectionnez un des simulateurs présent dans &quot;Simulator&quot;.</p> <p><img src="../assets/img/news/firefox_os/firefox_os_selection_simulateur.jpg" alt="" /></p> <p>Le simulateur est lancé mais l'application n'apparait pas et c'est normal. Cliquez sur l'icone play, au milieu dans l'éditeur.</p> <p><img src="../assets/img/news/firefox_os/firefox_os_simulateur.jpg" alt="" /></p> <p>Et le &quot;console.log&quot; dans tout ça ?<br /> Sur WebIDE, cliquez sur l'icone réprésentant une clef à molette (ou F12) puis sur l'onglet &quot;Console&quot;. Le message &quot;Hello World !&quot; apparait. Vous pouvez ainsi avec cet outil débugger votre application comme pour un site web.</p> <p><img src="../assets/img/news/firefox_os/firefox_os_console.jpg" alt="" /></p> <p>Remarque : lorsque vous modifiez un fichier et que vous souhaitez voir le changement affecté par ce dernier, cliquez sur le logo de rechargement ou faites un CTRL-R depuis WebIDE pour recharger l'application sur le simulateur.</p> <p>Il est possible de changer les informations de l'application suivant la langue du téléphone.<br /> Dans votre fichier de manifeste :</p> <pre><code class="language-javascript">{ "name": "Rouge", "description": "Une application Hello World", "launch_path": "/index.html", "icons": { "16": "/icons/icon16x16.png", "48": "/icons/icon48x48.png", "60": "/icons/icon60x60.png", "128": "/icons/icon128x128.png" }, "type": "privileged", "permissions": {}, "locales": { "en": { "name": "Red", "description": "An Hello World app" }, "es": { "name": "Roja", "description": "Una aplicación Hello World" } }, "default_locale": "fr" }</code></pre> <p>Pour tester sur Firefox OS, il faut changer la langue du système. Allez dans &quot;Paramètres&quot;, &quot;Langues&quot; et sélectionnez une des langues présente dans le manifeste.</p> <h2>Ionic</h2> <p>Vous vous êtes déja formé sur Ionic? Ça tombe bien car le framework carburant à Angular fonctionne également sur Firefox OS :)<br /> Vous pouvez faire le test en lancant les commandes ci-dessous dans un terminal :</p> <ul> <li><code>npm install -g cordova ionic</code></li> <li><code>ionic start myIonicApp tabs</code></li> <li><code>cd myIonicApp</code></li> <li><code>ionic platform add firefoxos</code></li> <li><code>ionic build firefoxos</code></li> </ul> <p>Lors de la commande de construction de l'application, Ionic a créé le dossier &quot;firefoxos&quot; dans &quot;platforms&quot;. Dans le dossier &quot;myIonicApp\platforms\firefoxos\www&quot; se trouve le fichier de manifeste ainsi que l'application compilée pour Firefox OS.</p> <p>Dans WebIDE, ajoutez le chemin de votre application. Pour cela, cliquez sur &quot;Ouvrir une application&quot; puis sur &quot;Ouvrir une application empaquetée&quot; et indiquez le répertoire de l'application (celui cité ci-dessus).</p> <p><img src="../assets/img/news/firefox_os/firefox_os_ionic.jpg" alt="" /></p> <h2>Conclusion</h2> <p>Développer une application mobile sur cette OS mobile est un jeu d'enfant pour développeur web. Et ce, malgré l'absence à l'heure actuelle de framework CSS pour la touche graphique propre à Firefox OS. De son coté, la fondation Mozilla recommande chaudement d'utiliser les frameworks front Javascript tels que Backbone, Ember et Angular. Mais rien ne vous empèche d'utiliser les API propres (ou non) à Firefox (Battery Status API, Geolocation API, Vibration API, etc...)</p> <h2>Aller plus loin</h2> <ul> <li>Plus de détails sur le fichier de manifeste d'application : <a href="https://developer.mozilla.org/fr/Apps/Manifeste">https://developer.mozilla.org/fr/Apps/Manifeste</a></li> <li>Documentation sur les web API :<a href="https://developer.mozilla.org/en-US/docs/WebAPI">https://developer.mozilla.org/en-US/docs/WebAPI</a></li> <li>Guide du webdesigner : <a href="https://www.mozilla.org/en-US/styleguide/products/firefox-os">https://www.mozilla.org/en-US/styleguide/products/firefox-os</a></li> <li>Le marketplace officiel : <a href="https://marketplace.firefox.com">https://marketplace.firefox.com</a></li> <li>Ionic : <a href="http://ionicframework.com/getting-started">http://ionicframework.com/getting-started</a></li> </ul>; 2015-07-06 09:00:00 Débuter sur Docker https://etienner.fr/debuter-sur-docker <p>Docker permet de virtualiser des images dans lesquelles on peut créer plusieurs containers. Docker étant natif sous Linux, pour l'utiliser sous Windows ou Mac OS X il est nécessaire d'installer boot2docker qui est une VM tournant dans le logiciel de virtualisation VirtualBox. Cette VM est une interface Linux très... dépouillée. Dans cet article, on va créer un environnement de travail pour développeur Web avec un stack Nginx, MariaDB et MongoDB.</p> <p><img src="../assets/img/news/docker/docker_logo.png" alt="" /></p> <h2>Installation de boot2docker</h2> <p>Avant de commencer, il faut VirtualBox d'installé impérativement sur votre machine <a href="https://www.virtualbox.org/wiki/Downloads">https://www.virtualbox.org/wiki/Downloads</a>.<br /> Téléchargez et installez boot2docker <a href="http://boot2docker.io">http://boot2docker.io</a>.<br /> Sur votre invite de commande, tapez :<br /> <code>boot2docker start</code><br /> Puis pour se connecter à distance via SSH :<br /> <code>boot2docker ssh</code></p> <p><img src="../assets/img/news/docker/docker_boot2docker_start_ssh.jpg" alt="" /></p> <!-- Il est possible de configurer un accès SSH avec Putty. Avec puttygen, cliquez sur "File", "Load Private Key" et sélectionnez le dossier "C:\Users\Votre_session\.ssh". A droite de "Nom du fichier", sélectionnez "Tous les fichiers" puis sélectionnez le fichier "id_boot2docker". Cliquez sur "Save private key" puis sur Generate". Je l'ai appellé "boot2docker_ssh". Lancez Putty, allez dans "Connection", "SSH", "Auth" puis cliquez sur "Browse" et sélectionnez la clef "boot2docker_ssh.ppk". Ensuite, allez dans "Session" : * Hostname : 192.168.59.103 * Saved Session : boot2docker (ou un autre nom de votre choix) Cliquez sur "Saved". Remarque : si vous ne mettez pas en place de clef SSH (coté client), les identifiants par défaut sont : * login : docker * password : tcuser --> <h2>Commandes de base</h2> <p>Liste de commandes non exaustives</p> <h3>Boot2docker</h3> <p>Quelques commandes pour gérer la VM :</p> <ul> <li>Afficher toutes les commandes de Docker :<br /> <code>boot2docker</code> </li> <li>Connaitre le status de la VM :<br /> <code>boot2docker status</code> </li> <li>Démarrer la VM :<br /> <code>boot2docker start</code> </li> <li>Arréter la VM :<br /> <code>boot2docker stop</code> </li> <li>Accéder à la VM via SSH :<br /> <code>boot2docker ssh</code></li> <li>Connaitre l'IP de la VM :<br /> <code>boot2docker ip</code> </li> <li>Mettre à jour la VM :<br /> <code>boot2docker download</code></li> </ul> <h2>Docker</h2> <ul> <li>Afficher toutes les commandes de Docker :<br /> <code>docker</code></li> <li>Afficher toutes les images :<br /> <code>docker images</code> </li> <li>Installer une image à partir du dépot officiel de Docker :<br /> <code>docker pull NOM_DE_L'IMAGE</code> </li> <li>Installer une image depuis un fichier Dockerfile :<br /> <code>docker build NOM_IMAGE CHEMIN_VERS_DOCKERFILE</code></li> <li>Supprimer une image :<br /> <code>docker rmi NOM_DE_L'IMAGE</code><br /> <code>docker rmi ID_IMAGE</code> </li> <li>Créer un container à partir d'une image :<br /> <code>docker run OPTIONS NOM_DE_L'IMAGE</code> </li> <li>Démarrer un container :<br /> <code>docker start NOM_DU_CONTAINER</code> </li> <li>Relancer un container :<br /> <code>docker restart NOM_DU_CONTAINER</code> </li> <li>Stopper un container :<br /> <code>docker stop NOM_DU_CONTAINER</code> </li> <li>Supprimer un container (il faut que ce dernier soit obligatoirement arrété) :<br /> <code>docker rm NOM_DU_CONTAINER</code> </li> <li>Renomer un container :<br /> <code>docker rename NOM_DU_CONTAINER NOUVEAU_NOM_DU_CONTAINER</code> </li> <li>Afficher les infos d'un container (cpu, mémoire, etc..) :<br /> <code>docker stats NOM_DU_CONTAINER</code> </li> <li>Exécuter une commande dans un container :<br /> <code>docker exec NOM_DU_CONTAINER</code> </li> <li>Lister les containers actifs :<br /> <code>docker ps</code> </li> <li>Lister tous les containers :<br /> <code>docker ps -a</code></li> </ul> <h2>Volume partagé</h2> <p>Le volume partagé permet à boot2docker d'accéder à votre disque dur physique. Ainsi, il est possible de configurer un serveur Web dont la racine des fichiers pointe directement sur votre machine Windows.<br /> Si vous lancez VirtualBox, il y a notre image &quot;boot2docker-vm&quot;. Faites un clic droit dessus et cliquez sur &quot;Configuration&quot; puis allez dans l'onglet &quot;Dossiers partagés&quot;. Par défaut, dans &quot;dossier permanents&quot;, le chemin &quot;c/Users&quot; pointe vers &quot;C:\Users&quot;. A gauche, se situe le chemin de la machine boot2docker et à droite le chemin de votre Windows.<br /> On peut ainsi, dans Boot2docker, accéder au dossier &quot;C:\Users&quot; en tapant avec <code>cd /c</code> (ou <code>ls /</code> pour lister le contenu).</p> <p><img src="../assets/img/news/docker/docker_virtualbox_volume.jpg" alt="" /></p> <h2>Nginx</h2> <h3>Préparation de l'image</h3> <p>On va désormais rentrer dans le vif du sujet. Dans votre dossier &quot;C:\Users\votre_session&quot;, ouvrez une console qui pointe sur ce dossier. Récupérez le repository <a href="https://github.com/fideloper/docker-nginx-php">https://github.com/fideloper/docker-nginx-php</a> avec la commande :<br /> <code>git clone <a href="https://github.com/fideloper/docker-nginx-php.git">https://github.com/fideloper/docker-nginx-php.git</a></code>.<br /> Dans le dossier &quot;www&quot;, créez un fichier &quot;index.php&quot; :</p> <pre><code class="language-php">&lt;?php phpinfo(); ?&gt;</code></pre> <p>Créez aussi un nouveau dossier &quot;css&quot;.<br /> Retournez sur boot2docker.<br /> Dans boot2docker, on se place dans le dossier qui contient le dockerfile.<br /> <code>cd /c/Users/votre_session/docker-nginx-php</code></p> <h3>Installation de l'image</h3> <p><code>docker build -t webapp .</code><br /> On construit notre image nommée &quot;webapp&quot; et le point correspond au fichier &quot;dockerfile&quot; présent dans le dossier &quot;docker-nginx-php&quot;.</p> <p>Remarque : cette opération prend un certains temps car, Docker récupère l'image à distance puis l'installe sur la VM (voir les commandes présentes dans le Dockerfile).</p> <h3>Création du container</h3> <p>On instancie un nouveau container :<br /> <code>docker run -v /c/Users/votre_session/docker-nginx-php/www:/var/www:rw -p 80:80 --name nginx-dev -d webapp</code></p> <p>Avec cette commande, on :</p> <ol> <li>Monte le volume via la commande <code>-v</code> pointant vers le dossier &quot;www&quot; présent dans le dossier &quot;docker-nginx-php&quot;. </li> <li>Précise le port 80 (des 2 cotés) via la commande <code>-p</code>.</li> <li>Spécifie le nom de notre futur container avec <code>--name</code>.</li> <li>Lance le container en arrière plan via la commande <code>-d</code> (daemon).</li> <li>Indique que le container est créé à partir de l'image &quot;webapp&quot;.</li> </ol> <p>Remarque : si vous oubliez de mettre un nom à votre container, Docker en génère un aléatoirement.</p> <p>Tapez <code>docker ps</code>, vous devriez voir votre container actif.</p> <p>Et vous pouvez accéder à votre serveur Nginx avec l'URL par défaut (que renvoit <code>boot2docker ip</code>) : 192.168.59.103</p> <p><img src="../assets/img/news/docker/docker_nginx_phpfm.jpg" alt="" /></p> <p>Mais par défaut, Nginx renvoit une erreur 403 pour lister le contenu d'un dossier (<a href="http://192.168.59.103/css">http://192.168.59.103/css</a> dans notre cas). Pour ce faire, il faut activer l'option &quot;autoindex on&quot;. On va donc éditer le fichier de configuration de Nginx qui se trouve dans le répertoire &quot;/etc/nginx/sites-enabled/default&quot; de notre container &quot;nginx-dev&quot;.</p> <h3>Edition</h3> <p><code>docker exec -i -t nginx-dev bash</code> ou <code>docker exec -it nginx-dev bash</code></p> <p>Explications :</p> <ul> <li><code>-i</code> capture la saisie.</li> <li><code>-t</code> donne un terminal.</li> <li>&quot;nginx&quot; le nom de notre container concerné.</li> <li><code>bash</code> lance l'interpréteur de commande Bash.</li> </ul> <p>On peut désormais éditer notre fichier de configuration avec Vim (car l'éditeur a été installé lors de la création de l'image, cf. au Dockerfile) :<br /> <code>vim /etc/nginx/sites-enabled/default</code><br /> Placez vous dans &quot;location /&quot; et ajoutez l'option ci-dessous :</p> <pre><code class="language-markup"> autoindex on;</code></pre> <p>Tapez sur votre touche Echap pour sortir du mode édition, puis sauvegardez et quittez en tapant la commande <code>:wq</code>.<br /> <img src="../assets/img/news/docker/docker_nginx_autoindex.jpg" alt="" /><br /> Une fois sorti de l'éditeur, relancez le service Nginx :<br /> <code>service nginx reload</code> </p> <p><a href="http://192.168.59.103/css">http://192.168.59.103/css</a> est désormais accessible dans votre navigateur favori.</p> <p>Par défaut, lorsqu'il y a une erreur PHP quelconque, Nginx renvoit directement une erreur 500. On va toucher à une option dans le fichier de config de PHP :<br /> <code>vim /etc/php5/fpm/php.ini</code><br /> Modifiez la ligne <code>display_errors = Off</code> par <code>display_errors = On</code></p> <p>Astuce : tapez la commande de recherche Vim <code>:/display_errors = Off</code> pour gagner du temps...</p> <p>Enregistrez puis relancez le service de PHP-FPM :<br /> <code>service php5-fpm reload</code></p> <h3>Installation de Xdebug</h3> <p>On veut afficher les erreurs PHP avec Xdebug. On commence par mettre à jour les dépendances :<br /> <code>apt-get update</code> Puis on installe Xdebug pour PHP :<br /> <code>apt-get install php5-xdebug</code><br /> On édite le fichier de configuration : <code>vim /etc/php5/fpm/conf.d/20-xdebug.ini</code></p> <pre><code class="language-markup">zend_extension=/usr/lib/php5/20121212/xdebug.so xdebug.profiler_output_dir=/tmp xdebug.profiler_output_name=cachegrind.out.%p xdebug.profiler_enable_trigger=1 xdebug.profiler_enable=0 xdebug.remote_enable=true xdebug.remote_host=127.0.0.1 xdebug.remote_port=9001 xdebug.remote_handler=dbgp xdebug.remote_autostart=0</code></pre> <p>Ensuite on rédémarre le service propre à PHP FPM : <code>service php5-fpm reload</code> </p> <p><img src="../assets/img/news/docker/docker_nginx_xdebug.jpg" alt="" /></p> <h3>Installation du driver MongoDB</h3> <p>Avant d'installer un serveur MongoDB dans un nouveau container, on installe son driver PHP.<br /> <code>apt-get install php5-mongo</code><br /> On rédémarre pour la dernière fois PHP FPM :<br /> <code>service php5-fpm reload</code> </p> <p><img src="../assets/img/news/docker/docker_nginx_mongo_driver.jpg" alt="" /></p> <p>Vous pouvez quittez votre container &quot;nginx-dev&quot; en tapant <code>exit</code> ou la combinaison CTRL D.</p> <h2>MariaDB</h2> <h3>Installation de l'image</h3> <p>On récupère directement l'image à distance sur les serveurs de Docker avec l'option &quot;pull&quot; :<br /> <code>docker pull mariadb</code></p> <p>Ou bien avec l'option &quot;build&quot; en pointant directement vers le bon Github :<br /> <code>docker build -t=&quot;dockerfile/mariadb&quot; github.com/dockerfile/mariadb</code></p> <h3>Création du container</h3> <p><code>docker run -d --name maria-dev -e MYSQL_ROOT_PASSWORD=votre_mot_de_passe -p 3306:3306 mariadb</code></p> <p>L'utilisateur par défaut est &quot;root&quot; et sans mot de passe :<br /> <code>mysql -h 192.168.59.103 -u root -p votre_mot_de_passe</code><br /> <code>show databases;</code></p> <p><img src="../assets/img/news/docker/docker_mariadb.jpg" alt="" /></p> <h2>MongoDB</h2> <h3>Installation de l'image</h3> <p><code>docker pull mongo</code></p> <h3>Création du container</h3> <p><code>docker run -d --name mongo-dev -p 27017:27017 mongo</code></p> <p>La base est accessible sans authentification :<br /> <code>mongo 192.168.59.103</code><br /> <code>show dbs</code></p> <p><img src="../assets/img/news/docker/docker_mongodb.jpg" alt="" /></p> <p>Remarque : sur Boot2Docker, il n'est pas possible de monter un container avec un volume dont les données pointent directement sur le disque dur. La raison est expliquée sur la documentation officielle de MongoDB :<br /> &quot;MongoDB requires a filesystem that supports fsync() on directories. For example, HGFS and Virtual Box’s shared folders do not support this operation.&quot;<br /> Cette option marchera uniquement avec un volume sur Linux...</p> <h2>Conclusion</h2> <p>On a désormais une configuration Nginx + MariaDB + MongoDB opérationnelle en ayant créé 3 containers distincts. Maintenant que les images sont installées, vous pouvez créer facilemment d'autres containers &quot;clone&quot; avec une configuration différente sur chaque par exemple... Docker permet de faire de la virtualisation de manière rapide en s'affranchissant de la barrière d'une interface graphique jugée encombrante par son poid.</p> <h2>Sources</h2> <ul> <li><a href="https://docs.docker.com/installation/windows">https://docs.docker.com/installation/windows</a></li> <li><a href="https://github.com/fideloper/docker-nginx-php">https://github.com/fideloper/docker-nginx-php</a> </li> <li><a href="https://github.com/dockerfile/mariadb">https://github.com/dockerfile/mariadb</a></li> <li><a href="https://registry.hub.docker.com/_/mongo/">https://registry.hub.docker.com/_/mongo/</a></li> <li><a href="http://docs.docker.com/examples/mongodb/">http://docs.docker.com/examples/mongodb/</a></li> <li><a href="http://docs.mongodb.org/manual/administration/production-notes/#fsync-on-directories">http://docs.mongodb.org/manual/administration/production-notes/#fsync-on-directories</a></li> </ul>; 2015-04-23 17:24:04 Une todolist avec Gin Gonic https://etienner.fr/une-todolist-avec-gin-gonic <p>A l'aide d'un micro framework, Gin, on va créer une application &quot;todolist&quot; en Go. On va mettre en place sur une page, un formulaire pour poster des messages. Ces derniers seront listés sur cette même page. Il sera également possible de les supprimer. Après un cours aperçu de Ginc, ces messages seront stockés dans une base de données MySQL avec l'aide d'un ORM, Gorp.</p> <h2>Préparation et installation</h2> <p>Structure de notre dossier &quot;todolist&quot; :</p> <pre><code>│ main.go │ ├───db │ db.go │ └───views index.html</code></pre> <p>Sur votre serveur MySQL, créez uniquement une nouvelle table &quot;todolist&quot; :</p> <pre><code class="language-sql">CREATE DATABASE IF NOT EXISTS todolist;</code></pre> <p>Installez les librairies suivantes :<br /> <code>go get github.com/gin-gonic/gin</code><br /> <code>go get gopkg.in/gorp.v1</code><br /> <code>go get github.com/go-sql-driver/mysql</code> </p> <h2>Aperçu de Gin</h2> <p>Dans le fichier &quot;main.go&quot; :</p> <pre><code class="language-go">package main import ( "github.com/gin-gonic/gin" "net/http" ) func main() { r := gin.Default() r.LoadHTMLGlob("views/*") r.GET("/", func(c *gin.Context) { obj := gin.H{"title": "My Title"} c.HTML(http.StatusOK, "index.html", obj) }) r.POST("/submit", func(c *gin.Context) { c.Request.ParseForm() title := c.Request.Form.Get("title") obj := gin.H{"title": title} c.HTML(http.StatusOK, "index.html", obj) }) r.Run(":3000") }</code></pre> <p>On déclare Gin ainsi que la librairie native de Golang &quot;net/http&quot;.<br /> Dans l'unique fonction &quot;main()&quot;, on :</p> <ol> <li>Initialise la fonction &quot;gin.default()&quot;.</li> <li>Définit le répertoire de templating &quot;views&quot;.</li> <li>Déclare une première route. Celle ci est la racine (&quot;/&quot;). Sa fonction a pour but d'afficher le contenu de la valeur &quot;title&quot; présente dans le fichier de templating &quot;index.html&quot;. Cette route renvoit un code HTTP 200.</li> <li>Déclare une seconde route de type POST. Celle ci pointe sur &quot;/submit&quot; (destination de notre futur formulaire). Dans un premier temps, on demande à Gin de parser le fomulaire afin de récupérer la valeur contenue dans le champ &quot;title&quot; renseigné par l'utilisateur. Puis, on stocke cette valeur dans la variable &quot;title&quot; pour le fichier de templating &quot;index.html&quot;. Cette route renvoit également un code HTTP 200.</li> <li>Exécute notre serveur sur le port 3000.</li> </ol> <p>Dans notre vue &quot;index.html&quot;, on met en place le formulaire ci-dessous :</p> <pre><code class="language-markup">&lt;h1&gt; {{ .title }} &lt;/h1&gt; &lt;form action="submit" method="post"&gt; &lt;input type="text" name="title" placeholder="Enter a title" /&gt; &lt;input type="submit" /&gt; &lt;/form&gt;</code></pre> <p>On affiche la valeur &quot;title&quot; dans notre fichier de templating.</p> <p><img src="../assets/img/news/golang_todolist/todolist_static_submit.jpg" alt="" /></p> <p>On réédite notre fichier &quot;main.go&quot; plus propremement en déclarant une fonction pour chaque route (et par la même occasion, mieux s'y retrouver...) :</p> <pre><code class="language-go">package main import ( "github.com/gin-gonic/gin" ) func main() { r := gin.Default() r.LoadHTMLGlob("views/*") r.GET("/", indexHandler) r.POST("/submit", postHandler) r.Run(":3000") } func indexHandler(c *gin.Context) { obj := gin.H{"title": "Main website"} c.HTML(200, "index.html", obj) } func postHandler(c *gin.Context) { c.Request.ParseForm() title := c.Request.Form.Get("title") obj := gin.H{"title": title} c.HTML(200, "index.html", obj) }</code></pre> <p>Remarque : on en a également profité pour se débarrasser de la librairie native &quot;net/http&quot;.</p> <h2>Configuration de connexion à MySQL</h2> <p>Dans notre fichier &quot;db.go&quot; :</p> <pre><code class="language-go">package db import ( "database/sql" _ "github.com/go-sql-driver/mysql" "gopkg.in/gorp.v1" "log" ) type Messages struct { Id int64 `db:"id"` Title string `db:"title"` } func InitDb() *gorp.DbMap { db, err := sql.Open("mysql", "root:@/todolist") checkErr(err, "sql.Open failed") dbmap := &amp;gorp.DbMap{Db: db, Dialect: gorp.MySQLDialect{"InnoDB", "UTF8"}} dbmap.AddTableWithName(Messages{}, "messages").SetKeys(true, "Id") err = dbmap.CreateTablesIfNotExists() checkErr(err, "Create table failed") return dbmap } func checkErr(err error, msg string) { if err != nil { log.Fatalln(msg, err) } }</code></pre> <p>On déclare une structure &quot;Messages&quot;.<br /> Ensuite, dans le fonction &quot;InitDb()&quot; (qu'on a prit soin de nomme avec une majuscule), on initialise la connexion à notre serveur MySQL. Ensuite, on initialise la connexion à notre serveur MySQL dans la fonction &quot;InitDb()&quot; (dont on a prit soin de la nommer avec une majuscule afin de pouvoir l'exporter par la suite).<br /> Avec Gorp, on définit la future table comme étant de type InnoDB au format UFT-8. On lui demande également de créer une nouvelle table &quot;messages&quot; basée sur la structure &quot;Messages&quot; si la table n'existe pas.<br /> On lui demande aussi d'afficher une erreur si le serveur MySQL n'est pas accessible ou qu'il n'est pas possible de créer une nouvelle table via la foncton &quot;checkErr&quot;.</p> <p>Remarque : si vous avez un mot de passe pour vous connecter à votre BDD, ajoutez-le dans le DSN :<br /> <code>sql.Open(&quot;mysql&quot;, &quot;root:my_password@/todolist&quot;)</code> </p> <p>Cela marche également avec MariaDB (testé depuis un container Docker) :<br /> <code>db, err := sql.Open(&quot;mysql&quot;, &quot;root:@tcp(192.168.59.103:3306)/todolist&quot;)</code></p> <h2>Communiquons avec la base de données</h2> <p>On va pouvoir se connecter à la BDD dans notre fichier &quot;main.go&quot; :</p> <pre><code class="language-go">package main import ( "github.com/gin-gonic/gin" "todolist/db" ) var dbmap = db.InitDb() func main() { r := gin.Default() r.LoadHTMLGlob("views/*") r.GET("/", indexHandler) r.POST("/submit", postHandler) r.GET("/delete/:id", deleteHandler) r.Run(":3000") } func indexHandler(c *gin.Context) { messages := []db.Messages{} _, err := dbmap.Select(&amp;messages, "SELECT * FROM messages") if err != nil || len(messages) == 0 { obj := gin.H{"error": "No message in the database"} c.HTML(200, "index.html", obj) } else { obj := gin.H{"title": messages} c.HTML(200, "index.html", obj) } } func postHandler(c *gin.Context) { c.Request.ParseForm() title := c.Request.Form.Get("title") if title != "" { data := &amp;db.Messages{0, title} dbmap.Insert(data) } c.Redirect(301, "/") } func deleteHandler(c *gin.Context) { id := c.Params.ByName("id") dbmap.Exec("DELETE FROM messages WHERE id=?", id) c.Redirect(301, "/") }</code></pre> <p>On appelle notre fichier &quot;db.go&quot; via &quot;package db&quot; dans l'import des bibiliothèques. On déclare la variable globale &quot;dbmap&quot; qui pointe vers la fonction &quot;InitDb&quot; présente dans le fichier &quot;db.go&quot;. De ce fait, on peut désormais exécuter des requêtes SQL dans les fonctions concernées.</p> <p>Il ne nous reste plus qu'à modifier notre vue &quot;index.html&quot; avec le templating adéquatet ya d (et un peu de CSS par la même occasion...) :</p> <pre><code class="language-markup">&lt;!DOCTYPE html&gt; &lt;html&gt; &lt;head&gt; &lt;title&gt;ToDoList&lt;/title&gt; &lt;link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/bootstrap/3.3.4/css/bootstrap.min.css"&gt; &lt;/head&gt; &lt;body class="container"&gt; &lt;h1 class="text-center"&gt;ToDoList&lt;/h1&gt; {{if .error}} &lt;p class="alert alert-warning" role="alert"&gt;{{.error}}&lt;/p&gt; {{end}} {{range $message := .title}} &lt;ul class="list-unstyled"&gt; &lt;li&gt; &lt;a href="delete/{{$message.Id}}"&gt;&lt;i class="glyphicon glyphicon-remove"&gt;&lt;/i&gt;&lt;/a&gt; {{$message.Title}} &lt;/li&gt; &lt;/ul&gt; {{end}} &lt;form action="submit" method="post"&gt; &lt;div class="form-group"&gt; &lt;input type="text" class="form-control" name="title" placeholder="Enter a title" /&gt; &lt;/div&gt; &lt;input type="submit" class="btn btn-primary" /&gt; &lt;/form&gt; &lt;/body&gt; &lt;/html&gt;</code></pre> <p><img src="../assets/img/news/golang_todolist/todolist_post.jpg" alt="" /></p> <p>Lorsque vous lancez votre serveur pour la première fois, la table &quot;messages&quot; est créée automatiquement.</p> <p><img src="../assets/img/news/golang_todolist/totolist_create_table.jpg" alt="" /></p> <h2>Sources</h2> <ul> <li>Gin : <a href="https://github.com/gin-gonic/gin">https://github.com/gin-gonic/gin</a></li> <li>Gorp : <a href="https://github.com/go-gorp/gorp">https://github.com/go-gorp/gorp</a></li> <li>Driver SQL : <a href="https://github.com/go-sql-driver/mysql">https://github.com/go-sql-driver/mysql</a></li> </ul>; 2015-04-20 20:00:13 API Restful sur Codeigniter 3 https://etienner.fr/api-restful-sur-codeigniter-3 <p>Après trois RC (Release Candidate), Codeigniter 3 est sorti ! Parmi les nouveautés qu'il compte, la possibilité de travailler avec les méthodes HTTP GET, POST mais également PUT et DELETE. Cela rend plus facile à mettre en place une API Restful par rapport à la version précédente du framework. Quant à la base de données, on va rester sur une base SQL.</p> <h2>Schéma simplifié</h2> <p>On va uniquement travailler sur un contrôleur &quot;Product.php&quot; accompagné de son modèle &quot;Model_product.php&quot;. Et on finira par configurer les routes dans le fichier &quot;routes.php&quot;.</p> <pre><code>├───controllers │ │ │ └───api │ └───v1 │ Product.php ├───config │ routes.php ├───models │ Model_product.php</code></pre> <h2>Préparation de la base de données</h2> <p>Avant de commencer à construire l'API, on prépare notre BDD sur MySQL ou MariaDB :</p> <pre><code class="language-sql">CREATE DATABASE IF NOT EXISTS `codeigniter3_api` DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci; USE `codeigniter3_api`; CREATE TABLE IF NOT EXISTS `product` ( `id` int(11) NOT NULL AUTO_INCREMENT, `title` text, PRIMARY KEY (`id`) );</code></pre> <p>Puis, on configure l'accès à la BDD dans le fichier &quot;config/database.php&quot;</p> <pre><code class="language-php"> 'hostname' =&gt; 'localhost', 'username' =&gt; 'root', 'password' =&gt; '', 'database' =&gt; 'codeigniter_api',</code></pre> <h2>Création du modèle</h2> <p>Dans notre modèle &quot;Model_product.php&quot;, on va déclarer les 5 fonctions comportant les requêtes SQL CRUD ayant pour mission :</p> <ol> <li>Obtenir tous les produits</li> <li>Obtenir un produit en particulier</li> <li>Créer un nouveau produit</li> <li>Modifier un produit</li> <li>Supprimer un produit</li> </ol> <pre><code class="language-php">&lt;?php defined('BASEPATH') OR exit('No direct script access allowed'); class Model_product extends CI_Model { function __construct() { parent::__construct(); $this-&gt;table = "product"; } function get_all() { return $this-&gt;db-&gt;get($this-&gt;table); } function get_one($id) { $this-&gt;db-&gt;select("id, title") -&gt;from($this-&gt;table) -&gt;where("id", $id) -&gt;limit(1); return $this-&gt;db-&gt;get(); } function post($title) { $data = array( "title" =&gt; $title, ); $this-&gt;db-&gt;insert($this-&gt;table, $data); } function put($id, $title) { $data = array( "title" =&gt; $title ); $this-&gt;db-&gt;where("id", $id) -&gt;update($this-&gt;table, $data); } function delete($id) { $this-&gt;db-&gt;where_in("id", $id) -&gt;delete($this-&gt;table); } } /* End of file Model_product.php */ /* Location: ./application/models/Model_product.php */</code></pre> <h2>Création du contrôleur</h2> <p>Dans notre contrôleur, on va retrouver les 5 fonctions que l'on a créé précédemment dans notre modèle et que l'on ajoutera dans notre fichier routes. Ces fonctions retourneront des valeurs aux formats Json et dans le bon code HTTP.</p> <pre><code class="language-php">&lt;?php defined('BASEPATH') OR exit('No direct script access allowed'); class Product extends CI_Controller { function __construct() { parent::__construct(); $this-&gt;load-&gt;database(); $this-&gt;load-&gt;model("Model_product"); } public function index() { $data = $this-&gt;Model_product-&gt;get_all(); if ($data-&gt;num_rows() &gt; 0) { foreach ($data-&gt;result() as $row) { $result[] = array("id" =&gt; intval($row-&gt;id), "title" =&gt; $row-&gt;title); } echo json_encode($result); } else { header("HTTP/1.0 204 No Content"); echo json_encode("204: no products in the database"); } } public function view($id) { $data = $this-&gt;Model_product-&gt;get_one($id); if ($data-&gt;num_rows() &gt; 0) { foreach ($data-&gt;result() as $row) { $result[] = array("id" =&gt; intval($row-&gt;id), "title" =&gt; $row-&gt;title); } echo json_encode($result); } else { header("HTTP/1.0 404 Not Found"); echo json_encode("404 : Product #$id not found"); } } public function create() { $title = $this-&gt;input-&gt;post('title', TRUE); if (!empty($title)) { $this-&gt;Model_product-&gt;post($title); echo json_encode('Product created'); } else { header("HTTP/1.0 400 Bad Request"); echo json_encode("400: Empty value"); } } public function update($id) { $title = utf8_encode($this-&gt;input-&gt;input_stream('title', TRUE)); if ($this-&gt;Model_product-&gt;get_one($id)-&gt;num_rows() == 1) { if (!empty($title)) { $this-&gt;Model_product-&gt;put($id, $title); echo json_encode("200: Product #$id updated"); } else { header("HTTP/1.0 400 Bad Request"); echo json_encode("400: Empty value"); } } else { header("HTTP/1.0 404 Not Found"); echo json_encode("404: Product #$id not found"); } } public function delete($id) { if ($this-&gt;Model_product-&gt;get_one($id)-&gt;num_rows() == 1) { $this-&gt;Model_product-&gt;delete($id); echo json_encode("200: Product #$id deleted"); } else { header("HTTP/1.0 404 Not Found"); echo json_encode("404: Product $id not found"); } } } /* End of file Product.php */ /* Location: ./application/controllers/Product.php */</code></pre> <p>Dans le constructeur, on charge la connexion à la BDD et le modèle. Pour les 2 dernières fonctions (modification et suppression), on vérifie que le produit existe.<br /> Dans la fonction <code>update</code>, on récupère les informations rentrées par l'utilisateur via la fonction <code>$this-&gt;input-&gt;input_stream</code>.</p> <p>Important : on active la protection contre les injections XSS avec le paramêtre &quot;TRUE&quot; pour les fonctions <code>input-&gt;post</code> et <code>input-&gt;input_stream</code>. </p> <p>Remarque : on convertit la valeur de l'id du ou des produit(s) en entier car par défaut, Codeigniter renvoit au format &quot;string&quot; (chaine de caractères) les données supposées être des &quot;int&quot; (entiers).</p> <h2>Préparation des routes</h2> <p>Pour finir, éditez le fichier &quot;application/config/routes.php&quot; et ajoutez les routes ci-dessous correspondant aux fonctions présentes dans notre contrôleur.</p> <pre><code class="language-php">$route["api/v1/product"]["get"] = "api/v1/product"; $route["api/v1/product/(:num)"]["get"] = "api/v1/product/view/$1"; $route["api/v1/product"]["post"] = "api/v1/product/create"; $route["api/v1/product/(:num)"]["put"] = "api/v1/product/update/$1"; $route["api/v1/product/(:num)"]["delete"] = "api/v1/product/delete/$1";</code></pre> <p>On suffixe dans chaque route, la méthode HTTP concernée (GET, POST, PUT ou DELETE).</p> <h2>Testons avec l'ami CURL</h2> <h3>GET :</h3> <p><code>curl <a href="http://localhost/codeigniter-api/api/v1/product">http://localhost/codeigniter-api/api/v1/product</a></code><br /> <code>curl <a href="http://localhost/codeigniter-api/api/v1/product/1">http://localhost/codeigniter-api/api/v1/product/1</a></code></p> <p>Remarque : vous pouvez aussi tester les requêtes HTTP de type GET sur votre navigateur Web...</p> <h3>POST :</h3> <p><code>curl -d title=&quot;product title&quot; <a href="http://localhost/codeigniter-api/api/v1/product">http://localhost/codeigniter-api/api/v1/product</a></code><br /> <code>curl -d title=&quot;<script src='alert'>alert</script>&quot; <a href="http://localhost/codeigniter-api/api/v1/product">http://localhost/codeigniter-api/api/v1/product</a></code></p> <h3>PUT :</h3> <p><code>curl -X PUT -d title=&quot;edit product title&quot; <a href="http://localhost/codeigniter-api/api/v1/product/1">http://localhost/codeigniter-api/api/v1/product/1</a></code><br /> <code>curl -X PUT -d title=&quot;<script src='alert'>alert</script>&quot; <a href="http://localhost/codeigniter-api/api/v1/product/1">http://localhost/codeigniter-api/api/v1/product/1</a></code></p> <h3>DELETE :</h3> <p><code>curl -X DELETE <a href="http://localhost/codeigniter-api/api/v1/product/1">http://localhost/codeigniter-api/api/v1/product/1</a></code></p> <h2>CORS</h2> <p>Par défaut, si vous souhaitez travailler sur une application tournant sur un autre port autre que celui de votre serveur Apache, Nginx, etc... vous aurez un message d'erreur car le CORS restreint l'accès à votre API.<br /> Dans le constructeur du contrôleur, ajoutez la ligne suivante :</p> <pre><code class="language-php">header("Access-Control-Allow-Origin: *");</code></pre> <h2>Conclusion</h2> <p>Codeigniter 3 permet de créer rapidement une API Restful grâce aux nouvelles fonctionnalitées telles que <code>$this-&gt;input-&gt;valeur</code> et la possibilité de déclarer les méthodes HTTP dans les routes. Vous pouvez également rajouter un système de vérification de token pour ajouter de la sécurité à votre API. Dans ce cas, il faudra rajouter une condition sur les fonctions concernées dans le contrôleur.</p> <h2>Sources</h2> <ul> <li>php://input stream : <a href="http://www.codeigniter.com/userguide3/libraries/input.html#using-the-php-input-stream">http://www.codeigniter.com/userguide3/libraries/input.html#using-the-php-input-stream</a></li> <li>Les routes avec leur méthode HTTP : <a href="http://www.codeigniter.com/userguide3/general/routing.html#using-http-verbs-in-routes">http://www.codeigniter.com/userguide3/general/routing.html#using-http-verbs-in-routes</a></li> </ul>; 2015-04-15 10:40:22 Les structures en Go https://etienner.fr/les-structures-en-go <p>Les structures font parties des fondamentaux de Golang notamment dans le développement Web où elles sont utilisées dans les modèles lors de développement d'applications en MVC. Elles permettent également de travailler à partir de fichiers de type Json ou XML facilement. Une structure retournera des données sous formes de tableau de différent typages (entier, chaine de carcatères, décimal, etc...).</p> <h2>Déclaration d'une structure</h2> <p>On déclare une structure de la manière suivante :</p> <pre><code class="language-go">type Post struct { // Futur contenu de notre structure }</code></pre> <p>Il ne reste plus qu'à compléter notre structure</p> <pre><code class="language-go">type Post struct { Id int64 Title string Content string }</code></pre> <p>En résumé, on a notre structure nommée &quot;Post&quot; qui contient un champ &quot;Id&quot; de type entier et 2 chaines de caractères &quot;Title&quot; et &quot;Content&quot;.</p> <p>Attention : le nom d'une structure et le nom des valeurs qui la compose commencent toujours par une lettre !</p> <h2>Lecture de notre structure</h2> <p>On va maintenant rendre notre structure &quot;Post&quot; opérationnelle. Autrement dit, on va l'appeler et y introduire les données ci-dessous :</p> <pre><code class="language-go">post := Post{1, "Mon titre", "Mon contenu"}</code></pre> <p>Dans la variable &quot;post&quot;, on importe le contenu de la structure &quot;Post&quot;. Ce qui donne :</p> <pre><code class="language-go">package main import ( "fmt" ) type Post struct { Id int Title, Content string } func main() { post := Post{1, "Mon titre", "Mon contenu"} fmt.Print(post) // {1 Mon titre Mon contenu} }</code></pre> <p>On peut aussi appeler la structure en précisant chaque nom des champs :</p> <pre><code class="language-go"> post := Post{ Id: 1, Title: "Mon titre", Content: "Mon contenu", }</code></pre> <p>Ou encore :</p> <pre><code class="language-go"> post := Post{} post.Id = 1 post.Title = "Mon titre" post.Content = "Mon contenu"</code></pre> <p>Le résultat final sera toujours le même.</p> <h2>Cas pratique avec du JSON</h2> <h3>Structure simple</h3> <p>On veut ouvrir un fichier &quot;post.json&quot; (présent dans le même dossier que le fichier Go) dont la structure est la suivante : </p> <pre><code class="language-javascript">{"id: 1", "title": "Mon titre 1", "content": "Mon contenu 1"}</code></pre> <p>On édite notre code :</p> <pre><code class="language-go">package main import ( "fmt" "io/ioutil" ) type Post struct { Id int Title, Content string } func main() { file, err := ioutil.ReadFile("post.json") if err == nil { fmt.Println(file) } else { fmt.Print(err) } }</code></pre> <p>On ouvre le fichier json via &quot;ioutil.ReadFile&quot; (fonction de la librairie <strong>io/ioutil</strong>). S'il n'y a pas d'erreur, on affiche le contenu de la variable &quot;json&quot;. Sinon, on affiche une erreur. Normalement, notre code fonctionne mais renvoie des données non exploitables. On ré-édite notre code pour décoder ces données :</p> <pre><code class="language-go">package main import ( "encoding/json" "fmt" "io/ioutil" ) type Post struct { Id int Title, Content string } func main() { file, err := ioutil.ReadFile("post.json") if err == nil { var post Post json.Unmarshal(file, &amp;post) fmt.Println(post) } else { fmt.Print(err) } }</code></pre> <p>Concrètement, on déclare une variable &quot;post&quot; accompagnée de la structure &quot;Post&quot;. Puis, on utilise la fonction &quot;json.Unmarshal&quot; de la librairie <strong>encoding/json</strong> pour décoder le contenu au format Json présent dans le fichier &quot;post.json&quot; (via la variable &quot;file&quot;) avec la structure &quot;Post&quot;. Ensuite, on affiche le résultat.</p> <p>On modifie le fichier Json en ajoutant des lignes supplémentaires :</p> <pre><code class="language-javascript">[ {"id": 1, "title": "Mon titre 1", "content": "Mon contenu 1"}, {"id": 2, "title": "Mon titre 2", "content": "Mon contenu 2"}, {"id": 3, "title": "Mon titre 3", "content": "Mon contenu 3"}, {"id": 4, "title": "Mon titre 4", "content": "Mon contenu 4"} ]</code></pre> <p>Le résultat affichera <code>{0 }</code> :(<br /> Remplacez <code>var post Post</code> par <code>var post []Post</code> car il s'agit d'un tableau de données.</p> <p>Remarque : votre fichier Json ne doit pas comporter de code commenté !</p> <p>On peut ainsi lui demander de n'afficher que la 1ère et 3ème ligne du fichier :</p> <pre><code class="language-go">package main import ( "encoding/json" "fmt" "io/ioutil" ) type Post struct { Id int Title, Content string } func main() { file, err := ioutil.ReadFile("post.json") if err == nil { var post []Post json.Unmarshal(file, &amp;post) fmt.Println(post[0]) // {1 Mon titre 1 Mon contenu 1} fmt.Println(post[2]) // {3 Mon titre 3 Mon contenu 3} } else { fmt.Print(err) } }</code></pre> <p>Exercice :<br /> On va interagir avec l'utilisateur en lui proposant 2 options :</p> <ul> <li>Afficher toutes les lignes</li> <li>Afficher une ligne en particulier</li> </ul> <pre><code class="language-go">package main import ( "encoding/json" "fmt" "io/ioutil" ) type Post struct { Id int Title, Content string } func main() { var number int file, err := ioutil.ReadFile("post.json") if err == nil { var post []Post json.Unmarshal(file, &amp;post) max := len(post) fmt.Printf("Entrez un chiffre entre 1 et %d : ", max) _, err := fmt.Scanf("%d", &amp;number) if err != nil { fmt.Println(err) } else if number == 0 { fmt.Print(post) // Affiche toutes les données } else if number &gt; max { fmt.Print("Pas de données") } else { fmt.Println(post[number-1]) } } }</code></pre> <p><img src="../assets/img/news/golang-struct/json.jpg" alt="" /></p> <h3>Structure avancée</h3> <p>Au lieu de créer un Json avec une structure avancée, on va piocher dans l'API du site de météorologie openweathermap.org, plus précisément sur <a href="http://api.openweathermap.org/data/2.5/find?q=Washington&amp;units=metric">http://api.openweathermap.org/data/2.5/find?q=Washington&amp;units=metric</a> qui retourne ce genre de Json :</p> <pre><code class="language-javascript"> { "message":"accurate", "cod":"200", "count":4, "list":[ { "id":2634715, "name":"Washington", "coord":{ "lon":-1.51667, "lat":54.900002 }, "main":{ "temp":7.98, "temp_min":7.98, "temp_max":7.98, "pressure":1020.08, "sea_level":1034.48, "grnd_level":1020.08, "humidity":74 }, "dt":1427981672, "wind":{ "speed":2.01, "deg":344 }, "sys":{ "country":"GB" }, "clouds":{ "all":80 }, "weather":[ { "id":803, "main":"Clouds", "description":"broken clouds", "icon":"04d" } ] }, { "id":4915545, "name":"Washington", "coord":{ "lon":-89.40731, "lat":40.703651 }, "main":{ "temp":17.98, "temp_min":17.98, "temp_max":17.98, "pressure":994.79, "sea_level":1019.2, "grnd_level":994.79, "humidity":83 }, "dt":1427981672, "wind":{ "speed":7.76, "deg":223 }, "sys":{ "country":"US" }, "rain":{ "3h":1.73 }, "clouds":{ "all":92 }, "weather":[ { "id":500, "main":"Rain", "description":"light rain", "icon":"10d" } ] }, { "id":5549222, "name":"Washington", "coord":{ "lon":-113.508293, "lat":37.130539 }, "main":{ "temp":6.98, "temp_min":6.98, "temp_max":6.98, "pressure":859.02, "sea_level":1024.06, "grnd_level":859.02, "humidity":30 }, "dt":1427981672, "wind":{ "speed":1.01, "deg":304.5 }, "sys":{ "country":"US" }, "clouds":{ "all":0 }, "weather":[ { "id":800, "main":"Clear", "description":"Sky is Clear", "icon":"01d" } ] }, { "id":5815135, "name":"Washington", "coord":{ "lon":-120.501472, "lat":47.500118 }, "main":{ "temp":-1.92, "temp_min":-1.92, "temp_max":-1.92, "pressure":916.98, "sea_level":1040.76, "grnd_level":916.98, "humidity":83 }, "dt":1427981672, "wind":{ "speed":0.86, "deg":287 }, "sys":{ "country":"US" }, "clouds":{ "all":0 }, "weather":[ { "id":800, "main":"Clear", "description":"Sky is Clear", "icon":"01n" } ] } ] }</code></pre> <p>L'API nous renvoit 4 résultats de villes (<code>&quot;count&quot;:4</code>) s'appelant &quot;Washington&quot; dans le tableau &quot;list&quot;.</p> <p>Vu la structure du fichier Json, cela nous oblige de créer des structures imbriquées en Go, le syndrome de la &quot;structureception&quot; :</p> <pre><code class="language-go">package main import ( "encoding/json" "fmt" "io/ioutil" "log" "net/http" ) type Location struct { Message, Cod string Count int List []List } type List struct { Id int Name string Coord Coord Main Main Dt int Wind Wind Sys Sys Clouds Clouds Weather []Weather } type Main struct { Temp, Temp_min, Pressure, Sea_level, Grnd_level, Humidity float32 } type Coord struct { Lon, Lat float32 } type Wind struct { Speed, Deg float32 } type Rain struct { Value float32 `json:"3h"` } type Sys struct { Country string } type Clouds struct { All int } type Weather struct { Id int32 Main, Description, Icon string } func main() { resp, err := http.Get("http://api.openweathermap.org/data/2.5/find?q=Washington&amp;units=metric") if err == nil { body, err := ioutil.ReadAll(resp.Body) if err == nil { var location Location err := json.Unmarshal(body, &amp;location) if err == nil { fmt.Println(location) } else { log.Print(err) } } else { log.Print(err) } } else { log.Print(err) } }</code></pre> <p>Quelques explications s'imposent :</p> <ol> <li>On déclare une structure &quot;location&quot; (vous pouvez l'appeler comme bon vous semble si ce terme ne vous semble pas explicite...). Au sein de cette structure, on récupère les valeurs contenues dans les champs &quot;Message&quot;, &quot;Cod&quot;, &quot;Count&quot;. Puis, on arrive sur un tableau qui va aller chercher dans la structure enfant &quot;List&quot;. &quot;List&quot; étant un tableau, on préfixe avec crochet fermant / ouvrant &quot;[]&quot; lors de sa déclaration dans sa structure parente (&quot;Location&quot;).</li> <li>La struture &quot;List&quot; comporte des données comme &quot;Id&quot;, &quot;Name&quot;, &quot;Dt&quot; mais également des structures enfants.</li> <li>On déclare les différentes structures enfants de &quot;List&quot;.</li> <li>Dans la function &quot;main&quot;, on appel l'API du site via &quot;http.Get&quot; (fonction de la librairie <strong>net/http</strong>).</li> <li>On lit le contenu de l'URL via &quot;ioutil.ReadAll&quot; (fonction de la librairie <strong>io/ioutil</strong>).</li> <li>On décode le contenu en Json via &quot;json.Unmarshal&quot; (fonction de la librairie <strong>encoding/json</strong>) par rapport à la structure &quot;location&quot;.</li> <li>On affiche le contenu dans notre console via la structure &quot;location&quot; qui affiche toutes les données de son enfant (&quot;List&quot;) et de ses petits enfants.</li> </ol> <p>Non mais attend, c'est quoi ce charabia dans la structure &quot;Rain&quot; ?</p> <pre><code class="language-go">type Rain struct { Value float32 `json:"3h"` }</code></pre> <p>Le nom de l'unique valeur présente dans &quot;Rain&quot; est &quot;3h&quot; dans le fichier Json. Hors Go, comme précisé plus haut dans cet article, n'accepte pas les noms de valeur dans les structures commencant par un chiffre. Pour cela, on attribue un nom de valeur quelconque et on précise avec le binding (liaison) <code><code>json:"3h"</code></code> qu'il s'agit du nom de la valeur &quot;3h&quot; présente dans le Json.<br /> <code>fmt.Println(location.List[1].Rain.Value)</code> affiche bien &quot;1.73&quot;.</p> <p>Remarque : si vous ne souhaitez pas afficher certaines informations, il n'est pas nécessaire de déclarer toutes les structures, seulement celles qui sont concernées (en faisant attention à l'héritage).</p> <p>Petit exercice :<br /> On souhaite afficher uniquement la ou les villes dont le code vaut &quot;GB&quot;.<br /> On a besoin de modifier uniquement la fonction <code>main()</code> et d'y ajouter une condition concernant le code du pays. Cela se passe dans la &quot;sous sous&quot; structure &quot;Sys&quot; et la valeur concernée est &quot;Country&quot;.</p> <pre><code class="language-go">func main() { resp, err := http.Get("http://api.openweathermap.org/data/2.5/find?q=Washington&amp;units=metric") if err == nil { body, err := ioutil.ReadAll(resp.Body) if err == nil { var location Location err := json.Unmarshal(body, &amp;location) if err == nil { for i := 0; i &lt; location.Count; i++ { if location.List[i].Sys.Country == "GB" { fmt.Println(location.List[i]) } } } else { log.Print(err) } } else { log.Print(err) } } else { log.Print(err) } }</code></pre> <p>Un dernier exercice avant de clore cet article :<br /> cette fois, on veut que l'utilisateur rentre lui-même le nom de la ville. On affiche un message d'erreur personnalisé si l'API ne trouve rien correspondant la ville saisie.</p> <pre><code class="language-go">func main() { var city string fmt.Print("Entrez le nom de la ville ") _, err := fmt.Scanf("%s", &amp;city) if err == nil { resp, err := http.Get("http://api.openweathermap.org/data/2.5/find?q=+" + city + "&amp;units=metric") if err == nil { body, err := ioutil.ReadAll(resp.Body) if err == nil { var location Location err := json.Unmarshal(body, &amp;location) if err == nil { if location.Count &gt; 0 { fmt.Println(location) } else { fmt.Println("Cette ville n'existe pas :(") } } else { log.Print(err) } } else { log.Print(err) } } else { log.Print(err) } } }</code></pre> <p>Explications :</p> <ol> <li>On déclare la variable &quot;city&quot;.</li> <li>On invite l'utilisateur à rentrer le nom de la ville.</li> <li>On concatène la variable &quot;city&quot; contenant le nom de la ville rentrée par l'utilisateur dans l'URL de l'API.</li> <li>On décode les données fournies par l'API en JSON.</li> <li>On met en place une condition en se basant sur la valeur de &quot;count&quot; pour savoir si il y a des données à afficher ou non.</li> </ol> <p><img src="../assets/img/news/golang-struct/openweathermap.jpg" alt="" /></p> <h2>Conclusion</h2> <p>Avant de se jeter corps et âme dans le code, prenez soin d'analyser soigneusement la structure globale des données. Afin de gérer au mieux l'héritage des structures.</p>; 2015-04-12 18:46:46 MongoDB sur PHP https://etienner.fr/mongodb-sur-php <p>Vous souhaitez développer une application PHP avec la base de données NoSQL MongoDB. Il existe bel et bien une classe pour travailler dessus. Mais pour ce faire, il faut au préalable disposer du driver officiel sur votre serveur. Par défaut, ce dernier est rarement installé sur les serveurs aussi bien en local qu'en production.</p> <h2>Installation</h2> <ul> <li>Sur Windows : <a href="https://s3.amazonaws.com/drivers.mongodb.org/php/index.html">https://s3.amazonaws.com/drivers.mongodb.org/php/index.html</a><br /> Avec WAMP : dans le fichier &quot;wamp/bin/php/votre_version_php/php.ini&quot;, ajoutez la ligne suivante :<br /> <code>extension=php_mongo.dll</code><br /> Lancez WAMP puis cliquez sur l'icone de WAMP (en bas à droite), allez dans &quot;PHP&quot;, &quot;PHP extensions&quot;, cliquez sur &quot;php_mongo&quot;.</li> <li>Sur Mac : <a href="http://php.net/manual/fr/mongo.installation.php#mongo.installation.osx">http://php.net/manual/fr/mongo.installation.php#mongo.installation.osx</a></li> <li>Sur Linux : <a href="http://php.net/manual/fr/mongo.installation.php#mongo.installation.nix">http://php.net/manual/fr/mongo.installation.php#mongo.installation.nix</a></li> </ul> <p>Pour savoir si vous avez bien installé le driver, faites un <code>phpinfo();</code>, vous devriez avoir dans votre listing &quot;mongo&quot; :<br /> <img src="../assets/img/news/php_mongo/php_mongo_driver.jpg" alt="" /></p> <h2>Connexion</h2> <p>Dans un nouveau fichier &quot;mongodb.php&quot;, on se connecte au serveur MongoDB :</p> <pre><code class="language-php">&lt;?php // Ouverture de la connexion (localhost par défaut) $mongo = new MongoClient(); // Sélection de la database "test" $db = $mongo-&gt;selectDB("test"); // Sélection de la collection "Users" $c_users = new MongoCollection($db, "Users"); // Obtenir tous les utilisateurs $get_users = $c_users-&gt;find(); // Obtenir le nombre d'utilisateurs $count_users = $c_users-&gt;count(); // Fermeture de la connexion $mongo-&gt;close(); ?&gt;</code></pre> <p>Si le driver n'est pas installé ou activé sur votre serveur, ce dernier vous renverra une erreur car il ne trouvera pas la classe &quot;MongoClient&quot;.<br /> <code>Fatal error: Class 'MongoClient' not found</code></p> <p>Ou si, votre serveur Mongo n'est pas démarré :<br /> <code>Fatal error: Uncaught exception 'MongoConnectionException' with message '</code></p> <p>C'est pour cela que l'on va améliorer notre fichier de connexion &quot;mongodb.php&quot; avec une gestion des messages d'erreurs ci-dessous :</p> <pre><code class="language-php">&lt;?php if ( ! class_exists('Mongo')) { echo "&lt;h1&gt;Le driver Mongo n'est pas installé sur ce serveur :(&lt;/h1&gt;"; } else { try { // Ouverture de la connexion $mongo = new MongoClient('mongodb://localhost'); // Sélection de la database "test" $db = $mongo-&gt;selectDB("test"); // Sélection de la collection "Users" $c_users = new MongoCollection($db, "Users"); // Obtenir tous les utilisateurs $get_users = $c_users-&gt;find(); // Obtenir le nombre d'utilisateurs $count_users = $c_users-&gt;count(); // Fermeture de la connexion $mongo-&gt;close(); } catch (MongoConnectionException $exception) { echo "&lt;h1&gt;Impossible de se connecter au serveur MongoDB :(&lt;/h1&gt;"; } } ?&gt;</code></pre> <h2>Utilisation des données (CRUD)</h2> <h3>Lecture des données avec find</h3> <p>Dans un nouveau fichier &quot;users.php&quot; :</p> <pre><code class="language-php">&lt;?php // Appel de la connexion require_once('mongodb.php'); var_dump($count_users); var_dump(iterator_to_array($get_users)); ?&gt;</code></pre> <p>On remarque que la variable <code>$get_users</code> n'est autre qu'un tableau. On peut donc appeler nos données dans une boucle <code>foreach</code> :</p> <pre><code class="language-php">&lt;?php // Appel de la connexion require_once('mongodb.php'); if (isset($count_users)) { echo '&lt;a href="form_users.php"&gt;Ajouter un utilisateur&lt;/a&gt;'; if ($count_users &gt; 0){ echo '&lt;p&gt;' . $count_users . ' utilisateur(s)&lt;/p&gt;'; echo '&lt;table&gt;'; echo '&lt;tr&gt;&lt;th&gt;firstname&lt;/th&gt;&lt;th&gt;lastname&lt;/th&gt;&lt;th&gt;&lt;/th&gt;&lt;th&gt;&lt;/tr&gt;'; foreach ($get_users as $user) { echo '&lt;tr&gt;'; echo '&lt;td&gt;' . $user['firstname'] . '&lt;/td&gt;'; echo '&lt;td&gt;' . $user['lastname'] . '&lt;/td&gt;'; echo '&lt;td&gt;&lt;a href="form_users.php?edit=' . $user['_id'] . '"&gt;Modifier&lt;/td&gt;'; echo '&lt;td&gt;&lt;a href="users.php?delete=' . $user['_id'] . '"&gt;Supprimer&lt;/td&gt;'; echo '&lt;/tr&gt;'; } echo '&lt;/table&gt;'; } else { echo "Pas d'utilisateurs"; } } ?&gt;</code></pre> <p>Ce qui nous donne, cette <del>magnifique</del> interface dépouillée :<br /> <img src="../assets/img/news/php_mongo/php_mongo_listing.jpg" alt="" /></p> <h2>Insertion des données avec &quot;insert&quot;</h2> <p>On créé un nouveau fichier &quot;form_users.php&quot; qui va nous servir de formulaire :</p> <pre><code class="language-markup">&lt;form action="users.php" method="POST" /&gt; &lt;div&gt; &lt;label for="firstname"&gt;Firstname&lt;/label&gt; &lt;br /&gt;&lt;input type="text" id="firstname" name="firstname" value="" required /&gt; &lt;/div&gt; &lt;div&gt; &lt;label for="lastname"&gt;Lastname&lt;/label&gt; &lt;br /&gt;&lt;input type="text" id="lastname" name="lastname" value="" required /&gt; &lt;/div&gt; &lt;div&gt; &lt;input type="submit" value="Envoyer" /&gt; &lt;/div&gt; &lt;/form&gt;</code></pre> <p>Puis, on édite le fichier sur lequel le formulaire envoie les données au format &quot;POST&quot;, &quot;users.php&quot; :</p> <pre><code class="language-php">&lt;?php # [Connexion à la base] if (isset($_POST) &amp;&amp; !empty($_POST)) { $c_users-&gt;insert(array('firstname' =&gt; $_POST['firstname'], 'lastname' =&gt; $_POST['lastname'])); header('Location: users.php'); exit; } # [Tableau des données] ?&gt;</code></pre> <p>On récupère les données présentes dans la super variable <code>$_POST</code> pour les insérer dans le tableau de la requête <code>insert</code>.</p> <p><img src="../assets/img/news/php_mongo/php_mongo_post.jpg" alt="" /></p> <h2>Modification des données avec &quot;update&quot;</h2> <p>On commence par modifier le formulaire &quot;form_users.php&quot; (dans sa version finale) :</p> <pre><code class="language-php">&lt;?php // Appel de la connexion require_once('mongodb.php'); // Cas d'une édition if ( isset($_GET['edit']) &amp;&amp; !empty($_GET['edit']) ) { // Récupération de l'ObjectID au format ... $id = new MongoId($_GET['edit']); // Lecture du document concerné $get_user = $c_users-&gt;findOne(array("_id" =&gt; $id)); } if ( isset($_GET['edit']) &amp;&amp; !empty($_GET['edit']) &amp;&amp; !isset($get_user) ) { echo "Impossible de modifier cet utilisateur car il n'existe plus ou n'a jamais existé"; } else { ?&gt; &lt;form action="users.php" method="POST" /&gt; &lt;label for="firstname"&gt;Firstname&lt;/label&gt; &lt;br /&gt;&lt;input type="text" id="firstname" name="firstname" value="&lt;?php if (isset($_GET['edit']) &amp;&amp; !empty($_GET['edit'])) echo $get_user['firstname']; ?&gt;" required /&gt; &lt;br /&gt;&lt;label for="lastname"&gt;Lastname&lt;/label&gt; &lt;br /&gt;&lt;input type="text" id="lastname" name="lastname" value="&lt;?php if (isset($_GET['edit']) &amp;&amp; !empty($_GET['edit'])) echo $get_user['lastname']; ?&gt;" required /&gt; &lt;?php if (isset($_GET['edit']) &amp;&amp; !empty($_GET['edit'])) { ?&gt; &lt;input type="hidden" name="user_id" value="&lt;?php echo $id; ?&gt;" /&gt; &lt;?php }; ?&gt; &lt;br /&gt;&lt;input type="submit" value="Envoyer" /&gt; &lt;/form&gt; &lt;?php } ?&gt;</code></pre> <p>On se connecte au serveur pour récupérer les données présentes dans la collection &quot;Users&quot;, puis on n'oublie pas de déclarer le champ caché contenant l'ObjectId du document concerné.</p> <p>Ensuite, on édite le fichier &quot;users.php&quot; :</p> <pre><code class="language-php">&lt;?php # [Connexion à la base] if (isset($_POST) &amp;&amp; !empty($_POST)) { if (isset($_POST['user_id'])) { $id = new MongoId($_POST['user_id']); $newdata = array('$set' =&gt; array('firstname' =&gt; $_POST['firstname'], 'lastname' =&gt; $_POST['lastname'])); $c_users-&gt;update(array("_id" =&gt; $id), $newdata); } else { $c_users-&gt;insert(array('firstname' =&gt; $_POST['firstname'], 'lastname' =&gt; $_POST['lastname'])); } header('Location: users.php'); exit; } # [Tableau des données] ?&gt;</code></pre> <p>Comme pour l'insertion des données, on récupère les données du formulaire pour mettre à jour les données dans le tableau de la requête <code>update</code>.</p> <h2>Suppression des données avec &quot;remove&quot;</h2> <p>Dans le fichier &quot;users.php&quot;, on ajoute la condition ci-dessous :</p> <pre><code class="language-php">&lt;?php # [Connexion à la base] # [Conditions modification / ajout] if (isset($_GET['delete']) &amp;&amp; !empty($_GET['delete'])) { $id = new MongoId($_GET['delete']); $c_users-&gt;remove(array('_id' =&gt; $id)); header('Location: users.php'); exit; } # [Tableau des données] ?&gt;</code></pre> <p>On supprime le document concerné via son ObjectId.</p> <h2>Sources</h2> <ul> <li>La classe MongoDB PHP: <a href="http://php.net/manual/fr/class.mongodb.php">http://php.net/manual/fr/class.mongodb.php</a></li> <li>Site officiel de MongoDB : <a href="http://www.mongodb.org">http://www.mongodb.org</a></li> </ul>; 2015-03-16 16:00:00 Serveur web en mode MVC https://etienner.fr/serveur-web-en-mode-mvc <p>Avec Golang, on peut déployer rapidement mettre en place un serveur HTTP &quot;from scratch&quot; grâce à la librarie de base <strong>net/http</strong>. On pourra par la suite, l'améliorer en ajoutant un système de templating avec la librairies <strong>html/template</strong>. On va mettre en place ce serveur sur une architecture basée sur le modèle MVC (Modèle Vue Contrôleur).</p> <h2>Création d'un serveur basique</h2> <p>Dans un nouveau dossier (je l'ai appelé &quot;myserver&quot;), créez un fichier &quot;main.go&quot;:</p> <pre><code class="language-go">// main.go package main import ( "fmt" "net/http" ) func main() { http.HandleFunc("/", homeHandler) http.ListenAndServe(":3000", nil) } func homeHandler(w http.ResponseWriter, r *http.Request) { fmt.Fprint(w, "home page") }</code></pre> <p>Quelques explications à propos de <strong>net/http</strong>:</p> <ul> <li><code>http.HandleFunc</code> : correspond à une route.</li> <li><code>http.ListenAndServe</code> : correspond au port d'écoute. Ici &quot;3000&quot;, donc le serveur est accessible via l'URL suivante : <a href="http://localhost:3000">http://localhost:3000</a>.</li> <li><code>w http.ResponseWriter</code> : paramètre d'écriture (&quot;write&quot;).</li> <li><code>r *http.Request</code> : paramètre de lecture (&quot;read&quot;).</li> </ul> <p>De cette façon, il est simple de rajouter une nouvelle route:</p> <pre><code class="language-go">package main import ( "fmt" "log" "net/http" ) func main() { port := ":3000" log.Println("Starting Web Server 127.0.0.1" + port) http.HandleFunc("/", homeHandler) http.HandleFunc("/about", aboutHandler) err := http.ListenAndServe(port, nil) if err != nil { log.Fatal("ListenAndServe: ", err) } } func homeHandler(w http.ResponseWriter, r *http.Request) { fmt.Fprint(w, "home page") } func aboutHandler(w http.ResponseWriter, r *http.Request) { fmt.Fprint(w, "about page") }</code></pre> <p>Lancez le serveur dans votre console:<br /> <code>go run main.go</code></p> <p>Remarque: si vous tentez d'accéder à une page non définie dans les routes comme <a href="http://localhost:3000/42">http://localhost:3000/42</a>, vous êtes automatiquement redirigé vers la page d'accueil mais avec un code 200.</p> <h2>Mise en place du templating</h2> <h3>Templating simple</h3> <p>Dans le répertoire de votre projet, créez un nouveau dossier que vous nommez &quot;views&quot; et ajoutez-y les deux fichiers de templating suivant &quot;home.html&quot;:</p> <pre><code class="language-markup">&lt;!-- views/home.html --&gt; &lt;!DOCTYPE html&gt; &lt;html&gt; &lt;head&gt; &lt;title&gt;{{ .Name }}&lt;/title&gt; &lt;/head&gt; &lt;body&gt; &lt;h1&gt;{{ .Name }}&lt;/h1&gt; &lt;ul&gt; {{ range $content := .Content }} &lt;li&gt;{{ $content }}&lt;/li&gt; {{end}} &lt;/ul&gt; &lt;p&gt;&lt;a href="about"&gt;About&lt;/a&gt;&lt;/p&gt; &lt;/body&gt; &lt;/html&gt;</code></pre> <p>et &quot;about.html&quot; :</p> <pre><code class="language-markup">&lt;!-- views/about.html --&gt; &lt;!DOCTYPE html&gt; &lt;html&gt; &lt;head&gt; &lt;title&gt;{{ .Name }}&lt;/title&gt; &lt;/head&gt; &lt;body&gt; &lt;h1&gt;{{ .Name }}&lt;/h1&gt; &lt;p&gt; Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. &lt;/p&gt; &lt;p&gt;&lt;a href="/"&gt;Retour sur la home&lt;/a&gt;&lt;/p&gt; &lt;/body&gt; &lt;/html&gt;</code></pre> <p>On va devoir utiliser la librairie dédiée au templating <strong>html/template</strong> et ajouter une structure &quot;Info&quot; dans le fichier &quot;main.go&quot;:</p> <pre><code class="language-go">// main.go package main import ( "html/template" "log" "net/http" ) type Info struct { Name string Content []string } func main() { port := ":3000" log.Println("Starting Web Server 127.0.0.1" + port) http.HandleFunc("/", IndexHandler) http.HandleFunc("/about", AboutHandler) err := http.ListenAndServe(port, nil) if err != nil { log.Fatal("ListenAndServe: ", err) } } func IndexHandler(w http.ResponseWriter, r *http.Request) { info := Info{"Welcome", []string{"a content", "another content"}} tmpl, err := template.ParseFiles("views/home.html") if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) log.Printf(err.Error()) } tmpl.Execute(w, info) log.Printf("Connection to %v%v - %v \n", r.Host, r.URL, http.StatusOK) } func AboutHandler(w http.ResponseWriter, r *http.Request) { info := Info{"About", nil} tmpl, err := template.ParseFiles("views/about.html") if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) log.Printf(err.Error()) } tmpl.Execute(w, info) log.Printf("Connection to %v%v - %v \n", r.Host, r.URL, http.StatusOK) }</code></pre> <p>La gestion des erreurs permet d'afficher une erreur sur le navigateur (mais aussi dans la console du serveur).<br /> Si le dossier n'existe pas:<br /> <code>open views/home.html: Le chemin d’accès spécifié est introuvable.</code><br /> Si le fichier renseigné n'est pas bon:<br /> <code>open views/home.html: Le fichier spécifié est introuvable.</code></p> <p>Pour éviter de répéter du code, on met en place une fonction <strong>TemplateMe</strong> pour générer le templating en appelant en paramètres le fichier de la vue et les informations correspondant à la page concernée:</p> <pre><code class="language-go">// main.go package main import ( "html/template" "log" "net/http" ) type Info struct { Name string Content []string } func main() { port := ":3000" log.Println("Starting Web Server 127.0.0.1" + port) http.HandleFunc("/", IndexHandler) http.HandleFunc("/about", AboutHandler) http.ListenAndServe(":3000", nil) err := http.ListenAndServe(port, nil) if err != nil { log.Fatal("ListenAndServe: ", err) } } func IndexHandler(w http.ResponseWriter, r *http.Request) { info := Info{"Welcome", []string{"a content", "another content"}} TemplateMe(w, r, "views/home", info) } func AboutHandler(w http.ResponseWriter, r *http.Request) { info := Info{"About", nil} TemplateMe(w, r, "views/about", info) } func TemplateMe(w http.ResponseWriter, r *http.Request, page string, info interface{}) { tmpl, err := template.ParseFiles(page+".html") if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) log.Printf(err.Error()) } tmpl.Execute(w, info) log.Printf("Connection to %v%v - %v \n", r.Host, r.URL, http.StatusOK) }</code></pre> <p>On en profite également pour ne pas avoir à mettre l'extension &quot;html&quot; à chaque appel de la fonction <strong>TemplateMe</strong>...</p> <h3>Templating avec layout</h3> <p>Toujours dans l'optique de gagner du temps et de la flexibilité dans le code des vues, on met en place un système de templating.<br /> Dans le dossier &quot;views&quot;, créez un nouveau fichier &quot;layout.html&quot; :</p> <pre><code class="language-markup">&lt;!-- views/layout.html --&gt; {{ define "layout" }} &lt;!DOCTYPE html&gt; &lt;html&gt; &lt;head&gt; &lt;title&gt;{{ .Name }}&lt;/title&gt; &lt;/head&gt; &lt;body&gt; &lt;h1&gt;{{ .Name }}&lt;/h1&gt; {{ template "content" . }} &lt;p&gt;Powered by Golang&lt;/p&gt; &lt;/body&gt; &lt;/html&gt; {{ end }}</code></pre> <p><code>{{ template "content" . }}</code> va chercher directement dans les fichiers templates concernés (ci-dessous).</p> <p>Editez vos deux fichiers de vue &quot;home.html&quot; :</p> <pre><code class="language-markup"> &lt;!-- views/home.html --&gt; {{ define "content" }} &lt;ul&gt; {{ range $content := .Content }} &lt;li&gt;{{ $content }}&lt;/li&gt; {{ end }} &lt;/ul&gt; &lt;p&gt;&lt;a href="/"&gt;About&lt;/a&gt;&lt;/p&gt; {{ end }}</code></pre> <p>et &quot;about.html&quot; :</p> <pre><code class="language-markup"> &lt;!-- views/about.html --&gt; {{ define "content" }} &lt;p&gt; Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.a &lt;/p&gt; &lt;p&gt;&lt;a href=""&gt;Retour sur la home&lt;/a&gt;&lt;/p&gt; {{ end }}</code></pre> <p>On va également en profiter pour ajouter un dossier &quot;static&quot; à la racine de notre projet. Celui-ci contiendra les éléments tels que les fichiers CSS, Javascript, images, etc... Pour cela, on va lister ce dossier dans le fichier &quot;main.go&quot;. On modifie également la fonction <code>IndexHandler</code> pour activer une erreur 404 (et non un code 200):</p> <pre><code class="language-go">// main.go package main import ( "html/template" "log" "net/http" ) type Info struct { Name string Content []string } func main() { port := ":3000" log.Println("Starting Web Server 127.0.0.1" + port) http.HandleFunc("/", IndexHandler) http.HandleFunc("/about", AboutHandler) static_folder := http.FileServer(http.Dir("static")) http.Handle("/static/", http.StripPrefix("/static/", static_folder)) err := http.ListenAndServe(port, nil) if err != nil { log.Fatal("ListenAndServe: ", err) } } func IndexHandler(w http.ResponseWriter, r *http.Request) { if r.URL.Path == "/" { info := Info{"Welcome", []string{"a content", "another content"}} TemplateMe(w, r, "views/home", 200, info) } else { info := Info{"Welcome", nil} TemplateMe(w, r, "views/home", 404, info) } } func AboutHandler(w http.ResponseWriter, r *http.Request) { info := Info{"About", nil} TemplateMe(w, r, "views/about", 200, info) } func TemplateMe(w http.ResponseWriter, r *http.Request, page string, status int, info interface{}) { tmpl, err := template.ParseFiles("views/layout.html", page+".html") if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) log.Printf(err.Error()) } if status == 200 { w.WriteHeader(http.StatusOK) log.Printf("Connection to %v%v - %v \n", r.Host, r.URL, http.StatusOK) } else { w.WriteHeader(http.StatusNotFound) log.Printf("Connection to %v%v - %v \n", r.Host, r.URL, http.StatusNotFound) } tmpl.ExecuteTemplate(w, "layout", info) }</code></pre> <p>Dans le dossier &quot;views&quot;, ajoutez le fichier &quot;404.html&quot; :</p> <pre><code class="language-markup"> &lt;!-- views/404.html --&gt; {{ define "content" }} &lt;p&gt;&lt;a href="/"&gt;Back to the home&lt;/a&gt;&lt;/p&gt; {{ end }}</code></pre> <h2>Création d'un serveur avec un contrôleur global</h2> <p>On va libérer de la place dans le fichier &quot;main.go&quot; en créant un contrôleur correspondant à nos deux routes.<br /> Pour cela créez, à la racine du projet, un nouveau dossier intitulé &quot;controllers&quot; accompagné à l'intérieur d'un nouveau fichier (provisoire par la suite) &quot;controller.go&quot;:</p> <pre><code class="language-go">// controllers/controller.go package controller import ( "html/template" "log" "net/http" ) type Info struct { Name string Content []string } func IndexHandler(w http.ResponseWriter, r *http.Request) { if r.URL.Path == "/" { info := Info{"Welcome", []string{"a content", "another content"}} TemplateMe(w, r, "views/home", 200, info) } else { info := Info{"404", nil} TemplateMe(w, r, "views/404", 404, info) } } func AboutHandler(w http.ResponseWriter, r *http.Request) { info := Info{"About", nil} TemplateMe(w, r, "views/about", 200, info) } func TemplateMe(w http.ResponseWriter, r *http.Request, page string, status int, info interface{}) { tmpl, err := template.ParseFiles("views/layout.html", page+".html") if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) log.Printf(err.Error()) } if status == 200 { w.WriteHeader(http.StatusOK) log.Printf("Connection to %v%v - %v \n", r.Host, r.URL, http.StatusOK) } else { w.WriteHeader(http.StatusNotFound) log.Printf("Connection to %v%v - %v \n", r.Host, r.URL, http.StatusNotFound) } tmpl.ExecuteTemplate(w, "layout", info) } </code></pre> <p>Remarque: il est important de nommer les fonctions avec une majuscule au début, sinon la fonction ne pourra pas être lu depuis un autre fichier (&quot;main.go&quot; dans notre cas).</p> <p>Revenons à notre fichier &quot;main.go&quot;:</p> <pre><code class="language-go">// main.go package main import ( "log" "net/http" "myserver/controllers" ) type Info struct { Name string Content []string } func main() { port := ":3000" log.Println("Starting Web Server 127.0.0.1" + port) http.HandleFunc("/", indexHandler) http.HandleFunc("/about", aboutHandler) static_folder := http.FileServer(http.Dir("static")) http.Handle("/static/", http.StripPrefix("/static/", static_folder)) err := http.ListenAndServe(port, nil) if err != nil { log.Fatal("ListenAndServe: ", err) } } func indexHandler(w http.ResponseWriter, r *http.Request) { controller.IndexHandler(w, r) } func aboutHandler(w http.ResponseWriter, r *http.Request) { controller.AboutHandler(w, r) }</code></pre> <p>On y voit déjà plus clair dans ce fichier :)<br /> <img src="http://i.giphy.com/Qyrja9VbIgOre.gif" alt="" /></p> <h2>Notre 1er modèle et réforme du contrôleur global</h2> <p>Le modèle va contenir la structure &quot;Info&quot; qui sera ensuite appelée dans nos deux contrôleurs. Oui car on va on également, diviser <del>pour mieux régner</del> en deux notre controleur en restant sur <code>package controller</code> (et par la même occasion, se débarasser du fichier &quot;controller.go&quot;).</p> <p>Dans le dossier de votre projet, créez à la racine un nouveau dossier que vous nommez &quot;models&quot; puis un nouveau fichier &quot;info.go&quot;:</p> <pre><code class="language-go">// models/info.go package models type Info struct { Name string Content []string }</code></pre> <p>Dans un nouveau dossier &quot;helpers&quot;, créez un fichier &quot;helper.go&quot; pour y stocker notre fonction de templating <strong>TemplateMe</strong>:</p> <pre><code class="language-go">// helpers/helper.go package helpers import ( "html/template" "log" "net/http" ) func TemplateMe(w http.ResponseWriter, r *http.Request, page string, status int, info interface{}) { tmpl, err := template.ParseFiles("views/layout.html", page+".html") if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) log.Printf(err.Error()) } if status == 200 { w.WriteHeader(http.StatusOK) log.Printf("Connection to %v%v - %v \n", r.Host, r.URL, http.StatusOK) } else { w.WriteHeader(http.StatusNotFound) log.Printf("Connection to %v%v - %v \n", r.Host, r.URL, http.StatusNotFound) } tmpl.ExecuteTemplate(w, "layout", info) }</code></pre> <p>Ensuite, nos deux contrôleurs en commencant par &quot;home.go&quot;:</p> <pre><code class="language-go">// controllers/home.go package controller import ( "net/http" "myserver/helper" "myserver/models" ) func IndexHandler(w http.ResponseWriter, r *http.Request) { info := &amp;models.Info{} if r.URL.Path == "/" { info.Name = "Welcome" info.Content = []string{"a content", "another content"} helpers.TemplateMe(w, r, "views/home", 200, info) } else { info.Name = "404" helpers.TemplateMe(w, r, "views/404", 404, info) } }</code></pre> <p>Et en terminant par &quot;about.go&quot;:</p> <pre><code class="language-go">// controllers/about.go package controller import ( "net/http" "myserver/helpers" "myserver/models" ) func AboutHandler(w http.ResponseWriter, r *http.Request) { info := &amp;models.Info{} info.Name = "About" helpers.TemplateMe(w, r, "views/about", 200, info) }</code></pre> <p>Sans oublier notre fichier &quot;main.go&quot; version allégée:</p> <pre><code class="language-go">// main.go package main import ( "log" "net/http" "myserver/controllers" ) type Info struct { Name string Content []string } func main() { port := ":3000" log.Println("Starting Web Server 127.0.0.1" + port) http.HandleFunc("/", indexHandler) http.HandleFunc("/about", aboutHandler) static_folder := http.FileServer(http.Dir("static")) http.Handle("/static/", http.StripPrefix("/static/", static_folder)) err := http.ListenAndServe(port, nil) if err != nil { log.Fatal("ListenAndServe: ", err) } } func indexHandler(w http.ResponseWriter, r *http.Request) { controller.IndexHandler(w, r) } func aboutHandler(w http.ResponseWriter, r *http.Request) { controller.AboutHandler(w, r) }</code></pre> <p>N'oubliez pas de supprimer le fichier &quot;controller.go&quot; avant de redémarrer votre serveur.</p> <h2>Conclusion</h2> <p>A ce stade, malgré notre structure MVC fonctionnelle, il n'est pas possible de déclarer des routes dynamiques. En revanche, il existe une multitude de micro framework de routage tels que Gin, Gorilla/mux, etc... qui existent pour combler cette fonctionnalité (mais aussi la gestion des erreurs HTTP) et permettent également d'alléger le code (un bon point pour les yeux).</p> <p><img src="http://i.giphy.com/ZQbON1Fr2Ada0.gif" alt="" /></p> <h2>Sources</h2> <ul> <li>Documentation officielle: <a href="https://golang.org/doc/articles/wiki">https://golang.org/doc/articles/wiki</a></li> <li>Différents types de serveur: <a href="http://www.alexedwards.net/blog/golang-response-snippets">http://www.alexedwards.net/blog/golang-response-snippets</a></li> <li>Codes HTTP sur Golang: <a href="http://www.sergiotapia.me/return-http-status-codes-using-go">http://www.sergiotapia.me/return-http-status-codes-using-go</a></li> <li>Définition de &quot;Handler&quot;: <a href="http://fr.wiktionary.org/wiki/handler">http://fr.wiktionary.org/wiki/handler</a></li> <li>GoSublime, extension indispensable sur SublimeText: <a href="https://github.com/DisposaBoy/GoSublime">https://github.com/DisposaBoy/GoSublime</a></li> <li>Changer les délimiteurs par défaut: <a href="https://medium.com/@etiennerouzeaud/change-default-delimiters-templating-with-template-delims-857938a0b661">https://medium.com/@etiennerouzeaud/change-default-delimiters-templating-with-template-delims-857938a0b661</a></li> </ul>; 2015-03-06 07:30:00 Aperçu de MongoDB https://etienner.fr/apercu-de-mongodb <p>MongoDB est un base de données orientée documents. Il appartient à la catégorie des SGBD qualifiés NoSQL (Not only SQL). Autrement dit, MongoDB n'est pas un SGBDR (Sytème de Gestion de Base de Données Relationnelles) que sont MySQL, Oracle, Postegres, etc... Les documents sont stockés au format BSON (JSON binaire). Quant aux reqûetes, elles sont effectuées en Javascript.</p> <p><img src="../assets/img/news/apercu_de_mongodb/mongodb_logo.jpg" alt="" /></p> <h2>Vocabulaire</h2> <p>Avant de commencer, il faut savoir que MongoDB n'utilise pas le mème vocabulaire que les SGBD traditionnels. Ci-dessous, un tableau pour vous y retrouver plus facilement.</p> <table> <thead> <tr> <th>SGBD</th> <th>MongoDB</th> </tr> </thead> <tbody> <tr> <td>table</td> <td>collection</td> </tr> <tr> <td>colonne (&quot;column&quot;)</td> <td>champ (&quot;field&quot;)</td> </tr> <tr> <td>ligne (&quot;row&quot;)</td> <td>document</td> </tr> <tr> <td>&quot;offset&quot;</td> <td>&quot;skip&quot;</td> </tr> </tbody> </table> <p>Ce dernier terme est souvent utilisé pour les paginations.</p> <h2>Installation</h2> <p>Rendez-vous sur le site officiel de MongoDB et installez la version adéquate pour votre système d'exploitation : <a href="http://www.mongodb.org/downloads">http://www.mongodb.org/downloads</a><br /> Puis, suivez les instructions d'installation : <a href="http://docs.mongodb.org/manual/installation">http://docs.mongodb.org/manual/installation</a><br /> Une fois installé correctement, allez dans la ligne de console et tapez la commande suivante :<br /> <code>mongo</code></p> <p>Pour afficher les bases de données disponibles :<br /> <code>show dbs</code></p> <h2>Création de la base de données</h2> <p>On créé notre BDD de test &quot;Test&quot; <del>car c'est original</del> :<br /> <code>use Test</code><br /> Cette fonction permet de créer une BDD si elle n'existe pas. Si elle est déja existante, de se placer à l'intérieur.</p> <p>A noter : vous pouvez vous connecter directement sur votre BDD lors de la connexion à Mongo :<br /> <code>mongo test</code></p> <h2>Insertion des données</h2> <p>On établit notre première collection &quot;Users&quot; lors de l'insertion des données dans la commande ci-dessous :</p> <pre><code class="language-javascript">db.Users.insert( { firstname: "John", lastname: "Doe" })</code></pre> <p>ou :</p> <pre><code class="language-javascript">db.Users.save( { firstname: "John", lastname: "Doe" })</code></pre> <ul> <li><code>db</code> : on se place dans la base utilisée Test&quot; (tapez <code>db</code> dans votre ligne de console, MongoDB affiche &quot;Test&quot;).</li> <li><code>Users</code> : le nom de notre future collection.</li> <li><code>insert</code> ou <code>save</code> : on utilise la fonction d'insertion de données.</li> <li>Dans les accolades, on indique le nom du champs suivie de 2 points, puis on indique nos données (double ou simple quote pour une chaine de caractères).</li> </ul> <p><img src="../assets/img/news/apercu_de_mongodb/mongo_insert.jpg" alt="" /></p> <p>Remarque : lors de l'insertion des données un id est généré automatiquement. Cet id est nommé &quot;_id&quot;, de type <strong>ObjectId</strong>. C'est une chaine de caractère en héxadécimal.</p> <p>On peut lister toutes les collections disponibles dans notre base avec :<br /> <code>show collections</code></p> <p>Afin de pouvoir procéder à des tests plus approfondis par la suite, on insère plus de données dans notre collection :</p> <pre><code class="language-javascript">db.Users.insert( {firstname: "Valdez", lastname: "Tatiana", birthday: "1990-07-05", country: "Liberia"} ); db.Users.insert( {firstname: "Hayes", lastname: "Willow", birthday: "1990-09-18", country: "Luxembourg"} ); db.Users.insert( {firstname: "Ward", lastname: "Hanae", birthday: "1990-07-30", country: "Egypt"} ); db.Users.insert( {firstname: "Cleveland", lastname: "Jescie", birthday: "1990-05-20", country: "Montenegro"} ); db.Users.insert( {firstname: "Whitfield", lastname: "Britanni", birthday: "1990-10-07", country: "Belgium"} ); db.Users.insert( {firstname: "Howard", lastname: "Cecilia", birthday: "1990-11-19", country: "India"} ); db.Users.insert( {firstname: "Olson", lastname: "Xavier", birthday: "1990-06-21", country: "Venezuela"} ); db.Users.insert( {firstname: "Cardenas", lastname: "Cathleen", birthday: "1990-10-03", country: "China"} ); db.Users.insert( {firstname: "Warren", lastname: "Nelle", birthday: "1990-07-07", country: "Belarus"} ); db.Users.insert( {firstname: "Beach", lastname: "Kellie", birthday: "1990-01-17", country: "Benin"} ); db.Users.insert( {firstname: "Alford", lastname: "Tanek", birthday: "1990-10-10", country: "France"} ); db.Users.insert( {firstname: "Ryan", lastname: "Wang", birthday: "1990-08-20", country: "Gibraltar"} ); db.Users.insert( {firstname: "Shannon", lastname: "Buckminster", birthday: "1990-08-27", country: "Turkey"} ); db.Users.insert( {firstname: "Caldwell", lastname: "Willow", birthday: "1990-08-03", country: "Italy"} ); db.Users.insert( {firstname: "Brennan", lastname: "Karleigh", birthday: "1990-03-03", country: "Dominican Republic"} ); db.Users.insert( {firstname: "Howe", lastname: "Richard", birthday: "1990-09-27", country: "Japan"} ); db.Users.insert( {firstname: "Dyer", lastname: "Bernard", birthday: "1990-01-11", country: "Hong Kong"} ); db.Users.insert( {firstname: "Cooke", lastname: "Laurel", birthday: "1990-05-06", country: "Malaysia"} ); db.Users.insert( {firstname: "Pace", lastname: "Karleigh", birthday: "1990-06-24", country: "Australia"}) ; db.Users.insert( {firstname: "Price", lastname: "Grady", birthday: "1990-04-18", country: "Belgium"} );</code></pre> <p>Pour connaitre le nombre de documents présents dans la collection &quot;Users&quot; :</p> <pre><code class="language-javascript">db.Users.count()</code></pre> <p>Pour plus de détails sur cette collection :</p> <pre><code class="language-javascript">db.Users.stats()</code></pre> <p><img src="../assets/img/news/apercu_de_mongodb/mongo_count_stats.jpg" alt="" /></p> <h2>Lecture des données</h2> <p>Ci-dessous une liste non exhaustive de commandes.</p> <h3>Afficher toutes les données</h3> <pre><code class="language-javascript">db.Users.find()</code></pre> <h3>Données en particulier</h3> <pre><code class="language-javascript">db.Users.find({country: "Belgium"})</code></pre> <p><img src="../assets/img/news/apercu_de_mongodb/mongo_find.jpg" alt="" /></p> <h3>Recherche multiple</h3> <p>&quot;or&quot; :</p> <pre><code class="language-javascript">db.Users.find({$or: [ {country: "Belgium"}, {firstname: "Alford"} ]})</code></pre> <p>Retourne toutes les données dont le pays vaut &quot;Belgium&quot; ou bien où le prénom vaut &quot;Alford&quot;.</p> <p>&quot;and&quot; :</p> <pre><code class="language-javascript">db.Users.find({$and: [ {country: "Belgium"}, {birthday: "1990-10-07"} ]})</code></pre> <p>Retourne toutes les données dont le pays vaut &quot;Belgium&quot; et où la date de naissance vaut &quot;1990-10-07&quot;.</p> <p>&quot;not&quot; :</p> <pre><code class="language-javascript">db.Users.find( { country: { $ne: "Belgium" } } )</code></pre> <p>Retourne toutes les données dont le pays ne vaut pas &quot;Belgium&quot;.</p> <p>&quot;like&quot; :</p> <pre><code class="language-javascript">db.Users.find({country: /in/ })</code></pre> <p>Retourne toutes les données dont le pays contient &quot;in&quot;.</p> <pre><code class="language-javascript">db.Users.find({country: {$regex: /^be/i} })</code></pre> <p>Retourne toutes les données dont le pays commence par &quot;be&quot;.</p> <pre><code class="language-javascript">db.Users.find({country: {$regex: /ia$/} })</code></pre> <p>Retourne toutes les données dont le pays termine par &quot;ia&quot;.</p> <p>Remarque : MongoDB est sensible à la casse.</p> <h3>Limitation</h3> <p>Limitation unique (1 seul résultat) avec <code>findOne()</code></p> <pre><code class="language-javascript">db.Users.findOne({_id: ObjectId("54df6bf34403b474fc1df363")})</code></pre> <p>Affiche uniquement les données pour le document dont l'ObjectId vaut &quot;54df6bf34403b474fc1df363&quot;.</p> <pre><code class="language-javascript">db.Users.findOne({country: "Belgium"})</code></pre> <p>Retourne uniquement le 1er résultat dont le pays vaut &quot;Belgium&quot; dans la liste des documents.</p> <h3>Limitation définie</h3> <pre><code class="language-javascript">db.Users.find().limit(3)</code></pre> <p>Affiche uniquement les 3 premiers documents.</p> <h3>Offset</h3> <pre><code class="language-javascript">db.Users.find().limit(10).skip(5)</code></pre> <p>Affiche les 10 résultats après les 5 premiers documents.</p> <h3>Ordre d'affichage</h3> <p>Croissant :</p> <pre><code class="language-javascript">db.Users.find().sort({country: 1})</code></pre> <p>Affiche tous les résultats de la collection &quot;Users&quot; en classant les données pas pays en ordre alphabétique croissant.</p> <p>Décroissant :</p> <pre><code class="language-javascript">db.Users.find().sort({country: -1})</code></pre> <p>Affiche tous les résultats de la collection &quot;Users&quot; en classant les données pas pays en ordre alphabétique décroissant.</p> <h2>Modification des données</h2> <p>On choisit notre document à modifier :</p> <pre><code class="language-javascript">db.Users.update({_id: ObjectId("54df6bf54403b474fc1df374")}, { $set: {firstname: "Martine"} })</code></pre> <p>On peut également choisir plusieurs champs :</p> <pre><code class="language-javascript">db.Users.update({firstname: "Pace", lastname: "Karleigh"}, {$set: {country: "England"}})</code></pre> <h2>Suppression des données</h2> <p>Un document en particulier :</p> <pre><code class="language-javascript">db.Users.remove({_id: ObjectId("54f6342cc6dd3b2aef142568")})</code></pre> <p>Plusieurs documents avec un opérateur logique :</p> <pre><code class="language-javascript">db.Users.remove({$or: [ {firstname: "Alford"}, {lastname: "Laurel"} ]})</code></pre> <p>Supprimer toute la collection :</p> <pre><code class="language-javascript">db.Users.drop()</code></pre> <h2>Un peu de relationnel</h2> <h3>Relation 0,N ou 1,N</h3> <p>Une relation 1,N est définie par le fait qu'une collection possède une clef étrangère dans un champ qui peut se retrouver dans un ou plusieurs document d'une autre collection.<br /> Dans l'exemple suivant, on va mettre en relation des <strong>articles</strong> avec une <strong>rubrique</strong>. Un <strong>article</strong> appartient à une et une seul <strong>rubrique</strong>. A l'inverse, une <strong>rubrique</strong> appartient à un ou plusieurs <strong>articles</strong>.</p> <p>On créé d'abord notre collection <strong>rubrique</strong> :</p> <pre><code class="language-javascript">db.rubric.insert( {rubric_name: "MongoDB"} ) db.rubric.insert( {rubric_name: "MySQL"} )</code></pre> <p>L'inseration renvoie l'ObjectId suivant &quot;54c4dfa24b4453f4eb85f70b&quot; pour &quot;MongoDB&quot; et &quot;54c4dfa24b4453f4eb85f70c&quot; pour &quot;MySQL&quot; que l'on va avoir besoin pour l'injecter comme clef étrangère dans la seconde collection <strong>article</strong> :</p> <pre><code class="language-javascript">db.article.insert( { article_title: "Mon premier article sur MongoDB", article_content: "Lorem Ipsum 1", rubric_id: ObjectId("54c4dfa24b4453f4eb85f70b") }); db.article.insert( { article_title: "Mon premier article sur MySQL", article_content: "Lorem Ipsum 2", rubric_id: ObjectId("54c4dfa24b4453f4eb85f70c") });</code></pre> <p>Contrairement à un SGBDR qui utilise un système de jointures dans une seule requête, sur MongoDB, il faudra effectuer 2 requetes. Une première pour afficher les infos de la <strong>rubrique</strong> afin de récupérer son ObjectId et une seconde pour afficher les <strong>articles</strong> concernés.</p> <p>Les informations sur la <strong>rubrique</strong> :</p> <pre><code class="language-javascript">db.rubric.findOne({_id: ObjectId("54c4dfa24b4453f4eb85f70b")})</code></pre> <p>Les <strong>articles</strong> de cette utilisateur via son id :</p> <pre><code class="language-javascript">db.article.find({rubric_id: ObjectId("54c4dfa24b4453f4eb85f70b")})</code></pre> <p>A noter : en cas de suppression d'une <strong>rubrique</strong>, cela ne supprimera pas le champ &quot;rubric_id&quot; présent dans la collection <strong>article</strong> (pas de &quot;Cascade delete&quot;).</p> <h3>Relation M,N (Many to Many)</h3> <p>Un peu plus complexe, une relation M,N est un cas dans laquel, une collection doit stocker plusieurs clefs étrangères au minium de 2 autres collections.<br /> Prenons en exemple, un système de messagerie quelconque, composé de 3 collections.<br /> Un <strong>utilisateur</strong> peut converser avec 1 ou plusieurs autre(s) <strong>utilisateur(s)</strong> en créant une <strong>conversation</strong> unique composée d'un ou plusieur(s) <strong>message(s)</strong>.<br /> On commence par la collection la plus simple, celle de l'<strong>utilisateur</strong> :</p> <pre><code class="language-javascript">db.user.insert( { name_user: "User1" }) db.user.insert( { name_user: "User2" })</code></pre> <p>Cette dernière renvoie les ObjectId : &quot;54cd613880ae1a574dc812d2&quot; et &quot;54cd613880ae1a574dc812d3&quot;.<br /> On continue avec la seconde collection concerant la <strong>conversation</strong> entre 2 (ou plus) <strong>utilisateurs</strong> en stockant les ObjectId des <strong>utilisateurs</strong> concernés.</p> <pre><code class="language-javascript">db.conversation.insert( { users_id: [ {user_id: ObjectId("54cd613880ae1a574dc812d2")}, {user_id: ObjectId("54cd613d80ae1a574dc812d3")} ] })</code></pre> <p>Et, on fini par la fameuse collection qui va accueillir les 2 clefs étrangères des 2 collections créez ci-dessus. C'est la collection pour stocker les <strong>messages</strong> de l'<strong>utilisateur</strong> dans la <strong>conversation</strong> dédiée.</p> <pre><code class="language-javascript">db.message.insert( { content_message: "Mon premier message de User1", user_id: ObjectId("54cd613880ae1a574dc812d2"), conversation_id: ObjectId("54cd627f80ae1a574dc812d4") }) db.message.insert( { content_message: "Mon premier message de User2", user_id: ObjectId("54cd613d80ae1a574dc812d3"), conversation_id: ObjectId("54cd627f80ae1a574dc812d4") }) db.message.insert( { content_message: "Mon second message de User2", user_id: ObjectId("54cd613d80ae1a574dc812d3"), conversation_id: ObjectId("54cd627f80ae1a574dc812d4") })</code></pre> <h2>Exporter</h2> <h3>Mongodump</h3> <p>Avec Mongodump vous pouvez sauvegarder toutes vos bases et collections aux formats BSON et JSON.<br /> Dans votre console, tapez :<br /> <code>mongodbump</code><br /> Un dossier &quot;dump&quot; sera créé à l'emplacement de votre invité de commandes.<br /> On veut seulement exporter une base de donnée, &quot;test&quot; :<br /> <code>mongodump --db test</code><br /> ou<br /> <code>mongodump -d test</code><br /> Ou plus précisement, une collection :<br /> <code>mongodump --db test --collection Users</code><br /> ou<br /> <code>mongodump -d test -c User</code><br /> Il est également possible d'indiquer le répertoire d'extraction en ajoutant le paramètre <code>--out</code> ou <code>-o</code>.</p> <h3>Mongoexport</h3> <p>Contrairement à Mongodump, Mongoexport permet d'exporter une collection à la fois, dans un fichier JSON.<br /> <code>mongoexport --db test --collection Users --out Users.json</code><br /> Il est possible d'indiquer le répertoire d'extraction en ajoutant le paramètre <code>--dpath</code>.<br /> Pour plus d'options, tapez <code>mongoexport</code>.</p> <h2>Importer</h2> <h3>Mongorestore</h3> <p>Permet de restaurer l'ensemble des fichiers sauvegardés avec Mongodump.</p> <h3>Mongoimport</h3> <p>Permet d'importer des données d'une collection dans une base de donnée existante ou non :<br /> <code>mongoimport -d test2 -c Users --file Users.json</code><br /> Si vous insérez ou modifier des données sur des documents déja existants dans votre collection, ajoutez le paramètre <code>--upsert</code>.<br /> Pour plus d'options, tapez <code>mongoimport</code>.</p> <h2>Bonus</h2> <p>Afficher vos données au format JSON non formaté :</p> <pre><code class="language-javascript">db.Users.find().pretty()</code></pre> <h2>Conclusion</h2> <p>MongoDB est une base de données NoSQL légère, rapide et simple à prendre en main pour vos futures applications. Pour aller <del>beaucoup</del> plus loin sur Mongo, consultez la documentation officielle (voir liens ci-dessous) pour notamment des requêtes plus complexe et sécuriser l'accès à la base de données (en production).</p> <h2>Sources</h2> <ul> <li>Documentation officielle : <a href="http://docs.mongodb.org/manual">http://docs.mongodb.org/manual</a></li> <li>Liste des commandes pour travailler dans les collections : <a href="http://docs.mongodb.org/manual/reference/method/js-collection">http://docs.mongodb.org/manual/reference/method/js-collection</a></li> <li>Comparaison entre le SQL et MongoDB : <a href="http://docs.mongodb.org/manual/reference/sql-comparison">http://docs.mongodb.org/manual/reference/sql-comparison</a></li> <li><a href="http://docs.mongodb.org/manual/reference/command">http://docs.mongodb.org/manual/reference/command</a></li> <li>Robomongo est une interface graphique : <a href="http://robomongo.org">http://robomongo.org</a></li> <li>Traducteur de requête SQL vers MongoDB : <a href="http://www.querymongo.com">http://www.querymongo.com</a></li> </ul>; 2015-03-04 09:00:00 Mise en place du CRUD sur Beego (Partie 3) https://etienner.fr/mise-en-place-du-crud-sur-beego-partie-3 <p>Maintenant que l'on sait se connecter à une BDD, on va pouvoir s'attaquer au CRUD (Create Read Update Delete). Concrètement, on va pouvoir lire, créer, éditer et supprimer des données à travers un mini blog très basique. Ce tutoriel va vous montrer comment mettre en place un système de contenus d'articles afin de vous servir de base pour vos futurs projets sur Beego. </p> <h2>Templating</h2> <p>Avant d'attaquer le côté purement Golang, on va se contenter de préparer le terrain coté front.<br /> Le framework CSS utilisé est Materialize (<a href="http://materializecss.com">http://materializecss.com</a>).<br /> Dans le dossier &quot;views&quot;, créez un dossier &quot;blog&quot;.<br /> Créez un nouveau fichier que vous nommez &quot;layout_blog.tpl&quot; :</p> <pre><code class="language-markup">&lt;!DOCTYPE html&gt; &lt;html&gt; &lt;head&gt; &lt;title&gt;{{.title}}&lt;/title&gt; &lt;link rel="stylesheet" href="/static/css/materialize.min.css"&gt; &lt;/head&gt; &lt;body&gt; &lt;nav class="nav-wrapper light-blue darken-4"&gt; &lt;a href="/blog" class="brand-logo"&gt;My blog&lt;/a&gt; &lt;ul id="nav-mobile" class="right side-nav"&gt; &lt;li&gt; &lt;a href="/blog/add"&gt;Add an article&lt;/a&gt; &lt;/li&gt; &lt;/ul&gt; &lt;a class="button-collapse" href="#" data-activates="nav-mobile"&gt; &lt;i class="mdi-navigation-menu"&gt;&lt;/i&gt; &lt;/a&gt; &lt;/nav&gt; &lt;br /&gt;&lt;!-- Not pretty :( --&gt; &lt;div class="container"&gt; &lt;div class="row"&gt; {{if .flash.notice}} &lt;div id="toast-container"&gt; &lt;div class="toast"&gt;{{.flash.notice}}&lt;/div&gt; &lt;/div&gt; {{end}} {{.LayoutContent}} &lt;/div&gt; &lt;div class="valign-demo valign-wrapper"&gt; Powered by Beego (a Go framework) &lt;/div&gt; &lt;/div&gt; &lt;script src="http://code.jquery.com/jquery-2.1.3.min.js"&gt;&lt;/script&gt; &lt;script src="/static/js/materialize.min.js"&gt;&lt;/script&gt; &lt;script&gt; $(document).ready(function(){ $("input[type='text']") .addClass("validate") .wrapAll('&lt;div class="input-field"&gt;&lt;/div&gt;'); $("textarea") .addClass("materialize-textarea validate") .wrapAll('&lt;div class="input-field"&gt;&lt;/div&gt;'); $(".button-collapse").sideNav({edge: "left"}); }); &lt;/script&gt; &lt;/body&gt; &lt;/html&gt;</code></pre> <p>Mais à quoi sert <code>{{.LayoutContent}}</code> ?<br /> À appeler le layout des pages concernées depuis le contrôleur. Dans notre cas, cela va correspondre aux futures pages ci-dessus. </p> <p>Un second fichier template &quot;listing.tpl&quot; :</p> <pre><code class="language-markup"> {{range $article := .articles}} &lt;article class="row"&gt; &lt;div class="col s12 m12"&gt; &lt;div class="card brown blue lighten-5"&gt; &lt;div class="card-content"&gt; &lt;p&gt;Created {{date $article.Created "Y-m-d H:i:s"}}&lt;/p&gt; &lt;span class="card-title black-text"&gt;{{$article.Title}}&lt;/span&gt; &lt;p&gt;{{substr $article.Content 0 400 | str2html}}&lt;/p&gt; &lt;/div&gt; &lt;div class="card-action"&gt; &lt;a class="blue-text" href="/blog/article/{{$article.Id}}"&gt;Read the next&lt;/a&gt; &lt;a class="blue-text" href="/blog/edit/{{$article.Id}}"&gt;Edit&lt;/a&gt; &lt;a class="blue-text" href="/blog/delete/{{$article.Id}}"&gt;Delete&lt;/a&gt; &lt;/div&gt; &lt;/div&gt; &lt;/div&gt; &lt;/article&gt; {{end}}</code></pre> <p>On utilise dans ce layout, 4 fonctions de templating :</p> <ul> <li><code>range</code> : boucle des données à partir d'un tableau (à l'image de <code>foreach</code> en PHP).</li> <li><code>date</code> : permet de modifier l'écriture de la date.</li> <li><code>substr 0 400</code> : limite le nombre de caractères à 400.</li> <li><code>str2html</code> : interpréte les balises HTML comme du HTML.</li> </ul> <p>Un troisième pour afficher le contenu d'un article &quot;content.tpl&quot; :</p> <pre><code class="language-markup"> &lt;h1&gt;{{.title}}&lt;/h1&gt; &lt;p&gt;{{.content | str2html}}&lt;/p&gt; &lt;a href="/blog/" class="waves-effect waves-light btn"&gt;Back to articles&lt;/a&gt;</code></pre> <p>Un quatrième pour afficher le formulaire &quot;form.tpl&quot; :</p> <pre><code class="language-markup"> {{range $error := .errors}} {{$error.Key}} {{$error.Message}} {{end}} &lt;div class="row"&gt; &lt;form id="articles" method="POST"&gt; {{.Form | renderform}} &lt;button type="submit" class="waves-effect waves-light btn"&gt;Send&lt;/button&gt; &lt;/form&gt; &lt;/div&gt;</code></pre> <p>Un cinquième et dernier pour afficher d'éventuelles erreurs &quot;error.tpl&quot; :</p> <pre><code class="language-go"> &lt;h1&gt;{{.error}}&lt;/h1&gt;</code></pre> <h2>Préparation du nouveau contrôleur</h2> <p>Dans le dossier &quot;controllers&quot;, créez un nouveau contrôleur que vous nommez &quot;blog.go&quot; ayant pour entête :</p> <pre><code class="language-go">package controllers import ( "github.com/astaxie/beego" "github.com/astaxie/beego/orm" models "myapp/models" "github.com/astaxie/beego/validation" "strconv" ) type BlogController struct { beego.Controller }</code></pre> <p>On appelle bien le modèle ainsi que l'ORM et la validation pour le formulaire d'ajout ou de modification d'article.</p> <h3>Afficher tous les articles</h3> <p>Dans un premier temps, on va se contenter de lister toutes les données présentent dans la table &quot;articles&quot; :</p> <pre><code class="language-go">func (this *MainController) Get() { flash := beego.ReadFromRequest(&amp;this.Controller) if _,ok:=flash.Data["notice"];ok{ } o := orm.NewOrm() o.Using("default") var articles []*models.Articles num, err := o.QueryTable("Articles").OrderBy("-id").All(&amp;articles) if err != orm.ErrNoRows &amp;&amp; num &gt; 0 { this.TplNames = "blog/listing.tpl" this.Data["articles"] = articles } else { // No result this.TplNames = "blog/error.tpl" this.Data["error"] = "No article in the database" } this.Data["title"] = "My blog" this.Layout = "blog/layout_blog.tpl" }</code></pre> <p>Avant de charger l'ORM, on initialise la variable <code>flash</code> où seront stockés les messages de succès dans les futures fonctions concernées.<br /> Puis, on charge la structure de la table &quot;articles&quot;. On fait notre requête SQL via l'ORM en prenant soin d'afficher les articles par ordre décroissant.<br /> Et pour finir, on charge le layout (rappelez-vous de <code>{{.LayoutContent}}</code> dans le fichier &quot;layout_blog.tpl&quot;) et son template associé. </p> <p>On appel cette fonction dans le fichier de routage (&quot;router/router.go&quot;) :</p> <pre><code class="language-go">beego.Router("/blog", &amp;controllers.BlogController{},"get:Get")</code></pre> <p><a href="http://localhost:8080/blog">http://localhost:8080/blog</a><br /> Par défaut, votre table est vide, ce qui affichera donc le message d'erreur.<br /> <img src="../assets/img/news/mise-en-place-du-crud-sur-beego/beego_no_article.jpg" alt="" /></p> <h3>Afficher un article en particulier</h3> <p>Dans notre contrôleur &quot;blog.go&quot;, à la suite :</p> <pre><code class="language-go">func (this *MainController) GetOne() { o := orm.NewOrm() o.Using("default") // Get the ID page articlesId := this.Ctx.Input.Param(":id") var articles []*models.Articles err := o.QueryTable("articles").Filter("id", articlesId).One(&amp;articles) if err == orm.ErrNoRows { // No result this.TplNames = "blog/error.tpl" this.Data["title"] = "Error :(" this.Data["error"] = "No available article" } else { this.TplNames = "blog/content.tpl" for _, data := range articles { this.Data["title"] = data.Title this.Data["content"] = data.Content } } this.Layout = "blog/layout_blog.tpl" }</code></pre> <p>Même principe que pour lister tous les articles, excepté que l'on récupère l'ID de l'article pour l'insérer dans la requête SQL.</p> <p>Puis dans le fichier de routage (&quot;router/router.go&quot;), on ajoute la route suivante :</p> <pre><code class="language-go">beego.Router("/blog/article/:id([0-9]+)", &amp;controllers.BlogController{}, "get:GetOne")</code></pre> <p><a href="http://localhost:8080/blog/article/1">http://localhost:8080/blog/article/1</a></p> <p>A noter : dans la fonction, on a mis en place un message personnalisé dans le cas où un article n'existe pas ou n'a jamais existé. Si vous souhaitez afficher la page 404 par défaut, à la place du code existant dans la condition placez ceci : <code>this.Abort(&quot;404&quot;)</code>.</p> <h3>Insérer un article</h3> <p>On reste dans le même contrôleur :</p> <pre><code class="language-go"> o := orm.NewOrm() o.Using("default") articles := models.Articles{} this.Data["Form"] = &amp;articles if err := this.ParseForm(&amp;articles); err != nil { beego.Error("Couldn't parse the form. Reason: ", err) } else { valid := validation.Validation{} valid.Required(articles.Title, "Title") valid.Required(articles.Content, "Content") isValid, _ := valid.Valid(articles) if this.Ctx.Input.Method() == "POST" { if !isValid { this.Data["errors"] = valid.ErrorsMap for _, err := range valid.Errors { beego.Error(err.Key, err.Message) } } else { _, err := o.Insert(&amp;articles) flash := beego.NewFlash() if err == nil { flash.Notice("Article "+ articles.Title +" added") flash.Store(&amp;this.Controller) this.Redirect("/blog", 302) } else { beego.Debug("Couldn't insert new article. Reason: ", err) } } } } this.Layout = "blog/layout_blog.tpl" this.TplNames = "blog/form.tpl" this.Data["title"] = "Add an article"</code></pre> <p>On fait appel à la bibliothèque (&quot;validation&quot;), celle appelée dans les imports de notre contrôleur (&quot;blog.go&quot;). On met donc en place une validation sur nos 2 champs : &quot;Title&quot; et &quot;Content&quot;. Si ces 2 derniers sont bien renseignés par l'utilisateur, on insère les données dans la BDD et on redirige l'utilisateur vers la page des articles sinon on affiche un message d'erreur.<br /> On n'oublie pas de stocker notre message de &quot;succès&quot; en mémoire (malgré la redirection 302) dans <code>Flash.Notice</code>, cela afin d'avertir l'utilisateur que l'insertion a bien été effectué.</p> <p>Info : les fonctions (facultatives) <code>beego.Error</code> et <code>beego.Debug</code> permettent d'afficher une erreur dans la console du serveur.</p> <p>Dans le fichier de routage (&quot;router/router.go&quot;), on ajoute la route suivante :</p> <pre><code class="language-go">beego.Router("/blog/add", &amp;controllers.BlogController{}, "get,post:Add")</code></pre> <p><a href="http://localhost:8080/blog/add">http://localhost:8080/blog/add</a><br /> <img src="../assets/img/news/mise-en-place-du-crud-sur-beego/beego_insert.jpg" alt="" /><br /> Lorsque le formulaire est bien rempli, Beego nous redirige vers la page des articles : <img src="../assets/img/news/mise-en-place-du-crud-sur-beego/beego_after_insert.jpg" alt="" /></p> <h3>Editer un article</h3> <p>Comme pour l'insertion des données, on a besoin d'un formulaire mais avec les champs pré-remplis :</p> <pre><code class="language-go">func (this *MainController) Edit() { o := orm.NewOrm() o.Using("default") articlesId, _ := strconv.Atoi(this.Ctx.Input.Param(":id")) articles := models.Articles{} flash := beego.NewFlash() err := o.QueryTable("articles").Filter("id", articlesId).One(&amp;articles) if err != orm.ErrNoRows { this.Data["Form"] = &amp;articles if err := this.ParseForm(&amp;articles); err != nil { beego.Error("Couldn't parse the form. Reason: ", err) } else { valid := validation.Validation{} valid.Required(articles.Title, "Title") valid.Required(articles.Content, "Content") isValid, _ := valid.Valid(articles) if this.Ctx.Input.Method() == "POST" { if !isValid { this.Data["errors"] = valid.ErrorsMap beego.Error("Form didn't validate.") } else { _, err := o.Update(&amp;articles) if err == nil { flash.Notice("Article "+ articles.Title +" updated") flash.Store(&amp;this.Controller) this.Redirect("/blog", 302) } else { beego.Debug("Couldn't update new article. Reason: ", err) } } } } this.Data["title"] = "Edit this article" this.Layout = "blog/layout_blog.tpl" this.TplNames = "blog/form.tpl" } else { flash.Notice("Article #%d doesn't exists", articlesId) flash.Store(&amp;this.Controller) this.Redirect("/blog", 302) } }</code></pre> <p>On récupère l'id de l'article que l'on convertit en string via <code>strconv.Atoi</code>. Ensuite, on éxécute avec la mème logique que dans l'insertion des données, excepté la requête SQL qui diffère.</p> <p>Dans le fichier de routage (&quot;router/router.go&quot;), on ajoute la route suivante :</p> <pre><code class="language-go">beego.Router("/blog/edit/:id([0-9]+)", &amp;controllers.BlogController{}, "get,post:Edit")</code></pre> <p><a href="http://localhost:8080/blog/edit/1">http://localhost:8080/blog/edit/1</a></p> <h3>Supprimer un article</h3> <p>Dernière fonction de notre contrôleur &quot;blog.go&quot; :</p> <pre><code class="language-go">func (this *MainController) Delete() { o := orm.NewOrm() o.Using("default") articlesId, _ := strconv.Atoi(this.Ctx.Input.Param(":id")) articles := models.Articles{} flash := beego.NewFlash() if exist := o.QueryTable(articles.TableName()).Filter("Id", articlesId).Exist(); exist { if num, err := o.Delete(&amp;models.Articles{Id: articlesId}); err == nil { beego.Info("Record Deleted. ", num) flash.Notice("Article #%d deleted", articlesId) } else { beego.Error("Record couldn't be deleted. Reason: ", err) } } else { flash.Notice("Article #%d doesn't exists", articlesId) } flash.Store(&amp;this.Controller) this.Redirect("/blog", 302) }</code></pre> <p>On récupère l'id de l'article concerné. On vérifie que ce dernier existe bien dans la BDD. Si c'est le cas on le supprime et on redirige l'utilisateur sur la page des articles sinon on fait pareil mais avec un message d'avertissement différent.</p> <p>Et on n'oublie pas notre dernière route <del>pour la route</del> (&quot;router/router.go&quot;) :</p> <pre><code class="language-go">beego.Router("/blog/delete/:id([0-9]+)", &amp;controllers.MainController{}, "get:Delete")</code></pre> <p><a href="http://localhost:8080/blog/delete/1">http://localhost:8080/blog/delete/1</a><br /> <img src="../assets/img/news/mise-en-place-du-crud-sur-beego/beego_delete.jpg" alt="" /></p> <h2>Conclusion</h2> <p>Désormais, vous avez dès à présent, un système de CRUD et de templating opérationnel. Il ne manque plus qu'un espace d'authentification pour effectuer les opérations d'ajout, de modification et de suppression d'articles.</p> <h2>Sources</h2> <ul> <li>Documentation officiel de Beego : <a href="http://beego.me/docs">http://beego.me/docs</a></li> <li>Documentation complète de la bibliothèque &quot;validation&quot; : <a href="https://gowalker.org/github.com/astaxie/beego/validation">https://gowalker.org/github.com/astaxie/beego/validation</a></li> </ul>; 2015-01-20 17:00:00 Connexion MySQL avec Beego (Partie 2) https://etienner.fr/connexion-mysql-avec-beego-partie-2 <p>Nous allons dans cet article, connecter le framework à une BDD (Base De Données) de type MySQL via l'ORM (Object-Relational Mapping) fournis par Beego. Il est tout à fait possible de se servir d'un autre type de BDD comme PostgreSQL et SQlite3 (officiellement supportées, mais vous pouvez tester avec d'autres drivers de BDD).</p> <h2>Création du premier modèle</h2> <p>Dans le répertoire &quot;models&quot;, on crée un fichier que l'on nomme &quot;models.go&quot; :</p> <pre><code class="language-go">package models</code></pre> <p>Puis, on importe la bibliothèque &quot;Time&quot; :</p> <pre><code class="language-go">import( "time" )</code></pre> <p>Ensuite, on déclare la structure basique de notre future table &quot;articles&quot; :</p> <pre><code class="language-go">type Articles struct { Id int `form:"-"` Title string `form:"title" required` Content string `orm:";type(text)" form:"content,textarea"` Created time.Time `orm:"auto_now_add;type(datetime)"` Updated time.Time `orm:"auto_now;type(datetime)"` }</code></pre> <p>On précise à l'ORM que l'on veut un champ de type texte et non un champ de type &quot;varchar&quot; pour le champ &quot;content&quot;.<br /> Avec l'aide de la librairie &quot;Time&quot; native de Go, on implémente 2 champs de type &quot;datetime&quot;. </p> <p>Puis on renvoie notre structure à travers une fonction :</p> <pre><code class="language-go">func (a *Articles) TableName() string { return "articles" }</code></pre> <p>Et c'est tout pour notre unique modèle !</p> <h2>Connexion à la base de données</h2> <p>Dans cette partie, on va utiliser le fichier de configuration de Beego. Pour cela, ouvrez le fichier &quot;app.conf&quot; présent dans le dossier &quot;conf&quot;.<br /> On va ajouter 5 variables dont les valeurs seront :</p> <ul> <li>Le type de base de données (&quot;mysql&quot;, &quot;sqlite3&quot;, etc…).</li> <li>Le nom d'utilisateur de la BDD</li> <li>Le mot de passe de l'utilisateur</li> <li>L'URL du serveur de la BDD</li> <li>Le nom de la base utilisée pour ce projet</li> </ul> <pre><code class="language-markup">data_type = "mysql" data_user = "root" data_pass = "" data_urls = "127.0.0.1" data_db = "beego_test"</code></pre> <p>Sur votre BDD MySQL, créez une nouvelle base que vous nommez &quot;beego_test&quot;.</p> <p>Les cinq variables ci-dessus vont être utilisées dans le fichier &quot;main.go&quot; (présent à la racine de votre projet). Ouvrez ce fichier.</p> <pre><code class="language-go">import ( "github.com/astaxie/beego" _ "myapp/routers" models "myapp/models" "github.com/astaxie/beego/orm" _ "github.com/go-sql-driver/mysql" )</code></pre> <p>Comme vous pouvez le constater, on importe le modèle, les bibliothèques pour l'ORM de Beego et le driver de MySQL.</p> <p>Attention : le driver de MySQL en Go doit être présent dans le dossier &quot;%gopath%/src/github.com/go-sql-driver/mysql&quot;.<br /> Si ce n'est pas le cas (par défaut), lancez la commande pour l'installer :<br /> <code>go get github.com/go-sql-driver/mysql</code></p> <p>Puis, on passe à la fonction &quot;init&quot; :</p> <pre><code class="language-go">func init() { orm.RegisterDriver(beego.AppConfig.String("data_type"), orm.DR_MySQL) orm.RegisterDataBase("default", beego.AppConfig.String("data_type"), beego.AppConfig.String("mysqluser")+":"+beego.AppConfig.String("password")+"@/"+beego.AppConfig.String("mysqldb")+"?charset=utf8&amp;loc=Europe%2FParis") orm.RegisterModel(new(models.Articles)) }</code></pre> <p>Sur la première ligne, on précise l'utilisation du driver de MySQL à l'ORM puis sur la seconde, on lui indique le DSN (Data Source Name) pour pouvoir se connecter sur notre serveur de BDD et sur la troisième et dernière ligne, on charge notre modèle.</p> <p>A noter : il est notamment possible de créer plusieurs connexions sur différentes BDD avec Beego.</p> <h2>Gestion des erreurs de connexion</h2> <p>Lors du démarrage de votre serveur, on exécute une fonction qui permet d'afficher une erreur, si le serveur de BDD n'est pas lancé mais aussi de générer la table présente dans le modèle si cette dernière n'existe pas. Dans notre cas, la table &quot;articles&quot;. Pour ce faire, dans la fonction suivante (&quot;main&quot;), du fichier &quot;main.go&quot; ajoutez avant &quot;beego.Run()&quot; :</p> <pre><code class="language-go">func main() { name := "default" force := false verbose := true err := orm.RunSyncdb(name, force, verbose) if err != nil { beego.Debug(err) } beego.Run() }</code></pre> <ul> <li>&quot;name&quot; : on utilise la connexion &quot;default&quot; définit dans la fonction &quot;init&quot;</li> <li>&quot;force&quot; : si vaut &quot;true&quot;, alors l'ORM écrase (sans sommation) la table.</li> <li>&quot;verbose&quot; : affiche la connexion dans la console du serveur.</li> </ul> <p>Ci-dessous, les 3 types de messages possibles renvoyés dans la console du serveur (si &quot;verbose&quot; vaut &quot;true&quot;).<br /> Le serveur de BDD n'est pas démarré / non accessible : </p> <pre><code class="language-markup">[ORM]register db Ping `default`, dial tcp 127.0.0.1:3306: ConnectEx tcp: Aucune connexion n'a pu être établie car l'ordinateur cible l'a expressément refusée. must have one register DataBase alias named `default`</code></pre> <p>La table &quot;articles&quot; n'existe pas : </p> <pre><code class="language-markup">create table `articles` -- -------------------------------------------------- -- Table Structure for `myapp/models.Articles` -- -------------------------------------------------- CREATE TABLE IF NOT EXISTS `articles` ( `id` integer AUTO_INCREMENT NOT NULL PRIMARY KEY, `title` varchar(255) NOT NULL, `content` longtext NOT NULL, `created` datetime NOT NULL, `updated` datetime NOT NULL ) ENGINE=InnoDB;</code></pre> <p>La table &quot;articles&quot; existe : </p> <pre><code class="language-markup">table `articles` already exists, skip</code></pre> <p>Remarque : si vous ajoutez un nouveau champ après avoir déjà généré la table, Beego se charge de mettre à jour la structure de votre table.</p> <h2>Sources</h2> <ul> <li>Driver MySQL : <a href="https://github.com/go-sql-driver/mysql">https://github.com/go-sql-driver/mysql</a></li> <li>Driver Postgres : <a href="https://github.com/lib/pq">https://github.com/lib/pq</a></li> <li>Driver Sqlite3 : <a href="https://github.com/mattn/go-sqlite3">https://github.com/mattn/go-sqlite3</a></li> <li>Liste des types de champs: <a href="http://beego.me/docs/mvc/model/models.md#model-fields-mapping-with-database-type">http://beego.me/docs/mvc/model/models.md#model-fields-mapping-with-database-type</a></li> <li>Le site officiel de Beego : <a href="http://beego.me">http://beego.me</a></li> </ul>; 2015-01-17 09:00:00 Installation et premiers pas sur Beego (Partie 1) https://etienner.fr/installation-et-premiers-pas-sur-beego-partie-1 <p>Beego est un framework écrit en Go (diminutif de Golang). Il permet de coder des sites Internet (tout comme Django sur Python, Ruby On Rails sur Ruby, etc…). Dans cette première partie, on va voir comment installer Go et Beego sur une machine Windows, disséquer rapidement le framework, créer une nouvelle page, transférer des données du contrôleur à la vue et mettre en place un formulaire.</p> <h2>Installation de Golang</h2> <p>Dans un premier temps, si vous n'avez pas encore installé Go sur votre machine, allez sur la page de téléchargement : <a href="https://golang.org/dl">https://golang.org/dl</a>, téléchargez puis installez Golang.<br /> Tapez dans votre console <code>go version</code>.<br /> <img src="../assets/img/news/installation-et-premiers-pas-sur-beego/go-version.jpg" alt="" /></p> <p>Puis, dans un second temps, il crééz un dossier qui va recueillir les fichiers sources du framework Beego mais aussi les autres projets sur Golang.<br /> Pour ce faire, allez à la racine de votre disque dur et créez le dossier &quot;gopath&quot;.<br /> Créez une nouvelle variable d'environnement que vous nommez &quot;GOPATH&quot; puis dans &quot;Valeur&quot; copier-coller le chemin de votre répertoire &quot;gopath&quot; et validez.<br /> <img src="../assets/img/news/installation-et-premiers-pas-sur-beego/go-environnement.jpg" alt="" /></p> <p>Si vous faites Windows + R et que vous tapez &quot;%gopath%&quot; vous accédez bien à votre répertoire.</p> <p>Astuce : dans la console Windows, tapez <code>go env</code>.</p> <h2>Téléchargement de Beego</h2> <p>Ouvrez une console Windows et tapez la ligne de commande suivante :<br /> <code>go get github.com/astaxie/beego</code></p> <p>Info : avec la commande <code>go get</code>, les fichiers sont téléchargés et placés dans le dossier &quot;%gopath%/src/github.com/astaxie/beego&quot;.</p> <p>NB : Le logiciel Git doit être installé sur votre ordinateur. Si &quot;go get&quot; contient une URL avec &quot;code.google.com/[…]&quot; vous aurez besoin du logiciel Mercurial.</p> <h2>Installation de l'outil &quot;bee tool&quot;</h2> <p>Dans votre dossier &quot;%gopath%&quot;, créez un répertoire &quot;bin&quot;. Dans la console Windows tapez :<br /> <code>go get github.com/beego/bee</code></p> <p>Recréez une variable d'environnement, mais cette fois, à la suite de la variable &quot;Path&quot; :<br /> <code>;%GOPATH%/bin/</code></p> <p>Ouvrez une nouvelle console Windows en mode administrateur et tapez &quot;bee&quot;.<br /> <img src="../assets/img/news/installation-et-premiers-pas-sur-beego/bee-tool.jpg" alt="" /></p> <h2>Création d'un nouveau projet</h2> <p>On va maintenant pouvoir passer aux choses sérieuses. Toujours dans la fenêtre de console en mode administrateur, changez de répertoire :<br /> <code>cd %gopath%/src</code><br /> <code>bee new myapp</code></p> <p>Cela va générer le projet &quot;myapp&quot; dans &quot;%gopath%/src/myapp&quot;.<br /> <img src="../assets/img/news/installation-et-premiers-pas-sur-beego/bee-generate-project.jpg" alt="" /></p> <p>On peut alors lancer le serveur en se plaçant d'abord dans le répertoire de notre application :<br /> <code>cd myapp</code></p> <p>Puis, on exécute notre application :<br /> <code>bee run</code></p> <p>Cette commande va compiler un exécutable &quot;myapp.exe&quot; qui n'est autre que le serveur Web sur le port 8080 par défaut. Lorsque vous modifierez un fichier de votre projet avec l'extension go, le serveur redémarra automatiquement pour ainsi, recompiler votre application.<br /> Vous pouvez désormais ouvrir votre navigateur Web préféré sur l'adresse locale : <a href="http://localhost:8080">http://localhost:8080</a><br /> <img src="../assets/img/news/installation-et-premiers-pas-sur-beego/bee-run.jpg" alt="" /></p> <p>Attention : si vous avez un serveur Web traditionnel (Apache, Nginx, IIS, etc...) en cours d'exécution, le port 8080 peut rentrer en conflit. Si ce dernier vous handicape, vous pouvez le changer dans le fichier &quot;conf/app.conf&quot;. Editez la valeur contenue dans la variable intitulée &quot;httport&quot; (ligne 2). Vous devez alors relancer votre serveur pour prendre en compte cette modification.<br /> <img src="../assets/img/news/installation-et-premiers-pas-sur-beego/beego-home.jpg" alt="" /></p> <h2>Architecture MVC de l'application</h2> <ul> <li>&quot;conf&quot; : le fichier de configuration</li> <li>&quot;controllers&quot; : les contrôleurs</li> <li>&quot;models&quot; : les modèles</li> <li>&quot;routers&quot;: le fichier de routage</li> <li>&quot;static&quot; : pour les fichiers de type css, images et javascript dans leur dossier respectif</li> <li>&quot;test&quot; : les fichiers de test</li> <li>&quot;views&quot; : les vues</li> </ul> <h2>Comment ça marche ?</h2> <p><img src="../assets/img/news/installation-et-premiers-pas-sur-beego/beego-architecture.jpg" alt="" /><br /> Comme sur un MVC classique. Le routeur appelle le contrôleur qui à son tour affiche la vue. </p> <p>Le contrôleur (&quot;controllers/defaut.go&quot;) :</p> <pre><code class="language-go">package controllers import ( "github.com/astaxie/beego" ) type MainController struct { beego.Controller } func (this *MainController) Get() { this.Data["Website"] = "beego.me" this.Data["Email"] = "astaxie@gmail.com" this.TplNames = "index.tpl" }</code></pre> <p>La vue (views/index.tpl) :</p> <pre><code class="language-markup">&lt;h1&gt;Welcome to Beego!&lt;/h1&gt; &lt;p class="description"&gt; Beego is a simple &amp; powerful Go web framework which is inspired by tornado and sinatra. &lt;br /&gt; Official website: &lt;a href="http://{{.Website}}"&gt;{{.Website}}&lt;/a&gt; &lt;br /&gt; Contact me: {{.Email}} &lt;/p&gt;</code></pre> <p>Beego utilise le moteur de templating natif de Go (&quot;html/template&quot;). Bien entendu, vous pouvez le changer par un autre.</p> <p>Les données sont bien transmises du contrôleur vers la vue et la page est accessible grâce à la table de routage contenue dans la fonction &quot;init&quot; du fichier de routage par défaut &quot;routers/router.go&quot; :</p> <pre><code class="language-go">package routers import ( "someone/controllers" "github.com/astaxie/beego" ) func init() { beego.Router("/", &amp;controllers.MainController{}) }</code></pre> <p>La fonction de base du contrôleur appelée par le fichier de routage par défaut est &quot;Get()&quot; (contrairement à &quot;Index()&quot; dans de nombreux frameworks en PHP).</p> <h2>Créons notre première page.</h2> <p>Dans le dossier &quot;controllers&quot; créez un nouveau contrôleur &quot;test.go&quot;. A l'intérieur, on reprend le même en-tête que le contrôleur &quot;defaut.go&quot;, c'est-à-dire :</p> <pre><code class="language-go">package controllers import ( "github.com/astaxie/beego" )</code></pre> <p>Puis on déclare notre contrôleur en lui attribuant un nom :</p> <pre><code class="language-go">type TestController struct { beego.Controller }</code></pre> <p>Ensuite, on déclare nos variables ainsi que le template que l'on désire charger :</p> <pre><code class="language-go">func (this *TestController) About() { this.Data["title"] = "About" this.Data["content"] = "Lorem Ipsum" this.TplNames = "test/about.tpl" }</code></pre> <p>Attention : bien mettre des doubles quotes et non des simples quotes (mauvais réflexe de développeur PHP... et sur Golang, c'est le Mal !).</p> <p>Dans &quot;routers/router.go&quot; (dans la fonction &quot;init&quot;), à la suite du code actuel, rajoutez cette route :</p> <pre><code class="language-go">beego.Router("/about", &amp;controllers.TestController{}, "get:About")</code></pre> <p>Explications :<br /> En 1er paramètre, on indique l'URI (Uniform Resource Identifier) de notre application (&quot;/about&quot;).<br /> En 2nd paramètre, on indique le contrôleur (&quot;TestController&quot;).<br /> En 3ème paramètre, on indique la fonction de notre contrôleur concernée (&quot;About&quot;).<br /> Bee re-compile comme un grand et relance le serveur. </p> <p>Si vous vous rendez sur la page dédiée (<a href="http://localhost:8080/about">http://localhost:8080/about</a>), vous aurez droit au message classique :<br /> <code>someone:can't find templatefile in the path:test/about.tpl</code></p> <p>En effet, Beego nous signale que le fichier template appelé dans le contrôleur &quot;Test&quot; n'existe pas. Pour régler cela, dans le dossier &quot;views&quot;, créez un nouveau dossier &quot;test&quot; puis un nouveau fichier template &quot;about.tpl&quot; :</p> <pre><code class="language-markup">&lt;h1&gt;{{.title}}&lt;/h1&gt; &lt;p&gt;{{.content}}&lt;/p&gt;</code></pre> <h2>Les routes dynamiques</h2> <p>On veut créer une URL du type : <a href="http://localhost:8080/author/robert">http://localhost:8080/author/robert</a><br /> Dans le contrôleur &quot;test.go&quot;, ajoutez la fonction ci-dessous :</p> <pre><code class="language-go">func (this *TestController) Author() { this.Data["title"] = this.Ctx.Input.Param(":name") this.TplNames = "test/about.tpl" }</code></pre> <p>Et dans le fichier de routage (&quot;routers/routes.go&quot;), ajoutez la route suivante :</p> <pre><code class="language-go">beego.Router("/author/:name", &amp;controllers.TestController{}, "get:Author")</code></pre> <p><a href="http://localhost:8080/author/robert">http://localhost:8080/author/robert</a> est accessible.</p> <h2>Formulaire</h2> <p>Allons plus loin avec les formulaires. Après la déclaration de notre contrôleur &quot;test.go&quot;, mettez en place la structure suivante :</p> <pre><code class="language-go">type user struct { Id int `form:"-"` Name string `form:"username"` Email string `form:"email,email"` }</code></pre> <p>Puis, créez une nouvelle fonction dans le contrôleur :</p> <pre><code class="language-go">func (this *TestController) User() { this.Data["Form"] = &amp;user{} this.TplNames = "test/add_user.tpl" }</code></pre> <p>Dans le répertoire &quot;views/test&quot;, créez un nouveau fichier template que vous nommez &quot;add_user.tpl&quot;.</p> <pre><code class="language-markup">&lt;form id="user" action="" method="POST"&gt; {{.Form | renderform}} &lt;/form&gt;</code></pre> <p>Ensuite, dans le fichier route :</p> <pre><code class="language-go">beego.Router("/user", &amp;controllers.TestController{}, "get:User")</code></pre> <p>En vous rendant sur la page du formulaire, <a href="http://localhost:8080/user">http://localhost:8080/user</a>, Beego a créé automatiquement un formulaire à partir des 2 champs renseignés dans la structure &quot;user&quot; de notre contrôleur &quot;Test&quot;. <img src="../assets/img/news/installation-et-premiers-pas-sur-beego/beego-renderform.jpg" alt="" /></p> <p>Sauf qu'il manque quelque chose sur ce formulaire… Avant la fin de la balise de fermeture du formulaire (<code>&lt;/form&gt;</code>), ajoutez le bouton de soumission des données :</p> <pre><code class="language-markup">&lt;br /&gt; &lt;input type="submit" value="Envoyer" /&gt;</code></pre> <p>On va afficher le résultat dans une autre vue (&quot;test/result_user.tpl&quot;) :</p> <pre><code class="language-markup">&lt;h1&gt;{{.username}}&lt;/h1&gt; &lt;h2&gt;{{.email}}&lt;/h2&gt;</code></pre> <p>Ces variables mentionnées ci-dessus, ont besoin d'être définies dans le contrôleur. Ce sont les variables récupérées depuis notre formulaire. A la suite dans notre fonction &quot;User&quot;, ajoutez la condition suivante :</p> <pre><code class="language-go"> if this.Ctx.Input.Method() == "POST" { this.Data["username"] = this.Input().Get("username") this.Data["email"] = this.Input().Get("email") this.TplNames = "test/result_user.tpl" }</code></pre> <p>Et là, vous vous dites que le formulaire à l'adresse <a href="http://localhost:8080/user">http://localhost:8080/user</a>… marche sauf que lors de l'envoi des données une erreur de type 404 apparait.<br /> <img src="../assets/img/news/installation-et-premiers-pas-sur-beego/beego-404.jpg" alt="" /></p> <p>Allez dans votre fichier de routage et spécifiez que vous désirez à la fois du GET et du POST comme type de requêtes HTTP sur la page &quot;/user&quot;. Remplacez donc :</p> <pre><code class="language-go">beego.Router("/user", &amp;controllers.TestController{}, "get:User")</code></pre> <p>Par :</p> <pre><code class="language-go">beego.Router("/user", &amp;controllers.TestController{}, "get,post:User")</code></pre> <p>Désormais, le formulaire peut envoyer des données via POST.</p> <p>En ce qui concerne le parsage automatique du formulaire, rapide à déployer, hélas, ce dernier est assez limité car il ne propose pas d'inclure de label et il n'est pas possible d'ajouter un id ou une classe pour chaque champ d'un formulaire (input et textarea).</p> <h2>Upgrade du framework</h2> <p>Avant de terminer cet article, j'ai mis à jour Beego (de la version 1.4.1 vers 1.4.2) en tapant simplement la commande ci-dessous :<br /> <code>go get –u github.com/astaxie/beego</code><br /> <img src="../assets/img/news/installation-et-premiers-pas-sur-beego/beego-upgrade.jpg" alt="" /></p> <h2>Conclusion</h2> <p>Beego est un framework Web qui permet de se mettre doucement au Golang, même si il est recommandé de connaitre les bases du langage au préalable. Conçu par des ingénieurs de chez Google, il fait beaucoup parler de lui en ce moment, principalement dans les benchmarks.</p> <h2>Sources</h2> <ul> <li>Le site officiel de Beego : <a href="http://beego.me">http://beego.me</a></li> <li>Moteur de templating natif : <a href="http://golang.org/pkg/html/template">http://golang.org/pkg/html/template</a></li> <li>Autres moteurs de templating en Go : <a href="https://github.com/avelino/awesome-go#template-engines">https://github.com/avelino/awesome-go#template-engines</a></li> <li>S'initier à Golang : <a href="https://tour.golang.org">https://tour.golang.org</a></li> <li>Benchmark regroupant plusieurs langages et framework (dont Go et Beego) : <a href="http://www.techempower.com/benchmarks">http://www.techempower.com/benchmarks</a></li> </ul>; 2015-01-15 16:00:00 Générer des données avec GenerateData https://etienner.fr/generer-des-donnees-avec-generatedata <p>Vous développez une application web et vous avez besoin de faire des tests avec de la data que vous n'avez pas sous la main afin de vérifier le bon fonctionnement de votre site (charge serveur, affichage). Pour cela il existe une application web &quot;Generatedata&quot; disponible à l'adresse suivante : <a href="http://www.generatedata.com/?lang=fr">http://www.generatedata.com/?lang=fr</a><br /> Le site propose de générer des fausses données (une sorte de lorem ipsum) dans de nombreux formats : CSV, Excel, Json, Ldif, SQL, XML mais aussi pour PHP, Javascript, Perl et Python (sous forme de tableau).<br /> Le nombre d'enregistrements de lignes est limité à 1000 enregistrements à la fois. Si vous désirez en produire d'avantage, l'application est disponible en téléchargement sur le GitHub officiel : <a href="https://github.com/benkeen/generatedata/tags">https://github.com/benkeen/generatedata/tags</a> en PHP.</p> <h2>Installation</h2> <p>L'application s'installe comme un CMS. Dans votre base de données, créez une nouvelle table que vous nommez &quot;generatedata&quot;.<br /> Désipez le dossier de &quot;generatedata-3.1.3&quot; à la racine de votre serveur Apache (dossier &quot;www&quot; par défaut) et renommez-le en &quot;generatedata&quot;.<br /> Connectez-vous sur votre serveur à l'adresse suivante : <a href="http://localhost/generatedata">http://localhost/generatedata</a> (par défaut).<br /> Etape 1 : sélectionnez en haut à droite &quot;Français&quot;.<br /> Dans le champ &quot;Nom de l'hôte&quot;, indiquez l'adresse de votre serveur de base de données (localhost ou 127.0.0.1 par défaut).<br /> Dans &quot;Nom de la base&quot; indiquez le nom de la base de donnée ci-dessus &quot;generatedata&quot; puis dans &quot;Nom d'utilisateur MySQL&quot; tapez &quot;root&quot; par défaut et dans &quot;Mot de passe MySQL&quot; indiquez votre mot de passe si vous en possédez un (sinon laissez vide). Cliquez sur &quot;Continuer&quot;<br /> <img src="../assets/img/news/generate_data/generate_data_install_1.jpg" alt="" /> Etape 2 : cliquez sur &quot;Créer un fichier&quot;.<br /> Etape 3 : laissez cocher par défaut &quot;Un seul compte utilisateur, connexion anonyme&quot;.<br /> Etape 4 : cliquez sur &quot;Installer les Plugins&quot;. Une fois la fin de la génération des plugins finie, cliquez sur &quot;Continuer&quot;.<br /> <img src="../assets/img/news/generate_data/generate_data_install_2.jpg" alt="" /> Etape 5 : vous pouvez désormais vous servir de Generatedata en cliquant sur &quot;Aller au script&quot;.</p> <h2>Utilisation</h2> <p>Dans la colonne &quot;Titre de la colonne&quot; donnez un nom à votre entité, puis sélectionnez quel type de données vous désirez (&quot;nom&quot;, &quot;email&quot;, &quot;date&quot;, etc...).<br /> Dans &quot;Type d'export&quot; sélectionnez votre langage. En dessous, sélectionnez le nombre de ligne à générer et cliquez sur &quot;Générer&quot;.<br /> <img src="../assets/img/news/generate_data/generate_data_1.jpg" alt="" /></p> <p><img src="../assets/img/news/generate_data/generate_data_2.jpg" alt="" /> Generatedata étant un outil génial, vous pouvez lui demander de générer les données directement dans un fichier en cochant &quot;Télécharger comme un fichier&quot; (avant la génération des données).</p> <h2>Conclusion</h2> <p>Cet outil se révèle indispensable dans la création d'une API afin de mener à bien vos tests de performances.</p>; 2014-08-28 20:46:22 Phalcon 1.3 : installation et configuration https://etienner.fr/phalcon-1.3-installation-et-configuration <p>Phalcon est un framework PHP ayant la particularité d'avoir son moteur écrit en Zephir ce qui le rend plus rapide que les autres framework PHP traditionnels. En effet, Zephir est un langage open-source de bas niveau conçu pour faciliter la création d'extensions PHP en les compilant en langage C.</p> <p><img src="../assets/img/news/phalcon_installation_configuration/phalcon_benchmark.jpg" alt="" /></p> <h2>Installation et configuration sur WAMP</h2> <p>Afin de faire fonctionner Phalcon sur votre serveur WAMP, il faut télécharger l'extension qui permettra à votre serveur d'interpréter l'ensemble des librairies de Phalcon compilées en C. </p> <ol> <li>Allez sur la page de téléchargement officielle :<a href="http://docs.phalconphp.com/fr/latest/reference/wamp.html">http://docs.phalconphp.com/fr/latest/reference/wamp.html</a><br /> Téléchargez la version correspondant à votre version de WAMP 32 ou 64 bits mais aussi de la version de votre PHP.<br /> Allez dans le dossier &quot;C://bin/php/php5.4.12/ext&quot; et placez le fichier dll que vous venez de télécharger. </li> <li>Puis, ouvrez le fichier de configuration php.ini présent dans le dossier de PHP :<br /> &quot;C://bin/php/php5.4.12/php.ini&quot; et ajoutez à la fin du fichier :<br /> <code>extension=php_falcon.dll</code> </li> <li>Faites de même pour celui d'Apache : &quot;C://bin/apache/Apache2.4.4/bin/php.ini&quot;</li> </ol> <p>Démarrez (ou redémarrez) votre serveur WAMP.<br /> En faisant un <code>&lt;?php echo phpinfo(); ?&gt;</code>, on voit que l'extension &quot;phalcon&quot; est chargée.<br /> <img src="../assets/img/news/phalcon_installation_configuration/phalcon_extension_php_info.jpg" alt="" /></p> <h2>Téléchargement et installation de l'outil phalcon-devtools</h2> <p>A l'heure actuelle, en version alpha, le phalcon-devtools reprend le même principe que le Zend Tool pour ZendFramework, générer un nouveau projet, un nouveau contrôleur, un nouveau model, etc…<br /> Allez à l'adresse : <a href="https://github.com/phalcon/phalcon-devtools">https://github.com/phalcon/phalcon-devtools</a><br /> Ou via GIT : <code>git clone <a href="https://github.com/phalcon/phalcon-devtools.git">https://github.com/phalcon/phalcon-devtools.git</a></code><br /> Editez le fichier &quot;phalcon.bat&quot; en modifiant la ligne suivante :<br /> <code>set PTOOLSPATH=&quot;%~dp0/&quot;</code><br /> Par le chemin correct où se situe &quot;phalcon-devtools&quot; :<br /> <code>set PTOOLSPATH=C:/phalcon-devtools/</code><br /> Ajoutez dans la variable d'environnement &quot;Path&quot; le chemin de PHP et de phalcon-devtools : <code>;C:\wamp\bin\php\php5.4.12;C:\phalcon-devtools</code> et validez.<br /> Tapez la ligne de commande ci-dessous :<br /> <code>phalcon</code><br /> <img src="../assets/img/news/phalcon_installation_configuration/phalcon_devtools_options.jpg" alt="" /> On va s'intéresser à la commande &quot;project&quot; afin de générer un nouveau projet Phalcon.<br /> Pour cela, placez-vous dans le dossier de développement de Wamp :<br /> <code>cd C://www</code><br /> Puis lancez la commande &quot;create-project&quot; ci-dessous :<br /> <code>phalcon create-project mon_projet --enable-webtools</code><br /> Les fichiers sont générés dans un nouveau dossier nommé &quot;mon_projet&quot;.<br /> <img src="../assets/img/news/phalcon_installation_configuration/phalcon_devtools_create_project.jpg" alt="" /> Avec une architecture MVC classique :<br /> <img src="../assets/img/news/phalcon_installation_configuration/phalcon_mvc.jpg" alt="" /></p> <h2>Configurer l'accès à la base de données</h2> <p>Par défaut, Phalcon introduit le nom de la table concernée par le nom du projet (&quot;mon_projet&quot; dans notre cas).<br /> Pour le modifier, c'est très simple, ouvrez le fichier &quot;config.php&quot; présent dans le dossier &quot;app/config&quot; et éditez les lignes concernées.</p> <pre><code class="language-php">'database' =&gt; array( 'adapter' =&gt; 'Mysql', 'host' =&gt; 'localhost', 'username' =&gt; 'root', 'password' =&gt; '', 'dbname' =&gt; 'mon_projet', ),</code></pre> <p>Ensuite ces informations seront traitées dans le fichier &quot;service.php&quot; dans le dossier &quot;app/config&quot; (ligne 56).</p> <h2>Le WebTools</h2> <p>On accède au WebTools à l'adresse suivante : <a href="http://localhost/mon_projet/webtools.php">http://localhost/mon_projet/webtools.php</a> <img src="../assets/img/news/phalcon_installation_configuration/phalcon_webtools.jpg" alt="" /> Vous pouvez désormais éditer vos controllers, vos models, le scaffold et les migrations (ces 3 derniers après avoir configuré l'accès à votre base de données) en vous passant des lignes de commandes...<br /> Oui mais n'importe qui peut se connecter sur cet outil de gestion en connaissant l'url ?<br /> Non, car si vous allez dans le fichier de configuration de webtools, c'est-à dire &quot;public/webtools.config.php&quot;, vous pouvez voir que l'accès à ce service est restreint ligne 30 :</p> <pre><code class="language-php">define('PTOOLS_IP', '192.168.');</code></pre> <h2>Accéder au site</h2> <p>Pour accéder au site : <a href="http://localhost/mon_projet">http://localhost/mon_projet</a><br /> Par défaut, c'est le contrôleur &quot;IndexController&quot; qui se charge de l'index du site<br /> Comment savoir quelle vue ce contrôleur charge ?<br /> Le nom du contrôleur &quot;IndexController&quot; correspond au dossier &quot;index&quot;<br /> La fonction <code>indexAction</code> correspondant au nom du fichier de la vue &quot;index&quot;<br /> Les vues étant par défaut chargées dans le dossier &quot;app/views (cf : app/config/config.php)&quot;, on peut en conclure que le chemin est :<br /> &quot;app/views/index/index.volt&quot;<br /> En ce qui concerne le layout, il se trouve à la racine du répertoire des vues :<br /> &quot;app/views/layouts/index.volt&quot;</p> <pre><code class="language-markup">&lt;html&gt; &lt;head&gt; &lt;title&gt;{{title}}&lt;/title&gt; &lt;/head&gt; &lt;body&gt; {{ content() }} &lt;/body&gt; &lt;/html&gt;</code></pre> <p>La vue précédente, index/index.volt s'affiche par défaut dans la fonction <code>content()</code>.<br /> Pour charger la feuille de style de Twitter Bootstrap présente dans le dossier &quot;public&quot;, placez cette ligne sous la balise title : </p> <pre><code class="language-markup">&lt;link rel="stylesheet" href="public/css/bootstrap/bootstrap.min.css" /&gt;</code></pre> <h2>Conclusion</h2> <p>Phalcon permet via son outil de générer un projet rapidement malgré la configuration initiale qui peut sembler laborieuse pour les moins téméraires. Mais il ne déçoit pas au niveau performance et de son poids (moins d'1Mo du fait que le moteur du framework est présent dans le dll).</p> <h2>Sources</h2> <ul> <li>Site officiel de Phalcon PHP : <a href="http://www.phalconphp.com">http://www.phalconphp.com</a></li> <li>Site officiel du langage Zephir : <a href="http://www.zephir-lang.com">http://www.zephir-lang.com</a></li> <li>Cmder est une alternative à CMD et PowerShell (onglets et coloration syntaxique) : <a href="http://bliker.github.io/cmder">http://bliker.github.io/cmder</a></li> </ul>; 2014-06-22 21:49:36 Slider tactile via du swipe https://etienner.fr/slider-tactile-via-du-swipe <p>Nous allons réaliser un slider ne marchant que sur tactile. Pour ce faire nous allons utiliser en plus de jQuery, la librairie de jQuery Mobile. Bien entendu, nous allons nous servir des fonctions qui nous serons utiles, c'est-à-dire <code>swiperight</code> et <code>swipeleft</code>, c'est pour cela que l'on ne chargera pas la librairie complète de jQuery Mobile. On appelle cette méthode de navigation &quot;swipe&quot; car ce mot signifie en anglais &quot;faire glisser&quot;.<br /> NB : Avant de commencer, sachez que ce tutoriel ne traitera pas des boutons de navigation, ni des raccourcis clavier.</p> <h2>Téléchargement de la librairie</h2> <p>Pour télécharger la version customisée de jQuery Mobile, allez sur la page de téléchargement &quot;custom&quot; officielle : <a href="http://jquerymobile.com/download-builder">http://jquerymobile.com/download-builder</a><br /> Dans &quot;Select branch&quot;, sélectionnez la dernière version (&quot;1.4.2&quot; à l'heure actuelle), plus bas dans la partie &quot;Events&quot; cochez &quot;Touch&quot; puis cliquez sur le bouton &quot;Build My download&quot;.</p> <h2>Arborescence de ce projet</h2> <pre><code>│ index.html │ ├─── css │ style.css │ ├─── img │ image1.jpg │ image2.jpg │ image3.jpg │ image4.jpg │ ├─── js │ jquery-min.js │ jquery.mobile.custom.min.js │ slider.js</code></pre> <h2>Contenu du fichier index.html</h2> <pre><code class="language-markup">&lt;html&gt; &lt;head&gt; &lt;meta charset="UTF-8"&gt; &lt;meta name="viewport"content="width=device-width, maximum-scale=1"/&gt; &lt;title&gt;Slider jQuery Swipe&lt;/title&gt; &lt;link rel="stylesheet"href="css/style.css"&gt; &lt;/head&gt; &lt;body&gt; &lt;div id="slider" data-ride="slider"&gt; &lt;ul&gt; &lt;li&gt; &lt;img src="img/image1.jpg"alt="image 1"/&gt; &lt;/li&gt; &lt;li&gt; &lt;img src="img/image2.jpg"alt="image 2"/&gt; &lt;/li&gt; &lt;li&gt; &lt;img src="img/image3.jpg"alt="image 3"/&gt; &lt;/li&gt; &lt;li&gt; &lt;img src="img/image4.jpg"alt="image 4"/&gt; &lt;/li&gt; &lt;/ul&gt; &lt;span id="legend"&gt;&lt;/span&gt; &lt;/div&gt; &lt;script src="js/jquery-2.1.0.min.js"&gt;&lt;/script&gt; &lt;script src="js/jquery.mobile.custom.min.js"&gt;&lt;/script&gt; &lt;script src="js/slider.js"&gt;&lt;/script&gt; &lt;/body&gt; &lt;/html&gt;</code></pre> <p>On charge les images dans une liste à puce sans oublier d'appeler nos fichiers javascript.</p> <h2>Contenu du fichier style.css</h2> <pre><code class="language-css">body{ margin: 0; padding: 0; } #slider{ margin: 0 auto; width: 480px; } #slider ul{ list-style-type: none; margin: 0; padding: 0; } #slider ul li{ display: none; } span#legend{ display: block; text-align: center; }</code></pre> <p>Comme pour un carrousel classique, le principe consiste à cacher toutes les images, c'est-à-dire toutes les puces présentes dans la DIV &quot;slider&quot;.</p> <h2>Contenu du fichier slider.js</h2> <pre><code class="language-javascript">$( document ).ready(function() { // On compte le nombre d'images var count = $('#slider ul li').length; // On affiche la 1ère image $('#slider ul li:eq(0)').show().addClass('active'); $('#legend').append( $('#slider ul li.active img').attr('alt') ); // Image suivante $('#slider ul').swipeleft(function(){ var current = $('#slider ul li.active').index(); var next = current + 1; $('#slider ul li').hide().removeClass('active'); if (current == (count - 1)) { $('#slider ul li:eq(0)').show().addClass('active'); } else { $('#slider ul li:eq(' + next + ')').show().addClass('active'); } $('#legend').html('').append( $('#slider ul li.active img').attr('alt') ); }); // Image précédente $('#slider ul').swiperight(function(){ var current = $('#slider ul li.active').index(); var prev = current - 1; $('#slider ul li').hide().removeClass('active'); if (current &lt; count) { $('#slider ul li:eq(' + prev + ')').show().addClass('active'); } $('#legend').html('').append( $('#slider ul li.active img').attr('alt') ); }); });</code></pre> <p>Dans un premier temps, on affiche la 1ère image de la liste ainsi que sa légende.<br /> Puis, dès que l'on glisse le doigt à gauche ou à droite on affiche l'image précédente ou suivante. Pour cela on attribue une classe CSS &quot;active&quot; sur l'image demandée et l'image précédente ou suivante se voit retirer cette classe.</p>; 2014-06-09 15:51:32 Encoder des images en webp sous Windows https://etienner.fr/encoder-des-images-en-webp-sous-windows <p>WebP est un nouveau format développé par Google. Il vise à remplacer le PNG, le JPEG et le TIFF en permettant une meilleur compression pour un résultat au plus semblable. Cette performance permet de réduire la taille de 25% à 34% par rapport ses vieux concurrents. A noter que WebP gère la transparence des PNG. A l'heure actuelle de cette rédaction, seuls Chrome (32+) et Opera (19+) peuvent décoder ce nouveau format.</p> <h2>Téléchargement de la librairie</h2> <p>Allez sur le site de Google dédiés aux développeurs et téléchargez la version adapté à votre système d'exploitation :<a href="https://code.google.com/p/webp/downloads/list?hl=fr">https://code.google.com/p/webp/downloads/list?hl=fr</a><br /> Créez un nouveau dossier dans le dossier &quot;C:/Programmes&quot; (pour la version 64 bits), &quot;C:/Programmes&quot; (pour la version 32 bits) que vous nommez &quot;webp&quot; où vous désipez tous les fichiers présents dans le dossier &quot;libwebp-0.4.0-windows-x..&quot;.</p> <h2>Ajouter la variable système</h2> <p>L'encodage se faisant via la ligne de commande &quot;cwebp&quot;, il faut informer Windows de l'emplacement de l'exécutable.<br /> Dans &quot;Panneau de configuration&quot;, allez dans &quot;Système&quot;, &quot;Paramètres système avancés&quot;, &quot;Variable d'environnement&quot; puis double cliquez sur &quot;Path&quot;, à la fin de la ligne ajoutez la ligne suivante :<br /> Pour la version 64 bits :<br /> <code>;C:/Program Files/web/bin</code><br /> Pour la version 32 bits :<br /> <code>;C:/Programmes/web/bin</code><br /> Validez<br /> Lancez l'invite de commande (cmd).<br /> Tapez &quot;cwebp&quot; afin de confirmer que l'exécutable fonctionne.<br /> Si vous n'avez pas d'erreur, vous pouvez continuer. <img src="../assets/img/news/encoder_images_webp/encoder_images_webp_1.jpg" alt="" /></p> <h2>Encoder une image</h2> <p>Via l'invite de commande, allez dans le dossier où se trouve l'image à convertir (PNG, JPEG ou TIFF). Tapez la ligne de commande suivante :<br /> <code>cwebp mon_image.mon_extension -o mon_image.webp</code><br /> L'image &quot;mon_image.webp&quot; est créée. <img src="../assets/img/news/encoder_images_webp/encoder_images_webp_2.jpg" alt="" /></p> <p>Dans l'exemple ci-dessus, l'image en PNG pèse 175 Ko, encodée en WebP, elle ne fait plus que 76.2 Ko avec la même qualité. <img src="../assets/img/news/encoder_images_webp/encoder_images_webp_3.jpg" alt="" /></p> <p>Dans le second exemple ci-dessus, l'image JPEG pèse 3.05 Mo, encodée en WebP, elle ne fait plus que 452 Ko avec la même qualité !</p> <h2>Configurer la qualité de l'image</h2> <p>Pour configurer la qualité de l'image de sortie, c'est simple : <code>cwebp –q 0 mon_image.extension -o mon_image.webp</code><br /> L'image générée sera de faible qualité.<br /> <code>cwebp –q 100 mon_image.extension -o mon_image.webp</code><br /> L'image générée sera de très grande qualité.<br /> Pour plus de commande ci-dessous :<br /> <code>cwebp -longhelp</code></p> <h2>Sources</h2> <ul> <li>En savoir plus sur le format WebP : <a href="http://fr.wikipedia.org/wiki/WebP">http://fr.wikipedia.org/wiki/WebP</a></li> <li>En savoir plus sur les lignes de commandes cwebp : <a href="https://developers.google.com/speed/webp/docs/cwebp">https://developers.google.com/speed/webp/docs/cwebp</a></li> <li>Liste des navigateurs compatibles avec le WebP :<a href="http://caniuse.com/web">http://caniuse.com/web</a></li> </ul>; 2014-03-31 23:36:14 Modèle entité-association avec Mysql Workbench 6.0 https://etienner.fr/modele-entite-association-avec-mysql-workbench-6.0 <p>MySQL Workbench est un logiciel développé par Oracle (éditeur de MySQL) permettant de gérer et d'administrer ses bases de données MySQL. Beaucoup plus complet que PHP My Admin, MySQL Workbench de par son interface graphique, offre la possibilité de modéliser dans un premier temps un schéma de table reliées entre elles. Puis, dans un second temps de l'importer directement dans MySQL sous forme de requêtes SQL.</p> <h2>Installation</h2> <p>Si nous ne l'avez pas encore installé, le logiciel est disponible gratuitement sur le site officiel : <a href="http://dev.mysql.com/downloads/tools/workbench">http://dev.mysql.com/downloads/tools/workbench</a><br /> Sélectionnez la version Windows (x86, 32-bit), MSI Installer si vous êtes sur Windows et cliquez sur &quot;Download&quot; puis sur &quot;No thanks, just start my download&quot;.<br /> Dès que MySQL Workbench est lancé, n'oubliez pas de démarrer votre serveur MySQL présent sur votre machine ou à distance (de type WAMP, LAMP ou MAMP).</p> <h2>Connexion au serveur MySQL</h2> <p>Dans la barre des menus, cliquez sur &quot;Database&quot; &gt; &quot;Manage Server Connections&quot;.<br /> Si vous avez un mot de passe sur votre serveur MySQL, cliquez à droite de &quot;Password&quot; sur &quot;Store in Vault&quot;, entrez votre mot de passe et validez.<br /> <img src="../assets/img/news/mysql-workbench/mysql-workbench_img_1.jpg" alt="" /></p> <h2>Modélisation du schéma (création de la nouvelle base de données) :</h2> <p>Cliquez sur le 4ème icone en haut à droite &quot;Create a new schema in the connected server&quot;.<br /> Dans le champ &quot;Name&quot; rentrez le nom de votre nouvelle base de donnée (dans ce tuto, nous l'appellerons &quot;mon_blog&quot;). <img src="../assets/img/news/mysql-workbench/mysql-workbench_img_2.jpg" alt="" /></p> <p>Puis cliquez sur &quot;Apply&quot;.<br /> La fenêtre &quot;Apply SQL Script to Database&quot; apparait, cliquez de nouveau sur &quot;Apply&quot; puis sur &quot;Finish&quot;. <img src="../assets/img/news/mysql-workbench/mysql-workbench_img_3.jpg" alt="" /></p> <p>La nouvelle base (&quot;mon_blog&quot;) est disponible.</p> <p>Passons maintenant au diagramme EER (Entity-Relationship Diagram ou Modèle entité-association dans le monde francophone) pour créer notre schéma.<br /> Allez dans &quot;File&quot; &gt; &quot;New Model&quot; (CTRL+N) et doubles cliquez sur &quot;Add Diagram&quot;. <img src="../assets/img/news/mysql-workbench/mysql-workbench_img_4.jpg" alt="" /></p> <p>Pour placer une table, maintenez la touche T de votre clavier enfoncée puis faites un clic gauche.<br /> Doubles cliquez sur votre nouvelle table. En dessous de votre diagramme, vous avez les propriétés de votre table, ajoutez les entités.<br /> Dans le champ :</p> <ul> <li>&quot;Table name&quot; : le nom de votre table</li> <li>&quot;Colums Name &quot;: le nom de votre entité</li> <li>&quot;Datatype&quot; : le type de champ (INT, VARCHAR, TEXT, DATETIME, etc...)</li> <li>&quot;PK&quot; : Primary Key</li> <li>&quot;NN&quot; : Not Null (coché par défaut)</li> <li>&quot;UQ&quot; : Unique Key</li> <li>&quot;AI&quot; : Auto Increment</li> </ul> <p>Vous pouvez aussi commenter l'entité dans le champ &quot;Comments&quot; à droite.</p> <p>Dans l'exemple ci-dessous, on va créer 2 tables &quot;content&quot; et &quot;rubric&quot; que l'on va ensuite mettre en relation par le biais d'une clef étrangère.<br /> La clef &quot;content&quot; contient les champs suivant :</p> <ul> <li>c_id</li> <li>c_title</li> <li>c_content</li> <li>c_cdate</li> <li>r_id</li> </ul> <p>Quant à la clef &quot;rubric&quot; :</p> <ul> <li>r_id</li> <li>r_title</li> <li>r_description</li> </ul> <p><img src="../assets/img/news/mysql-workbench/mysql-workbench_img_5.jpg" alt="" /></p> <p>Comme en cours de MERISE, on rajoute la liaison 1, n partant du principe qu'un article est contenu dans une et une seule rubrique tandis qu'une rubrique peut contenir 1 ou plusieurs article(s).<br /> Cliquez sur l'icône &quot;1:n&quot; avec une pipette (le dernier de la liste), c'est-à-dire une relation avec des entités déjà existante puis sélectionnez &quot;r_id&quot; dans &quot;Content&quot; puis &quot;r_id&quot; dans &quot;rubric&quot;. MySQL Workbench fait la liaison automatiquement. <img src="../assets/img/news/mysql-workbench/mysql-workbench_img_6.jpg" alt="" /></p> <h2>Importation du schéma dans la base de données</h2> <p>Maintenant que notre schéma est fini, on peut exporter les données dans notre base &quot;mon_blog&quot;.<br /> Dans la barre des menus, cliquez sur &quot;Database&quot; &gt; &quot;Synchronize Model&quot;.<br /> Cliquez sur &quot;Next&quot;.<br /> Cliquez de nouveau sur &quot;Next&quot;.<br /> Sélectionnez votre Model Schema.<br /> Changez de base dans &quot;Override target schema to be synchronized&quot; puis cliquez sur &quot;Override Target&quot; et cochez au-dessus votre Model Schema.<br /> Arrivé(e) à l'étape &quot;Review DB Changes&quot; vous pouvez voir le SQL généré pour exécuter votre schéma. Cliquez sur &quot;Execute&quot; pour générer les tables. <img src="../assets/img/news/mysql-workbench/mysql-workbench_img_8.jpg" alt="" /></p> <h2>Problème rencontré lors de l'installation de MySQL Workbench</h2> <p>&quot;MySQL Workbench requires the Visual C++ 2010 Redistribuale Package to be installed&quot; <img src="../assets/img/news/mysql-workbench/mysql-workbench_img_9.jpg" alt="" /></p> <p>Téléchargez et installez Visual C++ 2010 Redistribuale à l'adresse suivante : <a href="http://www.microsoft.com/fr-fr/download/details.aspx?id=14632">http://www.microsoft.com/fr-fr/download/details.aspx?id=14632</a></p>; 2014-03-15 12:07:33 Codeigniter Blog : le flux RSS (Partie 3) https://etienner.fr/codeigniter-blog-le-flux-rss-partie-3 <p>Le flux RSS (Really Simple Syndication) permet à vos visiteurs de garder le contact avec votre blog.<br /> Vous pouvez aussi diffuser votre flux RSS de syndication sur des services Web comme Facebook, Linkedin, etc...</p> <h2>Objectif</h2> <p>Afficher le flux RSS des 10 derniers articles, à l'adresse suivante : <a href="http://mon-blog.com/feed">http://mon-blog.com/feed</a></p> <ul> <li>1 contrôleur</li> <li>1 modèle</li> <li>1 vue</li> <li>modification du fichier routes.php</li> </ul> <h2>1) Le contrôleur &quot;Feed&quot;</h2> <p>Dans le dossier <code>application/controllers/front</code>, créez un nouveau contrôleur &quot;feed.php&quot;.</p> <pre><code class="language-php">&lt;?php if ( ! defined('BASEPATH')) exit('No direct script access allowed'); class Feed extends CI_Controller{ function __construct() { parent::__construct(); // Chargement des ressources pour ce controller $this-&gt;load-&gt;database(); $this-&gt;load-&gt;model('front/model_feed'); $this-&gt;load-&gt;helper(array('url', 'xml')); } public function index() { $data['site_name'] = 'Mon blog'; $data['site_link'] = base_url(); $data['site_description'] = 'Les flux RSS de mes articles'; $data['encoding'] = 'utf-8'; $data['feed_url'] = base_url() . '/feed'; $data['page_language'] = 'fr-fr'; $data['posts'] = $this-&gt;model_feed-&gt;getRecentPosts(); header("Content-Type: application/rss+xml"); $this-&gt;load-&gt;view('front/view_feed', $data); } } /* End of file feed.php */ /* Location: ./application/controllers/front/feed.php */</code></pre> <p>On met en place les variables que l'on placera dans le header du flux RSS :</p> <ul> <li>le nom du site</li> <li>l'encodage</li> <li>l'url du flux rss</li> <li>la description du flux</li> </ul> <p>Puis on récupère les données dans le modèle et on précise le content type du header (ou MIME) avec pour type &quot;application&quot; et en sous type &quot;rss+xml&quot;.</p> <h2>2) Le modèle &quot;Model_feed&quot;</h2> <p>Dans le dossier <code>application/models/front</code>, créez un nouveau modèle &quot;model_feed.php&quot;.</p> <pre><code class="language-php">&lt;?php if ( ! defined('BASEPATH')) exit('No direct script access allowed'); class Model_feed extends CI_Model{ function getRecentPosts() { $this-&gt;db-&gt;select('c_title, c_content, c_cdate, c_url_rw, r_title, r_description, r_url_rw') -&gt;from('content') -&gt;join('rubric', 'rubric.r_id = content.r_id') -&gt;order_by('c_id', 'DESC') -&gt;limit(10); $query = $this-&gt;db-&gt;get(); return $query; } } /* End of file model_feed.php */ /* Location: ./application/models/front/model_feed.php */</code></pre> <h2>3) La vue &quot;view_feed&quot;</h2> <p>Dans le dossier <code>application/views/front</code> créez un nouvelle vue &quot;view_rss.php&quot;.</p> <pre><code class="language-php">&lt;?php echo '&lt;?xml version="1.0" encoding="' . $encoding . '"?&gt;' . ""; ?&gt; &lt;rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:sy="http://purl.org/rss/1.0/modules/syndication/" xmlns:admin="http://webns.net/mvcb/" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:content="http://purl.org/rss/1.0/modules/content/"&gt; &lt;channel&gt; &lt;description&gt;&lt;?php echo $site_description; ?&gt;&lt;/description&gt; &lt;link&gt;&lt;?php echo $site_link; ?&gt;&lt;/link&gt; &lt;title&gt;&lt;?php echo $site_name; ?&gt;&lt;/title&gt; &lt;dc:language&gt;&lt;?php echo $page_language; ?&gt;&lt;/dc:language&gt; &lt;dc:rights&gt;Copyright &lt;?php echo gmdate("Y", time()); ?&gt;&lt;/dc:rights&gt; &lt;?php foreach($posts-&gt;result() as $post): ?&gt; &lt;item&gt; &lt;title&gt;&lt;?php echo xml_convert($post-&gt;c_title); ?&gt;&lt;/title&gt; &lt;link&gt;&lt;?php echo base_url($post-&gt;r_url_rw . '/' . $post-&gt;c_url_rw); ?&gt;&lt;/link&gt; &lt;guid&gt;&lt;?php echo base_url($post-&gt;r_url_rw . '/' . $post-&gt;c_url_rw); ?&gt;&lt;/guid&gt; &lt;?php $post-&gt;c_content = strip_tags($post-&gt;c_content); ?&gt; &lt;description&gt; &lt;?php echo $post-&gt;c_content; ?&gt; &lt;/description&gt; &lt;?php $date = strtotime($post-&gt;c_cdate); // Conversion date to timestamp ?&gt; &lt;pubDate&gt;&lt;?php echo date('r', $date);?&gt;&lt;/pubDate&gt; &lt;/item&gt; &lt;?php endforeach; ?&gt; &lt;/channel&gt; &lt;/rss&gt;</code></pre> <p>On se base sur la version RSS 2 qui est au format XML.</p> <p>Si vous souhaitez afficher uniquement les 256 premiers caractères de l'article, remplacez la ligne suivante :</p> <pre><code class="language-php">&lt;description&gt; &lt;?php echo $post-&gt;c_content; ?&gt; &lt;/description&gt;</code></pre> <p>Par le code ci-dessous :</p> <pre><code class="language-php">&lt;description&gt; &lt;?php echo character_limiter($post-&gt;c_content, 256); ?&gt; &lt;/description&gt;</code></pre> <p>Il faudra alors appeler le helper &quot;text&quot; dans le constructeur du contrôleur &quot;Feed&quot; dans le contrôleur pour que la fonction &quot;character_limiter&quot; fonctionne.</p> <h2>4) Routage</h2> <p>Dans le fichier de routage (<code>application/config/routes.php</code>) ajoutez après la route du &quot;default_controller&quot; :</p> <pre><code class="language-php"># RSS $route['feed'] = 'front/feed';</code></pre>; 2014-02-05 23:54:10 Codeigniter Blog : le back office (Partie 2) https://etienner.fr/codeigniter-blog-le-back-office-partie-2 <p>Après, s'être attaqué à la partie &quot;front&quot; du blog, la partie &quot;back office&quot; s'annonce plus longue mais semée de curiosités. En effet, il faudra dans un premier temps créer un système d'identification simple puis dans un second temps, mettre en place un système de CRUD (Create Read Update Delete) pour les articles et les rubriques dans le dashboard.</p> <h2>Objectif</h2> <p>Faire la partie back office d'un blog via une interface de connexion : <a href="http://mon-blog.com/admin">http://mon-blog.com/admin</a><br /> avec :</p> <ul> <li>2 contrôleurs</li> <li>2 modèles</li> <li>6 vues</li> <li>1 helper (cf : le front)</li> <li>Modification du fichier routes.php</li> <li>1 table de données (MySQL) (cf : le front)</li> </ul> <h2>Préparation</h2> <p>Créez 3 dossiers &quot;admin&quot; dans les répertoires ci-dessous :</p> <ul> <li><code>application/controllers</code></li> <li><code>application/models</code></li> <li><code>application/views</code></li> </ul> <p>Dans le helper (<code>application/helpers</code>) functions.php, à la suite de la fonction <code>css_url()</code>, on va ajoutez la fonction <code>js_url()</code> nous aurons besoin d'appeler du javascript :</p> <pre><code class="language-php">if ( ! function_exists('js_url')) { function js_url($nom) { return '&lt;script src="' . base_url() . 'assets/js/' . $nom . '.js"&gt;&lt;/script&gt; '; } }</code></pre> <p>Allez dans le répertoire <code>assets</code> créez un nouveau dossier &quot;js&quot; et déposez les fichiers jquery.min.js (<a href="http://jquery.com/download" target="_blank"><a href="http://jquery.com/download">http://jquery.com/download</a></a>) et bootstrap.min.js. Au niveau de la base de données, rajoutez une table &quot;user&quot; avec la structure et les données suivante :</p> <pre><code class="language-sql">CREATE TABLE IF NOT EXISTS `user` ( `u_id` int(11) NOT NULL AUTO_INCREMENT, `u_login` varchar(255) NOT NULL, `u_pass` varchar(255) NOT NULL, PRIMARY KEY (`u_id`) ); INSERT INTO `user` (`u_id`, `u_login`, `u_pass`) VALUES (1, 'admin', '1a1dc91c907325c69271ddf0c944bc72 ');</code></pre> <p>Ci-dessus, le mot de passe en MD5 est &quot;pass&quot;.<br /> Encore une fois, il s'agit pour les besoins du tutoriel, d'un système d'utilisateur basique (sans niveau d'accréditation, sans table de log&hellip;)</p> <h2>I) La connexion</h2> <p>Avant de démarrer dans l'authentification sur Codeigniter, il faut mettre une clef de chiffrement dans le fichier de configuration (application/config/config.php) afin que les sessions puissent fonctionner correctement (à défaut d'avoir le message suivant : <em>In order to use the Session class you are required to set an encryption key in your config file.</em>).<br /> A la ligne 227 :</p> <pre><code class="language-php">$config['encryption_key'] = '';</code></pre> <p>Copiez coller votre clef de chiffrement (ou bien collez en une générée sur ce site : <a href="http://jeffreybarke.net/tools/codeigniter-encryption-key-generator"><a href="http://jeffreybarke.net/tools/codeigniter-encryption-key-generator">http://jeffreybarke.net/tools/codeigniter-encryption-key-generator</a></a>).<br /> Concrètement, on veut, dans un 1er temps diriger l'utilisateur sur le formulaire d'authentification comprenant 2 champs : &quot;login&quot; et &quot;password&quot;.<br /> Si le login et le mot de passe correspondent (via une fonction callback) alors on laisse l'utilisateur accéder à son dashboard et de ce fait, sa session est ouverte, sinon on l'invite à réessayer.<br /> On met aussi une fonction logout qui détruira la session lorsque l'utilisateur voudra se déconnecter du dashboard.<br /> L'url vers le formulaire (après configuration des routes) sera : <a href="http://localhost/blog/admin">http://localhost/blog/admin</a>.</p> <h3>a) Contrôleur &quot;Admin&quot;</h3> <p>Dans le dossier <code>application/controllers/admin</code>, créez un contrôleur que vous nommez &quot;admin.php&quot;.</p> <pre><code class="language-php">&lt;?php if ( ! defined('BASEPATH')) exit('No direct script access allowed'); class Admin extends CI_Controller { public function __construct() { parent::__construct(); $this-&gt;load-&gt;database(); $this-&gt;load-&gt;model('admin/model_user'); $this-&gt;load-&gt;library(array('encrypt','session')); $this-&gt;load-&gt;helper(array('functions', 'url')); session_start(); } function index() { if(!$this-&gt;session-&gt;userdata('logged_in')): $this-&gt;load-&gt;library('form_validation'); // Mise en place du formulaire $this-&gt;form_validation-&gt;set_rules('username', 'Username', 'trim|required|xss_clean'); $this-&gt;form_validation-&gt;set_rules('password', 'Password', 'trim|required|xss_clean|callback_check_database'); // Si le formulaira n'est pas bon if($this-&gt;form_validation-&gt;run() == FALSE): $data['title'] = 'Connexion'; $this-&gt;load-&gt;view('admin/view_form_login', $data); else: $this-&gt;session-&gt;set_flashdata('success', 'Bienvenue sur votre dashboard.'); // Redirection vers le dashboard redirect(base_url('admin/dashboard')); endif; elseif($this-&gt;session-&gt;userdata('logged_in')): redirect(base_url('admin/dashboard')); echo 'ok'; endif; } // Vérification login / mot de passe dans la BDD function check_database($password) { $login = $this-&gt;input-&gt;post('username'); $query = $this-&gt;model_user-&gt;login($login, $password); if($query): $sess_array = array(); foreach($query as $row): $sess_array = array( 'id' =&gt; $row-&gt;u_id, 'login' =&gt; $row-&gt;u_login ); $u_id = $row-&gt;u_id; // Création de la session $this-&gt;session-&gt;set_userdata('logged_in', $sess_array); endforeach; return TRUE; else: $this-&gt;form_validation-&gt;set_message('check_database', 'Login ou mot de passe incorrect'); return FALSE; endif; } // Déconnexion du dashboard public function logout() { $this-&gt;session-&gt;unset_userdata('logged_in'); $this-&gt;session-&gt;set_flashdata('success', 'Vous êtes désormais déconnecté(e).'); session_destroy(); redirect(base_url('admin'), 'refresh'); } } /* End of file admin.php */ /* Location: ./application/controllers/admin/admin.php */</code></pre> <p>Attention : dans ce contrôleur, nous utilisons le helper &quot;functions&quot; (<code>application/helpers/functions_helper.php</code>) créé dans le précédent tuto.</p> <h3>b) Modèle &quot;Model_user&quot;</h3> <p>Dans le dossier <code>application/models/admin</code>, créez un modèle que vous nommez &quot;model_user.php&quot;.<br /> Nous n'utiliserons qu'une seule fonction afin de déterminer si le login et le mot de passe correspondent aux champs rentrés par l'utilisateur.</p> <pre><code class="language-php">&lt;?php if ( ! defined('BASEPATH')) exit('No direct script access allowed'); class Model_user extends CI_Model { function login($login, $password) { $this-&gt;db-&gt;select('*') -&gt;from('user') -&gt;where('u_login', $login) -&gt;where('u_pass', MD5($password)) -&gt;limit(1); $query = $this-&gt;db-&gt;get(); if($query-&gt;num_rows() == 1): return $query-&gt;result(); else: return false; endif; } } /* End of file model_user.php */ /* Location: ./application/models/admin/model_user.php */</code></pre> <h3>c) Vue view_form_login</h3> <p>Dans le dossier <code>application/views/admin</code>, créez une vue que vous nommez &quot;view_form_login.php&quot;.</p> <pre><code class="language-markup">&lt;!DOCTYPE html&gt; &lt;html lang="fr"&gt; &lt;head&gt; &lt;meta charset="utf-8"&gt; &lt;title&gt;&lt;?php echo $title; ?&gt;&lt;/title&gt; &lt;meta name="description" content="&lt;?php echo $title; ?&gt;" /&gt; &lt;?php echo css_url('bootstrap.min'); ?&gt; &lt;/head&gt; &lt;body&gt; &lt;div class="container"&gt; &lt;div class="row"&gt; &lt;div class="col-md-7 col-md-offset-2 panel panel-default"&gt; &lt;?php if($this-&gt;session-&gt;flashdata('success')): ?&gt; &lt;div class="alert alert-success"&gt; &lt;?php echo $this-&gt;session-&gt;flashdata('success'); ?&gt; &lt;a class="close" data-dismiss="alert" href="#"&gt;&amp;times;&lt;/a&gt; &lt;/div&gt; &lt;?php endif; ?&gt; &lt;?php if(validation_errors()): ?&gt; &lt;?php echo validation_errors('&lt;div class="alert alert-danger"&gt;', ' &lt;a class="close" data-dismiss="alert" href="#"&gt;&amp;times;&lt;/a&gt;&lt;/div&gt;'); ?&gt; &lt;?php endif; ?&gt; &lt;h1 class="text-center"&gt;&lt;?php echo $title; ?&gt;&lt;/h1&gt; &lt;?php echo form_open(base_url('admin')); ?&gt; &lt;div class="input-prepend"&gt; &lt;label class="col-sm-1 control-label" for="username"&gt; &lt;i class="glyphicon glyphicon-user" style="font-size: 43px;"&gt;&lt;/i&gt; &lt;/label&gt; &lt;div class="col-sm-11"&gt; &lt;input type="text" class="form-control input-lg" placeholder="Username" name="username" id="username" required /&gt; &lt;/div&gt; &lt;br/&gt; &lt;br/&gt; &lt;br/&gt; &lt;label class="col-sm-1 control-label" for="password"&gt; &lt;i class="glyphicon glyphicon-lock" style="font-size: 43px;"&gt;&lt;/i&gt; &lt;/label&gt; &lt;div class="col-sm-11"&gt; &lt;input type="password" class="form-control input-lg" placeholder="Password" name="password" id="password" required /&gt; &lt;/div&gt; &lt;br/&gt; &lt;br/&gt; &lt;br/&gt; &lt;div class="row"&gt; &lt;div class="col-md-4 col-md-offset-10"&gt; &lt;input type="submit" value="Login" class="col-md-4 btn btn-lg btn-primary" /&gt; &lt;/div&gt; &lt;/div&gt; &lt;br/&gt; &lt;/div&gt; &lt;/form&gt; &lt;/div&gt;&lt;!-- end .main content --&gt; &lt;/div&gt;&lt;!-- end .row --&gt; &lt;/div&gt;&lt;!-- end .container --&gt; &lt;?php echo js_url('jquery.min'); echo js_url('bootstrap.min'); ?&gt; &lt;/body&gt; &lt;/html&gt;</code></pre> <h2>Le Dashboard</h2> <p>Le dashboard va permettre de gérer le contenu présent sur le blog, c'est-à-dire les articles et les rubriques. Ainsi, dans le dashboard on aura la liste des articles, des rubriques, l'ajout, la modification ou la suppression du contenu.</p> <h3>1) Contrôleur &quot;Dashboard&quot;</h3> <p>Dans le dossier <code>application/controllers/admin</code>, créez un contrôleur que vous nommez &quot;dashboard.php&quot;.</p> <pre><code class="language-php">&lt;?php if ( ! defined('BASEPATH')) exit('No direct script access allowed'); class Dashboard extends CI_Controller { public function __construct() { parent::__construct(); $this-&gt;load-&gt;database(); $this-&gt;load-&gt;model('admin/model_admin'); $this-&gt;load-&gt;library(array('form_validation', 'session')); $this-&gt;load-&gt;helper(array('functions', 'text', 'url')); define('URL_LAYOUT' , 'admin/view_dashboard'); define('URL_HOME_CONTENT', 'admin/dashboard'); define('URL_HOME_RUBRIC' , 'admin/dashboard/rubric'); session_start(); } // Obtenir le login private function get_login() { $data = $this-&gt;session-&gt;userdata('logged_in'); return $data['login']; } // Obtenir la redirection private function get_redirect() { $data = $this-&gt;session-&gt;set_flashdata('alert', 'Cette page est protégée par un mot de passe.').redirect(base_url('admin')); return $data; }</code></pre> <p>Attention : dans ce contrôleur, nous utilisons le helper &quot;functions&quot; (<code>application/helpers/functions_helper.php</code>) créé dans le précédent tuto.</p> <h4>Afficher tous les articles (page d'accueil)</h4> <pre><code class="language-php">// Afficher tous les articles function index() { if ($this-&gt;session-&gt;userdata('logged_in')): // Pour afficher le login $data['login'] = $this-&gt;get_login(); $data['page'] = 'home'; $data['title'] = 'Ma dashboard'; $data['query'] = $this-&gt;model_admin-&gt;read_content(); $this-&gt;load-&gt;view(URL_LAYOUT, $data); else: $this-&gt;get_redirect(); endif; }</code></pre> <h4>b) Ajouter / modifier un article</h4> <pre><code class="language-php">// Ajouter ou éditer un article function edit($c_id = '') { if ($this-&gt;session-&gt;userdata('logged_in')): // Pour afficher le login $data['login'] = $this-&gt;get_login(); // Chargement des rubriques $data['rubrics'] = $this-&gt;model_admin-&gt;read_rubric(); // Mise en place du formulaire $this-&gt;form_validation-&gt;set_rules('c_title', 'Titre', 'trim|required'); $this-&gt;form_validation-&gt;set_rules('c_content', 'Contenu', 'trim|required'); $this-&gt;form_validation-&gt;set_rules('rubric', 'Rubrique', 'required'); // Assignations du formulaire $c_title = $this-&gt;input-&gt;post('c_title'); $c_content = $this-&gt;input-&gt;post('c_content'); $r_id = $this-&gt;input-&gt;post('rubric'); // On vérifie si c'est pour ajouter ou modifier via l'URI if ($this-&gt;uri-&gt;total_segments() == 3): $data['page'] = 'add_content'; $data['title'] = 'Ajouter un article'; // Réécriture du titre pour la future URL de l'article $c_url_rw = url_title(convert_accented_characters($c_title), '-', TRUE); if ($this-&gt;form_validation-&gt;run() !== FALSE): $this-&gt;model_admin-&gt;create_content($r_id, $c_title, $c_content, $c_url_rw); $this-&gt;session-&gt;set_flashdata('success', 'Article "' . $c_title . '" ajouté.'); redirect(URL_HOME_CONTENT); endif; else: $data['page'] = 'edit_content'; $row = $this-&gt;model_admin-&gt;get_content($c_id)-&gt;row(); $data['r_id'] = $row-&gt;r_id; $data['c_title'] = $row-&gt;c_title; $data['c_content'] = $row-&gt;c_content; $data['title'] = 'Modifer la rubrique ' . $data['c_title']; if ($this-&gt;form_validation-&gt;run() !== FALSE): $this-&gt;model_admin-&gt;update_content($r_id, $c_title, $c_content, $c_id); $this-&gt;session-&gt;set_flashdata('success', 'Article "' . $c_title . '" modifié.'); redirect(URL_HOME_CONTENT); endif; endif; $this-&gt;load-&gt;view(URL_LAYOUT, $data); else: $this-&gt;get_redirect(); endif; }</code></pre> <h4>c) Supprimer un article</h4> <pre><code class="language-php">// Supprimer un article function delete($id = '') { if ($this-&gt;session-&gt;userdata('logged_in')): // Si l'utilisateur existe, on peut le supprimer if ($this-&gt;model_admin-&gt;get_content($id)-&gt;num_rows() == 1): $this-&gt;model_admin-&gt;delete_content($id); $this-&gt;session-&gt;set_flashdata('success', 'L'article a bien été supprimé'); redirect(base_url('admin')); // Sinon on affiche le message ci-dessous : else: $this-&gt;session-&gt;set_flashdata('alert', 'Cette article n'existe pour ou n'a jamais existé'); redirect(base_url(URL_HOME_CONTENT)); endif; else: $this-&gt;get_redirect(); endif; }</code></pre> <h4>d) Afficher toutes les catégories</h4> <pre><code class="language-php">// Afficher toutes les rubriques function rubric() { if ($this-&gt;session-&gt;userdata('logged_in')): // Pour afficher le login $data['login'] = $this-&gt;get_login(); $data['page'] = 'rubric'; $data['title'] = 'Rubriques'; $data['query'] = $this-&gt;model_admin-&gt;read_rubric(); $this-&gt;load-&gt;view(URL_LAYOUT, $data); else: $this-&gt;get_redirect(); endif; }</code></pre> <h4>e) Ajouter / modifier une catégorie</h4> <pre><code class="language-php">// Ajouter ou modifier une rubrique function edit_rubric($r_id = '') { if ($this-&gt;session-&gt;userdata('logged_in')): // Pour afficher le login $data['login'] = $this-&gt;get_login(); // Mise en place du formulaire via form-validation $this-&gt;form_validation-&gt;set_rules('r_title', 'Titre', 'trim|required'); $this-&gt;form_validation-&gt;set_rules('r_description', 'Description', 'trim|required'); // Assignations du formulaire $r_title = $this-&gt;input-&gt;post('r_title'); $r_description = $this-&gt;input-&gt;post('r_description'); // On vérifie si c'est pour ajouter ou modifier via l'URI if ($this-&gt;uri-&gt;total_segments() == 3): $data['page'] = 'add_rubric'; $data['title'] = 'Ajouter une rubrique'; // Réécriture du titre pour la future URL de la rubrique $r_url_rw = url_title(convert_accented_characters($r_title), '-', TRUE); if ($this-&gt;form_validation-&gt;run() !== FALSE): $this-&gt;model_admin-&gt;create_rubric($r_title, $r_description, $r_url_rw); $this-&gt;session-&gt;set_flashdata('success', 'Rubrique "' . $r_title . '" ajoutée'); redirect(base_url(URL_HOME_RUBRIC)); endif; else: $data['page'] = 'edit_rubric'; $row = $this-&gt;model_admin-&gt;get_rubric($r_id)-&gt;row(); $data['r_title'] = $row-&gt;r_title; $data['r_description'] = $row-&gt;r_description; $data['title'] = 'Mofidifer la rubrique ' . $data['r_title']; if($this-&gt;form_validation-&gt;run() !== FALSE): $this-&gt;model_admin-&gt;update_rubric($r_title, $r_description, $r_id); $this-&gt;session-&gt;set_flashdata('success', 'Catégorie "' . $r_title . '" modifiée.'); redirect(base_url(URL_HOME_RUBRIC)); endif; endif; $this-&gt;load-&gt;view(URL_LAYOUT, $data); else: $this-&gt;get_redirect(); endif; }</code></pre> <h4>f) Supprimer une catégorie</h4> <pre><code class="language-php">// Supprimer une catégorie function delete_rubric($r_id) { if ($this-&gt;session-&gt;userdata('logged_in')): // On vérifie si la rubrique existe toujours if ($this-&gt;model_admin-&gt;get_rubric($r_id)-&gt;num_rows() == 1): // On vérifie si il y a des articles rattachés à cette rubrique if ($this-&gt;model_admin-&gt;get_content_by_rubric($r_id)-&gt;num_rows() == 0): $this-&gt;model_admin-&gt;delete_rubric($r_id); $this-&gt;session-&gt;set_flashdata('success', 'Rubrique supprimée.'); else: $this-&gt;session-&gt;set_flashdata('alert', 'Impossible de supprimer cette rubrique car il y a un ou plusieurs article(s) rattaché(s).'); endif; else: $this-&gt;session-&gt;set_flashdata('alert', 'Cette rubrique n'existe pas ou n'a jamais existé.'); endif; redirect(base_url(URL_HOME_RUBRIC)); else: $this-&gt;get_redirect(); endif; }</code></pre> <p>Et on ferme le contrôleur :</p> <pre><code class="language-php">} /* End of file dashboard.php */ /* Location: ./application/controllers/admin/dashboard.php */</code></pre> <h3>2) Modèle Model_admin</h3> <p>Dans le dossier <code>application/models/admin</code>, créez un modèle &quot;model_admin.php&quot;.</p> <pre><code class="language-php">&lt;?php if ( ! defined('BASEPATH')) exit('No direct script access allowed'); class Model_admin extends CI_Model {</code></pre> <h4>a) Requêtes pour les articles CREATE</h4> <p>1) Create : Créer un article</p> <pre><code class="language-php">// Create : Créer un article function create_content($r_id, $c_title, $c_content, $c_url_rw) { $date = new DateTime(null, new DateTimeZone('Europe/Paris')); $data = array( 'r_id' =&gt; $r_id, 'c_title' =&gt; $c_title, 'c_content' =&gt; $c_content, 'c_cdate' =&gt; $date-&gt;format('Y-m-d H:i:s'), 'c_udate' =&gt; $date-&gt;format('Y-m-d H:i:s'), 'c_url_rw' =&gt; $c_url_rw, ); $this-&gt;db-&gt;insert('content', $data); }</code></pre> <p>2) Read : Lire tous les articles</p> <pre><code class="language-php">// Read : Lire tous les articles function read_content() { $this-&gt;db-&gt;select('c_id, c_title, c_content, c_cdate, c_udate, c_url_rw, r_title, r_url_rw') -&gt;from('content') -&gt;join('rubric', 'rubric.r_id = content.r_id') -&gt;order_by('c_id', 'DESC'); $query = $this-&gt;db-&gt;get(); return $query; }</code></pre> <p>3) Lire un article en particulier</p> <pre><code class="language-php">// Lire un article en particulier : function get_content($id) { $this-&gt;db-&gt;select('c_id, content.r_id, c_title, c_content') -&gt;from('content') -&gt;join('rubric', 'content.r_id = rubric.r_id') -&gt;where('c_id', $id) -&gt;limit(1); $query = $this-&gt;db-&gt;get(); return $query; }</code></pre> <p>4) Le contenu dans une rubrique spécifique</p> <pre><code class="language-php">// Le contenu dans une rubrique spécifique : function get_content_by_rubric($id) { $this-&gt;db-&gt;select('c_id') -&gt;from('content') -&gt;join('rubric', 'content.r_id = rubric.r_id') -&gt;where('rubric.r_id', $id); $query = $this-&gt;db-&gt;get(); return $query; }</code></pre> <p>5) Update : mettre à jour un article</p> <pre><code class="language-php">// Update : mettre à jour un article function update_content($r_id, $c_title, $c_content, $c_id) { $date = new DateTime(null, new DateTimeZone('Europe/Paris')); $data = array( 'r_id' =&gt; $r_id, 'c_title' =&gt; $c_title, 'c_content' =&gt; $c_content, 'c_udate' =&gt; $date-&gt;format('Y-m-d H:i:s'), ); $this-&gt;db-&gt;where('c_id', $c_id); $this-&gt;db-&gt;update('content', $data); }</code></pre> <p>6) Delete : supprimer un article</p> <pre><code class="language-php">// Delete : supprimer un article : function delete_content($id) { $this-&gt;db-&gt;where('c_id', $id) -&gt;delete('content'); }</code></pre> <h4>b) Requêtes pour les catégories</h4> <p>1) Create : créer une rubrique</p> <pre><code class="language-php">// Create : créer une rubrique function create_rubric($r_title, $r_description, $r_url_rw) { $data = array( 'r_title' =&gt; $r_title, 'r_description' =&gt; $r_description, 'r_url_rw' =&gt; $r_url_rw ); $this-&gt;db-&gt;insert('rubric', $data); }</code></pre> <p>2) Read : Lire toutes les rubriques</p> <pre><code class="language-php">// Read : Lire toutes les rubriques function read_rubric() { $this-&gt;db-&gt;select('r_id, r_title, r_description') -&gt;from('rubric') -&gt;order_by('r_id', 'DESC'); $query = $this-&gt;db-&gt;get(); return $query; }</code></pre> <p>3) Lire une rubrique en particulier</p> <pre><code class="language-php">// Lire une rubrique en particulier function get_rubric($id) { $this-&gt;db-&gt;select('r_title, r_description') -&gt;from('rubric') -&gt;where('r_id', $id) -&gt;limit(1); $query = $this-&gt;db-&gt;get(); return $query; }</code></pre> <p>4) Update : mettre à jour une rubrique</p> <pre><code class="language-php">// Update : mettre à jour une rubrique function update_rubric($r_title, $r_description, $r_id) { $data = array( 'r_title' =&gt; $r_title, 'r_description' =&gt; $r_description ); $this-&gt;db-&gt;where('r_id', $r_id); $this-&gt;db-&gt;update('rubric', $data); }``` 5) Delete : supprimer une rubrique ```php // Delete : supprimer une rubrique function delete_rubric($id) { $this-&gt;db-&gt;where('r_id', $id) -&gt;delete('rubric'); }</code></pre> <h4>Et on ferme le modèle :</h4> <pre><code class="language-php">} /* End of file model_admin.php */ /* Location: ./application/models/admin/model_admin.php */</code></pre> <h3>3) Vues</h3> <h4>a) Mise en place d'un layout :</h4> <p>Dans le dossier <code>application/views/admin</code>.</p> <ul> <li>&quot;view_dashboard.php&quot; : contiendra le header et le footer</li> </ul> <p>Créez un dossier et nommez-le &quot;dashboard&quot;, puis créez les 5 fichiers suivant : </p> <ul> <li>&quot;view_admin_article_home.php&quot; : affichera tous les articles</li> <li>&quot;view_admin_article_edit.php&quot; : affichera l'édition d'un article (insertion / modification)</li> <li>&quot;view_admin_categorie_home.php&quot; : affichera toutes les rubriques</li> <li>&quot;view_admin_categorie_edit.php&quot; : affichera l'édition d'une rubrique (insertion / modification)</li> </ul> <h4>b) &quot;view_dashboard.php&quot;</h4> <pre><code class="language-markup">&lt;!DOCTYPE html&gt; &lt;html lang="fr"&gt; &lt;head&gt; &lt;meta charset="utf-8"&gt; &lt;title&gt;&lt;?php echo $title; ?&gt;&lt;/title&gt; &lt;meta name="description" content="&lt;?php echo $title ; ?&gt;" /&gt; &lt;meta name="viewport" content="width=device-width, initial-scale=1.0"&gt; &lt;?php echo css_url('bootstrap.min'); ?&gt; &lt;/head&gt; &lt;body class="container"&gt; &lt;nav class="navbar navbar-default" role="navigation"&gt; &lt;div class="navbar-header"&gt; &lt;button type="button" class="navbar-toggle" data-toggle="collapse" data-target="#bs-example-navbar-collapse-1"&gt; &lt;span class="sr-only"&gt;Toggle navigation&lt;/span&gt; &lt;span class="icon-bar"&gt;&lt;/span&gt; &lt;span class="icon-bar"&gt;&lt;/span&gt; &lt;span class="icon-bar"&gt;&lt;/span&gt; &lt;/button&gt;&lt;!-- end .navbar-toggle --&gt; &lt;a class="navbar-brand" href="&lt;?php echo base_url('admin/dashboard'); ?&gt;"&gt;Dashboard&lt;/a&gt; &lt;/div&gt;&lt;!-- end .navbar-header --&gt; &lt;div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1"&gt; &lt;ul class="nav navbar-nav"&gt; &lt;li &lt;?php if ($page == 'home' or $page == 'add_content' or $page == 'edit_content'){ echo "class='active'"; }; ?&gt;&gt; &lt;a href="&lt;?php echo base_url('admin/dashboard'); ?&gt;"&gt; Articles &lt;/a&gt; &lt;/li&gt; &lt;li &lt;?php if ($page == 'rubric' or $page == 'add_rubric' or $page == 'edit_rubric'){ echo "class='active'"; }; ?&gt;&gt; &lt;a href="&lt;?php echo base_url('admin/dashboard/rubric'); ?&gt;"&gt; Rubriques &lt;/a&gt; &lt;/li&gt; &lt;!-- end .nav navbar-nav --&gt; &lt;ul class="nav navbar-nav navbar-right"&gt; &lt;li&gt; &lt;a href="&lt;?php echo base_url(); ?&gt;" target="_blank"&gt;Le blog&lt;/a&gt; &lt;/li&gt; &lt;li class="dropdown"&gt; &lt;a href="#" class="dropdown-toggle" data-toggle="dropdown"&gt;&lt;?php echo $login; ?&gt; &lt;b class="caret"&gt;&lt;/b&gt;&lt;/a&gt; &lt;ul class="dropdown-menu"&gt; &lt;li&gt; &lt;a href="&lt;?php echo base_url('admin/logout'); ?&gt;"&gt; Se déconnecter &lt;/a&gt; &lt;/li&gt; &lt;!-- end .dropdown-menu--&gt; &lt;!-- end .dropdown --&gt; &lt;!-- end .nav .navbar-nav .navbar-right --&gt; &lt;/div&gt;&lt;!-- end .collapse .navbar-collapse #bs-example-navbar-collapse-1 --&gt; &lt;/nav&gt;&lt;!-- end .navbar .navbar-default --&gt; &lt;?php if ($this-&gt;session-&gt;flashdata('success')): ?&gt; &lt;div class="alert alert-success"&gt; &lt;?php echo $this-&gt;session-&gt;flashdata('success'); ?&gt; &lt;a class="close" data-dismiss="alert" href="#"&gt;&amp;times;&lt;/a&gt; &lt;/div&gt; &lt;?php endif; ?&gt; &lt;?php if ($this-&gt;session-&gt;flashdata('alert')): ?&gt; &lt;div class="alert alert-danger"&gt; &lt;?php echo $this-&gt;session-&gt;flashdata('alert'); ?&gt; &lt;a class="close" data-dismiss="alert" href="#"&gt;&amp;times;&lt;/a&gt; &lt;/div&gt; &lt;?php endif; ?&gt; &lt;div class="row"&gt; &lt;div class="col-md-2"&gt; &lt;ul class="nav nav-pills nav-stacked"&gt; &lt;li&gt; &lt;button onClick="window.location.href='&lt;?php echo base_url('admin/dashboard/edit'); ?&gt;'" class="btn btn-danger"&gt; &lt;i class="glyphicon glyphicon-plus"&gt;&lt;/i&gt; Ajouter un article &lt;/button&gt; &lt;/li&gt; &lt;li&gt; &lt;button onClick="window.location.href='&lt;?php echo base_url('admin/dashboard/edit_rubric'); ?&gt;'" class="btn btn-primary"&gt; &lt;i class="glyphicon glyphicon-plus"&gt;&lt;/i&gt; Ajouter une rubrique &lt;/button&gt; &lt;/li&gt; &lt;!-- end of .col-md-2 .nav-stacked --&gt; &lt;/div&gt;&lt;!-- end of .col-md-2 --&gt; &lt;div class="col-md-10"&gt; &lt;?php switch ($page) { case 'home': $this-&gt;load-&gt;view('admin/dashboard/view_listing_content'); break; case 'add_content': case 'edit_content': $this-&gt;load-&gt;view('admin/dashboard/view_edit_content'); break; case 'rubric': $this-&gt;load-&gt;view('admin/dashboard/view_listing_rubric'); break; case 'add_rubric': case 'edit_rubric': $this-&gt;load-&gt;view('admin/dashboard/view_edit_rubric'); break; default: break; } ?&gt; &lt;/div&gt;&lt;!-- end .col-md-10 --&gt; &lt;/div&gt;&lt;!-- end .row --&gt; &lt;footer&gt; &lt;footer data-role="footer"&gt; &lt;p class="footer" style="text-align: center"&gt;Propulsé par Codeigniter - Temps d'exécution : &lt;strong&gt;0.0610&lt;/strong&gt; seconds &lt;/footer&gt; &lt;/footer&gt; &lt;?php echo js_url('jquery.min'); echo js_url('bootstrap.min'); ?&gt; &lt;/body&gt; &lt;/html&gt;</code></pre> <h4>c) &quot;view_listing_content.php&quot;</h4> <pre><code class="language-markup">&lt;?php if($query-&gt;num_rows() &gt; 0): ?&gt; &lt;div class="table-responsive"&gt; &lt;table class="table table-hover"&gt; &lt;tr&gt; &lt;th&gt;ID&lt;/th&gt; &lt;th&gt;Titre&lt;/th&gt; &lt;th&gt;Description&lt;/th&gt; &lt;th&gt;Rubrique&lt;/th&gt; &lt;th&gt;Date&lt;/th&gt; &lt;th&gt;MAJ&lt;/th&gt; &lt;th&gt;&lt;/th&gt; &lt;th&gt;&lt;/th&gt; &lt;/tr&gt; &lt;?php foreach($query-&gt;result() as $row): ?&gt; &lt;tr&gt; &lt;td&gt;&lt;?php echo $row-&gt;c_id; ?&gt;&lt;/td&gt; &lt;td&gt;&lt;?php echo $row-&gt;c_title; ?&gt;&lt;/td&gt; &lt;td&gt;&lt;?php echo character_limiter($row-&gt;c_content, 64); ?&gt;&lt;/td&gt; &lt;td&gt;&lt;?php echo $row-&gt;r_title; ?&gt;&lt;/td&gt; &lt;td&gt;&lt;?php echo date("d/m/Y à H:i:s", strtotime($row-&gt;c_cdate)); ?&gt;&lt;/td&gt; &lt;td&gt;&lt;?php echo date("d/m/Y à H:i:s", strtotime($row-&gt;c_udate)); ?&gt;&lt;/td&gt; &lt;td&gt;&lt;a href="&lt;?php echo base_url('admin/dashboard/edit/' . $row-&gt;c_id); ?&gt;" title="Modifier"&gt;&lt;i class="glyphicon glyphicon-pencil"&gt;&lt;/i&gt;&lt;/a&gt;&lt;/td&gt; &lt;td&gt;&lt;a href="&lt;?php echo base_url('admin/dashboard/delete/' . $row-&gt;c_id); ?&gt;" onclick="return deleteConfirm()" title="Supprimer"&gt;&lt;i class="glyphicon glyphicon-trash"&gt;&lt;/i&gt;&lt;/a&gt;&lt;/td&gt; &lt;/tr&gt; &lt;?php endforeach; ?&gt; &lt;/table&gt;&lt;!-- end .table .table-hover --&gt; &lt;/div&gt;&lt;!-- end .table-responsive --&gt; &lt;script&gt; function deleteConfirm() { var a = confirm("Etes-vous sur de vouloir supprimer cet article ?!"); if (a){ return true; } else{ return false; } } &lt;/script&gt; &lt;?php endif; ?&gt;</code></pre> <h4>d) &quot;view_edit_content.php&quot;</h4> <pre><code class="language-markup">&lt;?php if (validation_errors()): echo validation_errors('&lt;div class="alert alert-danger"&gt;', ' &lt;a class="close" data-dismiss="alert" href="#"&gt;&amp;times;&lt;/a&gt;&lt;/div&gt;'); endif; echo form_open(current_url()); // $config['index_page'] = ''; dans config/config.php ?&gt; &lt;div class="form-group"&gt; &lt;label for="c_title"&gt;Titre de l'article:&lt;/label&gt; &lt;input type="text" class="form-control" id="c_title" name="c_title" value="&lt;?php if(isset($c_title)): echo $c_title; else: echo set_value('c_title'); endif; ?&gt;" required /&gt; &lt;/div&gt;&lt;!-- end .form-group --&gt; &lt;div class="form-group"&gt; &lt;label for="c_content"&gt;Contenu de l'article :&lt;/label&gt; &lt;textarea id="c_content" class="form-control" name="c_content" required&gt;&lt;?php if(isset($c_content)): echo $c_content; else: echo set_value('c_content'); endif; ?&gt;&lt;/textarea&gt; &lt;/div&gt;&lt;!-- end .form-group --&gt; Rubriques : &lt;?php foreach ($rubrics-&gt;result() as $row): ?&gt; &lt;div class="radio"&gt; &lt;label&gt; &lt;input type="radio" name="rubric" id="&lt;?php echo $row-&gt;r_title ;?&gt;" value="&lt;?php echo $row-&gt;r_id ;?&gt;" &lt;?php if( $page == 'edit_content' and isset($rubrics) and $row-&gt;r_id == $r_id or set_value('rubrique') == $row-&gt;r_id ){ echo 'checked="checked"'; } ?&gt; required /&gt; &lt;?php echo $row-&gt;r_title; ?&gt; &lt;/label&gt; &lt;/div&gt;&lt;!-- end .radio --&gt; &lt;?php endforeach; ?&gt; &lt;input type="submit" class="btn btn-default" value="&lt;?php if ($page == 'add_content'){ echo 'Ajouter';} else{ echo 'Modifier'; }; ?&gt;" /&gt; &lt;/form&gt;</code></pre> <h4>e) &quot;view_listing_rubric.php&quot;</h4> <pre><code class="language-markup">&lt;?php if($query-&gt;num_rows() &gt; 0): ?&gt; &lt;div class="table-responsive"&gt; &lt;table class="table table-hover"&gt; &lt;tr&gt; &lt;th&gt;ID&lt;/th&gt; &lt;th&gt;Titre&lt;/th&gt; &lt;th&gt;Description&lt;/th&gt; &lt;th&gt;&lt;/th&gt; &lt;th&gt;&lt;/th&gt; &lt;/tr&gt; &lt;?php foreach($query-&gt;result() as $row): ?&gt; &lt;tr&gt; &lt;td&gt;&lt;?php echo $row-&gt;r_id; ?&gt;&lt;/td&gt; &lt;td&gt;&lt;?php echo $row-&gt;r_title; ?&gt;&lt;/td&gt; &lt;td&gt;&lt;?php echo character_limiter($row-&gt;r_description, 64); ?&gt;&lt;/td&gt; &lt;td&gt;&lt;a href="&lt;?php echo base_url('admin/dashboard/edit_rubric/' . $row-&gt;r_id); ?&gt;" title="Modifier"&gt;&lt;i class="glyphicon glyphicon-pencil"&gt;&lt;/i&gt;&lt;/a&gt;&lt;/td&gt; &lt;td&gt;&lt;a href="&lt;?php echo base_url('admin/dashboard/delete_rubric/' . $row-&gt;r_id); ?&gt;" onclick="return deleteConfirm()" title="Supprimer" &gt;&lt;i class="glyphicon glyphicon-trash"&gt;&lt;/i&gt;&lt;/a&gt;&lt;/td&gt; &lt;/tr&gt; &lt;?php endforeach; ?&gt; &lt;/table&gt;&lt;!-- end .table .table-hover --&gt; &lt;/div&gt;&lt;!-- end .table-responsive --&gt; &lt;script&gt; function deleteConfirm() { var a = confirm("Etes-vous sur de vouloir supprimer cette catégorie ?!"); if (a){ return true; } else{ return false; } } &lt;/script&gt; &lt;?php endif; ?&gt;</code></pre> <h4>d) &quot;view_edit_rubric.php&quot;</h4> <pre><code class="language-markup">&lt;?php if (validation_errors()): echo validation_errors('&lt;div class="alert alert-danger"&gt;', ' &lt;a class="close" data-dismiss="alert" href="#"&gt;&amp;times;&lt;/a&gt;&lt;/div&gt;'); endif; echo form_open(base_url(uri_string())); ?&gt; &lt;div class="form-group"&gt; &lt;label for="r_title"&gt;Titre de la rubrique:&lt;/label&gt; &lt;input type="text" class="form-control" id="r_title" name="r_title" value="&lt;?php if(isset($r_title)): echo $r_title; else: echo set_value('r_title'); endif; ?&gt;" required /&gt; &lt;/div&gt;&lt;!-- end .form-group --&gt; &lt;div class="form-group"&gt; &lt;label for="r_description"&gt;Description (256 caractères) de la rubrique :&lt;/label&gt; &lt;input type="text" id="r_description" class="form-control" name="r_description" value="&lt;?php if(isset($r_description)): echo $r_description; else: echo set_value('r_description'); endif; ?&gt;" required /&gt; &lt;/div&gt;&lt;!-- end .form-group --&gt; &lt;input type="submit" class="btn btn-default" value="&lt;?php if ($page == 'add_rubric'){ echo 'Ajouter';} else{ echo 'Modifier'; }; ?&gt;" /&gt; &lt;/form&gt;</code></pre> <h2>3) Le routage</h2> <p>Dans le fichier de routage (<code>application/config/routes.php</code>) ajoutez après la route du default_controller :</p> <pre><code>#admin $route['admin'] = 'admin/admin'; $route['admin/logout'] = 'admin/admin/logout'; $route['admin/dashboard'] = 'admin/dashboard'; # ADMIN content $route['admin/dashboard/edit'] = 'admin/dashboard/edit'; $route['admin/dashboard/edit/(:num)'] = 'admin/dashboard/edit/$1'; $route['admin/dashboard/delete/(:num)'] = 'admin/dashboard/delete/$1'; # ADMIN rubric $route['admin/dashboard/rubric'] = 'admin/dashboard/rubric'; $route['admin/dashboard/edit_rubric'] = 'admin/dashboard/edit_rubric'; $route['admin/dashboard/edit_rubric/(:num)'] = 'admin/dashboard/edit_rubric/$1'; $route['admin/dashboard/delete_rubric/(:num)'] = 'admin/dashboard/delete_rubric/$1';</code></pre> <h2>Conclusion</h2> <p>L'interface de gestion est opérationnelle. Il ne manque plus qu'à rajouter des vérificateurs d'unicité pour éviter les doublons dans la création d'article ou de catégorie.<br /> Vous pouvez faire évoluer ce blog en mettant en place la gestion des utilisateurs plus poussée et un historique de log pour améliorer la sécurité de l'application. Mais aussi un formulaire de création d'utilisateur avec niveau d'accréditation, etc...</p>; 2014-01-20 21:15:47 Codeigniter Blog : le front (Partie 1) https://etienner.fr/codeigniter-blog-le-front-partie-1 <p>Ce tutoriel n'a pas pour but, ni la prétention de réinventer un système de blog (ce n'est pas pour faire un WordPress bis) mais pour s'entraîner à coder sur le framework Codeigniter 2 avec du HTML 5 et Twitter Bootstrap 3 pour la partie CSS. Dans cette première partie, nous allons voir la méthode slug avec la technique de routage qui s'impose par la suite afin de belles URL...<br /> Avant de commencer, j'espère que vous avez jeté un coup d'oeuil sur l'article précédent &quot;<a href="codeigniter/installation-et-preparation-de-codeigniter-2">Installation et préparation de CodeIgniter 2&quot;</a>.<br /> Le code présent dans ce tutoriel est une façon parmi tant d'autre de créer son blog. Codeigniter est loin d'être restrictif pour cela.</p> <h2>Objectif : faire la partie front d'un blog avec de l'url rewriting</h2> <p>On veut obtenir pour un article l'URL suivante : <a href="http://mon-blog.com/rubrique/mon-article">http://mon-blog.com/rubrique/mon-article</a><br /> Pour cela, il nous faut :</p> <ul> <li>1 contrôleur</li> <li>1 modèle</li> <li>7 vues (dont 3 includes)</li> <li>1 helper</li> <li>modification du fichier routes.php</li> <li>1 table de données (MySQL en utf8_unicode_ci) comportant 2 tables :</li> </ul> <pre><code class="language-sql">CREATE TABLE IF NOT EXISTS `rubric` ( `r_id` int(11) NOT NULL AUTO_INCREMENT, `r_title` varchar(255) NOT NULL, `r_description` text NOT NULL, `r_url_rw` varchar(255) NOT NULL, PRIMARY KEY (`r_id`), UNIQUE KEY `r_url_rw` (`r_url_rw`) ) ENGINE=InnoDB; CREATE TABLE IF NOT EXISTS `content` ( `c_id` int(11) NOT NULL AUTO_INCREMENT, `r_id` int(11) NOT NULL, `c_title` varchar(255) NOT NULL, `c_content` text NOT NULL, `c_cdate` datetime NOT NULL, `c_udate` datetime NOT NULL, `c_url_rw` varchar(255) NOT NULL, PRIMARY KEY (`c_id`), UNIQUE KEY `c_url_rw` (`c_url_rw`), CONSTRAINT fk_r_id FOREIGN KEY (r_id) REFERENCES rubric (r_id) ) ENGINE=InnoDB; INSERT INTO `rubric` VALUES (1, 'Rubrique 1', 'La description de la rubrique 1', 'rubrique-1'), (2, 'Rubrique 2', 'La description de la rubrique 2', 'rubrique-2'), (3, 'Rubrique 3', 'La description de la rubrique 3', 'rubrique-3'), (4, 'Rubrique 4', 'La description de la rubrique 4', 'rubrique-4'); INSERT INTO `content` VALUES (1, 1, 'Article 1', "Lorem ipsum 1. Généralement, on utilise un texte en faux latin (le texte ne veut rien dire, il a été modifié), le Lorem ipsum ou Lipsum, qui permet donc de faire office de texte d'attente. L'avantage de le mettre en latin est que l'opérateur sait au premier coup d'oeil que la page contenant ces lignes n'est pas valide, et surtout l'attention du client n'est pas dérangée par le contenu, il demeure concentré seulement sur l'aspect graphique.", '2013-01-05 12:00:10', '0000-00-00 00:00:00', 'article-1'), (2, 2, 'Article 2', "Lorem ipsum 2. Généralement, on utilise un texte en faux latin (le texte ne veut rien dire, il a été modifié), le Lorem ipsum ou Lipsum, qui permet donc de faire office de texte d'attente. L'avantage de le mettre en latin est que l'opérateur sait au premier coup d'oeil que la page contenant ces lignes n'est pas valide, et surtout l'attention du client n'est pas dérangée par le contenu, il demeure concentré seulement sur l'aspect graphique.", '2013-03-10 12:00:10', '0000-00-00 00:00:00', 'article-2'), (3, 1, 'Article 3', "Lorem ipsum 3. Généralement, on utilise un texte en faux latin (le texte ne veut rien dire, il a été modifié), le Lorem ipsum ou Lipsum, qui permet donc de faire office de texte d'attente. L'avantage de le mettre en latin est que l'opérateur sait au premier coup d'oeil que la page contenant ces lignes n'est pas valide, et surtout l'attention du client n'est pas dérangée par le contenu, il demeure concentré seulement sur l'aspect graphique.", '2013-05-05 12:00:10', '0000-00-00 00:00:00', 'article-3'), (4, 1, 'Article 4', "Lorem ipsum 4. Généralement, on utilise un texte en faux latin (le texte ne veut rien dire, il a été modifié), le Lorem ipsum ou Lipsum, qui permet donc de faire office de texte d'attente. L'avantage de le mettre en latin est que l'opérateur sait au premier coup d'oeil que la page contenant ces lignes n'est pas valide, et surtout l'attention du client n'est pas dérangée par le contenu, il demeure concentré seulement sur l'aspect graphique.", '2013-06-06 12:00:10', '0000-00-00 00:00:00', 'article-4'), (5, 2, 'Article 5', "Lorem ipsum 5. Généralement, on utilise un texte en faux latin (le texte ne veut rien dire, il a été modifié), le Lorem ipsum ou Lipsum, qui permet donc de faire office de texte d'attente. L'avantage de le mettre en latin est que l'opérateur sait au premier coup d'oeil que la page contenant ces lignes n'est pas valide, et surtout l'attention du client n'est pas dérangée par le contenu, il demeure concentré seulement sur l'aspect graphique.", '2013-07-01 12:00:10', '0000-00-00 00:00:00', 'article-5'), (6, 2, 'Article 6', "Lorem ipsum 6. Généralement, on utilise un texte en faux latin (le texte ne veut rien dire, il a été modifié), le Lorem ipsum ou Lipsum, qui permet donc de faire office de texte d'attente. L'avantage de le mettre en latin est que l'opérateur sait au premier coup d'oeil que la page contenant ces lignes n'est pas valide, et surtout l'attention du client n'est pas dérangée par le contenu, il demeure concentré seulement sur l'aspect graphique.", '2013-07-02 12:00:10', '0000-00-00 00:00:00', 'article-6'), (7, 3, 'Article 7', "Lorem ipsum 7. Généralement, on utilise un texte en faux latin (le texte ne veut rien dire, il a été modifié), le Lorem ipsum ou Lipsum, qui permet donc de faire office de texte d'attente. L'avantage de le mettre en latin est que l'opérateur sait au premier coup d'oeil que la page contenant ces lignes n'est pas valide, et surtout l'attention du client n'est pas dérangée par le contenu, il demeure concentré seulement sur l'aspect graphique.", '2013-07-03 12:00:10', '0000-00-00 00:00:00', 'article-7'), (8, 1, 'Article 8', "Lorem ipsum 8. Généralement, on utilise un texte en faux latin (le texte ne veut rien dire, il a été modifié), le Lorem ipsum ou Lipsum, qui permet donc de faire office de texte d'attente. L'avantage de le mettre en latin est que l'opérateur sait au premier coup d'oeil que la page contenant ces lignes n'est pas valide, et surtout l'attention du client n'est pas dérangée par le contenu, il demeure concentré seulement sur l'aspect graphique.", '2013-08-31 12:00:10', '0000-00-00 00:00:00', 'article-8'), (9, 4, 'Article 9', "Lorem ipsum 9. Généralement, on utilise un texte en faux latin (le texte ne veut rien dire, il a été modifié), le Lorem ipsum ou Lipsum, qui permet donc de faire office de texte d'attente. L'avantage de le mettre en latin est que l'opérateur sait au premier coup d'oeil que la page contenant ces lignes n'est pas valide, et surtout l'attention du client n'est pas dérangée par le contenu, il demeure concentré seulement sur l'aspect graphique.", '2013-09-01 12:00:10', '0000-00-00 00:00:00', 'article-9'), (10, 4, 'Article 10', "Lorem ipsum 10. Généralement, on utilise un texte en faux latin (le texte ne veut rien dire, il a été modifié), le Lorem ipsum ou Lipsum, qui permet donc de faire office de texte d'attente. L'avantage de le mettre en latin est que l'opérateur sait au premier coup d'oeil que la page contenant ces lignes n'est pas valide, et surtout l'attention du client n'est pas dérangée par le contenu, il demeure concentré seulement sur l'aspect graphique.", '2013-10-20 12:00:10', '0000-00-00 00:00:00', 'article-10');</code></pre> <p>Pensez à configurer votre fichier database.php (application/config) afin d'être connecté à votre base de données et n'oubliez pas le .htaccess (à la racine de votre site) pour le rewriting...</p> <h2>Principe du slug</h2> <p>Dans la table &quot;rubrique&quot;, le champ &quot;r_url_rw&quot; ainsi que le champ &quot;c_url_rw&quot; dans la table &quot;content&quot;, contiendrons respectivement l'url du titre de la rubrique et de l'article dont les mots seront séparés par des tirets (&quot;dash&quot; ou &quot;hyphens&quot;). Cela devient facile grâce au helper &quot;URL&quot; inclut dans Codeigniter :<br /> <code>$url_rw = url_title($titre, 'dash', TRUE);</code><br /> Cette fonctionnalité sera abordée dans une autre partie de ce tutoriel, du coté back-office lorsque cette dernière sera créée à partir du titre de l'article rentré par l'utilisateur.</p> <h2>1 - Le contrôleur blog</h2> <p>Dans le dossier &quot;controllers&quot; (<code>application/controllers</code>), créez un nouveau dossier &quot;front&quot; puis un contrôleur &quot;blog.php&quot;.</p> <pre><code class="language-php">&lt;?php if ( ! defined('BASEPATH')) exit('No direct script access allowed'); class Blog extends CI_Controller{ function __construct() { parent::__construct(); // Chargement des ressources pour ce controller $this-&gt;load-&gt;database(); $this-&gt;load-&gt;model('front/model_blog'); $this-&gt;load-&gt;library('pagination'); $this-&gt;load-&gt;helper(array('functions', 'text', 'url')); } public function index($numero_page = 1) { $data['page'] = 'home'; $data['title'] = 'Mon blog Codeigniter'; $data['meta_title'] = 'Mon Blog'; if($numero_page &gt; 1): $data['numero_page'] = $numero_page; $data['meta_title'] .= ' - page ' . $numero_page; endif; $data['meta_desc'] = "Mon super blog propulsé par Codeigniter 2"; // Retourne tous les articles (sidebar) $data['all_content'] = $this-&gt;model_blog-&gt;get_all_contents(); // Retourne toutes les rubriques (sidebar) $data['query_all_rubrics'] = $this-&gt;model_blog-&gt;get_all_rubrics(); // Paramêtres pour la pagination $config = pagination_custom(); # Chemin $config['base_url'] = base_url('page'); # Lien de la 1ère page de la pagination $config['first_url'] = base_url(); # Nombre de chiffres dans la pagination $config['total_rows'] = $data['all_content']-&gt;num_rows(); # Nombre total de pages possibles $config['num_links'] = round(($config['total_rows'] / $config['per_page']) + 1); # page/1 donc 2 en URI $config['uri_segment'] = 2; // Initialisation de la pagination $this-&gt;pagination-&gt;initialize($config); // Vérification du numéro de la page if($numero_page &gt; $config['num_links']): redirect(base_url('erreur404'), 404); else: $data['query'] = $this-&gt;model_blog-&gt;get_contents_listing($numero_page, $config['per_page'] ); // Génération de la pagination $data['pagination'] = $this-&gt;pagination-&gt;create_links(); endif; // Chargement des données dans la vue $this-&gt;load-&gt;view('front/view_layout', $data); } public function view($slug_rubric = '', $slug_content = '', $numero_page = 1) { // Retourne toutes les rubriques (sidebar) $data['query_all_rubrics'] = $this-&gt;model_blog-&gt;get_all_rubrics(); // Cas d'une rubrique if($this-&gt;uri-&gt;total_segments() == 1 or $this-&gt;uri-&gt;total_segments() == 3): // Récupération de tous les articles (listing) $data['all_content'] = $this-&gt;model_blog-&gt;get_all_contents(); // Paramêtres pour la pagination $config = pagination_custom(); # Chemin $config['base_url'] = base_url($slug_rubric . '/page'); # Lien de la 1ère page de la pagination $config['first_url'] = base_url($this-&gt;uri-&gt;segment(1)); # Nombre de chiffres dans la pagination $config['total_rows'] = $this-&gt;model_blog-&gt;get_contents_rubric_listing($slug_rubric, '', '')-&gt;num_rows(); # Nombre total de pages possibles $config['num_links'] = round(($config['total_rows'] / $config['per_page']) + 1); # "nom_rubrique/page/1" donc 3 en URI $config['uri_segment'] = 3; // Initialisation de la pagination $this-&gt;pagination-&gt;initialize($config); // Vérification du numéro de la page if($numero_page &gt; $config['num_links']): redirect(base_url('erreur404'), 404); else: // Récupération du contenu de la rubrique $data['query'] = $this-&gt;model_blog-&gt;get_contents_rubric_listing($slug_rubric, $numero_page, $config['per_page'] ); // Si la reqûete abouti sans résultat, on redirige vers la page 404 if ($data['query']-&gt;num_rows == 0): redirect(base_url('erreur404'), 404); endif; // Génération de la pagination $data['pagination'] = $this-&gt;pagination-&gt;create_links(); endif; // On récupère la valeur de r_url_rw pour faire une vérification ensuite $row = $data['query']-&gt;row(); $data['page'] = 'rubric'; // Encapsulation des données $data['title'] = $data['meta_title'] = $row-&gt;r_title; if($numero_page &gt; 1): $data['numero_page'] = $numero_page; $data['meta_title'] .= ' - page ' . $numero_page; endif; $data['meta_desc'] = $row-&gt;r_description; // Cas d'un article elseif($this-&gt;uri-&gt;total_segments() == 2): // Récupération du contenu de l'article $data['query_article'] = $this-&gt;model_blog-&gt;get_content($slug_rubric, $slug_content); // Si la requête sort un résultat if ($data['query_article']-&gt;num_rows() == 1): $data['page'] = 'content'; // Encapsulation des données $row = $data['query_article']-&gt;row(); $data['title'] = $data['c_title'] = $row-&gt;c_title; $data['c_content'] = $row-&gt;c_content; $data['c_cdate'] = $row-&gt;c_cdate; $data['c_url_rw'] = $row-&gt;c_url_rw; $data['r_title'] = $row-&gt;r_title; $data['r_url_rw'] = $row-&gt;r_url_rw; $data['meta_title'] = $row-&gt;c_title; $data['meta_desc'] = character_limiter(strip_tags($row-&gt;c_content, 160)); // Récupération du contenu des autres articles de la même rubrique $data['query_same_rubric'] = $this-&gt;model_blog-&gt;get_contents_same_rubric($slug_rubric, $slug_content); if(!$this-&gt;agent-&gt;mobile()): // Récupération du contenu des autres articles (sidebar) $data['all_content'] = $this-&gt;model_blog-&gt;get_contents_others($slug_content); else: $data['all_content'] = ''; endif; // Sinon on redirige vers la page 404 else: redirect(base_url('erreur404'), 404); endif; else: redirect(base_url('erreur404'), 404); endif; // Chargement des données dans la vue $this-&gt;load-&gt;view('front/view_layout', $data); } public function erreur404() { // Retourne tous les articles (sidebar) $data['all_content'] = $this-&gt;model_blog-&gt;get_all_contents(); // Retourne toutes les rubriques (sidebar) $data['query_all_rubrics'] = $this-&gt;model_blog-&gt;get_all_rubrics(); $data['page'] = '404'; $data['title'] = $data['meta_title'] = $data['meta_desc'] = 'Erreur 404 sur la page demandée'; // Instancie une vraie erreur 404 http_response_code(404); // Chargement des données dans la vue $this-&gt;load-&gt;view('front/view_layout', $data); } } /* End of file blog.php */ /* Location: ./application/controllers/blog.php */</code></pre> <h2>Explications</h2> <p>Dans la fonction de constructeur, on appel le model blog, la librairie pagination, et les helpers assets, text et url via <code>$this-&gt;load</code>. </p> <p>Dans les 2 fonctions principales à savoir &quot;index&quot; pour la page d'accueil et &quot;view&quot; pour les pages rubriques et les articles, il y a des paramètres qui reviennent comme le type de la page (<code>$data['page']</code>), le titre de la page (<code>$data['title']</code>) le titre méta (<code>['meta_title']</code>), etc...</p> <p>Il y aussi la fonction &quot;pagination_custom&quot; (<code>$config = pagination_custom();</code>) qui est appelée dans les 2 fonctions précédentes afin d'éviter de retaper du code. Cette fonction provient du helper &quot;functions&quot; que l'on verra plus bas dans ce tutoriel.</p> <p>Arrêtons-nous sur la pagination, car elle n'est pas forcement simple à comprendre du 1er coup.<br /> Tout d'abord, il faut comprendre la notion de &quot;digit&quot;.<br /> Un digit correspond au numéro dans la pagination (ci-dessous : &quot;1&quot;, &quot;2&quot;, &quot;3&quot; et &quot;4&quot;).</p> <p><img src="../assets/img/news/codeigniter_blog_front/codeigniter_blog_pagination.jpg" alt="Aperçu pagination" /></p> <p><strong>Configuration de la pagination :</strong></p> <p><strong>$config['base_url']</strong><br /> Il faut indiquer à Codeginiter l'URL pointant vers chaque digit.<br /> Exemple dans notre code :<br /> <code>$config['base_url'] = base_url('page');</code><br /> Le digit 2 va pointer sur : <a href="http://localhost/blog/page/2">http://localhost/blog/page/2</a><br /> Autre exemple :<br /> <code>$config['base_url'] = base_url($slug_rubric . '/page');</code><br /> Le digit 2 de la rubrique &quot;rubrique 1&quot; va pointer sur : <a href="http://localhost/blog/rubrique_1/page/2">http://localhost/blog/rubrique_1/page/2</a></p> <p><strong>$config['first_url']</strong><br /> Concerne le lien du digit 1 autrement dit, le lien de la 1ère page.<br /> Si l'on ne touche pas à ce paramètre alors le lien sera du type :<br /> <a href="http://localhost/blog/page/1">http://localhost/blog/page/1</a><br /> Dans notre code, on veut redirigé ce digit vers la 1ère page classique, c'est-à-dire :<br /> <a href="http://localhost/blog/page/1vers">http://localhost/blog/page/1vers</a> <a href="http://localhost/blog">http://localhost/blog</a><br /> <code>$config['first_url']= base_url();</code><br /> <a href="http://localhost/blog/rubrique_1/page/1">http://localhost/blog/rubrique_1/page/1</a> vers <a href="http://localhost/blog/rubrique_1">http://localhost/blog/rubrique_1</a><br /> <code>$config['first_url']= base_url($slug_rubrique);</code></p> <p><strong>$config['total_rows']</strong><br /> Le nombre total de content.</p> <p><br /><strong>$config['per_page']</strong><br /> <br />Le nombre de content (d'articles dans notre cas) par page.</p> <p><strong>$config['num_links']</strong><br /> Le nombre de digits que l'on veut afficher dans la pagination.<br /> Formule magique pour afficher pour obtenir toutes les pages :<br /> <code>round(($config['total_rows'] / $config['per_page']) + 1</code></p> <p><strong>$config['uri_segment']</strong><br /> Le nombre d'occurrences dans l'url<br /> Exemple :<br /> <a href="http://localhost/blog/page/1">http://localhost/blog/page/1</a><br /> &quot;page&quot; correspond à l'URI 1<br /> &quot;1&quot; correspond à l'URI 2<br /> Autre exemple :<br /> <a href="http://localhost/blog/rubrique_1/page/1">http://localhost/blog/rubrique_1/page/1</a><br /> &quot;rubrique_1&quot; correspond à l'URI 1<br /> &quot;page&quot; correspond à l'URI 2<br /> &quot;1&quot; correspond à l'URI 3<br /> Il faut indiquer la dernière occurrence (celle qui correspond au digit).</p> <p><strong>$config['use_page_numbers'] = TRUE;</strong><br /> Pour lister les pages par numéro de digit (et non par le nombre de content).</p> <p>Ensuite on initialise la pagination avec le code suivant :<br /> <code>$this-&gt;pagination-&gt;initialize($config);</code><br /> Coté SQL la requête générée doit être du type suivant (par la suite dans le model) : pour la 1ère page (<a href="http://localhost/blog">http://localhost/blog</a>) :</p> <pre><code class="language-sql">SELECT `c_title`, `c_content`, `c_cdate`, `c_url_rw`, `r_title`, `r_url_rw` FROM (`content`) JOIN `rubric` ON `rubric`.`r_id` = `content`.`r_id` ORDER BY `c_id` DESC LIMIT 3</code></pre> <p>Pour la seconde page (<a href="http://localhost/blog/page/2">http://localhost/blog/page/2</a>) :</p> <pre><code class="language-sql">SELECT `c_title`, `c_content`, `c_cdate`, `c_url_rw`, `r_title`, `r_url_rw` FROM (`content`) JOIN `rubric` ON `rubric`.`r_id` = `content`.`r_id` ORDER BY `c_id` DESC LIMIT 3,3</code></pre> <p>Il faut mettre une limitation au niveau de la page courante.<br /> Comment on peut connaitre la page courante ?<br /> Formule magique : (page_courante - 1) <em> nombre_de_content_par_page<br /> Exemple : on est sur la page 2 avec 3 contents par page :<br /> (2 -1) </em> 3 = 3<br /> On mettra cette formule dans le modèle qui arrive...</p> <p>Et pour afficher la pagination :<br /> <code>$data['pagination'] = $this-&gt;pagination-&gt;create_links();</code></p></p> <h2>2- Le modèle Model_blog</h2> <p>Dans le dossier &quot;models&quot; (<code>application/models</code>), créez un nouveau dossier &quot;front&quot; puis un modèle que vous nommerez &quot;model_blog.php&quot;.<br /> On veut obtenir, de notre base de données :</p></p> <ul> <li>tous les articles</li> <li>les articles pour le listing</li> <li>un article à partir de son slug</li> <li>les autres articles de la même rubrique (en dessous d'un article)</li> <li>tous les autre articles (pour la sidebar)</li> <li>toutes les rubriques (pour la sidebar)</li> <li>une rubrique à partir de son slug</li> </ul> <pre><code class="language-php">&lt;?php if ( ! defined('BASEPATH')) exit('No direct script access allowed'); class Model_blog extends CI_Model { // les 7 fonctions (ci-dessous) à placer ici } /* End of file model_blog.php */ /* Location: ./application/models/front/model_blog.php */</code></pre> <h3>Obtenir tous les articles</h3> <pre><code class="language-php">// Obtenir tous les articles function get_all_contents() { $this-&gt;db-&gt;select('c_title, c_content, c_cdate, c_url_rw, r_title, r_url_rw') -&gt;from('content') -&gt;join('rubric', 'rubric.r_id = content.r_id') -&gt;order_by('c_id', 'DESC'); $query = $this-&gt;db-&gt;get(); return $query; }</code></pre> <h3>Obtenir les articles pour le listing</h3> <pre><code class="language-php">// Obtenir les articles pour le listing function get_contents_listing($numero_page, $per_page) { $this-&gt;db-&gt;select('c_title, c_content, c_cdate, c_url_rw, r_title, r_url_rw'); $this-&gt;db-&gt;from('content'); $this-&gt;db-&gt;join('rubric', 'rubric.r_id = content.r_id'); $this-&gt;db-&gt;order_by('c_id', 'DESC'); if($numero_page): $this-&gt;db-&gt;limit($per_page, ($numero_page-1) * $per_page); else: $this-&gt;db-&gt;limit($per_page); endif; $query = $this-&gt;db-&gt;get(); return $query; }</code></pre> <h3>Obtenir un article à partir de son slug</h3> <pre><code class="language-php">// Obtenir un article en particulier (via son slug) pour le content function get_content($slug_rubric, $slug_content) { $this-&gt;db-&gt;select('c_title, c_content, c_cdate, c_url_rw, r_title, r_url_rw') -&gt;from('content') -&gt;join('rubric', 'content.r_id = rubric.r_id') -&gt;where('r_url_rw', $slug_rubric) -&gt;where('c_url_rw', $slug_content); $query = $this-&gt;db-&gt;get(); return $query; }</code></pre> <h3>Obtenir les autres articles que celui en cours</h3> <pre><code class="language-php">// Obtenir les autres articles function get_contents_others($slug_content) { $this-&gt;db-&gt;select('c_title, c_url_rw, r_url_rw') -&gt;join('rubric', 'rubric.r_id = content.r_id') -&gt;from('content') -&gt;where('c_url_rw &lt;&gt;', $slug_content) -&gt;order_by('c_id', 'DESC'); $query = $this-&gt;db-&gt;get(); return $query; }</code></pre> <h3>Obtenir les autres articles de la même rubrique que celui en cours</h3> <pre><code class="language-php">// Obtenir les autres articles de la même rubrique function get_contents_same_rubric($slug_rubric, $slug_content) { $this-&gt;db-&gt;select('c_title, c_url_rw, r_url_rw') -&gt;join('rubric', 'content.r_id = rubric.r_id') -&gt;from('content') -&gt;where('rubric.r_url_rw', $slug_rubric) -&gt;where('content.c_url_rw &lt;&gt;', $slug_content) -&gt;order_by('c_id', 'DESC'); $query = $this-&gt;db-&gt;get(); return $query; }</code></pre> <h3>Obtenir toutes les rubriques</h3> <pre><code class="language-php">// Obtenir toutes les rubriques function get_all_rubrics() { $this-&gt;db-&gt;select('r_title, r_url_rw') -&gt;from('rubric') -&gt;order_by('r_title', 'ASC'); $query = $this-&gt;db-&gt;get(); return $query; }</code></pre> <h3>Obtenir les rubriques pour le listing</h3> <pre><code class="language-php">// Obtenir les rubriques pour le listing (via son slug) function get_contents_rubric_listing($slug_rubric, $numero_page, $per_page) { $this-&gt;db-&gt;select('c_title, c_content, c_cdate, c_url_rw, r_title, r_description, r_url_rw'); $this-&gt;db-&gt;from('content'); $this-&gt;db-&gt;join('rubric', 'rubric.r_id = content.r_id'); $this-&gt;db-&gt;where('rubric.r_url_rw', $slug_rubric); $this-&gt;db-&gt;order_by('content.c_id', 'DESC'); if($numero_page and $per_page): $this-&gt;db-&gt;limit($per_page, ($numero_page-1) * $per_page); elseif($per_page): $this-&gt;db-&gt;limit($per_page); endif; $query = $this-&gt;db-&gt;get(); return $query; }</code></pre> <h2>3 - Les vues</h2> <p>Dans le dossier <code>application/views</code>, créez un nouveau dossier que vous nommerez &quot;front&quot;. Dans ce dossier créez un nouveau dossier que vous nommerez &quot;include&quot; puis créez les 6 fichiers suivants :</p> <ul> <li>include/view_header.php</li> <li>include/view_sidebar.php</li> <li>include/view_footer.php</li> <li>view_layout.php</li> <li>view_listing php</li> <li>view_content.php</li> </ul> <h3>Architecture du template des vues :</h3> <p><img src="../assets/img/news/codeigniter_blog_front/codeigniter_blog_architecture_views.jpg" alt="Aperçu architecture des vues" /></p> <p>Le view_layout.php va appeler dans tous les cas, les vues suivantes (stockées dans le dossier views/include) :</p> <ul> <li>view_header.php</li> <li>view_sidebar.php</li> <li>view_footer.php</li> </ul> <p>Quant aux 3 dernières vues, on mettra une condition (via un switch case) suivant le type de page.</p> <p>Pour les pages accueil et rubrique : view_listing.php<br /> Car on veut juste le listing des articles et la pagination avec la même mise en page.</p> <p>Pour la page article : view_content.php<br /> Car on veut juste le contenu de l'article.</p> <p>Pour la page 404 :view_404.php<br /> Car on veut juste afficher un message personnalisé.</p> <p>Pour la partie CSS, crééz à la racine du répertoire de Codeigniter, un dossier &quot;assets&quot;, puis mettez le contenu de Twitter Bootstrap 3 dedans (seuls les dossiers &quot;css&quot; et &quot;font&quot; sont utilisés dans cette partie).</p> <h3>include/view_header.php</h3> <pre><code class="language-markup">&lt;!DOCTYPE html&gt; &lt;html lang="fr"&gt; &lt;head&gt; &lt;meta charset="utf-8"&gt; &lt;title&gt;&lt;?php echo $meta_title; ?&gt;&lt;/title&gt; &lt;meta name="description" content="&lt;?php echo $meta_desc; ?&gt;" /&gt; &lt;?php echo css_url('bootstrap.min'); ?&gt; &lt;/head&gt;</code></pre> <h3>include/view_sidebar.php</h3> <pre><code class="language-markup">&lt;aside class="col-md-4 hidden-xs"&gt; &lt;h3&gt;About&lt;/h3&gt; &lt;p&gt;Lorem ipsum Eiusmod irure sint Ut magna incididunt ut esse eu enim consequat et mollit cupidatat irure veniam laborum veniam dolore amet in et aliqua deserunt occaecat laborum proident Ut officia sunt laboris laborum adipisicing reprehenderit anim proident quis.&lt;/p&gt; &lt;?php if($query_all_rubrics-&gt;num_rows &gt; 0): ?&gt; &lt;h3&gt;Catégories (&lt;?php echo $query_all_rubrics-&gt;num_rows(); ?&gt;)&lt;/h3&gt; &lt;ul class="unstyled"&gt; &lt;?php foreach ($query_all_rubrics-&gt;result() as $row): ?&gt; &lt;li&gt;&lt;a href="&lt;?php echo base_url($row-&gt;r_url_rw); ?&gt;" &lt;?php if ($this-&gt;uri-&gt;segment(1) == $row-&gt;r_url_rw): echo 'title="Categorie actuelle"'; endif; ?&gt;&gt;&lt;?php echo $row-&gt;r_title; ?&gt;&lt;/a&gt;&lt;/li&gt; &lt;?php endforeach; ?&gt; &lt;/ul&gt; &lt;?php endif; ?&gt; &lt;?php if($all_content-&gt;num_rows &gt; 0): ?&gt; &lt;h3&gt;Archives (&lt;?php echo $all_content-&gt;num_rows(); ?&gt;) &lt;/h3&gt; &lt;ul class="unstyled"&gt; &lt;?php foreach($all_content-&gt;result() as $row): ?&gt; &lt;li&gt;&lt;?php echo content_url($row-&gt;r_url_rw, $row-&gt;c_url_rw, $row-&gt;c_title); ?&gt;&lt;/li&gt; &lt;?php endforeach;?&gt; &lt;/ul&gt; &lt;?php endif; ?&gt; &lt;/aside&gt;&lt;!-- end of .col-md-4 --&gt;``` &lt;h3&gt;include/view_footer.php&lt;/h3&gt; ```markup &lt;footer data-role="footer"&gt; &lt;p class="footer" style="text-align: center"&gt;Propulsé par Codeigniter - Temps d'exécution : &lt;strong&gt;0.0610&lt;/strong&gt; seconds&lt;/p&gt; &lt;/footer&gt; &lt;/body&gt; &lt;/html&gt;</code></pre> <h3>view_layout.php</h3> <pre><code class="language-markup">&lt;?php $this-&gt;load-&gt;view('front/include/view_header.php'); ?&gt; &lt;div class="container"&gt; &lt;!-- Start breadcrumb --&gt; &lt;ol class="breadcrumb" itemtype="http://data-vocabulary.org/Breadcrumb" itemscope=""&gt; &lt;?php if($page == 'home'): ?&gt; &lt;li class="active"&gt; &lt;span itemprop="title"&gt; &lt;?php if(isset($numero_page)): ?&gt; &lt;a href="&lt;?php echo base_url(); ?&gt;"&gt;Home&lt;/a&gt; - page &lt;?php echo $numero_page; ?&gt; &lt;?php else: ?&gt; &lt;span itemprop="title"&gt;Home&lt;/span&gt; &lt;?php endif; ?&gt; &lt;/span&gt; &lt;/li&gt; &lt;?php else: ?&gt; &lt;li&gt;&lt;span itemprop="title"&gt;&lt;a itemprop="url" href="&lt;?php echo base_url(); ?&gt;"&gt;Home&lt;/a&gt;&lt;/span&gt;&lt;/li&gt; &lt;?php if($page == 'rubric'): ?&gt; &lt;li class="active"&gt; &lt;span itemprop="title"&gt; &lt;?php if(isset($numero_page)): ?&gt; &lt;a href="&lt;?php echo base_url($this-&gt;uri-&gt;segment(1)); ?&gt;"&gt;&lt;?php echo $title; ?&gt;&lt;/a&gt; - page &lt;?php echo $numero_page; ?&gt; &lt;?php else: ?&gt; &lt;?php echo $title; ?&gt; &lt;?php endif; ?&gt; &lt;/span&gt; &lt;/li&gt; &lt;?php endif; ?&gt; &lt;?php if($page == 'content'): ?&gt; &lt;li&gt; &lt;span itemprop="title"&gt;&lt;?php echo rubric_url($r_url_rw, $r_title); ?&gt;&lt;/span&gt; &lt;/li&gt; &lt;li class="active"&gt; &lt;span itemprop="title"&gt;&lt;?php echo $c_title; ?&gt;&lt;/span&gt; &lt;/li&gt; &lt;?php endif; ?&gt; &lt;?php if($page == '404'): ?&gt; &lt;li class="active"&gt; &lt;span itemprop="title"&gt;Erreur 404&lt;/span&gt; &lt;/li&gt; &lt;?php endif; ?&gt; &lt;?php endif; ?&gt; &lt;/ol&gt; &lt;!-- End breadcrumb --&gt; &lt;div class="page-header"&gt; &lt;h1 class="text-center"&gt;&lt;?php echo $title; ?&gt;&lt;/h1&gt; &lt;/div&gt; &lt;div class="row"&gt; &lt;div class="col-md-8"&gt; &lt;?php switch ($page) { case 'home': case 'rubric': $this-&gt;load-&gt;view('front/view_listing.php'); break; case 'content': $this-&gt;load-&gt;view('front/view_content.php'); break; case '404': $this-&gt;load-&gt;view('front/view_404.php'); break; default: break; } ?&gt; &lt;/div&gt;&lt;!-- end of .col-md-8 --&gt; &lt;?php $this-&gt;load-&gt;view('front/include/view_sidebar.php'); ?&gt; &lt;/div&gt;&lt;!-- end of .row --&gt; &lt;/div&gt;&lt;!-- end of .container --&gt; &lt;?php $this-&gt;load-&gt;view('front/include/view_footer.php'); ?&gt;</code></pre> <h3>view_listing.php</h3> <pre><code class="language-markup">&lt;?php if($query-&gt;num_rows() &gt; 0): ?&gt; &lt;?php foreach($query-&gt;result() as $row): ?&gt; &lt;article class="thumbnail"&gt; &lt;div class="caption"&gt; &lt;p class="row"&gt; &lt;span class="col-md-2"&gt; &lt;i class="glyphicon glyphicon-tag"&gt;&lt;/i&gt; &lt;?php echo rubric_url($row-&gt;r_url_rw, $row-&gt;r_title); ?&gt; &lt;/span&gt; &lt;span class="col-md-3 col-md-offset-7 text-right"&gt; &lt;i class="glyphicon glyphicon-calendar"&gt;&lt;/i&gt; &lt;?php $jour = date("d", strtotime($row-&gt;c_cdate)); ?&gt; &lt;?php $mois = date("m", strtotime($row-&gt;c_cdate)); ?&gt; &lt;?php $annee = date("Y", strtotime($row-&gt;c_cdate)); ?&gt; &lt;em&gt;&lt;?php echo date_fr($jour, $mois, $annee); ?&gt;&lt;/em&gt; &lt;/span&gt; &lt;/p&gt;&lt;!-- end of .row --&gt; &lt;h2&gt;&lt;?php echo content_url($row-&gt;r_url_rw, $row-&gt;c_url_rw, $row-&gt;c_title); ?&gt;&lt;/h2&gt; &lt;p&gt;&lt;?php echo character_limiter($row-&gt;c_content, 256); ?&gt;&lt;/p&gt; &lt;?php echo content_url_button($row-&gt;r_url_rw, $row-&gt;c_url_rw); ?&gt; &lt;/div&gt;&lt;!-- end of .caption --&gt; &lt;/article&gt;&lt;!-- end of .thumbnail --&gt; &lt;?php endforeach; ?&gt; &lt;?php echo $pagination; ?&gt; &lt;?php else: ?&gt; &lt;p&gt;Aucun article n'est disponible pour le moment&lt;/p&gt; &lt;?php endif; ?&gt;</code></pre> <h3>view_content.php</h3> <pre><code class="language-markup">&lt;article class="thumbnail"&gt; &lt;div class="caption"&gt; &lt;p class="row"&gt; &lt;span class="col-md-2"&gt; &lt;i class="glyphicon glyphicon-tag"&gt;&lt;/i&gt; &lt;a href="&lt;?php echo base_url($r_url_rw);?&gt;"&gt;&lt;?php echo $r_title; ?&gt;&lt;/a&gt; &lt;/span&gt; &lt;span class="col-md-3 col-md-offset-7 text-right"&gt; &lt;i class="glyphicon glyphicon-calendar"&gt;&lt;/i&gt; &lt;?php $jour = date("d", strtotime($c_cdate)); ?&gt; &lt;?php $mois = date("m", strtotime($c_cdate)); ?&gt; &lt;?php $annee = date("Y", strtotime($c_cdate)); ?&gt; &lt;em&gt;&lt;?php echo date_fr($jour, $mois, $annee); ?&gt;&lt;/em&gt; &lt;/span&gt; &lt;/p&gt;&lt;!-- end of .row --&gt; &lt;?php echo $c_content; ?&gt; &lt;/div&gt;&lt;!-- end of .caption --&gt; &lt;/article&gt;&lt;!-- end of .thumbnail --&gt; &lt;?php if($query_same_rubric-&gt;num_rows() &gt; 0): ?&gt; &lt;h3&gt;Article&lt;?php if($query_same_rubric-&gt;num_rows() &gt; 1){ echo 's';} ?&gt; de la même catégorie :&lt;/h3&gt; &lt;ul&gt; &lt;?php foreach($query_same_rubric-&gt;result() as $row): ?&gt; &lt;li&gt;&lt;?php echo content_url($row-&gt;r_url_rw, $row-&gt;c_url_rw, $row-&gt;c_title); ?&gt;&lt;/li&gt; &lt;?php endforeach; ?&gt; &lt;/ul&gt; &lt;?php endif; ?&gt;``` ### view_404.php ```markup &lt;h2&gt;C'est embetant...&lt;/h2&gt; &lt;p&gt;La page que vous demandez n'existe pas ou n'existe plus&lt;/p&gt;</code></pre> <h2>4 - Helper function</h2> <p>Dans le dossier <code>application/helpers</code>, créez un fichier helper : &quot;functions_helper.php&quot;.<br /> Avec ce helper, on va pouvoir simplifier le code dans nos vues en créant des fonctions.<br /> Exemple avec l'appel de l'URL d'un article que l'on appel dans la vue :</p> <pre><code class="language-php">&lt;?php echo content_url($row-&gt;r_url_rw, $row-&gt;c_url_rw, $row-&gt;c_title); ?&gt;</code></pre> <p>au lieu de :</p> <pre><code class="language-php">&lt;a href="&lt;?php echo base_url($row-&gt;r_url_rw . '/' . $row-&gt;c_url_rw); ?&gt;"&gt;&lt;?php echo $row-&gt;c_title; ?&gt;&lt;/a&gt;</code></pre> <p>Appeler la feuille de style css :</p> <pre><code class="language-php">&lt;?php if ( ! defined('BASEPATH')) exit('No direct script access allowed'); if ( ! function_exists('css_url')) { function css_url($nom) { return '&lt;link rel="stylesheet" href="' . base_url() . 'assets/css/' . $nom . '.css" /&gt; '; } }</code></pre> <p>Appeler l'url de l'article :</p> <pre><code class="language-php">if ( ! function_exists('content_url')) { function content_url($rubric, $content, $titre) { return '&lt;a href="' . base_url($rubric . '/' . $content) . '"&gt;' . $titre . '&lt;/a&gt;'; } }</code></pre> <p>Appeler l'url de l'article à partir du bouton &quot;lire la suite&quot; :</p> <pre><code class="language-php">if ( ! function_exists('content_url_button')) { function content_url_button($rubric, $content) { return '&lt;a href="' . base_url($rubric . '/' . $content) . '" class="btn btn-primary"&gt;Lire la suite&lt;/a&gt;'; } }</code></pre> <p>Appeler l'url de la rubrique d'un article :</p> <pre><code class="language-php">if ( ! function_exists('rubric_url')) { function rubric_url($rubric, $titre) { return '&lt;a href="' . base_url($rubric) . '"&gt;' . $titre . '&lt;/a&gt;'; } }</code></pre> <p>Codeigniter ne gérant pas les dates en français, la création de ce helper va nous permettre de pouvoir exécuter la fonction datefr() :</p> <pre><code class="language-php">if ( ! function_exists('date_fr')) { function date_fr ($jour, $mois, $annee) { $mois_n = $mois; switch ($mois) { case '01': $mois = 'Janvier'; break; case '02': $mois = 'Février'; break; case '03': $mois = 'Mars'; break; case '04': $mois = 'Avril'; break; case '05': $mois = 'Mai'; break; case '06': $mois = 'Juin'; break; case '7': $mois = 'Juillet'; break; case '8': $mois = 'Aout'; break; case '9': $mois = 'Septembre'; break; case '10': $mois = 'Octobre'; break; case '11': $mois = 'Novembre'; break; case '12': $mois = 'Décembre'; break; default: break; } return '&lt;time datetime="' . $annee . '-' . $mois_n . '-' . $jour . '"&gt;' .$jour . ' ' . $mois . ' ' . $annee.'&lt;/time&gt;'; } }</code></pre> <p>Pour la pagination :</p> <pre><code class="language-php">if ( ! function_exists('pagination_custom')) { function pagination_custom() { // Paramètres de configuration # Nombre d'articles par page $config['per_page'] = 3; # Lister les pages par numéro (page 1, page 2, etc...) $config['use_page_numbers'] = TRUE; # HTML entre les digits $config['full_tag_open'] = '&lt;ul class="pagination"&gt;'; $config['full_tag_close'] = '&lt;/ul&gt;&lt;!--pagination--&gt;'; $config['num_tag_open'] = '&lt;li&gt;'; $config['num_tag_close'] = '&lt;/li&gt;'; $config['cur_tag_open'] = '&lt;li class="active"&gt;&lt;span&gt;'; $config['cur_tag_close'] = '&lt;/span&gt;&lt;/li&gt;'; $config['next_tag_open'] = '&lt;li&gt;'; $config['next_tag_close'] = '&lt;/li&gt;'; $config['prev_tag_open'] = '&lt;li&gt;'; $config['prev_tag_close'] = '&lt;/li&gt;'; $config['first_tag_open'] = '&lt;li style="display: none;"&gt;'; $config['first_tag_close'] = '&lt;/li&gt;'; $config['last_tag_open'] = '&lt;li style="display: none;"&gt;'; $config['last_tag_close'] = '&lt;/li&gt;'; return $config; } }</code></pre> <h2>5 - Routage</h2> <p>Dans le fichier de configuration &quot;routes.php&quot; (<code>application/config</code>), on veut que notre url soit routée correctement, de la forme <a href="http://localhost/blog/nom-rubrique/nom-article">http://localhost/blog/nom-rubrique/nom-article</a>. Avant cela, il faut définir notre controller par défaut, autrement dit celui que l'on vient de créer auparavant :<br /> <code>$route['default_controller'] = 'front/blog';</code></p> <p>On ajoute la pagination de la page d'accueil :<br /> <code>$route['page/(:num)'] = $route['default_controller'] . '/index/$1';</code></p> <p>Puis, les rubriques :<br /> <code>$route['(:any)'] = 'front/blog/view/$1';</code><br /> <a href="http://localhost/blog/nom-rubrique">http://localhost/blog/nom-rubrique</a><br /> &quot;blog&quot; correspond au contrôleur blog.php<br /> $1 correspond à la valeur dans le contrôleur blog.php de la variable $slug_rubrique.</p> <p>Puis, on finit par les articles :<br /> <code>$route['(:any)/(:any)'] = 'front/blog/view/$1/$2';</code><br /> <a href="http://localhost/blog/nom-rubrique/nom-article">http://localhost/blog/nom-rubrique/nom-article</a>.<br /> $2 correspond alors à la valeur dans le contrôleur blog.php de la variable $slug_content.</p> <p>Important : ces 2 lignes de codes sont toujours à placer à la fin du fichier.</p> <p>Ce qui donne au final :</p> <pre><code class="language-php">$route['default_controller'] = 'front/blog'; # 404 $route['erreur404'] = $route['default_controller'] . '/erreur404'; #pagination home $route['page/(:num)'] = $route['default_controller'] . '/index/$1'; # rubrique $route['(:any)'] = $route['default_controller'] . '/view/$1'; # rubrique + content $route['(:any)/(:any)'] = $route['default_controller'] . '/view/$1/$2';</code></pre> <h2>Captures d'écran du résultat à l'écran</h2> <p>Page d'accueil : <a href="../assets/img/news/codeigniter_blog_front/codeigniter_blog_accueil.jpg" target="_blank"> <img src="../assets/img/news/codeigniter_blog_front/min_codeigniter_blog_accueil.jpg" alt="Aperçu page d'accueil" /></p> <p></a></p> <p>Page rubrique :</p> <p><a href="../assets/img/news/codeigniter_blog_front/codeigniter_blog_rubrique.jpg" target="_blank"> <img src="../assets/img/news/codeigniter_blog_front/min_codeigniter_blog_rubrique.jpg" alt="Aperçu page rubrique" /> </a></p> <p>Page article :</p> <p><a href="../assets/img/news/codeigniter_blog_front/codeigniter_blog_article.jpg" target="_blank"> <img src="../assets/img/news/codeigniter_blog_front/min_codeigniter_blog_article.jpg" alt="Aperçu page article" /></a></p> <p>Page 404 :</p> <p><a href="../assets/img/news/codeigniter_blog_front/codeigniter_blog_404.jpg" target="_blank"> <img src="../assets/img/news/codeigniter_blog_front/min_codeigniter_blog_404.jpg" alt="Aperçu page 404" /></a></p>; 2013-12-08 20:56:16 Lazyload d'images avec Unveil.js https://etienner.fr/lazyload-dimages-avec-unveiljs <p>Il peut vous arriver d'être amener à utiliser une page qui demande d'afficher beaucoup de photos. Force est de constater que la page prend du temps à charger (sans compter que sur mobile cela est plus lent via la 3G) même si le poids des images a été allégé au préalable.<br /> Pour cela, il existe quelques plugins en jQuery (via la fonction <code>scrollTop</code>) qui permettent d'optimiser le chargement de la page web. Cette méthode s'appelle le Lazy load que l'on peut littéralement traduire par &quot;chargement paresseux&quot;. Lors du chargement de la page web, les images qui sont hors du champ de la page seront chargées au moment du scroll par le visiteur.</p> <h2>Téléchargement et utilisation</h2> <p>Téléchargez le script à l'adresse suivante : <a href="https://github.com/luis-almeida/unveil">https://github.com/luis-almeida/unveil</a><br /> Aperçu du code HTML :</p> <pre><code class="language-markup">&lt;img src="bg.png" data-src="img1.jpg" /&gt; &lt;img src="bg.png" data-src="img2.jpg" /&gt; &lt;img src="bg.png" data-src="img3.jpg" /&gt; &lt;script src="js/jquery.js"&gt;&lt;/script&gt; &lt;script src="js/jquery.unveil.min.js"&gt;&lt;/script&gt;</code></pre> <p>&quot;bg.png&quot; est l'image qui s'affiche par défaut avant le scroll (vous pouvez mettre un loader en gif ou laisser vide).<br /> &quot;img1.jpg&quot; est l'image finale.<br /> Vous n'êtes pas obligés de remplir le chemin src.</p> <p>Remarque : vous pouvez entourer la balise <code>img</code> par une balise &quot;noscript&quot; afin d'afficher les images pour les internautes dont le Javascript est désactivé sur leur navigateur.</p> <h2>Sources</h2> <ul> <li>Site officiel : <a href="http://luis-almeida.github.io/unveil">http://luis-almeida.github.io/unveil</a></li> <li>Compresser les images PNG : <a href="http://tinypng.org">http://tinypng.org</a></li> <li>Compresser les images JPEG : <a href="https://kraken.io/web-interface">https://kraken.io/web-interface</a> ou <a href="http://www.smushit.com/ysmush.it">http://www.smushit.com/ysmush.it</a></li> <li>Exemples de sites utilisant le lazy loading : <a href="http://pinterest.com/all">http://pinterest.com/all</a>, <a href="http://www.flickr.com">http://www.flickr.com</a></li> </ul>; 2013-09-13 16:12:39 Menu déroulant / Drop-Down Menu responsive avec Twitter Bootstrap https://etienner.fr/menu-deroulant-drop-down-menu-responsive-avec-twitter-bootstrap <p>Twitter Bootstrap est un framework CSS qui reprend les aspects graphiques de Twitter (boutons, formulaires, etc) et qui permet de rendre votre site responsive à l'aide d'un système de grillage (ligne comportant une ou plusieurs grilles). Le code présenté dans cet article est issue de la version 2.3.2 du framework.</p> <h2>Téléchargement &amp; préparation</h2> <p>Téléchargez le framework à l'adresse suivante : <a href="http://getbootstrap.com/2.3.2">http://getbootstrap.com/2.3.2</a><br /> Dans le header de votre fichier html :</p> <pre><code class="language-markup">&lt;meta name="viewport" content="width=device-width" /&gt; &lt;link href="css/bootstrap.min.css" rel="stylesheet" /&gt; &lt;link href="css/bootstrap-responsive.min.css" rel="stylesheet" /&gt;</code></pre> <p>Dans le footer :</p> <pre><code class="language-markup">&lt;script src="js/jquery.min.js"&gt;&lt;/script&gt; &lt;script src="js/bootstrap.min.js"&gt;&lt;/script&gt;</code></pre> <p>Dans le corps :</p> <pre><code class="language-markup">&lt;pre class="line-numbers"&gt;&lt;code class="language-markup"&gt;&lt;div class="navbar"&gt; &lt;div class="navbar-inner"&gt; &lt;div class="container"&gt; &lt;button class="btn btn-navbar" data-target=".nav-collapse" data-toggle="collapse" type="button"&gt; &lt;span class="icon-bar"&gt;&lt;/span&gt; &lt;span class="icon-bar"&gt;&lt;/span&gt; &lt;span class="icon-bar"&gt;&lt;/span&gt; &lt;/button&gt; &lt;a class="brand" href="#"&gt; Nom de mon site &lt;/a&gt; &lt;nav class="nav-collapse" role="navigation"&gt; &lt;ul class="nav"&gt; &lt;li&gt;&lt;a href="#"&gt;Lien #1&lt;/a&gt;&lt;/li&gt; &lt;li class="divider-vertical"&gt;&lt;/li&gt; &lt;li class="dropdown"&gt; &lt;a data-toggle="dropdown" class="dropdown-toggle" href="#"&gt;Lien #2 &lt;b class="caret"&gt;&lt;/b&gt;&lt;/a&gt; &lt;ul class="dropdown-menu"&gt; &lt;li&gt;&lt;a href="#"&gt;Lien #2-a&lt;/a&gt;&lt;/li&gt; &lt;li&gt;&lt;a href="#"&gt;Lien #2-b&lt;/a&gt;&lt;/li&gt; &lt;li&gt;&lt;a href="#"&gt;Lien #2-c&lt;/a&gt;&lt;/li&gt; &lt;/ul&gt; &lt;/li&gt; &lt;li class="divider-vertical"&gt;&lt;/li&gt; &lt;li&gt;&lt;a href="#"&gt;Lien #3&lt;/a&gt;&lt;/li&gt; &lt;li class="divider-vertical"&gt;&lt;/li&gt; &lt;li&gt;&lt;a href="#"&gt;Lien #4&lt;/a&gt;&lt;/li&gt; &lt;/ul&gt; &lt;/nav&gt; &lt;/div&gt;&lt;!-- end of .container --&gt; &lt;/div&gt;&lt;!-- end of .navbar-inner --&gt; &lt;/div&gt;&lt;!-- end of .navbar .navbar --&gt;</code></pre> <h2>Aperçu</h2> <p><img src="../assets/img/news/menu_responsive_twitter-bootstrap/menu_responsive_twitter-bootstrap_320.jpg" alt="" /><br /> Avec Firefox (CTRL+SHIFT+M), vous pouvez voir que le menu est parfaitement responsive sur une résolution de 320*480.</p> <h2>Aller plus loin</h2> <ul> <li>Documentation officielle de Twitter Boostrap: <a href="http://getbootstrap.com/2.3.2/getting-started.html" target="_blank"><a href="http://getbootstrap.com/2.3.2/getting-started.html">http://getbootstrap.com/2.3.2/getting-started.html</a></li> <li>Des thêmes gratuits : <a href="http://bootswatch.com">http://bootswatch.com</a></li> <li>&quot;Flat UI&quot; thême flat design : <a href="http://designmodo.com/demo/flat-ui">http://designmodo.com/demo/flat-ui</a></li> </ul>; 2013-05-16 23:10:57 MySQL en ligne de commande sous WAMP https://etienner.fr/mysql-en-ligne-de-commande-sous-wamp <p>MySQL par le bias de PHP My Admin permet de faire de la manipulation de données SQL sur une interface graphique. Hélas, cette dernière est en partie bridée dans l'importation de données.<br /> Par défaut l'importation de données est limitée à 1Mo sur PMA. Il est possible de s'affranchir de cette limite en partant directement à la source, sur MySQL.</p> <h2>Variable d'environnement</h2> <p>Allez dans &quot;Système&quot;, &quot;Paramètres système avancé&quot;, &quot;Variables d'environnement&quot;.<br /> Dans &quot;Variable systèmes&quot;, doubles cliquez sur la variable &quot;Path&quot;. A la suite de la &quot;Valeur de la variable&quot;, indiquez l'emplacement du dossier de MySQL :<br /> <code>;C:\wamp\bin\mysql\mysql5.5.24\bin</code></p> <p><img src="../assets/img/news/mysql-workbench/mysql-workbench_img_3.jpg" alt="" /></p> <p>Validez et redémarrez votre ordinateur.</p> <p>NB : vous pouvez sauter cette partie et directement passer à la partie 2 si vous ne souhaitez pas rajouter cette variable d'environnement sur votre machine.<br /> Lancez l'exécution (Windows + R &gt; cmd) de MySQL : <code>C\:wamp\bin\mysql\mysql5.5.24\bin\mysql.exe -u root -p</code></p> <h2>Lancement de la console</h2> <p>Lancez votre serveur Wamp puis l'invité de commande (Windows + R, &quot;cmd&quot;).<br /> Tapez <code>mysql -u root -p</code><b>- u</b> correspond à l'utilisateur<br /> <b>- p</b> correspond au mot de passe (par défaut vide)<br /> Le message suivant apparaît :<br /> &quot;Enter password&quot;</p> <p><img src="../assets/img/news/mysql_wamp_commandes/mysql_wamp_commande_2.jpg" alt="" /></p> <p>Ne tapez rien si vous n'avez pas de mot de passe par défaut, pressez la touche Entrée de votre clavier pour valider.<br /> Vous devriez avoir le message suivant :<br /> <b>Welcome to the Mysql monitor [...] mysql&gt;</b><br /> Si vous avez le message suivant :<br /> <b>'mysql' n'est pas reconnu en tant que commande interne ou externe, un programme exécutable ou un fichier de commandes.</b><br /> Cela signifie que le chemin de votre variable d'environnement est erroné.<br /> Ou si vous avez le message ci-dessous :</p> <pre><code>ERROR 2003 &lt;HY000&gt; : Can't connect to MySQL server on 'localhost' (10061)</code></pre> <p>c'est que votre serveur MySQL, par le bias de WAMP, n'est pas allumé. </p> <p>Une fois connecté, dans l'invité de commande, tapez &quot;status&quot;.<br /> Vous pouvez voir votre version de MySQL, l'utilisateur en cours d'utilisation, le temps depuis que votre serveur est allumé, etc...</p> <p><img src="../assets/img/news/mysql_wamp_commandes/mysql_wamp_commande_3.jpg" alt="" /></p> <h2>Les commandes pour bien démarrer :</h2></h2> <h3>Naviguer</h3> <p><code>show databases</code> : affiche toutes les bases.<br /> <code>use nom_table (ou u nom_table)</code> : sélectionne la base (affiche &quot;Database changed&quot; lors de l'exécution de la commande).<br /> <code>show tables</code> : affiche toutes les tables présentes dans la base (après avoir sélectionné une table).</p> <h3>Utilisateurs</h3> <p><code>select user()</code> : affiche les utilisateurs.<br /> <code>select current_user()</code> : affiche l'utilisateur en cours d'utilisation.</p> <h3>Insertion / exportation</h3> <p>Il faut directement aller dans la console de Windows et non dans celle de MySQL.<br /> <b>Importer</b> : <code>mysql -u root nom_table_importer &lt; C:\chemin_fichierdump.sql</code><br /> La table a été créée au préalable par vos soins dans la console MySQL (<code>create database nom_table_import</code>).</p> <p><img src="../assets/img/news/mysql_wamp_commandes/mysql_wamp_commande_4.jpg" alt="" /></p> <p><b>Exporter</b> : <code>mysql -u root nom_table_exporter &gt; C:\backup.sql</code><br /> A noter : en cas de succès de la tache effectuée, contrairement à l'opération d'import, aucun message ne sera affiché.<br /> Si le message &quot;Accès refusé&quot; apparaît cela peut signifier que vous avez besoin des droits d'administrateur de Windows pour pouvoir créer un fichier. Changez l'emplacement du fichier ou bien lancez la console en tant qu'administrateur (Windows &gt; &quot;cmd.exe&quot;, clic droit &gt; &quot;Exécuter en tant qu'administrateur&quot;).</p>; 2013-04-23 00:27:39 Installation et préparation de CodeIgniter 2 https://etienner.fr/installation-et-preparation-de-codeigniter-2 <p>CodeIgniter 2 est un framework PHP gratuit distribué par la société EllisLab. Il est conçu et suit la logique MVC (Modèle Vue Contrôleur) par le biais de PHP 5 (depuis la version 2 de Codeigniter). Comme tout framework PHP, il permet de développer rapidement des sites Web dynamiques. Le gros avantage de Codeigniter est sa légèreté (moins de 5 Mo décompressé) et sa rapidité. Ci-dessous, quelques astuces pour démarrer rapidement sur ce framework à condition d'avoir été initié aux fonctionnements du MVC.</p> <h2>Activer l'URL Rewriting</h2> <p>Il va surement vous arrivez à un moment ou un autre lors du développement de votre projet d'avoir recours à l'URL Rewrting. Afin de palier à une erreur :<br /> <code>&quot;Not Found The requested URL /lien_vers_votre_urlrewriting was not found on this server&quot;</code>,<br /> créez un fichier .htaccess à la racine du dossier de Codeigniter pour activer l'URL Rewriting :</p> <pre><code>RewriteEngine on RewriteCond $1 !^(index.php|resources|robots.txt) RewriteCond %{REQUEST_FILENAME} !-f RewriteCond %{REQUEST_FILENAME} !-d RewriteRule ^(.*)$ index.php/$1 [L,QSA]</code></pre> <h2>Changer le controller par défaut</h2> <p>Vous pouvez voir afficher la page welcome qui n'est autre que le contrôleur welcome.php dans le dossier :<br /> <code>application/controllers</code><br /> qui appel la vue :<br /> <code>application/views/welcome_message.php</code><br /> Par défaut, c'est ce contrôleur qui est indiqué comme contrôleur par défaut sur l'index de l'application de CI.<br /> Pour modifier ce paramètre, allez dans le fichier de configuration des routes : <code>application/config/routes.php</code>.<br /> Modifiez la ligne 51 &quot;welcome&quot; par &quot;index_page&quot; si vous venez de renommer &quot;welcome.php&quot; par &quot;index_page.php&quot; dans le dossier <code>application/controllers</code> (n'oubliez pas de modifier la classe principale<br /> <code>class Welcome extends CI_Controller</code><br /> par<br /> <code>class Index_page extends CI_Controller</code> sinon vous aurez droit au fameux &quot;404 Page Not Found&quot;).<br /> En effet, il n'est pas possible de nommer le contrôleur par défaut index.php sinon vous aurez l'erreur suivante :<br /> <code>Message: Undefined property: Index::$load<br /> Filename: controllers/index.php</code></p> <h2>Configuration de la base de données</h2> <p>Dans le fichier <code>application/config/database.php</code> : de la ligne 51 à 54, remplissez les champs vides :</p> <pre><code class="language-php">$db['default']['username'] = ''; $db['default']['password'] = ''; $db['default']['database'] = '';</code></pre> <p>Le champ password peut être vide si vous n'avez pas de mot de passe.<br /> Si vous utilisez une base de données PostgreSQL vous devez modifier la ligne 55</p> <pre><code class="language-php">$db['default']['dbdriver'] = 'mysql';</code></pre> <p>par</p> <pre><code class="language-php">$db['default']['dbdriver'] = 'postgres';</code></pre> <h2>Automatisation de connexion à la base de données</h2></h2> <p>Par défaut, il faudra charger manuellement la base avec le code suivant :</p> <pre><code class="language-php">$this-&gt;load-&gt;database();</code></pre> <p>Pour automatiser cette tache, allez dans le fichier de configuration des auto-chargements <code>application/config/autoload.php</code> et remplissez le champ de la ligne 55 en demandant de charger la librairie &quot;database&quot; sur toutes les pages du site.</p> <pre><code class="language-php">$autoload['libraries'] = array('database');</code></pre> <h2>Où mettre les fichiers CSS, Javascript et les images ?!</h2> <p>En fait, il existe plusieurs méthodes, je vais vous en montrer 3.<br /> Avant toute chose, pour ne pas troubler l'arborescence de Codeigniter, il est conseillé de créer un dossier &quot;assets&quot; à la racine de votre projet et d'y placer les sous dossiers &quot;css&quot;, &quot;js&quot; et &quot;img&quot;.<br /> Cela permet d'avoir accès rapidement à ce dossier qui sera lourd en fichiers média et ainsi faciliter les sauvegardes.<br /> Allez dans le fichier de configuration des auto-chargements <code>application/config/autoload.php</code> et remplissez le champ de la ligne 55 en demandant de charger le helper &quot;url&quot; :</p> <pre><code class="language-php">$autoload['helper'] = array('url');</code></pre> <p>afin d'activer la fonction</p> <pre><code class="language-php">&lt;?php echo base_url(''); ?&gt;</code></pre> <h3>Chargement du css dans la vue</h3></h3> <p>Dans la vue, rajoutez la ligne d'appel de CSS.</p> <pre><code class="language-markup">&lt;link rel="stylesheet" href="&lt;?php echo base_url("assets/css/style.css"); ?&gt;" /&gt;;</code></pre> <p>Si vous regardez dans le code source de votre navigateur, vous pouvez remarquer que <code>&lt;?php echo base_url('');?&gt;</code> permet d'afficher le lien absolu dans lequel se situe votre fichier.</p> <h3>Chargement par include</h3></h3> <p>Créez un fichier &quot;inc.header.php&quot; dans &quot;/application/view/inc&quot;. Ce fichier va permettre de charger la feuille de style commune à toutes vos vues directement dans l'include inc.header.php.</p> <pre><code class="language-markup">&lt;!DOCTYPE html&gt; &lt;html lang="fr"&gt; &lt;head&gt; &lt;meta charset="utf-8"&gt; &lt;title&gt;&lt;?php echo $title; ?&gt; &lt;/title&gt; &lt;!-- Variable présente dans les controllers respectifs --&gt; &lt;link rel="stylesheet" href="&lt;?php echo base_url("assets/css/style.css"); ?&gt;" /&gt; &lt;/head&gt;</code></pre> <p>puis, dans vos vues, ajoutez l'inclusion en début de page :</p> <pre><code class="language-php">&lt;?php include("layouts/header.php"); ?&gt;</code></pre> <h3>Chargement dans le contrôleur</h3> <p>Cette méthode se rapproche beaucoup de la méthode ci-dessus et supprime l'inclusion dans le fichier vue (&quot;inc.header.php&quot;) au profit du contrôleur.</p> <pre><code class="language-php">&lt;?php if ( ! defined('BASEPATH')) exit('No direct script access allowed'); class Index_page extends CI_Controller { function index() { $this-&gt;load-&gt;view("layouts/header"); $this-&gt;load-&gt;view("index"); } }</code></pre> <h2>Personnaliser la page 404</h2> <p>a) Allez dans le fichier de configuration des routes : <code>//application/config/routes.php</code> et remplissez le champ vide de la ligne 42 :</p> <pre><code class="language-php">$route['404_override'] = 'erreur404';</code></pre> <p>b) Créer un nouveau controller &quot;erreur404.php&quot; dans le dossier des contrôleurs : <code>application/controllers</code></p> <pre><code class="language-php">&lt;?php if ( ! defined('BASEPATH')) exit('No direct script access allowed'); class erreur404 extends CI_Controller { public function __construct() { parent::__construct(); } public function index() { $this-&gt;load-&gt;view("view_404"); } }</code></pre> <p>c) Créez la vue &quot;view_404.php&quot; dans le dossier des vues <code>application/views</code>. Personnalisez la page à votre goût.<br /> Bien entendu, vous pouvez appeler dans votre contrôleur des bouts de vues de votre template (header, menu de navigation, sidebar, footer, etc...). La liste des erreurs HTTP se trouvent dans le fichier <code>systemcoreCommon.php</code> à partir de la ligne 378.</p> <h2>Astuce : créer un fichier .htaccess sur Windows</h2> <p>Ouvrez notepad (Windows + R : <code>notepad</code>) et enregistrez sous &quot;Nom de fichier&quot; : .htaccess puis dans &quot;type&quot; : &quot;Tous les fichiers&quot;.</p> <h2>Documentation</h2> <ul> <li>Dans le dossier <code>user_guideindex.html</code></li> <li>Sur le site officiel : <a href="http://ellislab.com/codeigniter/user-guide">http://ellislab.com/codeigniter/user-guide</a></li> </ul>; 2013-04-08 10:49:57 Eviter les doublons en import avec MySQL https://etienner.fr/eviter-les-doublons-en-import-avec-mysql <p>Vous souhaitez importer des nouvelles données dans votre base de données MySQL via PHP My Admin par le biais d'un fichier CSV. Pour autant vous ne souhaitez pas importer des doublons. Il est possible de remédier facilement à ce problème en mettant en place une déclaration d'unicité.</p> <pre><code class="language-sql">CREATE TABLE client( id_client int(11) NOT NULL AUTO_INCREMENT, email_client varchar(255) NOT NULL, PRIMARY KEY (id_client) ) DEFAULT CHARSET=utf8; INSERT INTO client (email_client) VALUES ('toto@toto.com'), ('lorem@ipsum.net');</code></pre> <h2>Création du fichier CSV</h2> <p>Dans un tableau Excel, copiez les adresses email dans la colonne B et mettez 0 (les id des clients étant en auto_increment...) à A1.<br /> Enregistrez votre fichier sous le format : CSV (séparteur point virgule) (*.csv). <img src="../assets/img/news/sql_csv/sql_csv-1.jpg" alt="" /></p> <h2>Déclaration d'unicité</h2> <p>Allez dans la table concernée (dans cet exemple, le nom de la table sera &quot;client&quot; contenu dans la table &quot;ma_table&quot;).<br /> La clef primaire est &quot;id_client&quot;.<br /> Le champ auquel nous nous intéressons est le champ &quot;email_client&quot;. On veut importer les nouvelles adresses des clients en évitant les doublons.<br /> Sur PMA, allez dans la structure et survolez &quot;Plus&quot; au bout de la ligne &quot;email_client&quot; et cliquez sur &quot;Ajouter un index unique&quot;. <img src="../assets/img/news/sql_csv/sql_csv-2.jpg" alt="" /></p> <p>ou bien en SQL :</p> <pre><code class="language-sql">ALTER TABLE `client` ADD UNIQUE ( `email_client` );</code></pre> <h2>Testons</h2> <p>On fait le test avec le client ayant pour adresse : toto@toto.com</p> <pre><code class="language-sql">INSERT INTO client (email_client) VALUES ('toto@toto.com');</code></pre> <p>Une erreur SQL vous est retournée : <b>#1062 - Duplicate entry 'toto@toto.com' for key 'email_client'</b></p> <!-- <h2>Importation</h2> <p>Toujours dans votre table "client", cliquez sur "Importer". <br />Importez votre fichier dans "Fichier à importer" puis sélectionnez le format "CSV via LOAD DATA" (et non "CSV") et cliquez sur "Exécuter". <br /><img src='#' data-src='../assets/img/news/sql_csv/sql_csv-3.jpg' alt='' /> </p> -->; 2013-03-25 01:32:47 Installer Zend Framework avec Zend_Tool sous WAMP https://etienner.fr/installer-zend-framework-avec-zend_tool-sous-wamp <p>Zend Tools est un outil fournit avec Zend Framework depuis la branche 1.7. C'est un outil qui fonctionne en ligne de commande. Il permet de mettre en place l'architecture d'un nouveau projet, créer de nouveaux controllers, nouvelles actions (duo controller / vues), nouveaux modèles, etc... et ainsi, gagner en terme de temps de productivité.</p> <h2>Téléchargement et installation de Zend</h2> <p>Téléchargez le pack Zend Framework à l'adresse suivante : <a href="http://framework.zend.com/downloads/latest">http://framework.zend.com/downloads/latest</a><br /> Dézippez le pack.<br /> Mettez le dossier dans <code>C:\wamp\bin\php</code> et renommer votre pack en &quot;ZendFramework&quot;.<br /> Ouvrez votre &quot;php.ini&quot; en cliquant sur l'icone de Wamp Server &gt; PHP &gt; php.ini (ce fichier se trouve dans <code>C:\wamp\bin\apache\apache2.2.22\bin</code>) <img src="../assets/img/news/zend_tool/zend_tool_1.jpg" alt="" /></p> <p>Trouvez la ligne (par défaut la 792)<br /> <code>; Windows: 'path1;path2'</code><br /> mettez en dessous :<br /> <code>include_path = '.;C:\wamp\bin\php\ZendFramework\library'</code><br /> Enregistrez la modification.<br /> Ajout des variables d'environnement à Windows pour faire fonctionner Zen_Tool :<br /> allez dans votre panneau de configuration : &quot;Système&quot; (ou Windows + Pause) &gt; &quot;Paramètres système avancés&quot; &gt; &quot;Variables d'environnement&quot;.<br /> Dans &quot;Variables d'utilisateur pour&quot;, cliquez sur &quot;Nouvelle&quot;. Dans &quot;Nom de la variable&quot; mettez &quot;ZEND_TOOL_INCLUDE_PATH&quot; et dans &quot;Valeur de la variable&quot; mettez &quot;C:\wamp\bin\php\ZendFramework\library&quot;.</p> <p><img src="../assets/img/news/zend_tool/zend_tool_2.jpg" alt="" /> Sélectionnez la variable &quot;Path&quot; et cliquez sur &quot;Modifier&quot; :<br /> mettez à la suite &quot;;C:\wamp\bin\php\php5.4.3&quot; (ne pas mettre les guillemets). </p> <p>Remarque : si la variable &quot;Path&quot; n'existe pas, créez la (et ne mettez pas le point virgule).</p> <p><b>Indispensable pour faire fonctioner Zend Framework</b> : activez le module &quot;Rewrite Module&quot; : &quot;Apache&quot; &gt; &quot;Apache modules&quot; &gt; &quot;Rewrite module&quot;. <img src="../assets/img/news/zend_tool/zend_tool_3.jpg" alt="" /></p> <h2>Installation de Zend Tools</h2> <p>Copiez les fichiers &quot;zf.bat&quot; et &quot;zf.php&quot; présents dans <code>C:\wamp\bin\php\ZendFramework\bin</code> dans le dossier <code>C:\wamp\bin\php\php5.4.3</code><br /> Ouvrez l'invité de commande (Windows + R : &quot;cmd&quot;) puis entrez la commande suivante :<br /> <code>zf ?</code> <img src="../assets/img/news/zend_tool/zend_tool_4.jpg" alt="" /></p> <p>Puis placez-vous à la racine du dossier de vos projets :<br /> <code>cd C:\wamp\www</code><br /> Tapez<br /> <code>zf create project le-nom-de-votre-projet</code><br /> Copiez le dossier &quot;Library&quot; (C:\wamp\bin\php\ZendFramework) à la racine du nouveau projet récemment créé.<br /> Allez sur <a href="http://127.0.0.1/mon_projet/public">http://127.0.0.1/mon_projet/public</a></p> <h2>Les commandes de base</h2> <p>Quelques commande avec Zen_Tool (il faudra alors ce déplacer dans le dossier du projet / public à l'aide de la commande &quot;cd&quot;)<br /> Créer un contrôleur :<br /> <code>zf create controller Auth</code><br /> Créer une vue :<br /> <code>zf create view Auth my-script-name</code><br /> Créer un modèle :<br /> <code>zf create model User</code></p> <h2>Sources</h2> <ul> <li><a href="http://m-vaudin.developpez.com/tutoriels/zend-framework/installation-creation-projet">http://m-vaudin.developpez.com/tutoriels/zend-framework/installation-creation-projet</a></li> <li><a href="http://framework.zend.com/manual/1.12/fr/zend.tool.usage.cli.html">http://framework.zend.com/manual/1.12/fr/zend.tool.usage.cli.html</a></li> <li><a href="http://devzone.zend.com/1451/zend_tool-and-zf-18">http://devzone.zend.com/1451/zend_tool-and-zf-18</a></li> </ul>; 2013-03-10 23:34:42 Bouton top au scroll https://etienner.fr/bouton-top-au-scroll <p>Le principe consite lors du scroll de la page, de faire apparaître un bouton pour remonter en haut de la page via jQuery.<br /> Le code jQuery consiste dans un premier temps à mettre en place une condition d'affichage ou non pour le bouton lors du scroll en se servant des fonctions <code>.scroll()</code> et <code>.scrollTop()</code>.<br /> Puis, dans un second temps, utiliser la fonction <code>.animate()</code> pour instaurer un effet de &quot;smooth scroll&quot; (défilement fluide) lors du clic sur le bouton.</p> <h2>Code HTML</h2> <pre><code class="language-markup">&lt;a href="#" class="go_top"&gt;Remonter&lt;/a&gt;</code></pre> <p>Le mot &quot;Remonter&quot; s'affichera si le CSS est désactivé sur le navigateur.</p> <h2>Code CSS</h2> <pre><code class="language-css">.go_top{ background: url('votre_image.png') no-repeat; display: none; position: fixed; width: 128px; /* A régler selon votre image */ height: 128px; /* A régler selon votre image */ bottom: 0; /* A régler selon votre image */ right: 0; /* A régler selon votre image */ text-indent: -9999px; }</code></pre> <h2>Code jQuery</h2> <pre><code class="language-javascript">$(document).ready(function(){ // Condition d'affichage du bouton $(window).scroll(function(){ if ($(this).scrollTop() &gt; 100){ $('.go_top').fadeIn(); } else{ $('.go_top').fadeOut(); } }); // Evenement au clic $('.go_top').click(function(){ $('html, body').animate({scrollTop : 0},800); return false; }); });</code></pre> <p>La valeur <code>100</code> à la ligne <code>$(this).scrollTop() &gt; 100</code> correspond au nombre de pixels scrollés.</p> <h2>Sources</h2> <ul> <li><code>.scroll()</code> : <a href="http://api.jquery.com/scroll">http://api.jquery.com/scroll</a></li> <li><code>.scrollTop()</code> : <a href="http://api.jquery.com/scrollTop">http://api.jquery.com/scrollTop</a></li> <li><code>.animate()</code> :<a href="http://api.jquery.com/animate">http://api.jquery.com/animate</a></li> </ul>; 2013-02-18 21:36:31 Afficher les mots de passe enregistrés en clair https://etienner.fr/afficher-les-mots-de-passe-enregistres-en-clair <p>Lorsque vous vous connectez sur un terminal (PC, Tablette, Smartphone, etc...) public mais aussi privé et que vous enregistrez vos mots de passe, vous êtes susceptible d'être victime d'un vol de compte (Facebook, Twitter, Gmail, Yahoo, etc...). En effet, votre navigateur favoris garde vos identifiant et mot de passe en clair. Il est possible en cherchant dans les options de les trouver mais aussi en changeant le code HTML de la page d'identification.</p> <h2>Directement dans les options du navigateur</h2> <p>Sur Firefox 18 : &quot;Options&quot; &gt; &quot;Options&quot; &gt; onglet &quot;Sécurité&quot; &gt; &quot;Mots de passe enregistrés&quot; &gt; &quot;Afficher les mots de passe&quot;. <img src="../assets/img/news/mdp/mdp-1.jpg" alt="" /></p> <p>Sur Chrome 24 : &quot;Paramètres&quot; &gt; &quot;Afficher les paramètres avancés...&quot; &gt; &quot;Mots de passe et formulaires&quot; &gt; &quot;Gérer les mots de passe enregistrés&quot;. <img src="../assets/img/news/mdp/mdp-2.jpg" alt="" /></p> <h2>En modifiant le code HTML</h2> <p>Le principe est simple si vous avez des notions de base en HTML. Cela consiste à changer le champs input du mot de passe du type &quot;password&quot; (qui permet de masquer la saisie) en un champ de type &quot;text&quot; à partir de l'outil de débogage interne du navigateur (ou avec Firebug). Sur Firefox, faites un clic droit sur le champs du mot de passe &quot;Examiner l'élément&quot;. Editez la ligne <code>type=&quot;password&quot;</code> en <code>type=&quot;text&quot;</code>.</p> <p><img src="../assets/img/news/mdp/mdp-3.jpg" alt="" /> Sur Chrome vous pouvez faire la même manipulation.</p>; 2013-01-26 16:42:26 Axure : grilles responsive https://etienner.fr/axure-grilles-responsive <p>Avec Axure, il est possible de mettre en place un système de grillage pour aider à faire des maquettes responsives grâce aux &quot;Masters&quot;.<br /> Un grillage responsive basique se base sur 12 grilles pour une surface de 960px (largeur de l'illustration ci-dessus).<br /> Chaque grille fait 60 pixels de large (en vert foncé).<br /> Les gouttières (en vert clair) entre les grilles font 20 pixels de largeur et les 2 extrêmes font 10 pixels (vert marron).<br /> <img src="../assets/img/news/axure2/axure2-grille.jpg" alt="" /></p> <h2>Mise en place</h2> <p>Ouvrez Axure, dans l'encadré &quot;Masters&quot; cliquez sur &quot;Add Master&quot;.<br /> Nommez le &quot;960px (12)&quot;.<br /> Sélectionnez ce master (simple clic) puis sélectionnez l'outil &quot;Rectangle&quot; (dans l'encadré Widgets). <img src="../assets/img/news/axure2/axure2-img1.jpg" alt="" /></p> <p>Tracez un rectangle d'une largeur de 60px de 10px à gauche d'une hauteur quelconque. Copiez ce rectangle que vous positionnez à 20 pixels du précédent, répétez l'action 10 fois de suite (la dernière grille / rectangle doit s'arrêter à 950px). <img src="../assets/img/news/axure2/axure2-img2.jpg" alt="" /></p> <p>Vous souhaitez désormais créer un &quot;master&quot; de 640px soient 8 grilles. Faites un clic droit sur le master précédent &quot;Duplicate&quot; &gt; &quot;Master&quot;. Renommez le en &quot;640px (8)&quot; et supprimez les 4 derniers grilles / rectangles.<br /> Utilisation courante des masters :<br /> faites un clic droit sur le master &gt; &quot;Add To Pages...&quot;, cochez les pages désirées puis validez.</p> <h2>Liste des grilles</h2> <ul> <li><b>12 colonnes : 960px</b></li> <li>11 colonnes : 880px</li> <li>10 colonnes : 800px</li> <li><b>9 colonnes : 720px</b></li> <li>8 colonnes : 640px</li> <li>8 colonnes : 560px</li> <li><b>6 colonnes : 480px</b></li> <li>5 colonnes : 400px</li> <li><b>4 colonnes : 320px</b></li> <li>3 colonnes : 240px</li> <li>2 colonnes : 160px</li> <li>1 colonne : 80px</li> </ul> <h2>Sources</h2> <ul> <li>Générateur de grilles : <a href="https://grids.heroku.com">https://grids.heroku.com</a></li> <li>Définition des Masters sur Axure (en anglais) : <a href="http://www.axure.com/masters">http://www.axure.com/masters</a></li> </ul>; 2013-01-08 01:00:46 Formulaire de contact HTML5 / PHP & AJAX https://etienner.fr/formulaire-de-contact-html5-php-ajax <p>Mettre en place un formulaire de contact est devenu simple avec l'arrivée de nouvelles balises HTML et de l'AJAX couplé avec du jQuery. Cela va permettre d'appeler une page de vérification que l'utilisateur final ne verra pas apparaitre à l'écran.<br /> Attention : vous ne pourrez pas tester ce formulaire en local.</p> <h2>Création du formulaire</h2> <p>Formulaire simple en HTML5 avec les nouvelles balises de formulaires tels que <code>required</code> et <code>email</code> dans un fichier &quot;contact.html&quot;</p> <ul> <li><code>required</code> : cet attribut défini un champ obligatoire à remplir.</li> <li><code>email</code> : ou plus précisément <code>type= email</code> permet de vérifier si l'adresse email rentrée dans le input est valide (exit les REGEX...)</li> </ul> <p>Ce qui donne un formulaire assez simple.</p> <pre><code class="language-markup">&lt;div id="form_contact"&gt; &lt;form action="process.php" id="contact" method="POST"&gt; &lt;p&gt; &lt;label for="nom" class="nom"&gt;Nom&lt;/label&gt; &lt;br /&gt;&lt;input id="nom" name="nom" type="text"&gt; &lt;span id="msg_nom"&gt;&lt;/span&gt; &lt;/p&gt; &lt;p&gt; &lt;label for="sujet" class="sujet"&gt;Sujet&lt;/label&gt; &lt;br /&gt;&lt;input id="sujet" name="sujet" type="text"&gt; &lt;span id="msg_sujet"&gt;&lt;/span&gt; &lt;/p&gt; &lt;p&gt; &lt;label for="email"&gt;Email&lt;/label&gt; &lt;br /&gt;&lt;input id="email" name="email" type="email"&gt; &lt;span id="msg_email"&gt;&lt;/span&gt; &lt;/p&gt; &lt;p&gt; &lt;label for="message"&gt;Message&lt;/label&gt; &lt;br /&gt;&lt;textarea id="message" name="message" rows="10" cols="80"&gt;&lt;/textarea&gt; &lt;span id="msg_message"&gt;&lt;/span&gt; &lt;/p&gt; &lt;p&gt; &lt;input type="submit" value="Envoyer" /&gt; &lt;/p&gt; &lt;/form&gt; &lt;span id="msg_all"&gt;&lt;/span&gt; &lt;/div&gt;&lt;!-- end of #form_contact --&gt;</code></pre> <h2>Ajout de jQuery et Ajax</h2> <p>A la suite du fichier &quot;contact.html&quot; (de préférence dans le footer), vient s'implémenter l'AJAX mais qui ne sera exécutable que si la librairie jQuery est appelée.<br /> On va récupérer les valeurs saisies en jQuery dans les champs du formulaire (&quot;nom&quot;, &quot;sujet&quot;, &quot;email&quot; et &quot;message&quot;).</p> <pre><code class="language-markup">&lt;script src="http://code.jquery.com/jquery-1.11.3.min.js"&gt;&lt;/script&gt; &lt;script&gt; $(function(){ $("#contact").submit(function(event){ var nom = $("#nom").val(); var sujet = $("#sujet").val(); var email = $("#email").val(); var message = $("#message").val(); var dataString = nom + sujet + email + message; var msg_all = "Merci de remplir tous les champs"; var msg_alert = "Merci de remplir ce champs"; if (dataString == "") { $("#msg_all").html(msg_all); } else if (nom == "") { $("#msg_nom").html(msg_alert); } else if (sujet == "") { $("#msg_sujet").html(msg_alert); } else if (email == "") { $("#msg_email").html(msg_alert); } else if (message == "") { $("#msg_message").html(msg_alert); } else { $.ajax({ type : "POST", url: $(this).attr("action"), data: $(this).serialize(), success : function() { $("#contact").html("&lt;p&gt;Formulaire bien envoyé&lt;/p&gt;"); }, error: function() { $("#contact").html("&lt;p&gt;Erreur d'appel, le formulaire ne peut pas fonctionner&lt;/p&gt;"); } }); } return false; }); }); &lt;/script&gt;</code></pre> <h2>Création de la page de vérification</h2> <p>Créez un nouveau fichier &quot;process.php&quot;.</p> <pre><code class="language-php">&lt;?php header('Content-Type: text/html; charset=utf-8'); // CONDITIONS NOM if ( (isset($_POST["nom"])) &amp;&amp; (strlen(trim($_POST["nom"])) &gt; 0) ) { $nom = stripslashes(strip_tags($_POST["nom"])); } else { echo "Merci d'écrire un nom &lt;br /&gt;"; $nom = ""; } // CONDITIONS SUJET if ( (isset($_POST["sujet"])) &amp;&amp; (strlen(trim($_POST["sujet"])) &gt; 0) ) { $sujet = stripslashes(strip_tags($_POST["sujet"])); } else { echo "Merci d'écrire un sujet &lt;br /&gt;"; $sujet = ""; } // CONDITIONS EMAIL if ( (isset($_POST["email"])) &amp;&amp; (strlen(trim($_POST["email"])) &gt; 0) &amp;&amp; (filter_var($_POST["email"], FILTER_VALIDATE_EMAIL)) ) { $email = stripslashes(strip_tags($_POST["email"])); } elseif (empty($_POST["email"])) { echo "Merci d'écrire une adresse email &lt;br /&gt;"; $email = ""; } else { echo "Email invalide :(&lt;br /&gt;"; $email = ""; } // CONDITIONS MESSAGE if ( (isset($_POST["message"])) &amp;&amp; (strlen(trim($_POST["message"])) &gt; 0) ) { $message = stripslashes(strip_tags($_POST["message"])); } else { echo "Merci d'écrire un message&lt;br /&gt;"; $message = ""; } // Les messages d'erreurs ci-dessus s'afficheront si Javascript est désactivé // PREPARATION DES DONNEES $ip = $_SERVER["REMOTE_ADDR"]; $hostname = gethostbyaddr($_SERVER["REMOTE_ADDR"]); $destinataire = "monadresse@example.com"; $objet = "[Site Web] " . $sujet; $contenu = "Nom de l'expéditeur : " . $nom . "\r\n"; $contenu .= $message . "\r\n\n"; $contenu .= "Adresse IP de l'expéditeur : " . $ip . "\r\n"; $contenu .= "DLSAM : " . $hostname; $headers = "CC: " . $email . " \r\n"; // ici l'expediteur du mail $headers .= "Content-Type: text/plain; charset=\"ISO-8859-1\"; DelSp=\"Yes\"; format=flowed /r/n"; $headers .= "Content-Disposition: inline \r\n"; $headers .= "Content-Transfer-Encoding: 7bit \r\n"; $headers .= "MIME-Version: 1.0"; // SI LES CHAMPS SONT MAL REMPLIS if ( (empty($nom)) &amp;&amp; (empty($sujet)) &amp;&amp; (empty($email)) &amp;&amp; (!filter_var($email, FILTER_VALIDATE_EMAIL)) &amp;&amp; (empty($message)) ) { echo 'echec :( &lt;br /&gt;&lt;a href="contact.html"&gt;Retour au formulaire&lt;/a&gt;'; } else { // ENCAPSULATION DES DONNEES mail($destinataire, $objet, utf8_decode($contenu), $headers); echo 'Formulaire envoyé'; } // Les messages d'erreurs ci-dessus s'afficheront si Javascript est désactivé ?&gt;</code></pre> <p>On vérifie la véracité des valeurs du formulaire stockées dans des variables dans un premier temps puis on met en place les données à envoyer par email grâce à la fonction php <code>mail</code> en prenant bien soin d'encapsuler les données aux format UTF-8 afin de lire correctement les caractères spéciaux tels que les accents.</p> <p>Si vous souhaitez mettre plusieurs destinataires, il suffit de mettre une virgule suivie d'un espace.<br /> Exemple :<br /> <code>"contact@contact.com, toto@toto.com"</code></p> <h2>Test avec Maildev</h2> <p>MailDev est une application tournant sur NodeJS qui permet de simuler un serveur SMTP (Simple Mail Transfer Protocal) et fournit également un webmail à des fins de tests en local. Les instructions d'installation sont disponibles à l'adresse suivante <a href="http://djfarrelly.github.io/MailDev"><a href="http://djfarrelly.github.io/MailDev">http://djfarrelly.github.io/MailDev</a></a>.</p> <p>Le port SMTP par défaut de MailDev est &quot;1025&quot; alors que celui de PHP est &quot;25&quot;. Dans votre fichier &quot;php.ini&quot;* à la ligne &quot;smtp_port = 25&quot;, remplacez cette valeur par &quot;1025&quot;. Redémarrez votre serveur afin de prendre en compte cette modification.</p> <p>Une fois MailDev lancé, envoyez un email depuis votre formulaire de contact puis à l'adresse <a href="http://localhost:1080">http://localhost:1080</a> vous pouvez consulté ce message comme dans un webmail classique.</p> <ul> <li>Sur WAMP c'est dans le répertoire &quot;wamp\bin\apache\apacheX\bin&quot;</li> </ul>; 2013-01-06 13:54:44 Galerie d'albums Flickr avec PHP https://etienner.fr/galerie-dalbums-flickr-avec-php <p>Dans un précédent billet, je vous expliquais comment se servir de l'API Flickr avec Jquery &amp; JSON. Avec PHP, vous pouvez faire un système de galerie d'album. Cela consiste à afficher le titre et la vignette de chaque album sur une page. De là, il est possible sur la vignette pour afficher toutes les images de l'album. Vous aurez besoin d'une clef d'API générée à la demande par Flickr via votre compte.</p> <h2>galerie.php</h2> <p>Dans un 1er temps, on stocke dans un tableau les données servant à se connecter à l'API Flickr qui sont :</p></p> <ul> <li><code>api key</code> : fournie par Flickr</li> <li><code>user_id</code> : vous le récupéré sur ce site <a href="http://idgettr.com">http://idgettr.com</a></li> <li><code>method</code> : <code>flickr.photosets.getList</code> permet de récupérer la liste des photoset d'un utlisateur, c'est-à-dire la liste des albums</li> <li><code>format</code> : <code>php_serial</code> correspond au format PHP</li> </ul> <pre><code class="language-php">&lt;?php $params = array( 'api_key' =&gt; 'api_key_donnée_par_flickr', 'user_id' =&gt; 'votre_user_id', 'method' =&gt; 'flickr.photosets.getList', 'format' =&gt; 'php_serial' ); ?&gt;</code></pre> <p>Puis, dans un second temps, la suite du code avec vérification de la connexion à l'API :</p> <pre><code class="language-php">&lt;?php $encoded_params = array(); foreach ($params as $k =&gt; $v){ $encoded_params[] = urlencode($k).'='.urlencode($v); } # # appeler l'API et décoder la réponse # $url = "http://api.flickr.com/services/rest/?".implode('&amp;amp;', $encoded_params); $rsp = file_get_contents($url); $rsp_obj = unserialize($rsp); if ($rsp_obj['stat'] == 'ok'){ echo '&lt;ul&gt;'; foreach ($rsp_obj['photosets']['photoset'] as $photo){ // Début de la boucle $id = $photo['id']; // ID du photoset $primary = $photo['primary']; // première photo $secret = $photo['secret']; // identifiant secret de la photo $server = $photo['server']; // ID du serveur $farm = $photo['farm']; // Numéro du sous domaine $title = $photo['title']['_content']; // Titre de l'album $nb = $photo['photos']; // Nombre de photos ?&gt; &lt;li&gt; &lt;a href="album.php?id=&lt;?php echo $id; ?&gt;"&gt; &lt;img src="http://farm&lt;?php echo $farm; ?&gt;.static.flickr.com/&lt;?php echo $server; ?&gt;/&lt;?php echo $primary ?&gt;_&lt;?php echo $secret; ?&gt;_q.jpg" alt="&lt;?php echo $title; ?&gt;" /&gt; &lt;h5&gt;&lt;?php echo $title; ?&gt;&lt;/h5&gt; &lt;?php echo $nb; ?&gt; photos &lt;/a&gt; &lt;/li&gt; &lt;?php } // Fin de la boucle FOREACH echo '&lt;/ul&gt;'; } else{ echo "Echec de l'appel !"; } ?&gt;</code></pre> <h2>album.php</h2> <p>Dans le header du fichier (1ère ligne), on récupère la valeur de l'id de l'album qui n'est autre que le photoset de l'album (méthode permettant de récupérer les informations d'un album) :</p> <pre><code class="language-php">&lt;?php $id = $_GET['id']; ?&gt;</code></pre> <p>Pour l'authentification de l'album, le principe est le même excepté que <code>method</code> devient <code>flickr.photosets.getPhotos</code> et le <code>user_id</code> est remplacé par <code>photoset_id</code> :</p> <pre><code class="language-php">&lt;?php # # créer l'URL API à appeler # $params = array( 'api_key' =&gt; 'c632b4c956418475df8a75d42ce24ed1', // A remplacer par votre clef 'method' =&gt; 'flickr.photosets.getPhotos', 'photoset_id' =&gt; $id, 'format' =&gt; 'php_serial' ); $encoded_params = array(); foreach ($params as $k =&gt; $v){ $encoded_params[] = urlencode($k).'='.urlencode($v); } # # appeler l'API et décoder la réponse # $url = "http://api.flickr.com/services/rest/?".implode('&amp;amp;', $encoded_params); $rsp = file_get_contents($url); $rsp_obj = unserialize($rsp); if ($rsp_obj['stat'] == 'ok'){ echo '&lt;ul&gt;'; foreach ($rsp_obj['photoset']['photo'] as $photo){ // Début de la boucle $id = $photo['id']; // ID photo $secret = $photo['secret']; // Identifiant secret de la photo $server = $photo['server']; // ID du serveur $farm = $photo['farm']; // Numéro du sous domaine $title = $photo['title']; // Titre de la photo ?&amp;gt; &lt;li&gt; &lt;a href="http://farm&lt;?php echo $farm; ?&gt;.static.flickr.com/&lt;?php echo $server; ?&gt;/&lt;?php echo $id; ?&gt;_&lt;?php echo $secret; ?&gt;_b.jpg"&gt; &lt;img src="http://farm&lt;?php echo $farm; ?&gt;.static.flickr.com/&lt;?php echo $server; ?&gt;/&lt;?php echo $id; ?&gt;_&lt;?php echo $secret; ?&gt;_s.jpg" alt="&lt;?php echo $titre; ?&gt;"&gt; &lt;h5&gt;&lt;?php echo $title; ?&gt;&lt;/h5&gt; &lt;/a&gt; &lt;/li&gt; &lt;?php } // Fin de la boucle FOREACH echo '&lt;/ul&gt;'; } else{ echo "échec de l'appel !"; } ?&gt;</code></pre> <h2>Sources</h2> <ul> <li>Documentation API Flickr : <a href="http://www.flickr.com/services/api">http://www.flickr.com/services/api</a></li> <li>Format de réponse PHP en série : <a href="http://www.flickr.com/services/api/response.php.html">http://www.flickr.com/services/api/response.php.html</a></li> </ul>; 2012-09-06 12:07:02 Menu responsive from scratch https://etienner.fr/menu-responsive-from-scratch <p>De plus en plus de sites se développent en responsive afin de répondre à la demande du marché de la navigation mobile. Créer un menu responsive permet de rendre la navigation de votre site Web agréable sur support mobile. Concevoir un menu de navigation responsive est juste une question de bonne pratique des media queries en CSS.</p> <h2>Objectif</h2> <p>Le menu du header sera cachée et remplacé par une image cliquable qui permettra d'accéder au menu du footer. Quant au liens du menu du footer, initialement disposés horizontalement, il passeront à un dispositif de liens positionnés verticalement les uns au dessus des autres.<br /> Explication en image :<br /> version desktop <img src="../assets/img/news/menu-responsive/min_menu-responsive-1.jpg" alt="" /></p> <p>version mobile, menu du header <img src="../assets/img/news/menu-responsive/menu-responsive-2.jpg" alt="" /></p> <p>version mobile, menu du footer <img src="../assets/img/news/menu-responsive/menu-responsive-3.jpg" alt="" /></p> <h2>Partie HTML</h2> <pre><code class="language-markup">&lt;header id="header"&gt; &lt;h1&gt; &lt;a href="#"&gt; &lt;img id="logo" src="http://placehold.it/300x50.gif" alt="Title of my website" /&gt; &lt;/a&gt; &lt;/h1&gt; &lt;nav&gt; &lt;ul&gt; &lt;li&gt; &lt;a href="#"&gt;Link #1&lt;/a&gt; &lt;/li&gt; &lt;li&gt; &lt;a href="#"&gt;Link #2&lt;/a&gt; &lt;/li&gt; &lt;li&gt; &lt;a href="#"&gt;Link #3&lt;/a&gt; &lt;/li&gt; &lt;li&gt; &lt;a href="#"&gt;Link #4&lt;/a&gt; &lt;/li&gt; &lt;/ul&gt; &lt;/nav&gt; &lt;div class="anchor-nav"&gt; &lt;a id="top" href="#bottom"&gt; &lt;img src="img/menu.png" alt="Menu" /&gt; &lt;/a&gt; &lt;/div&gt; &lt;/header&gt; &lt;!-- contenu de la page --&gt; &lt;footer&gt; &lt;div class="anchor-nav"&gt; &lt;a id="bottom" href="#header"&gt; &lt;img src="img/top.png" alt="top" /&gt; &lt;/a&gt; &lt;/div&gt; &lt;ul&gt; &lt;li id="current"&gt; &lt;a href="#"&gt;Homepage&lt;/a&gt; &lt;/li&gt; &lt;li&gt; &lt;a href="#"&gt;Link #1&lt;/a&gt; &lt;/li&gt; &lt;li&gt; &lt;a href="#"&gt;Link #2&lt;/a&gt; &lt;/li&gt; &lt;li&gt; &lt;a href="#"&gt;Link #3&lt;/a&gt; &lt;/li&gt; &lt;li&gt; &lt;a href="#"&gt;Link #4&lt;/a&gt; &lt;/li&gt; &lt;/ul&gt; &lt;/footer&gt;</code></pre> <h2>Partie CSS</h2> <p>Version desktop (prévoir un reset CSS) :</p> <pre><code class="language-css">#logo{ margin: 10px 0 0 10px; } nav{ float: right; } nav ul li{ float: left; } nav ul li a{ line-height: 75px; margin: 0 10px 0 10px; padding: 0 10px 0 10px; text-transform: uppercase; } a#top{ display: none; }</code></pre> <p>Version mobile avec media queries :</p> <pre><code class="language-css">@media only screen and (max-device-width: 960px), only screen and (max-width: 960px){ nav{ display: none; } a#top{ display: block; float: right; height: 50px; margin: 10px 20px; text-transform: uppercase; width: 50px; } footer{ padding: 0; } footer ul{ float: none; } footer ul li{ display: block; margin: 0; width: 100%; } footer ul li a{ border-top: 1px solid #ddd; float: none; padding: 15px 0 15px 0; text-align: center; text-transform: uppercase; } footer ul li a:hover,{ background: #000; color: #fff; text-decoration: none; } footer ul li#current a{ color: #000; } a#bottom{ margin: -32px 0 0; } }</code></pre> <h2>Partie jQuery</h2> <p>Pour rajouter un coté &quot;smoothcroll&quot; (&quot;défilement régulier&quot;) lors des clics entre les 2 anchors (image de menu du haut et celui du menu du bas) :</p> <pre><code class="language-javascript">&lt;script src="http://code.jquery.com/jquery-1.8.0.min.js"&gt;&lt;/script&gt; &lt;script src="http://gsgd.co.uk/sandbox/jquery/easing/jquery.easing.1.3.js"&gt;&lt;/script&gt; &lt;script&gt; $(function() { $('.anchor-nav a').bind('click',function(event){ var $anchor = $(this); $('html, body').stop().animate({ scrollTop: $($anchor.attr('href')).offset().top }, 1500,'easeInOutExpo'); /* if you don't want to use the easing effects: $('html, body').stop().animate({ scrollTop: $($anchor.attr('href')).offset().top }, 1000); */ event.preventDefault(); }); });</code></pre>; 2012-09-03 15:02:41 Debugger Opera Mobile Emulator https://etienner.fr/debugger-opera-mobile-emulator <p>&lt;p&gt; Opera Mobile Emulator permet, comme son nom l'indique d'&eacute;muler Opera Mobile, navigateur d&eacute;di&eacute; au supports mobiles. Il est donc int&eacute;ressant de se servir de ce logiciel si vous d&eacute;velopper vos sites en responsive. Vous pouvez aussi proc&eacute;der au d&eacute;bogage de vos pages Web en passant par Opera Dragonfly, outils de d&eacute;bogage int&eacute;gr&eacute; dans Opera Desktop en passant par le d&eacute;bogage &agrave; distance. &lt;br /&gt;&lt;br /&gt;&lt;/p&gt;</p> <p>&lt;p&gt; &lt;strong&gt;Taille du texte&lt;/strong&gt; &lt;br /&gt;Par d&eacute;faut, la taille du texte est de 150%. Pour avoir le m&ecirc;me rendu que sur votre navigateur Web de PC, il faut que la taille soit de 100%. &lt;br /&gt;Cliquez sur le logo d'Opera &gt; &quot;R&eacute;glages&quot; &gt; &quot;Zoom&quot; &gt; cochez &quot;100%&quot;. &lt;img class=&quot;center&quot; src=&quot;&quot; data-src=&quot;img/news/opera/opera-1.jpg&quot; alt=&quot;&quot; /&gt; &lt;br /&gt; &lt;/p&gt;</p> <p>&lt;p&gt; &lt;strong&gt;D&eacute;bogage&lt;/strong&gt; &lt;br /&gt;Sur Opera (version desktop), lancez Opera Dragonfly en faisant Ctrl + Shift + i (ou clic droit sur la page en cours &quot;Inspecter l'&eacute;l&eacute;ment&quot;) &gt; cliquez sur le pictogramme &quot;D&eacute;bogage &agrave; distance&quot; (3&egrave;me icone situ&eacute; &agrave; droite) puis copiez le num&eacute;ro du port (7701 par d&eacute;faut) et validez en cliquant sur &quot;Appliquer&quot;. &lt;img class=&quot;center&quot; src=&quot;&quot; data-src=&quot;img/news/opera/opera-2.jpg&quot; alt=&quot;&quot; /&gt; &lt;br /&gt;Dans Opera Mobile Emulator, tapez &lt;code&gt;opera:debug&lt;/code&gt; et validez ( touche Entr&eacute;e de votre clavier). Dans &quot;port&quot;, tapez le num&eacute;ro du port (7001 par d&eacute;faut) puis cliquez sur &quot;Connecter&quot;. &lt;img class=&quot;center&quot; src=&quot;&quot; data-src=&quot;img/news/opera/opera-3.jpg&quot; alt=&quot;&quot; /&gt; &lt;br /&gt;Tapez l'adresse URL du d&eacute;sir&eacute; dans cet onglet. &lt;br /&gt;Revenez sur Opera Dragonfly, vous pouvez voir que l'outil de d&eacute;bogage est bien connect&eacute; sur l'&eacute;mulateur mobile. &lt;img class=&quot;center&quot; src=&quot;&quot; data-src=&quot;img/news/opera/opera-4.jpg&quot; alt=&quot;&quot; /&gt; &lt;br /&gt; &lt;/p&gt;</p> <p>&lt;p&gt; Petit bonus : afficher le code source sur votre &eacute;diteur de texte pr&eacute;f&eacute;r&eacute; depuis Opera. &lt;br /&gt;Allez dans &quot;Outils&quot; &gt; &quot;Pr&eacute;f&eacute;rences&quot; &gt; &quot;Avanc&eacute;s&quot; &gt; &quot;Programmes&quot; &gt; &quot;Choisir l'application pour afficher le code source&quot; &gt; &quot;Editer&quot; &gt; cochez &quot;Ouvrir avec une autre application&quot; &gt; &quot;Choisir&quot; et cliquez sur &quot;OK&quot; &lt;/p&gt;</p> <p>&lt;p&gt; Aller plus loin : &lt;br /&gt;Page officiel de Dragon Fly : &lt;a href=&quot;<a href="http://fr.opera.com/dragonfly&amp;quot">http://fr.opera.com/dragonfly&amp;quot</a>; target=&quot;_blank&quot;&gt;<a href="http://fr.opera.com/dragonfly&amp;lt;/a&amp;gt">http://fr.opera.com/dragonfly&amp;lt;/a&amp;gt</a>; &lt;/p&gt;</p>; 2012-08-31 16:49:26 Menu déroulant / Drop-Down Menu responsive avec Foundation https://etienner.fr/menu-deroulant-drop-down-menu-responsive-avec-foundation <p>Foundation est un framework développé par Zurb qui permet de rendre votre site responsive à l'aide d'un système de grillage (ligne comportant une ou plusieurs grilles). Ce framework est une grosse bibliothèque écrite sur une feuille de style CSS comportant un grand nombre de propriétés permettant d'être appelées dans vos pages web mais aussi du jQuery pour quelques plugins responsive (un slider basique &quot;Orbit&quot;, une modalbox &quot;Reveal&quot;, etc..).</p> <p><img src="../assets/img/news/menu_responsive_foundation/menu_responsive_foundation_320.jpg" alt="" /></p> <h2>Téléchargement</h2> <p>Téléchargez le framework à l'adresse suivante : <a href="http://foundation.zurb.com">http://foundation.zurb.com</a><br /> Attention, la feuille de style &quot;foundation.css&quot; comporte un reset CSS intégré (celui d'Eric Meyer's).</p> <h2>Contenu de la page</h2> <p>Dans le header :</p> <pre><code class="language-markup">&lt;meta name="viewport" content="width=device-width" /&gt; &lt;link rel="stylesheet" href="stylesheets/foundation.css"&gt; &lt;link rel="stylesheet" href="stylesheets/ie.css"&gt; &lt;script src="javascripts/modernizr.foundation.js"&gt;&lt;/script&gt;</code></pre> <p>Dans le footer :</p> <pre><code class="language-markup">&lt;script src="javascripts/jquery.min.js"&gt;&lt;/script&gt; &lt;script src="javascripts/foundation.js"&gt;&lt;/script&gt; &lt;script src="javascripts/app.js"&gt;&lt;/script&gt;</code></pre> <p>Dans le corps de la page :</p> <pre><code class="language-markup">&lt;div class="container"&gt; &lt;div class="row"&gt; &lt;ul class="nav-bar"&gt; &lt;li class="has-flyout"&gt; &lt;a href="" class="main"&gt;Lien #1&lt;/a&gt; &lt;a href="" class="flyout-toggle"&gt;&lt;span&gt;&lt;/span&gt;&lt;/a&gt; &lt;div class="flyout small"&gt; &lt;ul&gt; &lt;li&gt;&lt;a href="#"&gt;Lien #1-a&lt;/a&gt;&lt;/li&gt; &lt;li&gt;&lt;a href="#"&gt;Lien #1-b&lt;/a&gt;&lt;/li&gt; &lt;li&gt;&lt;a href="#"&gt;Lien #1-c&lt;/a&gt;&lt;/li&gt; &lt;/ul&gt; &lt;/div&gt; &lt;/li&gt; &lt;li&gt;&lt;a href="#" class="main"&gt;Lien #2&lt;/a&gt;&lt;/li&gt; &lt;li class="has-flyout"&gt; &lt;a href="" class="main"&gt;Lien #3&lt;/a&gt; &lt;a href="" class="flyout-toggle"&gt;&lt;span&gt;&lt;/span&gt;&lt;/a&gt; &lt;div class="flyout small"&gt; &lt;ul&gt; &lt;li&gt;&lt;a href="#"&gt;Lien #3-a&lt;/a&gt;&lt;/li&gt; &lt;li&gt;&lt;a href="#"&gt;Lien #3-b&lt;/a&gt;&lt;/li&gt; &lt;li&gt;&lt;a href="#"&gt;Lien #3-c&lt;/a&gt;&lt;/li&gt; &lt;li&gt;&lt;a href="#"&gt;Lien #4-c&lt;/a&gt;&lt;/li&gt; &lt;/ul&gt; &lt;/div&gt; &lt;/li&gt; &lt;li&gt;&lt;a href="#" class="main"&gt;Lien #4&lt;/a&gt;&lt;/li&gt; &lt;/ul&gt; &lt;/div&gt; &lt;/div&gt;</code></pre> <h2>Extrait du fichier &quot;app.js&quot;</h2> <pre><code class="language-javascript"> /* DROPDOWN NAV ------------- */ var lockNavBar = false; $('.nav-bar a.flyout-toggle').live('click', function(e) { e.preventDefault(); var flyout = $(this).siblings('.flyout'); if (lockNavBar === false) { $('.nav-bar .flyout').not(flyout).slideUp(500); flyout.slideToggle(500, function(){ lockNavBar = false; }); } lockNavBar = true; }); if (Modernizr.touch) { $('.nav-bar&gt;li.has-flyout&gt;a.main').css({ 'padding-right' : '75px' }); $('.nav-bar&gt;li.has-flyout&gt;a.flyout-toggle').css({ 'border-left' : '1px dashed #eee' }); } else { $('.nav-bar&gt;li.has-flyout').hover(function() { $(this).children('.flyout').show(); }, function() { $(this).children('.flyout').hide(); }) }</code></pre> <h2>Sources</h2> <ul> <li>Documentation officielle de Foundation : <a href="http://foundation.zurb.com/docs">http://foundation.zurb.com/docs</a></li> <li>&quot;Responsive webdesign : adapter un site à toutes les résolutions&quot; : <a href="http://www.ergonomie-interface.com/conception-maquettage/responsive-webdesign-adapter-resolutions">http://www.ergonomie-interface.com/conception-maquettage/responsive-webdesign-adapter-resolutions</a></li> </ul>; 2012-06-16 21:00:19 6 plugins intéressant sur Notepad++ https://etienner.fr/6-plugins-interessant-sur-notepad <p>Notepad++ est un éditeur de code source gratuit. On peut installer des plugins sur ce logiciel afin de le compléter et rendre l'usage de coder plus intéressant. Les plugins s'installent en interne. Pour ce faire, lancez Notepad++ puis allez dans &quot;Compléments&quot; &gt; &quot;Plugin manager&quot; &gt; &quot;Show plugin manager&quot; &gt; cochez le ou les plugins désiré(s) et cliquez sur &quot;Install&quot;. Le logiciel va vous demander de redémarrer pour prendre en compte les nouveaux paramètres, cliquez sur &quot;Oui&quot;.</p> <h2>Autosave</h2> <p>Oubliez d'enregistrer vos fichiers, autosave est un gestionnaire de sauvegarde qui permet de sauvegarder toutes les tant de minutes vos fichiers. Allez dans &quot;Compléments&quot; &gt; &quot;Autosave&quot; &gt; cochez &quot;At timed intervaks every&quot; et tapez le nombre de minutes que vous désirez puis validez. <img src="../assets/img/news/notepadplus/autosave.jpg" alt="" /></p> <h2>Explorer</h2> <p>Un explorateur de fichiers, rien de plus simple. Cliquez sur l'icone représentant une loupe sur un dossier ou bien par le raccourci clavier Ctrl+Alt+Shift+E. <img src="../assets/img/news/notepadplus/explorer.jpg" alt="" /></p> <h2>InsertLoremIpsum</h2> <p>Vous avez toujours eu un faible pour le latin mais n'étant pas une flèche dans cette langue vous préférez vous servir d'un générateur de lorem ipsum.<br /> Allez dans &quot;Compléments&quot; &gt; &quot;InsertLoremIpsum&quot; &gt; &quot;View Insert Dialog&quot;. La boite de dialogue apparait sur la droite. Vous pouvez sélectionner le nombre de mots, de phrases ou de paragraphes puis cliquez sur &quot;Insert&quot;. <img src="../assets/img/news/notepadplus/insertloremipsum.jpg" alt="" /></p> <h2>TextFX</h2> <p>De base, avec Notepad++, lorsque que vous faites Ctrl + Space, vous avez la liste des balises disponibles en sélection.<br /> TextFX a une option qui permet de faire de l'autocomplétion sur du code html.<br /> Allez dans &quot;TextFx&quot; &gt; &quot;TextFX Settings&quot; cliquez sur &quot;+Autoclose XHTML/XML &lt;Tag&gt;&quot;.</p> <h2>NppExport</h2> <p>Permet d'exporter le code avec la colorisation syntaxique vers du format RTF (idéal pour Word).<br /> Allez dans &quot;Compléments&quot; &gt; &quot;NppExport&quot; &gt; &quot;Copy RTF to Clipboard&quot; pour copier coller sinon &quot;Export to RTF&quot; pour générer le fichier au format Wordpad.</p> <h2>NppDocShare</h2> <p>2 machines connectées sur un réseau local travaillant en même temps sur Notepad++ (comme sur Google Doc), c'est possible.<br /> Allez dans &quot;Complément&quot; &gt; &quot;NppDocShare&quot; et cliquez sur &quot;Serve&quot; pour devenir hôte et &quot;Connect&quot; si vous êtes le client. Malheureusement, à l'heure actuelle, le plugin ne supporte pas plus d'une machine cliente :(</p>; 2012-05-30 18:27:43 Iframe, object & embed responsive https://etienner.fr/iframe-object-embed-responsive <p>Vous venez de finir de coder votre site mais vous vous rendez compte qu'une vidéo Youtube, Vimeo, Dailymotion, présentation Slideshare garde sa largeur fixe et casse la résolution minimale de votre smartphone ou de votre tablette. Il existe une astuce toute simple en CSS qui s'applique aux <code>iframes</code>, <code>object</code> et <code>embed</code>.</p> <h2>Partie HTML</h2> <pre><code class="language-markup">&lt;div class="video"&gt; &lt;div class="embed-container"&gt; &lt;iframe src="http://www.youtube.com/embed/id-youtube" frameborder="0" allowfullscreen&gt;&lt;/iframe&gt; &lt;/div&gt; &lt;/div&gt;</code></pre> <p>Le but l'astuce est de définir la largeur et la hauteur dans le css et non dans le code html.</p> <h2>Partie CSS</h2> <pre><code class="language-css">.video{ max-width: /*Largeur de la video*/px; } .embed-container{ height: 0; overflow: hidden; padding-bottom: 56.25%; /* 16/9 ratio */ position: relative; } .embed-container iframe, .embed-container object, .embed-container embed{ height: 100%; left: 0; top: 0; width: 100%; }</code></pre> <p>Petit rappel le calcul du ratio se fait de cette façon :<br /> 16/9 = 1,777777777777778 puis le pourcentage 100/1,777777777777778 = 56,2459%.<br /> Autre exemple avec le 16/10 :<br /> 16/10 = 1,6 ce qui donne 100/1.6 = 62.5%.</p> <h2>Aller plus loin</h2> <p>Pour que le rendu soit responsive sur votre smartphone ou votre tablette pleine de traces de doigts, il ne faut pas oublier la meta suivante dans la balise <code>head</code> :</p></p> <pre><code class="language-markup">&lt;meta name="viewport" content="initial-scale=1.0" /&gt;</code></pre> <p>Il existe une autre méthode qui permet de rendre vos vidéos responsive, FITVIDS.JS. C'est un script qui tourne sur jQuery ce qui engendre du temps de chargement en plus... à vous de voir !</p>; 2012-05-28 00:27:13 Créer un carrousel simple avec Axure https://etienner.fr/creer-un-carrousel-simple-avec-axure <p>Axure est un logiciel permettant de faire du prototypage de site Web. Vous pouvez faire des maquettes de site assez simplement avant de passer par l'étape de réalisation de la maquette (ou wireframe) sous un logiciel de PAO. Axure embarque des possibilités d'animations que l'on peut retrouver sur certains sites web ce qui est idéal pour montrer le fonctionnement du futur site à son client.</p> <h2>Mise en place du Dynamic Panel</h2> <p>Axure lancé, insérez un &quot;Dynamic Panel&quot; sur la page souhaitée <img src="../assets/img/news/axure1/image1.jpg" alt="" /></p> <p>Dans le &quot;Dynamic Panel Manager&quot; (en bas à gauche), doubles cliquez sur &quot;Unlabeled&quot; (ou bien clic gauche, &quot;Edit&quot;). La fenêtre &quot;Dynamic Panel State Manager&quot; apparaît. <img src="../assets/img/news/axure1/image2.jpg" alt="" /></p> <p>Dans &quot;Dynamic Panel Label&quot;, tapez &quot;Carrousel&quot;. Et dans &quot;Panel States&quot;, renommez &quot;State 1&quot; en &quot;Slide 1&quot; puis ajoutez 2 autres &quot;States&quot; que vous renommez &quot;Slide 2&quot; et &quot;Slide 3&quot; puis validez. <img src="../assets/img/news/axure1/image3.jpg" alt="" /></p> <h2>Création du contenu des slides</h2> <p>Doubles cliquez sur chaque States (&quot;Slide 1&quot;, &quot;Slide 2&quot; et &quot;Slide 3&quot;). Vous pouvez remarquer que ces derniers ce sont ouverts dans un onglet à leur nom.<br /> Placez le contenu souhaité dans chacun des slides.<br /> Ajoutez-y 2 flèches de directions (précédente et suivante) avec l'outil rectangle (pour faire une flèche en forme de triangle : clic droit sur le rectangle, &quot;Edit Button Shape&quot;, &quot;Triangle Left&quot; pour précédent, &quot;Triangle Right&quot; pour suivant).</p> <h2>Passons maintenant aux transitions</h2> <p>Dans le &quot;slide 1&quot;, sélectionnez la flèche suivante que vous venez de créer. Dans &quot;Widget Properties&quot; (en haut à droite), doubles cliquez sur &quot;OnClick&quot;. La fenêtre &quot;Case Editor&quot;. <img src="../assets/img/news/axure1/image4.jpg" alt="" /></p> <p>Cliquez sur &quot;Set Panel state(s) to State(s)&quot; (dans &quot;Dynamic Panels&quot;), cochez &quot;Carrousel (Dynamic Panel)&quot;, selectionnez dans &quot;Select the state&quot; le slide suivant, dans le cas présent, &quot;Slide 2&quot;.<br /> Nous allons sélectionner dans &quot;Animate In&quot; l'option de transition &quot;Slide left&quot; pour faire un effet de slide. Une fois ces options définies, cliquez sur &quot;OK&quot;. <img src="../assets/img/news/axure1/image5.jpg" alt="" /></p> <p>Répétez cette étape pour les autres slides puis générer votre prototype (touche F5).</p>; 2012-05-23 16:58:07 Flickr & JSON https://etienner.fr/flickr-json <p>Vous souhaitez afficher un album de votre galerie Flickr sur votre site Web. La méthode de &quot;callback&quot; sert à récupérer des données stockées sur un serveur distant. L'API Flickr permet d'effectuer ce genre de requête combiné avec du jQuery (utilisation de <code>$.getJSON</code>) et du JSON (JavaScript Object Notation).</p> <h2>photosets.getPhotos</h2> <p>Pour récupérer l'id correspondant à l'un de vos albums, copiez le lien url pointant vers l'un de vos albums.<br /> En copiant le lien, par défaut, l'url se présente sous la forme suivante :<br /> <a href="http://www.flickr.com/photos/nom_de_votre_compte/sets/photoset_id/">http://www.flickr.com/photos/nom_de_votre_compte/sets/photoset_id/</a><br /> Exemple :<br /> <a href="http://www.flickr.com/photos/etienne-r/sets/72157629339061530">http://www.flickr.com/photos/etienne-r/sets/72157629339061530</a><br /> le &quot;photoset_id&quot; est 72157629339061530<br /> Pour générer le JSON, allez à l'adresse suivante après vous êtes connecté sur le site :<br /> <a href="http://www.flickr.com/services/api/explore/flickr.photosets.getPhotos">http://www.flickr.com/services/api/explore/flickr.photosets.getPhotos</a> : </p> <ul> <li>&quot;photoset_id&quot;, rentrez l'id de votre album</li> <li>&quot;Sortie&quot;, sélectionnez JSON</li> </ul> <p>Cliquez sur &quot;Call method&quot; afin de générer le JSON.<br /> Nous allons nous intéresser à l'URL de callback qui se situe en dessous du cadre où le JSON vient d'être généré car il vous faut copier ce lien dans le code suivant :</p> <pre><code class="language-javascript">&lt;div id="container"&gt; &lt;ul id="images"&gt; &lt;/div&gt; &lt;script src="http://code.jquery.com/jquery-1.7.2.min.js"&gt;&lt;/script&gt; &lt;script&gt; $.getJSON("http://api.flickr.com/services/rest/?method=flickr.photosets.getPhotos&amp;api_key=clef-API&amp;photoset_id=le_photoset&amp;extras=original_format&amp;format=json&amp;jsoncallback=?", function(data){ // Debut de la boucle $.each(data.photoset.photo, function(i,item){ // Sockage de l'image dans une variable var photo = 'http://farm' + item.farm + '.static.flickr.com/' + item.server + '/' + item.id + '_' + item.secret + '_s.jpg'; // Sockage de l'url dans une variable var url= 'http://farm' + item.farm + '.static.flickr.com/' + item.server + '/' + item.id + '_' + item.secret + '_c.jpg'; // Affichage des images dans la balise ul#images avec le l'url dans la balise li $("&lt;img/&gt;").attr({src: photo, alt: item.title}).appendTo("#images").wrap("&lt;li&gt;&lt;a href=' "+ url +"' title=' "+ item.title +" ' &gt;&lt;/a&gt;&lt;/li&gt;"); }); //Fin de la boucle }); // Fin appel JSON &lt;/script&gt;</code></pre> <p>Petites explications :</p> <ul> <li>pour charger une image sur Flickr, il faut qu'elle soit de la forme : <a href="http://farm#.static.flickr.com/serveur/id-photo">http://farm#.static.flickr.com/serveur/id-photo</a> _id-secret.jpg</li> <li><code>farm#</code> : numéro du sous domaine (notion de &quot;ferme de sous domaines&quot;)</li> <li><code>server</code> : id du serveur</li> <li><code>id-photo</code> : identifiant de la photo</li> <li><code>secret</code> : identifiant secret de la photo</li> </ul> <p>Le script ci-dessus va chercher le JSON par le biais de <code>$.getJSON</code> et va récupérer les données dans une boucle <code>$.each</code>. La variable <code>photo</code> va stocker le lien de chaque photo au format vignette tandis que la variable <code>url</code> va enregistrer l'adresse de chaque photo au format 800px. La troisième et dernière ligne de la boucle permet d'afficher le résultat au format HTML dans la balise dont l'id est image. Cette dernière comportera l'affichage de la variable photo dans une balise image englobée dans les puces avec l'url de la photo au format 800px.<br /> Pour la taille de l'image, devant l'extension jpg, on peut mettre les options suivantes :</p> <ul> <li><code>_s</code> : carré (75 * 75px)</li> <li><code>_m</code> : medium (paysage : 240 <em> 160px - portrait : 160 </em> 240px)</li> <li><code>_z</code> : 640px</li> <li><code>_c</code> : 800px</li> </ul> <h2>photo_public.gne</h2> <p>Si vous souhaitez afficher les 20 dernières images d'un compte Flickr, vous pouvez passer par <code>photo_public.gne</code> qui est aussi disponible pour les non abonnés.</p> <pre><code class="language-javascript">&lt;div id="container"&gt; &lt;ul id="images"&gt; &lt;/div&gt; &lt;script src="js/http://code.jquery.com/jquery-1.7.2.min.js"&gt;&lt;/script&gt; &lt;script&gt; // Debut appel JSON $.getJSON("http://api.flickr.com/services/feeds/photos_public.gne?id=idGettr-du-compte&amp;format=json&amp;jsoncallback=?", function(data){ // Debut de la boucle $.each(data.items, function(i,item){ // Sockage de l'image dans une variable et remplacement de la taille "m" par la taille "s" var min = (item.media.m).replace("_m.jpg", "_s.jpg"); // Affichage des images dans la balise ul#images avec le l'url dans la balise li $("&lt;img/&gt;").attr("src", min).appendTo("#photos").wrap("&lt;li&gt;&lt;a href='" + url + "' '&gt;&lt;/a&gt;&lt;li&gt;"); }); // Fin de la boucle }); // Fin appel JSON &lt;/script&gt;</code></pre> <p>Pour obtenir le idGettr allez sur <a href="http://idgettr.com">http://idgettr.com</a><br /> Si vous souhaitez limiter l'affichage des images à 10 par exemple, il faudra imposer une limite en fin de la boucle <code>$.each</code> :</p> <pre><code class="language-javascript">if (i == 9) return false; // "9" car la variable i commence à 0</code></pre> <h2>Débogage</h2> <p>Vous pouvez afficher le JSON sous Firebug en allant dans Script (il faut rafraichir la page) puis à droite de &quot;Tous&quot;, il y a le nom de la page sur laquelle vous êtes, cliquez sur la flèche pointant vers le bas et sélectionnez le lien en dessous de &quot;api.flickr.com/services/rest/&quot;.<br /> Sous Chrome, allez dans &quot;Scripts&quot; cliquez sur le premier icone à gauche (Scripts navigator) puis allez dans &quot;api.flickr.com&quot; &gt; &quot;Services&quot;.<br /> <img src="../assets/img/news/flickr/flickr_capture_json.jpg" alt="" /></p> <p>En effet, dans le mode <code>photo_public.gne</code>, vous pouvez observer que l'adresse url de chaque photos est données dans &quot;items&quot; &gt; &quot;media&quot; &gt; &quot;m&quot; ce qui n'est pas le cas dans le mode <code>photosets.getPhotos</code>.</p>; 2012-05-18 16:45:57