You will find the 🇬🇧 english version of this article here
Aujourd’hui j’ai envie de vous parler d’un outil que j’utilise désormais presque tous les jours : Protocol Buffers (ou ProtoBuf pour les intimes).
Contrairement à une idée reçue, il est tout à fait possible (et efficace !) d’utiliser ProtoBuf en PHP
.
ProtoBuf, c’est :
Java
, PHP
, Go
…) pour traiter ces données.Mon cas d’usage est assez basique : je dois faire transiter de l’information entre plusieurs microservices, via un bus RabbitMQ
.
Je me sert donc de ProtoBuf pour ça.
Nous allons échanger de la donnée entre une application PHP et une application Go 🎉 . Voyons comment ça marche !
Si vous avez regardé le site officiel, vous voyez le mot “Google” un peu partout. Pas de panique, ça reste très intéropérable. Le couplage à Google est assez inexistant, et la technologie est utilisée par beaucoup d’acteurs différents. Google est surtout à l’initiative du projet.
L’idée derrière tout ça est de décrire une donnée via des fichiers .proto
, standardisés et agnostiques. À partir de ces fichiers,
toute donnée sera sérialisée et désérialisée, en binaire ou en JSON.
Pour un exemple basique, nous allons décrire un message simple, de type billet de blog :
# fichier src/BlogPost.proto
syntax = "proto3";
message BlogPost {
string uuid = 1;
string title = 2;
string content = 3;
}
C’est un message simple, qui contient un titre et un contenu. Chaque attribut est associé à une position (1, 2, 3, …), qui ne doit jamais changer dans le temps. C’est sur elle que s’appuie la sérialisation et déserialisation.
Continuons avec notre BlogPost
, afin de lui ajouter des tags et un auteur (de manière assez simpliste, mais l’idée est là) :
# fichier src/User.proto
syntax = "proto3";
message User {
string uuid = 1;
string name = 2;
optional string avatar = 3;
}
# fichier src/Tag.proto
syntax = "proto3";
message Tag {
string label = 1;
}
Modifions le BlogPost
pour relier le tout. Le fichier ressemble désormais à :
syntax = "proto3";
import "src/User.proto";
import "src/Tag.proto";
message BlogPost {
string uuid = 1;
string title = 2;
string content = 3;
User author = 4;
repeated Tag tags = 5;
}
Nous pouvons avoir autant de tags que nous le souhaitons, via l’instruction repeated
.
Nous allons enfin ajouter une date de publication à notre BlogPost. Pour cela, nous allons devoir importer le
type timestamp
, qui est natif, mais à importer si vous souhaitez l’utiliser. Il existe pas mal de types, je vous
laisse les découvrir dans la documentation.
# ...
import "google/protobuf/timestamp.proto";
message BlogPost {
# ...
google.protobuf.Timestamp published = 6;
}
Pour aller jusqu’au bout et découvrir un dernier aspect assez utile, sachez qu’il est possible également d’utiliser des enums :
# ...
message BlogPost {
# ...
enum PublicationStatus {
DRAFT = 0;
PUBLISHED = 1;
ARCHIVED = 2;
}
PublicationStatus status = 7;
}
Si vous avez envie de tester, et pas le courage de tout copier-coller, voici le code complet pour le BlogPost
:
syntax = "proto3";
import "src/User.proto";
import "src/Tag.proto";
import "google/protobuf/timestamp.proto";
# Il reste une étape ici pour les namespaces
message BlogPost {
string uuid = 1;
string title = 2;
string content = 3;
User author = 4;
repeated Tag tags = 5;
google.protobuf.Timestamp published = 6;
enum PublicationStatus {
DRAFT = 0;
PUBLISHED = 1;
ARCHIVED = 2;
}
PublicationStatus status = 7;
}
Il est possible de définir des Namespaces pour les classes générées par ProtoBuf. C’est même requis pour certains langages (comme le Go).
Nous allons ajouter des metadonnées à chacun de nos fichiers .proto
, en y ajoutant :
option go_package = "blog/demo";
option php_namespace = "Blog\\Demo";
option php_metadata_namespace = "Blog\\Demo\\Metadata";
💡 Astuce
Au-fur-et-à-mesure de la vie du projet, vous allez faire évoluer vos messages. Si vous craignez de briser la rétrocompatibilité (par exemple en rendant obsolète un attribut), une bonne pratique consiste à utiliser un attribut de version.
message ... { optional int32 version = 999; }
Stockez-y la version actuelle de votre donnée, vous pourrez alors gérer cette dernière en fonction de sa version sans tout casser.
On a décrit tout plein de belles choses, c’est bien. Mais les utiliser c’est mieux ! Il est temps d’installer ProtoBuf.
Téléchargez simplement la dernière release sur le
dépôt Github officiel (cherchez le fichier protoc-xxx
qui correspond
à votre distribution).
Par exemple dans mon cas, je télécharge la version 21.2 pour Ubuntu :
curl https://github.com/protocolbuffers/protobuf/releases/download/v21.2/protoc-21.2-linux-x86_64.zip \
-o protoc-21.2-linux-x86_64.zip
unzip -qq protoc-21.2-linux-x86_64.zip -d protoc
chmod +x protoc/bin/protoc
J’ai désormais un dossier protoc
dans mon dossier courant, avec le binaire bin/protoc
qui nous servira pour
tout le reste.
Nous allons maintenant faire quelque chose d’assez magique : nous allons générer du code PHP pour sérialiser et désérialiser
des BlogPost
.
Toujours en bash, lancez :
mkdir -p generated # le dossier "generated" va accueillir le code généré
protoc/bin/protoc --php_out=./generated --proto_path=src $(find src -name '*.proto')
Vous trouverez dans le dossier generated
un ensemble de classes PHP prêtes à l’emploi.
Créons un petit script pour les utiliser. La première étape sera d’installer ProtoBuf pour PHP:
composer require google/protobuf
Puis créons un script qui va générer le code PHP :
<?php
require_once __DIR__ . '/vendor/autoload.php';
$blogSpot = new \Blog\Demo\BlogPost();
$blogSpot
->setTitle('Mon super billet')
->setContent('Lorem ipsum')
->setAuthor(
(new \Blog\Demo\User())
->setName('Jean-François')
)
->setPublished(new Google\Protobuf\Timestamp());
// le contenu sérialisé en JSON:
$json = $blogSpot->serializeToJsonString();
// le contenu sérialisé en binaire
$binary = $blogSpot->serializeToString();
// Nous déposons le binaire dans un fichier temporaire, qui sera lu par Go
file_put_contents('blogpost.bin', $json);
le JSON ressemble à :
{"title":"Mon super billet","content":"Lorem ipsum","author":{"name":"Jean-François"},"published":"1970-01-01T00:00:00Z"}
Et voilà ! Nous allons maintenant désérialiser notre billet, mais en Go cette fois.
Installons les dépendances :
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
go mod init Version1
go get github.com/golang/protobuf
go get github.com/golang/protobuf/proto
Utilisons ProtoBuf pour générer le code Go automatiquement :
protoc/bin/protoc --go_out=vendor $(find src -name '*.proto')
Voici un code très simple pour lire et parser le fichier blogspot.bin
que nous avons généré en PHP:
package main
import "io/ioutil"
import "github.com/golang/protobuf/proto"
import "blog/demo"
import "log"
func main() {
in, err := ioutil.ReadFile("blogpost.bin")
if err != nil {
log.Fatalf("Read File Error: %s ", err.Error())
}
blogpost := &demo.BlogPost{}
err2 := proto.Unmarshal(in, blogpost)
if err2 != nil {
log.Fatalf("DeSerialization error: %s", err.Error())
}
log.Printf("BlogPost: %s", blogpost.Title)
log.Printf("Author is: %s", blogpost.Author.Name)
}
Lançons le :
go run demo.go
# BlogPost: Mon super billet
# Author is: Jean-François
Notre programme en Go a bien lu le fichier serialisé par PHP, et a pu en extraire les informations, sans aucun problème.
Et voilà, nous avons fait passer de la donnée, structurée, de PHP vers Go. Dans les deux cas nous avons pu utiliser des objets ou des structures typées. Si la donnée est déserialisée, c’est qu’elle est valide !
Les avantages de ProtoBuf sont vraiment nombreux :
Avec toutefois, de mon expérience, une réserve : la documentation mériterait d’être largement simplifiée, pour la rendre plus abordable pour les débutants.
En espérant vous avoir donné envie de tester cet outil, n’hésitez pas à faire part de votre expérience sur le sujet.
💡 Pour aller plus loin
vous pouvez également découvrir un cas d’usage réel de production de ProtoBuf, dans le cadre d’un bus de données RabbitMQ
© Jean-François Lépine, 2013 - 2024