Rezonant's MapperBundle
, (*1)
NOTE: This library is NOT ready for general use! This is sooo pre-alpha!, (*2)
This Symfony2 bundle provides a flexible mechanism for object-to-object mapping (and back).
Note that this bundle does not cover constructing a Model object from request data nor does it handle creating response text
from a model, as both of these should be the responsibility of your [de]serializer, we recommend JMS Serializer for that., (*3)
Maps can be provided automatically using annotations, manually via configuration, or via custom map providers.
The caller can also explicitly specify a map object to be used. The bundle has a built-in caching mechanism which can use
one of a number of built in (or custom) caching strategies. Built in cache strategies include an in-memory store, a
strategy based on a Doctrine-Commons Cache object, and a mostly useless implementation within a Symfony session bag., (*4)
Also, while we don't explicitly test for this use case you can probably use the library to do free declarative
mapping between anonymous (stdclass) objects. There is rather substantial support for dealing with associative arrays when
they are part of object-to-object maps so mapping to/from them should work, but your mileage may vary if our tests
aren't covering your usage., (*5)
Introduction: Use this within a RESTful service
While fairly generic in it's implementation, this bundle is primarily designed to be used within RESTful services written
using Symfony2. Standard Symfony2+FOSRest+JMS projects will usually procure model objects from requests automatically via
the FOSRest body listener combined with the JMS serializer. One might then use the Symfony validator to assert that the user's
data is valid before continuing., (*6)
On the other end of things, FOSRestControllers which use the response listener
feature will typically return model objects (instead of Symfony HTTP Kernel Response objects), which will be automatically
serialized using JMS. One might then use the "Accepts" or extension-based formatting hints that FOSRest comes built in with
to select an output format based on what the API consumer wants., (*7)
The model object which you (de)serialize to/from should be of a class coupled to your RESTful API, not your entity layer.
One reason for this is that you may have to support multiple versions of your API. How then should you handle converting
from your RESTful model into an underlying persistence object?, (*8)
RezonantMapperBundle provides a simple but powerful way to accomplish object-to-object mapping. The built-in annotation-based
mapper allows you to declaratively specify the destinations for fields on the source object. Note that destination-driven
annotations are not available, but they could be easily added in by writing a new variant of AnnotationMapProvider., (*9)
The decision to focus on source-driven annotations is influenced by the primary use case of this library: to map
model (service-layer) data onto entity (persistence-layer) data and back again. Since there could be many model classes which
map to the same entity within a single version of an API, and perhaps many versions of an API, it makes sense to
stick to source-driven annotations because it enforces the same dependency graph that your services already follow:
Your API layer depends on your persistence layer, but your persistence layer does not depend on your API layer.
That being said, you are free to graft support onto the library if you require it., (*10)
Installation
NOTE: This is still a pre-alpha bundle. Since there are no released versions, you must depend on dev-master (the bleeding
edge). We make absolutely no guarantees about API stability, but we hope this will change soon. Once versions are tagged,
strict semantic versioning will be followed., (*11)
$ composer require rezonant/mapper-bundle=dev-master
You must also depend on the bundle within app/AppKernel.php:, (*12)
$bundles = array(
...,
new Rezonant\MapperBundle\RezonantMapperBundle()
)
If you fail to do so, you will be able to use the bundle's classes, but none of the Symfony DI services will be
available, and your configurations will be ignored., (*13)
Using the Mapper service
The simplest way to call Mapper is by allowing a map provider to do the heavy lifting:, (*14)
$dest = $mapper->map($sourceInstance, "ABC\MyDestinationClass");
Above, the map is found by consulting all registered map providers for a valid mapping between the source and
destination classes. If one is found, it is used. If one isn't found, an exception is thrown
(Rezonant\MapperBundle\Exceptions\UnableToMapException)., (*15)
You can also explicitly provide the map using the third parameter of map(). You can use this along with the
MapBuilder API to produce arbitrary mappings., (*16)
$map = new MapBuilder()
->field(new Reference("fromField"), new Reference("toField"))
->build();
$dest = $mapper->map($sourceInstance, "ABC\MyDestinationClass", $map);
Annotation-based Mapping
The bundle supports source-driven annotations for specifying field-to-field mappings:, (*17)
use Rezonant\MapperBundle\Annotations as Mapper;
class MyModel {
/**
* @Mapper\MapTo("toField")
*/
public $fromField;
}
For a deep model structure, you must be sure to specify types using the @Mapper\Type() annotation.
If you are also using JMS serializer, you can use the @JMS\Type() annotation instead and MapperBundle
will respect that. The map generated between two classes will automatically handle all subclasses., (*18)
In the above example, the field $fromField will map onto the destination $toField., (*19)
Configuration-based Mapping
In your config.yml (or whereever using Symfony config resources):, (*20)
rezonant_mapper:
maps:
- source: ABC\MySourceClass
destination: ABC\MyDestinationClass
fields:
- from: fromField
to: toField
You can also specify submaps below each field, for when the source value is an object, and the
destination value is a different kind of object:, (*21)
rezonant_mapper:
maps:
- source: ABC\MySourceClass
destination: ABC\MyDestinationClass
fields:
- from: fromField
to: toField
map:
fields:
- from: deeperFromField
to: deeperToField
You can also use deep references:, (*22)
rezonant_mapper:
maps:
- source: ABC\MySourceClass
destination: ABC\MyDestinationClass
fields:
- from: fromField
to: someField.someOtherField.toField
map:
fields:
- from: anotherField.deeperFromField
to: deeperToField
Note that the types involved in each field reference are auto-detected using PHP reflection if possible.
You can also specify these explicitly:, (*23)
rezonant_mapper:
maps:
- source: ABC\MySourceClass
destination: ABC\MyDestinationClass
fields:
- from:
name: fromField
type: \TypeOfThisField
to:
name: someField.someOtherField.toField
types: [\TypeOfSomeField, \TypeOfSomeOtherField, \TypeOfToField]
This may be required if such information cannot be detected from reflection and/or annotations, or if you
do not want to incur that overhead when the map is first constructed (but don't forget about the caching system), (*24)
Usage in Symfony2
Once installed you can depend on the @rezonant.mapper
service to obtain a configured instance of the Mapper service., (*25)
Configuration
The rezonant_mapper config section can be used for much more than just declaring maps., (*26)
Enabling/Disabling Map Providers
You can control which map providers are used. The following sample shows the default values:, (*27)
rezonant_mapper:
providers:
annotations:
enabled: true
config:
enabled: true
custom: []
The "custom" field is an array of classes implementing MapProviderInterface which should be used as map providers., (*28)
Caching
The included cache layer can be controlled by manipulating the "caching" section of the configuration. The
following shows the default values:, (*29)
rezonant_mapper:
caching:
enabled: false
strategy: 'Rezonant\MapperBundle\Cache\Strategies\MemoryCacheStrategy'
The "enabled" flag will enable/disable the caching engine. The "strategy" field lets you specify a strategy class
to be used. You can implement your own strategies by implementing Rezonant\MapperBundle\Cache\CacheStrategyInterface
or use one of the built-in ones found within the Rezonant\MapperBundle\Cache\Strategies namespace. Of particular note
is the DoctrineCacheStrategy, which will allow you to wrap any Doctrine-Commons Cache object. Doctrine-Commons comes with
a number of cache providers including APC and Memcache., (*30)
Usage outside of Symfony2
Coming soon! This package is a Symfony2 dependency injection bundle for Symfony2 app kernels, and as such is built to
be used with Symfony's dependency injector. Its functionality is not yet available in a non-Symfony manner, but
that will likely change in the future. In the mean time, even if you are not using Symfony DI, you can still pull
this project using Composer and construct a Mapper service manually using the Rezonant\MapperBundle\Mapper class.
Should you construct your own instance, you should provide a MapProvider instance (we recommend AnnotationMapper,
you will need to depend on Doctrine Commons and provide an AnnotationReader). If you intend to always pass
explicit maps you can pass NULL for the map provider., (*31)
use Rezonant\MapperBundle\Mapper;
use Rezonant\MapperBundle\Providers\AnnotationMapProvider;
use Doctrine\Common\Annotations\AnnotationReader;
$mapper = new Mapper(new AnnotationMapProvider(new AnnotationReader()));
You could also use Rezonant\MapperBundle\Providers\ConfigMapProvider to inject a number of premade maps into the Mapper
service. Simply construct your maps (manually or using Rezonant\MapperBundle\MapBuilder) and pass them as an array into
a new ConfigMapProvider, then use that for your Mapper instance., (*32)
Caching
Caching can be used without using the Symfony configuration layer by constructing a CacheProvider and passing your
MapProvider of choice to it along with your chosen CacheStrategy implementation. You can moderate between
several MapProviders by using the Rezonant\MapperBundle\Providers\MapProviderModerator class. This is how the
Symfony bundle internally constructs the MapProvider for the Mapper service based on the rezonant_mapper configuration
section., (*33)
Testing
To run the tests you (currently) must construct a Symfony project and symlink the sources into it:, (*34)
$ cd mapper-bundle
$ symfony new symfony2
$ ln -s ../../src/Rezonant symfony2/src/Rezonant
$ phpunit