Analyse de code statique avec phpstan

Phpstan est un outil en ligne de commande qui va vous permettre de détecter automatiquement les erreurs les plus simples en scannant l’intégralité de votre projet.

Démonstration phpstan

De base, l’outil va analyser une application PHP sans tenir compte des spécificités des différents frameworks et librairies. C’est pourquoi il existe des extensions comme PHPstan Symfony pour affiner l’analyse.

Si comme moi vous voulez l’utiliser sur un projet qui a beaucoup de legacy (plus de 10 ans) vous allez avoir un rapport gigantesque. Il faut trouver des solutions pour réduire le rapport à la pull request (merge request) que l’on doit relire. 

Il existe justement un outil qui permet de faire cette corrélation entre le diff et le rapport de PHPstan. Reviewdog est une application en go qui permet de prendre en entrée le rapport d’un outil d’analyse de code comme eslint, golint ou dans notre cas PHPstan.

Sans passer par reviewdog :

➜  vendor/bin/phpstan analyse src -l 4 --no-progress --error-format=raw | wc -l

     292

292 anomalies sur la globalité du projet.

En passant par reviewdog :

➜ vendor/bin/phpstan analyse src -l 4 --no-progress --error-format=raw | reviewdog -diff="git diff master" -f=phpstan | wc -l

	2

Il se reste que deux anomalies, et elles sont directement liées au diff entre la branche courante et master.

Reviewdog peut se connecter directement sur votre gitlab ou github pour ajouter des commentaires dans les pull requests.

Tout ceci ne remplace pas une relecture par un « humain » mais cela va nous donner des indicateurs de qualité et va vous délester des vérifications les plus simples (Est-ce que cette méthode/ classe existe ?). Selon le niveau de vérification que l’on donne à PHPstan on va pouvoir vérifier plus ou moins de choses, comme les dockblocks par exemple.

Ce qui est assez intéressant c’est que PHPstan est extensible et qu’il est donc possible de créer nos propres vérifications. Il serait donc possible, par exemple, de remonter une alerte quand on utilise une classe ou méthode marquée comme dépréciée dans notre code.

Du machine learning dans mes cocktails

Dans cet article, nous allons parler Mojitos, machine learning et PHP, trois sujets que l’on mélange assez rarement, mais qui dans le cas du site Cocktailand sont rassemblés.

Use case

Je souhaite afficher la recette de cocktail qui ressemble le plus à celle que le visiteur est en train de lire. Comme je suis finalement quelqu’un d’assez fainéant, je n’ai pas envie de maintenir des listes de cocktails associés manuellement. Il faut donc trouver un moyen de calculer automatiquement cette liste pour les quelques 600 recettes et que cette liste soit mise à jour automatiquement pour intégrer les nouvelles recettes.

Données à disposition:

  • Les ingrédients de la recette, avec les volumes pour chacun
  • Est-ce que le cocktail est alcoolisé ou non
  • La catégorie du cocktail
  • Le nombre de vues de la recette de cocktail
Vision simplifiée de la base de données 

KNearestNeighbors

Afin de résoudre ce problème, je vais utiliser un algorithme de classification nommé KNearestNeighbors mis à disposition en PHP via la libraire php-ml avec une méthode de calcul de distance custom entre les cocktails.

Cet algo, aussi appelé le K-NN, permet de classifier des objets en fonction de la classe des autres objets déjà classifiés à proximité. La proximité est calculée par une méthode du type Euclidienne, Hamming,  Manhattan ou encore selon des règles spécifiques au domaine. Il suffit que la méthode soit idempotente et qu’elle retourne une valeur numérique pour qu’elle soit utilisable.

 Le K de cet algorithme est la valeur de validation croisée qui va permettre de choisir la classe à associer à l’objet. 

Pour déterminer la classe de l’objet vert, on va regarder la classe des objets à proximité, selon la valeur de K.

Pour K=1 on va choisir la classe de l’élément le plus proche.

Pour K=n on va choisir la classe la plus représentée dans les éléments sélectionnés.

Méthodologie

