Skip to content
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
21 changes: 16 additions & 5 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,10 @@ permissions:
contents: read

jobs:
codecheck:
phpstan:
runs-on: ubuntu-latest
steps:
- uses: shivammathur/setup-php@2cb9b829437ee246e9b3cac53555a39208ca6d28
- uses: shivammathur/setup-php@v2
with:
php-version: '8.3'
- uses: actions/checkout@v3
Expand All @@ -31,13 +31,16 @@ jobs:
run: composer install -q --no-ansi --no-interaction --no-scripts --no-progress --prefer-dist

- name: PHPStan
run: composer phpstan
run: vendor/bin/phpstan analyse --memory-limit=1G

test:
needs: [phpstan]
runs-on: ubuntu-latest
steps:
- uses: shivammathur/setup-php@2cb9b829437ee246e9b3cac53555a39208ca6d28
- uses: shivammathur/setup-php@v2
with:
php-version: '8.3'
coverage: xdebug
- uses: actions/checkout@v3

- name: Cache Composer packages
Expand All @@ -53,4 +56,12 @@ jobs:
run: composer install -q --no-ansi --no-interaction --no-scripts --no-progress --prefer-dist

- name: PHPUnit
run: composer test
run: vendor/bin/phpunit --coverage-html coverage-report

- name: Upload Coverage Report
uses: actions/upload-artifact@v4
if: always()
with:
name: coverage-report
path: coverage-report/
retention-days: 30
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@ vendor/
.phpunit.cache/
tests/data/Generated
composer.lock
.DS_Store
.DS_Store
coverage-report
File renamed without changes.
143 changes: 117 additions & 26 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,17 +1,26 @@
# Transfer Bundle

