diff --git a/docs/sdks/languages/php/oss-comparison-php.mdx b/docs/sdks/languages/php/oss-comparison-php.mdx index cf1c5e79..e0bac20d 100644 --- a/docs/sdks/languages/php/oss-comparison-php.mdx +++ b/docs/sdks/languages/php/oss-comparison-php.mdx @@ -1,8 +1,12 @@ +{/* TODO +- OG does not handle any polymorphism whatsoever, no allof or anyOf +*/} + import { Table } from "@/mdx/components"; -# OpenAPI PHP SDK creation: Speakeasy vs open source +# Comparing OpenAPI PHP SDK Generators -Many of our users have switched from [OpenAPI Generator](https://openapi-generator.tech/) to Speakeasy for their PHP SDKs. Learn how to use both SDK creators in this guide, and the differences between them. +The biggest generator of SDKs (a.k.a API clients) in PHP is the multilingual open-source generator [OpenAPI Generator](https://openapi-generator.tech/), but so many of our users have switched to Speakeasy for their PHP SDKs it seemed helpful to compare the offerings and help others make an informed decision. Open-source OpenAPI generators are great for experimentation but lack the reliability, performance, and intuitive developer experience required for critical applications. As an alternative, Speakeasy creates [idiomatic SDKs](/post/client-sdks-as-a-service) that meet the bar for enterprise use. @@ -15,21 +19,18 @@ Here's the high-level summary of the differences between Speakeasy and OpenAPI G { key: "openapi", header: "OpenAPI Generator" } ]} data={[ - { feature: "OpenAPI 3.0 support", speakeasy: "✅", openapi: "✅" }, - { feature: "OpenAPI 3.1 support", speakeasy: "✅", openapi: "❌" }, + { feature: "OpenAPI 3.2 support", speakeasy: "✅", openapi: "❌" }, + { feature: "OpenAPI 3.1 support", speakeasy: "✅", openapi: "⚠️ Beta" }, + { feature: "PHP version support", speakeasy: "PHP >=8.2", openapi: "PHP >=8.1" }, { feature: "Laravel integration", speakeasy: "✅", openapi: "❌" }, - { feature: "Code readability", speakeasy: "Concise, human-readable code", openapi: "Verbose, messy code" }, - { feature: "Files generated", speakeasy: "84 granular separation", openapi: "16 less separation" }, - { feature: "Code generated", speakeasy: "3,915 lines", openapi: "6,316 lines" }, - { feature: "PHP version support", speakeasy: "PHP 8.1+", openapi: "PHP 7.4+" }, { feature: "Type safety", speakeasy: "✅", openapi: "❌" }, { feature: "Runtime type checking", speakeasy: "✅ JMS Serializer", openapi: "❌" }, + { feature: "Async support", speakeasy: "✅", openapi: "✅" }, + { feature: "OAuth 2.0 support", speakeasy: "⚠️ Implict grant only", openapi: "⚠️ Implict grant only" }, { feature: "Serialization", speakeasy: "✅ JMS Serializer", openapi: "✅ PHP extensions" }, - { feature: "Enum support", speakeasy: "✅", openapi: "⚠️ Uses constant strings and functions" }, - { feature: "OAuth 2.0 support", speakeasy: "⚠️ Coming soon", openapi: "❌" }, - { feature: "Content type support", speakeasy: "JSON and form", openapi: "JSON, form, and XML" }, - { feature: "Async support", speakeasy: "❌", openapi: "✅" }, + { feature: "Enum support", speakeasy: "✅", openapi: "⚠️ Uses getter/setter workarounds" }, { feature: "Union type handling", speakeasy: "✅", openapi: "⚠️ Creates custom implementation" }, + { feature: "Content type support", speakeasy: "JSON and Form", openapi: "JSON, Form, and XML" }, { feature: "Documentation", speakeasy: "✅", openapi: "⚠️ Examples may lack required fields" }, { feature: "CI/CD integration", speakeasy: "✅", openapi: "❌" } ]} @@ -37,87 +38,126 @@ Here's the high-level summary of the differences between Speakeasy and OpenAPI G In this post, we'll do a technical deep dive on creating PHP SDKs using both Speakeasy and OpenAPI Generator, then we'll compare the generated SDKs. -## What is OpenAPI Generator? +## History of PHP OpenAPI generators + +**OpenAPI Generator** is a community-run open-source tool for generating SDKs from OpenAPI documents. The project is not connected to the OpenAPI Initiative at all, which has been the source of much confusion. The origin of this project was as a [fork of Swagger Codegen](https://openapi-generator.tech/docs/fork-qna). [Swagger CodeGen](https://swagger.io/tools/swagger-codegen) is a similar tool maintained by SmartBear. + +Both OpenAPI Generator and Swagger Codegen have a history of slow updates and lack of support for the latest OpenAPI versions, with Swagger CodeGen [showing no sign of supporting OpenAPI v3.1](https://github.com/swagger-api/swagger-codegen/issues/12560), and OpenAPI Generator still only offering beta support for OpenAPI v3.1. Seeing as OpenAPI v3.2 has been out for months at time of writing, still having incomplete or entirely missing support for v3.1 shows a worrying lack of progress. + +PHP version support has also been slow. Managing which PHP versions to support is difficult, but maintaining compatibility with new PHP versions is critical and something these projects have struggled with. The last mention of PHP in the Swagger Generator roadmap was "adding compatibility with PHP 8.1" in late 2022, which is rather concerning given we're on PHP 8.5 at time of writing. + +OpenAPI Generator has been able to able to maintain compatibility with newer PHP versions (does not throw errors using it on the latest versions), but the minimum version is PHP 8.1 which lost security support end of 2025. There is always a tradeoff with supporting older versions to help users stuck on older versions, but the project has also been slow to take advantage of newer PHP 8.x features to simplify generated code and improve user experience in an increasingly type safe world. -**OpenAPI Generator** (not to be confused with a generic **OpenAPI generator**) is a community-run, open-source tool for generating SDKs from OpenAPI specifications, with a [focus on version 3](https://openapi-generator.tech/docs/fork-qna). OpenAPI Generator originated as a fork of [Swagger Codegen](https://swagger.io/tools/swagger-codegen), a similar tool maintained by Smartbear. +[JanePHP](https://github.com/janephp/janephp) appeared on the scene as a dedicated PHP SDK generator with more focus on modern PHP features. Sadly it too [lacks support for OpenAPI v3.1](https://github.com/janephp/janephp/issues/759 ) and above, and [does not support enums](https://github.com/janephp/janephp/issues/615) natively. + +This comparison will only focus on tools with OpenAPI v3.1 support, so JanePHP will not be included and neither will Swagger CodeGen. ## Preparing the SDK generators -For our comparison, we ran Speakeasy and OpenAPI Generator in separate Docker containers, which work on Windows, macOS, and Linux. Using Docker instead of running code directly on your physical machine is safer, as the code cannot access files outside the folder you specify. +OpenAPI Generator requires Java to run, so some folks prefer to wrap that in a Docker image to keep their workstation clean. Thankfully there's a few [alternative installs](https://openapi-generator.tech/docs/installation/) for OpenAPI Generator, including a [Homebrew package](https://openapi-generator.tech/docs/installation/#homebrew) for macOS users, [Scoop package](https://openapi-generator.tech/docs/installation/#scoop) for Windows users, and [an NPM wrapper](https://openapi-generator.tech/docs/installation/#npm). + +```sh +# macOS +brew install openapi-generator + +# Windows +scoop install scoop install openapi-generator-cli -We used the PetStore 3.1 YAML schema file from the [Swagger editor](https://editor-next.swagger.io) examples menu. +# NPM +npm install @openapitools/openapi-generator-cli -g +``` -To follow along with this guide, locate the PetStore file in **File -> Load Example -> OpenAPI 3.1 Petstore** and save it to a subfolder called `app` in your current path, such as `app/schema.yaml`. +To install the Speakeasy CLI follow the steps in the [Speakeasy Getting Started guide](/docs/speakeasy-reference/cli/getting-started). Again there are multiple installation options, with [Homebrew for macOS](https://www.speakeasy.com/docs/speakeasy-reference/cli/getting-started#homebrew-macos) or [Chocolatey](https://www.speakeasy.com/docs/speakeasy-reference/cli/getting-started#chocolatey-windows) for Windows users. -OpenAPI Generator provides a Docker image, but Speakeasy does not. To install the Speakeasy CLI, you can either follow the steps in the [Speakeasy Getting Started guide](/docs/speakeasy-reference/cli/getting-started) to install the Go binary directly on your computer, or run it in Docker, as we did. +```sh +# macOS +brew install speakeasy-api/tap/speakeasy -To use Docker, first create a `Dockerfile` with the content below, replacing `YourApiKey` with your key from the Speakeasy website. +# Windows +choco install speakeasy -```bash -FROM alpine:3.19 -WORKDIR /app -RUN apk add bash go curl unzip sudo nodejs npm -RUN curl -fsSL https://go.speakeasy.com/cli-install.sh | sh; -ENV GOPATH=/root/go -ENV PATH=$PATH:$GOPATH/bin -ENV SPEAKEASY_API_KEY=YourApiKey +# Other platforms +curl -fsSL https://go.speakeasy.com/cli-install.sh | sh ``` -Then build the Speakeasy image with the command below. +Speakeasy has a user interface component as it is a full SaaS platform, but account creation will happen as part of the quickstart process later. + +Finally to work with these SDK generators we'll need an OpenAPI document. We'll use the [Train Travel API](https://raw.githubusercontent.com/bump-sh-examples/train-travel-api/main/openapi.yaml) from our friends at Bump.sh for demonstration purposes. Save the file as `openapi.yaml` in your working directory. ```sh -docker build -t seimage . +wget https://raw.githubusercontent.com/bump-sh-examples/train-travel-api/main/openapi.yaml -O openapi.yaml ``` -## Validating the schemas +## Validating OpenAPI + +Before creating an SDK its a good idea to validate the OpenAPI documents. There are plenty of tools available to do that, but it's best to use the built-in validation functionality of the SDK generator to make sure it's not only valid, but will work well in that tool. + +OpenAPI Generator and Speakeasy CLI can validate OpenAPI documents, so let's run the two and see how they compare. + +Seeing as the Train Travel API is a known valid document there won't be any errors or warnings to see, but we can quickly borrow a known broken file to see the output. + +``` +wget https://github.com/Mermade/openapi3-examples/raw/refs/heads/master/3.0/fail/deprecated.yaml -O openapi.bad.yaml +``` -Both OpenAPI Generator and the Speakeasy CLI can validate an OpenAPI schema. We'll run both and compare the output. +To validate using OpenAPI Generator, run the following in the terminal: -### Validation using OpenAPI Generator +```sh +openapi-generator-cli validate -i openapi.bad.yaml +``` -To validate `schema.yaml` using OpenAPI Generator, run the following in the terminal: +Surprisingly OpenAPI Generator seems to think this file is ok, despite it existing purely to be used as an example of an invalid OpenAPI document. -```bash -docker run --rm -v "./app:/local" openapitools/openapi-generator-cli validate -i /local/schema.yaml +``` +Validating spec (https://github.com/Mermade/openapi3-examples/raw/refs/heads/master/3.0/fail/deprecated.yaml) +No validation issues detected. ``` -OpenAPI Generator returns two warnings: +The validation command in Speakeasy is pretty similar, but seeing as Speakeasy's `validate` command can validate multiple formats including [Arazzo](https://www.speakeasy.com/openapi/arazzo), it needs `validate openapi` to specify the format: +```sh +speakeasy validate openapi -s openapi.bad.yaml ``` -Warnings: - - Unused model: Address - - Unused model: Customer -[info] Spec has 2 recommendation(s). +Speakeasy correctly identifies the issues with the file, seeing 1 error, 1 warning, and 3 hints. + +``` +validation error: [line 8] validate-json-schema - OpenAPI document invalid: expected boolean, but got string - https://spec.openapis.org/oas/3.0/schema/2021-09-28#/definitions/Operation/properties/deprecated/type ``` -### Validation using Speakeasy +An error like this is critical to catch before generating an SDK, as it could lead to runtime errors or unexpected behavior in the generated code, so make sure to fix any errors before proceeding. -Validate the schema with Speakeasy by running the following in the terminal: +Warnings can also exist, but usually are not blockers. Many warnings are best practice suggestions, like missing `operationId`, which will make generated method names less readable. -```bash -docker run --rm -v "./app:/app" seimage speakeasy validate openapi -s /app/schema.yaml +``` +validation warn: [line 7] operation-operationId - the `GET` operation does not contain an `operationId` ``` -The Speakeasy validator returns 72 hints about missing examples, seven warnings about missing responses, and three warnings about unused components. Each warning includes a detailed JSON-formatted error with line numbers. +Hint messages are more minor suggestions, like missing descriptions on parameters, missing examples, all things that are technically optional but helpful for SDK users. For example, not defining at least one server URL in the OpenAPI means the SDK will not know where to send requests, unless configured manually. + +``` +• validation hint: [line 1] validate-servers - No servers found in document, either add servers to the document or set a baseServerUrl in the gen.yaml config file +• validation hint: [line 10] missing-examples - Missing example for parameter. Consider adding an example +• validation hint: [line 17] missing-error-response - An error response should be defined for all operations +``` -Since both validators return only warnings and not errors, we can assume both generators will create SDKs without issues. +The warnings and hints in that document are more like opinions than specifically something that's definitely going to be a problem, but it's worrying that OpenAPI Generator didn't catch the error. ## Creating the SDKs -First, we'll create an SDK with OpenAPI Generator, and then we'll create one with Speakeasy. +Right, time to generate some code. Let's create one with OpenAPI Generator and one with Speakeasy, then compare the results. ### Creating an SDK with OpenAPI Generator -OpenAPI Generator includes three different PHP SDK creators (and six server creators). We'll use the stable [PHP creator](https://openapi-generator.tech/docs/generators/php), as the others are in beta testing and have fewer features. +OpenAPI Generator includes three different PHP templates for SDK generation: `php`, `php-dt`, `php-nexgen`. We'll use the stable [`php` generator](https://openapi-generator.tech/docs/generators/php) as `php-dt` is a wrapper around a very niche [DataTransfer](https://github.com/Articus/DataTransfer) package, and `php-nextgen` has been in beta for a long time without making much progress. -To create an SDK from the schema file using OpenAPI Generator, we ran the command below, which we found in the [OpenAPI Generator README](https://github.com/OpenAPITools/openapi-generator#16---docker). +To create an SDK from the OpenAPI document using OpenAPI Generator, run this command: ```sh -docker run --rm -v "./app:/local" openapitools/openapi-generator-cli generate -i /local/schema.yaml -g php -o /local/og +openapi-generator-cli generate -i openapi.yaml -g php -o og ``` -OpenAPI Generator creates three folders: +OpenAPI Generator creates three folders inside the `./og/` output folder: -Speakeasy does not create test stubs, as unit testing is performed on Speakeasy's generator instead of the generated SDK. Shipping unit tests for generated SDKs adds unnecessary complexity and dependencies. - -## Calling the server +Unlike OpenAPI Generator, Speakeasy does not create test stubs. [Testing SDKs](https://www.speakeasy.com/docs/sdks/sdk-contract-testing) is performed on Speakeasy's generator instead of the shipped SDK, so sharing those unit tests for generated SDKs adds unnecessary complexity and dependencies. -Swagger provides a complete test server for the PetStore OpenAPI 3.1 schema at https://petstore31.swagger.io. +## First API request with each SDK -We called the pet operations given in each SDK's README file against the test server to check that the SDKs contain working code. +Both SDKs have been created successfully, so let's try calling the API with each SDK to make sure they work as expected. Each README.md should have instructions on how to call the API, so we'll follow those instructions for each SDK. -We used a [Docker Composer 2.7](https://hub.docker.com/layers/library/composer/2.7/images/sha256-692dd0a0b775cc25ea0cf3ed936b1470647191a6417047e6a77d757a9f29c956?context=explore) container, which is based on Alpine 3 and PHP 8. +### Calling the API server with the OpenAPI Generator SDK -### Calling the server with the OpenAPI Generator SDK +Here's an example of calling the Train Travel API to get a list of stations using the OpenAPI Generator SDK. The example below assumes you have installed the SDK using Composer and have autoloading set up, as well as having Guzzle installed as a dependency. -We used the `app/og/main.php` script below to call the API with the SDK generated by OpenAPI Generator. The example code was mostly given in the `README.md` file. - -```php filename="app/og/main.php" -setAccessToken('test'); -$apiInstance = new OpenAPI\Client\Api\PetApi(new GuzzleHttp\Client(), $config); -$pet = new \OpenAPI\Client\Model\Pet(); // \OpenAPI\Client\Model\Pet | Create a new pet in the store -$pet->setId(1); -$pet->setName("1"); - -try { - $result = $apiInstance->addPet($pet); - print_r($result); -} catch (Exception $e) { - echo 'Exception when calling PetApi->addPet: ', $e->getMessage(), PHP_EOL; -} -``` - -To get access to the folder to create the script, give yourself permissions to the shared Docker volume with the command below, using your username. +use OpenAPI\Client as OGTrainTravelClient; -```sh -sudo chown -R yourUsername ./app -``` +$config = OGTrainTravelClient\Configuration::getDefaultConfiguration() + ->setHost('https://try.microcks.io/rest/Train+Travel+API/1.0.0') // Mock Server URL + ->setAccessToken('some-access-token'); -Next, we ran the command below and received a successful response. +$apiInstance = new OGTrainTravelClient\Api\StationsApi( + new GuzzleHttp\Client(), + $config +); -```sh -docker run --rm -v "./app/og:/app" -w "/app" composer:2.7 sh -c "composer install && php main.php" -``` +try { + $result = $apiInstance->getStations(); + + echo 'OG Stations list:', PHP_EOL; -The response of `$apiInstance->addPet($pet)` is below. + foreach ($result->getData() as $station) { + echo $station->getName() . "\n"; + } -```text filename="Output" -OpenAPI\Client\Model\Pet Object -( - [openAPINullablesSetToNull:protected] => Array() - [container:protected] => Array - ( - [id] => 1 - [name] => 1 - [category] => - [photo_urls] => Array() - [tags] => Array() - [status] => - ) -) +} catch (Exception $e) { + echo 'Exception when StationsApi->getStations: ', $e->getMessage(), PHP_EOL; +} ``` -First, the command installs the PHP dependencies in the Docker container as recommended in the SDK `README.md` file, then it runs the sample `main.php` script to call the server using the SDK. +The OG SDK example is quite verbose, requiring setting up a configuration object, creating an API instance, and handling exceptions. The model objects also use getter methods to access properties found in the response. By default it uses exceptions to handle errors which some people prefer over checking response objects directly. -### Calling the server with the Speakeasy SDK +### Calling the API server with the Speakeasy SDK -The SDK Speakeasy creates also calls the server successfully. +Here's an example of calling the Train Travel API to get a list of stations using the Speakeasy SDK. The example below assumes you have installed the SDK using Composer and have autoloading set up. -Below is an example script to call the API with the SDK created by Speakeasy. Save it as `app/se/main.php`. - -```php filename="app/se/main.php" +```php "); +$sdk = TrainTravelSdk\TrainTravelSDK::builder() + ->setSecurity('some-access-token') + ->setServerIndex(0) + ->build(); -$sdk = OpenAPI\SDK::builder() - ->setSecurity($security->petstoreAuth) - ->build(); +$request = new Operations\GetStationsRequest(); -try { - // Fully typed SDK objects - $request = new Components\Pet10( - name: 'doggie', - photoUrls: [ - 'https://example.com/doggie.jpg', - 'https://example.com/doggie2.jpg', - ], - id: 10, - tags: [ - new Components\Tag( - id: 123, - name: 'pets', - ), - new Components\Tag( - id: 3, - name: 'good-dogs', - ), - new Components\Tag( - id: 900, - name: 'not-cats', - ), - ], - // Typed subobjects - category: new Components\Category( - id: 1, - name: 'Dogs', - ), - // Enums help you validate the input data - status: Components\Status::Available - ); - $response = $sdk->pet->addPetForm($request); - if ($response->pet !== null) { - print_r($response->pet); - } -} catch (Throwable $e) { - print_r($e); -} -``` +$response = $sdk->stations->list( + request: $request +); -In the example above, we use the `Components` namespace to create a typed security object and a typed request object. We then call the `addPetForm` operation on the `pet` object in the SDK. You'll notice that the SDK helps you validate the input data with enums and typed subobjects. +if ($response->twoHundredApplicationJsonObject !== null) { + echo PHP_EOL, 'Speakeasy Stations list:', PHP_EOL; -Let's run the script to see the response. + foreach ($response->twoHundredApplicationJsonObject->data as $station) { + echo $station->name . "\n"; + } +} else { + // handle unexpected response + echo "nah mate"; +} +``` -The command to run the script is nearly identical to the command the OpenAPI Generator SDK used, except for using the Speakeasy folder. +The Speakeasy SDK example is more concise, using a builder pattern to create the SDK instance and directly accessing properties on the response object. The request and response handling is also more straightforward. -```sh -docker run --rm -v "./app/se:/app" -w "/app" composer:2.7 sh -c "composer install && php main.php" -``` - -The response of `$sdk->pet->addPetForm($request)` is below. - -```text filename="Output" -OpenAPI\OpenAPI\Models\Components\Pet15 Object -( - [id] => 10 - [name] => doggie - [category] => OpenAPI\OpenAPI\Models\Components\Category Object - ( - [id] => 1 - [name] => Dogs - ) - [photoUrls] => Array - ( - [0] => https://example.com/doggie2.jpg - ) - [tags] => Array - ( - [0] => OpenAPI\OpenAPI\Models\Components\Tag Object - ( - [id] => 3 - [name] => good-dogs - ) - - [1] => OpenAPI\OpenAPI\Models\Components\Tag Object - ( - [id] => 900 - [name] => not-cats - ) - ) - [status] => OpenAPI\OpenAPI\Models\Components\Status Enum:string - ( - [name] => Available - [value] => available - ) -) -``` ## Package structure @@ -356,9 +302,9 @@ Let's compare the structure of the SDKs in terms of code volume and folder struc You can count the lines of code in the SDKs by running `cloc` for each (ignoring documentation and test folders): -```bash -cloc ./app/og/lib -cloc ./app/se/src +```sh +cloc ./og/lib +cloc ./speakeasy/src ``` Below are the results for each SDK. @@ -374,163 +320,148 @@ Below are the results for each SDK. data={[ { project: "OpenAPI Generator", - files: "16", - blank: "1198", - comment: "4267", - code: "6316" + files: "35", + blank: "1839", + comment: "8182", + code: "7752" }, { project: "Speakeasy", - files: "84", - blank: "1073", - comment: "2214", - code: "3915" + files: "114", + blank: "1352", + comment: "2743", + code: "4826" } ]} /> -We see that the Speakeasy SDK has five times as many files as OpenAPI Generator, but 40% less code. The libraries Speakeasy uses, as well as shared utility functions, allow it to create more concise code than OpenAPI Generator. +OpenAPI Generator generates fewer files but has significantly more lines of code, comments, and blank lines than the Speakeasy SDK. This indicates that OpenAPI Generator produces more verbose code, which can be harder to read and maintain. The following commands output the files of each SDK. ```sh -tree ./app/og/lib -tree ./app/se/src +tree ./og/lib +tree ./speakeasy/src ``` Below is the output for OpenAPI Generator. ```sh ├── Api -│ ├── PetApi.php -│ ├── StoreApi.php -│ └── UserApi.php +│ ├── BookingsApi.php +│ ├── PaymentsApi.php +│ ├── StationsApi.php +│ └── TripsApi.php ├── ApiException.php ├── Configuration.php +├── FormDataProcessor.php ├── HeaderSelector.php ├── Model -│ ├── Address.php -│ ├── ApiResponse.php -│ ├── Category.php -│ ├── Customer.php -│ ├── ModelInterface.php -│ ├── Order.php -│ ├── Pet.php -│ ├── Tag.php -│ └── User.php +│ ├── BankAccount.php +│ ├── Booking.php +│ ├── BookingPayment.php +│ ├── BookingPaymentSource.php +│ ├── Card.php +│ ├── CreateBooking201Response.php +│ ├── CreateBookingPayment200Response.php +│ ├── GetBookings200Response.php +│ ├── GetBookings200Response1.php +│ ├── GetStations200Response.php +│ ├── GetStations200Response1.php +│ ├── GetStations200ResponseAllOfLinks.php +│ ├── GetTrips200Response.php +│ ├── GetTrips200Response1.php +│ ├── GetTrips200ResponseAllOfDataInner.php +│ ├── LinksBooking.php +│ ├── LinksDestination.php +│ ├── LinksOrigin.php +│ ├── LinksPagination.php +│ ├── LinksSelf.php +│ ├── ModelInterface.php +│ ├── NewBookingRequest.php +│ ├── Problem.php +│ ├── Station.php +│ ├── Trip.php +│ └── WrapperCollection.php └── ObjectSerializer.php ``` -The folder structure is simple and clear with nothing unexpected. Files are separated at the API level (pet, store, and user) and by model. There are a few helper files, like `ApiException.php`. +The folder structure is simple and clear with nothing unexpected. Files are separated at the resource level (booking, trip, station) but there are also a lot of models for shared components like bank accounts, cards, and links. Then there are files like `GetStations200Response1` and `GetTrips200ResponseAllOfDataInner` that just seem like its having a very confusing time with something. Below is the output for Speakeasy. ```sh +./speakeasy/src +├── Bookings.php ├── Models -│ ├── Components -│ │ ├── ApiResponse.php -│ │ ├── Category.php -│ │ ├── Order1.php -│ │ ├── Order2.php -│ │ ├── Order3.php -│ │ ├── Order4.php -│ │ ├── Order5.php -│ │ ├── Order6.php -│ │ ├── OrderStatus.php -│ │ ├── Pet1.php -│ │ ├── Pet10.php -│ │ ├── Pet11.php -│ │ ├── Pet12.php -│ │ ├── Pet13.php -│ │ ├── Pet14.php -│ │ ├── Pet15.php -│ │ ├── Pet16.php -│ │ ├── Pet17.php -│ │ ├── Pet18.php -│ │ ├── Pet19.php -│ │ ├── Pet2.php -│ │ ├── Pet20.php -│ │ ├── Pet21.php -│ │ ├── Pet22.php -│ │ ├── Pet3.php -│ │ ├── Pet4.php -│ │ ├── Pet5.php -│ │ ├── Pet6.php -│ │ ├── Pet7.php -│ │ ├── Pet8.php -│ │ ├── Security.php -│ │ ├── Status.php -│ │ ├── Tag.php -│ │ ├── User1.php -│ │ ├── User10.php -│ │ ├── User11.php -│ │ ├── User12.php -│ │ ├── User13.php -│ │ ├── User15.php -│ │ ├── User2.php -│ │ ├── User3.php -│ │ ├── User4.php -│ │ ├── User5.php -│ │ ├── User6.php -│ │ ├── User7.php -│ │ ├── User8.php -│ │ └── User9.php -│ ├── Errors -│ │ └── SDKException.php -│ └── Operations -│ ├── AddPetFormResponse.php -│ ├── AddPetJsonResponse.php -│ ├── AddPetRawResponse.php -│ ├── CreateUserFormResponse.php -│ ├── CreateUserJsonResponse.php -│ ├── CreateUserRawResponse.php -│ ├── CreateUsersWithListInputResponse.php -│ ├── DeleteOrderRequest.php -│ ├── DeleteOrderResponse.php -│ ├── DeletePetRequest.php -│ ├── DeletePetResponse.php -│ ├── DeleteUserRequest.php -│ ├── DeleteUserResponse.php -│ ├── FindPetsByStatusRequest.php -│ ├── FindPetsByStatusResponse.php -│ ├── FindPetsByTagsRequest.php -│ ├── FindPetsByTagsResponse.php -│ ├── GetInventoryResponse.php -│ ├── GetInventorySecurity.php -│ ├── GetOrderByIdRequest.php -│ ├── GetOrderByIdResponse.php -│ ├── GetPetByIdRequest.php -│ ├── GetPetByIdResponse.php -│ ├── GetPetByIdSecurity.php -│ ├── GetUserByNameRequest.php -│ ├── GetUserByNameResponse.php -│ ├── LoginUserRequest.php -│ ├── LoginUserResponse.php -│ ├── LogoutUserResponse.php -│ ├── PlaceOrderFormResponse.php -│ ├── PlaceOrderJsonResponse.php -│ ├── PlaceOrderRawResponse.php -│ ├── Status.php -│ ├── UpdatePetFormResponse.php -│ ├── UpdatePetJsonResponse.php -│ ├── UpdatePetRawResponse.php -│ ├── UpdatePetWithFormRequest.php -│ ├── UpdatePetWithFormResponse.php -│ ├── UpdateUserFormRequest.php -│ ├── UpdateUserFormResponse.php -│ ├── UpdateUserJsonRequest.php -│ ├── UpdateUserJsonResponse.php -│ ├── UpdateUserRawRequest.php -│ ├── UpdateUserRawResponse.php -│ ├── UploadFileRequest.php -│ └── UploadFileResponse.php -├── Pet.php -├── SDK.php -├── SDKBuilder.php +│ ├── Components +│ │ ├── AccountType.php +│ │ ├── BankAccount.php +│ │ ├── Booking.php +│ │ ├── BookingInput.php +│ │ ├── BookingPayment.php +│ │ ├── Card.php +│ │ ├── Currency.php +│ │ ├── LinksBooking.php +│ │ ├── LinksSelf.php +│ │ ├── Security.php +│ │ ├── Station.php +│ │ └── Trip.php +│ ├── Errors +│ │ └── APIException.php +│ ├── Operations +│ │ ├── AccountType.php +│ │ ├── BankAccount.php +│ │ ├── Card.php +│ │ ├── CreateBookingPaymentRequest.php +│ │ ├── CreateBookingPaymentResponse.php +│ │ ├── CreateBookingPaymentResponseBody.php +│ │ ├── CreateBookingRawResponse.php +│ │ ├── CreateBookingRawResponseBody.php +│ │ ├── CreateBookingRawXMLResponseBody.php +│ │ ├── CreateBookingResponse.php +│ │ ├── CreateBookingResponseBody.php +│ │ ├── CreateBookingXMLResponseBody.php +│ │ ├── Currency.php +│ │ ├── Data.php +│ │ ├── DeleteBookingRequest.php +│ │ ├── DeleteBookingResponse.php +│ │ ├── GetBookingRequest.php +│ │ ├── GetBookingResponse.php +│ │ ├── GetBookingResponseBody.php +│ │ ├── GetBookingsLinks.php +│ │ ├── GetBookingsRequest.php +│ │ ├── GetBookingsResponse.php +│ │ ├── GetBookingsResponseBody.php +│ │ ├── GetBookingsXMLLinks.php +│ │ ├── GetBookingsXMLResponseBody.php +│ │ ├── GetBookingXMLResponseBody.php +│ │ ├── GetStationsLinks.php +│ │ ├── GetStationsRequest.php +│ │ ├── GetStationsResponse.php +│ │ ├── GetStationsResponseBody.php +│ │ ├── GetStationsXMLLinks.php +│ │ ├── GetStationsXMLResponseBody.php +│ │ ├── GetTripsLinks.php +│ │ ├── GetTripsRequest.php +│ │ ├── GetTripsResponse.php +│ │ ├── GetTripsResponseBody.php +│ │ ├── GetTripsXMLLinks.php +│ │ ├── GetTripsXMLResponseBody.php +│ │ └── Status.php +│ └── Webhooks +│ ├── Links.php +│ ├── NewBookingRequest.php +│ └── NewBookingResponse.php +├── Payments.php ├── SDKConfiguration.php -├── Store.php -├── User.php +├── Stations.php +├── TrainTravelSDK.php +├── TrainTravelSDKBuilder.php +├── Trips.php └── Utils + ├── BigDecimalHandler.php + ├── BigIntHandler.php ├── DateHandler.php ├── DateTimeHandler.php ├── DefaultRequest.php @@ -543,133 +474,116 @@ Below is the output for Speakeasy. ├── JSON.php ├── MixedJSONHandler.php ├── MultipartMetadata.php + ├── Options.php ├── ParamsMetadata.php ├── PathParameters.php ├── PhpDocTypeParser.php ├── QueryParameters.php ├── RequestBodies.php ├── RequestMetadata.php + ├── Retry + │ ├── PermanentError.php + │ ├── RetryConfig.php + │ ├── RetryConfigBackoff.php + │ ├── RetryConfigNone.php + │ ├── RetryStrategy.php + │ ├── RetryUtils.php + │ └── TemporaryError.php ├── Security.php ├── SecurityClient.php ├── SecurityMetadata.php + ├── ServerDetails.php ├── SpeakeasyMetadata.php ├── UnionHandler.php └── Utils.php ``` -The Speakeasy SDK is more complex and has more features. Files are separated at a lower level than OpenAPI Generator — at the operation level – and further split into content types of the operation, like `AddPetJsonResponse.php`. There are more helper files bundled with the SDK in the `Utils` folder. +The Speakeasy SDK has a more complex folder structure, with separate folders for models, operations, errors, and utilities. Each operation has its own set of request and response classes, which can make it easier to understand the API's functionality. It's also not been tripped up by any weird schema constructs for allOfs like OpenAPI Generator has, so this structure at least looks a lot cleaner from here. ## Code readability -We'll compare the SDKs in terms of code readability, focusing on the `Pet` model first. +We'll compare the SDKs in terms of code readability, focusing on the `BookingPayment` model first. ### OpenAPI Generator -The `Pet` model generated by OpenAPI Generator inherits a `ModelInterface` and has a `container` property that holds the model's fields. The model's constructor can either take an associative array of field names and values or no arguments. Then, the model exposes getter and setter methods for each field. +The `BookingPayment` model generated by OpenAPI Generator inherits a `ModelInterface` and has a `container` property that holds the model's fields. The model's constructor can either take an associative array of field names and values or no arguments. Then, the model exposes getter and setter methods for each field. -Type mapping is presented as an associative array of field names and types as strings. The `Pet` model has the following fields: +Type mapping is presented as an associative array of field names and types as strings. The `BookingPayment` model has the following fields: -```php filename="app/og/lib/Model/Pet.php" -//... - protected static $openAPITypes = [ - 'id' => 'int', - 'name' => 'string', - 'category' => '\OpenAPI\Client\Model\Category', - 'photo_urls' => 'string[]', - 'tags' => '\OpenAPI\Client\Model\Tag[]', - 'status' => 'string' - ]; -//... +```php filename="og/lib/Model/BookingPayment.php" + // ... + protected static $openAPITypes = [ + 'id' => 'string', + 'amount' => 'float', + 'currency' => 'string', + 'source' => '\OpenAPI\Client\Model\BookingPaymentSource', + 'status' => 'string' + ]; + // ... ``` -Overall, the `Pet` model is extremely verbose, coming in at 623 lines of code, including comments and whitespace, but excluding dependencies. +Overall, the `BookingPayment` model is extremely verbose, coming in at 630 lines of code, including comments and whitespace, but excluding dependencies. -Contrast this with the `Pet` model generated by Speakeasy. +Contrast this with the `BookingPayment` model generated by Speakeasy. ### Speakeasy -The `Pet10` model generated by Speakeasy is more concise and readable, presented in its entirety below: - -```php filename="app/se/src/Models/Components/Pet10.php" +The `BookingPayment` model generated by Speakeasy is more concise and readable, presented in its entirety below: +```php filename="app/se/src/Models/Components/BookingPayment.php" $photoUrls + * @var ?float $amount */ - #[SpeakeasyMetadata('form:name=photoUrls')] - public array $photoUrls; + #[\Speakeasy\Serializer\Annotation\SerializedName('amount')] + #[\Speakeasy\Serializer\Annotation\SkipWhenNull] + public ?float $amount = null; /** - * $tags + * Three-letter [ISO currency code](https://www.iso.org/iso-4217-currency-codes.html), in lowercase. * - * @var ?array $tags + * @var ?Currency $currency */ - #[SpeakeasyMetadata('form:name=tags,json=true')] - public ?array $tags = null; + #[\Speakeasy\Serializer\Annotation\SerializedName('currency')] + #[\Speakeasy\Serializer\Annotation\Type('\Speakeasy\TrainTravelSdk\Models\Components\Currency|null')] + #[\Speakeasy\Serializer\Annotation\SkipWhenNull] + public ?Currency $currency = null; /** - * pet status in the store + * The payment source to take the payment from. This can be a card or a bank account. Some of these properties will be hidden on read to protect PII leaking. * - * @var ?Status $status + * @var Card|BankAccount|null $source */ - #[SpeakeasyMetadata('form:name=status')] - public ?Status $status = null; + #[\Speakeasy\Serializer\Annotation\SerializedName('source')] + #[\Speakeasy\Serializer\Annotation\Type('\Speakeasy\TrainTravelSdk\Models\Components\Card|\Speakeasy\TrainTravelSdk\Models\Components\BankAccount|null')] + #[\Speakeasy\Serializer\Annotation\SkipWhenNull] + public Card|BankAccount|null $source = null; /** - * @param string $name - * @param array $photoUrls - * @param ?int $id - * @param ?Category $category - * @param ?array $tags - * @param ?Status $status + * @param ?float $amount + * @param ?Currency $currency + * @param Card|BankAccount|null $source + * @phpstan-pure */ - public function __construct(string $name, array $photoUrls, ?int $id = null, ?Category $category = null, ?array $tags = null, ?Status $status = null) + public function __construct(?float $amount = null, ?Currency $currency = null, Card|BankAccount|null $source = null) { - $this->name = $name; - $this->photoUrls = $photoUrls; - $this->id = $id; - $this->category = $category; - $this->tags = $tags; - $this->status = $status; + $this->amount = $amount; + $this->currency = $currency; + $this->source = $source; } } ``` -The `Pet10` model, at 76 lines of code, including comments and whitespace, is more concise and readable than the `Pet` model generated by OpenAPI Generator. Speakeasy uses modern PHP features like typed properties, attributes, and named arguments to make the model more readable. +The `BookingPayment` model, at 56 lines of code, including comments and whitespace, is more concise and readable than the `BookingPayment` model generated by OpenAPI Generator. Speakeasy uses modern PHP features like typed properties, enums, attributes, and named arguments to make the model more readable. Serialization and deserialization are handled by [JMS/Serializer](http://jmsyst.com/libs/serializer), which uses annotations in the model to convert objects to and from JSON. This allows Speakeasy to create more concise and readable code. @@ -700,15 +614,13 @@ Both creators use similar libraries, but OpenAPI Generator relies as much as pos ## Supported PHP versions -At the time of compiling this comparison, the Speakeasy SDK required at least PHP version 8.1. PHP 8 introduced language features to support stronger typing. +At the time of compiling this comparison, the Speakeasy SDK required at least PHP 8.2, and the OpenAPI Generator SDK still supports PHP 8.1. -The OpenAPI Generator SDK still supports PHP version 7.4, though it is compatible with PHP 8. +Seeing as PHP 8.1 has stopped receiving security updates we recommend updating to use the latest PHP version regardless of the SDK being used, but when the SDK is being used by external customers it's important to consider the lowest PHP version supported. -We recommend you use the latest PHP version with both SDKs. +## Type system -## Strong typing - -Both creators use DocBlocks to provide type annotations to all parameters and variables in the SDKs, which is useful for IDEs and for programmers to understand the code. +Both tools create DocBlocks to provide type annotations to all parameters and variables in the SDKs, which is useful for IDEs and for programmers to understand the code. But files in the Speakeasy SDK include the line `declare(strict_types=1);`, which causes PHP to throw a `TypeError` if a function accepts or returns an invalid type at runtime. The OpenAPI Generator SDK files do not have this line and so don't check types at runtime. @@ -716,67 +628,106 @@ In Speakeasy, the JMS Serializer checks types when converting from JSON to PHP o ### Enums -OpenAPI Generator provides a workaround for enumerations using constant strings and functions. Below is the pet status enumeration for OpenAPI Generator. +By leveraging modern enum functionality in PHP 8.x, Speakeasy is able to bypass a lot of boilerplate code and allow PHP to optimize itself efficiently. For example, the Train Travel API has a currency enumeration defined inside booking payments that looks like this in OpenAPI: + +```yaml +currency: + description: Three-letter [ISO currency code](https://www.iso.org/iso-4217-currency-codes.html), in lowercase. + type: string + enum: + - bam + - bgn + - chf + - eur + - gbp +``` + +The PHP to handle this in Speakeasy is straightforward using `enum`: ```php -public const STATUS_AVAILABLE = 'available'; -public const STATUS_PENDING = 'pending'; -public const STATUS_SOLD = 'sold'; - -/** - * Gets allowable values of the enum - * - * @return string[] - */ -public function getStatusAllowableValues() +setIfExists('id', $data ?? [], null); + $this->setIfExists('amount', $data ?? [], null); + $this->setIfExists('currency', $data ?? [], null); + $this->setIfExists('source', $data ?? [], null); + $this->setIfExists('status', $data ?? [], null); + } +} +``` + +You can see how much more verbose and complex the OpenAPI Generator code is compared to Speakeasy's use of enums. ### Unions In OpenAPI, you can use `oneOf` in a schema like this: ```yaml -Pet: +User: type: object properties: age: @@ -788,88 +739,134 @@ Pet: The `age` property will be typed as a union in PHP in Speakeasy: ```php -class Pet10 +class User { - /** - * - * @var int|string|null $age - */ - #[SpeakeasyMetadata('form:name=age')] - public int|string|null $age = null; + /** + * + * @var int|string|null $age + */ + #[SpeakeasyMetadata('form:name=age')] + public int|string|null $age = null; -... + // ... - public function __construct(?string $name = null, ?array $photoUrls = null, int|string|null $age = null, + public function __construct(?string $name = null, int|string|null $age = null) ``` -OpenAPI Generator can handle this schema, but creates a 380-line file called `PetAge.php` with custom code to implement unions. +OpenAPI Generator can handle this schema, but creates a 380-line file called `UserAge.php` with custom code to implement unions. -## Created documentation +## Asynchronous calls + +Both Speakeasy and OpenAPI Generator support asynchronous HTTP calls, but they implement them differently. + +Speakeasy: `getStation` and `getStationAsync`. +OpenAPI Generator `AddPet` and `AddPetAsync`. + +## Content types + +Below are the content types in the schema for creating a booking, in JSON, XML, or as a form. + +```yaml +requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/Booking" + application/xml: + schema: + $ref: "#/components/schemas/Booking" + application/x-www-form-urlencoded: + schema: + $ref: "#/components/schemas/Booking" +``` -Both Speakeasy and OpenAPI Generator create a `docs` directory with Markdown documentation and PHP usage examples for every operation and every model. +Speakeasy supports JSON and form content types, but not XML. OpenAPI Generator supports all three. -We found the usage examples in the Speakeasy SDK worked flawlessly, while the examples in the OpenAPI Generator SDK don't always include required fields when instantiating objects. For instance, the `PetApi.md` example in the OpenAPI Generator SDK doesn't include any fields for the `Pet` object. +In Speakeasy, each content type for each operation will become its own file in the SDK. In OpenAPI Generator, all operations are combined into one API file. + +## Created documentation -```php filename="app/og/docs/PetApi.md" +Both Speakeasy and OpenAPI Generator create a `docs` directory with Markdown documentation and PHP usage examples for every operation and every model. Unfortunately despite the formatting not looking to great to the eye of the beholder, there are two syntax errors in here which will need to be fixed before trying to run this code. Can you spot them? + +```php filename="og/docs/PaymentsApi.md" setAccessToken('YOUR_ACCESS_TOKEN'); -$apiInstance = new OpenAPI\Client\Api\PetApi( - // If you want to use a custom http client, pass your client which implements `GuzzleHttp\ClientInterface`. +$apiInstance = new OpenAPI\Client\Api\PaymentsApi( + // If you want use custom http client, pass your client which implements `GuzzleHttp\ClientInterface`. // This is optional, `GuzzleHttp\Client` will be used as default. new GuzzleHttp\Client(), $config ); -$pet = new \OpenAPI\Client\Model\Pet(); // \OpenAPI\Client\Model\Pet | Create a new pet in the store +$booking_id = 1725ff48-ab45-4bb5-9d02-88745177dedb; // string | The ID of the booking to pay for. +$booking_payment = {"amount":49.99,"currency":"gbp","source":{"object":"card","name":"J. Doe","number":"4242424242424242","cvc":"123","exp_month":12,"exp_year":2025,"address_line1":"123 Fake Street","address_line2":"4th Floor","address_city":"London","address_country":"gb","address_post_code":"N12 9XX"}}; // \OpenAPI\Client\Model\BookingPayment | Payment details try { - $result = $apiInstance->addPet($pet); + $result = $apiInstance->createBookingPayment($booking_id, $booking_payment); print_r($result); } catch (Exception $e) { - echo 'Exception when calling PetApi->addPet: ', $e->getMessage(), PHP_EOL; + echo 'Exception when calling PaymentsApi->createBookingPayment: ', $e->getMessage(), PHP_EOL; } ``` -Both SDKs include detailed documentation for operations and models, but the Speakeasy SDK includes more detailed usage examples that work out of the box. +The code `$booking_id = 1725ff48-ab45-4bb5-9d02-88745177dedb;` is missing quotation marks around the string value, and the line `$booking_payment = {"amount":49.99,"currency":"gbp","source":{...}};` is using JSON syntax instead of PHP array or object syntax. Those `{}` curly braces need to be replaced with `[]` because PHP does not support JSON syntax for array or object literals. -Speakeasy also creates appropriate example strings based on a field's `format` in the OpenAPI schema. +The Speakeasy documentation example for the same operation is below: -For example, if we add `format: uri` to the item for a pet's photo URLs, we can compare each SDK's usage documentation for this field. +```php filename="speakeasy/docs/sdks/payments/README.md" +declare(strict_types=1); -The SDK created by Speakeasy includes a helpful example of this field that lists multiple random URLs: +require 'vendor/autoload.php'; -```php -# Speakeasy SDK Usage Example -pet = shared.Pet( - # ... - photo_urls=[ - 'https://salty-stag.name', - 'https://moral-star.info', - 'https://present-giggle.info', - ] -) -``` +use Speakeasy\TrainTravelSdk; +use Speakeasy\TrainTravelSdk\Models\Components; + +$sdk = TrainTravelSdk\TrainTravelSDK::builder() + ->setSecurity( + '' + ) + ->build(); -The OpenAPI Generator SDK's documentation uses a single random string in its example: +$body = new Components\BookingPayment( + amount: 49.99, + currency: Components\Currency::Gbp, + source: new Components\Card( + name: 'J. Doe', + number: '4242424242424242', + cvc: '123', + expMonth: 12, + expYear: 2025, + addressLine1: '123 Fake Street', + addressLine2: '4th Floor', + addressCity: 'London', + addressCountry: 'gb', + addressPostCode: 'N12 9XX', + ), +); -```php -# PHP SDK Usage Example -pet = Pet( - # ... - photo_urls=[ - "photo_urls_example" - ] -) +$response = $sdk->bookings->payments->create( + bookingId: '1725ff48-ab45-4bb5-9d02-88745177dedb', + body: $body + +); + +if ($response->object !== null) { + // handle response +} ``` +Instead of using associative arrays or awkwardly invalid JSON syntax, Speakeasy uses typed objects and named arguments to create the request body, making it more readable and less error-prone. + ## Automation -This comparison focuses on installing and using Speakeasy and OpenAPI Generator using the command line, but both tools can also run as part of a CI workflow. For example, you can set up a [GitHub Action](https://github.com/speakeasy-api/sdk-generation-action) to ensure your Speakeasy SDK is always up-to-date when your API schema changes. +This comparison focuses on installing and using Speakeasy and OpenAPI Generator using the command line, but both tools can also run as part of a CI workflow. For example, you can set up a [GitHub Action](https://github.com/speakeasy-api/sdk-generation-action) to ensure your Speakeasy SDK is always up-to-date when OpenAPI documents change. + +OpenAPI Generator can run in any CI environment that supports Java, as it is a Java-based tool, but the generated SDKs come with a `travis-ci.yaml` which you'll need to delete or convert to another platform like GitHub Actions, as Travis CI is no longer widely used. ## Unsupported features