Plusieurs étapes sont nécessaires pour utiliser cet algorithme sur un jeu de données :

  1. Normaliser les données pour construire notre modèle
  2. Construire le modèle
  3. Ecrire ou Choisir la méthode de calcul de distance
  4. Choisir notre K
  5. Utiliser le modèle pour faire de la prédiction

Normalisation des données

Cette étape va nous permettre de supprimer tout ce qui n’apporte pas de sens et transformer toutes les données qui composent un cocktail par des entiers. Pour mon modèle j’ai choisi de prendre en compte seulement les informations suivantes :

  • Les ingrédients, en les représentant par leur id en base
  • Le fait de savoir si le cocktail est alcoolisé ou non (0 ou 1)

Afin de limiter le bruit dans le modèle j’ai décidé de supprimer des ingrédients non discriminants comme l’eau, la glace et le sucre. L’idée est de ne pas laisser l’algorithme penser que deux cocktails sont similaires car on y met des glaçons. Une fois que j’ai ma liste d’ingrédients nettoyée, il faut normaliser les données afin de les transformer en tableau d’entier. C’est un prérequis de l’implémentation de php-ml.

Pour le moment, je vais mettre de côté la popularité du cocktail, mais je pourrais la segmenter en 3 groupes peu/ moyen/ très populaire. Pour ce classement, il faudrait simplement ramener le nombre de vues en un nombre de 1 à 3 (via les percentiles 25% et 75% par exemple) et de valoriser cette information au moment du calcul de distance.

Construire le modèle

Pour cette étape, je vais créer une matrice qui aura en nombre de lignes le nombre de cocktails du site et en nombre de colonnes le nombre maximum d’ingrédients plus un d’un cocktail sur le site. Cette étape peut être particulièrement coûteuse en mémoire.
Une ligne de la matrice contient donc en premier index 0 ou 1 selon si le cocktail contient de l’alcool et dans les colonnes suivantes les ids des ingrédients qui composent le cocktail. Les cellules vides de la matrice sont remplies par des 0 car il est nécessaire d’avoir des lignes de la même taille.
Sur l’implémentation proposée, il y a un second tableau qui doit contenir les labels (classes) associées aux différentes lignes. J’ai choisi d’y mettre l’id (casté en string) du cocktail.
Comme cette étape est un peu coûteuse, je serialize la matrice dans un fichier pour pouvoir m’en resservir au besoin.

Une fois la matrice générée nous pouvons entrainer notre modèle (avec la méthode « train » ou « fit » selon vos librairies) sur notre jeu de données.

Choisir notre K

Dans notre cas, chaque élément est porteur de sa propre classe. C’est un cas un peu particulier, mais qui ne pose pas spécialement de problème pour cet algo. Mais au vu du fonctionnement de la valeur de validation croisée, il faut que nous utilisions un K à 1. Une valeur supérieure n’aurait pas de sens, car nous aurions autant d’ex aequo que la valeur de K.

Ecrire la méthode de calcul de distance

Comme notre modèle est assez spécifique et que faire une ACP sur notre matrice n’aurait pas de sens, j’ai pris le parti d’écrire ma propre méthode de calcul de distance.
La méthode de calcul de distance prend en paramètre deux lignes de la matrice et doit retourner un float (la distance). Libre à nous de réfléchir à la meilleure façon de faire ce calcul ou d’utiliser une méthode disponible de base.
Voici mon implémentation :

<?php
namespace App\Services;
use Phpml\Math\Distance;
class CocktailsDistance implements Distance
{
/**
* @param array $a
* @param array $b
*
* @return float
*/
public function distance(array $a, array $b):float
{
// un sans et un avec alcool ?
$distance = array_shift($a) !== array_shift($b)?10:0;
$count = count($a);
for ($i = 0; $i < $count; ++$i) {
// +2 de distance par différence de nombre d'ingrédient
if ($a[$i] === 0 && $b[$i] !== 0) {
$distance += 2;
continue;
}
// +3 si un ingrédient de a n'est pas présent dans le cocktail b
$distance += in_array($a[$i], $b, true) ? 0 : 3;
}
return $distance;
}
}
view raw cocktail distance hosted with ❤ by GitHub