### Requirements:
* php: >= 8.3
* symfony: >= 7.2
* ext-simplexml: *
[![CI](https://github.com/philipphermes/transfer-bundle/actions/workflows/ci.yml/badge.svg)](https://github.com/philipphermes/transfer-bundle/actions/workflows/ci.yml)
[![PHP](https://img.shields.io/badge/php-%3E%3D%208.3-8892BF.svg)]((https://img.shields.io/badge/php-%3E%3D%208.3-8892BF.svg))
[![Symfony](https://img.shields.io/badge/symfony-%3E%3D%207.4-8892BF.svg)]((https://img.shields.io/badge/symfony-%3E%3D%207.4-8892BF.svg))

## Usage:
## Table of Contents

1. [Installation](#installation)
1. [configuration](#configuration)
2. [openApi](#openapi)
2. [Code Quality](#code-quality)
2. [phpstan](#phpstan)
3. [Test](#test)
1. [phpunit](#phpunit)

## Installation

```shell
composer require philipphermes/transfer-bundle
```

### Configuration:
### Configuration

```php
// config/bundles.php
Expand All @@ -21,15 +30,17 @@ return [
];
```

#### Optional Configs:
#### Optional Configs

* `transfer.namespace`: `App\\Generated\\Transfers`
* `transfer.schema_dir`: `%kernel.project_dir%/transfers`
* `transfer.output_dir`: `%kernel.project_dir%/src/Generated/Transfers`

### Define Transfers:
### Define Transfers

* you can create multiple files
* if multiple files have the same transfer they will be merged
* if you define the same property twice the first on it gets is taken
* if you define the same property twice the first on it gets is taken

```xml
<?xml version="1.0" encoding="UTF-8"?>
Expand All @@ -39,7 +50,8 @@ return [
<transfer name="User">
<property name="email" type="string" description="The email of the user"/>
<property name="password" type="string" description="The password of the user"/>
<property name="addresses" type="Address[]" description="Shipping addresses" singular="address" isNullable="true"/>
<property name="addresses" type="Address[]" description="Shipping addresses" singular="address"
isNullable="true"/>
<property name="roles" type="string[]" description="List of roles" isNullable="false"/>
</transfer>

Expand All @@ -48,34 +60,113 @@ return [
</transfer>
</transfers>
```
#### Security Bundle Integration
If you want to use this feature make sure you have the Security Bundle installed.

```shell
composer require symfony/security-bundle
```
### OpenAPI

Then you can define your transfer eg. like this:
> [!CAUTION]
> This currently does not work with Symfony 8

you can add `api="true"` to transfers to add attributes and a ref automatically.
child transfers won't get it automatically.

```xml
<?xml version="1.0" encoding="UTF-8"?>
<transfers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="../vendor/philipphermes/transfer-bundle/src/Resources/schema/transfer.xsd">

<transfer name="User" type="user">
<property name="email" type="string" isIdentifier="true"/>
<property name="password" type="string"/>
<property name="plainPassword" type="string" isSensitive="true" isNullable="true"/>
</transfer>
<transfer name="User" api="true">
<property name="email" type="string" description="The email of the user"/>
<property name="password" type="string" description="The password of the user"/>
<property name="addresses" type="Address[]" description="Shipping addresses" singular="address"
isNullable="true"/>
<property name="roles" type="string[]" description="List of roles" isNullable="false"/>
</transfer>

<transfer name="Address" api="true">
<property name="street" type="string"/>
</transfer>

<transfer name="Error" api="true">
<property name="status" type="int"/>
<property name="messages" singular="message" type="ErrorMessage"/>
</transfer>

<transfer name="ErrorMessage" api="true">
<property name="message" type="string"/>
</transfer>
</transfers>
```

it will implement the UserInterface and have all required methods like:
* getUserIdentifier
* eraseCredentials
* get/set Roles
then you can use it in api routes for example like this:

```php
<?php

declare(strict_types=1);

namespace App\Controller;

use App\Generated\Transfers\UserTransfer;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
use OpenApi\Attributes as OA;

class UserApiController extends AbstractController
{
#[OA\Tag(name: 'user')]
#[OA\Response(
response: 200,
description: 'Returns a user by id',
content: new OA\JsonContent(
ref: '#/components/schemas/User',
)
)]
#[OA\Response(
response: 404,
description: 'Returns a error',
content: new OA\JsonContent(
ref: '#/components/schemas/Error',
)
)]
#[OA\Response(
response: 500,
description: 'Returns a error',
content: new OA\JsonContent(
ref: '#/components/schemas/Error',
)
)]
#[Route('/api/user/{id}', name: 'get_user_by_id', methods: ['GET'])]
public function getUserByIdAction(int $id): Response
{
$user = $this->userFacade->getUserById($id);

return $this->json($user);
}
}
```

## Generate transfers

### Run
```shell
symfony console transfer:generate
```

## Code Quality

### Phpstan

```bash
vendor/bin/phpstan analyse --memory-limit=1G
```

## Test

### Phpunit

```bash
vendor/bin/phpunit

# With coverage
XDEBUG_MODE=coverage vendor/bin/phpunit --coverage-html coverage-report
```
27 changes: 11 additions & 16 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,28 +23,23 @@
"require": {
"php": ">=8.3",
"ext-simplexml": "*",
"nette/php-generator": "^4.1",
"symfony/config": "^7.2",
"symfony/console": "^7.2",
"symfony/dependency-injection": "^7.2",
"symfony/filesystem": "^7.2",
"symfony/finder": "^7.2",
"symfony/framework-bundle": "^7.2"
"nette/php-generator": "^v4.2",
"symfony/config": "^7.4",
"symfony/console": "^7.4",
"symfony/dependency-injection": "^7.4",
"symfony/filesystem": "^7.4",
"symfony/finder": "^7.4",
"symfony/framework-bundle": "^7.4"
},
"require-dev": {
"phpstan/phpstan": "^2.1",
"phpunit/phpunit": "^12.1",
"symfony/security-bundle": "^7.2"
"zircote/swagger-php": "^5.7"
},
"suggest": {
"zircote/swagger-php": "^5.7"
},
"config": {
"sort-packages": true
},
"scripts": {
"phpstan": [
"vendor/bin/phpstan analyse -c phpstan.neon"
],
"test": [
"XDEBUG_MODE=coverage vendor/bin/phpunit"
]
}
}
84 changes: 81 additions & 3 deletions src/PhilippHermesTransferBundle.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ class PhilippHermesTransferBundle extends AbstractBundle
*/
public function configure(DefinitionConfigurator $definition): void
{
/** @phpstan-ignore-next-line */
$definition->rootNode()
->children()
->scalarNode('schema_dir')
Expand All @@ -39,10 +38,9 @@ public function configure(DefinitionConfigurator $definition): void
}

/**
*
* @inheritDoc
*
* @param array<array-key, mixed> $config
* @param array<string, mixed> $config
*/
public function loadExtension(array $config, ContainerConfigurator $container, ContainerBuilder $builder): void
{
Expand All @@ -66,4 +64,84 @@ public function loadExtension(array $config, ContainerConfigurator $container, C
->setArgument('$outputDir', '%transfer.output_dir%')
->setArgument('$namespace', '%transfer.namespace%');
}

/**
* @inheritDoc
*/
public function prependExtension(ContainerConfigurator $container, ContainerBuilder $builder): void
{
try {
$outputDir = $builder->getParameter('transfer.output_dir');
} catch (\Exception) {
$projectDir = $builder->getParameter('kernel.project_dir');
if (!is_string($projectDir)) {
return;
}
$outputDir = $projectDir . '/src/Generated/Transfers';
}

try {
$namespace = $builder->getParameter('transfer.namespace');
} catch (\Exception) {
$namespace = 'App\\Generated\\Transfers';
}

if (!is_string($outputDir) || !is_string($namespace) || !is_dir($outputDir)) {
return;
}

$models = [];
$filePaths = glob($outputDir . '/*Transfer.php');
if (!$filePaths) {
return;
}

foreach ($filePaths as $filePath) {
$className = $this->classFromPath($filePath, $namespace, $outputDir);

$data = file_get_contents($filePath);
if (!$data || !str_contains($data, 'use OpenApi\Attributes as OA;')) {
continue;
}

$models[] = [
'alias' => $this->aliasFromPath($filePath),
'type' => $className,
];
}

if ($models !== []) {
$builder->prependExtensionConfig('nelmio_api_doc', [
'models' => [
'names' => $models,
],
]);
}
}

/**
* @param string $filePath
* @param string $namespace
* @param string $outputDir
*
* @return string
*/
protected function classFromPath(string $filePath, string $namespace, string $outputDir): string
{
$relativePath = str_replace([$outputDir, '/', '.php'], ['', '\\', ''], $filePath);

return $namespace . $relativePath;
}

/**
* @param string $filePath
*
* @return string|null
*/
protected function aliasFromPath(string $filePath): ?string
{
$filename = basename($filePath, '.php');

return preg_replace('/Transfer$/', '', $filename);
}
}
Loading