PHP typed client DTOs for .NET APIs

PHP typed client DTOs for .NET APIs Background
7 min read

We're happy to announce the 11th Add ServiceStack Reference language to enjoy end-to-end typed support for calling .NET APIs - PHP!

The Add ServiceStack Reference feature enables a simple way for PHP clients and Applications to generate native PHP DTO classes to access to your ServiceStack APIs.

End-to-end typed PHP

Learn about the rich JsonServiceClient & end-to-end typed API support for PHP

PhpStorm ServiceStack Plugin

PHP developers of PhpStorm can get a simplified development experience for consuming ServiceStack Services by installing the ServiceStack Plugin from the JetBrains Marketplace:

Where you'll be able to right-click on a directory and click on ServiceStack Reference on the context menu:

To launch the Add PHP ServiceStack Reference dialog where you can enter the remote URL of the ServiceStack endpoint you wish to call to generate the Typed PHP DTOs for all APIs which by default will saved to dtos.php:

Then just import the DTOs and JsonServiceClient to be able to consume any of the remote ServiceStack APIs:

<?php

require_once __DIR__ . '/vendor/autoload.php'; // Autoload files using Composer autoload
require_once 'dtos.php';

use dtos\FindTechnologies;
use Servicestack\JsonServiceClient;

$client = JsonServiceClient::create("https://techstacks.io");

$response = $client->send(new FindTechnologies(
    ids: [1,2,4,6],
    vendorName: "Google"));

print_r($response);

If any of the the remote APIs change their DTOs can be updated by right-clicking on dtos.php and clicking Update ServiceStack Reference:

Install PHP ServiceStack Client

The only requirements for PHP apps to perform typed API Requests are the generated PHP DTOs and the generic JsonServiceClient which can be installed in Composer projects with:

$ composer require servicestack/client

Or by adding the package to your composer.json then installing the dependencies:

{
  "require": {
    "servicestack/client": "^1.0"
  }
}

First class development experience

PHP is one of the worlds most popular programming languages thanks to its ease of use, platform independence, large standard library, flexibility and fast development experience which sees it excels as a popular language for web development and for development of popular CMS products like WordPress, Drupal and Joomla thanks to its flexibility, embeddability and ease of customization.

To maximize the experience for calling ServiceStack APIs within these environments ServiceStack now supports PHP as a 1st class Add ServiceStack Reference supported language which gives PHP developers an end-to-end typed API for consuming ServiceStack APIs, complete with IDE integration in PhpStorm as well as built-in support in x dotnet tool to generate Typed and annotated PHP DTOs for a remote ServiceStack instance from a single command-line.

Ideal idiomatic Typed Message-based API

To maximize the utility of PHP DTOs and enable richer tooling support and greater development experience, PHP DTOs are generated as Typed JsonSerializable classes with promoted constructors and annotated with PHPDoc Types - that's invaluable when scaling large PHP code-bases and greatly improves discoverability of a remote API. DTOs are also enriched with interface markers and Annotations which enables its optimal end-to-end typed API:

The PHP DTOs and JsonServiceClient library follow PHP naming conventions so they'll naturally fit into existing PHP code bases. Here's a sample of techstacks.io generated PHP DTOs containing string and int Enums, an example AutoQuery and a standard Request & Response DTO showcasing the rich typing annotations and naming conventions used:

enum TechnologyTier : string
{
    case ProgrammingLanguage = 'ProgrammingLanguage';
    case Client = 'Client';
    case Http = 'Http';
    case Server = 'Server';
    case Data = 'Data';
    case SoftwareInfrastructure = 'SoftwareInfrastructure';
    case OperatingSystem = 'OperatingSystem';
    case HardwareInfrastructure = 'HardwareInfrastructure';
    case ThirdPartyServices = 'ThirdPartyServices';
}

enum Frequency : int
{
    case Daily = 1;
    case Weekly = 7;
    case Monthly = 30;
    case Quarterly = 90;
}

// @Route("/technology/search")
#[Returns('QueryResponse')]
/**
 * @template QueryDb of Technology
 * @template QueryDb1 of TechnologyView
 */
class FindTechnologies extends QueryDb implements IReturn, IGet, JsonSerializable
{
    public function __construct(
        /** @var array<int>|null */
        public ?array $ids=null,
        /** @var string|null */
        public ?string $name=null,
        /** @var string|null */
        public ?string $vendorName=null,
        /** @var string|null */
        public ?string $nameContains=null,
        /** @var string|null */
        public ?string $vendorNameContains=null,
        /** @var string|null */
        public ?string $descriptionContains=null
    ) {
    }

    /** @throws Exception */
    public function fromMap($o): void {
        parent::fromMap($o);
        if (isset($o['ids'])) $this->ids = JsonConverters::fromArray('int', $o['ids']);
        if (isset($o['name'])) $this->name = $o['name'];
        if (isset($o['vendorName'])) $this->vendorName = $o['vendorName'];
        if (isset($o['nameContains'])) $this->nameContains = $o['nameContains'];
        if (isset($o['vendorNameContains'])) $this->vendorNameContains = $o['vendorNameContains'];
        if (isset($o['descriptionContains'])) $this->descriptionContains = $o['descriptionContains'];
    }
    
    /** @throws Exception */
    public function jsonSerialize(): mixed
    {
        $o = parent::jsonSerialize();
        if (isset($this->ids)) $o['ids'] = JsonConverters::toArray('int', $this->ids);
        if (isset($this->name)) $o['name'] = $this->name;
        if (isset($this->vendorName)) $o['vendorName'] = $this->vendorName;
        if (isset($this->nameContains)) $o['nameContains'] = $this->nameContains;
        if (isset($this->vendorNameContains)) $o['vendorNameContains'] = $this->vendorNameContains;
        if (isset($this->descriptionContains)) $o['descriptionContains'] = $this->descriptionContains;
        return empty($o) ? new class(){} : $o;
    }
    public function getTypeName(): string { return 'FindTechnologies'; }
    public function getMethod(): string { return 'GET'; }
    public function createResponse(): mixed { return QueryResponse::create(genericArgs:['TechnologyView']); }
}

// @Route("/orgs/{Id}", "DELETE")
class DeleteOrganization implements IReturnVoid, IDelete, JsonSerializable
{
    public function __construct(
        /** @var int */
        public int $id=0
    ) {
    }

    /** @throws Exception */
    public function fromMap($o): void {
        if (isset($o['id'])) $this->id = $o['id'];
    }
    
    /** @throws Exception */
    public function jsonSerialize(): mixed
    {
        $o = [];
        if (isset($this->id)) $o['id'] = $this->id;
        return empty($o) ? new class(){} : $o;
    }
    public function getTypeName(): string { return 'DeleteOrganization'; }
    public function getMethod(): string { return 'DELETE'; }
    public function createResponse(): void {}
}

The smart PHP JsonServiceClient available in the servicestack/client packagist package enables the same productive, typed API development experience available in our other 1st-class supported client platforms.

Using promoted constructors enables DTOs to be populated using a single constructor expression utilizing named parameters which together with the generic JsonServiceClient enables end-to-end typed API Requests in a single LOC:

use Servicestack\JsonServiceClient;
use dtos\Hello;

$client = new JsonServiceClient("https://test.servicestack.net");

/** @var HelloResponse $response */
$response = client->get(new Hello(name:"World"));

The HelloResponse optional type hint doesn't change runtime behavior but enables static analysis tools and IDEs like PyCharm to provide rich intelli-sense and development time feedback.

For more usage examples and information about ServiceStack's PHP support checkout the PHP Add ServiceStack Reference docs.