I have a backend service written in PHP and a client mobile app. I want to share the code of a model class that describes the payload of a push notification. How can this be done? Can I avoid code duplication?

As you may already know, there’s no direct way to share code implmenetation bewteen a backend and a client app if the two are written in different languages. However, there are strategies you can employ to minimize code duplication.

The naive approach

One such approach is to create a shared understanding of the data being passed between the two systems by using a common data format. Let’s make an example using a class PushMessagePayload:

class PushMessagePayload {
    private $title;
    private $message;
 
    public function __construct($title, $message) {
        $this->title = $title;
        $this->message = $message;
    }
 
    public function getTitle() {
        return $this->title;
    }
 
    public function getMessage() {
        return $this->title;
    }
}

we can serialize the payload to JSON (or another common data format) when sending from the server:

$pushMessage = new PushMessagePayloadImpl("payload data...");
echo json_encode($pushMessage->getPayload());

Suppose that the client app is written in dart/Flutter. We create a corresponding class that can decode from this JSON data:

class PushMessagePayload {
  final String payload;
 
  PushMessagePayload({required this.payload});
 
  factory PushMessagePayload.fromJson(Map<String, dynamic> json) {
    return PushMessagePayload(
      payload: json['payload'],
    );
  }
}

When you receive the payload in Flutter, you can decode it using the factory method you defined:

PushMessagePayload payload = PushMessagePayload.fromJson(jsonDecode(jsonString));

This strategy allows you to maintain the structure and functionality of the PushMessagePayload in both systems without directly sharing code between them.

Automate model generation using Quicktype

For more complex objects, you may want to look into automatically generating data classes from a common schema using tools like Quicktype. You can create a json schema once, then generate data classes in multiple languages. Quicktype is available as a web app or command line tool.

Automate model generation using Protobuf

A more comprehensive solution for model generation is protobuf. Protocol Buffers is a very good way to share data structures and RPC interfaces across multiple languages. It is a language-agnostic, platform-neutral, extensible mechanism for serializing structured data.

Protocol Buffers are defined in .proto files which can be compiled by the Protocol Buffer Compiler (protoc) into source code usable by multiple different languages.

Here’s an example of how you might define your PushMessagePayload using protobuf:

syntax = "proto3";

message PushMessagePayload {
    string message = 1;
    int32 itemId = 2;
}

Then, you would use the protoc compiler to generate the data classes in your target languages.

In PHP:

protoc --php_out=. payload.proto

In Dart:

protoc --dart_out=. payload.proto

In PHP, the generated class would look something like this:

namespace GPBMetadata;
 
class PushMessagePayload
{
    private $message = null;
    private $itemId = null;
 
    public function getMessage()
    {
        return $this->message;
    }
 
    public function setMessage($value)
    {
        GPBUtil::checkString($value, True);
        $this->message = $value;
 
        return $this;
    }
 
    public function getItemId()
    {
        return $this->itemId;
    }
 
    public function setItemId($value)
    {
        GPBUtil::checkInt32($value, True);
        $this->itemId = $value;
        return $this;
    }
}
 

the generated classes come with built-in methods for serializing to and from JSON. You can use the serializeToJsonString() method to convert your message to a JSON string.

Using Protocol Buffers would allow you to avoid manually redefining your data structure in each language, and would give you a robust, efficient method for serializing and deserializing your data. The Protocol Buffers library also includes methods for performing RPC, if you want to share not just data structures but also interfaces between your PHP server and Dart client. However, keep in mind that Protocol Buffers might be overkill if your use case is relatively simple and you’re only trying to share a few data structures.

Install protobuf dependencies and compiler

To add the protobuf runtime library to your PHP project, you can use Composer:

composer require google/protobuf

Composer will automatically handle the installation and will create or modify a composer.json file in your project directory, which keeps track of the project’s dependencies.

The Protobuf PHP runtime library provides Protobuf support to your PHP application, but it doesn’t include the protoc compiler that you’ll need to generate PHP classes from your .proto files. The protoc compiler is part of the protobuf project and can be downloaded from the protobuf GitHub releases page.

You can use composer’s scripts feature to add custom commands that can be run using composer run-script <command>. Here’s how you could add a script to run the protoc command:

  1. Install protoc on your system. This step is platform-dependent and typically involves downloading the appropriate binary or package for your operating system from the protobuf GitHub releases page. Ensure that protoc is accessible from your system’s PATH.

  2. Add a scripts section to your composer.json file that looks something like this:

"scripts": {
  "generate-protos": "protoc --php_out=src/ --proto_path=protos/ protos/*.proto"
}

This script will run the protoc command to generate PHP classes from your .proto files. You should replace src/ with the path to the directory where you want to generate your PHP classes, and protos/ with the path to the directory where your .proto files are located.

  1. Now you can generate your protobuf PHP classes by running the following command:
composer run-script generate-protos

This will execute the protoc command that you specified in your composer.json file, generating your PHP classes.

In Dart you’ll need to install protobuf compiler plugin:

dart pub global activate protoc_plugin

Using a docker image

If you’re using a docker image to build your project, you may need to install the latest version of protobuf compiler. Here’s an example of Dockerfile:

# Use an official Ubuntu runtime as a parent image
FROM ubuntu:latest
 
# Install unzip and curl
RUN apt-get update && \
    apt-get install -y unzip curl && \
    rm -rf /var/lib/apt/lists/*
 
# Set working directory
WORKDIR /tmp
 
# Download protoc using curl
RUN curl -LO https://github.com/protocolbuffers/protobuf/releases/download/v23.4/protoc-23.4-linux-x86_64.zip
 
# Unzip the file and install
RUN unzip protoc-23.4-linux-x86_64.zip && \
    mv bin/protoc /usr/local/bin/ && \
    mv include/* /usr/local/include/
 
# Check the installed version of protoc
RUN protoc --version

And what about namespaces?

You can set the namespace for the generated PHP files from your Protocol Buffers (protobuf) .proto files by using the php_namespace option in the .proto file itself.

Here’s an example of what your .proto file might look like:

syntax = "proto3";
 
package mypackage;
 
option php_namespace = "MyNamespace\\SubNamespace";
 
message MyMessage {
  string my_field = 1;
}

In this example, the php_namespace option is set to MyNamespace\SubNamespace. When you compile this .proto file with the protoc command, the generated PHP class for the MyMessage message will be in the MyNamespace\SubNamespace namespace.

Remember that backslashes in strings need to be escaped, so you have to write \\ in your .proto file to get a single backslash in the resulting PHP namespace.

Also, note that the php_namespace option is a file-level option, so it will apply to all messages in the .proto file. If you want to use different namespaces for different messages, you have to define them in different .proto files.