addiks/symfony_rdm

Helps with the use of domain driven design & rich domain model in symfony/doctrine applications.

v3.2.10 2021-04-12 11:25 UTC

README

Build Status Build Status Scrutinizer Code Quality Code Coverage

What

The goal of this project is to enrich the doctrine2-ORM mapping capabilities so that entities do not have to be developed for doctrine anymore, but make it possible to map any object from anywhere to a database using doctrine, even if it was not developed for that purpose. It add's a lot of new ways on how data can be mapped from the database to an object (and back).

The currently implemented features are the following:

Each of these can be combined with any other, allowing for extremely dynamic ORM mapping capabilities.

How

It hooks into the events of doctrine and hydrates the marked fields with the described values. There are multiple ways of defining which mappings should be in what fields of the services: Per annotations, YAML, XML, PHP or Static PHP. YAML-Mapping is not fully implemented yet and may be removed soon.

I would suggest you to use the XML (or YAML) mapping because entities should be framework-agnostic. I personally prefer XML over YAML because with XML you at least have a schemata while with yaml you often have to guess what keys are allowed, what all the keys mean and who actually uses them. For more detauls see the linked documentations above.

Setup

To enable this functionality first install the project via composer (symfony normally comes with composer) using the following command: composer require addiks/symfony_rdm

Then register the bundle in your symfony-application. Prior to symfony-4.0 this is done in the file "app/AppKernel.php" inside the method "registerBundles". From 4.0 onwards this is done in the file "config/bundles.php". (If you know how to automate this, please let me know.)

Symfony 2.x & 3.x:

// app/AppKernel.php
public function registerBundles()
{
    $bundles = array(
        new Symfony\Bundle\FrameworkBundle\FrameworkBundle(),
        # ...
        new Addiks\RDMBundle\AddiksRDMBundle(), # <== Add this line
        new AppBundle\AppBundle(),
    );

    …

    return $bundles;
}

Symfony >= 4.0:

// config/bundles.php
return [
    // 'all' means that the bundle is enabled for any Symfony environment
    Symfony\Bundle\FrameworkBundle\FrameworkBundle::class => ['all' => true],
    …
    Addiks\RDMBundle\AddiksRDMBundle::class => ['all' => true], # <== Add this line

];

After that this bundle should work. If not create an issue here and provide as much details about the environment this is being used in, i may be able to help.

Two ways of retrieving data: safe or fast

Using this extension to extend the ORM mapping of doctrine entities can (depending on the ORM mapping configuration) introduce new database columns that doctrine would normally not know about. When loading or storing the data for these additional columns, there are two ways to interact with them:

The safe but slow method

The safe but slow way to deal with these additional database columns is to load them one by one outside of doctrine, every time an entity is hydrated and store (UPDATE / INSERT) all modified entities in the database when the doctrine entity-manager is flushed. This is safer as the faster approach because everything happens outside of doctrine's scope and changes to doctrine or other doctrine- plugins cannot interfere with the functionality of the loading and storing process. At the same time this approach is slower - often even MUCH slower - then the faster approach because now we may need to execute one extra select statement for every hydrated entity. This can have a serious performance impact! Nevertheless, this is the default approach because while being slower, it is also the safer approach.

The fast but instable way

The alternative of loading and storing all data from and to the database outside of doctrine via extra SELECT statements is to let doctrine load and store these data for us in doctrine's own mechanisms. This way the performance is (nearly) the same as if these columns were native doctrine columns, no extra SELECT, UPDATE or INSERT statements need to be executed. The problematic part of this solution is that for this to work, we need to make doctrine aware of all these additional database columns so that doctrine can handle them for us without having doctrine actually using these data during it's own hydration of the entities. These are database-columns but not entity fields. If doctrine would try to map these columns to the entity fields on it's own, it would fail because they have no corresponding entity-fields by doctrines own logic. To prevent this from happening, this approach hooks deep into doctrine's own reflection mechanisms and fakes these entitiy-fields for doctrine. From doctrines point of view, these fields on the entities actually exist even if they are fake. This construct of hooking deep within doctrine makes a lot of assumptions about the internals of doctrine. If any of these assumptions fail (because either doctrine changes them in a new version or another extension changes them) then this extension could fail to work properly! Because of this instability / uncertainty, this method is not default but opt-in. To use this method you must define a symfony service parameter called addiks_rdm.data_loader.stability and set it to fast-and-unstable.

app/config/config.yml:

parameters:
    addiks_rdm.data_loader.stability: 'fast-and-unstable'

Service-FormType

This bundle also provides an additional new form-type called "ServiceFormType" which should prove valuable in conjunction with the service-hydration-abilities of this bundle. It allows to specify a list of service-id's as choices that can be selected between in a form and the selected being set on the entity.

<?php

use Addiks\RDMBundle\Symfony\FormType\ServiceFormType;

class MyEntityFormType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder->add("someField", ServiceFormType::class, [
            'required' => false,
            'choices' => [
                'app.example_services.foo' => 'foo',
                'app.example_services.bar' => 'bar',
            ]
        ]);
    }
    …
}

The future

This project may be extended with more features in the future, here are some ideas i have: