Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add PHP JsonSerializable Preset #1491

Merged
merged 6 commits into from
Aug 26, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 15 additions & 4 deletions docs/languages/Php.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,21 +15,32 @@ There are special use-cases that each language supports; this document pertains

## Description Present

By default, descriptions are not rendered for the model, you can change that by applying `PHP_DESCRIPTION_PRESET`.
By default, descriptions are not rendered for the model; you can change that by applying `PHP_DESCRIPTION_PRESET`.

Check out this [example for a live demonstration](../../examples/php-generate-documentation-preset).

## Generate serializer and deserializer functionality

The most widely used usecase for Modelina is to generate models that include serilization and deserialization functionality to convert the models into payload data. This payload data can of course be many different kinds, JSON, XML, raw binary, you name it.
The most widely used usecase for Modelina is to generate models that include serialization and deserialization functionality to convert the models into payload data.
This payload data can, of course, be many different kinds, JSON, XML, raw binary, you name it.

As you normally only need one library to do this, we developers can never get enough with creating new stuff, therefore there might be one specific library you need or want to integrate with. Therefore there is not one specific preset that offers everything. Below is a list of all the supported serialization presets.
As you normally only need one library to do this, we developers can never get enough of creating new stuff, therefore, there might be one specific library you need or want to integrate with.
Therefore, there is not one specific preset that offers everything. Below is a list of all the supported serialization presets.

### To and from JSON
Currently not supported, [let everyone know you need it](https://github.com/asyncapi/modelina/issues/new?assignees=&labels=enhancement&template=enhancement.md)!

Objects in PHP can generally be serialized to JSON using the [`json_encode()` function](https://www.php.net/manual/en/function.json-encode.php).
To ensure that the data is serialized correctly, the [`JsonSerializable` interface](https://www.php.net/manual/en/class.jsonserializable.php) needs to be implemented.
This will ensure that, for example, enum values and property names are serialized correctly.

To add support of serialization to JSON, apply the `PHP_JSON_SERIALIZABLE_PRESET` preset.
Check out this [example for a live demonstration](../../examples/php-generate-json-serializable-preset).


### To and from XML

Currently not supported, [let everyone know you need it](https://github.com/asyncapi/modelina/issues/new?assignees=&labels=enhancement&template=enhancement.md)!

### To and from binary

Currently not supported, [let everyone know you need it](https://github.com/asyncapi/modelina/issues/new?assignees=&labels=enhancement&template=enhancement.md)!
17 changes: 17 additions & 0 deletions examples/php-generate-json-serializable-preset/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# PHP Generate JSON Serializable Models Example

A basic example of how to use Modelina and output PHP model that supports JSON serialization.

## How to run this example

Run this example using:

```sh
npm i && npm run start
```

If you are on Windows, use the `start:windows` script instead:

```sh
npm i && npm run start:windows
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`Should be able to render PHP and should log expected output to console 1`] = `
Array [
"<?php
declare(strict_types=1);
namespace Asyncapi;
final class Root implements \\\\JsonSerializable
{
private ?string $email;
public function getEmail(): ?string { return $this->email; }
public function setEmail(?string $email): void { $this->email = $email; }
public function jsonSerialize(): array
{
return [
'email' => $this->email,
];
}
}
",
]
`;
15 changes: 15 additions & 0 deletions examples/php-generate-json-serializable-preset/index.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
const spy = jest.spyOn(global.console, 'log').mockImplementation(() => {
return;
});
import { generate } from './index';

describe('Should be able to render PHP', () => {
afterAll(() => {
jest.restoreAllMocks();
});
test('and should log expected output to console', async () => {
await generate();
expect(spy.mock.calls.length).toEqual(1);
expect(spy.mock.calls[0]).toMatchSnapshot();
});
});
33 changes: 33 additions & 0 deletions examples/php-generate-json-serializable-preset/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import {
OutputModel,
PHP_JSON_SERIALIZABLE_PRESET,
PhpGenerator
} from '../../src';

const generator: PhpGenerator = new PhpGenerator({
presets: [PHP_JSON_SERIALIZABLE_PRESET]
});
const jsonSchemaDraft7 = {
$schema: 'http://json-schema.org/draft-07/schema#',
type: 'object',
additionalProperties: false,
properties: {
email: {
type: 'string',
format: 'email'
}
}
};

export async function generate(): Promise<void> {
const models: OutputModel[] = await generator.generateCompleteModels(
jsonSchemaDraft7,
{}
);
for (const model of models) {
console.log(model.result);
}
}
if (require.main === module) {
generate();
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 10 additions & 0 deletions examples/php-generate-json-serializable-preset/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"config" : { "example_name" : "php-generate-json-serializable-preset" },
"scripts": {
"install": "cd ../.. && npm i",
"start": "../../node_modules/.bin/ts-node --cwd ../../ ./examples/$npm_package_config_example_name/index.ts",
"start:windows": "..\\..\\node_modules\\.bin\\ts-node --cwd ..\\..\\ .\\examples\\%npm_package_config_example_name%\\index.ts",
"test": "../../node_modules/.bin/jest --config=../../jest.config.js ./examples/$npm_package_config_example_name/index.spec.ts",
"test:windows": "..\\..\\node_modules\\.bin\\jest --config=..\\..\\jest.config.js examples/%npm_package_config_example_name%/index.spec.ts"
}
}
91 changes: 91 additions & 0 deletions src/generators/php/presets/JsonSerializablePreset.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import { PhpPreset } from '../PhpPreset';
import { PhpRenderer } from '../PhpRenderer';
import {
ConstrainedDictionaryModel,
ConstrainedMetaModel
} from '../../../models';

function renderSelf({
content
}: {
content: string;
renderer: PhpRenderer<ConstrainedMetaModel>;
}): string {
const contentLines = content.split('\n');
contentLines[0] += ` implements \\JsonSerializable`;

return contentLines.join('\n');
}

/**
* Preset, which implements PHP’s JsonSerializable interface.
*
* Using this will allow to json serialize the model using `json_encode()`.
*
* @implements {PhpPreset}
*/
export const PHP_JSON_SERIALIZABLE_PRESET: PhpPreset = {
class: {
self({ content, renderer }): string {
return renderSelf({ content, renderer });
},
additionalContent({ renderer, model, content }): string {
const serializedProperties = Object.values(model.properties).map(
(property) => {
if (
property.property instanceof ConstrainedDictionaryModel &&
property.property.serializationType === 'unwrap'
) {
return `...$this->${property.propertyName},`;
}

return `'${property.unconstrainedPropertyName}' => $this->${property.propertyName},`;
}
);

return (
content +
renderer.renderBlock([
'public function jsonSerialize(): array',
'{',
renderer.indent(
renderer.renderBlock([
'return [',
renderer.indent(renderer.renderBlock(serializedProperties)),
'];'
])
),
'}'
])
);
}
},
enum: {
self({ content, renderer }): string {
return renderSelf({ content, renderer });
},
additionalContent({ content, model, renderer }) {
return (
content +
renderer.renderBlock([
`public function jsonSerialize(): mixed`,
'{',
renderer.indent(
renderer.renderBlock([
'return match($this) {',
renderer.indent(
renderer.renderBlock(
Object.values(model.values).map(
(value) => `self::${value.key} => ${value.value},`
)
)
),
'};'
])
),
'}'
])
);
}
}
};
1 change: 1 addition & 0 deletions src/generators/php/presets/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * from './DescriptionPreset';
export * from './JsonSerializablePreset';
53 changes: 53 additions & 0 deletions test/generators/php/presets/JsonSerializablePreset.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { PHP_JSON_SERIALIZABLE_PRESET, PhpGenerator } from '../../../../src';

describe('PHP_JSON_SERIALIZABLE_PRESET', () => {
let generator: PhpGenerator;
beforeEach(() => {
generator = new PhpGenerator({
presets: [PHP_JSON_SERIALIZABLE_PRESET]
});
});

test('should render jsonSerialize method for class', async () => {
const doc = {
$id: 'Clazz',
type: 'object',
properties: {
prop: {
type: 'string'
},
'prop-with-dash': {
type: 'string'
}
}
};

const models = await generator.generate(doc);
expect(models).toHaveLength(1);
expect(models[0].result).toMatchSnapshot();
});

test('should render jsonSerialize method for enum', async () => {
const doc = {
$id: 'Enumm',
type: 'enum',
enum: ['value-A', 'value-B']
};

const models = await generator.generate(doc);
expect(models).toHaveLength(1);
expect(models[0].result).toMatchSnapshot();
});

test('should render jsonSerialize method for enum with mixed types', async () => {
const doc = {
$id: 'Enumm',
type: 'enum',
enum: [1, 'B']
};

const models = await generator.generate(doc);
expect(models).toHaveLength(1);
expect(models[0].result).toMatchSnapshot();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`PHP_JSON_SERIALIZABLE_PRESET should render jsonSerialize method for class 1`] = `
"final class Clazz implements \\\\JsonSerializable
{
private ?string $prop;
private ?string $propMinusWithMinusDash;
private mixed $additionalProperties;
public function getProp(): ?string { return $this->prop; }
public function setProp(?string $prop): void { $this->prop = $prop; }
public function getPropMinusWithMinusDash(): ?string { return $this->propMinusWithMinusDash; }
public function setPropMinusWithMinusDash(?string $propMinusWithMinusDash): void { $this->propMinusWithMinusDash = $propMinusWithMinusDash; }
public function getAdditionalProperties(): mixed { return $this->additionalProperties; }
public function setAdditionalProperties(mixed $additionalProperties): void { $this->additionalProperties = $additionalProperties; }
public function jsonSerialize(): array
{
return [
'prop' => $this->prop,
'prop-with-dash' => $this->propMinusWithMinusDash,
...$this->additionalProperties,
];
}
}
"
`;
exports[`PHP_JSON_SERIALIZABLE_PRESET should render jsonSerialize method for enum 1`] = `
"enum Enumm implements \\\\JsonSerializable
{
case VALUE_MINUS_A;
case VALUE_MINUS_B;
public function jsonSerialize(): mixed
{
return match($this) {
self::VALUE_MINUS_A => \\"value-A\\",
self::VALUE_MINUS_B => \\"value-B\\",
};
}
markuspoerschke marked this conversation as resolved.
Show resolved Hide resolved
}
"
`;
exports[`PHP_JSON_SERIALIZABLE_PRESET should render jsonSerialize method for enum with mixed types 1`] = `
"enum Enumm implements \\\\JsonSerializable
{
case NUMBER_1;
case B;
public function jsonSerialize(): mixed
{
return match($this) {
self::NUMBER_1 => 1,
self::B => \\"B\\",
};
}
}
"
`;
Loading