it-bens / object-transformer
Mediator that performs transformations between objects by using transformation strategies.
Installs: 120
Dependents: 1
Suggesters: 0
Security: 0
Stars: 1
Watchers: 1
Forks: 1
Open Issues: 1
Type:package
Requires
- php: ^8.0
Requires (Dev)
- captainhook/plugin-composer: ^5.3
- phpstan/phpstan: ^0.12.99
- phpunit/phpunit: ^9.5
- roave/security-advisories: dev-latest
This package is auto-updated.
Last update: 2024-11-15 16:44:49 UTC
README
How to install the package?
The package can be installed via Composer:
composer require it-bens/object-transformer
It requires at least PHP 8, but no other extensions or packages.
How to use the Object Transformer?
First, at least one implementation of the TransformerInterface
has to be created.
use ITB\ObjectTransformer\TransformerInterface; class OptimusPrime implements TransformerInterface { public static function supportedTransformations(): array { return [['input' => MissionCity::class, 'output' => Ruins::class]]; } public function transform(TransformationEnvelope $envelope, string $outputClassName): object { // This method performs the actual transformation and returns the resulting object. } } class Megatron implements TransformerInterface { public static function supportedTransformations(): array { return [['input' => SamWitwicky::class, 'output' => Corpse::class]]; } public function transform(TransformationEnvelope $envelope, string $outputClassName): object {...} }
The input- and output class names are used to register the supported transformations in the TransformationMediator
.
The TransformationMediator
requires an iterable of TransfromerInterface
-implementing objects (at least one).
use ITB\ObjectTransformer\TransformationMediator; $mediator = new TransformationMediator(new \ArrayObject([new OptimusPrime(), new Megatron()]));
Because the TransformerInterface
objects are processed at the first call of the transform
method of the TransformationMediator
,
the TranformationMediator
(or the TransformationMediatorInterface
) can be passed as constructor argument to the transformer
(otherwise, this could lead to endless circle calls).
After everything is prepared, the transform
method can be used.
$object1 = new Object1('The hell am I doing here?'); $object2 = $mediator->transform($object1, Object2::class); // Explicit envelope usage $object2 = $mediator->transform(TransformationEnvelope::wrap($object1), Object2::class);
The transform
method of the TransformationMediator
can handle any object. If the passed object isn't a TransformationEnvelope
,
it will be wrapped with one. So if you don't want to use any stamps (see below), just pass the ordinary object.
But be aware, that you will always receive a TransformationEnvelope
in your implementations of TransformerInterface?
.
What do you mean with "stamp"?
The envelope and stamp system is inspired, but not identical to the system used by the Symfony messenger component.
Every object that is passed to the transform
method of the TransformationMediator
is wrapped with an TransformationEnvelope
(if it's not already one).
Like a real envelope, the TransformationEnvelope
can carry stamps. Stamps have two tasks:
- pass data to the
transform
method of theTransformerInterface
implementation - provide data that can be used during processing in the
TransformationMediator
All stamps have to implement the TransformationStampInterface
and provide a priority.
The main difference to the Symfony messenger component is, that the envelope can only hold one stamp per type. If two or more stamps of the same type are passed to the envelope, a later stamp will overwrite an earlier one, if it's priority is higher.
Looping data through the mediator
To loop data through the mediator to the transformer, any custom stamp can be passed to the envelope. They won't be touched during processing and are accessible via the envelope:
public function transform(TransformationEnvelope $envelope, string $outputClassName): object { $customStamp = $envelope->getStamp(CustomStampClass::class); // returns null if the envelope contains no such stamp }
Data processed by the mediator
All implementations of the TransformationStampInterface
provided by this package are used inside the mediator.
After there usage they are removed from the envelope and are not accessible in the TransformerInface
implementation.
InputClassStamp
Because of its internal data flow, the passed input object has to be of the exact same class
that was defined as input in the supportedTransformations
method of the factory.
This could lead to problems with packages like Doctrine. Doctrine creates proxy classes for managed entities,
that can be used just like the Entity itself. However, the TransformationMediator
would not find a matching transformer
and throw an exception, because the exact class of the proxy object is not registered for transformation.
That's where the InputClassStamp
comes into play. Let's define some objects first.
class Object1 { public $someString; public function __construct($someString) { $this->someString = $someString; } } class Object2 { public $letterCount; public function __construct($letterCount) { $this->letterCount = $letterCount; } } class Object3 extends Object1 { }
The following lines would lead to an UnsupportedInputOutputTypes
exception.
$object3 = new Object3('The hell am I doing here?'); $result = $mediator->tranform($object3, Object2::class);
With the InputClassStamp
it's working.
use ITB\ObjectTransformer\TransformationMediator; $envelope = new \ITB\ObjectTransformer\TransformationEnvelope( new Object3('The hell am I doing here?'), [new InputClassStamp(Object1::class)] ); $result = $mediator->tranform($envelope, Object2::class);
Why does this package exist?
A common pattern I stumbled across in my projects is to map data between different objects types like DTOs and Entities.
The mapping is often very simple: the value of the DTO (Data Transfer Object) property is the same as the property of the Entity (and vice versa). However, things get more complicated if value objects are used (which I strongly recommend). New objects has to be created and the few lines of code get more and more complex.
In my projects, this often lead to the creation of factory classes, that handle the object transformation and contain as little business logic as possible. But, as a class should only serve a single purpose, there can be a lot of such factories. When engineering more complex entities, the factories sometimes depend on each other. That makes the DI (Dependency Injection) or Singleton usage quite a mess.
That's where the object transformer comes into play: the mediator is sufficient to do all the transformations needed.
How does the Object Transformer work?
A factory or any other class that should be used by the TransformationMediator
has to implement the TransformerInterface
.
All classes that implement the interface (and are passed to the mediator) provide an array of supported transformations
via the static supportedTransformations
method. The array contains an array for every supported transformation.
Every one of these inner arrays requires an input
and an ouput
key, which both represent existing classes.
When the transform
method of the TransformationMediator
is first called, it will populate it's internal transformer registry.
For performance reasons every supported transformation is registered with it's input and output class.
This way, the associations can be used to find a responsible transformer.
Contributing
I am really happy that the software developer community loves Open Source, like I do! ♥
That's why I appreciate every issue that is opened (preferably constructive) and every pull request that provides other or even better code to this package.
You are all breathtaking!