On remarque que j’utilise des nombres magiques dans mon algorithme (2, 3, 5, 10) . Les valeurs me permettent de donner plus ou moins d’importance à un type de différence. Une grande discrimination est donnée pour une différence au niveau de la présence ou non d’alcool.

Exemple sur un jeu de données

[table id=2 /]

Voici la matrice qui est générée pour ce jeu de données avec sur la droite le nom des cocktails associés à chaque ligne:

Dans les colonnes du premier tableau, nous avons dans la première colonne la présence ou non d’alcool et dans les autres les ids des ingrédients qui composent la recette.

Nous allons maintenant utiliser notre modèle pour trouver le cocktail le plus proche du Mojito.

// récupération de mon entité, avec doctrine ici
$mojito = $this->entityManager->getRepository(Cocktail::class)->find(666);
// nous génère la matrice en enlevant le mojito
$matrices = $this->cocktailMachineLearning->generateMatricesWithoutCocktails([$mojito]);
// initialisation de l'algo
$classifier = new KNearestNeighbors(1, new CocktailsDistance());
// entrainement de l'algo sur les données normalisées
$classifier->train($matrices['samples'], $matrices['labels']);
// normalisation du mojito
$normalizeMojito = [
intval($mojito->isAlcohol())
];
foreach($mojito->getIngredients() as $ingredientWithMeasure) {
$normalizeMojito[] = $ingredientWithMeasure->getIngredient()->getId();
}
sort($normalizeMojito);
while (count($normalizeMojito) < $model['maxNbIngredients']) {
$normalizeMojito[] = 0;
}
// prediction de la recette la plus proche
$cocktailLabel = $classifier->predict($normalizeMojito);
dump(cocktailLabel);
view raw prediction hosted with ❤ by GitHub

La sortie en console est la suivante:

C’est bien le résultat que l’on aurait imaginé, mais pour comprendre pourquoi ce résultat, il faut s’intéresser aux distances que l’algo a calculé pour chaque cocktail. Voici ce que l’on obtient :

On remarque que la discrimination sur l’absence d’alcool est forte. C’est une volonté de ma part pour éviter, si possible, de proposer des recettes avec alcool pour un cocktail sans alcool.

Sur le site le volume de données à traiter est beaucoup plus gros évidement mais le principe reste le même. 

Voici le résultat en production sur la recette du mojito:

Pour arriver à ce résultat sur un grand jeu de données, je répète l’algorithme n fois en retirant du modèle les recettes déjà sélectionnées.

Automatisation

La génération de la matrice est assez coûteuse, il faut quasiment dumper toute la base de données et travailler dessus. Il n’est donc pas possible de le faire « à la demande ». J’ai pris le parti de faire une commande Symfony qui est exécutée plusieurs fois par jour pour mettre à jour les données normalisées dans un fichier plat. Et pour la partie entraînement et prédiction, le résultat est mis en cache HTTP à l’aide d’un ESI pour 24h. Cela signifie qu’un nouveau cocktail ne remontera dans les suggestions que maximum 24h après son ajout à moins de flusher le cache varnish manuellement. Dans le cadre de Cocktailand cette latence est tout à fait acceptable. 

Afficher des graphiques avec JQPlot

JQPlot est un plugin JQuery qui permet de dessiner des graphiques dans vos pages HTML. Si vous faites des pages de statistiques sur vos pages Internet vous comprendrez rapidement quelles sont les applications et surtout en quoi il vous simplifie les choses. En effet, il vous suffit de lui donner les données et de décider quel type de graphique vous voulez obtenir en sortie et le tour est joué ! On peut facilement imager une fonction PHP (ou autre) qui va générer les données qu’il faut mettre en entrée de JQPlot à partir de votre base de données.

Voici quelques exemples de graphiques que l’on peut obtenir grâce à ce plugin:

Sécurité PHP

Les informations de cet article datent

