philipphermes / transfer-bundle
generates transfers from xml
Package info
github.com/philipphermes/transfer-bundle
Type:symfony-bundle
pkg:composer/philipphermes/transfer-bundle
Requires
- php: >=8.3
- ext-dom: *
- ext-libxml: *
- ext-simplexml: *
- nette/php-generator: ^4.2
- symfony/config: ^7.4 || ^8.0
- symfony/console: ^7.4 || ^8.0
- symfony/dependency-injection: ^7.4 || ^8.0
- symfony/finder: ^7.4 || ^8.0
- symfony/framework-bundle: ^7.4 || ^8.0
- symfony/http-kernel: ^7.4 || ^8.0
Requires (Dev)
- nelmio/api-doc-bundle: ^5.8.3
- phpstan/phpstan: ^2.1
- phpunit/phpunit: ^12.1
- zircote/swagger-php: ^5.1 || ^6.0
Suggests
- nelmio/api-doc-bundle: Adds OpenAPI attributes and model names for transfers marked with api="true"
Provides
None
Conflicts
None
Replaces
None
README
A Symfony bundle for generating type-safe transfer objects (DTOs) from XML schema definitions. Supports OpenAPI attribute generation for API documentation.
Table of Contents
Installation
composer require philipphermes/transfer-bundle
Register the bundle in config/bundles.php:
return [ // ... PhilippHermes\TransferBundle\PhilippHermesTransferBundle::class => ['all' => true], ];
Configuration
Create config/packages/transfer.yaml to customize the bundle:
transfer: schema_dirs: - '%kernel.project_dir%/transfers' exclude_dirs: [] output_dir: '%kernel.project_dir%/src/Generated/Transfers' namespace: 'App\Generated\Transfers'
Configuration Options
| Option | Default | Description |
|---|---|---|
schema_dirs |
['%kernel.project_dir%/transfers'] |
Directories to scan for XML schema files (supports glob patterns) |
exclude_dirs |
[] |
Directories to exclude from scanning |
output_dir |
%kernel.project_dir%/src/Generated/Transfers |
Output directory for generated transfer classes |
namespace |
App\Generated\Transfers |
PHP namespace for generated classes |
Vendor-Level Discovery
To include transfers from vendor packages:
transfer: schema_dirs: - '%kernel.project_dir%/transfers' - '%kernel.project_dir%/vendor/*/*/transfers' exclude_dirs: - '%kernel.project_dir%/vendor/*/tests' - '%kernel.project_dir%/vendor/*/*/tests'
Usage
Defining Transfers
Create XML schema files in your configured schema directories (default: transfers/).
<?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"> <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[]" singular="address" isNullable="true"/> <property name="roles" type="string[]"/> </transfer> <transfer name="Address"> <property name="street" type="string"/> </transfer> </transfers>
Key features:
- Multiple XML files are supported and will be merged (files are read in sorted path order)
- Transfers with the same name across files are combined;
api="true"in any definition makes it an API transfer - First definition of a property takes precedence; a later definition with a different type produces a warning
Property Attributes
| Attribute | Required | Description |
|---|---|---|
name |
Yes | Property name |
type |
Yes | Property type, see Types |
description |
No | Property description (used in OpenAPI docs) |
singular |
No | Singular name used for the addX() method of [] properties (default: the property name) |
isNullable |
No | Whether the property can be null (true/false/1/0) |
default |
No | Initial value for string, int, float and bool properties, converted to the property type |
example |
No | Example value for the OpenAPI docs |
deprecated |
No | Marks the property and its accessors @deprecated (and deprecated in the OpenAPI docs) |
Names of transfers, properties and singular must be valid PHP identifiers, and the generated
accessors must not collide (method names are case-insensitive, so foo and Foo can't coexist).
Types
| Type | Result |
|---|---|
string, int, float, bool, array, object, mixed |
used as-is |
Name of a defined transfer, e.g. Address |
AddressTransfer from the generated namespace |
Existing class, interface or enum, e.g. DateTime, App\Enum\Status |
used as fully qualified name |
X[] of a PHP type, e.g. string[] |
array |
X[] of a transfer or class, e.g. Address[] |
ArrayObject |
Any other type is reported as an error.
Enums
Enums are not generated. Write the enum yourself and use its fully qualified name as the type. It must be
autoloadable when transfer:generate runs, otherwise it is reported as an unknown type.
namespace App\Enum; enum Status: string { case Active = 'active'; case Blocked = 'blocked'; }
<transfer name="User"> <property name="status" type="App\Enum\Status" isNullable="true"/> <property name="history" type="App\Enum\Status[]" singular="historyEntry"/> </transfer>
This generates getStatus(): ?Status, setStatus() and hasStatus(), plus a Status collection with
getHistory(): ArrayObject and addHistoryEntry(Status $historyEntry).
Array conversion works as follows (enum instances are accepted as-is in both directions):
| Enum | toArray() |
createFromArray() / fromArray() |
|---|---|---|
Backed (enum Status: string) |
the case value, e.g. 'active' |
Status::from($value), throws a ValueError for unknown values |
Pure (enum Size) |
the case name, e.g. 'Small' |
the case with that name, throws an Error for unknown names |
$user = UserTransfer::createFromArray([UserTransfer::STATUS => 'active']); $user->getStatus(); // Status::Active $user->toArray()[UserTransfer::STATUS]; // 'active'
default is not supported for enum properties, so set the initial value in code. For the OpenAPI output of enums see
OpenAPI Integration.
Schema Validation
Every file is validated against transfer.xsd. Violations such as a misspelled attribute (isNulable="true") are
reported as warnings with file and line, parsing continues as before.
Generated Methods
For every property the generator creates getX(), setX() and hasX() (true if the property is set and not
null). [] properties additionally get addX(). Getters of [] properties always return a collection (an empty
one if unset or set to null).
Each property also gets a public constant holding its name in UPPER_SNAKE_CASE (e.g. public const string CREATED_AT = 'createdAt';).
toArray() and fromArray() use them as array keys, so use them as well instead of string literals ($data[UserTransfer::EMAIL]).
Properties whose constant names collide (e.g. fooBar and foo_bar, or apiAlias in an api="true" transfer) are reported as errors.
Every transfer also gets:
| Method | Description |
|---|---|
toArray(): array |
Converts the transfer recursively: nested transfers become arrays, collections plain arrays, dates DATE_ATOM strings and enums their value (backed) or name. Every property is present, unset ones are null. |
static createFromArray(array $data): self |
The reverse of toArray(). Missing keys stay unset, unknown keys are ignored, already converted values (e.g. a transfer object) are accepted as well. |
fromArray(array $data): self |
Same conversion as createFromArray(), but on an existing instance: only the keys present are set, all other properties keep their value. Returns $this. |
__clone() |
Makes clone deep for nested transfers, ArrayObject collections and DateTime values, so a clone never shares state with the original. Only generated when needed. |
$user = UserTransfer::createFromArray(['email' => 'jane@example.com', 'addresses' => [['street' => 'Main St']]]); $user->getAddresses()[0]; // AddressTransfer $user->toArray(); // ['email' => 'jane@example.com', 'password' => null, 'addresses' => [['street' => 'Main St']], 'roles' => []] $user->fromArray([UserTransfer::EMAIL => 'john@example.com']); // only the email changes $user->toArray()[UserTransfer::EMAIL]; // 'john@example.com'
Generating Transfers
Run the generator command:
php bin/console transfer:generate
Options:
--clean-disable- Keep stale transfers in the output directory--check- Don't write anything, only check whether the generated transfers are up to date. Lists new, changed and stale files and exits with a non-zero code if there are any, e.g. for CI:
php bin/console transfer:generate --check
Files whose content didn't change are not rewritten, so their modification time stays the same.
After a successful generation, generated transfers that no longer exist in the schemas are removed from the output
directory. Only top-level files carrying the This file is auto-generated. header are removed. If parsing fails, the
command prints the errors, exits with a non-zero code and doesn't touch the output directory. Warnings (e.g. a schema
directory that matches nothing) are printed but don't fail the command.
OpenAPI Integration
Add api="true" to transfers to automatically generate OpenAPI attributes.
Transfer Attributes
| Attribute | Required | Description |
|---|---|---|
name |
Yes | Transfer name (generates {name}Transfer class) |
api |
No | Set to true to generate OpenAPI attributes |
apiAlias |
No | Custom name for OpenAPI documentation (default: transfer name without "Transfer" suffix) |
Example
<transfer name="User" api="true" apiAlias="UserResource"> <property name="email" type="string" description="The email of the user"/> <property name="password" type="string" description="The password of the user"/> </transfer> <transfer name="Error" api="true"> <property name="status" type="int"/> <property name="message" type="string"/> </transfer>
Use in your controllers with NelmioApiDocBundle:
use App\Generated\Transfers\UserTransfer; use App\Generated\Transfers\ErrorTransfer; use Nelmio\ApiDocBundle\Attribute\Model; 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 Model(type: UserTransfer::class) )] #[OA\Response( response: 404, description: 'User not found', content: new Model(type: ErrorTransfer::class) )] #[Route('/api/user/{id}', methods: ['GET'])] public function getUserById(int $id): Response { // ... } }
Property attributes in the generated OA\Property:
description,exampleanddefaultfrom the XMLnullable: truefor nullable non-collection properties,deprecated: truefor deprecated ones- backed enums get their backing type and
enumwith the case values, pure enumstype: 'string'with the case names
Note
Child transfers do not inherit api="true" - you must set it explicitly on each transfer.
Development
Static Analysis
vendor/bin/phpstan analyse --memory-limit=1G
Testing
vendor/bin/phpunit
With coverage report:
XDEBUG_MODE=coverage vendor/bin/phpunit --coverage-html coverage-report