De PHP à Go: PHP2Go

Enfant avec casque rouge sur un tricycle bleu
Crédit image: Christian ter Maat, via Unsplash

Lorsqu’on est un développeur PHP avancé, passer à Go est souvent plus tentant que passer à Node.JS. Mais dans un premier temps, tant de fonctions de la bibliothèque PHP font défaut. Comment migrer au plus vite ?

Le besoin: migrer de PHP

Démarrer avec Go lorsqu’on vient d’une expérience longue avec PHP ne demande pas seulement de repenser l’organisation entière de ses programmes, mais fait aussi — souvent — réaliser que toutes sortes de choses que l’on tenait pour implicites par habitude dépendaient de fonctions qui ne sont pas identiques en Go, et dont il faut identifier les remplacements au fil des besoins, ralentissant les premiers travaux.

L’outil

Pour faciliter les premiers projets durant la transition, le module github.com/syyongx/php2go, disponible depuis Go 1.10 et comporte plus de 140 fonctions équivalentes aux fonctions PHP du même nom.

Il inclut même une fonction Ternary pour convertir les instructions ternaires ?: que Go a choisi de ne pas inclure. Voyons un mini-exemple:

1
2
3
4
5
6
7
8
9
<?php
// Version PHP
// https://3v4l.org/4ZpV6

$pwd = getcwd();
$racine = strlen($pwd) === 1
  ? "Racine"
  : "Ailleurs";
echo $racine;

Comme le montre l’exemple, il suffit d’importer - et éventuellement d’aliaser le paquet du module, qui ne contient que ce paquet, et d’utiliser les fonctions en modifiant la capitalisation, les identificateurs Go devant commencer par une majuscule pour être exportés par les paquets qui les définissent.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// Version Go avec php2go
// https://play.golang.org/p/sOdreEMLGKx

package main

import (
	"fmt"

	. "github.com/syyongx/php2go"
)

func main() {
	// Renvoie un code d'erreur, absent en PHP
	pwd, _ := Getcwd()

	// Arguments et résultat sont des interface{}...
	racine := Ternary(Strlen(pwd) == 1,
		"Racine",
		"Ailleurs",
	)

	// ...donc il faut ajouter une assertion de type.
	println(racine)          // (0x134fa0,0x19af18)
	println(racine.(string)) // Racine

	// Ou utiliser une fonction qui la fait en interne.
	fmt.Println(racine)      // Racine
}

Limitations

Par la force des différences, certaines limitations sont à prendre en compte, mises en évidence même par ce petit exemple:

  • La signature peut être un peu différente: dans cet exemple, Getcwd() respecte les particularités de son implémentation sous-jacente en Go os.Getwd() en renvoyant un code d’erreur, au lieu d’avoir la même signature qu’en PHP, de sorte qu’il faut ignorer explicitement l’erreur (ou, mieux, la traiter !).
  • Du fait du passage par des types interface{} pour simuler le typage dynamique de PHP, le résultat de certaines fonctions comme Ternary n’est pas forcément prêt à l’emploi, nécessitant une assertion v.(<type>) commen dans l’exemple; ou un commutateur de type switch v.(type) pour des situations où le type du résultat n’est pas connu à l’avance.

En outre, le résultat est un code un peu plus verbeux et non idiomatique, qui ne sera pas familier à la majorité des développeurs Go. À titre de comparaison, le listing en Go natif pour le même minuscule exemple n’est pas vraiment moins simple, et sera familier à tout gopher.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
// Version Go native
// https://play.golang.org/p/w3DcSD5RXNw

package main

import "os"

func main() {
	pwd, _ := os.Getwd()

	var racine string
	if len(pwd) == 1 {
		racine = "Racine"
	} else {
		racine = "Ailleurs"
	}

	println(racine) // Racine
}

Utilisation pratique

D’autres fonctions sont plus utiles, comme la panoplie des fonctions liées aux tableaux PHP, qui n’ont pas d’équivalent natif en Go et sont souvent la partie la plus difficile à convertir lors du passage de PHP à Go.

Le paquet inclut nombre d’entre elles, appliquées à des tranches []interface{}:

PHP 7.4php2go v0.9.4
array_change_case
array_chunkArrayChunk
array_columnArrayColumn
array_combineArrayCombine
array_count_valuesArrayFill
array_diff_assoc
array_diff_key
array_diff_uassoc
array_diff_ukey
array_diff
array_fill_keys
array_fill
array_filter
array_flipArrayFlip
array_intersect_assoc
array_intersect_key
array_intersect_uassoc
array_intersect_ukey
array_intersect
array_key_existsArrayKeyExists
array_key_first
array_key_last
array_keysArrayKeys
array_map
array_merge_recursive
array_mergeArrayMerge
array_multisort
array_padArrayPad
array_popArrayPop
array_product
array_pushArrayPush
array_randArrayRand
array_reduce
array_replace_recursive
array_replace
array_reverseArrayReverse
array_search
array_shiftArrayShift
array_sliceArraySlice
array_splice
array_sumArrayUnshift
array_udiff_assoc
array_udiff_uassoc
array_udiff
array_uintersect_assoc
array_uintersect_uassoc
array_uintersect
array_unique
array_unshift
array_valuesArrayValues
array_walk_recursive
array_walk

Conclusion

Ce module est un moyen utile pour parvenir à ses fins durant les premiers temps de l’apprentissage de Go, facilitant la migration aux yeux des gestionnaires en permettant de livrer en un temps record des versions converties.

Pour la plupart des besoins, toutefois, il illustre bien la difficulté qu’il y a à migrer d’un langage sans adapter aussi sa façon de travailler:

  • PHP est pensé autour du typage dynamique et de ses tableaux à tout faire, qui sont à la fois des tableaux ordinaires et associatifs, et préservent l’ordre d’insertion, sans réel équivalent en Go.
  • Go est fortement typé, et convertir un projet de PHP en Go nécessite en général de repenser la conception des données plutôt que de passer par une bibliothèque générique comme celle-ci.

Et un jour, il faut bien changer ses habitudes: autant le faire rapidement.