L’actualité étant chargée niveau hack, il est temps de sortir un petit article présentant différentes méthodes de sécurisation d’une application PHP.
Je vais donc brièvement vous présenter différentes librairies ou classes PHP orientées sécurité.

  • PHPIDS
    Un monitoring complet des actions de l’utilisateur avec évaluation de la dangerosité de chaque requête, et de la session. L’outil indispensable pour détecter les recherches de failles.
  • Input filter
    Cette librairie permet de filtrer les tableaux $_GET, $_POST, $_REQUEST pour y detecter le PHP, JavaScript et HTML.
  • Anti csrf
    Permet d’ajouter un token dans ses formulaires afin de ne pas se les faire détourner. Le but étant que chaque formulaire ne soit soumis qu’une seule fois.

L’utilisation des librairies précédentes est un complément mais elles ne constituent pas une sécurité optimale à elles seules.

Voici donc une liste de bonnes pratiques niveau sécurité :

  • Ne jamais insérer des données dans une requête SQL sans un minimum de traitement. ( mysql_real_escape_string ou mieux PDO::prepare() et PDOStatment::bindValue() )
  • Ne jamais faire passer en paramètre une page à inclure
    exemple : include $_GET[« ma_page »]; préférez une solution du type : switch($_GET[« ma_page »]){ case « home » : include « home.php »; break; case « forum » : include « forum.php »; break; default : inclue « error.php » break; }
  • Avoir un fichier index.html dans chaque dossier du projet, contenant <html> <head></head> <body> <h1>Accés interdit</h1> </body> </html>
  • Sécuriser ses session_start contre le vol de session. $ip = !empty( $_SERVER[‘HTTP_X_FORWARDED_FOR’] ) ? $_SERVER[‘HTTP_X_FORWARDED_FOR’] : $_SERVER[‘REMOTE_ADDR’]; $securite = $ip.’_’.$_SERVER[‘HTTP_USER_AGENT’]; if(empty($_SESSION)) { session_start(); $_SESSION[‘securite’] = $securite; } elseif($_SESSION[‘securite’] != $securite) { session_regenerate_id(); $_SESSION = array(); } Voici donc une technique qui permet simplement de tester si une session est bien utilisée par le même navigateur sur la même machine.
  • Crypter les données sensibles de votre base (email,pseudo par exemple ).
    J’ai mis au point cette classe : Crypage.   Elle n’est certes pas extrêmement puissante mais elle permet de se protéger contre une injection SQL. Pensez bien sûr à changer la clef 😛 .
  • Le cryptage des mots de passe est une base de la sécurité. Pour cela utilisez une fonction comme celle-ci : protected function crypteMotPasse($mot){ $salt = « !123 GRAIN DE SEL# »; return sha1(strrev(ucfirst($mot).$salt)); } Ceci est une idée le but est d’éviter que les mots de passe soient facilement lisibles en cas de piratage de notre base de données. Tout en sachant que des rainbow table existent pour inverser un md5 ou un sha1.
    Vous pouvez aussi définir un salt par user. Pour cela, au moment de la création du compte, vous générez une chaîne de caractères le plus aléatoirement possible, que vous stockez dans votre table user. Pour la connexion de l’utilisateur, il faudra dans un premier temps aller chercher le salt dans la base. Puis utilisez la fonction ci-dessus comme suit : protected function crypteMotPasse($mot,$saltuser){ $salt = « !123 SALT FIXE# ».$saltuser; return sha1(strrev(ucfirst($mot).$salt)); } Comme le rappelle Simon dans son commentaire : En effet, l’attaquant, pour obtenir l’ensemble des données de la base doit :
    – avec la fonction crypteMotPasse (salt à l’intérieur de la fonction) : regénérer une rainbow table
    – avec une fonction qui prend un salt différent par utilisateur : regénérer autant de rainbow table que d’utilisateurs
  • Empêcher un accès direct à vos fichiers PHP. Dans votre/vos fichier(s) d’entrée(s) (index.php par exemple) faites un : define(« _BASE_URL »,TRUE); Puis commencez tous vos autres fichiers PHP par : <?php if(!defined(‘_BASE_URL’)) die(« Erreur, la page est indisponible »);
  • Ajoutez la ligne suivante dans votre/vos fichier(s) d’entrée(s) afin d’activer les logs d’erreur PHP. Ceci est très utile pour débugger votre application lors de la phase de développement. Il faut cepandant penser à enlever cette ligne au moment de la mise en production. <?php error_reporting(E_ALL); ?>
  • Ne stockez que le minimum vital dans les cookies (voire rien si possible), réduisez la durée de vie de vos cookies, et cryptez les données qui sont dedans.
  • Pour les utilisateurs de CodeIgniter, activez le stockage de la session en base de données.
    Documentation allez à « Saving Session Data to a Database »

Commencer un petit site Internet avec Instant Blueprint

Instant Blueprint est un service en ligne qui vous permet en quelques clics de mettre en place une architecture de base pour votre nouveau projet de site Internet. En effet, après avoir renseigné quelques informations, il va vous générer automatiquement le squelette de votre site Internet.

Pour utiliser cet outil, il vous faut renseigner :

  • Titre du projet
  • Le doctype désiré
  • Le framework javascript à inclure
  • Comment inclure le framework (local ou distant, compressé ou normal)
  • L’extension du fichier index (html, htm, php, asp)
  • Que mettre dans le dossier include (config.php et/ou fonction.php)
  • Centrer ou non le contenu du site
  • Les principales div de la page

Voici le squelette généré :

A partir de ces quelques informations, Instant Blueprint génère le projet et vous propose de le télécharger. Bien entendu, tout reste à faire, mais la partie rébarbative est déjà faite.

Toutefois, il faudra penser à changer le charset par défaut, histoire de le passer en UTF-8 et ainsi se simplifier la vie niveau encodage.

C’est l’outil idéal pour les petits projets.

Système de pagination sous CodeIgniter

Avec Codigniter vous pouvez mettre en place simplement un système de pagination pour afficher vos informations par paquets. Tout cela sans se prendre la tête et se noyer dans des dizaines de lignes de code.

Nous allons créer la base de donnée de l’article : Installation et Configuration de CodeIgniter .

Dans un premier temps il créer le model. Pour cela nous allons utiliser la classe active record de CodeIgniter.

class model extends CI_Model {

    function __construct()
    {
        // Call the Model constructor
        parent::__construct();
    }
    
     public function getAll($debut=0) {
        return $this->db->select('*')->from('message')->order_by("id", "desc")->limit(30, $debut)->get()->result();
    }

	public function count($where = array()) {
        return (int) $this->db->where($where)->count_all_results('message');
    }


}

Puis nous allons créer notre contrôleur.

defined('BASEPATH') OR exit('No direct script access allowed');

class controleur extends MY_Controller {

function liste($nb=0) {
        $messages = array();
	$this->load->model('model');
        $this->load->library('pagination');
        $config['base_url'] = base_url() . "/index.php/controleur/liste/";
        $config['total_rows'] = $this->model->count();
        $config['per_page'] = '30';
        $config['uri_segment'] = 3;
        $config['num_links'] = 5;
        $config['full_tag_open'] = '<div class="pagination">';
        $config['full_tag_close'] = "</div>";
        $config['cur_tag_open'] = '<span class="current">';
        $config['cur_tag_close'] = '</span>';
        $config['next_tag_open'] = $config['prev_tag_open'] = '<span class="disabled">';
        $config['next_tag_close'] = $config['prev_tag_close'] = "</span>";
        $this->pagination->initialize($config);
        $data['pages'] = $this->pagination->create_links();
        $data['messages'] = $this->model->getAll($nb);
	$this->load->view("liste_message",$data);
    }

}

Dans le fichier de vue :

// A vous de mettre en page vos messages
var_dump($messages);

// pour afficher vos lien de paginations 
echo $pages;

Vous aurez un beau système de pagination. Si jamais vous devez toujours passer des paramètres en plus à votre contrôleur vous devez changer quelques lignes :

function liste($param1,$param2,$nb=0) {
...
$config['base_url'] = base_url() . "/index.php/controlleur/liste/$param1/$param2/"; // ajout des params
...
$config['uri_segment'] = 5; // 3+ le nombre de param
...
}

Installation et Configuration de CodeIgniter

Pré requis

  • Un serveur Apache ( lamp, wamp, easy php, mamp, … )
  • Un SGBD mysql

La Base de données

Dans un premier temps voici un exemple de petite base de données. Par exemple la base de données suivante

CREATE DATABASE `tuto` DEFAULT CHARACTER SET latin1 COLLATE latin1_swedish_ci;
USE `tuto`;

CREATE TABLE IF NOT EXISTS `message` (
  `id` int(8) NOT NULL AUTO_INCREMENT,
  `sujet` varchar(255) NOT NULL,
  `message` text NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=MyISAM DEFAULT CHARSET=latin1 AUTO_INCREMENT=1 ;

Maintenant que la base est prête nous alors configurer CodeIgniter pour qu’il puisse s’y connecter.

Rendez-vous dans le fichier : application/config/database.php

$db['default']['hostname'] = 'localhost';
$db['default']['username'] = 'VOTRE LOGIN MYSQL';
$db['default']['password'] = 'VOTRE MOT DE PASSE MYSQL';
$db['default']['database'] = 'tuto';
$db['default']['dbdriver'] = 'mysql';
$db['default']['dbprefix'] = '';
$db['default']['pconnect'] = TRUE;
$db['default']['db_debug'] = TRUE; // à passer à FALSE une fois le dev fini
$db['default']['cache_on'] = FALSE;
$db['default']['cachedir'] = '';
$db['default']['char_set'] = 'utf8';
$db['default']['dbcollat'] = 'utf8_general_ci';
$db['default']['swap_pre'] = '';
$db['default']['autoinit'] = TRUE;
$db['default']['stricton'] = FALSE;

Maintenant le framework peut en théorie se connecter à votre base.

Les configurations systèmes

Pour un fonctionnement optimal de CodeIgniter il y a plusieurs configurations à faire.
Pour les config suivantes rendez-vous dans le fichier /application/config/config.php. Et là beaucoup de choses à changer.

//ligne 17
$config['base_url']	= 'http://localhost/outweb/';
//ligne 60
$config['url_suffix'] = '.html';
//ligne 72
$config['language']	= 'french'; // si vous changez cette valeur il faudra aller chercher les traduction, si vous avez la flemmes ne faites rien ici
//ligne 227
$config['encryption_key'] = 'UN TRUC BIEN COMPLIQUE AVEC DES 1233 ET des !#?$_-éà@';
//ligne 282
$config['global_xss_filtering'] = TRUE;

D’autres paramètres de sécurité sont disponibles mais je les expliquerai dans un autre post.

Les autoloads

Codeigniter permet l’utilisation de librairies, helpers, et de fichiers de config perso.
On peut charger dans le code directement chacun des fichiers de configuration de  l’application mais certains d’entre eux sont indispensables sur chacunes de nos pages. Autant dire à CodeIgniter de les charger automatiquement.
Voici selon moi le minimum indipensable.

$autoload['libraries'] = array("session");
$autoload['helper'] = array("url","database");

Téléchargement

Dans un premier temps il va falloir télécharger le fameux framework. Le mieux étant de prendre la dernière mouture.
Téléchargement du framework.
Dezipper l’archive et copier le dossier qui est dedans à la racine de votre répertoire www.
Pour plus de simplicité nous allons le renommer outweb.
Supprimer le dossier user_guide qui contient une copie de la documentation en ligne de CodeIgniter.

Configuration

Il y a trois grandes parties à configurer dans CodeIgniter :

Une fois les quelques configs précédentes effectuées votre CodeIgniter est prêt.
Si vous vous rendez à l’adresse http://localhost/outweb/ vous aurez le message de bienvenue de CodeIgniter

Nous allons donc pouvoir commencer notre application.

Comme vous le savez surement CodeIgniter est basé sur un model MVC. Par conséquent il est préfèrable de garder cette approche tout au long du développement.